diff --git a/.github/stale.yml b/.github/stale.yml index e493a2dd3..dea6213de 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,11 +1,11 @@ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 60 +daysUntilStale: 365 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 7 +daysUntilClose: 365 # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) onlyLabels: [] diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c416a5a0..5c0b536e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,10 @@ cmake_minimum_required(VERSION 3.17.0) project(arrus LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) # Version -set(PROJECT_VERSION 0.4.6) +set(PROJECT_VERSION 0.5.9) + option(ARRUS_DEVELOP_VERSION "Build develop version." ON) if(ARRUS_DEVELOP_VERSION) set(PROJECT_FULL_VERSION "${PROJECT_VERSION}-dev") @@ -11,20 +13,28 @@ else() endif() string(TIMESTAMP CURRENT_YEAR "%Y") -set(CMAKE_CXX_STANDARD 17) ################################################################################ # Modules ################################################################################ -set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) +set(CMAKE_MODULE_PATH + "${PROJECT_SOURCE_DIR}/cmake" + ${CMAKE_BINARY_DIR} # Includes cmake scripts generated by conan package + ${CMAKE_MODULE_PATH} +) ################################################################################ # Global options and settings ################################################################################ +include(common) + +set(ARRUS_ROOT_DIR ${CMAKE_SOURCE_DIR}) set(ARRUS_DOCS_INSTALL_DIR docs) set(ARRUS_MATLAB_INSTALL_DIR matlab) set(ARRUS_PYTHON_INSTALL_DIR python) +set(Us4_LIB_DIR ${Us4_ROOT_DIR}/lib64) + option(ARRUS_BUILD_PY "Build python API." OFF) option(ARRUS_BUILD_MATLAB "Build MATLAB API." OFF) option(ARRUS_BUILD_DOCS "Build documentation." OFF) @@ -39,20 +49,64 @@ if(NOT ARRUS_BUILD_SWIG) message(WARNING "BUILDING PACKAGE WITHOUT LOW-LEVEL API WRAPPERS!") endif() +# Determining host platform. +if(MSVC) + set(ARRUS_BUILD_PLATFORM windows) +elseif(UNIX AND NOT APPLE) + set(ARRUS_BUILD_PLATFORM linux) +else() + message(FATAL_ERROR "Unsupported platform.") +endif() + +# Common C++ compile options +if("${ARRUS_BUILD_PLATFORM}" STREQUAL "windows") + # permissive- is required by range-v3/0.5.0 + set(ARRUS_CPP_COMMON_COMPILE_OPTIONS /permissive- /EHsc) + set(ARRUS_CPP_STRICT_COMPILE_OPTIONS "/W4 /WX") + + set(ARRUS_CPP_COMMON_COMPILE_DEFINITIONS + _WIN32 + # warnings occurs in boost bimap implementation headers + _SILENCE_CXX17_ALLOCATOR_VOID_DEPRECATION_WARNING + _CRT_SECURE_NO_WARNINGS) +else("${ARRUS_BUILD_PLATFORM}" STREQUAL "linux") + set(ARRUS_CPP_COMMON_COMPILE_OPTIONS "") + set(ARRUS_CPP_STRICT_COMPILE_OPTIONS "-Wall -Wextra -pedantic -Werror") + set(ARRUS_CPP_COMMON_COMPILE_DEFINITIONS + ARRUS_LINUX + _SILENCE_CXX17_ALLOCATOR_VOID_DEPRECATION_WARNING) +endif() + +# installation directories +set(ARRUS_BIN_INSTALL_DIR bin) +set(ARRUS_LIB_INSTALL_DIR lib64) +set(ARRUS_INCLUDE_INSTALL_DIR include) +set(ARRUS_DOCS_INSTALL_DIR docs) + ################################################################################ # Common dependencies ################################################################################ if(ARRUS_BUILD_SWIG) - find_package(Us4 0.4.6 EXACT REQUIRED US4OEM HV256 DBARLite) + find_package(Us4 0.5.3 EXACT REQUIRED US4OEM HV256 DBARLite) endif() - +find_package(Boost REQUIRED) +set(Boost_USE_STATIC_LIBS ON) +find_package(protobuf REQUIRED) +# Use project root to search for .proto files. +set(Protobuf_IMPORT_DIRS ${CMAKE_CURRENT_SOURCE_DIR}) +find_package(fmt REQUIRED) +find_package(Microsoft.GSL REQUIRED) +find_package(Eigen3 REQUIRED) ################################################################################ # Sub-projects ################################################################################ -add_subdirectory(core) if(ARRUS_RUN_TESTS) + include(tests) enable_testing() endif() + +add_subdirectory(arrus/core) + if(ARRUS_BUILD_PY) add_subdirectory(api/python) endif() @@ -76,26 +130,30 @@ install( # Embed external dependencies ################################################################################ if(ARRUS_EMBED_DEPS) - message("ROOT DIR: ${Us4_ROOT_DIR}") + # TODO Remove transitive dependency on boost (embed deps directly). + message("Us4r API ROOT DIR: ${Us4_ROOT_DIR}") + # Sanitize provided path. + string(REPLACE "\\" "/" Us4_ROOT_DIR_SANITIZED ${Us4_ROOT_DIR}) + install( DIRECTORY - ${Us4_ROOT_DIR}/lib64/ + ${Us4_ROOT_DIR_SANITIZED}/lib64/ DESTINATION - lib64 + ${ARRUS_LIB_INSTALL_DIR} ) install( FILES # TODO(pjarosik) make it more general (will not work for OS!=win) - ${Us4_ROOT_DIR}/matlab/Us4MEX.mexw64 + ${Us4_ROOT_DIR_SANITIZED}/matlab/Us4MEX.mexw64 DESTINATION matlab/arrus ) install( FILES # TODO(pjarosik) make it more general (will not work for OS!=win) - ${Us4_ROOT_DIR}/bin/us4OEMFirmwareUpdate.exe + ${Us4_ROOT_DIR_SANITIZED}/bin/us4OEMFirmwareUpdate.exe DESTINATION - bin + ${ARRUS_BIN_INSTALL_DIR} ) endif() diff --git a/api/matlab/+arrus/+devices/+probe/Probe.m b/api/matlab/+arrus/+devices/+probe/Probe.m new file mode 100644 index 000000000..b7ed5db5a --- /dev/null +++ b/api/matlab/+arrus/+devices/+probe/Probe.m @@ -0,0 +1,2 @@ +classdef Probe < arrus.MexObject +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+probe/ProbeModel.m b/api/matlab/+arrus/+devices/+probe/ProbeModel.m new file mode 100644 index 000000000..7850465ac --- /dev/null +++ b/api/matlab/+arrus/+devices/+probe/ProbeModel.m @@ -0,0 +1,29 @@ +classdef ProbeModel + % Probe model. + % + % :param modelId: id of the model + % :param nElements: (scalar (for 2-D probe) or a pair (for 3-D probe))\ + % probe's number of elements + % :param pitch: (scalar (for 2-D probe) or a pair (for 3-D probe))\ + % probe's element pitch + % :param txFrequencyRange: (a pair - two-element vector) + % a range [min, max] of the available tx center frequencies + + properties(GetAccess = public, SetAccess = private) + modelId arrus.devices.probe.ProbeModelId + nElements + pitch + txFrequencyRange (1, 2) + end + + methods + function obj = ProbeModel(modelId, nElements, pitch, ... + txFrequencyRange) + obj.modelId = modelId; + obj.nElements = nElements; + obj.pitch = pitch; + obj.txFrequencyRange = txFrequencyRange; + end + end + +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+probe/ProbeModelId.m b/api/matlab/+arrus/+devices/+probe/ProbeModelId.m new file mode 100644 index 000000000..aa57f84cf --- /dev/null +++ b/api/matlab/+arrus/+devices/+probe/ProbeModelId.m @@ -0,0 +1,19 @@ +classdef ProbeModelId + % Probe model id. + % + % :param manufacturer: name of the manufacturer + % :param name: name of the model + + properties(GetAccess = public, SetAccess = private) + manufacturer + name + end + + methods + function obj = ProbeModelId(manufacturer, name) + obj.manufacturer = convertCharsToStrings(manufacturer); + obj.name = convertCharsToStrings(name); + end + end +end + diff --git a/api/matlab/+arrus/+devices/+probe/ProbeSettings.m b/api/matlab/+arrus/+devices/+probe/ProbeSettings.m new file mode 100644 index 000000000..0d476101c --- /dev/null +++ b/api/matlab/+arrus/+devices/+probe/ProbeSettings.m @@ -0,0 +1,22 @@ +classdef ProbeSettings + % Probe adapter settings. + % + % :param probeModel: probe's model description + % :param channelMapping: (vector 1 x nChannels) channel mapping to \ + % apply; if the `i`-th value is equal to `j`, it means that the \ + % probe's channel `i` is connected to connector's channel `j`. + + properties(GetAccess = public, SetAccess = private) + probeModel arrus.devices.probe.ProbeModel + channelMapping + end + + methods(Access = public) + + function obj = ProbeSettings(probeModel, channelMapping) + obj.probeModel = probeModel; + obj.channelMapping = channelMapping; + end + + end +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+us4r/ProbeAdapter.m b/api/matlab/+arrus/+devices/+us4r/ProbeAdapter.m new file mode 100644 index 000000000..6e75b26cd --- /dev/null +++ b/api/matlab/+arrus/+devices/+us4r/ProbeAdapter.m @@ -0,0 +1,2 @@ +classdef ProbeAdapter < arrus.MexObject +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+us4r/ProbeAdapterModelId.m b/api/matlab/+arrus/+devices/+us4r/ProbeAdapterModelId.m new file mode 100644 index 000000000..596f34f13 --- /dev/null +++ b/api/matlab/+arrus/+devices/+us4r/ProbeAdapterModelId.m @@ -0,0 +1,19 @@ +classdef ProbeAdapterModelId + % Probe adapter model id. + % + % :param name: name of the model + % :param manufacturer: name of the manufacturer + + properties(GetAccess = public, SetAccess = private) + manufacturer + name + end + + methods + function obj = ProbeAdapterModelId(manufacturer, name) + obj.manufacturer = convertCharsToStrings(manufacturer); + obj.name = convertCharsToStrings(name); + end + end +end + diff --git a/api/matlab/+arrus/+devices/+us4r/ProbeAdapterSettings.m b/api/matlab/+arrus/+devices/+us4r/ProbeAdapterSettings.m new file mode 100644 index 000000000..318948126 --- /dev/null +++ b/api/matlab/+arrus/+devices/+us4r/ProbeAdapterSettings.m @@ -0,0 +1,28 @@ +classdef ProbeAdapterSettings + % Probe adapter settings. + % + % :param modelId: id of the model + % :param nChannels: number of adapter output channels + % :param channelMapping: (matrix 2 x nChannels) channel mapping to \ + % apply; for each i-th column the first row should be equal to the \ + % Us4OEM's ordinal number (`o`), second row should be equal to \ + % Us4OEM's channel; such a column means that adapter's channel \ + % `i` is connected to o-th Us4OEM, channel number `ch`. + + properties(GetAccess = public, SetAccess = private) + modelId arrus.devices.us4r.ProbeAdapterModelId + nChannels (1, 1) + channelMapping + end + + methods(Access = public) + function obj = ProbeAdapterSettings(modelId, nChannels, ... + channelMapping) + + obj.modelId = modelId; + obj.nChannels = nChannels; + obj.channelMapping = channelMapping; + end + end + +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+us4r/RxSettings.m b/api/matlab/+arrus/+devices/+us4r/RxSettings.m new file mode 100644 index 000000000..270e77e2d --- /dev/null +++ b/api/matlab/+arrus/+devices/+us4r/RxSettings.m @@ -0,0 +1,48 @@ +classdef RxSettings + % Us4R data acquisition settings. + % + % :param dtgcAttenuation: (scalar, optional), DTGC attenuation value, \ + % when set to empty array, DTGC will be off [dB] + % :param pgaGain: (scalar) Programable Gain Amplifier gain value [dB] + % :param lnaGain: (scalar) Low-noise Amplifier gain value [dB] + % :param tgcSamples: (vector, optional) TGC curve to apply, when set \ + % to empty array, TGC will be off [dB] + % :param lpfCutoff: Low-pass filter cutoff value [Hz] + % :param activeTermination: active termination value + + properties(GetAccess = public, SetAccess = private) + dtgcAttenuation + pgaGain (1, 1) + lnaGain (1, 1) + tgcSamples + lpfCutoff + activeTermination + end + + methods(Access = public) + + function obj = RxSettings(varargin) + % Rx settings constructor. + % + % Values can be provided in the order of the class properties + % or by providing a list of 'param1Name', 'param1Value', + % 'param2Name', 'param2Value', ... + mc = metaclass(obj); + nParams = size(mc.PropertyList); + nParams = nParams(1); + if nargin == nParams + for i = 1:nParams + obj.(mc.PropertyList(i).Name) = varargin{i}; + end + elseif nargin == 2*nParams + for i = 1:2:nargin + obj.(varargin{i}) = varargin{i+1}; + end + else + error("ARRUS:IllegalArgument", "Invalid number of arguments."); + end + end + + end + +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+us4r/Us4OEM.m b/api/matlab/+arrus/+devices/+us4r/Us4OEM.m new file mode 100644 index 000000000..390e76d18 --- /dev/null +++ b/api/matlab/+arrus/+devices/+us4r/Us4OEM.m @@ -0,0 +1,3 @@ +classdef Us4OEM < arrus.MexObject + +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+us4r/Us4OEMSettings.m b/api/matlab/+arrus/+devices/+us4r/Us4OEMSettings.m new file mode 100644 index 000000000..edb7577a2 --- /dev/null +++ b/api/matlab/+arrus/+devices/+us4r/Us4OEMSettings.m @@ -0,0 +1,27 @@ +classdef Us4OEMSettings + % Us4OEM module settings. + % + % :param channelMapping: (128 element vector) channel mapping + % (permutation) to apply for given Us4OEM + % :param activeChannelGroups: (16 element vector) a boolean vector, + % true at position `i` means that the i-th group should be active. + % :param rxSettings: Rx settings to apply to given Us4OEM + + properties(GetAccess = public, SetAccess = private) + channelMapping (1, 128) + activeChannelGroups (1, 16) + rxSettings arrus.devices.us4r.RxSettings + end + + methods(Access = public) + function obj = Us4OEMSettings(channelMapping, ... + activeChannelGroups, ... + rxSettings) + + obj.channelMapping = channelMapping; + obj.activeChannelGroups = activeChannelGroups; + obj.rxSettings = rxSettings; + end + end + +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+us4r/Us4R.m b/api/matlab/+arrus/+devices/+us4r/Us4R.m new file mode 100644 index 000000000..d0261e2f7 --- /dev/null +++ b/api/matlab/+arrus/+devices/+us4r/Us4R.m @@ -0,0 +1,3 @@ +classdef Us4R < arrus.MexObject + +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/+us4r/Us4RSettings.m b/api/matlab/+arrus/+devices/+us4r/Us4RSettings.m new file mode 100644 index 000000000..bd5e2ae6c --- /dev/null +++ b/api/matlab/+arrus/+devices/+us4r/Us4RSettings.m @@ -0,0 +1,41 @@ +classdef Us4RSettings + % Us4R system settings. + % + % Only one of the following options to configure system may be + % provided: us4OEMSettings or a tuple (probeSettings, + % probeAdapterSettings, rxSettings). + % + % :param us4OEMSettings: settings to apply to Us4OEMs that are \ + % available in a Us4R system + % :param probeAdapterSettings: probe adapter settings to apply + % :param probeSettings: probe settings to apply + % :param rxSettings: data acquisition settings to apply + + properties(GetAccess = public, SetAccess = private) + us4OEMSettings + probeAdapterSettings + probeSettings + rxSettings + end + + methods(Access = public) + function obj = Us4RSettings(varargin) + % Us4Settings constructor + % + % Constructor can take one parameter (us4OEMSettings) or + % three parameters: (probeAdapterSettings, probeSettings, + % rxSettings). + if nargin == 1 + obj.us4OEMSettings = varargin{1}; + elseif nargin == 3 + obj.probeAdapterSettings = varargin{1}; + obj.probeSettings = varargin{2}; + obj.rxSettings = varargin{3}; + else + error("ARRUS:IllegalArgument", ... + "Constructor should take 1 or 3 parameters."); + end + end + end + +end \ No newline at end of file diff --git a/api/matlab/+arrus/+devices/DeviceId.m b/api/matlab/+arrus/+devices/DeviceId.m new file mode 100644 index 000000000..ba9a047cf --- /dev/null +++ b/api/matlab/+arrus/+devices/DeviceId.m @@ -0,0 +1,20 @@ +classdef DeviceId + % System device identifier. + % + % :param deviceType: (string) device type, available values: 'Us4OEM',\ + % 'ProbeAdapter', 'Probe', 'Us4R', 'GPU', 'CPU' + % :param ordinal: (integer) ordinal number of the device in the system + + properties(GetAccess = public, SetAccess = private) + deviceType + ordinal + end + + methods(Access = public) + function obj = DeviceId(deviceType, ordinal) + obj.deviceType = deviceType; + obj.ordinal = ordinal; + end + end + +end \ No newline at end of file diff --git a/api/matlab/+arrus/+ops/+us4r/LINSequence.m b/api/matlab/+arrus/+ops/+us4r/LINSequence.m new file mode 100644 index 000000000..d30d1cdba --- /dev/null +++ b/api/matlab/+arrus/+ops/+us4r/LINSequence.m @@ -0,0 +1,95 @@ +% function LINSequence(aperture, focus, probe, c) +% % TODO inputs: tx aperture size, etc., probe (arrus.devices.probe.Probe), assumed speed of sound / Medium object +% % TODO output: a sequence of tx rx operations to apply on a given probe +% +% +% +% +% end + + +function txrxlist = LINSequence(nElem, nElemSub, fc,pitch, c, focalDepth, sampleRange,pri) + + import arrus.ops.us4r.* + + if mod(nElemSub,2) + focus = [-pitch/2, focalDepth]; + else + focus = [0,focalDepth]; + end + + nPeriods = 2; + pulse = Pulse(fc, nPeriods); + subApertureDelays = enumClassicDelays(nElemSub,pitch, c, focus); + [apMasks,apDelays] = simpleApertureScan(nElem, subApertureDelays); + + for iElem = 1:nElem + thisAp = apMasks(iElem,:); + thisDel = apDelays(iElem,:); + tx = Tx(thisAp,thisDel,pulse); + rx = Rx(thisAp,sampleRange); + txrx = TxRx(tx,rx,pri); + txrxlist(iElem) = txrx; + end + +end + + +function [apMasks,apDelays] = simpleApertureScan(nElem, subApertureDelays) +% Function generate array which describes which elements are turn on during +% 'classic' txrx scheme scan. The supaberture step is equal 1. +% +% :param nElem: number of elements in the array, i.e. full aperture length, +% :param subApertureDelays: vector of delays for subaperture, +% :param apMasks: output array of subsequent aperture masks, +% (nLines x nElements size), +% :param apDelays: output array of subsequent aperture delays +% (nLines x nElements size). + + subApLen = length(subApertureDelays); + bigHalf = ceil(subApLen/2); + smallHalf = floor(subApLen/2); + apMasks = false(nElem, nElem); + apDelays = zeros(nElem, nElem); + for iElement = 1:nElem + vAperture = false(1, smallHalf + nElem + bigHalf); + vAperture(1, iElement:iElement+subApLen-1) = true; + apMasks(iElement, :) = vAperture(bigHalf:end - smallHalf-1); + + vDelays = zeros(1, smallHalf + nElem + bigHalf); + vDelays(1, iElement:iElement+subApLen-1) = subApertureDelays; + apDelays(iElement, :) = vDelays(bigHalf:end - smallHalf-1); + + end +end + + +function delays = enumClassicDelays(nElem, pitch, c, focus) +% The function enumerates classical focusing delays for linear array. +% It assumes that the 0 is in the center of the aperture. +% +% :param nElem: number of elements in aperture, +% :param pitch: distance between two adjacent probe elements [m], +% :param c: speed of sound [m/s], +% :param focus: coordinates of the focal point ([xf,zf]), +% or focal length only (then xf = 0 is assumed) [m], +% :param delays: output delays vector. + + + if isscalar(focus) + xf = 0; + zf = focus; + elseif length(focus(:)) == 2 + xf = focus(1); + zf = focus(2); + else + error('Inproper focus value.') + end + + probeWidth = (nElem-1)*pitch; + elCoordX = linspace(-probeWidth/2, probeWidth/2, nElem); + element2FocusDistance = sqrt((elCoordX-xf).^2 + zf.^2); + distMax = max(element2FocusDistance); + delays = (distMax - element2FocusDistance)/c; + +end diff --git a/api/matlab/+arrus/+ops/+us4r/Pulse.m b/api/matlab/+arrus/+ops/+us4r/Pulse.m new file mode 100644 index 000000000..f4865df50 --- /dev/null +++ b/api/matlab/+arrus/+ops/+us4r/Pulse.m @@ -0,0 +1,32 @@ +classdef Pulse + % A description of a transmitted pulse. + % + % :param centerFrequency: pulse transmit center frequency in [Hz] + % :param nPeriods: number of sine burst periods, should be full and half cycles + % are supported (e.g. 1, 1.5, etc.) + % :param inverse: true if the polarity of the generated signal should be inverted, + % optional, default value: false + + properties + centerFrequency + nPeriods + inverse + end + + methods + function obj = Pulse(varargin) + p = inputParser; +% addRequired(p, 'centerFrequency', @(x) isscalar(x) && isnumeric(x) && isfinite(x) && (x > 0) && isreal(x)); +% addRequired(p, 'nPeriods', @(x) isscalar(x) && isinteger(x) && (x > 0)); + addRequired(p, 'centerFrequency',@(x) 1); + addRequired(p, 'nPeriods', @(x) isscalar(x) && mod(x,1)==0 && (x > 0)); + addOptional(p, 'inverse', false, @(x) isscalar(x) && islogical(x)); + parse(p,varargin{:}) + + obj.centerFrequency = p.Results.centerFrequency; + obj.nPeriods = p.Results.nPeriods; + obj.inverse = p.Results.inverse; + end + end + +end diff --git a/api/matlab/+arrus/+ops/+us4r/Rx.m b/api/matlab/+arrus/+ops/+us4r/Rx.m new file mode 100644 index 000000000..65de4f5bf --- /dev/null +++ b/api/matlab/+arrus/+ops/+us4r/Rx.m @@ -0,0 +1,40 @@ +classdef Rx + % A data reception op. + % + % Example usage: + % .. code:: matlab + % + % aperture = false(128); + % aperture(2) = 1; + % aperture(4) = 1; + % Rx('aperture', aperture, 'sampleRange', [0 4095], 'decimationFactor', 2); + % + % :param aperture: logical mask where 0 and 1 corresponds to active and inactive element respectively + % :param sampleRange: two-element vector determining sample range to acqure [first, last] (closed interval). + % NOTE: zero-based numbering applies here. + % :param decimationFactor: subsampling factor, starts from 1. One means no subsampling, 2 - skip every + % 2nd sample, 3 - skip every 3rd sample, etc. Optional, 1 is default. + properties + aperture + sampleRange + decimationFactor + end + + methods + function obj = Rx(varargin) + p = inputParser; + isAllInteger = @(x) isnumeric(x) && all(x == floor(x)); + isAllPositiveInteger = @(x) isAllInteger(x) && all(x > 0); + isAllNonnegativeInteger = @(x) isAllInteger(x) && all(x >= 0); + addRequired(p, 'aperture', @(x) isvector(x) && ~isempty(x) && islogical(x)); + addRequired(p, 'sampleRange', @(x) isvector(x) && length(x) == 2 && isAllNonnegativeInteger(x)); + addOptional(p, 'decimationFactor', 1, @(x) isscalar(x) && isAllPositiveInteger(x)); + + parse(p,varargin{:}); + obj.aperture = p.Results.aperture; + obj.sampleRange = p.Results.sampleRange; + obj.decimationFactor = p.Results.decimationFactor; + end + end + +end diff --git a/api/matlab/+arrus/+ops/+us4r/Tx.m b/api/matlab/+arrus/+ops/+us4r/Tx.m new file mode 100644 index 000000000..6fc037f07 --- /dev/null +++ b/api/matlab/+arrus/+ops/+us4r/Tx.m @@ -0,0 +1,38 @@ +classdef Tx + % A 'transmit' pulse operation. + % + % Example usage: + % + % .. code:: matlab + % + % aperture = false(128); + % aperture(2) = 1; + % aperture(4) = 1; + % pulse = .... + % Tx('aperture', aperture, 'delays', [5e-6, 6e-6], 'pulse', pulse); + % + % :param aperture: logical mask where 0 and 1 corresponds to active and inactive element respectively + % :param delays: vector of transmit delays in [s], should be the size of the number of active channels in Tx + % aperture; delays[i] will be applied to the i-th active tx channel + % :param pulse: a pulse to transmit, object of type `arrus.ops.us4r.Pulse` + + properties + aperture + delays + pulse + end + + methods + function obj = Tx(varargin) + p = inputParser; + addRequired(p, 'aperture', @(x) isvector(x) && islogical(x)); + addRequired(p, 'delays', @(x) isvector(x) && isreal(x)); + addRequired(p, 'pulse', @(x) isscalar(x) && isa(x, 'arrus.ops.us4r.Pulse')); + parse(p,varargin{:}); + obj.aperture = p.Results.aperture; + obj.delays = p.Results.delays; + obj.pulse = p.Results.pulse; + end + end + +end diff --git a/api/matlab/+arrus/+ops/+us4r/TxRx.m b/api/matlab/+arrus/+ops/+us4r/TxRx.m new file mode 100644 index 000000000..ef6fe09d1 --- /dev/null +++ b/api/matlab/+arrus/+ops/+us4r/TxRx.m @@ -0,0 +1,40 @@ +classdef TxRx + % A class encapsulating a single 'transmit-and-receive' op. + % + % Example usage: + % + % .. code:: matlab + % + % TxRx('tx', tx, 'rx', rx, 'pri', 100e-6) + % + % :param tx: pulse emission, object of type `arrus.ops.us4r.Tx` + % :param rx: echo data reception, object of type `arrus.ops.us4r.Rx` + % :param pri: pulse repetition interval [s] + + properties + tx + rx + pri + end + + methods + function obj = TxRx(varargin) + % TxRx constructor. + % To pass arguments to the constructor name-value convetion is used. + % + + + p = inputParser; + addRequired(p, 'tx', @(x) isa(x, 'arrus.ops.us4r.Tx')); + addRequired(p, 'rx', @(x) isa(x, 'arrus.ops.us4r.Rx')); + addRequired(p, 'pri', @(x) isreal(x) && isscalar(x) && x > 0); + parse(p, varargin{:}); + + obj.tx = p.Results.tx; + obj.rx = p.Results.rx; + obj.pri = p.Results.pri; + end + + + end +end diff --git a/api/matlab/+arrus/+ops/+us4r/TxRxSequence.m b/api/matlab/+arrus/+ops/+us4r/TxRxSequence.m new file mode 100644 index 000000000..cf549aeda --- /dev/null +++ b/api/matlab/+arrus/+ops/+us4r/TxRxSequence.m @@ -0,0 +1,25 @@ +classdef TxRxSequence + % A sequence of Tx/Rx operations. + % + % :param ops: a list of TxRx operations + % :param tgcCurve: an array of TGC samples to apply, leave empty if TGC should be turned off + + properties + ops + tgcCurve + end + + methods + function obj = TxRxSequence(varargin) + p = inputParser; + addRequired(p, 'ops', @(x) isvector(x) && ~isempty(x) && isa(x, "arrus.ops.us4r.TxRx")); + addRequired(p, 'tgcCurve', @(x) isvector(x)); + parse(p, varargin{:}); + + obj.ops = p.Results.ops; + obj.tgcCurve = p.Results.tgcCurve; + + end + + end +end diff --git a/api/matlab/+arrus/+ops/+us4r/getLinearTGC.m b/api/matlab/+arrus/+ops/+us4r/getLinearTGC.m new file mode 100644 index 000000000..b4745f48a --- /dev/null +++ b/api/matlab/+arrus/+ops/+us4r/getLinearTGC.m @@ -0,0 +1,13 @@ +function getLinearTGC(varargin) + % Computes linear function TGC samples. + % + % :param intercept: TGC starting gain [dB] + % :param slope: TGC gain slope [dB/m] + % :param tgcSamplingFrequency: TGC curve sampling frequency + % :param speedOfSound: assumed speed of sound + % :param nSamples: number of samples to generate + % :return: a vector of tgc samples + + % TODO implement + +end diff --git a/api/matlab/+arrus/+session/Session.m b/api/matlab/+arrus/+session/Session.m new file mode 100644 index 000000000..cbcc89cfa --- /dev/null +++ b/api/matlab/+arrus/+session/Session.m @@ -0,0 +1,11 @@ +classdef Session < arrus.MexObject + methods + function obj = Session(sessionSettings) + obj = obj@arrus.MexObject("Session", sessionSettings); + end + + function res = getDevice(obj, deviceId) + res = obj.callMethod("getDevice", deviceId); + end + end +end \ No newline at end of file diff --git a/api/matlab/+arrus/+session/SessionSettings.m b/api/matlab/+arrus/+session/SessionSettings.m new file mode 100644 index 000000000..6831064d9 --- /dev/null +++ b/api/matlab/+arrus/+session/SessionSettings.m @@ -0,0 +1,16 @@ +classdef SessionSettings + % Session configuration object. + % + % :param us4RSettings: us4Settings to apply durring session + + properties(GetAccess = public, SetAccess = private) + us4RSettings arrus.devices.us4r.Us4RSettings + end + + methods(Access = public) + function obj = SessionSettings(us4RSettings) + obj.us4RSettings = us4RSettings; + end + end + +end \ No newline at end of file diff --git a/api/matlab/+arrus/MexObject.m b/api/matlab/+arrus/MexObject.m new file mode 100644 index 000000000..bf037a83d --- /dev/null +++ b/api/matlab/+arrus/MexObject.m @@ -0,0 +1,37 @@ +classdef (Abstract = true) MexObject < handle + properties(GetAccess = private, SetAccess = immutable, Transient = true, Hidden = true) + className + handle + end + + methods + function obj = MexObject(className, varargin) + obj.className = className; + % Verify available mex file. + % TODO(pjarosik) check if the version of mex file is the same as for version of this toolbox + obj.handle = arrus.arrus_mex_object_wrapper(obj.className, ... + "create", varargin{:}); + end + + function delete(obj) + if ~isempty(obj.handle) + arrus.arrus_mex_object_wrapper(obj.className, "remove", ... + obj.handle); + end + end + end + + methods(Access = protected, Sealed = true) + + function res = callMethod(obj, methodName, varargin) + if isempty(obj.handle) + error("ARRUS:IllegalState", "Objects handle is not set."); + end + res = arrus.arrus_mex_object_wrapper(obj.className, methodName, ... + obj.handle, varargin{:}); + end + + end + +end + diff --git a/api/matlab/+arrus/TxRX_example.m b/api/matlab/+arrus/TxRX_example.m new file mode 100644 index 000000000..1c8369221 --- /dev/null +++ b/api/matlab/+arrus/TxRX_example.m @@ -0,0 +1,82 @@ +% Hej. Na prosbe PK pisze skrypt jak mniej wiecej wyobrazam sobie +% korzystanie z klas TxRx + komentarze. Skrypt ma w zamierzeniu charakter +% tymczasowy, bo byc moze jakies zmiany zajda, dlatego pozwalam sobie go pisac po polsku ;). +% +% powiedzmy, ze chcemy zakodowac 3-krotne nadanie i odbior fali plaskiej przez +% aperture 192 elementowa. +% +% najpierw obiekt opisujacy impuls nadawczy. Zamknalem impuls nadawczy w +% klasie, bo kiedys moga byc arbitralne impulsy i wtedy moze sie to +% przydac. Na razie jednak obiekt jest b.prosty +pulse = TxPulse('nPeriods',[2], 'frequency', [5e6]); +% zastanawialem sie, czy by nie zrobic jakiegos domyslnego impulsu np. +% takiego jak wyzej, ale nie wiem, czy ze wzgledow bezpieczenstwa +% nie lepiej przymusic uzytkownika do wygenerowania takiego obiektu - jak +% myslicie? W tej chwili domyslna wartoscia jest pusta tablica, ktora w +% zalozeniu (moim) miala oznaczac 'pusty strzal' czyli po prostu nic. + +% potem generujemy obiekt opisujacy nadanie +t1 = Tx('pulse', pulse, 'aperture', true(1,192)); + +% nastepnie obiekt opisujacy odbior +r1 = Rx('aperture', true(1,192)); + +% nastepnie z tych dwoch obiektow produkujemy obiekt TxRX opisujacy +% pojedyncze nadanie/odbior. +txrx1 = TxRx('Tx',t1,'Rx', r1); +% Niestety, po napisaniu Tx i Rx zorientowalem sie, ze +% tablice obiektow musza sie skladac z +% obiektow tego samego typu, wiec nie mozna bylo poprzestac tylko na +% obiektach Tx i Rx. Pierwotnie chcialem, zeby byla mozliwa sekwencja w +% stylu [tx1, tx2, tx3, rx1, tx4, tx5, tx6, rx2...], +% dlatego potrzebny obiekt TxRx laczacy oba obiekty Tx i Rx. + + + + +% teraz budowana jest sekwencja (obiekt TxRxSequence) - na wejsciu podaje sie tablice obiektow +% TxRx, w tym wypadku 3 razy to samo tzn. [txrx1, txrx1, txrx1] +sequence = TxRxSequence([txrx1, txrx1, txrx1]); +% tutaj slowo komentarza - mozna pomyslec o tym, zeby w ogole nie bylo +% obiektu sequence (tak na poczatku chcialem zrobic), ale +% 1. jest tam miejsce na informacje o pri (sequence.pri), a nie wiem gdzie +% by bylo logicznie ja umiescic (no, ewentualnie mozna by w samym kernelu, o +% ktorym pozniej), +% 2. jesli bedzie taka potrzeba mozna tam trzymac metody do generowania +% sekwencji, +% 3. jest tam jakas kontrola, czy w tej liscie sa na pewno obiekty TxRX +% 4. mozna ewentualnie przerobic kernel, zeby lykal zarowno obiekty sequence jak i +% tablice TxRx. + + +% teraz o klasie TxRxKernel - wykorzystujac nasza sekwencje i obiekt +% sys (czyli ten, ktory jest uzywany w klasie Us4R) +% mozna wewnatrz Us4R uzyc obiektu TxRxKernel do zaprogramowania sekwencji. +% Zalozenie jest takie, ze w obiekcie Us4R bedzie gdzies jakas +% instrukcja warunkowa, ze jesli sekwencja jest obiektem TxRxSequence, to +% programowanie systemu bedzie przez kernel, ktory bedzie wygenerowany +% instrukcja w stylu: +kernel = TxRxKernel('sequence',sequence, 'usSystem', sys); +% samo programowanie jest zawarte w metodzie kernel.programHW() napisanej +% na postawie metody z Us4R o tej samej nazwie. + + +% a teraz uwagi: +% 1. Zalozylem, ze konstruktory wiekszosci tych obiektow, jesli sie nie +% poda odpowiednich parametrow na wejsciu, to odpowiednie propertisy sa +% puste. Musze w kernelu (chyba) dodac jeszcze jakies funkcje, ktore to +% badaja i w przypadku kiedy np. obiekt Rx ma pusta aperture, to wtedy +% kernel zamienia to na maske logiczna z samymi zerami (itd.) +% +% 2. Zalozylem, ze na razie (albo zawsze) bedzie to sluzyc do nadania i +% takze nie dotykalem rzeczy zwiazanych z rekonstrukcja. Miedzy innymi +% dlatego w kernelu nie ma ponizszej komendy obecnej w Us4R: +% Us4MEX(iArius, "TGCSetSamples", obj.seq.tgcCurve, iFire); +% Nie wiem czy to nie spowoduje jakis problemow (?). +% Przy okazji pomyslalem, ze napisze wstepnie klase tgc, do opisu krzywej +% tgc, ale na razie ona jeszcze niczego nie robi, poza sprawdzeniem czy +% prawidlowe argumenty podane sa do konstruktora. +% +% 3. Jeszcze tego nie testowalem. + + diff --git a/api/matlab/+arrus/TxRxKernel.m b/api/matlab/+arrus/TxRxKernel.m new file mode 100644 index 000000000..6eae0714a --- /dev/null +++ b/api/matlab/+arrus/TxRxKernel.m @@ -0,0 +1,573 @@ +classdef TxRxKernel < handle + % Class for ultrasound system programming when tx-rx sequence + % is defined via object of TxRxSequence class. + % + % + % properties: + % sequence - TxRxsequence class object (or empty array) + % usSystem - sys structure, for now internal structure in Us4R. + % + % methods: + % TxRxKernel() - class constructor. + % To pass arguments to the constructor name-value convetion + % is used, + % + % programHW(obj) - program the US system, + % calcNFire(obj) - calculate the number of firings used by sequence. + + properties + sequence = TxRxSequence() + usSystem = [] + end + + properties % (Access = private) + + nFire = 0 % number of all firings + nSamp = 0 % vector with sample numbers for all firings + startSamp = 0 % vector with first sample numbers for all firings + nSubTxRx = 0 % vector with number of firings in each TxRx event. sum(nSubTxRx) == nFire + pri = 0 % vector with pri in each firing + + % Arrays describing relation between module channel + % and probe element in each TxRx, used in programHW + module2RxMaps = {} + module2TxMaps = {} + module2TxDelaysMaps = {} + + % constant properties: + % minPriAddendum is a time which is added to estimated pri in case + % when 'min' is set to pri property in TxRx object. + % For now is is (arbitrarily/empirically) selected to 30us. + minPriAddendum = 30e-6; + + end + + + methods + function obj = TxRxKernel(varargin) + if nargin ~= 0 + p = inputParser; + + % adding parameters to parser + addParameter(p, 'usSystem', []) + addParameter(p, 'sequence', TxRxSequence()) + parse(p, varargin{:}) + + obj.usSystem = p.Results.usSystem; + obj.sequence= p.Results.sequence; + end + + + end + + + function programHW(obj) + % The method programHW() is for hardware programming. + + % The method propertiesPreprocessing() below + % enumerates following properties: + % nFire - number of all firings + % nSubTxRx - vector with number of firings in each TxRx event. + % sum(nSubTxRx) == nFire + % module2RxMaps, module2TxMaps, module2TxDelaysMaps + % - Arrays describing relation between module channel + % and probe element in each TxRx + obj.propertiesPreprocessing() + + + nArius = obj.usSystem.nArius; % number of arius modules +% nRxChannels = obj.usSystem.nChArius; % max number of rx channels + % new firmware +% nRxChannels = obj.usSystem.nChArius*3; % max number of rx channels + samplingFrequency = 65e6; + + % time need for switch from transmit to receive or vice-versa + trSwitchTime = 240./samplingFrequency; + nTxChannels = 128; % max number of tx channels + nTxRx = length(obj.sequence.TxRxList); + + actChanGroupMask = obj.usSystem.selElem(8:8:end,:) <= obj.usSystem.nElem; + actChanGroupMask = actChanGroupMask & obj.usSystem.actChan(8:8:end,:); + actChanGroupMask = obj.maskFormat(actChanGroupMask); + + % Unloading Us4MEX should clear the device state. + munlock('Us4MEX'); + clear Us4MEX; + + % Program mappings, gains, and voltage + for iArius = 0:nArius-1 + +% % Set Rx channel mapping + for iFire = 0:obj.nFire-1 + Us4MEX(iArius, "SetRxChannelMapping", ... + obj.usSystem.rxChannelMap(iArius+1,1:32), ... + iFire ... + ); +% disp(obj.usSystem.rxChannelMap(iArius+1,1:32)) + end + + % Set Tx channel mapping + for iChannel = 1:nTxChannels + Us4MEX(iArius, "SetTxChannelMapping", ... + obj.usSystem.txChannelMap(iArius+1, iChannel), ... + iChannel ... + ); + end + + % init RX + Us4MEX(iArius, "SetPGAGain","30dB"); + Us4MEX(iArius, "SetLPFCutoff","15MHz"); + Us4MEX(iArius, "SetActiveTermination","EN", "200"); + Us4MEX(iArius, "SetLNAGain","24dB"); + Us4MEX(iArius, "SetDTGC","DIS", "0dB"); + Us4MEX(iArius, "TGCDisable"); + + try + Us4MEX(0,"EnableHV"); + + catch + warning('1st "EnableHV" failed'); + Us4MEX(0,"EnableHV"); + + end + + try + Us4MEX(0, "SetHVVoltage", obj.usSystem.voltage); + + catch + warning('1st "SetHVVoltage" failed'); + Us4MEX(0, "SetHVVoltage", obj.usSystem.voltage); + + end + end + + % Program Tx/Rx sequence + for iArius = 0:nArius-1 + Us4MEX(iArius, "SetNumberOfFirings", obj.nFire); + Us4MEX(iArius, "ClearScheduledReceive"); + Us4MEX(iArius, "SetNTriggers", obj.nFire); + end + + iFire = 0; + nSamp = NaN(1,obj.nFire); + startSamp = NaN(1,obj.nFire); + pri = NaN(1,obj.nFire); + for iTxRx = 1:nTxRx + rxTime = obj.sequence.TxRxList(iTxRx).Rx.delay ... + + obj.sequence.TxRxList(iTxRx).Rx.time ... + + trSwitchTime ... + ; + fs = samplingFrequency./obj.sequence.TxRxList(iTxRx).Rx.fsDivider; + moduleTxApertures = obj.module2TxMaps{iTxRx}; + moduleTxDelays = obj.module2TxDelaysMaps{iTxRx}; + moduleRxApertures = obj.module2RxMaps{iTxRx}; + + thisNSamp = floor(obj.sequence.TxRxList(iTxRx).Rx.time.*fs); + thisStartSamp = floor(obj.sequence.TxRxList(iTxRx).Rx.delay.*fs); + + for iSubTxRx = 1:obj.nSubTxRx(iTxRx) + +% nSamp(iFire+1) = floor(obj.sequence.TxRxList(iTxRx).Rx.time.*fs); +% startSamp(iFire+1) = floor(obj.sequence.TxRxList(iTxRx).Rx.delay.*fs); + nSamp(iFire+1) = thisNSamp; + startSamp(iFire+1) = thisStartSamp; + + +% disp(obj.sequence.TxRxList(iTxRx).Rx.delay) +% disp(startSamp) + + for iArius = 0:nArius-1 +% disp(actChanGroupMask(iArius+1)) + Us4MEX(iArius, "SetActiveChannelGroup", actChanGroupMask(iArius+1), iFire); + + % Tx + Us4MEX(iArius, "SetTxAperture", obj.maskFormat(moduleTxApertures(iArius+1,:).'), iFire); + Us4MEX(iArius, "SetTxDelays", moduleTxDelays(iArius+1,:), iFire); + Us4MEX(iArius, "SetTxFrequency", obj.sequence.TxRxList(iTxRx).Tx.pulse.frequency, iFire); + Us4MEX(iArius, "SetTxHalfPeriods", obj.sequence.TxRxList(iTxRx).Tx.pulse.nPeriods*2, iFire); + Us4MEX(iArius, "SetTxInvert", 0, iFire); + + + % Rx +% disp(obj.maskFormat(moduleRxApertures(iArius+1, :, iSubTxRx).')) +% disp(obj.sequence.TxRxList(iTxRx).Rx.delay) + +% Us4MEX(iArius, "SetRxChannelMapping", ... +% obj.usSystem.rxChannelMap(iArius+1,1:32), ... +% iFire ... +% ); + + Us4MEX(iArius, "SetRxAperture", obj.maskFormat(moduleRxApertures(iArius+1, :, iSubTxRx).'), iFire); + Us4MEX(iArius, "SetRxTime", rxTime, iFire); + Us4MEX(iArius, "SetRxDelay", obj.sequence.TxRxList(iTxRx).Rx.delay, iFire); + % do zrobienia tgc +% Us4MEX(iArius, "TGCSetSamples", obj.seq.tgcCurve, iFire); +% Us4MEX(iArius, "ClearScheduledReceive"); + Us4MEX(iArius, "ScheduleReceive", ... + iFire, ... + iFire*nSamp(iFire+1), ... + nSamp(iFire+1), ... + startSamp(iFire+1) + obj.usSystem.trigTxDel, ... + obj.sequence.TxRxList(iTxRx).Rx.fsDivider-1,... + iFire ... + ); + + + % trigger + if isequal(obj.sequence.TxRxList(iTxRx).pri,'min') + thisPri = (thisNSamp+thisStartSamp-1)/fs + obj.minPriAddendum; + pri(iFire+1) = thisPri; +% disp(['current pri: ', num2str(thisPri), '; minimal pri: ', num2str((thisNSamp+thisStartSamp)/fs)]) +% Us4MEX(iArius, "SetTrigger", pri, 0, iFire); + else + + thisPri = obj.sequence.TxRxList(iTxRx).pri; + pri(iFire+1) = thisPri; +% disp(['current pri: ', num2str(thisPri), '; minimal pri: ', num2str((thisNSamp+thisStartSamp)/fs)]) +% Us4MEX(iArius, "SetTrigger", obj.sequence.TxRxList(iTxRx).pri*1e6, 0, iFire); + end + disp(['current pri: ', num2str(thisPri), '; minimal pri: ', num2str((thisNSamp+thisStartSamp)/fs)]) + Us4MEX(iArius, "SetTrigger", thisPri*1e6, 0, iFire); + + end + iFire = iFire+1; + end + + end + + % note: after loop over n TxRx events the 'iFire' represents + % total number of firings + obj.nSamp = nSamp; + obj.startSamp = startSamp; + obj.pri = pri; + + % This is the last trigger + for iArius = 0:obj.usSystem.nArius-1 + Us4MEX(iArius, "EnableTransmit"); +% Us4MEX(iArius, "SetTrigger", obj.sequence.TxRxList(end).pri*1e6, 1, obj.nFire-1); + Us4MEX(iArius, "SetTrigger", thisPri*1e6, 1, obj.nFire-1); + Us4MEX(iArius, "EnableSequencer"); + end + %} + + end + + + function propertiesPreprocessing(obj) + nTxRx = length(obj.sequence.TxRxList); + nSubTxRx = NaN(1,nTxRx); + for i = 1:nTxRx + + + % interpretation of empty properties in TxRx objects + + if isempty(obj.sequence.TxRxList(i).Tx.aperture) + obj.sequence.TxRxList(i).Tx.aperture = ... + false(1,obj.usSystem.nElem); + end + + if isempty(obj.sequence.TxRxList(i).Rx.aperture) + obj.sequence.TxRxList(i).Rx.aperture = ... + false(1,obj.usSystem.nElem); + end + + if isempty(obj.sequence.TxRxList(i).Tx.delay) + obj.sequence.TxRxList(i).Tx.delay = ... + zeros(1,obj.usSystem.nElem); + end + + if isempty(obj.sequence.TxRxList(i).Rx.delay) + obj.sequence.TxRxList(i).Rx.delay = 0; + end + + + if isempty(obj.sequence.TxRxList(i).Rx.time) + obj.sequence.TxRxList(i).Rx.time = 0; + end + + if isempty(obj.sequence.TxRxList(i).Rx.fsDivider) + obj.sequence.TxRxList(i).Rx.fsDivider = 1; + end + + if isempty(obj.sequence.TxRxList(i).Tx.pulse) + pulse = Pulse; + pulse.frequency = 0; + pulse.nPeriods = 0; + obj.sequence.TxRxList(i).Tx.pulse = pulse; + end + + [moduleTxApertures, moduleTxDelays, moduleRxApertures] = ... + obj.apertures2modules( ... + obj.sequence.TxRxList(i).Tx.aperture, ... + obj.sequence.TxRxList(i).Tx.delay, ... + obj.sequence.TxRxList(i).Rx.aperture ... + ); + + + obj.module2RxMaps{i} = moduleRxApertures; + obj.module2TxMaps{i} = moduleTxApertures; + obj.module2TxDelaysMaps{i} = moduleTxDelays; + nTxRxFire = size(moduleRxApertures,3); % number of fires for this single TxRx + nSubTxRx(i) = nTxRxFire; % number of subFirings will be used later in run() method + + end + + % if Rx.aperture is all false, then nSumTxRx is 0, but should + % be 1 for Us4MEX, because must be nFire <=1 + if isequal(nSubTxRx,0) + obj.nSubTxRx = 1; + else + obj.nSubTxRx = nSubTxRx; + end + + obj.nFire = sum(obj.nSubTxRx); + + + end % of propertiesPreprocessing() + + + function nFire = calcNFire(obj) + % The method calculate the number of firings neccessary to + % realize the TxRxSequence + +% maxRxChannels = obj.sys.nChArius; + maxRxChannels = 32; + nFire = 0; + for iTxRx = 1:length(obj.sequence.TxRxList) + thisRxAperture = obj.sequence.TxRxList(1,iTxRx).Rx.aperture; + apertureLenght = length(thisRxAperture); + iTxRxFirings = ceil(apertureLenght./maxRxChannels); + nFire = nFire + iTxRxFirings; + + end + end % of calcNFire() + + + function maskString = maskFormat(obj, maskLogical) + maskLogical = logical(maskLogical); + [maskLength,nMask] = size(maskLogical); + + if maskLength~=16 && maskLength~=128 + error("maskFormat: invalid mask length, should be 16 or 128"); + end + + if maskLength == 16 + % active channel group mask: needs reordering + maskLogical = reshape(permute(reshape(maskLogical,4,2,2,nMask),[3,2,1,4]),16,nMask); + end + + maskString = join(string(double(maskLogical.')),"").'; + maskString = reverse(maskString); + + end % of maskFormat() + + + function [moduleTxApertures, moduleTxDelays, moduleRxApertures] = apertures2modules(obj, txAp, txDel, rxAp) + % The method maps logical transmit aperture, transmit delays + % and receive aperture into mask corresponding to module + % channels. + % + % It returns 3 arrays: + % moduleTxApertures, moduleTxDelays are of size [nModules, nModuleChannels] + % moduleRxApertures, is of size [nModules, nModuleChannels, nFire] + % where nFire is the number of firings necessary to acquire + % rxAperture. + + % number of modules +% nModules = 2; + nModules = obj.usSystem.nArius; + + % number of channels in module +% nModuleChannels = 128; + nModuleChannels = obj.usSystem.nChTotal./obj.usSystem.nArius; + + % number of available rx channels in single module +% nRxChannerls = 32; + nRxChannels = obj.usSystem.nChArius; + + % number of rx channel groups + nRxChanGroups = 3; + + + % Creating array which maps module channels into probe elements: + % array indexes corresponds to iModule, iRxChannel and + % iRxGhanGroup while values corresponds to element numbers + + % allocation + module2elementArray = zeros(nModules,nRxChannels,nRxChanGroups); + elements0 = zeros(nModules, nRxChanGroups); + + % first-1 elements of groups operates by module 1 + elements0(1,:) = [0,64,128]; + + % first-1 elements of groups operates by module 2 + elements0(2,:) = [32,96,160]; + + for iModule = 1:nModules + for iGroup = 1:nRxChanGroups + iElement0 = elements0(iModule, iGroup); + module2elementArray(iModule, 1:nRxChannels, iGroup) = ... + (1:nRxChannels)+iElement0; + end + end + + % TX PART + + % allocation of tx output arrays i.e. aperture masks and delays + % for modules (used by Us4MEX()) + moduleTxApertures = zeros(nModules, nModuleChannels); + moduleTxDelays = zeros(nModules, nModuleChannels); + + % mapping tx module apertures + for iElement = 1:length(txAp) + for iModule = 1:nModules + if txAp(iElement)==1 + [iRxChannel, iRxChanGroup] = ... + find(squeeze(module2elementArray(iModule,:,:)) == iElement); + if ~isempty(iRxChannel) + iModuleChannel = iRxChannel+(iRxChanGroup-1)*nRxChannels; + moduleTxApertures(iModule, iModuleChannel) = iElement; + moduleTxDelays(iModule, iModuleChannel) = txDel(iElement); + end + end + end + end + + + % RX PART + + % allocation of rx output arrays + moduleRxApertures = zeros(nModules,nModuleChannels,nRxChanGroups); + % mapping rx array + for iElement = 1:length(rxAp) + for iModule = 1:nModules + for iRxChanGroup = 1:nRxChanGroups +% iRxChannel = mod(iChannel,nRxChannels+1)+(floor(iChannel/(nRxChannels+1))); + if rxAp(iElement)==1 %&& ismember(iElement, module2elementArray(iModule,:,iRxChanGroup)) + iRxChannel = ... + find(squeeze(module2elementArray(iModule,:,iRxChanGroup)) == iElement); + if ~isempty(iRxChannel) + iModuleChannel = iRxChannel+(iRxChanGroup-1)*nRxChannels; + moduleRxApertures(... + iModule, ... + iModuleChannel, ... + iRxChanGroup ... + ) = iElement; + end + end + end + end + end + + % + % clear empty channel groups (i.e. size(moduleRxApertures,3) + % will be equal to nFire + emptyGroups = []; + for iRxChanGroup = 1:nRxChanGroups + if isempty(find(moduleRxApertures(:,:,iRxChanGroup), 1)) + emptyGroups = [emptyGroups,iRxChanGroup]; + end + end + moduleRxApertures(:,:,emptyGroups) = []; + %} + + end % of apertures2modules() + + + function rf = run(obj) + pauseMultip = 2; +% pauseTime = 0; + +% for iTxRx = 1:length(obj.nSubTxRx) +% pauseTime = pauseTime + ... +% pauseMultip*... +% obj.sequence.TxRxList(iTxRx).pri*... +% obj.nSubTxRx(iTxRx); +% +% disp(['1: ',num2str(obj.sequence.TxRxList(iTxRx).pri)]) +% +% end + + + pauseTime = 0; + for iFire = 1:obj.nFire + pauseTime = pauseTime + ... + pauseMultip*... + obj.pri(iFire); + end + disp(['pauseTime: ',num2str(pauseTime*1e6), '[us], nFire: ',num2str(obj.nFire)]) + + + % Start acquisitions (1st sequence exec., no transfer to host) + Us4MEX(0, "TriggerStart"); + pause(pauseTime); + + Us4MEX(0, "TriggerSync"); + pause(pauseTime); + + %% Transfer to PC + + nAllSamp = sum(obj.nSamp); + rf = Us4MEX(0, ... + "TransferAllRXBuffersToHost", ... + zeros(obj.usSystem.nArius, 1), ... + repmat(nAllSamp, [obj.usSystem.nArius, 1]), ... + int8(1) ... + ); + + + rf = obj.reshapeMexRf(rf); + + % Stop acquisition + Us4MEX(0, "TriggerStop"); + + end % of run() + + + function rfRshpd = reshapeMexRf(obj, rf) + % The method reshapes rf array + % from UsRMEX(iModule, "TransferAllRXBuffersToHost", ... + % (size nChannels x nAllSamp, where nChannels is a number + % of available rx channels in a single module) + % to final rf array (size nSamp x nElements) + + [nChannels, nAllSampn] = size(rf); + nElement = obj.usSystem.nElem; + nTxRx = length(obj.sequence.TxRxList); + nModule = obj.usSystem.nArius; + + rfRshpd = zeros(max(obj.nSamp), nElement, nTxRx); + + maps = obj.module2RxMaps; + sample0 = 0; + for iModule = 1:nModule + iFire = 0; + for iTxRx = 1:nTxRx + map = maps{iTxRx}; + + for iSubTxRx = 1:obj.nSubTxRx(nTxRx) + iFire = iFire+1; + samples = (1:obj.nSamp(iFire))+sample0; + sample0 = sample0+obj.nSamp(iFire); + + % indexes of probe elements corresponding to + % subaperture of Rx.aperture + activeElements = map(iModule, :, iSubTxRx); + activeElements(activeElements==0)=[]; + activeChannels = mod(activeElements-1,nChannels)+1; + + if ~isempty(activeElements) + rfRshpd(:,activeElements, iTxRx) = rf(activeChannels, samples).'; + end + + end + end + + end + + + end + + end + +end \ No newline at end of file diff --git a/api/matlab/+arrus/addLogFile.m b/api/matlab/+arrus/addLogFile.m new file mode 100644 index 000000000..c6c612c34 --- /dev/null +++ b/api/matlab/+arrus/addLogFile.m @@ -0,0 +1,10 @@ +function addLogFile(filepath, level) +% Starts logging to the specified file. + +% :param filepath: path to log file +% :param level: log severity to set, available values: 'FATAL', \ +% 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'TRACE'. + arrus.arrus_mex_object_wrapper("__global", "addLogFile", ... + convertCharsToStrings(filepath), convertCharsToStrings(level)); +end + diff --git a/api/matlab/+arrus/setLogLevel.m b/api/matlab/+arrus/setLogLevel.m new file mode 100644 index 000000000..b89930452 --- /dev/null +++ b/api/matlab/+arrus/setLogLevel.m @@ -0,0 +1,12 @@ +function setLogLevel(level) +% Sets console logging level. +% +% NOTE: This should be the first function from arrus package to call if +% you want to change console log severity. +% +% :param level: log severity to set, available values: 'FATAL', \ +% 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'TRACE'. + arrus.arrus_mex_object_wrapper("__global", "setLogLevel", ... + convertCharsToStrings(level)); +end + diff --git a/api/matlab/+arrus/temp.m b/api/matlab/+arrus/temp.m new file mode 100644 index 000000000..ab7c9cfae --- /dev/null +++ b/api/matlab/+arrus/temp.m @@ -0,0 +1,160 @@ +% txAp = true(1,33); +% rxAp = true(1,33); +% txDel = zeros(1,33); + +% txAp = [true(1,33), false(1,64), true(1,31)]; +% txAp = true(1,192); +% rxAp = true(1,192); +% txDel = zeros(size(txAp)); + + +clc +tx = Tx(); +rx = Rx(); +txrx = TxRx('Tx', Tx, 'Rx', Rx, 'pri', 1e-6) +seq = TxRxSequence([txrx, txrx,txrx]) + + +% [moduleTxApertures, moduleTxDelays, moduleRxApertures] = apertures2modules(txAp, txDel, rxAp); + +% a = [1:32,65:96, 129:160] +% b = [33:64,97:128,161:192] + + +function [moduleTxApertures, moduleTxDelays, moduleRxApertures] = apertures2modules(txAp, txDel, rxAp) + + % The method maps logical transmit aperture, transmit delays + % and receive aperture into mask array + % + % It returns 3 arrays: + % moduleTxApertures, moduleTxDelays are of size [nModules, nModuleChannels] + % moduleRxApertures, is of size [nModules, nModuleChannels, nFire] + % where nFire is the number of firings necessary to acquire + % rxAperture. + + % number of modules + nModules = 2; +% nModules = usSystem.nArius; + + % number of channels in module + nModuleChannels = 128; +% nModuleChannels = usSystem.nChTotal./usSystem.nArius; + + % number of available rx channels in single module + nRxChannels = 32; +% nRxChannels = usSystem.nChArius; + + % number of rx channel groups + nRxChanGroups = 3; + + nElements = length(txAp); + + % some validation - not sure if it is necessary (should be + % checked later) + if length(txAp) > nModules*nModuleChannels + error('Transmit aperture length is bigger than number of available channels.') + end + + + + % Creating array which maps module channels into probe elements: + % array indexes corresponds to iModule, iRxChannel and + % iRxGhanGroup while values corresponds to element numbers + + % allocation + module2elementArray = zeros(nModules,nRxChannels,nRxChanGroups); + elements0 = zeros(nModules, nRxChanGroups); + + % first-1 elements of groups operates by module 1 + elements0(1,:) = [0,64,128]; + + % first-1 elements of groups operates by module 2 + elements0(2,:) = [32,96,160]; + + for iModule = 1:nModules + for iGroup = 1:nRxChanGroups + iElement0 = elements0(iModule, iGroup); + module2elementArray(iModule, 1:nRxChannels, iGroup) = ... + (1:nRxChannels)+iElement0; + end + end + + % alternative approach + +% m2e = false(nModuleChannels, nElements, nModule); +% +% for iModule = 1:nModules +% for iElement = 1:nElement +% for iChannel = 1:nModuleChannels +% +% end +% end +% end + +% module2elementArray + + + + % TX PART + + % allocation of tx output arrays i.e. aperture masks and delays + % for modules (used by Us4MEX()) + moduleTxApertures = zeros(nModules, nModuleChannels); + moduleTxDelays = zeros(nModules, nModuleChannels); + + % mapping tx module apertures + for iElement = 1:length(txAp) + for iModule = 1:nModules + if txAp(iElement)==1 + [iRxChannel, iRxChanGroup] = ... + find(squeeze(module2elementArray(iModule,:,:)) == iElement); + if ~isempty(iRxChannel) + iModuleChannel = iRxChannel+(iRxChanGroup-1)*nRxChannels; + moduleTxApertures(iModule, iModuleChannel) = iElement; + moduleTxDelays(iModule, iModuleChannel) = txDel(iElement); + end + end + end + end +% moduleTxApertures + + + + + % RX PART + + % allocation of rx output arrays + moduleRxApertures = zeros(nModules,nModuleChannels,nRxChanGroups); + % mapping rx array + for iElement = 1:length(rxAp) + for iModule = 1:nModules + for iRxChanGroup = 1:nRxChanGroups +% iRxChannel = mod(iChannel,nRxChannels+1)+(floor(iChannel/(nRxChannels+1))); + if rxAp(iElement)==1 %&& ismember(iElement, module2elementArray(iModule,:,iRxChanGroup)) + iRxChannel = ... + find(squeeze(module2elementArray(iModule,:,iRxChanGroup)) == iElement); + if ~isempty(iRxChannel) + iModuleChannel = iRxChannel+(iRxChanGroup-1)*nRxChannels; + moduleRxApertures(... + iModule, ... + iModuleChannel, ... + iRxChanGroup ... + ) = iElement; + end + end + end + end + end + + % clear empty channel groups (i.e. size(moduleRxApertures,3) + % will be equal to nFire + emptyGroups = []; + for iRxChanGroup = 1:nRxChanGroups + if isempty(find(moduleRxApertures(:,:,iRxChanGroup), 1)) + emptyGroups = [emptyGroups,iRxChanGroup]; + end + end + moduleRxApertures(:,:,emptyGroups) = []; + + + end % of apertures2modules() \ No newline at end of file diff --git a/api/matlab/CMakeLists.txt b/api/matlab/CMakeLists.txt index 1caeb7fbb..14afc39bc 100644 --- a/api/matlab/CMakeLists.txt +++ b/api/matlab/CMakeLists.txt @@ -1,24 +1,32 @@ +################################################################################ +# Mex Wrapper +################################################################################ +add_subdirectory(wrappers) + ################################################################################ # MATLAB API ################################################################################ set(SOURCE_FILES - "arrus/Us4R.m" - "arrus/Us4RSystem.m" - "arrus/BModeDisplay.m" - "arrus/Operation.m" - "arrus/SimpleTxRxSequence.m" - "arrus/PWISequence.m" - "arrus/STASequence.m" - "arrus/LINSequence.m" - "arrus/mustBeDivisible.m" - "arrus/probeParams.m" - "arrus/downConversion.m" - "arrus/reconstructRfImg.m" - "arrus/reconstructRfLin.m" - "arrus/scanConversion.m" - "examples/Us4R_control.m" - "examples/Us4R_maxSequence.m" - "examples/Us4RUltrasonix_control.m" + "arrus/Us4R.m" + "arrus/Us4RSystem.m" + "arrus/BModeDisplay.m" + "arrus/Operation.m" + "arrus/SimpleTxRxSequence.m" + "arrus/PWISequence.m" + "arrus/STASequence.m" + "arrus/LINSequence.m" + "arrus/mustBeDivisible.m" + "arrus/probeParams.m" + "arrus/downConversion.m" + "arrus/reconstructRfImg.m" + "arrus/reconstructRfLin.m" + "arrus/scanConversion.m" + "examples/Us4R_control.m" + "examples/Us4R_maxSequence.m" + "examples/Us4RUltrasonix_control.m" + + +arrus/MexObject.m + +arrus/+session/Session.m ) ################################################################################ @@ -29,37 +37,45 @@ set(TIMESTAMP "${CMAKE_CURRENT_BINARY_DIR}/timestamp") set(TOOLBOX_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/arrus) add_custom_command(OUTPUT ${TIMESTAMP} - COMMAND + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/arrus ${TOOLBOX_OUTPUT_DIR} - COMMAND + COMMAND ${CMAKE_COMMAND} -E touch ${TIMESTAMP} - DEPENDS ${SOURCE_FILES} -) + DEPENDS ${SOURCE_FILES} + ) add_custom_target(matlab_toolbox ALL DEPENDS ${TIMESTAMP}) set_target_properties( - matlab_toolbox - PROPERTIES + matlab_toolbox + PROPERTIES ARRUS_TIMESTAMP ${TIMESTAMP} MATLAB_TOOLBOX_BIN_DIR ${CMAKE_CURRENT_BINARY_DIR} ) install( - DIRECTORY - ${TOOLBOX_OUTPUT_DIR} - DESTINATION + DIRECTORY + ${TOOLBOX_OUTPUT_DIR} + DESTINATION ${ARRUS_MATLAB_INSTALL_DIR} ) install( - FILES - #TODO(pjarosik) to be consistent, move to CURRENT_BINARY_DIR - ${CMAKE_CURRENT_SOURCE_DIR}/examples/Us4R_control.m - ${CMAKE_CURRENT_SOURCE_DIR}/examples/Us4R_maxSequence.m + FILES + #TODO(pjarosik) to be consistent, move to CURRENT_BINARY_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/examples/Us4R_control.m + ${CMAKE_CURRENT_SOURCE_DIR}/examples/Us4R_maxSequence.m ${CMAKE_CURRENT_SOURCE_DIR}/examples/Us4RUltrasonix_control.m - DESTINATION + DESTINATION ${ARRUS_MATLAB_INSTALL_DIR}/examples ) +# New API +install( + DIRECTORY + ${CMAKE_CURRENT_SOURCE_DIR}/+arrus + DESTINATION + ${ARRUS_MATLAB_INSTALL_DIR} +) + diff --git a/api/matlab/arrus/Us4R.m b/api/matlab/arrus/Us4R.m index a7d110771..f4f525c84 100644 --- a/api/matlab/arrus/Us4R.m +++ b/api/matlab/arrus/Us4R.m @@ -209,7 +209,25 @@ function upload(obj, sequenceOperation, reconstructOperation) % :returns: RF frame and reconstructed image (if :class:`Reconstruction` operation was uploaded) obj.openSequence; - rf = obj.execSequence; + [rf, ~] = obj.execSequence; + obj.closeSequence; + + if obj.rec.enable + img = obj.execReconstr(rf(:,:,:,1)); + else + img = []; + end + end + + function [rf, img, metadata] = runWithMetadata(obj) + % Runs uploaded operations in the us4R system. + % + % Currently, only supports :class:`SimpleTxRxSequence` and :class:`Reconstruction` + % implementations. + % + % :returns: RF frame, reconstructed image (if :class:`Reconstruction` operation was uploaded) and metadata located in the first sample of the master module + obj.openSequence; + [rf, metadata] = obj.execSequence; obj.closeSequence; if obj.rec.enable @@ -827,7 +845,7 @@ function closeSequence(obj) end - function rf = execSequence(obj) + function [rf, metadata] = execSequence(obj) nArius = obj.sys.nArius; nChan = obj.sys.nChArius; @@ -848,7 +866,9 @@ function closeSequence(obj) repmat(nSamp * nTrig, [nArius 1]), ... int8(obj.logTime) ... ); - + %% Get metadata + metadata = zeros(nChan, nTrig, 'int16'); + metadata(:, :) = rf(:, 1:nSamp:nTrig*nSamp); %% Reorganize rf = reshape(rf, [nChan, nSamp, nSubTx, nTx, nRep, nArius]); diff --git a/api/matlab/examples/Us4R_TxRx_example.m b/api/matlab/examples/Us4R_TxRx_example.m new file mode 100644 index 000000000..82baccd9d --- /dev/null +++ b/api/matlab/examples/Us4R_TxRx_example.m @@ -0,0 +1,72 @@ + +% path to the MATLAB API filesqui +% addpath('../arrus'); + +nUs4OEM = 2; + + +adapterType = 'esaote2'; + + +probeName = 'SL1543'; +txFrequency = 7e6; + + +samplingFrequency = 65e6; +fsDivider = 1; + +[filtB,filtA] = butter(2,[0.5 1.5]*txFrequency/(samplingFrequency/fsDivider/2),'bandpass'); +% restart + + +%% Initialize the system, sequence, and reconstruction +us = Us4R(nUs4OEM, probeName, adapterType, 1, true); + +%% +% +% txap = false(1,192); +% txap(96) = true; +% TODO: pri powinno si? dostosowywa? do liczby sampli i delay. +% dorobic Rx.nSamp i Rx.startSamp +% Tx('pulse') -> Tx('excitation')? + +% test1 (full aperture) +txap = true(1,192); +rxap = true(1,192); + + +% % test2 (aperture first 32 elements) +% txap = false(1,32); txap(1:32) = true; +% rxap = false(1,32); rxap(1:32) = true; + + + +pulse = Pulse('nPeriods',[2], 'frequency', [7e6]); +t1 = Tx('pulse', pulse, 'aperture', txap); +% r1 = Rx('aperture', rxap, 'time', 7e-6,'delay',15e-6); +r1 = Rx('aperture', rxap, 'time', 40e-6, 'delay', 0e-6); +txrx1 = TxRx('Tx',t1,'Rx', r1, 'pri', 'min'); +sequence = TxRxSequence([txrx1]); +%% +tic +disp('uploading a sequence...') +us.upload(sequence); +% +toc +%% Run sequence (no reconstruction) +tic +[rf] = us.run; + +toc + +%% images +% for i = 1:size(rf,3) +% figure, imagesc(log(double(rf(:,:,i)).^2+1)) +% end + +figure, imagesc(log(double(rf(:,:,1)).^2+1)) +% set(gca,'xlim',[45,75]) +% set(gca,'ylim',[1300,1900]) + +% figure, +% imagesc(rf(900:1400,:,1)) \ No newline at end of file diff --git a/api/matlab/examples/us4OEMSettingsExample.m b/api/matlab/examples/us4OEMSettingsExample.m new file mode 100644 index 000000000..4f7be3d43 --- /dev/null +++ b/api/matlab/examples/us4OEMSettingsExample.m @@ -0,0 +1,21 @@ +addpath('C:\Users\pjarosik\arrus-releases\ref-115\matlab'); +addpath('C:\Users\pjarosik\src\arrus\arrus\api\matlab'); +arrus.setConsoleLogger('DEBUG'); + +rxSettings = arrus.devices.us4r.RxSettings(... + 'dtgcAttenuation', 42,... + 'pgaGain', 24, ... + 'lnaGain', 24, ... + 'tgcSamples', [14, 15, 16], ... + 'lpfCutoff', 10e6, ... + 'activeTermination', 200 ... +); + +channelMapping = 0:127; +groupsMask = cat(2, ones(1, 12), zeros(1, 4)); + +us4oemSettings = arrus.devices.us4r.Us4OEMSettings(channelMapping, groupsMask, rxSettings); +us4RSettings = arrus.devices.us4r.Us4RSettings({us4oemSettings}); +sessionSettings = arrus.session.SessionSettings(us4RSettings); + +arrus.session.Session(sessionSettings); \ No newline at end of file diff --git a/api/matlab/examples/us4RSettingsExample.m b/api/matlab/examples/us4RSettingsExample.m new file mode 100644 index 000000000..7982959e9 --- /dev/null +++ b/api/matlab/examples/us4RSettingsExample.m @@ -0,0 +1,31 @@ +addpath('C:\Users\pjarosik\arrus-releases\ref-115\matlab'); +addpath('C:\Users\pjarosik\src\arrus\arrus\api\matlab'); + +import arrus.devices.us4r.*; +import arrus.devices.probe.*; +import arrus.session.*; + +arrus.setLogLevel('TRACE'); +arrus.addLogFile('test.log', 'TRACE'); + +rxSettings = RxSettings(... + 'dtgcAttenuation', 42,... + 'pgaGain', 24, ... + 'lnaGain', 24, ... + 'tgcSamples', [14, 15, 16], ... + 'lpfCutoff', 10e6, ... + 'activeTermination', 200 ... +); + +adapterMapping = cat(1, cat(2, zeros(1, 64), ones(1, 64)), cat(2, 0:63, 0:63)); + +adapterSettings = ProbeAdapterSettings(ProbeAdapterModelId('us4us', 'esaote2'), ... + 128, adapterMapping); + +probeModel = ProbeModel(ProbeModelId('esaote','sl1543'), 128, 0.3e-3, [1e6, 10e6]); + +probeSettings = ProbeSettings(probeModel, 0:127); +us4RSettings = Us4RSettings(adapterSettings, probeSettings, rxSettings); +sessionSettings = SessionSettings(us4RSettings); + +Session(sessionSettings); \ No newline at end of file diff --git a/api/matlab/wrappers/CMakeLists.txt b/api/matlab/wrappers/CMakeLists.txt new file mode 100644 index 000000000..bc073ba5d --- /dev/null +++ b/api/matlab/wrappers/CMakeLists.txt @@ -0,0 +1,75 @@ +#set(TARGET_NAME arrus_mex_object_wrapper) +# +################################################################################# +## Target and dependencies +################################################################################# +#find_package(Matlab REQUIRED) # TODO require exact 9.5 version +# +#if (NOT Matlab_FOUND) +# message(WARNING "Matlab not found, Us4MEX target not available.") +# return() +#endif () +# +#matlab_add_mex( +# NAME +# ${TARGET_NAME} +# MODULE +# SRC +# ${ARRUS_ROOT_DIR}/arrus/common/compiler.h +# ${ARRUS_ROOT_DIR}/arrus/common/asserts.h +# ${ARRUS_ROOT_DIR}/arrus/common/format.h +# ${ARRUS_ROOT_DIR}/arrus/common/logging/impl/LoggerImpl.h +# ${ARRUS_ROOT_DIR}/arrus/common/logging/impl/Logging.cpp +# ${ARRUS_ROOT_DIR}/arrus/common/logging/impl/Logging.h +# ${ARRUS_ROOT_DIR}/arrus/common/logging/impl/LogSeverity.cpp +# common.h +# convert.h +# DefaultMexObjectManager.h +# MexContext.h +# MexFunction.cpp +# MexFunction.h +# MexObjectManager.h +# MexObjectWrapper.h +# session/SessionWrapper.h +# session/convertSessionSettings.h +# devices/convertDeviceId.h +# devices/us4r/convertUs4RSettings.h +# devices/us4r/convertUs4OEMSettings.h +# devices/us4r/convertProbeAdapterSettings.h +# devices/us4r/convertProbeAdapterModelId.h +# devices/probe/convertProbeModel.h +# devices/probe/convertProbeModelId.h +# devices/probe/convertProbeSettings.h +# devices/us4r/convertRxSettings.h +# MatlabOutBuffer.h +# LINK_TO +# arrus-core +# Boost::Boost +# fmt::fmt +#) +################################################################################# +## Include directories +################################################################################# +#target_include_directories( +# ${TARGET_NAME} +# PRIVATE +# ${PROJECT_SOURCE_DIR} +# ${ARRUS_ROOT_DIR} +#) +# +################################################################################# +## Compile and link options +################################################################################# +#target_compile_definitions(${TARGET_NAME} +# PRIVATE +# # Mute std::unique() deprecation warning from matlab api headers. +# "_SILENCE_CXX17_SHARED_PTR_UNIQUE_DEPRECATION_WARNING" +#) +# +#install( +# TARGETS +# ${TARGET_NAME} +# DESTINATION +# ${ARRUS_MATLAB_INSTALL_DIR}/+arrus +#) +# diff --git a/api/matlab/wrappers/DefaultMexObjectManager.h b/api/matlab/wrappers/DefaultMexObjectManager.h new file mode 100644 index 000000000..89ef92b50 --- /dev/null +++ b/api/matlab/wrappers/DefaultMexObjectManager.h @@ -0,0 +1,23 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEFAULTMEXOBJECTMANAGER_H +#define ARRUS_API_MATLAB_WRAPPERS_DEFAULTMEXOBJECTMANAGER_H + +#include "MexObjectManager.h" +#include "common.h" +#include "arrus/api/matlab/wrappers/session/SessionWrapper.h" + +namespace arrus::matlab { + +template +class DefaultMexObjectManager : public MexObjectManager { + using MexObjectManager::MexObjectManager; + + MexObjectHandle + create(std::shared_ptr ctx, MexMethodArgs &args) override { + auto ptr = std::unique_ptr(new T(ctx, args)); + return insert(std::move(ptr)); + } +}; + +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEFAULTMEXOBJECTMANAGER_H diff --git a/api/matlab/wrappers/MatlabOutBuffer.h b/api/matlab/wrappers/MatlabOutBuffer.h new file mode 100644 index 000000000..c736ac6a8 --- /dev/null +++ b/api/matlab/wrappers/MatlabOutBuffer.h @@ -0,0 +1,35 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_MATLABOUTBUFFER_H +#define ARRUS_API_MATLAB_WRAPPERS_MATLABOUTBUFFER_H + +#include +#include +#include "mex.hpp" +#include "mexAdapter.hpp" + +namespace arrus::matlab { + +class MatlabOutBuffer : public std::stringbuf { +public: + explicit MatlabOutBuffer( + std::shared_ptr<::matlab::engine::MATLABEngine> matlabEngine) + : basic_stringbuf(std::ios_base::out), + matlabEngine(std::move(matlabEngine)) {} + + int sync() override { + matlabEngine->feval(u"fprintf", 0, + std::vector<::matlab::data::Array>( + {factory.createScalar(this->str())})); + this->str(""); + return 0; + } + + +private: + ::matlab::data::ArrayFactory factory; + std::shared_ptr<::matlab::engine::MATLABEngine> matlabEngine; +}; + +} + + +#endif //ARRUS_API_MATLAB_WRAPPERS_MATLABOUTBUFFER_H diff --git a/api/matlab/wrappers/MexContext.h b/api/matlab/wrappers/MexContext.h new file mode 100644 index 000000000..1ad09804e --- /dev/null +++ b/api/matlab/wrappers/MexContext.h @@ -0,0 +1,72 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_MEXCONTEXT_H +#define ARRUS_API_MATLAB_WRAPPERS_MEXCONTEXT_H + +#include + +#include "arrus/common/compiler.h" +#include "arrus/core/api/common/Logger.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +#pragma warning(disable: 4100 4189 4458 4702) + +#include +#include +#include + +COMPILER_POP_DIAGNOSTIC_STATE + +namespace arrus::matlab { + + class MexContext { + public: + using MatlabEnginePtr = std::shared_ptr<::matlab::engine::MATLABEngine>; + + using SharedHandle = std::shared_ptr; + + explicit MexContext(MatlabEnginePtr matlabEngine) + : matlabEngine(std::move(matlabEngine)) {} + + [[nodiscard]] ::matlab::data::ArrayFactory &getArrayFactory() { + return factory; + } + + MatlabEnginePtr &getMatlabEngine() { + return matlabEngine; + } + + void setDefaultLogger(const Logger::SharedHandle &logger) { + this->defaultLogger = logger; + } + + void log(LogSeverity severity, const std::string &msg) { + if(this->defaultLogger != nullptr) { + this->defaultLogger->log(severity, msg); + } + else { + matlabEngine->feval(u"disp", 0, + std::vector<::matlab::data::Array>( + {factory.createScalar(msg)})); + } + } + + void logInfo(const std::string &msg) { + log(LogSeverity::INFO, msg); + } + + void raiseError(const std::string &msg) { + // TODO(pjarosik) add exception name as defined in common dropbox paper + matlabEngine->feval(u"error", 0, + std::vector<::matlab::data::Array>( + {factory.createScalar(msg)})); + }; + + private: + ::matlab::data::ArrayFactory factory; + MatlabEnginePtr matlabEngine; + Logger::SharedHandle defaultLogger; + }; + + +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_MEXCONTEXT_H diff --git a/api/matlab/wrappers/MexFunction.cpp b/api/matlab/wrappers/MexFunction.cpp new file mode 100644 index 000000000..48998c68f --- /dev/null +++ b/api/matlab/wrappers/MexFunction.cpp @@ -0,0 +1,126 @@ +#include "MexFunction.h" +#include "arrus/core/api/common/logging.h" + +#include +#include + +#undef ERROR + +MexFunction::MexFunction() { + mexLock(); + managers.emplace("Session", + new SessionWrapperManager(mexContext, "Session")); +} + +MexFunction::~MexFunction() { + mexUnlock(); +} + +void MexFunction::operator()(ArgumentList outputs, ArgumentList inputs) { + try { + ARRUS_REQUIRES_AT_LEAST(inputs.size(), 2, + "The class and method name are missing."); + + MexObjectClassId classId = inputs[0][0]; + MexObjectMethodId methodId = inputs[1][0]; + + if(classId == "__global" && methodId == "setLogLevel") { + // The first call to MexFunction should set console log + // verbosity, or the default one will be used. + arrus::LogSeverity sev = getLoggerSeverity(inputs); + setConsoleLogIfNecessary(sev); + return; + } + setConsoleLogIfNecessary(arrus::LogSeverity::INFO); + // Other global functions. + if(classId == "__global") { + if(methodId == "addLogFile") { + ARRUS_REQUIRES_AT_LEAST(inputs.size(), 4, + "A path to the log file and " + "logging level are required."); + std::string filepath = inputs[2][0]; + arrus::LogSeverity level = convertToLogSeverity(inputs[3]); + std::shared_ptr logFileStream = + std::make_shared(filepath.c_str(), + std::ios_base::app); + this->logging->addTextSink(logFileStream, level); + } else { + throw arrus::IllegalArgumentException(arrus::format( + "Unrecognized global function: {}", methodId)); + } + return; + } + + ManagerPtr &manager = managers.at(classId); + + if(methodId == "create") { + ArgumentList args(inputs.begin() + 2, inputs.end(), + inputs.size() - 2); + auto handle = manager->create(mexContext, args); + outputs[0] = mexContext->getArrayFactory().createScalar( + handle); + } else { + ARRUS_REQUIRES_AT_LEAST(inputs.size(), 3, + "Object handle is missing."); + MexObjectHandle handle = inputs[2][0]; + + if(methodId == "remove") { + manager->remove(handle); + } else { + ArgumentList args(inputs.begin() + 3, inputs.end(), + inputs.size() - 3); + + auto &object = manager->getObject(handle); + outputs[0] = object->call(methodId, args); + } + } + } + catch(const std::exception &e) { + mexContext->raiseError(e.what()); + } + +} + +void MexFunction::setConsoleLogIfNecessary(const arrus::LogSeverity severity) { + if(logging == nullptr) { + try { + this->logging = std::make_shared(); + this->logging->addTextSink(this->matlabOstream, severity, true); + arrus::Logger::SharedHandle defaultLogger = this->logging->getLogger(); + this->mexContext->setDefaultLogger(defaultLogger); + arrus::setLoggerFactory(logging); + } catch(const std::exception &e) { + this->logging = nullptr; + throw e; + } + + } +} + +arrus::LogSeverity MexFunction::getLoggerSeverity(ArgumentList inputs) { + ARRUS_REQUIRES_AT_LEAST(inputs.size(), 3, + "Log severity level is required."); + return convertToLogSeverity(inputs[2]); +} + +arrus::LogSeverity +MexFunction::convertToLogSeverity(const ::matlab::data::Array &severityStr) { + std::string severity = severityStr[0]; + if(severity == "FATAL") { + return arrus::LogSeverity::FATAL; + } else if(severity == "ERROR") { + return arrus::LogSeverity::ERROR; + } else if(severity == "WARNING") { + return arrus::LogSeverity::WARNING; + } else if(severity == "INFO") { + return arrus::LogSeverity::INFO; + } else if(severity == "DEBUG") { + return arrus::LogSeverity::DEBUG; + } else if(severity == "TRACE") { + return arrus::LogSeverity::TRACE; + } else { + throw arrus::IllegalArgumentException( + arrus::format("Unknown severity level: {}", severity)); + } +} + diff --git a/api/matlab/wrappers/MexFunction.h b/api/matlab/wrappers/MexFunction.h new file mode 100644 index 000000000..f1f1e7ba8 --- /dev/null +++ b/api/matlab/wrappers/MexFunction.h @@ -0,0 +1,66 @@ +#ifndef MEXOBJECTFUNCTION_H +#define MEXOBJECTFUNCTION_H + +#include +#include +#include +#include + +#include "arrus/common/compiler.h" +#include "arrus/common/asserts.h" +#include "arrus/api/matlab/wrappers/common.h" +#include "arrus/api/matlab/wrappers/MexObjectManager.h" +#include "arrus/api/matlab/wrappers/MexObjectWrapper.h" +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/api/matlab/wrappers/session/SessionWrapper.h" +#include "arrus/common/logging/impl/Logging.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4100 4189 4458 4702) + +#include +#include + +COMPILER_POP_DIAGNOSTIC_STATE + +using namespace arrus::matlab; +using namespace matlab::mex; + +/** + * + * This class is responsible for: + * - managing MexObjectManagers (stores map: class id -> MexObjectClassManager) + * - translating input arguments list to class, method, object handle, other parameters + * - handling all exceptions that happen durring method invocation + */ +class MexFunction : public matlab::mex::Function { +public: + MexFunction(); + + ~MexFunction() override; + + void operator()(matlab::mex::ArgumentList outputs, + matlab::mex::ArgumentList inputs) override; + +private: + using ManagerPtr = std::unique_ptr; + std::unordered_map managers; + + std::shared_ptr<::matlab::engine::MATLABEngine> matlabEngine{getEngine()}; + std::shared_ptr matlabOutBuffer{ + std::make_shared(matlabEngine)}; + std::shared_ptr matlabOstream{ + std::make_shared(matlabOutBuffer.get())}; + std::shared_ptr logging; + std::shared_ptr mexContext{new MexContext(matlabEngine)}; + + void setConsoleLogIfNecessary(const arrus::LogSeverity sev); + + arrus::LogSeverity getLoggerSeverity(ArgumentList inputs); + + arrus::LogSeverity convertToLogSeverity(const ::matlab::data::Array& severityStr); +}; + + +#endif // !MEXOBJECTFUNCTION_H + diff --git a/api/matlab/wrappers/MexObjectManager.h b/api/matlab/wrappers/MexObjectManager.h new file mode 100644 index 000000000..83fb8fb78 --- /dev/null +++ b/api/matlab/wrappers/MexObjectManager.h @@ -0,0 +1,61 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_MEXOBJECTMANAGER_H +#define ARRUS_API_MATLAB_WRAPPERS_MEXOBJECTMANAGER_H + +#include +#include +#include + +#include "api/matlab/wrappers/common.h" +#include "common/asserts.h" +#include "common/format.h" +#include "api/matlab/wrappers/MexObjectWrapper.h" +#include "api/matlab/wrappers/MexContext.h" + +namespace arrus::matlab { + +class MexObjectManager { +public: + using MexObjectPtr = std::unique_ptr; + + explicit MexObjectManager(std::shared_ptr ctx, + MexObjectClassId classId) + : classId(std::move(classId)), ctx(std::move(ctx)) {} + + virtual MexObjectHandle + create(std::shared_ptr ctx, MexMethodArgs &args) = 0; + + virtual void remove(const MexObjectHandle handle) { + objects.erase(handle); + } + + MexObjectPtr& getObject(const MexObjectHandle handle) { + return objects.at(handle); + } + +protected: + std::shared_ptr ctx; + + /** + * Assigns new unique handle to the object and stores in the underlying + * map. + */ + MexObjectHandle insert(MexObjectPtr obj) { + MexObjectHandle handle = lastHandle++; + auto res = objects.insert(std::make_pair(handle, std::move(obj))); + ARRUS_REQUIRES_TRUE( + res.second, + "Mex object manager internal error: could not store " + "newly created object"); + return handle; + } + +private: + MexObjectClassId classId; + std::unordered_map objects; + MexObjectHandle lastHandle{0}; +}; + +} + + +#endif //ARRUS_API_MATLAB_WRAPPERS_MEXOBJECTMANAGER_H diff --git a/api/matlab/wrappers/MexObjectWrapper.h b/api/matlab/wrappers/MexObjectWrapper.h new file mode 100644 index 000000000..2478667d5 --- /dev/null +++ b/api/matlab/wrappers/MexObjectWrapper.h @@ -0,0 +1,51 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_ARRUS_MEXOBJECTWRAPPER_H +#define ARRUS_API_MATLAB_WRAPPERS_ARRUS_MEXOBJECTWRAPPER_H + +#include +#include +#include + +#include "api/matlab/wrappers/common.h" +#include "api/matlab/wrappers/MexContext.h" + +namespace arrus::matlab { + + class MexObjectWrapper { + public: + explicit MexObjectWrapper(std::shared_ptr ctx) + : ctx(std::move(ctx)) {} + + virtual ~MexObjectWrapper() = default; + + virtual MexMethodReturnType + call(const MexObjectMethodId &methodId, MexMethodArgs &inputs) { + return methods.at(methodId)(inputs); + } + + protected: + // Note: most of the MexRange methods (e.g. size) are not const, thus const + // qualifier cannot be applied to the inputs. + using MexObjectMethod = std::function; + + void + addMethod(const MexObjectClassId &methodId, + const MexObjectMethod &method) { + methods.emplace(methodId, method); + } + + std::shared_ptr ctx; + std::unordered_map methods; + }; + +/** + * A macro that adds method to given mex object wrapper. + */ +#define ARRUS_MATLAB_ADD_METHOD(obj, methodStr, method) \ + obj->addMethod(methodStr, std::bind(&method, obj, \ + std::placeholders::_1)) + +} + + +#endif //ARRUS_API_MATLAB_WRAPPERS_ARRUS_MEXOBJECTWRAPPER_H diff --git a/api/matlab/wrappers/asserts.h b/api/matlab/wrappers/asserts.h new file mode 100644 index 000000000..65c48e56e --- /dev/null +++ b/api/matlab/wrappers/asserts.h @@ -0,0 +1,99 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_ASSERTS_H +#define ARRUS_API_MATLAB_WRAPPERS_ASSERTS_H + +#include +#include + +#include "arrus/common/asserts.h" +#include "arrus/common/format.h" +#include "arrus/api/matlab/wrappers/common.h" + +#define ARRUS_MATLAB_REQUIRES_N_PARAMETERS(inputs, n, methodName) \ + ARRUS_REQUIRES_EQUAL((inputs).size(), (n), \ + arrus::IllegalArgumentException(arrus::format( \ + "Function '{}' requires exactly {} parameters (got {})", \ + (methodName), (n), (inputs).size()))) + + +#define ARRUS_MATLAB_REQUIRES_SCALAR(array, msg) \ +do { \ + if (!::arrus::matlab::isArrayScalar(array)) { \ + throw arrus::IllegalArgumentException(msg); \ + } \ +} while(0) + +#define ARRUS_MATLAB_REQUIRES_DATA_TYPE_VALUE_EXCEPTION(value, dataType, e) \ +do { \ + dataType min = std::numeric_limits::min(); \ + dataType max = std::numeric_limits::max(); \ + if (value < min || value > max) { \ + throw e; \ + } \ +} while(0) + +#define ARRUS_MATLAB_REQUIRES_DATA_TYPE_VALUE(value, dataType) \ + ARRUS_MATLAB_REQUIRES_DATA_TYPE_VALUE_EXCEPTION(value, dataType, \ + arrus::IllegalArgumentException(arrus::format( \ + "Value {} should be in range [{}, {}]", value, min, max \ + )); \ + ) + +#define ARRUS_MATLAB_REQUIRES_INTEGER_EXCEPTION(value, exception) \ +do { \ + double ignore; \ + if (std::modf(value, &ignore) != 0.0) { \ + throw exception; \ + } \ +} while(0) + +#define ARRUS_MATLAB_REQUIRES_INTEGER(value) \ + ARRUS_MATLAB_REQUIRES_INTEGER_EXCEPTION( \ + value, arrus::IllegalArgumentException(arrus::format( \ + "Value {} should be integer", value))) + + +#define ARRUS_REQUIRES_ALL_DATA_TYPE_VALUE(list, dataType, msg) \ +do { \ + dataType min = std::numeric_limits::min(); \ + dataType max = std::numeric_limits::max(); \ + for(auto value : list) { \ + if (value < min || value > max) { \ + throw arrus::IllegalArgumentException(arrus::format( \ + "Value {} should be in range [{}, {}], {}", \ + value, min, max, msg \ + )); \ + } \ + } \ +} while(0) + +#define ARRUS_MATLAB_REQUIRES_ALL_INTEGER(list) \ +do { \ + double ignore; \ + for(auto value : list) { \ + if (std::modf(value, &ignore) != 0.0) { \ + throw arrus::IllegalArgumentException(arrus::format( \ + "Value {} should be integer", value \ + )); \ + } \ + }\ +} while(0) + +#define ARRUS_MATLAB_REQUIRES_ALL_BINARY(list) \ +do { \ + double ignore;\ + for(auto value : list) { \ + if (std::modf(value, &ignore) != 0.0) { \ + throw arrus::IllegalArgumentException(arrus::format( \ + "Value {} should be binary", value \ + )); \ + } \ + if(value != 1 && value != 0) { \ + throw arrus::IllegalArgumentException(arrus::format( \ + "Value {} should be binary", value \ + )); \ + } \ + }\ +} while(0) + +#endif //ARRUS_API_MATLAB_WRAPPERS_ASSERTS_H + diff --git a/api/matlab/wrappers/common.h b/api/matlab/wrappers/common.h new file mode 100644 index 000000000..149ff92c7 --- /dev/null +++ b/api/matlab/wrappers/common.h @@ -0,0 +1,37 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_COMMON_H +#define ARRUS_API_MATLAB_WRAPPERS_COMMON_H + +#include +#include +#include + +#include + +COMPILER_PUSH_DIAGNOSTIC_STATE +#pragma warning(disable: 4100 4189 4458 4702) + +#include +#include + +COMPILER_POP_DIAGNOSTIC_STATE + +namespace arrus::matlab { + using MexObjectHandle = uint32_t; + using MexObjectMethodId = std::string; + using MexObjectClassId = std::string; + + using MexMethodArgs = ::matlab::mex::ArgumentList; + using MexMethodReturnType = ::matlab::data::TypedArray<::matlab::data::Array>; + + + bool inline isArrayScalar(const ::matlab::data::Array &array) { + return array.getNumberOfElements() == 1; + } + + bool inline isInteger(const double value) { + double ignore; + return std::modf(value, &ignore) == 0.0; + } +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_COMMON_H diff --git a/api/matlab/wrappers/convert.h b/api/matlab/wrappers/convert.h new file mode 100644 index 000000000..cd556b8f7 --- /dev/null +++ b/api/matlab/wrappers/convert.h @@ -0,0 +1,98 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_CONVERT_H +#define ARRUS_API_MATLAB_WRAPPERS_CONVERT_H + +#include +#include + +#include "mex.hpp" +#include "mexAdapter.hpp" +// A header with converting routines mex - cpp for basic types: string, matlab, +// etc. + +namespace arrus::matlab { + +std::string convertToString(const ::matlab::data::CharArray &charArray) { + return charArray.toAscii(); +} + +template +std::vector convertToVector(const ::matlab::data::TypedArray &t) { + std::vector result(t.getNumberOfElements()); + std::transform(std::begin(t), std::end(t), std::begin(result), + [](In value) { return Out(value); }); + return result; +} + +template +T convertToIntScalar(const ::matlab::data::Array &array, + const std::string &arrayName) { + ARRUS_MATLAB_REQUIRES_SCALAR(array, arrayName); + double value = array[0]; + ARRUS_MATLAB_REQUIRES_DATA_TYPE_VALUE(value, T); + ARRUS_MATLAB_REQUIRES_INTEGER(value); + return T(value); +} + +::matlab::data::Array getProperty(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object, + const std::string &propertyName) { + return ctx->getMatlabEngine()->getProperty(object, propertyName); +} + +template +T getIntScalar(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object, + const std::string &propertyName) { + ::matlab::data::Array arr = getProperty(ctx, object, propertyName); + return convertToIntScalar(arr, propertyName); +} + +template +std::optional getOptionalIntScalar(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object, + const std::string &propertyName) { + ::matlab::data::Array arr = getProperty(ctx, object, propertyName); + if(arr.isEmpty()) { + return {}; + } + return convertToIntScalar(arr, propertyName); +} + +::matlab::data::Array getRequiredScalar(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object, + const std::string &propertyName) { + ::matlab::data::Array arr = getProperty(ctx, object, propertyName); + if(arr.isEmpty()) { + throw IllegalArgumentException( + arrus::format("Field '{}' is required", propertyName)); + } + ARRUS_MATLAB_REQUIRES_SCALAR(arr, arrus::format( + "Field '{}' should be scalar", propertyName)); + return arr; +} + +template +std::vector getVector(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object, + const std::string &propertyName) { + ::matlab::data::TypedArray arr = + getProperty(ctx, object, propertyName); + ARRUS_REQUIRES_ALL_DATA_TYPE_VALUE(arr, T, propertyName); + return convertToVector(arr); +} + +template +std::vector getIntVector(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object, + const std::string &propertyName) { + ::matlab::data::TypedArray arr = + getProperty(ctx, object, propertyName); + ARRUS_MATLAB_REQUIRES_ALL_INTEGER(arr); + ARRUS_REQUIRES_ALL_DATA_TYPE_VALUE(arr, T, propertyName); + return convertToVector(arr); +} + +} + + +#endif //ARRUS_API_MATLAB_WRAPPERS_CONVERT_H diff --git a/api/matlab/wrappers/devices/convertDeviceId.h b/api/matlab/wrappers/devices/convertDeviceId.h new file mode 100644 index 000000000..464750e34 --- /dev/null +++ b/api/matlab/wrappers/devices/convertDeviceId.h @@ -0,0 +1,34 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_CONVERTDEVICEID_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_CONVERTDEVICEID_H + +#include "mex.hpp" +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/api/matlab/wrappers/convert.h" +#include "arrus/api/matlab/wrappers/MexContext.h" + +namespace arrus::matlab { + + ::arrus::devices::DeviceId convertToDeviceId(const MexContext::SharedHandle& ctx, + const ::matlab::data::Array &object) { + using namespace arrus::devices; + + auto matlabEngine = ctx->getMatlabEngine(); + std::string deviceTypeStr = convertToString( + matlabEngine->getProperty(object, "deviceType")); + DeviceType dt = + arrus::devices::parseToDeviceTypeEnum(deviceTypeStr); + + ::matlab::data::TypedArray ordinalArr = + matlabEngine->getProperty(object, "ordinal"); + ARRUS_MATLAB_REQUIRES_SCALAR(ordinalArr, + "Device ordinal value should be a scalar"); + double ordinal = ordinalArr[0]; + ARRUS_MATLAB_REQUIRES_DATA_TYPE_VALUE(ordinal, Ordinal); + ARRUS_MATLAB_REQUIRES_INTEGER(ordinal); + return DeviceId(dt, (Ordinal)ordinal); + } + +} + + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_CONVERTDEVICEID_H diff --git a/api/matlab/wrappers/devices/probe/convertProbeModel.h b/api/matlab/wrappers/devices/probe/convertProbeModel.h new file mode 100644 index 000000000..372b5d346 --- /dev/null +++ b/api/matlab/wrappers/devices/probe/convertProbeModel.h @@ -0,0 +1,43 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBEMODEL_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBEMODEL_H + +#include "arrus/common/asserts.h" +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/api/matlab/wrappers/devices/probe/convertProbeModelId.h" +#include "arrus/core/api/devices/probe/ProbeModel.h" +#include "mex.hpp" + +namespace arrus::matlab { +::arrus::devices::ProbeModel +convertToProbeModel(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + using namespace arrus::devices; + auto modelIdArr = getProperty(ctx, object, "modelId"); + ProbeModelId id = convertToProbeModelId(ctx, modelIdArr); + + using ElementIdxType = ProbeModel::ElementIdxType; + + std::vector nElements = getIntVector( + ctx, object, "nElements"); + std::vector pitch = getVector( + ctx, object, "pitch"); + std::vector frequencyRangeVec = getVector( + ctx, object, "txFrequencyRange"); + std::vector voltageRangeVec = getVector( + ctx, object, "voltageRange"); + ARRUS_REQUIRES_EQUAL(frequencyRangeVec.size(), 2, + ::arrus::IllegalArgumentException( + "Tx frequency range should contain " + "exactly two elements.")); + + return ProbeModel( + id, + Tuple(nElements), + Tuple(pitch), + Interval(frequencyRangeVec[0], frequencyRangeVec[1]), + Interval(voltageRangeVec[0], voltageRangeVec[1]) + ); +} +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBEMODEL_H diff --git a/api/matlab/wrappers/devices/probe/convertProbeModelId.h b/api/matlab/wrappers/devices/probe/convertProbeModelId.h new file mode 100644 index 000000000..e3cca3f8d --- /dev/null +++ b/api/matlab/wrappers/devices/probe/convertProbeModelId.h @@ -0,0 +1,22 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBEMODELID_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBEMODELID_H + +#include "arrus/core/api/devices/probe/ProbeModelId.h" +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterModelId.h" +#include "arrus/api/matlab/wrappers/convert.h" +#include "mex.hpp" + +namespace arrus::matlab { +::arrus::devices::ProbeModelId +convertToProbeModelId(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + using namespace arrus::devices; + + std::string name = getProperty(ctx, object, "name")[0]; + std::string manuf = getProperty(ctx, object, "manufacturer")[0]; + return ProbeModelId(name, manuf); +} +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBEMODELID_H diff --git a/api/matlab/wrappers/devices/probe/convertProbeSettings.h b/api/matlab/wrappers/devices/probe/convertProbeSettings.h new file mode 100644 index 000000000..a686b4a99 --- /dev/null +++ b/api/matlab/wrappers/devices/probe/convertProbeSettings.h @@ -0,0 +1,27 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBESETTINGS_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBESETTINGS_H + +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/core/api/devices/probe/ProbeSettings.h" +#include "arrus/api/matlab/wrappers/convert.h" +#include "convertProbeModel.h" +#include "mex.hpp" + +namespace arrus::matlab { +::arrus::devices::ProbeSettings +convertToProbeSettings(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + using namespace arrus::devices; + auto modelArr = getProperty(ctx, object, "probeModel"); + ProbeModel model = convertToProbeModel(ctx, modelArr); + + using ElementType = ProbeModel::ElementIdxType; + + std::vector channelMapping = getIntVector( + ctx, object, "channelMapping"); + + return ProbeSettings(model, channelMapping); +} +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_PROBE_CONVERTPROBESETTINGS_H diff --git a/api/matlab/wrappers/devices/us4r/convertProbeAdapterModelId.h b/api/matlab/wrappers/devices/us4r/convertProbeAdapterModelId.h new file mode 100644 index 000000000..0cd97380e --- /dev/null +++ b/api/matlab/wrappers/devices/us4r/convertProbeAdapterModelId.h @@ -0,0 +1,19 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTPROBEADAPTERMODELID_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTPROBEADAPTERMODELID_H + +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterModelId.h" +#include "mex.hpp" + +namespace arrus::matlab { + ::arrus::devices::ProbeAdapterModelId + convertToProbeAdapterModelId(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + using namespace arrus::devices; + std::string name = getProperty(ctx, object, "name")[0]; + std::string manuf = getProperty(ctx, object, "manufacturer")[0]; + return ProbeAdapterModelId(name, manuf); + } +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTPROBEADAPTERMODELID_H diff --git a/api/matlab/wrappers/devices/us4r/convertProbeAdapterSettings.h b/api/matlab/wrappers/devices/us4r/convertProbeAdapterSettings.h new file mode 100644 index 000000000..20df9f030 --- /dev/null +++ b/api/matlab/wrappers/devices/us4r/convertProbeAdapterSettings.h @@ -0,0 +1,66 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTPROBEADAPTERSETTINGS_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTPROBEADAPTERSETTINGS_H + +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/api/matlab/wrappers/devices/us4r/convertProbeAdapterModelId.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +#include "arrus/common/format.h" +#include "mex.hpp" + +namespace arrus::matlab { +::arrus::devices::ProbeAdapterSettings +convertToProbeAdapterSettings(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + + using namespace arrus::devices; + auto modelIdArr = getProperty(ctx, object, "modelId"); + ProbeAdapterModelId modelId = convertToProbeAdapterModelId( + ctx, modelIdArr); + + auto nChannels = getIntScalar(ctx, object, "nChannels"); + + // Channel mapping + ::matlab::data::TypedArray chMap = getProperty( + ctx, object, "channelMapping"); + + // convert (2xn) array to list of pairs + ARRUS_REQUIRES_EQUAL( + chMap.getDimensions()[0], 2, + IllegalArgumentException("Probe adapter channel mapping " + "should have exactly two dimensions.")); + ProbeAdapterSettings::ChannelMapping channelMapping( + chMap.getDimensions()[1]); + + for(int i = 0; i < chMap.getDimensions()[1]; ++i) { + double ordinal = chMap[0][i]; + double channel = chMap[1][i]; + + ARRUS_MATLAB_REQUIRES_INTEGER_EXCEPTION( + ordinal, IllegalArgumentException( + arrus::format( + "Module's ordinal number should be an integer " + "(found {} in adapter channel mapping).", ordinal))); + ARRUS_MATLAB_REQUIRES_DATA_TYPE_VALUE_EXCEPTION( + ordinal, Ordinal, IllegalArgumentException( + arrus::format( + "Module's ordinal number should be uint16 " + "(found {} in adapter channel mapping).", ordinal))); + + ARRUS_MATLAB_REQUIRES_INTEGER_EXCEPTION( + channel, IllegalArgumentException( + arrus::format( + "Channel number should be an integer " + "(found {} in adapter channel mapping).", channel))); + ARRUS_MATLAB_REQUIRES_DATA_TYPE_VALUE_EXCEPTION( + channel, ChannelIdx, IllegalArgumentException( + arrus::format( + "Channel number should be uint16 " + "(found {} in adapter channel mapping).", channel))); + channelMapping[i] = {(Ordinal)ordinal, (ChannelIdx)channel}; + } + return ProbeAdapterSettings(modelId, nChannels, channelMapping); +} +} + + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTPROBEADAPTERSETTINGS_H diff --git a/api/matlab/wrappers/devices/us4r/convertRxSettings.h b/api/matlab/wrappers/devices/us4r/convertRxSettings.h new file mode 100644 index 000000000..2813028c3 --- /dev/null +++ b/api/matlab/wrappers/devices/us4r/convertRxSettings.h @@ -0,0 +1,32 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTRXSETTINGS_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTRXSETTINGS_H + +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/api/matlab/wrappers/convert.h" +#include "arrus/api/matlab/wrappers/asserts.h" +#include "arrus/core/api/devices/us4r/RxSettings.h" + +namespace arrus::matlab { + +::arrus::devices::RxSettings +convertToRxSettings(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + using namespace arrus::devices; + + std::optional dtgcAttenuation = getOptionalIntScalar( + ctx, object, "dtgcAttenuation"); + + auto pgaGain = getIntScalar(ctx, object, "pgaGain"); + auto lnaGain = getIntScalar(ctx, object, "lnaGain"); + auto lpfCutoff = getIntScalar(ctx, object, "lpfCutoff"); + + auto activeTermination = getOptionalIntScalar(ctx, object, + "activeTermination"); + auto tgcSamples = getVector(ctx, object, "tgcSamples"); + + return RxSettings(dtgcAttenuation, pgaGain, lnaGain, tgcSamples, + lpfCutoff, activeTermination); +} +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTRXSETTINGS_H diff --git a/api/matlab/wrappers/devices/us4r/convertUs4OEMSettings.h b/api/matlab/wrappers/devices/us4r/convertUs4OEMSettings.h new file mode 100644 index 000000000..cf9e7692f --- /dev/null +++ b/api/matlab/wrappers/devices/us4r/convertUs4OEMSettings.h @@ -0,0 +1,39 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTUS4OEMSETTINGS_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTUS4OEMSETTINGS_H + +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/api/matlab/wrappers/convert.h" +#include "arrus/api/matlab/wrappers/asserts.h" +#include "arrus/api/matlab/wrappers/devices/us4r/convertRxSettings.h" +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" +#include "mex.hpp" + +namespace arrus::matlab { +arrus::devices::Us4OEMSettings +convertToUs4OEMSettings(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + using namespace arrus::devices; + // Channel mapping. + ::matlab::data::TypedArray channelMappingArr = getProperty( + ctx, object, "channelMapping"); + ARRUS_REQUIRES_ALL_DATA_TYPE_VALUE(channelMappingArr, ChannelIdx, + "us4oem channel mapping."); + ARRUS_MATLAB_REQUIRES_ALL_INTEGER(channelMappingArr); + std::vector channelMapping = + convertToVector(channelMappingArr); + // Active channel groups. + ::matlab::data::TypedArray activeChGrArr = getProperty( + ctx, object, "activeChannelGroups"); + ARRUS_MATLAB_REQUIRES_ALL_BINARY(activeChGrArr); + BitMask activeChannelGroups = convertToVector(activeChGrArr); + + // rx settings. + ::matlab::data::Array rxSettingsArr = getProperty(ctx, object, + "rxSettings"); + RxSettings rxSettings = convertToRxSettings(ctx, rxSettingsArr); + return Us4OEMSettings(channelMapping, activeChannelGroups, + rxSettings); +} +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTUS4OEMSETTINGS_H diff --git a/api/matlab/wrappers/devices/us4r/convertUs4RSettings.h b/api/matlab/wrappers/devices/us4r/convertUs4RSettings.h new file mode 100644 index 000000000..1795eee5f --- /dev/null +++ b/api/matlab/wrappers/devices/us4r/convertUs4RSettings.h @@ -0,0 +1,46 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTUS4RSETTINGS_H +#define ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTUS4RSETTINGS_H + +#include + +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/api/matlab/wrappers/convert.h" +#include "arrus/api/matlab/wrappers/devices/us4r/convertUs4OEMSettings.h" +#include "arrus/api/matlab/wrappers/devices/us4r/convertProbeAdapterSettings.h" +#include "api/matlab/wrappers/devices/probe/convertProbeSettings.h" +#include "arrus/api/matlab/wrappers/devices/us4r/convertRxSettings.h" +#include "arrus/core/api/devices/us4r/Us4RSettings.h" +#include "mex.hpp" + +namespace arrus::matlab { + +::arrus::devices::Us4RSettings +convertToUs4RSettings(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + using namespace arrus::devices; + auto us4OEMProp = getProperty(ctx, object, "us4OEMSettings"); + if(!us4OEMProp.isEmpty()) { + ::std::vector res; + for(unsigned i = 0; i < us4OEMProp.getNumberOfElements(); ++i) { + ::matlab::data::Array arr = us4OEMProp[i]; + res.emplace_back(convertToUs4OEMSettings(ctx, arr)); + } + return Us4RSettings(res); + } else { + auto adapterArr = getRequiredScalar(ctx, object, + "probeAdapterSettings"); + ProbeAdapterSettings adapterSettings = convertToProbeAdapterSettings( + ctx, adapterArr); + + auto probeArr = getRequiredScalar(ctx, object, "probeSettings"); + ProbeSettings probeSettings = convertToProbeSettings(ctx, probeArr); + + auto rxArr = getRequiredScalar(ctx, object, "rxSettings"); + auto rxSettings = convertToRxSettings(ctx, rxArr); + + return Us4RSettings(adapterSettings, probeSettings, rxSettings); + } +} +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_DEVICES_US4R_CONVERTUS4RSETTINGS_H diff --git a/api/matlab/wrappers/session/SessionWrapper.h b/api/matlab/wrappers/session/SessionWrapper.h new file mode 100644 index 000000000..0974aacc6 --- /dev/null +++ b/api/matlab/wrappers/session/SessionWrapper.h @@ -0,0 +1,65 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_SESSION_SESSIONOBJECTWRAPPER_H +#define ARRUS_API_MATLAB_WRAPPERS_SESSION_SESSIONOBJECTWRAPPER_H + +#include +#include +#include + +#include "arrus/api/matlab/wrappers/MexObjectWrapper.h" +#include "arrus/api/matlab/wrappers/DefaultMexObjectManager.h" +#include "arrus/api/matlab/wrappers/asserts.h" +#include "arrus/api/matlab/wrappers/MatlabOutBuffer.h" +#include "arrus/api/matlab/wrappers/devices/convertDeviceId.h" +#include "arrus/api/matlab/wrappers/session/convertSessionSettings.h" + +#include "arrus/core/api/session/Session.h" +#include "arrus/core/api/session/SessionSettings.h" +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/common/format.h" + + +namespace arrus::matlab { + +class SessionWrapper : public MexObjectWrapper { +public: + + explicit SessionWrapper( + MexContext::SharedHandle ctx, MexMethodArgs &inputs) + : MexObjectWrapper(std::move(ctx)) { + // Callable methods declaration. + ARRUS_MATLAB_ADD_METHOD(this, "getDevice", SessionWrapper::getDevice); + + // Read constructor parameters. + ARRUS_MATLAB_REQUIRES_N_PARAMETERS(inputs, 1, "constructor"); + SessionSettings settings = convertToSessionSettings( + this->ctx, inputs[0]); + session = createSession(settings); + } + + ~SessionWrapper() override { + ctx->logInfo("Destructor"); + } + + MexMethodReturnType getDevice(MexMethodArgs &inputs) { + ARRUS_MATLAB_REQUIRES_N_PARAMETERS(inputs, 1, "getDevice"); + + arrus::devices::DeviceId deviceId = convertToDeviceId(ctx, inputs[0]); + ctx->logInfo(arrus::format("Got DeviceId: {}", deviceId.toString())); + return ctx->getArrayFactory().createCellArray({1, 1}, + ctx->getArrayFactory().createArray( + {2, 2}, + {1., 2., 3., 4.})); + } + +private: + Session::Handle session; + +}; + +class SessionWrapperManager : public DefaultMexObjectManager { + using DefaultMexObjectManager::DefaultMexObjectManager; +}; + +} + +#endif diff --git a/api/matlab/wrappers/session/convertSessionSettings.h b/api/matlab/wrappers/session/convertSessionSettings.h new file mode 100644 index 000000000..a0a04a071 --- /dev/null +++ b/api/matlab/wrappers/session/convertSessionSettings.h @@ -0,0 +1,25 @@ +#ifndef ARRUS_API_MATLAB_WRAPPERS_SESSION_CONVERTSESSIONSETTINGS_H +#define ARRUS_API_MATLAB_WRAPPERS_SESSION_CONVERTSESSIONSETTINGS_H + +#include "arrus/api/matlab/wrappers/MexContext.h" +#include "arrus/core/api/session/SessionSettings.h" +#include "arrus/api/matlab/wrappers/devices/us4r/convertUs4RSettings.h" +#include "mex.hpp" + +namespace arrus::matlab { + arrus::session::SessionSettings + convertToSessionSettings(const MexContext::SharedHandle &ctx, + const ::matlab::data::Array &object) { + auto matlabEngine = ctx->getMatlabEngine(); + + // us4RSettings + ::matlab::data::Array us4rSettingsObj = matlabEngine->getProperty( + object, "us4RSettings"); + ::arrus::devices::Us4RSettings us4RSettings = + convertToUs4RSettings(ctx, us4rSettingsObj); + return ::arrus::session::SessionSettings(us4RSettings); + } + +} + +#endif //ARRUS_API_MATLAB_WRAPPERS_SESSION_CONVERTSESSIONSETTINGS_H diff --git a/api/python/CMakeLists.txt b/api/python/CMakeLists.txt index d9b3af5c1..cdc131239 100644 --- a/api/python/CMakeLists.txt +++ b/api/python/CMakeLists.txt @@ -3,57 +3,48 @@ ################################################################################ # - Targets ################################################################################ - if(ARRUS_BUILD_SWIG) find_package(SWIG REQUIRED) include(UseSWIG) find_package(PythonInterp REQUIRED) find_package(PythonLibs REQUIRED) -set(Us4Components - US4OEM - HV256 - DBARLite +set_property(SOURCE wrappers/core.i PROPERTY CPLUSPLUS ON) +if(MSVC) + set_property( + SOURCE wrappers/core.i + PROPERTY + GENERATED_COMPILE_OPTIONS /Od /EHsc + ) +endif() +swig_add_library(py_core + TYPE SHARED + LANGUAGE PYTHON + OUTPUT_DIR arrus + OUTFILE_DIR wrappers + SOURCES + wrappers/core.i + ${ARRUS_ROOT_DIR}/arrus/common/logging/impl/Logging.cpp + ${ARRUS_ROOT_DIR}/arrus/common/logging/impl/LogSeverity.cpp ) -set(Us4Wrappers - ius4oem - ihv256 - idbarlite +set_target_properties(py_core + PROPERTIES + SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON + RUNTIME_OUTPUT_DIRECTORY + "${CMAKE_CURRENT_BINARY_DIR}/arrus/$<$:>" +) +target_include_directories(py_core + PRIVATE + ${PYTHON_INCLUDE_DIRS} + ${ARRUS_ROOT_DIR} ) -foreach(wrapper component IN ZIP_LISTS Us4Wrappers Us4Components) - set_property(SOURCE wrappers/${wrapper}.i PROPERTY CPLUSPLUS ON) - if(MSVC) - set_property( - SOURCE wrappers/${wrapper}.i - PROPERTY - GENERATED_COMPILE_OPTIONS /Od /EHsc - ) - endif() - swig_add_library(${wrapper} - TYPE SHARED - LANGUAGE PYTHON - OUTPUT_DIR arrus/devices - OUTFILE_DIR wrappers - SOURCES wrappers/${wrapper}.i - ) - set_target_properties(${wrapper} - PROPERTIES - SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON - RUNTIME_OUTPUT_DIRECTORY - "${CMAKE_CURRENT_BINARY_DIR}/arrus/devices/$<$:>" - ) - target_include_directories(${wrapper} - PRIVATE - ${PYTHON_INCLUDE_DIRS} - ${CMAKE_CURRENT_SOURCE_DIR} - ) - target_link_libraries(${wrapper} - PRIVATE - core - Us4::${component} - ${PYTHON_LIBRARIES} - ) -endforeach() +target_link_libraries(py_core + PRIVATE + arrus-core + Boost::Boost + fmt::fmt + ${PYTHON_LIBRARIES} +) endif() ################################################################################ @@ -63,27 +54,43 @@ include(python) set(PYTHON_PACKAGE_NAME arrus) set(SOURCE_FILES - "arrus/__init__.py" - "arrus/beam.py" - "arrus/interface.py" - "arrus/kernels.py" - "arrus/session.py" - "arrus/params.py" - "arrus/system.py" - "arrus/validation.py" - "arrus/ops/__init__.py" - "arrus/ops/operations.py" - "arrus/devices/__init__.py" - "arrus/devices/callbacks.py" - "arrus/devices/device.py" - "arrus/devices/hv256.py" - "arrus/devices/probe.py" - "arrus/devices/us4oem.py" - "arrus/utils/__init__.py" - "arrus/utils/imaging.py" - "arrus/utils/parameters.py" - "arrus/tests/tools.py" + + arrus/__init__.py + arrus/logging.py + arrus/session.py + arrus/validation.py + arrus/metadata.py + arrus/exceptions.py + arrus/medium.py + arrus/params.py + arrus/ops/__init__.py + arrus/ops/imaging.py + arrus/ops/tgc.py + arrus/ops/operation.py + arrus/ops/us4r.py + arrus/devices/__init__.py + arrus/devices/cpu.py + arrus/devices/gpu.py + arrus/devices/us4r.py + arrus/devices/mock_us4r.py + arrus/devices/probe.py + arrus/devices/device.py + arrus/kernels/__init__.py + arrus/kernels/kernel.py + arrus/kernels/imaging.py + arrus/kernels/tgc.py + arrus/utils/__init__.py + arrus/utils/imaging.py + arrus/utils/parameters.py + arrus/utils/us4r.py + arrus/utils/us4r_remap_gpu.py + arrus/utils/fir.py + arrus/utils/interpolate.py + arrus/utils/core.py + arrus/tests/tools.py ) + +# TODO(pjarosik) revise the below list set(TEST_FILES "arrus/tests/beam_test.py" "arrus/tests/session_test.py" @@ -103,11 +110,18 @@ add_custom_command(OUTPUT ${TIMESTAMP} COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/arrus ${CMAKE_CURRENT_BINARY_DIR}/arrus + COMMAND + ${CMAKE_COMMAND} -E copy_directory + ${Us4_ROOT_DIR}/lib64 ${CMAKE_CURRENT_BINARY_DIR}/arrus + COMMAND + ${CMAKE_COMMAND} -E copy + # Intentionally using Release version here. + ${PROJECT_BINARY_DIR}/arrus/core/Release/arrus-core.dll ${CMAKE_CURRENT_BINARY_DIR}/arrus COMMAND ${CMAKE_COMMAND} -E touch ${TIMESTAMP} COMMAND ${PYTHON_EXECUTABLE} ${SETUP_PY_OUT} bdist_wheel - DEPENDS ${SETUP_PY_IN} ${SOURCE_FILES} ${TEST_FILES} ${Us4Wrappers} + DEPENDS ${SETUP_PY_IN} ${SOURCE_FILES} ${TEST_FILES} py_core ) add_custom_target(python_whl ALL DEPENDS ${TIMESTAMP}) @@ -131,7 +145,7 @@ install( DESTINATION ${ARRUS_PYTHON_INSTALL_DIR} RENAME - "${PYTHON_PACKAGE_NAME}-${PROJECT_VERSION}-cp37-cp37m-win_amd64.whl" + "${PYTHON_PACKAGE_NAME}-${PROJECT_VERSION}-py3-none-any.whl" COMPONENT python_whl ) diff --git a/api/python/arrus/__init__.py b/api/python/arrus/__init__.py index 5bc412c8c..7ceb67339 100644 --- a/api/python/arrus/__init__.py +++ b/api/python/arrus/__init__.py @@ -1,67 +1,16 @@ """ARRUS.""" -import logging -from logging import ERROR, WARNING, INFO, DEBUG -_logger = logging.getLogger(__package__) -DEFAULT_LOGGER_LEVEL = logging.INFO -_logger.setLevel(DEFAULT_LOGGER_LEVEL) +import os +# use arrus-core.dll +os.environ["PATH"] += os.pathsep + os.path.dirname(os.path.join(os.path.abspath(__file__))) -_console_handler = logging.StreamHandler() -_console_handler.setLevel(DEFAULT_LOGGER_LEVEL) -_logger_formatter = logging.Formatter( - "%(asctime)s [%(levelname)s] %(name)s: %(message)s") -_console_handler.setFormatter(_logger_formatter) -_logger.addHandler(_console_handler) +from arrus.session import Session +from arrus.logging import ( + set_clog_level, + add_log_file +) -def set_log_level(level): - """ - Sets logging level. +from arrus.exceptions import * - :param level: logging level to set, available levels ``ERROR``, \ - ``WARNING``, ``INFO``, ``DEBUG`` - """ - _logger.setLevel(level) - _console_handler.setLevel(level) - -def add_log_file(filename: str, level): - """ - Add file, where logging information should appear. - - :param filename: a path to the output file - :param level: level to set, available levels: ``ERROR``, \ - ``WARNING``, ``INFO``, ``DEBUG`` - """ - log_file_handler = logging.FileHandler(filename) - log_file_handler.setLevel(level) - file_formatter = logging.Formatter( - "%(asctime)s [%(levelname)s] %(name)s: %(message)s") - log_file_handler.setFormatter(file_formatter) - _logger.addHandler(log_file_handler) - - -# TODO temporary ommiting importing some of the modules here, when -# low-level API is not available (for example currently on Unix systems). -import importlib -import importlib.util -is_ius4oem = importlib.util.find_spec("arrus.devices._ius4oem") -is_ihv256 = importlib.util.find_spec("arrus.devices._ihv256") -is_idbarlite = importlib.util.find_spec("arrus.devices._idbarlite") -if is_ius4oem and is_ihv256 and is_idbarlite: - # Legacy support attributes. - import arrus.devices.device as device - import arrus.session as session - import arrus.interface as interface - import arrus.beam as beam - from arrus.session import Session - from arrus.session import SessionCfg - from arrus.devices.us4oem import Us4OEMCfg - from arrus.devices.us4oem import ChannelMapping - from arrus.system import CustomUs4RCfg -else: - _logger.warn("Low-level API libraries are currently not available, " - "providing minimal version of the package.") - -from arrus.params import * -import arrus.ops as ops \ No newline at end of file diff --git a/api/python/arrus/beam.py b/api/python/arrus/beam.py deleted file mode 100644 index b86f16669..000000000 --- a/api/python/arrus/beam.py +++ /dev/null @@ -1,93 +0,0 @@ -import math - -import numpy as np - -import arrus.devices.device as _device -import arrus.utils as _utils - - -class BeamProfileBuilder: - def __init__(self): - self.speed_of_sound = None - self.pitch = None - self.aperture_size = None - - def set_speed_of_sound(self, speed_of_sound: float): - self.speed_of_sound = speed_of_sound - return self - - def set_pitch(self, pitch: float): - self.pitch = pitch - return self - - def set_aperture_size(self, aperture_size: int): - self.aperture_size = aperture_size - return self - - def build(self): - raise NotImplementedError - - -class PlaneWaveProfileBuilder(BeamProfileBuilder): - def __init__(self): - super().__init__() - self.angle = None - - def set_angle(self, angle: float): - self.angle = angle - return self - - def build(self): - """ - Returns Subaperture and delays according to builder attributes. - - :return: (subaperture, delays) - """ - delays = self.compute_delays( - angle=self.angle, - speed_of_sound=self.speed_of_sound, - pitch=self.pitch, - aperture_size=self.aperture_size - ) - aperture = _device.Subaperture(0, self.aperture_size) - return (aperture, delays) - - @staticmethod - def compute_delays(angle: float, speed_of_sound: float, pitch: float, aperture_size: int): - """ - Returns array of delays according to given parameters. - - :param angle: plane wave angle [rad] - :param speed_of_sound: assumed speed of sound [m/s] - :param pitch: a distance between probe's elements [m] - :param aperture_size: size of an aperture - :return: array of delays [s] - """ - _utils.assert_not_none([ - (angle, "angle"), - (speed_of_sound, "speed of sound"), - (pitch, "pitch"), - (aperture_size, "aperture size") - ]) - dt = math.tan(angle)*pitch/speed_of_sound - result = np.array([i*dt for i in range(aperture_size)]) - result = result-np.min(result) - return result - - -def plane_wave(angle: float, speed_of_sound: float=1540) -> PlaneWaveProfileBuilder: - """ - Returns a plane wave builder, with partially applied angle. - - The return value should be provided as an input to appropriate device - (i.e. a probe instance). - - :param speed_of_sound: assumed speed of sound - :param angle: plane wave angle [rad] - :return: plane wave builder with applied angle - """ - builder = PlaneWaveProfileBuilder() - builder.set_angle(angle) - builder.set_speed_of_sound(speed_of_sound) - return builder - diff --git a/api/python/arrus/devices/callbacks.py b/api/python/arrus/devices/callbacks.py deleted file mode 100644 index b6f10d189..000000000 --- a/api/python/arrus/devices/callbacks.py +++ /dev/null @@ -1,15 +0,0 @@ -import arrus.devices.ius4oem as _ius4oem -from logging import DEBUG, INFO -import logging -_logger = logging.getLogger(__name__) - -class ScheduleReceiveCallback(_ius4oem.ScheduleReceiveCallback): - def __init__(self, callback_fn): - super().__init__() - self.callback_fn = callback_fn - - def run(self, event): - try: - self.callback_fn(event) - except Exception as e: - _logger.exception(e) diff --git a/api/python/arrus/devices/cpu.py b/api/python/arrus/devices/cpu.py new file mode 100644 index 000000000..28cc26b22 --- /dev/null +++ b/api/python/arrus/devices/cpu.py @@ -0,0 +1,14 @@ +import arrus.core +from arrus.devices.device import Device, DeviceId, DeviceType + +DEVICE_TYPE = DeviceType("CPU", arrus.core.DeviceType_CPU) + + +class CPU(Device): + def __init__(self, index): + super().__init__() + self._index = index + + def get_device_id(self): + return DeviceId(DEVICE_TYPE, self._index) + diff --git a/api/python/arrus/devices/device.py b/api/python/arrus/devices/device.py index 05950187f..ab2ab949c 100644 --- a/api/python/arrus/devices/device.py +++ b/api/python/arrus/devices/device.py @@ -1,34 +1,45 @@ -""" ARRUS Devices. """ -import logging +""" Arrus device handle. """ import abc +import dataclasses +import arrus.core -_logger = logging.getLogger(__name__) +@dataclasses.dataclass(frozen=True) +class DeviceType: + type: str + core_repr: object -class DeviceCfg(abc.ABC): - pass +# Currently available python devices. +CPU = DeviceType("CPU", arrus.core.DeviceType_CPU) +Us4R = DeviceType("Us4R", arrus.core.DeviceType_Us4R) -class Device: - def __init__(self, name: str, index: int): - self.name = name - self.index = index - @staticmethod - def get_device_id(name, index): - if index is None: - return name - else: - return "%s:%d" % (name, index) +@dataclasses.dataclass(frozen=True) +class DeviceId: + device_type: object + ordinal: int - def get_id(self): - return Device.get_device_id(self.name, self.index) - def log(self, level, msg): - _logger.log(level, "%s: %s" % (self.get_id(), msg)) +class Device(abc.ABC): + """ + A handle to device. + + This is an abstract class and should not be instantiated. + """ + + @abc.abstractmethod + def get_device_id(self) -> DeviceId: + pass def __str__(self): - return self.get_id() + return str(self.get_device_id()) def __repr__(self): return self.__str__() + + +class UltrasoundDeviceDTO(abc.ABC): + @abc.abstractmethod + def get_id(self): + pass diff --git a/api/python/arrus/devices/gpu.py b/api/python/arrus/devices/gpu.py new file mode 100644 index 000000000..b1c7aca92 --- /dev/null +++ b/api/python/arrus/devices/gpu.py @@ -0,0 +1,14 @@ +import arrus.core +from arrus.devices.device import Device, DeviceId, DeviceType + +DEVICE_TYPE = DeviceType("GPU", arrus.core.DeviceType_GPU) + + +class GPU(Device): + def __init__(self, index): + super().__init__() + self._index = index + + def get_device_id(self): + return DeviceId(DEVICE_TYPE, self._index) + diff --git a/api/python/arrus/devices/hv256.py b/api/python/arrus/devices/hv256.py deleted file mode 100644 index 39cd0791b..000000000 --- a/api/python/arrus/devices/hv256.py +++ /dev/null @@ -1,75 +0,0 @@ -from logging import INFO, WARN - -import arrus.devices.device as _device -import arrus.devices.ihv256 as _hv256 - -import arrus.devices.us4oem as _us4oem - - -class HV256(_device.Device): - _DEVICE_NAME = "HV256" - _N_RETRIES = 2 - - @staticmethod - def get_card_id(index): - return _device.Device.get_device_id(_us4oem.Us4OEM._DEVICE_NAME, index) - - def __init__(self, hv256_handle: _hv256.IHV256): - """ - HV 256 Device. Provides means to steer the voltage - set on the master Us4OEM. - - :param card_handle: a handle to the HV256 C++ class. - """ - super().__init__(HV256._DEVICE_NAME, index=None) - self.hv256_handle = hv256_handle - - def start_if_necessary(self): - # Nothing to do here. - pass - - def enable_hv(self): - """ - Enables HV power supplier. - """ - self.log(INFO, "Enabling HV.") - - for i in range(HV256._N_RETRIES): - try: - self.hv256_handle.EnableHV() - return - except RuntimeError as e: - # TODO(pjarosik) write time out should be handled on lower level - # see Arius-software#29 - self.log(WARN, - ("An error occurred while enabling HV: '%s'" % (str(e))) - + (". Trying again..." if (i+1) < HV256._N_RETRIES else "") - ) - - - def disable_hv(self): - """ - Disables HV power supplier. - """ - self.log(INFO, "Disabling HV.") - self.hv256_handle.DisableHV() - - def set_hv_voltage(self, voltage): - """ - Sets HV voltage to a given value. - - :param voltage: voltage to set [V] - """ - self.log(INFO, "Setting HV voltage to %d [V]" % voltage) - for i in range(HV256._N_RETRIES): - try: - self.hv256_handle.SetHVVoltage(voltage=voltage) - return - except RuntimeError as e: - # TODO(pjarosik) write time out should be handled on lower level - # see Arius-software#29 - self.log(WARN, - ("An error occurred while setting HV voltage: '%s'" % (str(e))) - + (". Trying again..." if (i+1) < HV256._N_RETRIES else "") - ) - diff --git a/api/python/arrus/devices/mock_us4r.py b/api/python/arrus/devices/mock_us4r.py new file mode 100644 index 000000000..412e51869 --- /dev/null +++ b/api/python/arrus/devices/mock_us4r.py @@ -0,0 +1,118 @@ +import time +import arrus.metadata +import arrus.logging +from arrus.logging import (DEBUG, INFO) +import arrus +import numpy as np +from arrus.devices.device import Device + + +class MockFileBuffer: + def __init__(self, dataset: np.ndarray, metadata): + self.dataset = dataset + self.n_frames, _, _, _ = dataset.shape + self.i = 0 + self.counter = 0 + self.metadata = metadata + + def tail(self, timeout=None): + frame_metadata = np.zeros((self.n_frames, 32), dtype=np.int16) + custom_data = { + "frame_metadata_view": frame_metadata + } + metadata = arrus.metadata.Metadata( + context=self.metadata.context, + data_desc=self.metadata.data_description, + custom=custom_data) + return self.dataset[self.i], metadata + + def release_tail(self, timeout=None): + self.i = (self.i + 1) % self.n_frames + + def get_n_elements(self): + return self.n_frames + + def get_element(self, i): + return self.dataset[i].ctypes.data + + def get_element_size(self): + return self.dataset[0].nbytes + + +class MockUs4R(Device): + def __init__(self, dataset: np.ndarray, metadata, index: int): + super().__init__() + self.dataset = dataset + self.metadata = metadata + self.const_metadata = arrus.metadata.ConstMetadata( + context=metadata.context, + data_desc=metadata.data_description, + input_shape=dataset[0].shape, + is_iq_data=False, + dtype='int16') + self.buffer = None + + def get_device_id(self): + return arrus.devices.device.DeviceId("Us4R", 0) + + def set_hv_voltage(self, voltage): + """ + Enables high voltage supplier and sets a given voltage value. + + The voltage is determined by the probe specification; + Us4R can maximally accept 90 Vpp. + + :param voltage: voltage to set [0.5*Vpp] + """ + arrus.logging.log(DEBUG, f"Set voltage {voltage}") + + def disable_hv(self): + """ + Disables high voltage supplier. + """ + arrus.logging.log(DEBUG, "Disable HV voltage.") + + def start(self): + """ + Starts uploaded tx/rx sequence execution. + """ + arrus.logging.log(INFO, "Started device.") + + def stop(self): + """ + Stops tx/rx sequence execution. + """ + arrus.logging.log(INFO, "Stopped device.") + + @property + def sampling_frequency(self): + """ + Device sampling frequency [Hz]. + """ + # TODO use sampling frequency from the us4r device + return 65e6 + + def upload(self, seq: arrus.ops.Operation, + rx_buffer_size=None, host_buffer_size=None, + rx_batch_size=None): + """ + Uploads a given sequence of operations to perform on the device. + + The host buffer returns Frame Channel Mapping in frame + acquisition context custom data dictionary. + + :param seq: sequence to set + :param mode: mode to set (str), available values: "sync", "async" + :param rx_buffer_size: the size of the buffer to set on the Us4R device, + should be None for "sync" version (value 2 will be used) + :param host_buffer_size: the size of the buffer to create on the host + computer, should be None for "sync" version (value 2 will be used) + :param frame_repetition_interval: the expected time between successive + frame acquisitions to set, should be None for "sync" version. None + value means that no interval should be set + :raises: ValueError when some of the input parameters are invalid + :return: a data buffer + """ + arrus.logging.log(arrus.logging.DEBUG, f"Uploaded sequence: {seq}") + return MockFileBuffer(self.dataset, self.metadata), self.const_metadata + diff --git a/api/python/arrus/devices/probe.py b/api/python/arrus/devices/probe.py index 3eb38cf31..de3b65c38 100644 --- a/api/python/arrus/devices/probe.py +++ b/api/python/arrus/devices/probe.py @@ -1,317 +1,50 @@ -from logging import DEBUG - -import arrus.devices.device as _device +import dataclasses import numpy as np - -import arrus.devices.us4oem as _us4oem -import arrus.utils as _utils - - -class Subaperture: - def __init__(self, origin: int, size: int): - self.origin = origin - self.size = size - - def __eq__(self, other): - if not isinstance(other, Subaperture): - return NotImplementedError - - return self.origin == other.origin and self.size == other.size - - def __str__(self): - return "Subaperture(origin=%d, size=%d)" % (self.origin, self.size) - - def __repr__(self): - return self.__str__() - -class ProbeHardwareSubaperture(Subaperture): - def __init__(self, card: _us4oem.Us4OEM, origin: int, size: int): - super().__init__(origin, size) - self.card = card - - -class Probe(_device.Device): - _DEVICE_NAME = "Probe" - - @staticmethod - def get_probe_id(index): - return _device.Device.get_device_id(Probe._DEVICE_NAME, index) - - def __init__(self, - index: int, - model_name: str, - hw_subapertures, - pitch, - master_card: _us4oem.Us4OEM - ): - super().__init__(Probe._DEVICE_NAME, index) - self.model_name = model_name - self.hw_subapertures = hw_subapertures - self.master_card = master_card - self.n_channels = sum(s.size for s in self.hw_subapertures) - self.dtype = np.dtype(np.int16) - self.pitch = pitch - - def start_if_necessary(self): - for subaperture in self.hw_subapertures: - subaperture.card.start_if_necessary() - - def get_tx_n_channels(self): - return self.n_channels - - def get_rx_n_channels(self): - return self.n_channels - - def transmit_and_record( - self, - carrier_frequency: float, - beam=None, - tx_aperture: Subaperture=None, - tx_delays=None, - n_tx_periods: int = 1, - n_samples: int=4096, - rx_time: float=80e-6 - ): - _utils.assert_true( - (beam is not None) ^ (tx_aperture is not None and tx_delays is not None), - "Exactly one of the following parameters should be provided: " - "beam, (tx aperture, tx delays)" - ) - if beam is not None: - beam.set_pitch(self.pitch) - beam.set_aperture_size(self.get_tx_n_channels()) - tx_aperture, tx_delays = beam.build() - tx_delays = tx_delays.tolist() - # Validate input. - - _utils._assert_equal( - len(tx_delays), tx_aperture.size, - desc="Array of TX delays should contain %d numbers (probe aperture size)" % tx_aperture.size - ) - _utils.assert_true( - carrier_frequency > 0, - desc="Carrier frequency should be greater than zero." - ) - _utils.assert_true( - n_samples > 0, - desc="Number of samples should be greater than zero." - ) - _utils.assert_true( - n_samples % 4096 == 0, - desc="Number of samples should be a multiple of 4096." - ) - # Probe's Tx aperture settings. - tx_start = tx_aperture.origin - tx_end = tx_aperture.origin + tx_aperture.size - # We set aperture [tx_start, tx_end) - - current_origin = 0 - delays_origin = 0 - self.log( - DEBUG, - "Setting TX aperture: origin=%d, size=%d" % (tx_aperture.origin, tx_aperture.size) - ) - self.start_if_necessary() - for s in self.hw_subapertures: - # Set all TX parameters here (if necessary). - card, origin, size = s.card, s.origin, s.size - - current_start = current_origin - current_end = current_origin+size - if tx_start < current_end and tx_end > current_start: - # Current boundaries of the PROBE subaperture. - aperture_start = max(current_start, tx_start) - aperture_end = min(current_end, tx_end) - # Origin relative to the start of the CARD's aperture. - delays_subarray_size = aperture_end-aperture_start - - # Set delays - # TODO(pjarosik) is it necessary to apply 0.0 to inactive channels? - # It would be better to keep old values at inactive positions - card_delays = tx_delays[delays_origin:(delays_origin+delays_subarray_size)] - card_delays = [0.0]*(origin+aperture_start-current_origin) + card_delays \ - + [0.0]*(card.get_n_tx_channels() - ((aperture_end-current_origin)+origin)) - card.set_tx_delays(card_delays) - - # Other TX/RX parameters. - card.set_tx_frequency(carrier_frequency) - card.set_tx_periods(n_tx_periods) - card.set_tx_aperture(origin=0, size=0) - card.set_rx_time(rx_time) - - # Set the final TX Aperture - card.set_tx_aperture( - origin+(aperture_start-current_origin), - aperture_end-aperture_start) - delays_origin += delays_subarray_size - # TODO(pjarosik) update TX/RX parameters only when it is necessary. - current_origin += size - - # Rx parameters and an output buffer. - output = np.zeros((n_samples, self.n_channels), dtype=self.dtype) - # Cards, which will be used on the RX step. - # Currently all cards are used to acquire RF data. - rx_cards = [s.card for s in self.hw_subapertures] - card_host_buffers = {} - for rx_card in rx_cards: - card_host_buffers[rx_card.get_id()] = _utils.create_aligned_array( - (n_samples, rx_card.get_n_rx_channels()), - dtype=self.dtype, - alignment=4096 - ) - - subapertures_to_process = [ - (s.card, Subaperture(s.origin, s.size)) - for s in self.hw_subapertures - ] - buffer_device_addr = 0 - tx_nr = 0 - while subapertures_to_process: - self.log(DEBUG, "Performing transmission nr %d." % tx_nr) - next_subapertures = [] - # Initiate SGDMA. - for i, (card, s) in enumerate(subapertures_to_process): - #TODO(pjarosik) below won't work properly, when s.size < card.n_rx_channels - card.set_rx_aperture(s.origin, card.get_n_rx_channels()) - - card.schedule_receive(buffer_device_addr, n_samples) - s.origin += card.get_n_rx_channels() - if s.origin < s.size: - next_subapertures.append((card, s)) - - self.master_card.sw_trigger() - # Wait until the data is received. - for card, _ in subapertures_to_process: - card.wait_until_sgdma_finished() - - # Initiate RX transfer from device to host. - channel_offset = 0 - for card, s in subapertures_to_process: - buffer = card_host_buffers[card.get_id()] - card.transfer_rx_buffer_to_host( - dst_array=buffer, - src_addr=buffer_device_addr - ) - current_channel = channel_offset+tx_nr*card.get_n_rx_channels() - output[:,current_channel:(current_channel+card.get_n_rx_channels())] = buffer - channel_offset += s.size - tx_nr += 1 - # Remove completed subapertures. - subapertures_to_process = next_subapertures - - return output - - def set_tx_delay(self, channel_nr:int, delay:float): - _utils.assert_true( - self.get_tx_n_channels() > channel_nr, - desc="Maximum channel number is %d" % (self.get_tx_n_channels()-1) - ) - _utils.assert_true( - delay >= 0.0, - desc="Delay should be non-negative (is %d)" % delay - ) - # TODO(pjarosik) optimize below - current_origin = 0 - for subaperture in self.hw_subapertures: - current_end = current_origin+subaperture.size - if current_origin <= channel_nr < current_end: - return subaperture.card.set_tx_delay( - channel=channel_nr-current_origin, - delay=float(delay)) - current_origin += subaperture.size - raise ValueError("Channel not found: %d" % channel_nr) - - def set_tx_aperture(self, tx_aperture): - _utils.assert_true( - self.get_tx_n_channels() >= tx_aperture.origin+tx_aperture.size, - desc="Aperture cannot exceed number of TX channels (%d)" % self.get_tx_n_channels() - ) - self.log(DEBUG, - "Setting Probe TX aperture: origin=%d, size=%d" % (tx_aperture.origin, tx_aperture.size) - ) - tx_start = tx_aperture.origin - tx_end = tx_aperture.origin + tx_aperture.size - current_origin = 0 - for s in self.hw_subapertures: - # Set all TX parameters here (if necessary). - card, origin, size = s.card, s.origin, s.size - current_start = current_origin - current_end = current_origin+size - if tx_start < current_end and tx_end > current_start: - # Current boundaries of the PROBE subaperture. - aperture_start = max(current_start, tx_start) - aperture_end = min(current_end, tx_end) - card.set_tx_aperture( - origin+(aperture_start-current_origin), - aperture_end-aperture_start) - current_origin += size - - def set_tx_frequency(self, frequency): - _utils.assert_true( - frequency > 0, - desc="Carrier frequency should be greater than zero." - ) - for card in self._get_cards(): - card.set_tx_frequency(frequency) - - def set_tx_periods(self, n_tx_periods): - for card in self._get_cards(): - card.set_tx_periods(n_tx_periods) - - def set_rx_time(self, rx_time): - for card in self._get_cards(): - card.set_rx_time(rx_time) - - def set_pga_gain(self, gain): - for card in self._get_cards(): - card.set_pga_gain(gain) - - def set_lpf_cutoff(self, cutoff): - for card in self._get_cards(): - card.set_lpf_cutoff(cutoff) - - def set_active_termination(self, active_termination): - """ - Sets active termination for all cards handling this probe. - When active termination is None, the property is disabled. - - :param active_termination: active termination, can be None - """ - for card in self._get_cards(): - card.set_active_termination(active_termination) - - def set_lna_gain(self, gain): - for card in self._get_cards(): - card.set_lna_gain(gain) - - def set_dtgc(self, attenuation): - """ - Sets DTGC for all cards handling this probe. - When attenuation is None, this property is set to disabled. - - :param attenuation: attenuation, can be None - """ - for card in self._get_cards(): - card.set_dtgc(attenuation) - - def disable_test_patterns(self): - for card in self._get_cards(): - card.disable_test_patterns() - - def enable_test_patterns(self): - for card in self._get_cards(): - card.enable_test_patterns() - - def sync_test_patterns(self): - for card in self._get_cards(): - card.sync_test_patterns() - - def _get_cards(self): - card_ids = set() - cards = [] - for hw_subaperture in self.hw_subapertures: - card = hw_subaperture.card - if not card.get_id() in card_ids: - cards.append(card) - card_ids.add(card.get_id()) - return cards +import math + + +@dataclasses.dataclass(frozen=True) +class ProbeModelId: + manufacturer: str + name: str + + +@dataclasses.dataclass(frozen=True) +class ProbeModel: + model_id: ProbeModelId + n_elements: int + pitch: float + curvature_radius: float + + def __post_init__(self): + element_pos_x, element_pos_z = self._compute_element_position() + super().__setattr__("element_pos_x", element_pos_x) + super().__setattr__("element_pos_z", element_pos_z) + + def _compute_element_position(self): + # element position along the surface + element_position = np.arange(-(self.n_elements - 1) / 2, + self.n_elements / 2) + element_position = element_position * self.pitch + + if not self.is_convex_array(): + x_pos = element_position + z_pos = np.zeros((1, self.n_elements)) + else: + angle = element_position / self.curvature_radius + x_pos = self.curvature_radius * np.sin(angle) + z_pos = self.curvature_radius * np.cos(angle) + z_pos = z_pos - np.min(z_pos) + return (x_pos, z_pos) + + def is_convex_array(self): + return not (math.isnan(self.curvature_radius) + or self.curvature_radius == 0.0) + + +@dataclasses.dataclass(frozen=True) +class ProbeDTO: + model: ProbeModel + + def get_id(self): + return "Probe:0" \ No newline at end of file diff --git a/api/python/arrus/devices/us4oem.py b/api/python/arrus/devices/us4oem.py deleted file mode 100644 index d0da30a49..000000000 --- a/api/python/arrus/devices/us4oem.py +++ /dev/null @@ -1,1076 +0,0 @@ -import math -import time -from functools import wraps -from logging import DEBUG, INFO, WARN -from typing import List, Union, Optional -import dataclasses - - -import numpy as np - -import arrus.devices.device as _device -import arrus.devices.ius4oem as _ius4oem -import arrus.devices.callbacks as _callbacks -import arrus.utils as _utils -import arrus.validation as _validation -import arrus.interface as _interface - - -def assert_card_is_powered_up(f): - @wraps(f) - def wrapper(*args, **kwargs): - us4oem = args[0] - if us4oem.is_powered_down(): - raise RuntimeError("Card is powered down. Start the card first.") - return f(*args, **kwargs) - return wrapper - - -@dataclasses.dataclass(frozen=True) -class ChannelMapping: - """ - Tx/Rx channel mapping for Us4OEM module. - - :param tx: a list: tx[output channel number] = input channel number - :param rx: a list: rx[output channel number] = input channel number - """ - tx: list - rx: list - - def __post_init__(self): - _validation.assert_type(self.tx, list, "tx channel mapping") - _validation.assert_type(self.rx, list, "rx channel mapping") - _validation.assert_equal(len(self.tx), 128, "tx mapping array length") - _validation.assert_equal(len(self.rx), 32, "rx mapping array length") - - -@dataclasses.dataclass(frozen=True) -class Us4OEMCfg(_device.DeviceCfg): - """ - Us4OEM module configuration. - - :param channel_mapping: channel mapping to set. If str, ``esaote`` or \ - ``ultrasonix`` should be provided. - :param active_channel_groups: a list of True/False values (or non-zero \ - and zero values), which indicate which groups of channels should be \ - active during the whole session with the device. - :param dtgc: Digital time gain compensation. Actually this is an attenuation to \ - apply, e.g. ``0`` gives the highest gain, ``42`` the lowest. \ - When is None, DTGC is set to disabled. Available values: 0, 6, 12, \ - 18, 24, 30, 36, 42 [dB]; can be None - :param pga_gain: Configures programmable-gain amplifier (PGA). Gain to set,\ - available values: 24, 30 [dB] - :param lna_gain: Configures low-noise amplifier (LNA) gain. Gain to set; \ - available values: 12, 18, 24 [dB] - :param lpf_cutoff: Low-pass filter (LPF) cutoff frequency to set, \ - available values: 10e6, 15e6, 20e6, 30e6, 35e6, 50e6 [Hz] - :param active_termination: Active termination to set, \ - available values: 50, 100, 200, 400; can be None (disabled) - :param tgc_samples: a list of TGC curve samples to set [dB]. The values \ - should be in range 14-54 dB, maximum number of samples to set: 1022. \ - TGC curve sampling rate is equal 1MHz. Set to None if you want to \ - disable TGC. Currently if you want to use this parameter, pga_gain and \ - lna_gain must be set to 30 and 24 respectively. - :param log_data_transfer_time: set to True if you want to log data \ - transfer time (from Us4OEM to the PC) - """ - channel_mapping: Union[ChannelMapping, str] - active_channel_groups: list - dtgc: Optional[float] = None - pga_gain: float = 30 - lna_gain: float = 24 - lpf_cutoff: float = 10e6 - active_termination: float = None - tgc_samples: Union[list, np.ndarray, None] = None - log_transfer_time: bool = False - - def __post_init__(self): - - # Validate channel mapping - if isinstance(self.channel_mapping, str): - available_interfaces = _interface._INTERFACES.keys() - _validation.assert_one_of(self.channel_mapping, - available_interfaces, - "channel mapping") - else: - _validation.assert_type(self.channel_mapping, ChannelMapping, - "channel mapping") - - # Validate active channel groups. - _validation.assert_type(self.active_channel_groups, list, - "active channel groups") - _validation.assert_equal(len(self.active_channel_groups), 16, - "active_channel_groups length") - # Validate TGC samples. - if self.tgc_samples is not None: - max_value = np.max(self.tgc_samples) - min_value = np.min(self.tgc_samples) - _validation.assert_in_range((min_value, max_value), (14, 54), - "TGC curve values") - - -class Us4OEM(_device.Device): - """ - A single Us4OEM. - """ - _DEVICE_NAME = "Us4OEM" - - @staticmethod - def get_card_id(index): - return _device.Device.get_device_id(Us4OEM._DEVICE_NAME, index) - - # TODO(pjarosik) make cfg mandatory when InteractiveSession will be not - # anymore needed - def __init__(self, index: int, card_handle: _ius4oem.IUs4OEM, - cfg: Us4OEMCfg=None): - super().__init__(Us4OEM._DEVICE_NAME, index) - self.card_handle = card_handle - self.dtype = np.dtype(np.int16) - self.host_buffer = np.array([]) - self.pri_list = None - self.pri_total = None - self.callbacks = [] - self.cfg = cfg - self._default_active_channel_groups = None - self.tx_channel_mapping = None - self.rx_channel_mapping = None - - def start_if_necessary(self): - """ - Starts the card if is powered down. - """ - if self.is_powered_down(): - self.log( - INFO, - "Was powered down, initializing it and powering up...") - if self.cfg is not None: - if isinstance(self.cfg.channel_mapping, str): - interf = _interface.get_interface(self.cfg.channel_mapping) - tx_map = interf.get_tx_channel_mapping(self.index) - rx_map = interf.get_rx_channel_mapping(self.index) - mapping = ChannelMapping(tx=tx_map, rx=rx_map) - else: - mapping = self.cfg.channel_mapping - self.store_mappings(tx_m=mapping.tx, rx_m=mapping.rx) - self.card_handle.Initialize() - if self.tx_channel_mapping is not None \ - and self.rx_channel_mapping is not None: - self.set_tx_channel_mapping(self.tx_channel_mapping) - self.set_rx_channel_mapping(self.rx_channel_mapping) - else: - self.log(WARN, f"Device {self.get_id()} initialized " - f"without setting channel mapping.") - if self.cfg is not None: - self._default_active_channel_groups = \ - self.cfg.active_channel_groups - self.set_dtgc(attenuation=self.cfg.dtgc) - self.set_pga_gain(gain=self.cfg.pga_gain) - self.set_lna_gain(gain=self.cfg.lna_gain) - self.set_lpf_cutoff(cutoff=self.cfg.lpf_cutoff) - self.set_active_termination( - active_termination=self.cfg.active_termination) - if self.cfg.tgc_samples is None: - self.disable_tgc() - else: - if self.cfg.pga_gain != 30 or self.cfg.lna_gain != 24: - raise ValueError(f"When setting TGC samples curve " - f"pga_gain and and lna_gain should " - f"equal 30 and 24 respectively " - f"is: {self.cfg.pga_gain} and " - f"{self.cfg.lna_gain}") - tgc_db_values = np.array(self.cfg.tgc_samples) - tgc_db_values = tgc_db_values-14 - tgc_db_values = tgc_db_values/40 - self.enable_tgc() - self.set_tgc_samples(tgc_db_values) - - self.log(INFO, "... successfully powered up.") - - def get_sampling_frequency(self): - """ - Returns Us4OEM's sampling frequency. - """ - return 65e6 - - def get_n_rx_channels(self): - """ - Returns number of RX channels. - - :return: number of RX channels. - """ - return 32 # TODO(pjarosik) should be returned by us4oem - # return self.card_handle.GetNRxChannels() - - def get_n_tx_channels(self): - """ - Returns number of TX channels. - - :return: number of TX channels - """ - return 128 # TODO(pjarosik) should be returned by ius4oem - # return self.card_handle.GetNTxChannels() - - def get_n_channels(self): - """ - Returns number of channels available for this module. - - :return: number of channels of the module - """ - return 128 # TODO(pjarosik) should be returned by ius4oem - - def store_mappings(self, tx_m, rx_m): - self.tx_channel_mapping = tx_m - self.rx_channel_mapping = rx_m - - @assert_card_is_powered_up - def set_tx_channel_mapping(self, tx_channel_mapping: List[int]): - """ - Sets card's TX channel mapping. - - :param tx_channel_mapping: a list, where list[interface channel] = us4oem channel - """ - _utils.assert_true( - tx_channel_mapping is not None, - "TX channel mapping should be not None." - ) - self.log(DEBUG, "Setting TX channel mapping: %s" % str(tx_channel_mapping)) - self.tx_channel_mapping = tx_channel_mapping - for dst, src in enumerate(tx_channel_mapping): - self.card_handle.SetTxChannelMapping( - srcChannel=src, - dstChannel=dst - ) - - @assert_card_is_powered_up - def set_rx_channel_mapping(self, rx_channel_mapping: List[int]): - """ - Sets card's RX channel mapping. - - :param rx_channel_mapping: a list, where list[interface channel] = us4oem channel - """ - _utils.assert_true( - rx_channel_mapping is not None, - "RX channel mapping should be not None." - ) - self.log(DEBUG, "Setting RX channel mapping: %s" % str(rx_channel_mapping)) - self.rx_channel_mapping = rx_channel_mapping - for dst, src in enumerate(rx_channel_mapping): - self.card_handle.SetRxChannelMapping( - srcChannel=src, - dstChannel=dst - ) - - @assert_card_is_powered_up - def set_tx_aperture(self, origin: int, size: int, firing: int=0): - """ - Sets position of an active TX aperture. - :param origin: an origin channel of the aperture, **starts from 0** - :param size: a length of the aperture - :param firing: a firing, in which the delay should apply, **starts from 0** - """ - self.log( - DEBUG, - "Setting TX aperture: origin=%d, size=%d" % (origin, size) - ) - self.card_handle.SetTxAperture(origin, size, firing) - - @assert_card_is_powered_up - def set_tx_aperture_mask(self, aperture, firing: int=0): - """ - Sets mask of active TX aperture channels. - - :param aperture: a boolean numpy array of get_number_of_channels() channels, 'True' means to activate chanel on a given position. - :param firing: a firing, in which the delay should apply, **starts from 0** - """ - aperture = np.array(aperture).astype(np.bool) - if len(aperture.shape) != 1: - raise ValueError("Aperture should be a vector.") - if aperture.shape[0] != self.get_n_channels(): - raise ValueError("Aperture should have %d elements." - % aperture.shape[0]) - aperture = aperture.astype(np.uint16) - aperture_list = aperture.tolist() - n = len(aperture_list) - self.log(DEBUG, "Setting TX aperture: %s" % str(aperture_list)) - array = None - try: - array = _ius4oem.new_uint16Array(n) - for i, sample in enumerate(aperture_list): - _ius4oem.uint16Array_setitem(array, i, sample) - _ius4oem.setTxApertureCustom(self.card_handle, array, n, firing) - finally: - _ius4oem.delete_uint16Array(array) - - @assert_card_is_powered_up - def set_rx_aperture(self, origin: int, size: int, firing: int=0): - """ - Sets RX aperture’s origin and size. - - :param origin: an origin channel of the aperture - :param size: a length of the aperture - :param firing: a firing, in which the parameter value should apply, starts from 0 - """ - self.log( - DEBUG, - "Setting RX aperture: origin=%d, size=%d" % (origin, size) - ) - self.card_handle.SetRxAperture(origin, size, firing) - - @assert_card_is_powered_up - def set_rx_aperture_mask(self, aperture, firing: int = 0): - """ - Sets position of an active RX aperture. - - :param aperture: a boolean numpy array of get_number_of_channels() channels, 'True' means to activate chanel on a given position. - :param firing: a firing, in which the delay should apply, **starts from 0** - """ - aperture = np.array(aperture).astype(np.bool) - if len(aperture.shape) != 1: - raise ValueError("Aperture should be a vector.") - if aperture.shape[0] != self.get_n_channels(): - raise ValueError("Aperture should have %d elements." - % aperture.shape[0]) - aperture = aperture.astype(np.uint16) - aperture_list = aperture.tolist() - n = len(aperture_list) - self.log(DEBUG, "Setting RX aperture: %s" % str(aperture_list)) - array = None - try: - array = _ius4oem.new_uint16Array(n) - for i, sample in enumerate(aperture_list): - _ius4oem.uint16Array_setitem(array, i, sample) - _ius4oem.setRxApertureCustom(self.card_handle, array, n, firing) - finally: - _ius4oem.delete_uint16Array(array) - - @assert_card_is_powered_up - def set_active_channel_group(self, active_groups_mask, firing: int=0): - """ - Sets active channel groups. - Channel is active when it is TX/RX/CLAMP state. Channel is inactive - when in HIZ state. - Single group has 8 channels (single pulser). - - - [0] - channels 0-7 - - [4] - channels 8-15 - - [8] - channels 16-23 - - [12] - channels 24-31 - - [1] - channels 64-71 - - [5] - channels 72-79 - - [9] - channels 80-87 - - [13] - channels 88-95 - - [2] - channels 32-39 - - [6] - channels 40-47 - - [10] - channels 48-55 - - [14] - channels 56-63 - - [3] - channels 96-103 - - [7] - channels 104-111 - - [11] - channels 112-119 - - [15] - channels 120-127 - - :param active_groups_mask: list of boolean values, True means \ - a group of channels at given position should be active - :param firing: a firing, in which the parameter value should apply - """ - active_groups_mask = np.array(active_groups_mask).astype(np.bool) - if len(active_groups_mask.shape) != 1: - raise ValueError("Mask should be a vector.") - if active_groups_mask.shape[0] != self.get_n_channels()/8: - raise ValueError("Mask should have %d elements." - % (self.get_n_channels()/8)) - active_groups_mask = active_groups_mask.astype(np.uint16) - # Reverse mask to pass data in MSB first order. - active_groups_mask = active_groups_mask[::-1] - active_groups_mask_list = active_groups_mask.tolist() - n = len(active_groups_mask_list) - self.log( - DEBUG, - "Setting active channel group mask: %s, firing: %s" % ( - active_groups_mask, firing - ) - ) - array = None - try: - array = _ius4oem.new_uint16Array(n) - for i, sample in enumerate(active_groups_mask_list): - _ius4oem.uint16Array_setitem(array, i, sample) - - _ius4oem.setActiveChannelGroupCustom(self.card_handle, array, n, firing) - finally: - _ius4oem.delete_uint16Array(array) - - @assert_card_is_powered_up - def set_tx_delays(self, delays, firing: int=0): - """ - Sets channel's TX delays. - - :param delays: an array of delays to set [s], number of elements - should be equal to the number of module's TX channels - :param firing: a firing, in which the delay should apply, **starts from 0** - :return: an array of values, which were set [s] - """ - if isinstance(delays, np.ndarray): - if len(delays.shape) != 1: - raise ValueError("Delays should be a vector.") - if delays.shape[0] != self.get_n_tx_channels(): - raise ValueError("You should provide %d delays." % self.get_n_tx_channels()) - delays = delays.tolist() - _utils._assert_equal( - len(np.squeeze(delays)), self.get_n_tx_channels(), - desc="Array of TX delays should contain %d numbers (card number of TX channels)" - % self.get_n_tx_channels() - ) - self.log(DEBUG, "Setting TX delays: %s" % (delays)) - result_values = [] - for i, delay in enumerate(delays): - value = self.card_handle.SetTxDelay(i, delay, firing) - result_values.append(value) - self.log(DEBUG, "Applied TX delays: %s" % (result_values)) - return result_values - - @assert_card_is_powered_up - def set_tx_delay(self, channel: int, delay: float, firing: int=0): - """ - Sets channel's TX delay. - - :param channel: card's channel number - :param delay: delay to set [s] - :param firing: a firing, in which the delay should apply, **starts from 0** - :return: a value, which was set [s] - """ - _utils.assert_true( - channel < self.get_n_tx_channels(), - desc="Channel number cannot exceed number of available TX channels (%d)" - % self.get_n_tx_channels() - ) - self.log(DEBUG, "Setting TX delay: channel=%d, delay=%d" % (channel, delay)) - value = self.card_handle.SetTxDelay(channel, delay, firing) - return value - - @assert_card_is_powered_up - def set_rx_delay(self, delay: float, firing: int=0): - """ - Sets the starting point of the acquisition time [s] - - :param delay: expected acquisition time starting point relative to trigger [s] - :param firing: an firing, in which the parameter value should apply, **starts from 0** - """ - self.log(DEBUG, "Setting RX delay = %f for firing %d" % (delay, firing)) - self.card_handle.SetRxDelay(delay=delay, firing=firing) - - @assert_card_is_powered_up - def set_tx_frequency(self, frequency: float, firing: int=0): - """ - Sets TX frequency. - - :param frequency: frequency to set [Hz] - :param firing: a firing, in which the value should apply, **starts from 0** - :return: a value, which was set [Hz] - """ - self.log( - DEBUG, - "Setting TX frequency: %f" % frequency - ) - self.card_handle.SetTxFreqency(frequency, firing) - - @assert_card_is_powered_up - def set_tx_half_periods(self, n_half_periods: int, firing: int=0): - """ - Sets number of TX signal half-periods. - - :param n_half_periods: number of half-periods to set - :param firing: a firing, in which the parameter value should apply, **starts from 0** - :return: an exact number of half-periods that has been set on a given module - """ - self.log( - DEBUG, - "Setting number of half periods: %f" % n_half_periods - ) - return self.card_handle.SetTxHalfPeriods(n_half_periods, firing) - - @assert_card_is_powered_up - def sw_trigger(self): - """ - Triggers pulse generation and starts RX transmissions on all - (master and slave) modules. Should be called only for a master module. - """ - self.card_handle.SWTrigger() - - @assert_card_is_powered_up - def set_rx_time(self, time: float, firing: int=0): - """ - Sets length of acquisition time. - - :param time: expected acquisition time [s] - :param firing: a firing, in which the parameter value should apply, starts from 0 - """ - self.log( - DEBUG, - "Setting RX time: %f" % time - ) - self.card_handle.SetRxTime(time, firing) - - @assert_card_is_powered_up - def set_number_of_firings(self, n_firings: int=1): - """ - Sets number firings/acquisitions for new TX/RX sequence. - For each firing/acquisition a different TX/RX parameters can be applied. - - :param n_firings: number of firings to set - """ - self.log( - DEBUG, - "Setting number of firings: %d" % n_firings - ) - self.card_handle.SetNumberOfFirings(n_firings) - - @assert_card_is_powered_up - def sw_next_tx(self): - """ - Sets all TX and RX parameters for the next firing/acquisition. - """ - self.card_handle.SWNextTX() - - @assert_card_is_powered_up - def enable_receive(self): - """ - Enables RX data transfer from the probe’s adapter to the module’s internal memory. - """ - _ius4oem.EnableReceiveDelayed(self.card_handle) - - @assert_card_is_powered_up - def enable_transmit(self): - """ - Enables TX pulse generation. - """ - self.card_handle.EnableTransmit() - - @assert_card_is_powered_up - def schedule_receive(self, address, length, - start=0, decimation=0, callback=None): - """ - Schedules a new data transmission from the probe’s adapter to the module’s internal memory. - This function queues a new data transmission from all available RX channels to the device’s internal memory. - Data transfer starts with the next “SWTrigger” operation call. - - :param address: module's internal memory address, counted in number of samples - :param length: number of samples from each channel to acquire - :param callback: a callback function to call when data become available. - :param start: acquisition start sample - :param decimation: decimation to apply - :param callback: a callback function to apply - """ - self.log( - DEBUG, - "Scheduling data receive at address=0x%02X, length=%d," - " decimation=%d, start=%d" % - (address, length, decimation, start) - ) - - address = address*self.dtype.itemsize*self.get_n_rx_channels() - length = self.dtype.itemsize*length*self.get_n_rx_channels() - if callback is None: - _ius4oem.ScheduleReceiveWithoutCallback( - self.card_handle, - address=address, - length=length, - start=start, - decimation=decimation - ) - else: - cbk = _callbacks.ScheduleReceiveCallback(callback) - self.callbacks.append(cbk) - _ius4oem.ScheduleReceiveWithCallback( - self.card_handle, - address=address, - length=length, - start=start, - decimation=decimation, - callback=cbk - ) - - @assert_card_is_powered_up - def set_pga_gain(self, gain): - """ - Configures programmable-gain amplifier (PGA). - - :param gain: gain to set, available values: 24, 30 [dB] - :return: - """ - self.log( - DEBUG, - "Setting PGA Gain: %d" % gain - ) - enum_value = self._convert_to_enum_value( - enum_name="PGA_GAIN", - value=gain, - unit="dB" - ) - self.card_handle.SetPGAGain(enum_value) - - @assert_card_is_powered_up - def set_lpf_cutoff(self, cutoff): - """ - Sets low-pass filter (LPF) cutoff frequency. - - :param cutoff: cutoff frequency to set, available values: - 10e6, 15e6, 20e6, 30e6, 35e6, 50e6 [Hz] - """ - self.log( - DEBUG, - "Setting LPF Cutoff: %d" % cutoff - ) - # TODO(pjarosik) dirty, SetLPFCutoff should take values in Hz - cutoff_mhz = int(cutoff/1e6) - _utils.assert_true( - math.isclose(cutoff_mhz*1e6, cutoff), - "Unavailable LPF cutoff value: %f" % cutoff - ) - enum_value = self._convert_to_enum_value( - enum_name="LPF_PROG", - value=cutoff_mhz, - unit="MHz" - ) - self.card_handle.SetLPFCutoff(enum_value) - - @assert_card_is_powered_up - def set_active_termination(self, active_termination): - """ - Sets active termination for this card. When active termination is None, - the property is disabled. - - :param active_termination: active termination, available values: 50, 100, - 200, 400; can be None - - """ - if active_termination is not None: - self.log( - DEBUG, - "Setting active termination: %d" % active_termination - ) - enum_value = self._convert_to_enum_value( - enum_name="GBL_ACTIVE_TERM", - value=active_termination, - ) - self.card_handle.SetActiveTermination( - endis=_ius4oem.ACTIVE_TERM_EN_ACTIVE_TERM_EN, - term=enum_value - ) - else: - self.log(DEBUG, "Disabling active termination.") - self.card_handle.SetActiveTermination( - endis=_ius4oem.ACTIVE_TERM_EN_ACTIVE_TERM_DIS, - # TODO(pjarosik) when disabled, what value should be set? - term=_ius4oem.GBL_ACTIVE_TERM_GBL_ACTIVE_TERM_50 - ) - - @assert_card_is_powered_up - def set_lna_gain(self, gain): - """ - Configures low-noise amplifier (LNA) gain. - - :param gain: gain to set; available values: 12, 18, 24 [dB] - """ - - self.log( - DEBUG, - "Setting LNA Gain: %d" % gain - ) - enum_value = self._convert_to_enum_value( - enum_name="LNA_GAIN_GBL", - value=gain, - unit="dB" - ) - self.card_handle.SetLNAGain(enum_value) - - @assert_card_is_powered_up - def set_dtgc(self, attenuation): - """ - Configures time gain compensation (TGC). When attenuation is None, - DTGC is set to disabled. - - :param attenuation: attenuation to set, available values: 0, 6, 12, - 18, 24, 30, 36, 42 [dB]; can be None - """ - - if attenuation is not None: - self.log( - DEBUG, - "Setting DTGC: %d" % attenuation - ) - enum_value = self._convert_to_enum_value( - enum_name="DIG_TGC_ATTENUATION", - value=attenuation, - unit="dB" - ) - self.card_handle.SetDTGC( - endis=_ius4oem.EN_DIG_TGC_EN_DIG_TGC_EN, - att=enum_value - ) - else: - self.log(DEBUG, "Disabling DTGC") - self.card_handle.SetDTGC( - endis=_ius4oem.EN_DIG_TGC_EN_DIG_TGC_DIS, - att=_ius4oem.DIG_TGC_ATTENUATION_DIG_TGC_ATTENUATION_0dB - ) - - @assert_card_is_powered_up - def enable_tgc(self): - """ - Enables Time Gain Compensation (TGC). - """ - self.card_handle.TGCEnable() - self.log(DEBUG, "TGC enabled.") - - @assert_card_is_powered_up - def disable_tgc(self): - """ - Disables Time Gain Compensation (TGC). - """ - self.card_handle.TGCDisable() - self.log(DEBUG, "TGC disabled.") - - @assert_card_is_powered_up - def set_tgc_samples(self, samples): - """ - Sets TGC samples. - - :param samples: TGC samples to set, values from range [0, 1] are \ - available, 0 means minimum gain (maximum attenuation), 1 means \ - maximum gain. - """ - if isinstance(samples, np.ndarray): - if len(samples.shape) != 1: - raise ValueError("'Samples' should be a vector.") - samples = samples.tolist() - - if not (0.0 <= max(samples) <= 1.0): - raise ValueError("Samples should be in range [0, 1]") - - self.log(DEBUG, "Setting TGC samples: %s" % str(samples)) - n = len(samples) - array = None - try: - array = _ius4oem.new_doubleArray(n) - for i, sample in enumerate(samples): - _ius4oem.doubleArray_setitem(array, i, sample) - _ius4oem.setTGCSamplesCustom(self.card_handle, array, n, 0) - finally: - _ius4oem.delete_doubleArray(array) - - @assert_card_is_powered_up - def set_tx_invert(self, is_enable: bool, firing: int=0): - """ - Enables/disables inversion of TX signal. - - :param is_enable: should the inversion be enabled? - :param firing: a firing, in which the parameter value should apply - """ - if is_enable: - self.log(DEBUG, "Enabling inversion of TX signal.") - else: - self.log(DEBUG, "Disabling inversion of TX signal.") - self.card_handle.SetTxInvert(onoff=is_enable, firing=firing) - - @assert_card_is_powered_up - def set_tx_cw(self, is_enable: bool, firing: int=0): - """ - Enables/disables generation of long TX bursts. - - :param is_enable: should the generation of long TX bursts be enabled? - :param firing: a firing, in which the parameter value should apply - """ - if is_enable: - self.log(DEBUG, "Enabling generation of long TX bursts.") - else: - self.log(DEBUG, "Disabling geneartion of long TX bursts.") - self.card_handle.SetTxCw(onoff=is_enable, firing=firing) - - @assert_card_is_powered_up - def enable_test_patterns(self): - """ - Turns off probe’s RX data acquisition and turns on test patterns generation. - When test patterns are enabled, sawtooth signal is generated. - """ - self.log( - DEBUG, - "Enabling Test Patterns." - ) - self.card_handle.EnableTestPatterns() - - @assert_card_is_powered_up - def disable_test_patterns(self): - """ - Turns off test patterns generation and turns on probe’s RX data acquisition. - """ - self.log( - DEBUG, - "Disabling Test Patterns." - ) - self.card_handle.DisableTestPatterns() - - @assert_card_is_powered_up - def sync_test_patterns(self): - """ - Waits for update of test patterns. - """ - self.log( - DEBUG, - "Syncing with test patterns..." - ) - self.card_handle.SyncTestPatterns() - - @assert_card_is_powered_up - def transfer_rx_buffer_to_host(self, src_addr, length): - """ - Transfers data from the given module's memory address to the host's - memory, and returns data buffer (numpy.ndarray). - - **NOTE: This function returns a buffer which is managed internally by the module. - The content of the buffer may change between successive 'sw_trigger' function calls. - Please copy buffer's data to your own array before proceeding with the acquisition.** - - :param src_addr: module's memory address, where the RX data was stored. - :param length: how much data to transfer from each module's channel. - :return: a buffer (numpy.darray) of shape (length, n_rx_channels), data type: np.int16 - """ - required_nbytes = self.get_n_rx_channels()*length*self.dtype.itemsize - if self.host_buffer.nbytes != required_nbytes: - # Intentionally not '<' (we must return an array with given shape). - self.host_buffer = _utils.create_aligned_array( - (length, self.get_n_rx_channels()), - dtype=np.int16, - alignment=4096 - ) - dst_addr = self.host_buffer.ctypes.data - length = self.host_buffer.nbytes - self.log( - DEBUG, - "Transferring %d bytes from RX buffer at 0x%02X to host memory at 0x%02X..." % ( - length, src_addr, dst_addr - ) - ) - - if self.cfg is not None and self.cfg.log_transfer_time: - start_time = time.time() - _ius4oem.TransferRXBufferToHostLocation( - that=self.card_handle, - dstAddress=dst_addr, - length=length, - srcAddress=src_addr # address shift is applied by the low-level layer - ) - if self.cfg is not None and self.cfg.log_transfer_time: - end_time = time.time() - data_bytes = length - data_mbytes = data_bytes / 1e6 - elapsed = end_time - start_time - throughput = None if elapsed == 0.0 else data_mbytes / elapsed - msg = f"Transferred data {self.get_id()} -> PC: amount: " \ - f"{data_mbytes:.3f} MB in {elapsed:.3f} s" - if throughput is not None: - msg = msg + f", throughput: {throughput:.3f} MB/s" - self.log(INFO, msg) - - self.log( - DEBUG, - "... transferred." - ) - return self.host_buffer - - @assert_card_is_powered_up - def transfer_rx_buffer_to_host_buffer(self, src_addr, dst_buffer): - """ - Transfers data from the given module's memory address to the provided - host's buffer memory. - - The buffer's address should be aligned to 4096 - - :param src_addr: module's memory address, where the RX data was stored. - :param length: how much data to transfer from each module's channel. - :param dst_buffer: a buffer (numpy.darray) of shape (length, n_rx_channels), data type: np.int16 - """ - dst_addr = dst_buffer.ctypes.data - length = dst_buffer.nbytes - self.log(DEBUG, - "Transferring %d bytes from RX buffer at 0x%08X to host memory at 0x%08X..."%( - length, src_addr, dst_addr)) - - if self.cfg is not None and self.cfg.log_transfer_time: - start_time = time.time() - - _ius4oem.TransferRXBufferToHostLocation( - that=self.card_handle, - dstAddress=dst_addr, - length=length, - srcAddress=src_addr) - - if self.cfg is not None and self.cfg.log_transfer_time: - end_time = time.time() - data_bytes = length - data_mbytes = data_bytes / 1e6 - elapsed = end_time - start_time - throughput = None if elapsed == 0.0 else data_mbytes/elapsed - - msg = f"Transferred data {self.get_id()} -> PC: amount: "\ - f"{data_mbytes:.3f} MB in {elapsed:.3f} s" - if throughput is not None: - msg = msg + f", throughput: {throughput:.3f} MB/s" - self.log(INFO, msg) - - self.log(DEBUG, "... transferred.") - - def is_powered_down(self): - """ - Returns true, when module is turned off, false otherwise. - - :return: true, when module is turned off, false otherwise - """ - return self.card_handle.IsPowereddown() - - @assert_card_is_powered_up - def clear_scheduled_receive(self): - """ - Clears scheduled receive queue. - """ - self.log( - DEBUG, - "Clearing scheduled receive requests..." - ) - self.card_handle.ClearScheduledReceive() - - @assert_card_is_powered_up - def start_trigger(self): - """ - Starts generation of the hardware trigger. - """ - self.log( - DEBUG, - "Starting generation of the hardware trigger..." - ) - if self.pri_list is None and self.pri_total is None: - raise ValueError("Call 'set_n_triggers' first") - self.pri_total = sum(self.pri_list) - self.pri_list = None - self.card_handle.TriggerStart() - time.sleep(self.pri_total) - - @assert_card_is_powered_up - def stop_trigger(self): - """ - Stops generation of the hardware trigger. - """ - self.log( - DEBUG, - "Stops generation of the hardware trigger..." - ) - self.card_handle.TriggerStop() - - @assert_card_is_powered_up - def trigger_sync(self): - """ - Resumes generation of the hardware trigger. - """ - self.log( - DEBUG, - "Resuming generation of the hardware trigger..." - ) - if self.pri_total is None: - raise ValueError("Call 'trigger_start' first.") - self.card_handle.TriggerSync() - time.sleep(self.pri_total) - - @assert_card_is_powered_up - def set_n_triggers(self, n_triggers): - """ - Sets the number of trigger to be generated. - - :param n_triggers: number of triggers to set - - """ - self.log( - DEBUG, - "Setting number of triggers to generate to %d..." % n_triggers - ) - # TODO(pjarosik) trigger_sync should wait until all data is available - self.pri_total = None - self.pri_list = [0.0]*n_triggers - self.card_handle.SetNTriggers(n_triggers) - - @assert_card_is_powered_up - def set_trigger(self, - time_to_next_trigger: float, - time_to_next_tx: float=0.0, - is_sync_required: bool=False, - idx: int=0): - """ - Sets parameters of the trigger event. - Each trigger event will generate a trigger signal for the current - firing/acquisition and set next firing parameters. - - :param timeToNextTrigger: time between current and the next trigger [s] - :param timeToNextTx: delay between current trigger and setting next firing parameters [s] - :param syncReq: should the trigger generator pause and wait for the trigger_sync() call - :param idx: a firing, in which the parameters values should apply, **starts from 0** - """ - - #TODO(pjarosik) dirty, should be handled by an IUs4OEM implementation - time_to_next_trigger_us = int(time_to_next_trigger*1e6) - if not math.isclose(time_to_next_trigger_us/1e6, time_to_next_trigger): - raise RuntimeError( - "Numeric error when computing time to next trigger, " - "input value %.10f [s], value to set %.10f [us]." - %(time_to_next_trigger, time_to_next_trigger_us) - ) - - time_to_next_tx_us = int(time_to_next_tx*1e6) - if not math.isclose(time_to_next_tx_us/1e6, time_to_next_tx): - raise RuntimeError( - "Numeric error when computing time to next TX, " - "input value %.10f [s], value to set %.10f [us]." - %(time_to_next_tx, time_to_next_tx_us) - ) - - self.log( - DEBUG, - ("Setting trigger generation parameters to: " - "trigger number: %d, " - "time to next trigger: %f [s] (%d [us]), " - "time to next tx: %f [s] (%d [us]), " - "is sync required: %s") % - (idx, - time_to_next_trigger, time_to_next_trigger_us, - time_to_next_tx, time_to_next_tx_us, - str(is_sync_required)) - ) - self.card_handle.SetTrigger( - timeToNextTrigger=time_to_next_trigger_us, - timeToNextTx=time_to_next_tx, - syncReq=is_sync_required, - idx=idx - ) - self.pri_list[idx] = time_to_next_trigger - - def _convert_to_enum_value(self, enum_name, value, unit=""): - _utils.assert_true( - round(value) == value, - "Value %s for '%s' should be an integer value." % (value, enum_name) - ) - const_prefix = enum_name + "_" + enum_name - const_name = const_prefix + "_" + str(value) + unit - try: - return getattr(_ius4oem, const_name) - except AttributeError: - acceptable_values = set() - for key in dir(_ius4oem): - if key.startswith(const_prefix): - value_with_unit = key[len(const_prefix)+1:] - value = int("".join(list(filter(str.isdigit, value_with_unit)))) - acceptable_values.add(value) - if not acceptable_values: - raise RuntimeError("Invalid enum name: % s" % enum_name) - else: - raise ValueError( - "Invalid value for %s, should be one of the following: %s" % - (enum_name, str(acceptable_values)) - ) - diff --git a/api/python/arrus/devices/us4r.py b/api/python/arrus/devices/us4r.py new file mode 100644 index 000000000..056644db6 --- /dev/null +++ b/api/python/arrus/devices/us4r.py @@ -0,0 +1,322 @@ +import dataclasses +import numpy as np +import time +import ctypes +import collections.abc + +import arrus.utils.core +import arrus.logging +import arrus.core +from arrus.devices.device import Device, DeviceId, DeviceType +import arrus.exceptions +import arrus.devices.probe +import arrus.metadata +import arrus.kernels +import arrus.kernels.kernel +import arrus.kernels.tgc +import arrus.ops.tgc + + +DEVICE_TYPE = DeviceType("Us4R", arrus.core.DeviceType_Us4R) + + +@dataclasses.dataclass(frozen=True) +class FrameChannelMapping: + """ + Stores information how to get logical order of the data from + the physical order provided by the us4r device. + + :param frames: a mapping: (logical frame, logical channel) -> physical frame + :param channels: a mapping: (logical frame, logical channel) -> physical channel + """ + frames: np.ndarray + channels: np.ndarray + batch_size: int = 1 + + +class HostBuffer: + """ + Buffer storing data that comes from the us4r device. + + The buffer is implemented as a circular queue. The consumer gets data from + the queue's tails (end), the producer puts new data at the queue's head + (firt element of the queue). + + This class provides an access to the queue's tail only. The user + can access the latest data produced by the device by accessing `tail()` + function. To release the tail data that is not needed anymore the user + can call `release_tail()` function. + """ + + def __init__(self, buffer_handle, + fac: arrus.metadata.FrameAcquisitionContext, + data_description: arrus.metadata.EchoDataDescription, + frame_shape: tuple, + rx_batch_size: int): + self.buffer_handle = buffer_handle + self.fac = fac + self.data_description = data_description + self.frame_shape = frame_shape + self.buffer_cache = {} + self.frame_metadata_cache = {} + # Required to determine time step between frame metadata positions. + self.n_samples = fac.raw_sequence.get_n_samples() + if len(self.n_samples) > 1: + raise RuntimeError + self.n_samples = next(iter(self.n_samples)) + # FIXME This won't work when the the rx aperture has to be splitted to multiple operations + # Currently works for rx aperture <= 64 elements + self.n_triggers = self.data_description.custom["frame_channel_mapping"].frames.shape[0] + self.rx_batch_size = rx_batch_size + + def tail(self, timeout=None): + """ + Returns data available at the tail of the buffer. + + :param timeout: timeout in milliseconds, None means infinite timeout + :return: a pair: RF data, metadata + """ + data_addr = self.buffer_handle.tailAddress( + -1 if timeout is None else timeout) + if data_addr not in self.buffer_cache: + array = self._create_array(data_addr) + frame_metadata_view = array[:self.n_samples*self.n_triggers*self.rx_batch_size:self.n_samples] + self.buffer_cache[data_addr] = array + self.frame_metadata_cache[data_addr] = frame_metadata_view + else: + array = self.buffer_cache[data_addr] + frame_metadata_view = self.frame_metadata_cache[data_addr] + metadata = arrus.metadata.Metadata( + context=self.fac, + data_desc=self.data_description, + custom={"frame_metadata_view": frame_metadata_view} + ) + return array, metadata + + def head(self, timeout=None): + """ + Returns data available at the tail of the buffer. + + :param timeout: timeout in milliseconds, None means infinite timeout + :return: a pair: RF data, metadata + """ + data_addr = self.buffer_handle.headAddress( + -1 if timeout is None else timeout) + if data_addr not in self.buffer_cache: + array = self._create_array(data_addr) + frame_metadata_view = array[:self.n_samples*self.n_triggers*self.rx_batch_size:self.n_samples] + self.buffer_cache[data_addr] = array + self.frame_metadata_cache[data_addr] = frame_metadata_view + else: + array = self.buffer_cache[data_addr] + frame_metadata_view = self.frame_metadata_cache[data_addr] + # TODO extract first lines from each frame lazily + metadata = arrus.metadata.Metadata( + context=self.fac, + data_desc=self.data_description, + custom={"frame_metadata_view": frame_metadata_view} + ) + return array, metadata + + def release_tail(self, timeout=None): + """ + Marks the tail data as no longer needed. + + :param timeout: timeout in milliseconds, None means infinite timeout + """ + self.buffer_handle.releaseTail(-1 if timeout is None else timeout) + + def _create_array(self, addr): + ctypes_ptr = ctypes.cast(addr, ctypes.POINTER(ctypes.c_int16)) + arr = np.ctypeslib.as_array(ctypes_ptr, shape=self.frame_shape) + return arr + + def get_n_elements(self): + return self.buffer_handle.getNumberOfElements() + + def get_element(self, i): + return self.buffer_handle.getElementAddress(i) + + def get_element_size(self): + return self.buffer_handle.getElementSize() + + +class Us4R(Device): + """ + A handle to Us4R device. + + Wraps an access to arrus.core.Us4R object. + """ + + def __init__(self, handle, parent_session): + super().__init__() + self._handle = handle + self._session = parent_session + self._device_id = DeviceId(DEVICE_TYPE, + self._handle.getDeviceId().getOrdinal()) + # Context for the currently running sequence. + self._current_sequence_context = None + + def get_device_id(self): + return self._device_id + + def set_tgc(self, tgc_curve): + """ + Sets TGC samples for given TGC description. + + :param samples: a given TGC to set. + """ + if isinstance(tgc_curve, arrus.ops.tgc.LinearTgc): + if self._current_sequence_context is None: + raise ValueError("There is no tx/rx sequence currently " + "uploaded.") + tgc_curve = arrus.kernels.tgc.compute_linear_tgc( + self._current_sequence_context, tgc_curve) + else: + raise ValueError(f"Unrecognized tgc type: {type(tgc_curve)}") + self._handle.setTgcCurve(list(tgc_curve)) + + def set_hv_voltage(self, voltage): + """ + Enables HV and sets a given voltage. + + :param voltage: voltage to set + """ + self._handle.setVoltage(voltage) + + def start(self): + """ + Starts uploaded tx/rx sequence execution. + """ + self._handle.start() + + def stop(self): + """ + Stops tx/rx sequence execution. + """ + self._handle.stop() + + @property + def sampling_frequency(self): + """ + Device sampling frequency [Hz]. + """ + # TODO use sampling frequency from the us4r device + return 65e6 + + def upload(self, seq: arrus.ops.Operation, + rx_buffer_size=2, host_buffer_size=2, + rx_batch_size=1) -> HostBuffer: + """ + Uploads a given sequence of operations to perform on the device. + + The host buffer returns Frame Channel Mapping in frame + acquisition context custom data dictionary. + + :param seq: sequence to set + :param mode: mode to set (str), available values: "sync", "async" + :param rx_buffer_size: the size of the buffer to set on the Us4R device, + should be None for "sync" version (value 2 will be used) + :param host_buffer_size: the size of the buffer to create on the host + computer, should be None for "sync" version (value 2 will be used) + :param frame_repetition_interval: the expected time between successive + frame acquisitions to set, should be None for "sync" version. None + value means that no interval should be set + :param rx_batch_size: number of RF frames that should be acquired in a single run + :raises: ValueError when some of the input parameters are invalid + :return: a data buffer + """ + # Verify the input parameters. + if host_buffer_size % rx_batch_size != 0: + raise ValueError("Host buffer size should be a multiple " + "of rx batch size.") + + host_buffer_size = host_buffer_size // rx_batch_size + + # Prepare sequence to load + kernel_context = self._create_kernel_context(seq) + self._current_sequence_context = kernel_context + raw_seq = arrus.kernels.get_kernel(type(seq))(kernel_context) + core_seq = arrus.utils.core.convert_to_core_sequence(raw_seq) + + upload_result = self._handle.upload(core_seq, rx_buffer_size, host_buffer_size) + + # Prepare data buffer and constant context metadata + buffer_handle, fcm = upload_result[0], upload_result[1] + + # -- Constant metadata + # --- FCM + fcm_frame, fcm_channel = arrus.utils.core.convert_fcm_to_np_arrays(fcm) + fcm = FrameChannelMapping(frames=fcm_frame, channels=fcm_channel, + batch_size=rx_batch_size) + + # --- Frame acquisition context + fac = self._create_frame_acquisition_context(seq, raw_seq) + echo_data_description = self._create_data_description(raw_seq, fcm) + + # --- Data buffer + n_samples = raw_seq.get_n_samples() + + if len(n_samples) > 1: + raise arrus.exceptions.IllegalArgumentError( + "Currently only a sequence with constant number of samples " + "can be accepted.") + + n_samples = next(iter(n_samples)) + input_shape = self._get_physical_frame_shape(fcm, n_samples, + rx_batch_size=rx_batch_size) + + buffer = HostBuffer( + buffer_handle=buffer_handle, + fac=fac, + data_description=echo_data_description, + frame_shape=input_shape, + rx_batch_size=rx_batch_size) + + const_metadata = arrus.metadata.ConstMetadata( + context=fac, data_desc=echo_data_description, + input_shape=input_shape, is_iq_data=False, dtype="int16") + return buffer, const_metadata + + + def _create_kernel_context(self, seq): + return arrus.kernels.kernel.KernelExecutionContext( + device=self._get_dto(), + medium=self._session.get_session_context().medium, + op=seq, custom={}) + + def _create_frame_acquisition_context(self, seq, raw_seq): + return arrus.metadata.FrameAcquisitionContext( + device=self._get_dto(), sequence=seq, raw_sequence=raw_seq, + medium=self._session.get_session_context().medium, + custom_data={}) + + def _create_data_description(self, raw_seq, fcm): + return arrus.metadata.EchoDataDescription( + sampling_frequency=self.sampling_frequency / + raw_seq.ops[0].rx.downsampling_factor, + custom={"frame_channel_mapping": fcm} + ) + + def _get_physical_frame_shape(self, fcm, n_samples, n_channels=32, + rx_batch_size=1): + # TODO: We assume here, that each frame has the same number of samples! + # This might not be case in further improvements. + n_frames = np.max(fcm.frames) + 1 + return n_frames * n_samples * rx_batch_size, n_channels + + def _get_dto(self): + probe_model = arrus.utils.core.convert_to_py_probe_model( + core_model=self._handle.getProbe(0).getModel()) + probe_dto = arrus.devices.probe.ProbeDTO(model=probe_model) + return Us4RDTO(probe=probe_dto, sampling_frequency=65e6) + + +# ------------------------------------------ LEGACY MOCK +@dataclasses.dataclass(frozen=True) +class Us4RDTO(arrus.devices.device.UltrasoundDeviceDTO): + probe: arrus.devices.probe.ProbeDTO + sampling_frequency: float + + def get_id(self): + return "Us4R:0" diff --git a/api/python/arrus/exceptions.py b/api/python/arrus/exceptions.py new file mode 100644 index 000000000..18a184dcf --- /dev/null +++ b/api/python/arrus/exceptions.py @@ -0,0 +1,23 @@ +""" +Definition of exceptions thrown by arrus functions. +""" + + +class ArrusError(Exception): + pass + + +class IllegalArgumentError(ArrusError, ValueError): + pass + + +class DeviceNotFoundError(ArrusError, ValueError): + pass + + +class IllegalStateError(ArrusError, RuntimeError): + pass + + +class TimeoutError(ArrusError, TimeoutError): + pass \ No newline at end of file diff --git a/api/python/arrus/interface.py b/api/python/arrus/interface.py deleted file mode 100644 index 516fad519..000000000 --- a/api/python/arrus/interface.py +++ /dev/null @@ -1,148 +0,0 @@ -import itertools -from typing import Set - - -class UltrasoundInterface: - """ - Defines an interface provided by (possibly) multiple us4oem's. - """ - # TODO(pjarosik) inverse mapping - def get_card_order(self): - # TODO(pjarosik) deprecated - raise NotImplementedError - - def get_tx_channel_mapping(self, module_idx): - """ - Returns TX channel mapping for a module with given index. - - :param module_idx: module index - :return: TX channel mapping: a list, where list[probe's channel] = us4oem channel. - """ - raise NotImplementedError - - def get_rx_channel_mapping(self, module_idx): - """ - Returns RX channel mapping for a module with given index. - - :param module_idx: module index - :return: RX channel mapping: a list, where list[probe's channel] = us4oem channel. - """ - raise NotImplementedError - - -class EsaoteInterface(UltrasoundInterface): - - def __init__(self): - self.cards = (0, 1) - # A list of lists; - # for each list, list[interface channel] = us4oem channel. - self.tx_card_channels = self._compute_tx_channel_mapping() - self.rx_card_channels = self._compute_rx_channel_mapping() - - def get_card_order(self): - return self.cards - - def get_tx_channel_mappings(self): - return self.tx_card_channels - - def get_rx_channel_mappings(self): - return self.rx_card_channels - - def get_tx_channel_mapping(self, module_idx): - return self.tx_card_channels[module_idx] - - def get_rx_channel_mapping(self, module_idx): - return self.rx_card_channels[module_idx] - - def _compute_tx_channel_mapping(self): - block_size = 32 - card0_mapping = ( - self._get_esaote_block_mapping(i, block_size) - for i in range(0, 128, block_size) - ) - card0_mapping = list(itertools.chain.from_iterable(card0_mapping)) - card1_mapping = list(range(0, 128)) - return [ - card0_mapping, - card1_mapping - ] - - def _compute_rx_channel_mapping(self): - card0_mapping = self._get_esaote_block_mapping(0, 32) - card1_mapping = list(range(0, 32)) - return [ - card0_mapping, - card1_mapping - ] - - @staticmethod - def _get_esaote_block_mapping(origin, block_size): - block = list(reversed(range(origin, origin+block_size))) - block[block_size//2-1] = origin+block_size//2-1 - block[block_size//2] = origin+block_size//2 - return block - - -class UltrasonixInterface(UltrasoundInterface): - def __init__(self): - self.cards = (0, 1) - # A list of lists; - # for each list, list[interface channel] = us4oem channel. - self.tx_card_channels = self._compute_tx_channel_mapping() - self.rx_card_channels = self._compute_rx_channel_mapping() - - def get_card_order(self): - return self.cards - - def get_tx_channel_mappings(self): - return self.tx_card_channels - - def get_rx_channel_mappings(self): - return self.rx_card_channels - - def get_tx_channel_mapping(self, module_idx): - return self.tx_card_channels[module_idx] - - def get_rx_channel_mapping(self, module_idx): - return self.rx_card_channels[module_idx] - - def _compute_tx_channel_mapping(self): - block_size = 32 - card0_mapping = list(range(0, 128)) - card1_mapping = ( - self._get_ultrasonix_block_mapping(i, block_size) - for i in range(0, 128, block_size) - ) - card1_mapping = list(itertools.chain.from_iterable(card1_mapping)) - return [ - card0_mapping, - card1_mapping - ] - def _compute_rx_channel_mapping(self): - card0_mapping = list(range(0, 32)) - card1_mapping = self._get_ultrasonix_block_mapping(0, 32) - return [ - card0_mapping, - card1_mapping - ] - - @staticmethod - def _get_ultrasonix_block_mapping(origin, block_size): - block = list(reversed(range(origin, origin+block_size))) - return block - - -_INTERFACES = { - 'esaote': EsaoteInterface(), - 'ultrasonix': UltrasonixInterface() -} - -def get_interface(name: str): - """ - Returns an interface registered in arrus under given name. - - :param name: name to the interface - :return: an interface object - """ - return _INTERFACES[name] - diff --git a/api/python/arrus/kernels.py b/api/python/arrus/kernels.py deleted file mode 100644 index 8e8ad4af9..000000000 --- a/api/python/arrus/kernels.py +++ /dev/null @@ -1,461 +0,0 @@ -import abc -import numpy as np -from queue import Queue -import logging -from logging import DEBUG, INFO -import time -import arrus.ops as _operations -import arrus.params as _params -import arrus.devices.us4oem as _us4oem -import arrus.devices.hv256 as _hv256 -import arrus.utils as _utils -import arrus.validation as _validation - -_logger = logging.getLogger(__name__) - - -class Kernel(abc.ABC): - - @abc.abstractmethod - def run(self): - pass - - -class LoadableKernel(Kernel): - - @abc.abstractmethod - def validate(self): - pass - - @abc.abstractmethod - def load(self): - pass - - @abc.abstractmethod - def run_loaded(self): - pass - - def run(self): - self.validate() - self.load() - return self.run_loaded() - - -class AsyncKernel(abc.ABC): - - @abc.abstractmethod - def stop(self): - pass - - -def get_kernel(operation: _operations.Operation, feed_dict: dict): - device = feed_dict.get("device", None) - _validation.assert_not_none(device, "device") - - # Get kernels dictionary for given device. - device_type = type(device) - kernel_registries = { - _us4oem.Us4OEM: get_us4oem_kernel_registry(), - _hv256.HV256: get_hv256_kernel_registry() - } - if device_type not in kernel_registries: - raise ValueError("Unsupported device type: %s" % device_type) - kernel_registry = kernel_registries[device_type] - - # Get operation's kernel from the registry. - operation_type = type(operation) - if operation_type not in kernel_registry: - raise ValueError("Unsupported operation '%s' for device type: %s."% - (operation_type, device_type)) - kernel = kernel_registry[operation_type](operation, device, feed_dict) - return kernel - - -def get_us4oem_kernel_registry(): - return { - _operations.TxRx: TxRxModuleKernel, - _operations.Sequence: SequenceModuleKernel, - _operations.Loop: LoopModuleKernel - } - - -def get_hv256_kernel_registry(): - return { - _operations.SetHVVoltage: SetHVVoltageKernel, - _operations.DisableHVVoltage: DisableHVVoltageKernel, - } - - -# TODO(pjarosik) implement this class as a sequence with single txrx -class TxRxModuleKernel(LoadableKernel): - - def __init__(self, op: _operations.TxRx, device: _us4oem.Us4OEM, - feed_dict: dict, data_offset=0, sync_required=True, - callback=None, set_one_operation=True, firing=0): - self.op = op - self.device = device - self.feed_dict = feed_dict - self.data_offset = data_offset - self._sync_required = sync_required - self._set_one_operation = set_one_operation - if set_one_operation: - self._callback = self._default_callback - self._queue = Queue() - else: - self._callback = callback - self.firing = firing - self._tx_aperture_mask = self._get_aperture_mask(op.tx.aperture, device) - self._rx_aperture_mask = self._get_aperture_mask(op.rx.aperture, device) - - def _default_callback(self, e): - result_buffer = _utils.create_aligned_array( - (self.op.rx.n_samples, - self.device.get_n_rx_channels()), - dtype=np.int16, - alignment=4096 - ) - self.device.transfer_rx_buffer_to_host_buffer(0, result_buffer) - self._queue.put(result_buffer) - - def load(self): - if self._set_one_operation: - self.device.clear_scheduled_receive() - self.device.set_n_triggers(1) - self.device.set_number_of_firings(1) - self.load_with_sync_option(sync_required=self._sync_required, - callback=self._callback) - - def load_with_sync_option(self, sync_required: bool, callback): - op = self.op - device = self.device - firing = self.firing - # Tx - tx_delays = np.zeros(device.get_n_tx_channels()) - if op.tx.delays is not None: - tx_delays[np.where(self._tx_aperture_mask)] = op.tx.delays - device.set_tx_delays(delays=tx_delays, firing=firing) - # Excitation - wave = op.tx.excitation - device.set_tx_frequency(frequency=wave.frequency, firing=firing) - device.set_tx_half_periods(n_half_periods=int(wave.n_periods*2), - firing=firing) - device.set_tx_invert(is_enable=wave.inverse, firing=firing) - # Aperture - device.set_tx_aperture_mask(aperture=self._tx_aperture_mask, - firing=firing) - device.set_active_channel_group( - self.device._default_active_channel_groups, - firing=firing) - # RX - # Aperture - device.set_rx_aperture_mask(aperture=self._rx_aperture_mask, - firing=firing) - # Samples, rx time, delay - n_samples = op.rx.n_samples - device.set_rx_time(time=op.rx.rx_time, firing=firing) - device.set_rx_delay(delay=op.rx.rx_delay, firing=firing) - device.schedule_receive(address=self.data_offset, - length=n_samples, - decimation=op.rx.fs_divider-1, - callback=callback) - device.set_trigger(time_to_next_trigger=op.tx.pri, time_to_next_tx=0, - is_sync_required=sync_required, idx=firing) - # Intentionally zeroing pri total - we use only interrupt based - # communication. - device.pri_total = 0 - - @staticmethod - def _get_aperture_mask(aperture, device: _us4oem.Us4OEM): - if isinstance(aperture, _params.MaskAperture): - return aperture.mask.astype(bool).astype(int) - elif isinstance(aperture, _params.RegionBasedAperture): - mask = np.zeros(device.get_n_channels()).astype(bool) - origin = aperture.origin - size = aperture.size - mask[origin:origin+size] = True - return mask.astype(int) - elif isinstance(aperture, _params.SingleElementAperture): - mask = np.zeros(device.get_n_channels()).astype(bool) - _validation.assert_in_range(aperture.element, - (0, device.get_n_channels()), - "single element aperture") - mask[aperture.element] = True - return mask.astype(int) - else: - raise ValueError("Unsupported aperture type: %s" % type(aperture)) - - def validate(self): - device = self.device - # TX - tx_aperture = self.op.tx.aperture - self._validate_aperture(tx_aperture, device.get_n_channels(), "tx") - # Delays: - number_of_active_elements = np.sum(self._tx_aperture_mask.astype(bool)) - if self.op.tx.delays is not None: - _validation.assert_shape(self.op.tx.delays, - (number_of_active_elements,), - parameter_name="tx.delays") - - # Excitation: - excitation = self.op.tx.excitation - _validation.assert_type(excitation, _params.SineWave, "tx.excitation") - _validation.assert_in_range(excitation.frequency, (1e6, 10e6), - "tx.excitation.frequency") - _validation.assert_in_range(excitation.n_periods, (1, 20), - "tx.excitation.n_periods") - n_periods_rem = excitation.n_periods % 1 - if n_periods_rem not in {0.0, 0.5}: - raise _validation.InvalidParameterError( - "tx.excitation.n_periods", - "currently Us4OEM supports full or half periods only." - ) - - # Triggers: - _validation.assert_in_range(self.op.tx.pri, (100e-6, 2000e-6), - "tx.pri") - # RX - rx_aperture = self.op.rx.aperture - self._validate_aperture(rx_aperture, device.get_n_channels(), "rx") - rx_aperture_mask = self._rx_aperture_mask.astype(bool) - _validation.assert_in_range(np.sum(rx_aperture_mask), (0, 32), - "rx.aperture number of channels") - _validation.assert_in_range(self.op.rx.n_samples, - (0, 65536), "rx.n_samples") - _validation.assert_in_range(self.op.rx.fs_divider, - (0, 4), "rx decimation") - - expected_rx_time = self.op.rx.n_samples\ - * self.op.rx.fs_divider\ - / self.device.get_sampling_frequency()\ - + 5e-6 # epsilon - if self.op.rx.rx_time < expected_rx_time: - raise _validation.InvalidParameterError( - "rx time", f"should be greater than {expected_rx_time}, " - f"that is, the minimal time to acquire " - f"given number of samples assuming given " - f"sampling frequency.") - - if self.op.rx.rx_time >= self.op.tx.pri: - raise _validation.InvalidParameterError( - "rx time, pri", f"Rx time {self.op.rx.rx_time} should be " - f"shorter than PRI {self.op.tx.pri}.") - - def _validate_aperture(self, aperture, n_channels, aperture_type): - # TODO(pjarosik) this validation should be performed before creating - # aperture masks - if isinstance(aperture, _params.MaskAperture): - expected_shape = (n_channels,) - _validation.assert_shape(aperture.mask, expected_shape, - "%s.aperture" % aperture_type) - elif isinstance(aperture, _params.RegionBasedAperture): - start = aperture.origin - end = aperture.origin + aperture.size - _validation.assert_in_range((start, end), (0, n_channels), - parameter_name="%s.aperture" % - aperture_type) - elif isinstance(aperture, _params.SingleElementAperture): - channel = aperture.element - _validation.assert_in_range((channel, channel), (0, n_channels), - parameter_name="%s.aperture" % - aperture_type) - else: - raise ValueError("Unsupported aperture type: %s" % type(aperture)) - - def run_loaded(self): - self.device.enable_transmit() - self.device.start_trigger() - self.device.enable_receive() - result_buffer = self._queue.get(timeout=20) - self.device.stop_trigger() - return result_buffer - - def get_total_n_samples(self): - return self.op.rx.n_samples - - -class SequenceModuleKernel(LoadableKernel): - - def __init__(self, op: _operations.Sequence, device: _us4oem.Us4OEM, - feed_dict: dict, sync_required=True, callback=None): - self.op = op - self.device = device - self.feed_dict = feed_dict - self.total_n_samples = self._compute_total_n_samples() - self.sync_required = sync_required - if callback is None: - self.callback = self._default_callback - self._queue = Queue() - else: - self.callback = callback - self._kernels = self._get_txrx_kernels(self.op.operations, - self.feed_dict, - self.device, sync_required, - self.callback) - - def _default_callback(self, e): - _logger.log(DEBUG, "Running default callback") - result_buffer = _utils.create_aligned_array( - (self.total_n_samples, self.device.get_n_rx_channels()), - dtype=np.int16, - alignment=4096 - ) - self.device.transfer_rx_buffer_to_host_buffer(0, result_buffer) - self._queue.put(result_buffer) - - def validate(self): - seq = self.op - _validation.assert_not_greater_than(len(seq.operations), 1024, - "number of operations in sequence") - for tx_rx in self._kernels: - tx_rx.validate() - - def load(self): - self.device.clear_scheduled_receive() - self.device.set_n_triggers(len(self.op.operations)) - self.device.set_number_of_firings(len(self.op.operations)) - for i, kernel in enumerate(self._kernels): - _logger.log(DEBUG, f"Loading {i} TxRx.") - kernel.load() - - def run_loaded(self): - self.device.enable_transmit() - self.device.start_trigger() - self.device.enable_receive() - self.device.trigger_sync() - result_buffer = self._queue.get(timeout=20) - self.device.stop_trigger() - return result_buffer - - def get_total_n_samples(self): - return self.total_n_samples - - def _compute_total_n_samples(self): - total_n_samples = 0 - for tx_rx in self.op.operations: - total_n_samples += tx_rx.rx.n_samples - return total_n_samples - - def _get_txrx_kernels(self, operations: _operations.TxRx, - feed_dict, - device, - sync_required, - callback): - tx_rx_kernels = [] - data_offset = 0 - n_operations = len(operations) - for i, tx_rx in enumerate(operations): - sync = i == n_operations-1 and sync_required - if i == n_operations-1: - cb = callback - else: - cb = None - kernel = TxRxModuleKernel(op=tx_rx, device=device, - feed_dict=feed_dict, - data_offset=data_offset, - sync_required=sync, - set_one_operation=False, - callback=cb, firing=i) - tx_rx_kernels.append(kernel) - data_offset += tx_rx.rx.n_samples - return tx_rx_kernels - - -class LoopModuleKernel(LoadableKernel, AsyncKernel): - - def __init__(self, op: _operations.Loop, device: _us4oem.Us4OEM, - feed_dict: dict): - self.op = op - self.device = device - self.feed_dict = feed_dict - self._data_buffer = self._create_data_buffer(self.op.operation) - self._callback = self._create_callback(feed_dict, self._data_buffer) - self._kernel = self._create_kernel(self.op.operation, self.device, - self.feed_dict, self._callback) - - def validate(self): - self._kernel.validate() - - def load(self): - self._kernel.load() - - def run_loaded(self): - self.device.enable_transmit() - self.device.start_trigger() - self.device.enable_receive() - self.device.trigger_sync() - return None - - def stop(self): - self.device.stop_trigger() - _logger.log(INFO, "Waiting for module to stop %s..." % - self.device.get_id()) - time.sleep(5) - _logger.log(INFO, "... module stopped.") - - def _create_kernel(self, op, device, feed_dict, callback): - if isinstance(op, _operations.TxRx): - return TxRxModuleKernel(op, device, feed_dict, sync_required=True, - callback=callback) - elif isinstance(op, _operations.Sequence): - return SequenceModuleKernel(op, device, feed_dict, - sync_required=True, callback=callback) - else: - raise ValueError("Invalid type of operation to perform in loop, " - "should be one of: %s, %s" % (_operations.TxRx, - _operations.Sequence) - ) - - def _create_data_buffer(self, op): - if isinstance(op, _operations.TxRx): - n_samples = _operations.TxRx.rx.n_samples - elif isinstance(op, _operations.Sequence): - n_samples = sum([txrx.rx.n_samples for txrx in op.operations]) - else: - raise ValueError() - return _utils.create_aligned_array( - (n_samples, self.device.get_n_rx_channels()), - dtype=np.int16, - alignment=4096 - ) - - def _create_callback(self, feed_dict, data_buffer): - cb = feed_dict.get("callback", None) - _validation.assert_not_none(cb, "callback") - - def _callback_wrapper(event): - # GIL - self.device.transfer_rx_buffer_to_host_buffer(0, data_buffer) - callback_result = cb(data_buffer) - if callback_result: - self.device.enable_receive() - self.device.trigger_sync() - # End GIL - - return _callback_wrapper - - -class SetHVVoltageKernel(Kernel): - - def __init__(self, op: _operations.SetHVVoltage, device: _hv256.HV256, - feed_dict: dict): - self.op = op - self.hv256 = device - self.feed_dict = feed_dict - - def run(self): - # TODO validate - self.hv256.enable_hv() - self.hv256.set_hv_voltage(self.op.voltage) - - -class DisableHVVoltageKernel(Kernel): - - def __init__(self, op: _operations.DisableHVVoltage, device: _hv256.HV256, - feed_dict: dict): - self.op = op - self.hv256 = device - self.feed_dict = feed_dict - - def run(self): - self.hv256.disable_hv() - diff --git a/api/python/arrus/kernels/__init__.py b/api/python/arrus/kernels/__init__.py new file mode 100644 index 000000000..4817cda1d --- /dev/null +++ b/api/python/arrus/kernels/__init__.py @@ -0,0 +1,30 @@ +import arrus.ops.imaging +import arrus.exceptions +from .imaging import ( + create_lin_sequence +) + + +def _identity_func(context): + return context.op + + +# TODO should depend on the device, currently us4r is supported only +_kernel_registry = { + arrus.ops.us4r.TxRxSequence: _identity_func, + arrus.ops.imaging.LinSequence: create_lin_sequence +} + + +def get_kernel(op_type): + if op_type not in _kernel_registry: + raise arrus.exceptions.IllegalArgumentError( + f"Operation {op_type} is not supported.") + return _kernel_registry[op_type] + + +def register_kernel(op_type, func): + if op_type in _kernel_registry: + raise arrus.exceptions.IllegalArgumentError(f"Kernel for op {op_type} " + f"already registered") + _kernel_registry[op_type] = func \ No newline at end of file diff --git a/api/python/arrus/kernels/imaging.py b/api/python/arrus/kernels/imaging.py new file mode 100644 index 000000000..cd2982199 --- /dev/null +++ b/api/python/arrus/kernels/imaging.py @@ -0,0 +1,203 @@ +import numpy as np +import arrus.exceptions +from arrus.ops.us4r import ( + Tx, Rx, TxRx, TxRxSequence, Pulse +) +from arrus.ops.tgc import LinearTgc +import arrus.utils.imaging +import arrus.kernels.tgc + + +def create_lin_sequence(context): + """ + The function creates list of TxRx objects describing classic scheme. + + :param context: KernelExecutionContext object + """ + # device parameters + n_elem = context.device.probe.model.n_elements + pitch = context.device.probe.model.pitch + # sequence parameters + op = context.op + + n_elem_sub = op.tx_aperture_size + focal_depth = op.tx_focus + sample_range = op.rx_sample_range + start_sample, end_sample = sample_range + pulse = op.pulse + downsampling_factor = op.downsampling_factor + pri = op.pri + sri = op.sri + fs = context.device.sampling_frequency/op.downsampling_factor + init_delay = op.init_delay + + tx_centers = np.array(op.tx_aperture_center_element) + tx_ap_size = op.tx_aperture_size + rx_centers = np.array(op.rx_aperture_center_element) + rx_ap_size = op.rx_aperture_size + if tx_centers.shape != rx_centers.shape: + raise arrus.exceptions.IllegalArgumentError( + "Tx and rx aperture center elements list should have the " + "same length") + tgc_start = op.tgc_start + tgc_slope = op.tgc_slope + + # medium parameters + c = op.speed_of_sound + if c is None: + c = context.medium.speed_of_sound + + tgc_curve = arrus.kernels.tgc.compute_linear_tgc( + context, + arrus.ops.tgc.LinearTgc(op.tgc_start, op.tgc_slope)) + + if np.mod(n_elem_sub, 2) == 0: + # Move focal position to the center of the floor(n_sub_elem/2) element + focus = [-pitch/2, focal_depth] + else: + focus = [0, focal_depth] + + def get_ap(center_element, size): + left_half_size = (size-1)//2 # e.g. size 32 -> 15, size 33 -> 16 + right_half_size = size//2 # e.g. size 32 -> 16, size 33 -> 16 + # left side: + origin = center_element-left_half_size # e.g. center 0 -> origin -15 + actual_origin = max(0, origin) + left_padding = abs(min(origin, 0)) # origin -15 -> left padding 15 + # right side + # aperture last element, e.g. center 0, size 32 -> 16 + end = center_element+right_half_size + actual_end = min(n_elem-1, end) + right_padding = abs(min(actual_end-end, 0)) + aperture = np.zeros((n_elem, ), dtype=np.bool) + aperture[actual_origin:(actual_end+1)] = True + return aperture, (left_padding, right_padding) + tx_apertures, tx_delays, tx_delays_center = compute_tx_parameters( + op, context.device.probe.model, c) + txrxs = [] + + if init_delay == "tx_start": + actual_sample_range = sample_range + elif init_delay == "tx_center": + n_periods = pulse.n_periods + fc = pulse.center_frequency + burst_factor = n_periods / (2 * fc) + delay = burst_factor + tx_delays_center + delay = delay*fs + actual_sample_range = tuple(int(round(v+delay)) for v in sample_range) + else: + raise ValueError(f"Unrecognized value '{init_delay}' for init_delay.") + + for tx_aperture, delays, rx_center in zip(tx_apertures, tx_delays, + rx_centers): + + if delays.shape == (1, 1): + delays = delays.reshape((1, )) + else: + delays = np.squeeze(delays) + + tx = Tx(tx_aperture, pulse, delays) + rx_aperture, rx_padding = get_ap(rx_center, rx_ap_size) + rx = Rx(rx_aperture, actual_sample_range, downsampling_factor, rx_padding) + txrxs.append(TxRx(tx, rx, pri)) + return TxRxSequence(txrxs, tgc_curve=tgc_curve, sri=sri) + + +def get_aperture_with_padding(center_element, size, probe_model): + n_elem = probe_model.n_elements + left_half_size = (size-1)//2 # e.g. size 32 -> 15, size 33 -> 16 + right_half_size = size//2 # e.g. size 32 -> 16, size 33 -> 16 + # left side: + origin = center_element-left_half_size # e.g. center 0 -> origin -15 + actual_origin = max(0, origin) + left_padding = abs(min(origin, 0)) # origin -15 -> left padding 15 + # right side + # aperture last element, e.g. center 0, size 32 -> 16 + end = center_element+right_half_size + actual_end = min(n_elem-1, end) + right_padding = abs(min(actual_end-end, 0)) + aperture = np.zeros((n_elem, ), dtype=np.bool) + aperture[int(actual_origin):(int(actual_end)+1)] = True + return aperture, (left_padding, right_padding) + + +def get_tx_aperture_center_coords(sequence, probe): + n_elements = probe.n_elements + pitch = probe.pitch + curvature_radius = probe.curvature_radius + tx_aperture_center_element = sequence.tx_aperture_center_element + + element_position = np.arange(-(n_elements - 1) / 2, + n_elements / 2)*pitch + + if not probe.is_convex_array(): + angle = np.zeros(n_elements) + else: + angle = element_position / curvature_radius + + tx_aperture_center_angle = np.interp(tx_aperture_center_element, + np.arange(0, n_elements), angle) + tx_aperture_center_z = np.interp(tx_aperture_center_element, + np.arange(0, n_elements), + np.squeeze(probe.element_pos_z)) + tx_aperture_center_x = np.interp(tx_aperture_center_element, + np.arange(0, n_elements), + np.squeeze(probe.element_pos_x)) + + return tx_aperture_center_angle, tx_aperture_center_x, tx_aperture_center_z + + +def compute_tx_parameters(sequence, probe, speed_of_sound): + tx_ap_size = sequence.tx_aperture_size + tx_centers = sequence.tx_aperture_center_element + tx_apertures = [] + for tx_center_element in tx_centers: + tx_apertures.append(get_aperture_with_padding(tx_center_element, + tx_ap_size, probe)[0]) + + element_x, element_z = probe.element_pos_x, probe.element_pos_z + element_x, element_z = np.atleast_2d(element_x), np.atleast_2d(element_z) + + tx_center_angle, tx_center_x, tx_center_z = get_tx_aperture_center_coords( + sequence, probe) + tx_center_angle = np.atleast_2d(tx_center_angle) + tx_center_x = np.atleast_2d(tx_center_x) + tx_center_z = np.atleast_2d(tx_center_z) + + tx_angle = 0 + tx_focus = sequence.tx_focus + tx_angle_cartesian = tx_center_angle + tx_angle + + focus_x = tx_center_x + tx_focus*np.sin(tx_angle_cartesian) + focus_z = tx_center_z + tx_focus*np.cos(tx_angle_cartesian) + + # (n_elements, n_tx) + tx_delays = np.sqrt((focus_x-element_x.T)**2 + + (focus_z-element_z.T)**2) / speed_of_sound + tx_delays_center = np.sqrt((focus_x-tx_center_x)**2 + + (focus_z-tx_center_z)**2) / speed_of_sound + foc_defoc = 1 - 2*float(tx_focus > 0) + tx_delays = tx_delays*foc_defoc + tx_delays_center = tx_delays_center*foc_defoc + tx_delays_center = np.squeeze(tx_delays_center) + tx_aperture_delays = [] + tx_aperture_delays_center = [] + + for i, tx_aperture in enumerate(tx_apertures): + tx_del = tx_delays[np.argwhere(tx_aperture), i] + tx_delay_shift = - np.min(tx_del) + tx_del = tx_del + tx_delay_shift + tx_del_cent = tx_delays_center[i] + tx_delay_shift + tx_aperture_delays.append(tx_del) + tx_aperture_delays_center.append(tx_del_cent) + + tx_delays_center_max = np.max(tx_aperture_delays_center) + + # Equalize + for i in range(len(tx_aperture_delays)): + tx_aperture_delays[i] = tx_aperture_delays[i] \ + - tx_aperture_delays_center[i] \ + + tx_delays_center_max + return tx_apertures, tx_aperture_delays, tx_delays_center_max + + diff --git a/api/python/arrus/kernels/kernel.py b/api/python/arrus/kernels/kernel.py new file mode 100644 index 000000000..a5ff8b85e --- /dev/null +++ b/api/python/arrus/kernels/kernel.py @@ -0,0 +1,22 @@ +import dataclasses +import arrus.medium +import arrus.ops +import arrus.devices.device + +@dataclasses.dataclass(frozen=True) +class KernelExecutionContext: + """ + Kernel execution context. + + This function contains all data that is available for the implementation of + the kernel. + + :param device: device on which the kernel will be executed + :param medium: the medium assumed for the current session + :param op: operation to perform + :param custom: custom data + """ + device: arrus.devices.device.UltrasoundDeviceDTO + medium: arrus.medium.MediumDTO + op: arrus.ops.Operation + custom: dict diff --git a/api/python/arrus/kernels/tests/imaging_test.py b/api/python/arrus/kernels/tests/imaging_test.py new file mode 100644 index 000000000..d61a20d73 --- /dev/null +++ b/api/python/arrus/kernels/tests/imaging_test.py @@ -0,0 +1,86 @@ +import numpy as np +import arrus.kernels.imaging +import arrus.ops.imaging +import arrus.medium +import unittest +import dataclasses + + +@dataclasses.dataclass(frozen=True) +class ProbeMock: + pitch: float + n_elements: int + + +@dataclasses.dataclass(frozen=True) +class DeviceMock: + probe: ProbeMock + + +@dataclasses.dataclass(frozen=True) +class ContextMock: + device: DeviceMock + medium: arrus.medium.MediumDTO + op: object + + +class LinSequenceTest(unittest.TestCase): + + def test_simple_sequence_with_paddings(self): + + # three tx/rxs with aperture centers: 0, 16, 31 + seq = arrus.ops.imaging.LinSequence( + tx_aperture_center_element=np.array([0, 15, 16, 31]), + tx_aperture_size=32, + tx_focus=30e-3, + pulse=arrus.ops.us4r.Pulse(center_frequency=5e6, n_periods=3, + inverse=False), + rx_aperture_center_element=np.array([0, 15, 16, 31]), + rx_aperture_size=32, + pri=1000e-6, + downsampling_factor=1, + rx_sample_range=(0, 4096)) + + medium = arrus.medium.MediumDTO(name="test", speed_of_sound=1540) + device = DeviceMock(probe=ProbeMock(pitch=0.2e-3, n_elements=32)) + context = ContextMock(device=device, medium=medium, op=seq) + tx_rx_sequence = arrus.kernels.imaging.create_lin_sequence(context) + + # expected delays + # TODO check also if appropriate delays are computed + delays = arrus.kernels.imaging.enum_classic_delays( + n_elem=32, pitch=0.2e-3, c=1540, focus=30e-3) + + # Expected aperture tx/rx 1 + expected_aperture = np.zeros((32,), dtype=bool) + expected_aperture[0:16+1] = True + expected_delays = delays[15:] + tx_rx = tx_rx_sequence.ops[0] + np.testing.assert_array_equal(tx_rx.tx.aperture, expected_aperture) + np.testing.assert_array_equal(tx_rx.rx.aperture, expected_aperture) + np.testing.assert_almost_equal(tx_rx.tx.delays, expected_delays) + + # Expected aperture tx/rx 2 + expected_aperture = np.ones((32,), dtype=bool) + tx_rx = tx_rx_sequence.ops[1] + np.testing.assert_array_equal(tx_rx.tx.aperture, expected_aperture) + np.testing.assert_array_equal(tx_rx.rx.aperture, expected_aperture) + + # Expected aperture tx/rx 3 + expected_aperture = np.zeros((32,), dtype=bool) + expected_aperture[1:] = True + tx_rx = tx_rx_sequence.ops[2] + np.testing.assert_array_equal(tx_rx.tx.aperture, expected_aperture) + np.testing.assert_array_equal(tx_rx.rx.aperture, expected_aperture) + + # Expected aperture tx/rx 4 + expected_aperture = np.zeros((32,), dtype=bool) + expected_aperture[16:] = True + tx_rx = tx_rx_sequence.ops[3] + np.testing.assert_array_equal(tx_rx.tx.aperture, expected_aperture) + np.testing.assert_array_equal(tx_rx.rx.aperture, expected_aperture) + + + +if __name__ == "__main__": + unittest.main() diff --git a/api/python/arrus/kernels/tgc.py b/api/python/arrus/kernels/tgc.py new file mode 100644 index 000000000..55e59e721 --- /dev/null +++ b/api/python/arrus/kernels/tgc.py @@ -0,0 +1,21 @@ +import numpy as np + + +def compute_linear_tgc(seq_context, linear_tgc): + seq = seq_context.op + tgc_start = linear_tgc.start + tgc_slope = linear_tgc.slope + sample_range = seq.rx_sample_range + start_sample, end_sample = sample_range + downsampling_factor = seq.downsampling_factor + fs = seq_context.device.sampling_frequency/seq.downsampling_factor + # medium parameters + c = seq.speed_of_sound + if c is None: + c = seq_context.medium.speed_of_sound + + distance = np.arange(start=round(400/downsampling_factor), + stop=end_sample, + step=round(150/downsampling_factor))/fs*c + + return tgc_start + distance*tgc_slope diff --git a/api/python/arrus/logging.py b/api/python/arrus/logging.py new file mode 100644 index 000000000..399081899 --- /dev/null +++ b/api/python/arrus/logging.py @@ -0,0 +1,54 @@ +""" +Logging tools for arrus. +Currently the arrus logging mechanism is +implemented in c++ using boost::log library. +""" +import arrus.core + +# Wrap arrus core logging levels. +TRACE = arrus.core.LogSeverity_TRACE +DEBUG = arrus.core.LogSeverity_DEBUG +INFO = arrus.core.LogSeverity_INFO +WARNING = arrus.core.LogSeverity_WARNING +ERROR = arrus.core.LogSeverity_ERROR +FATAL = arrus.core.LogSeverity_FATAL + +DEFAULT_LEVEL = INFO + +# Init default level logging. +arrus.core.initLoggingMechanism(DEFAULT_LEVEL) + + +def set_clog_level(level): + """ + Sets console log level output. + + :param level: log level to use + """ + return arrus.core.setClogLevel(level) + + +def add_log_file(filepath, level): + """ + Adds message logging to given file. + + :param filepath: path to output log file + :param level: severity level + """ + return arrus.core.addLogFile(filepath, level) + + +def get_logger(): + """ + Returns a handle to new logger. + + :return: + """ + return arrus.core.getLogger() + + +DEFAULT_LOGGER = get_logger() + + +def log(level, msg): + DEFAULT_LOGGER.log(level, msg) diff --git a/api/python/arrus/medium.py b/api/python/arrus/medium.py new file mode 100644 index 000000000..2c76c2411 --- /dev/null +++ b/api/python/arrus/medium.py @@ -0,0 +1,11 @@ +import dataclasses + +@dataclasses.dataclass(frozen=True) +class Medium: + name: str + speed_of_sound: float + +@dataclasses.dataclass(frozen=True) +class MediumDTO: + name: str + speed_of_sound: float \ No newline at end of file diff --git a/api/python/arrus/metadata.py b/api/python/arrus/metadata.py new file mode 100644 index 000000000..d84292fef --- /dev/null +++ b/api/python/arrus/metadata.py @@ -0,0 +1,119 @@ +import dataclasses +import abc + +import arrus.devices.device +import arrus.medium +import arrus.ops +import arrus.ops.us4r + + +@dataclasses.dataclass(frozen=True) +class FrameAcquisitionContext: + """ + Metadata describing RF frame acquisition process. + + :param device: ultrasound device specification + :param sequence: a sequence that the user wanted to execute on the device + :param raw_sequence: an actual Tx/Rx sequence that was uploaded on the system + :param medium: description of the Medium assumed during communication session with the device + :param custom_data: a dictionary with custom data + """ + device: arrus.devices.device.UltrasoundDeviceDTO + sequence: arrus.ops.Operation + raw_sequence: arrus.ops.us4r.TxRxSequence + medium: arrus.medium.MediumDTO + custom_data: dict + + +class DataDescription(abc.ABC): + pass + + +@dataclasses.dataclass(frozen=True) +class EchoDataDescription(DataDescription): + """ + Data description of the ultrasound echo data. + + :param sampling_frequency: a sampling frequency of the data + :param custom: custom information + """ + sampling_frequency: float + custom: dict = dataclasses.field(default_factory=dict) + + +class ConstMetadata: + """ + A metadata that won't change while the system is running. + """ + def __init__(self, context: FrameAcquisitionContext, + data_desc: DataDescription, + input_shape, is_iq_data, dtype): + self._context = context + self._data_char = data_desc + self._input_shape = input_shape + self._is_iq_data = is_iq_data + self._dtype = dtype + + @property + def context(self) -> FrameAcquisitionContext: + return self._context + + @property + def data_description(self) -> DataDescription: + return self._data_char + + @property + def input_shape(self): + return self._input_shape + + @property + def is_iq_data(self): + return self._is_iq_data + + @property + def dtype(self): + return self._dtype + + def copy(self, **kwargs): + kw = dict(context=self.context, data_desc=self.data_description, + input_shape=self.input_shape, is_iq_data=self.is_iq_data, + dtype=self.dtype) + return ConstMetadata(**{**kw, **kwargs}) + + +class Metadata: + """ + Metadata describing the acquired data. + + This class is immutable. + + :param context: frame acquisition context + :param data_desc: data characteristic + :param custom: custom frame data (e.g. trigger counters, etc.) + """ + def __init__(self,context: FrameAcquisitionContext, + data_desc: DataDescription, + custom: dict): + self._context = context + self._data_char = data_desc + # TODO make _custom_data immutable + self._custom_data = custom + + @property + def context(self) -> FrameAcquisitionContext: + return self._context + + @property + def data_description(self) -> DataDescription: + return self._data_char + + @property + def custom(self): + return self._custom_data + + def copy(self, **kwargs): + # TODO validate kwargs + kw = dict(context=self.context, data_desc=self.data_description, + custom=self.custom) + return Metadata(**{**kw, **kwargs}) + diff --git a/api/python/arrus/ops/__init__.py b/api/python/arrus/ops/__init__.py index c036d6f78..d8a167a9e 100644 --- a/api/python/arrus/ops/__init__.py +++ b/api/python/arrus/ops/__init__.py @@ -1 +1 @@ -from arrus.ops.operations import * +from arrus.ops.operation import * \ No newline at end of file diff --git a/api/python/arrus/ops/imaging.py b/api/python/arrus/ops/imaging.py new file mode 100644 index 000000000..a0dac05c6 --- /dev/null +++ b/api/python/arrus/ops/imaging.py @@ -0,0 +1,41 @@ +import arrus.ops.us4r +import numpy as np +import arrus.ops +import dataclasses + + +@dataclasses.dataclass(frozen=True) +class LinSequence(arrus.ops.Operation): + """ + A sequence of Tx/Rx operations for classical beamforming (linear scanning). + + :param tx_aperture_center_element: vector of tx aperture center elements [element] + :param tx_aperture_size: size of the tx aperture [element] + :param rx_aperture_center_element: vector of rx aperture center elements [element] + :param rx_aperture_size: size of the rx aperture [element] + :param tx_focus: tx focal length [m] + :param pulse: an excitation to perform + :param rx_sample_range: [start, end) sample; total number of samples should be divisible by 64 + :param pri: pulse repetition interval [s] + :param downsampling_factor: downsampling factor (decimation), integer factor for decreasing sampling frequency of the output signal + :param speed_of_sound: assumed speed of sound; can be None, in this case a medium in current context will be used to determine speed of sound [m/s] + :param tgc_start: tgc starting gain [dB] + :param tgc_slope: tgc gain slope [dB/m] + :param sri: sequence repetition interval - the time between consecutive RF frames. When None, the time between consecutive RF frames is determined by the total pri only. [s] + :param init_delay: when the record should start, available options: 'tx_start' - the first recorded sample is when the transmit starts, 'tx_center' - the first recorded sample is delayed by tx aperture center delay and burst factor + """ + tx_aperture_center_element: np.ndarray + tx_aperture_size: float + tx_focus: float + pulse: arrus.ops.us4r.Pulse + rx_aperture_center_element: np.ndarray + rx_aperture_size: float + pri: float + rx_sample_range: tuple + tgc_start: float + tgc_slope: float + speed_of_sound: float = None + downsampling_factor: int = 1 + sri: float = None + init_delay: str = "tx_start" + diff --git a/api/python/arrus/ops/operation.py b/api/python/arrus/ops/operation.py new file mode 100644 index 000000000..5f68539b7 --- /dev/null +++ b/api/python/arrus/ops/operation.py @@ -0,0 +1,8 @@ +import abc + + +class Operation(abc.ABC): + """ + An abstract base class for all available operations. + """ + pass diff --git a/api/python/arrus/ops/operations.py b/api/python/arrus/ops/operations.py deleted file mode 100644 index 822fe96cd..000000000 --- a/api/python/arrus/ops/operations.py +++ /dev/null @@ -1,127 +0,0 @@ -from dataclasses import dataclass -from collections.abc import Iterable -from abc import ABC -import typing -import numpy as np - -import arrus.params - -import arrus - - -class Operation(ABC): - """ - A single operation to perform. - - This class is abstract and should not be instantiated. - """ - pass - - -@dataclass(frozen=True) -class Tx(Operation): - """ - Single atomic operation of signal transmit. - - :param delays: an array of delays to set to active elements. Should have the \ - shape (n_a,), where n_a is a number of active elements determined by \ - tx aperture. When None, firings are performed with no delay (delays=0) [s]. - :param excitation: an excitation to perform - :param aperture: a set of TX channels that should be enabled - :param pri: pulse repetition interval [s] - """ - excitation: arrus.params.Excitation - aperture: arrus.params.Aperture - pri: float - delays: typing.Optional[np.ndarray] = None - - def __post_init__(self): - if self.delays is not None and len(self.delays.shape) != 1: - raise ValueError("The array of delays should be a vector of " - "shape (number of active elements,)") - if self.delays is not None \ - and self.delays.shape[0] != self.aperture.get_size(): - raise ValueError(f"The array of delays should have the size equal " - f"to the number of active elements of aperture " - f"({self.aperture.get_size()})") - - -@dataclass(frozen=True) -class Rx(Operation): - """ - Single atomic operation of signal data reception. - - :param samples: number of samples to acquire - :param fs_divider: a sampling frequency divider. For example, if \ - nominal sampling frequency (fs) is equal to 65e6 Hz, ``fs_divider=1``,\ - means to use the nominal fs, ``fs_divider=2`` means to use 32.5e6 Hz, \ - etc. - :param aperture: a set of RX channels that should be enabled - :param rx_time: the total acquisition time [s] - :param rx_delay: initial rx delay [s] - """ - n_samples: int - aperture: arrus.params.Aperture - fs_divider: int = 1 - rx_time: float = 160e-6 - rx_delay: float = 5e-6 - - -@dataclass(frozen=True) -class TxRx(Operation): - """ - Single atomic operation of pulse transmit and signal data reception. - Returns acquired signal data -- a numpy ``ndarray`` of shape - ``(number_of_samples, number_of_rx_channels)`` - - :param tx: signal transmit to perform - :param rx: signal reception to perform - """ - tx: Tx - rx: Rx - - -@dataclass(frozen=True) -class Sequence(Operation): - """ - A sequence of operations to perform. - Returns acquired signal data -- a numpy ``ndarray`` of shape - ``(number_of_samples*number_of_operations, number_of_rx_channels)`` - - :param operations: sequence of TX/RX operations to perform - """ - operations: typing.List[TxRx] - - -@dataclass(frozen=True) -class Loop(Operation): - """ - Performs given operation in a loop. - - This operation returns no value. Requires ``callback`` function - as a session parameter. The callback function should take one parameter - ``rf`` that will be filled with acquired signal data. The callback - function should return a boolean value: ``True``, if the loop should - continue, ``False`` otherwise. - - :param operation: an operation to perform in a loop, accepted: ``Sequence``. - """ - operation: Operation - - -@dataclass(frozen=True) -class SetHVVoltage(Operation): - """ - Sets voltage on a given device. Returns no value. - - :param voltage: voltage to set [0.5Vpp] - """ - voltage: float - - -@dataclass(frozen=True) -class DisableHVVoltage(Operation): - """ - Disables High Voltage supplier in the system. - """ - pass diff --git a/api/python/arrus/ops/tgc.py b/api/python/arrus/ops/tgc.py new file mode 100644 index 000000000..96007ca1a --- /dev/null +++ b/api/python/arrus/ops/tgc.py @@ -0,0 +1,13 @@ +import dataclasses + + +@dataclasses.dataclass(frozen=True) +class LinearTgc: + """ + Set linear TGC on the device. + + :param start: tgc starting gain [dB] + :param slope: tgc gain slope [dB/m] + """ + start: float + slope: float \ No newline at end of file diff --git a/api/python/arrus/ops/us4r.py b/api/python/arrus/ops/us4r.py new file mode 100644 index 000000000..49154d1fd --- /dev/null +++ b/api/python/arrus/ops/us4r.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass +import typing +import numpy as np +from arrus.ops.operation import Operation + + +@dataclass(frozen=True) +class Pulse: + """ + A definition of the pulse that can be triggered by the us4r device. + + :param center_frequency: pulse center frequency [Hz] + :param n_periods: number of periods of the generated pulse, possible values: 0.5, 1, 1.5, ... + :param inverse: true if the signal amplitude should be reversed, false otherwise + """ + center_frequency: float + n_periods: float + inverse: bool + + +@dataclass(frozen=True) +class Tx(Operation): + """ + Single atomic operation of a signal transmit. + + :param aperture: a set of TX channels that should be enabled - a binary mask, where 1 at location i means that the channel should be turned on, 0 means that the channel should be turned off + :param pulse: an excitation to perform + :param delays: an array of delays to set to active elements. Should have the \ + shape (n_a,), where n_a is a number of active elements determined by \ + tx aperture. When None, firings are performed with no delay (delays=0) [s]. + """ + aperture: np.ndarray + excitation: Pulse + delays: typing.Optional[np.ndarray] = None + + def __post_init__(self): + if self.delays is not None and len(self.delays.shape) != 1: + raise ValueError("The array of delays should be a vector of " + "shape (number of active elements,)") + if self.delays is not None \ + and self.delays.shape[0] != np.sum(self.aperture): + raise ValueError(f"The array of delays should have the size equal " + f"to the number of active elements of aperture " + f"({self.aperture.shape})") + + +@dataclass(frozen=True) +class Rx(Operation): + """ + Single atomic operation of echo data reception. + + :param aperture: a set of RX channels that should be enabled - a binary mask, where 1 at location i means that the channel should be turned on, 0 means that the channel should be turned off + :param sample_range: a range of samples to acquire [start, end), starts from 0 + :param downsampling_factor: a sampling frequency divider. For example, if \ + nominal sampling frequency (fs) is equal to 65e6 Hz, ``fs_divider=1``,\ + means to use the nominal fs, ``fs_divider=2`` means to use 32.5e6 Hz, \ + etc. + :param padding: a pair of values (left, right); the left/right value means + how many zero-channels should be added from the left/right side of the + aperture. This parameter helps achieve a regular ndarray when + a sequence of Rxs has a non-constant aperture size (e.g. classical + beamforming). + """ + aperture: np.ndarray + sample_range: tuple + downsampling_factor: int = 1 + padding: tuple = (0, 0) + + def get_n_samples(self): + start, end = self.sample_range + return end-start + + +@dataclass(frozen=True) +class TxRx: + """ + Single atomic operation of pulse transmit and signal data reception. + + :param tx: signal transmit to perform + :param rx: signal reception to perform + :param pri: pulse repetition interval [s] - time to next event + """ + tx: Tx + rx: Rx + pri: float + +@dataclass(frozen=True) +class TxRxSequence: + """ + A sequence of tx/rx operations to perform. + + :param operations: sequence of TX/RX operations to perform + :param tgc_curve: TGC curve samples [dB] + :param sri: sequence repetition interval - the time between consecutive RF frames. When None, the time between consecutive RF frames is determined by the total pri only. [s] + """ + ops: typing.List[TxRx] + tgc_curve: np.ndarray + sri: float = None + + def get_n_samples(self): + """ + Returns a set of number of samples that the Tx/Rx sequence defines. + """ + return {op.rx.get_n_samples() for op in self.ops} + + + + + diff --git a/api/python/arrus/params.py b/api/python/arrus/params.py index 9d4f0fe1b..1596f982d 100644 --- a/api/python/arrus/params.py +++ b/api/python/arrus/params.py @@ -4,7 +4,7 @@ import numpy as np -class Excitation(abc.ABC): +class Pulse(abc.ABC): """ An excitation (a signal pulse) to transmit. """ @@ -12,7 +12,7 @@ class Excitation(abc.ABC): @dataclass(frozen=True) -class SineWave(Excitation): +class SineWave(Pulse): """ Sine wave excitation. @@ -20,7 +20,7 @@ class SineWave(Excitation): :param n_periods: number of sine periods in the transmitted burst, can be fractional :param inverse: whether the resulting wave should be inverted """ - frequency: float + center_frequency: float n_periods: float inverse: bool diff --git a/api/python/arrus/session.py b/api/python/arrus/session.py index 7bfcb16ee..23195dc17 100644 --- a/api/python/arrus/session.py +++ b/api/python/arrus/session.py @@ -1,272 +1,243 @@ -import logging -import os -from logging import DEBUG, INFO import abc +import numpy as np +import importlib +import importlib.util import dataclasses -import typing -import yaml +import arrus.core +import arrus.exceptions +import arrus.devices.us4r +import arrus.devices.mock_us4r +import arrus.medium +import arrus.metadata +import arrus.params +import arrus.devices.cpu +import arrus.devices.gpu +import arrus.ops.us4r +import arrus.ops.imaging -_logger = logging.getLogger(__name__) -import arrus.devices.probe as _probe -import arrus.devices.us4oem as _us4oem -import arrus.devices.device as _device -import arrus.interface as _interface -import arrus.utils as _utils -import arrus.devices.ius4oem as _ius4oem -import arrus.system as _system -import arrus.kernels -import arrus.validation - -_ARRUS_PATH_ENV = "ARRUS_PATH" - - -@dataclasses.dataclass(frozen=True) -class SessionCfg: +class AbstractSession(abc.ABC): """ - Configuration of the communication session with devices. - - This configuration allows to specify parameters that has to - applied when user starts a new session. + An abstract class of session. - :param system: description of a system with which the user wants to - communicate - :param devices: configuration of the devices with which the user wants - communicate; a map: device id → device configuration + This class is not intended to be instantiated. """ - system: _system.SystemCfg - devices: typing.Mapping[str, _device.DeviceCfg] - - def __post_init__(self): - arrus.validation.assert_not_none(self.devices, "devices") - arrus.validation.assert_not_none(self.devices, "system") - -class AbstractSession(abc.ABC): - - def __init__(self, cfg): - self._devices = self._load_devices(cfg) + def __init__(self): + pass + @abc.abstractmethod def get_device(self, id: str): """ Returns a device located at given path. - :param id: a path to a device, for example '/Us4OEM:0' - :return: a device located in given path. + :param id: a path to a device, for example '/Us4R:0' + :return: a device located in a given path. """ - dev_path = id.split("/")[1:] - if len(dev_path) != 1: - raise ValueError( - "Invalid path, top-level devices can be accessed only.") - dev_id = dev_path[0] - return self._devices[dev_id] - - def get_devices(self): + raise ValueError("Tried to access an abstract method.") + + @abc.abstractmethod + def set_current_medium(self, medium: arrus.medium.Medium): """ - Returns a list of all devices available in this session. + Sets a medium in the current session context. - :return: a list of available devices + :param medium: medium description to set """ - return self._devices + raise ValueError("Tried to access an abstract method.") - @abc.abstractmethod - def run(self, operation: arrus.ops.Operation, feed_dict: dict): - raise ValueError("This type of session cannot run operations.") - @abc.abstractmethod - def _load_devices(self, cfg): - pass +@dataclasses.dataclass(frozen=True) +class SessionContext: + medium: arrus.medium.Medium class Session(AbstractSession): """ - A communication session with the available devices. + A communication session with the ultrasound system. + Currently, only localhost session is available. """ - def __init__(self, cfg: SessionCfg): - """ - :param cfg: configuration parameters for the new session - """ - super().__init__(cfg) - self._async_kernels = {} - - def run(self, operation: arrus.ops.Operation, feed_dict: dict): + def __init__(self, cfg_path: str = None, + medium: arrus.medium.Medium = None, + mock: dict = None): """ - Runs a given operation in the system. Returns the result of operation - (if any). + Session constructor. - :param operation: operation to run - :param feed_dict: values to pass to the operation. All operations - requires `device`; check documentation of a particular documentation - if it requires any additional session values. + :param cfg_path: a path to configuration file + :param mock: a map device id -> a file that should be used to + mock the device + :param medium: medium description to set in context """ - _logger.log(DEBUG, f"Session run: {str(operation)}") - - kernel = arrus.kernels.get_kernel(operation, feed_dict) - device = feed_dict.get("device", None) - arrus.validation.assert_not_none(device, "device") - current_async_kernel = self._async_kernels.get(device.get_id(), None) - if current_async_kernel is not None: - device_id = device.get_id() - current_op = current_async_kernel.op - raise ValueError(f"An operation {current_op} is already running on " - f"the device {device_id}. Stop the device first.") - device.start_if_necessary() - result = kernel.run() - if kernel is arrus.kernels.AsyncKernel: - self._async_kernels[device.get_id()] = kernel - return result - - def stop_device(self, device: _device.Device): + super().__init__() + if not (bool(cfg_path is None) ^ bool(mock is None)): + raise ValueError("Exactly one of the following parameters should " + "be provided: cfg_path, mock.") + if cfg_path is not None: + self._session_handle = arrus.core.createSessionSharedHandle(cfg_path) + self._py_devices = self._create_py_devices(mock) + self._context = SessionContext(medium=medium) + + def get_device(self, path: str): """ - Stops executing any operations that run on a specific device. - """ - current_kernel = self._async_kernels.get(device.get_id(), None) - if current_kernel is not None: - op = current_kernel.op - current_kernel.stop() - _logger.info(f"Stopped operation {op} running on " - f"device {device.get_id()}") - - def stop(self): - for key, kernel in self._async_kernels: - _logger.debug(INFO, "Stopping device %s (operation %s)" % - (key, kernel.op)) - kernel.stop() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.stop() - - def _load_devices(self, cfg: SessionCfg): - result = {} - system_cfg = cfg.system - - if not isinstance(system_cfg, _system.CustomUs4RCfg): - raise ValueError("Only custom us4r configuration is currently " - "supported.") - - n_us4oems = system_cfg.n_us4oems - arrus.validation.assert_not_none(n_us4oems, "number of us4oems") - - us4oem_handles = [] - for i in range(n_us4oems): - us4oem_handle = _ius4oem.getUs4OEMPtr(i) - _logger.log(DEBUG, - f"Discovered Us4OEM handle " - f"with id {us4oem_handle.GetID()}") - us4oem_handles.append(us4oem_handle) - - us4oem_handles = sorted(us4oem_handles, key=lambda a: a.GetID()) - - for device_id, device_cfg in cfg.devices.items(): - if device_id.strip().upper().startswith("US4OEM"): - # parse id - device_type, index = device_id.split(":") - index = int(index.strip()) - arrus.validation.assert_type(device_cfg, _us4oem.Us4OEMCfg, - f"{device_id} configuration") - # validate and use configuration to create device - us4oem = _us4oem.Us4OEM(index,card_handle=us4oem_handles[index], - cfg=device_cfg) - us4oem.start_if_necessary() - if us4oem.get_id() in result.keys(): - raise ValueError(f"Found duplicated configuration for " - f"{us4oem.get_id()}") - result[us4oem.get_id()] = us4oem - _logger.log(INFO, f"Discovered module: {str(us4oem)}") - else: - raise ValueError(f"This device cannot be configured: " - f"{device_id}") - - # Below is a workaround for the following issue: - # - turn off us4r-lite, turn on us4r-lite - # - run script, which uses only us4oem:0 - # - run again - PCIDeviceException - # If only one module is initialized after the restart, the second - # call will fail. - for i in range(n_us4oems): - if _us4oem.Us4OEM.get_card_id(i) not in result: - unused_device = _us4oem.Us4OEM(i, card_handle=us4oem_handles[i]) - unused_device.start_if_necessary() - result[unused_device.get_id()] = unused_device - - if system_cfg.is_hv256: - module_id = system_cfg.master_us4oem - master_module = result[_us4oem.Us4OEM.get_card_id(module_id)] - - # Intentionally loading modules only when the HV256 is used. - import arrus.devices.idbarlite as _dbarlite - import arrus.devices.ihv256 as _ihv256 - import arrus.devices.hv256 as _hv256 - - dbar = _dbarlite.GetDBARLite( - _ius4oem.castToII2CMaster(master_module.card_handle)) - hv_handle = _ihv256.GetHV256(dbar.GetI2CHV()) - hv = _hv256.HV256(hv_handle) - result[hv.get_id()] = hv - return result - - -class InteractiveSession(AbstractSession): - """ - **THIS CLASS IS DEPRECATED AND WILL BE REMOVED IN THE NEAR FUTURE. Please - use** ``arrus.Session``. + Returns a device identified by a given id. - An user interactive session with available devices. - - If cfg_path is None, session looks for a file ``$ARRUS_PATH/default.yaml``, - where ARRUS_PATH is an user-defined environment variable. - - :param cfg_path: path to the configuration file, can be None - """ - def __init__(self, cfg_path: str = None): - if cfg_path is None: - cfg_path = os.path.join(os.environ.get(_ARRUS_PATH_ENV, ""), "default.yaml") - - with open(cfg_path, "r") as f: - cfg = yaml.safe_load(f) - super().__init__(cfg) - - def run(self, operation: arrus.ops.Operation, feed_dict: dict): - raise ValueError("This type of session cannot run operations.") - - def _load_devices(self, cfg): - """ - Reads configuration from given file and returns a map of top-level - devices. + The available devices are determined by the initial session settings. - Currently Us4OEM modules are supported. + The handle to device is invalid after the session is closed + (i.e. the session object is disposed). - :param cfg: configuration to read - :return: a map: device id -> Device + :param path: a path to the device + :return: a handle to device """ - result = {} - # --- Cards - n_us4oems = cfg["nModules"] - us4oem_handles = (_ius4oem.getUs4OEMPtr(i) for i in range(n_us4oems)) - us4oem_handles = sorted(us4oem_handles, key=lambda a: a.GetID()) - us4oems = [_us4oem.Us4OEM(i, h) for i, h in enumerate(us4oem_handles)] - _logger.log(INFO, "Discovered modules: %s"%str(us4oems)) - for us4oem in us4oems: - result[us4oem.get_id()] = us4oem - - is_hv256 = cfg.get("HV256", None) - if is_hv256: - module_id = cfg["masterModule"] - master_module = result[_us4oem.Us4OEM.get_card_id(module_id)] - - # Intentionally loading modules only when the HV256 is used. - import arrus.devices.idbarlite as _dbarlite - import arrus.devices.ihv256 as _ihv256 - import arrus.devices.hv256 as _hv256 - - dbar = _dbarlite.GetDBARLite( - _ius4oem.castToII2CMaster(master_module.card_handle)) - hv_handle = _ihv256.GetHV256(dbar.GetI2CHV()) - hv = _hv256.HV256(hv_handle) - result[hv.get_id()] = hv - return result + # First, try getting initially available devices. + if path in self._py_devices: + return self._py_devices[path] + # Then try finding a device using arrus core implementation. + + if self._session_handle is None: + # We have no handle to session (mock session), + # so we wont find any new device. + raise arrus.exceptions.DeviceNotFoundError(path) + + device_handle = self._session_handle.getDevice(path) + # Cast device to its type class. + device_id = device_handle.getDeviceId() + device_type = device_id.getDeviceType() + specific_device_cast = { + # Currently only us4r is supported. + arrus.core.DeviceType_Us4R: + lambda handle: arrus.devices.us4r.Us4R( + arrus.core.castToUs4r(handle), + self) + + }.get(device_type, None) + if specific_device_cast is None: + raise arrus.exceptions.DeviceNotFoundError(path) + specific_device = specific_device_cast(device_handle) + # TODO(pjarosik) key should be an id, not the whole path + self._py_devices[path] = specific_device + return specific_device + + def get_session_context(self): + return self._context + + def set_current_medium(self, medium: arrus.medium.Medium): + # TODO mutex, forbid when context is frozen (e.g. when us4r is running) + raise RuntimeError("NYI") + + def _create_py_devices(self, mock): + # Create mock devices + + devices = {} + + if mock is not None: + for k, v in mock.items(): + devices["/" + k] = MockMatlab045.load_device(v) + + # Create CPU and GPU devices + devices["/CPU:0"] = arrus.devices.cpu.CPU(0) + cupy_spec = importlib.util.find_spec("cupy") + if cupy_spec is not None: + import cupy + cupy.cuda.device.Device(0).use() + devices["/GPU:0"] = arrus.devices.gpu.GPU(0) + return devices + + +# ------------------------------------------ MATLAB 0.4.5 LEGACY MOCK DATA +class MockMatlab045: + + @staticmethod + def load_device(cfg): + metadata = MockMatlab045._read_metadata(cfg) + data = cfg["rf"] + return arrus.devices.mock_us4r.MockUs4R(data, metadata, 0) + + @staticmethod + def _get_scalar(obj, key): + return obj[key][0][0] + + @staticmethod + def _get_vector(obj, key): + return np.array(obj[key]).flatten() + + @staticmethod + def _read_metadata(data): + sys = data["sys"] + seq = data["seq"] + + # Device + pitch = MockMatlab045._get_scalar(sys, "pitch") + curv_radius = - MockMatlab045._get_scalar(sys, "curvRadius") + + probe_model = arrus.devices.probe.ProbeModel( + model_id=arrus.devices.probe.ProbeModelId( + manufacturer="nanoecho", name="magprobe"), + n_elements=int(MockMatlab045._get_scalar(sys, "nElem")), + pitch=pitch, curvature_radius=curv_radius) + probe = arrus.devices.probe.ProbeDTO(model=probe_model) + us4r = arrus.devices.us4r.Us4RDTO(probe=probe, sampling_frequency=65e6) + + # Sequence + tx_freq = MockMatlab045._get_scalar(seq, "txFreq") + n_periods = MockMatlab045._get_scalar(seq, "txNPer") + inverse = MockMatlab045._get_scalar(seq, "txInvert").astype(np.bool) + + pulse = arrus.ops.us4r.Pulse( + center_frequency=tx_freq, n_periods=n_periods, + inverse=inverse) + + # In matlab API the element numbering starts from 1 + tx_ap_center_element = MockMatlab045._get_vector(seq, "txCentElem") - 1 + tx_ap_size = MockMatlab045._get_scalar(seq, "txApSize") + tx_angle = MockMatlab045._get_scalar(seq, "txAng") + tx_focus = MockMatlab045._get_scalar(seq, "txFoc") + tx_ap_cent_ang = MockMatlab045._get_vector(seq, "txApCentAng") + + rx_ap_center_element = MockMatlab045._get_vector(seq, "rxCentElem") - 1 + rx_ap_size = MockMatlab045._get_scalar(seq, "rxApSize") + rx_samp_freq = MockMatlab045._get_scalar(seq, "rxSampFreq") + pri = MockMatlab045._get_scalar(seq, "txPri") + fsDivider = MockMatlab045._get_scalar(seq, "fsDivider") + start_sample = MockMatlab045._get_scalar(seq, "startSample") -1 + end_sample = MockMatlab045._get_scalar(seq, "nSamp") + tgc_start = MockMatlab045._get_scalar(seq, "tgcStart") + tgc_slope = MockMatlab045._get_scalar(seq, "tgcSlope") + + sequence = arrus.ops.imaging.LinSequence( + tx_aperture_center_element=tx_ap_center_element, + tx_aperture_size=tx_ap_size, + tx_focus=tx_focus, + pulse=pulse, + rx_aperture_center_element=rx_ap_center_element, + rx_aperture_size=rx_ap_size, + downsampling_factor=fsDivider, + rx_sample_range=(start_sample, end_sample), + pri=pri, + tgc_start=tgc_start, + tgc_slope=tgc_slope) + + # Medium + c = MockMatlab045._get_scalar(seq, "c") + medium = arrus.medium.MediumDTO("dansk_phantom_1525_us4us", + speed_of_sound=c) + + custom_data = dict() + custom_data["start_sample"] = MockMatlab045._get_scalar(seq, "startSample") - 1 + custom_data["tx_delay_center"] = MockMatlab045._get_scalar(seq, "txDelCent") + custom_data["rx_aperture_origin"] = MockMatlab045._get_vector(seq, "rxApOrig") + custom_data["tx_aperture_center_angle"] = MockMatlab045._get_vector(seq, + "txApCentAng") + + # Data characteristic: + data_char = arrus.metadata.EchoDataDescription( + sampling_frequency=rx_samp_freq) + + # create context + context = arrus.metadata.FrameAcquisitionContext( + device=us4r, sequence=sequence, medium=medium, + raw_sequence=None, + custom_data=custom_data) + return arrus.metadata.Metadata( + context=context, data_desc=data_char, custom={}) diff --git a/api/python/arrus/system.py b/api/python/arrus/system.py deleted file mode 100644 index c5c856a57..000000000 --- a/api/python/arrus/system.py +++ /dev/null @@ -1,26 +0,0 @@ -import abc -import dataclasses - - -class SystemCfg(abc.ABC): - """ - Description of the system with which the user wants to communicate. - - """ - pass - - -@dataclasses.dataclass(frozen=True) -class CustomUs4RCfg(SystemCfg): - """ - Description of the custom Us4R system. - - :param n_us4oems: number of us4oem modules a user wants to use - :param is_hv256: is the hv256 voltage supplier available? - :param master_us4oem: index of the master Us4OEM module. A master module \ - triggers TxRx execution and should be connected with the voltage \ - supplier (if available). By default equal to 0. - """ - n_us4oems: int - is_hv256: bool = False - master_us4oem: int = 0 diff --git a/api/python/arrus/tests/us4oem_kernels_test.py b/api/python/arrus/tests/us4oem_kernels_test.py index b823d3ff9..5bd3b9171 100644 --- a/api/python/arrus/tests/us4oem_kernels_test.py +++ b/api/python/arrus/tests/us4oem_kernels_test.py @@ -151,9 +151,9 @@ def test_parameters_loaded_correctly(self): expected_delays = np.zeros(128) expected_delays[64:96] = tx.delays np.testing.assert_equal(device.delays, expected_delays) - self.assertEqual(device.tx_frequency[0], tx.excitation.frequency) - self.assertEqual(device.n_half_periods, tx.excitation.n_periods*2) - self.assertEqual(device.tx_invert, tx.excitation.inverse) + self.assertEqual(device.tx_frequency[0], tx.pulse.frequency) + self.assertEqual(device.n_half_periods, tx.pulse.n_periods * 2) + self.assertEqual(device.tx_invert, tx.pulse.inverse) expected_tx_mask = np.zeros(128, dtype=np.float64) expected_tx_mask[64:96] = 1.0 diff --git a/api/python/arrus/utils/core.py b/api/python/arrus/utils/core.py new file mode 100644 index 000000000..6d72a3110 --- /dev/null +++ b/api/python/arrus/utils/core.py @@ -0,0 +1,92 @@ +import numpy as np +import arrus.core +import arrus.exceptions +import arrus.devices.probe + + +def convert_to_core_sequence(seq): + """ + Converts given tx/rx sequence to arrus.core.TxRxSequence + TODO this function can be simplified by improving swig core.i mapping + + :param seq: arrus.ops.us4r.TxRxSequence + :return: arrus.core.TxRxSequence + """ + core_seq = arrus.core.TxRxVector() + n_samples = None + for op in seq.ops: + tx, rx = op.tx, op.rx + # TODO validate shape + # TX + core_delays = np.zeros(tx.aperture.shape, dtype=np.float32) + core_delays[tx.aperture] = tx.delays + core_excitation = arrus.core.Pulse( + centerFrequency=tx.excitation.center_frequency, + nPeriods=tx.excitation.n_periods, + inverse=tx.excitation.inverse + ) + core_tx = arrus.core.Tx( + aperture=arrus.core.VectorBool(tx.aperture.tolist()), + delays=arrus.core.VectorFloat(core_delays.tolist()), + excitation=core_excitation + ) + # RX + core_rx = arrus.core.Rx( + arrus.core.VectorBool(rx.aperture.tolist()), + arrus.core.PairUint32(int(rx.sample_range[0]), int(rx.sample_range[1])), + rx.downsampling_factor, + arrus.core.PairChannelIdx(int(rx.padding[0]), int(rx.padding[1])) + ) + + + core_txrx = arrus.core.TxRx(core_tx, core_rx, op.pri) + arrus.core.TxRxVectorPushBack(core_seq, core_txrx) + + start_sample, end_sample = rx.sample_range + if n_samples is None: + n_samples = end_sample - start_sample + elif n_samples != end_sample - start_sample: + raise arrus.exceptions.IllegalArgumentError( + "Sequences with the constant number of " + "samples are supported only.") + + sri = -1 if seq.sri is None else seq.sri + core_seq = arrus.core.TxRxSequence(core_seq, seq.tgc_curve.tolist(), sri) + return core_seq + + +def convert_fcm_to_np_arrays(fcm): + """ + Converts frame channel mapping to a tupple of numpy arrays. + + :param fcm: arrus.core.FrameChannelMapping + :return: a pair of numpy arrays: fcm_frame, fcm_channel + """ + fcm_frame = np.zeros( + (fcm.getNumberOfLogicalFrames(), fcm.getNumberOfLogicalChannels()), + dtype=np.int16) + fcm_channel = np.zeros( + (fcm.getNumberOfLogicalFrames(), fcm.getNumberOfLogicalChannels()), + dtype=np.int8) + for frame in range(fcm.getNumberOfLogicalFrames()): + for channel in range(fcm.getNumberOfLogicalChannels()): + frame_channel = fcm.getLogical(frame, channel) + src_frame = frame_channel[0] + src_channel = frame_channel[1] + fcm_frame[frame, channel] = src_frame + fcm_channel[frame, channel] = src_channel + return fcm_frame, fcm_channel + + +def convert_to_py_probe_model(core_model): + n_elements = arrus.core.getNumberOfElements(core_model) + pitch = arrus.core.getPitch(core_model) + curvature_radius = core_model.getCurvatureRadius() + model_id = core_model.getModelId() + return arrus.devices.probe.ProbeModel( + model_id=arrus.devices.probe.ProbeModelId( + manufacturer=model_id.getManufacturer(), + name=model_id.getName()), + n_elements=n_elements, + pitch=pitch, + curvature_radius=curvature_radius) \ No newline at end of file diff --git a/api/python/arrus/utils/fir.py b/api/python/arrus/utils/fir.py new file mode 100644 index 000000000..991e02789 --- /dev/null +++ b/api/python/arrus/utils/fir.py @@ -0,0 +1,76 @@ +import cupy as cp + +# TODO currently only complex input data is supported (part of DDC) + +_gpu_fir_complex64_str = r''' +#include + +extern "C" __global__ void gpu_fir_complex64( + complex* __restrict__ output, const complex* __restrict__ input, + const int nSamples, const int length, const float* __restrict__ kernel, const int kernelWidth) { + + int idx = threadIdx.x + blockIdx.x*blockDim.x; + int ch = idx / nSamples; + int sample = idx % nSamples; + + extern __shared__ char sharedMemory[]; + + complex* cachedInputData = (complex*)sharedMemory; + float* cachedKernel = (float*)(sharedMemory+(kernelWidth+blockDim.x)*sizeof(complex)); + + + // Cache kernel. + for(int i = threadIdx.x; i < kernelWidth; i += blockDim.x) { + cachedKernel[i] = kernel[i]; + } + + // Cache input. + for(int i = sample-kernelWidth/2-1, localIdx = threadIdx.x; + localIdx < (kernelWidth+blockDim.x); i += blockDim.x, localIdx += blockDim.x) { + if (i < 0 || i >= nSamples) { + cachedInputData[localIdx] = 0; + } + else { + cachedInputData[localIdx] = input[ch*nSamples + i]; + } + + } + + __syncthreads(); + + if(idx >= length) { + return; + } + complex result(0.0f, 0.0f); + int localN = threadIdx.x + kernelWidth; + for (int i = 0; i < kernelWidth; ++i) { + result += cachedInputData[localN-i]*cachedKernel[i]; + } + output[idx] = result; +} +''' + +gpu_fir_complex64 = cp.RawKernel(_gpu_fir_complex64_str, "gpu_fir_complex64") + +_DEFAULT_BLOCK_SIZE = 512 + +def get_default_grid_block_size(n_samples, total_n_samples): + block_size = (min((n_samples, _DEFAULT_BLOCK_SIZE)), ) + grid_size = (int((total_n_samples-1) // block_size[0] + 1), ) + return (grid_size, block_size) + + +def get_default_shared_mem_size(n_samples, filter_size): + # filter size (actual filter coefficients) + (block size + filter_size (padding)) + return filter_size*4 + (min(n_samples, _DEFAULT_BLOCK_SIZE) + filter_size)*8 + + +def run_fir(grid_size, block_size, params, shared_mem_size): + """ + :param params: a list: data_out, data_in, input_n_samples, input_total_n_samples, kernel, kernel_size + """ + return gpu_fir_complex64(grid_size, block_size, params, shared_mem=shared_mem_size) + + + + diff --git a/api/python/arrus/utils/imaging.py b/api/python/arrus/utils/imaging.py index 993d037df..1b12d168d 100644 --- a/api/python/arrus/utils/imaging.py +++ b/api/python/arrus/utils/imaging.py @@ -1,477 +1,587 @@ import numpy as np +import math +import scipy import scipy.signal as signal -import matplotlib.pyplot as plt -from collections.abc import Iterable - -def reconstruct_rf_img(rf, x_grid, z_grid, - pitch, fs, fc, c, - tx_aperture, tx_focus, tx_angle, - n_pulse_periods, tx_mode='lin', n_first_samples=0, - use_gpu=0, - ): +import scipy.ndimage +import arrus.metadata +import arrus.devices.cpu +import arrus.devices.gpu +import arrus.kernels.imaging + + +class Pipeline: """ - Function for image reconstruction using delay-and-sum approach. - - :param rf: 3D array of rf signals before beamforming - :param x_grid: vector of pixel x coordinates [m] - :param z_grid: vector of pixel z coordinates [m] - :param pitch: the distance between contiguous elements [m] - :param fs: sampling frequency [Hz] - :param fc: carrier frequency [Hz] - :param c: assumed speed of sound [m/s] - :param tx_aperture: transmit aperture length [elements] - :param tx_focus: transmit focus [m] - :param tx_angle: transmit angle [radians] - :param n_pulse_periods: the length of the pulse in periods - :param tx_mode: imaging mode - lin (classical), - sta (synthetic transmit aperture) - pwi (plane wave imaging) - :param n_first_samples: samples recorded before transmission - :param use_gpu: if 0 - the cpu is used (default), - if 1 - the gpu is used (only for nvidia card with CUDA) - :return: rf beamformed image + Imaging pipeline. + Processes given data,metadata using a given sequence of steps. + The processing will be performed on a given device ('placement'). """ + def __init__(self, steps, placement=None): + self.steps = steps + if placement is not None: + self.set_placement(placement) + + def __call__(self, data): + for step in self.steps: + data = step(data) + return data + + def initialize(self, const_metadata): + input_shape = const_metadata.input_shape + input_dtype = const_metadata.dtype + for step in self.steps: + const_metadata = step._prepare(const_metadata) + # Force cupy to recompile kernels before running the pipeline. + init_array = self.num_pkg.zeros(input_shape, dtype=input_dtype)+1000 + self.__call__(init_array) + return const_metadata + + def set_placement(self, device): + """ + Sets the pipeline to be executed on a particular device. + + :param device: device on which the pipeline should be executed + """ + self.placement = device + # Initialize steps with a proper library. + if isinstance(self.placement, arrus.devices.gpu.GPU): + import cupy as cp + import cupyx.scipy.ndimage as cupy_scipy_ndimage + pkgs = dict(num_pkg=cp, filter_pkg=cupy_scipy_ndimage) + elif isinstance(self.placement, arrus.devices.cpu.CPU): + import scipy.ndimage + pkgs = dict(num_pkg=np, filter_pkg=scipy.ndimage) + else: + raise ValueError(f"Unsupported device: {device}") + for step in self.steps: + step.set_pkgs(**pkgs) + self.num_pkg = pkgs['num_pkg'] + self.filter_pkg = pkgs['filter_pkg'] - tx_angle = np.array(tx_angle) - if tx_angle.size != 1: - tx_angle = np.squeeze(tx_angle) - - if use_gpu: - import cupy as cp - rf = cp.array(rf) - x_grid = cp.array(x_grid) - z_grid = cp.array(z_grid) - tx_angle = cp.array(tx_angle) - print('recontruction using gpu') - xp = cp - else: - print('recontruction using cpu') - xp = np - - if tx_focus is None: - tx_focus = 0 - - # making x and z_grid column vector - z_grid = z_grid[xp.newaxis].T - - # getting size parameters - n_samples, n_channels, n_transmissions = rf.shape - z_size = max(z_grid.shape) - x_size = max(x_grid.shape) - - # check if data is iq (i.e. complex) or 'ordinary' rf (i.e. real) - is_iqdata = isinstance(rf[0, 0, 0], xp.complex) - if is_iqdata: - print('iq (complex) data on input') - else: - print('rf (real) data on input') - - # probe/transducer width - probe_width = (n_channels-1)*pitch - - # x coordinate of transducer elements - element_xcoord = xp.linspace(-probe_width/2, probe_width/2, n_channels) - - # initial delays [s] - delay0 = n_first_samples/fs - burst_factor = 0.5*n_pulse_periods/fc - is_lin_or_sta = tx_mode == 'lin' or tx_mode == 'sta' - if is_lin_or_sta and tx_focus > 0: - focus_delay = (xp.sqrt(((tx_aperture-1)*pitch/2)**2+tx_focus**2) - -tx_focus)/c - else: - focus_delay = 0 - - init_delay = focus_delay+burst_factor+delay0 - - # Delay & Sum - # add zeros as last samples. - # If a sample is out of range 1: nSamp, - # then use the sample no.nSamp + 1 which is 0. - # to be checked if it is faster than irregular memory access. - tail = xp.zeros((1, n_channels, n_transmissions)) - rf = xp.concatenate((rf, tail)) - - # buffers allocation - rf_tx = xp.zeros((z_size, x_size, n_transmissions)) - if is_iqdata: - rf_tx = rf_tx.astype(complex) - - weight_tx = xp.zeros((z_size, x_size, n_transmissions)) - - # loop over transmissions - for itx in range(0, n_transmissions): - - # calculate tx delays and apodization - - # classical linear scanning - # (only a narrow stripe is reconstructed at a time, no tx apodization) - if tx_mode == 'lin': - - # difference between image point x coordinate and element x coord - xdifference = xp.array(x_grid-element_xcoord[itx]) - - # logical indexes of valid x coordinates - lix_valid = (xdifference > (-pitch/2)) & (xdifference <= (pitch/2)) - n_valid = xp.sum(lix_valid) - n_valid = int(n_valid) - - # ix_valid = list(np.nonzero(lix_valid)) - tx_distance = xp.tile(z_grid, (1, n_valid)) - tx_apodization = xp.ones((z_size, n_valid)) - - # synthetic transmit aperture method - elif tx_mode == 'sta': - lix_valid = xp.ones(x_size, dtype=bool) - tx_distance = xp.sqrt((z_grid-tx_focus)**2 - + (x_grid-element_xcoord[itx])**2 - ) - - tx_distance = tx_distance*xp.sign(z_grid-tx_focus) + tx_focus - - f_number = max(abs(z_grid-tx_focus)) - f_number = max(f_number, xp.array(1e-12))\ - /abs(x_grid-element_xcoord[itx])*0.5 - - tx_apodization = f_number > 2 - - elif tx_mode == 'pwi': - lix_valid = xp.ones((x_size), dtype=bool) - - if tx_angle[itx] >= 0: - first_element = 0 - else: - first_element = n_channels-1 - tx_distance = \ - (x_grid-element_xcoord[first_element])*xp.sin(tx_angle[itx]) \ - +z_grid*xp.cos(tx_angle[itx]) +class BandpassFilter: + """ + Bandpass filtering to apply to signal data. - r1 = (x_grid-element_xcoord[0])*xp.cos(tx_angle[itx]) \ - -z_grid*xp.sin(tx_angle[itx]) + A bandwidth [0.5, 1.5]*center_frequency is currently used. - r2 = (x_grid-element_xcoord[-1])*xp.cos(tx_angle[itx]) \ - -z_grid*xp.sin(tx_angle[itx]) + The filtering is performed along the last axis. - tx_apodization = (r1 >= 0) & (r2 <= 0) + Currently only FIR filter is available. + """ - else: - raise ValueError('unknown reconstruction mode!') - - # buffers allocation - rf_rx = xp.zeros((z_size, x_size, n_channels)) - if is_iqdata: - rf_rx = rf_rx.astype(complex) - - weight_rx = xp.zeros((z_size, x_size, n_channels)) - - # loop over elements - for irx in range(0, n_channels): - - # calculate rx delays and apodization - rx_distance = xp.sqrt((x_grid[lix_valid]-element_xcoord[irx])**2 - + z_grid**2) - f_number = abs(z_grid/(x_grid[lix_valid]-element_xcoord[irx])*0.5) - rx_apodization = f_number > 2 - - # calculate total delays [s] - delays = init_delay + (tx_distance+rx_distance)/c - - # calculate sample number to be used in reconstruction - samples = delays*fs+1 - out_of_range = (0 > samples) | (samples > n_samples-1) - samples[out_of_range] = n_samples - - # calculate rf samples (interpolated) and apodization weights - rf_raw_line = rf[:, irx, itx] - ceil_samples = xp.ceil(samples).astype(int) - floor_samples = xp.floor(samples).astype(int) - valid = xp.where(lix_valid)[0].tolist() - rf_rx[:, valid, irx] = rf_raw_line[floor_samples]*(1-(samples % 1)) \ - + rf_raw_line[ceil_samples]*(samples % 1) - weight_rx[:, valid, irx] = tx_apodization*rx_apodization - - # modulate if iq signal is used - if is_iqdata: - rf_rx[:, lix_valid, irx] = rf_rx[:, lix_valid, irx] \ - * xp.exp(1j*2*xp.pi*fc*delays) - pass - - # calculate rf and weights for single tx - rf_tx[:, :, itx] = xp.sum(rf_rx*weight_rx, axis=2) - sumwrx = xp.sum(weight_rx, axis=2) - weight_tx[:, :, itx] = xp.divide(1, sumwrx, - out=xp.zeros_like(sumwrx), - where=sumwrx!=0) - - # show progress - percentage = round((itx+1)/n_transmissions*1000)/10 - if itx == 0: - print('{}%'.format(percentage), end='') - elif itx == n_transmissions-1: - print('\r{}%'.format(percentage)) - else: - print('\r{}%'.format(percentage), end='') + def __init__(self, numtaps=7, bounds=(0.5, 1.5), filter_type="butter", + num_pkg=None, filter_pkg=None): + """ + Bandpass filter constructor. + + :param bounds: determines filter's frequency boundaries, + e.g. setting 0.5 will give a bandpass filter + [0.5*center_frequency, 1.5*center_frequency]. + """ + self.taps = None + self.numtaps = numtaps + self.bound_l, self.bound_r = bounds + self.filter_type = filter_type + self.xp = num_pkg + self.filter_pkg = filter_pkg + + def set_pkgs(self, num_pkg, filter_pkg, **kwargs): + self.xp = num_pkg + self.filter_pkg = filter_pkg + + def _prepare(self, const_metadata: arrus.metadata.ConstMetadata): + l, r = self.bound_l, self.bound_r + center_frequency = const_metadata.context.sequence.pulse.center_frequency + sampling_frequency = const_metadata.data_description.sampling_frequency + # FIXME(pjarosik) implement iir filter + taps, _ = scipy.signal.butter( + 2, + [l * center_frequency, r * center_frequency], + btype='bandpass', fs=sampling_frequency) + self.taps = self.xp.asarray(taps).astype(self.xp.float32) + return const_metadata + + def __call__(self, data): + result = self.filter_pkg.convolve1d(data, self.taps, axis=-1, + mode='constant') + return result + + +class QuadratureDemodulation: + """ + Quadrature demodulation (I/Q decomposition). + """ + def __init__(self, num_pkg=None): + self.mod_factor = None + self.xp = num_pkg + + def set_pkgs(self, num_pkg, **kwargs): + self.xp = num_pkg - # calculate final rf image - rf_image = xp.sum(rf_tx, axis=2)*np.sum(weight_tx, axis=2) + def _is_prepared(self): + return self.mod_factor is not None - if use_gpu: - return cp.asnumpy(rf_image) - else: - return rf_image + def _prepare(self, const_metadata): + xp = self.xp + fs = const_metadata.data_description.sampling_frequency + fc = const_metadata.context.sequence.pulse.center_frequency + _, _, n_samples = const_metadata.input_shape + t = (xp.arange(0, n_samples) / fs).reshape(1, 1, -1) + self.mod_factor = (2 * xp.cos(-2 * xp.pi * fc * t) + + 2 * xp.sin(-2 * xp.pi * fc * t) * 1j) + self.mod_factor = self.mod_factor.astype(xp.complex64) + return const_metadata.copy(is_iq_data=True, dtype="complex64") + def __call__(self, data): + return self.mod_factor * data -def make_bmode_image(rf_image, x_grid, y_grid, db_range=-60): + +class Decimation: """ - The function for creating b-mode image. - - :param rf_image: 2D rf image - :param x_grid: vector of x coordinates - :param y_grid: vector of y coordinates - :param db_range: dynamic range in [dB]. - If int or float, it is the lower bound of dynamic range, - and upper bound equal 0 is assumed. - If list or tuple - min and max values are treated - as bounds of the dynamic range. - :return: + Decimation + CIC (Cascade Integrator-Comb) filter. + + See: https://en.wikipedia.org/wiki/Cascaded_integrator%E2%80%93comb_filter """ - if isinstance(db_range, int) or isinstance(db_range, float): - db_range = [db_range, 0] - min_db, max_db = db_range - if min_db >= max_db: - raise ValueError( - "Bad db_range: max_db (now max_db = {}) " - "should be larger than min_db (now min_db = {})" - .format(max_db, min_db) - ) - - if isinstance(db_range, Iterable): - min_db, max_db = db_range - if min_db >= max_db: + def __init__(self, decimation_factor, cic_order, num_pkg=None, impl="legacy"): + """ + Decimation op constructor. + + :param decimation_factor: decimation factor to apply + :param cic_order: CIC filter order + """ + self.decimation_factor = decimation_factor + self.cic_order = cic_order + self.xp = num_pkg + self.impl = impl + if self.impl == "legacy": + self._decimate = self._legacy_decimate + elif self.impl == "fir": + self._decimate = self._fir_decimate + + def set_pkgs(self, num_pkg, filter_pkg, **kwargs): + self.xp = num_pkg + self.filter_pkg = filter_pkg # not used by the GPU implementation (custom kernel for complex input data) + + def _prepare(self, const_metadata): + new_fs = (const_metadata.data_description.sampling_frequency + / self.decimation_factor) + new_signal_description = arrus.metadata.EchoDataDescription( + sampling_frequency=new_fs, custom= + const_metadata.data_description.custom) + + n_frames, n_channels, n_samples = const_metadata.input_shape + total_n_samples = n_frames*n_channels*n_samples + + output_shape = n_frames, n_channels, n_samples//self.decimation_factor + + # CIC FIR coefficients + if self.impl == "fir": + cicFir = self.xp.array([1], dtype=self.xp.float32) + cicFir1 = self.xp.ones(self.decimation_factor, dtype=self.xp.float32) + for i in range(self.cic_order): + cicFir = self.xp.convolve(cicFir, cicFir1, 'full') + fir_taps = cicFir + n_fir_taps = len(fir_taps) + if self.xp == np: + def _cpu_fir_filter(data): + return self.filter_pkg.convolve1d( + np.real(data), fir_taps, + axis=-1, mode='constant', + cval=0, origin=-1) \ + + self.filter_pkg.convolve1d(np.imag(data), + fir_taps, axis=-1, + mode='constant', cval=0, + origin=-1)*1j + # CPU + self._fir_filter = _cpu_fir_filter + else: + # GPU + import cupy as cp + _fir_output_buffer = cp.zeros(const_metadata.input_shape, + dtype=cp.complex64) + # Kernel settings + from arrus.utils.fir import ( + get_default_grid_block_size, + get_default_shared_mem_size, + run_fir) + grid_size, block_size = get_default_grid_block_size(n_samples, total_n_samples) + shared_memory_size = get_default_shared_mem_size(n_samples, n_fir_taps) + + def _gpu_fir_filter(data): + run_fir(grid_size, block_size, + (_fir_output_buffer, data, n_samples, + total_n_samples, fir_taps, n_fir_taps), + shared_memory_size) + return _fir_output_buffer + + self._fir_filter = _gpu_fir_filter + return const_metadata.copy(data_desc=new_signal_description, + input_shape=output_shape) + + def __call__(self, data): + return self._decimate(data) + + def _fir_decimate(self, data): + fir_output = self._fir_filter(data) + data_out = fir_output[:, :, 0:-1:self.decimation_factor] + return data_out + + def _legacy_decimate(self, data): + data_out = data + for i in range(self.cic_order): + data_out = self.xp.cumsum(data_out, axis=-1) + data_out = data_out[:, :, 0:-1:self.decimation_factor] + for i in range(self.cic_order): + data_out[:, :, 1:] = self.xp.diff(data_out, axis=-1) + return data_out + + +class RxBeamforming: + """ + Rx beamforming. + + Expected input data shape: n_emissions, n_rx, n_samples + + Currently the beamforming op works only for LIN sequence output data. + """ + + def __init__(self, num_pkg=None): + self.delays = None + self.buffer = None + self.rx_apodization = None + self.xp = num_pkg + self.interp1d_func = None + + def set_pkgs(self, num_pkg, **kwargs): + self.xp = num_pkg + if self.xp is np: + import scipy.interpolate + + def numpy_interp1d(input, samples, output): + n_samples = input.shape[-1] + x = np.arange(0, n_samples) + interpolator = scipy.interpolate.interp1d( + x, input, kind="linear", bounds_error=False, + fill_value=0.0) + interp_values = interpolator(samples) + n_scanlines, _, n_samples = interp_values.shape + interp_values = np.reshape(interp_values, (n_scanlines, n_samples)) + output[:] = interp_values + + self.interp1d_func = numpy_interp1d + else: + import cupy as cp + if self.xp != cp: + raise ValueError(f"Unhandled numerical package: {self.xp}") + import arrus.utils.interpolate + self.interp1d_func = arrus.utils.interpolate.interp1d + + def _prepare(self, const_metadata: arrus.metadata.ConstMetadata): + # TODO verify that all angles, focal points are the same + # TODO make sure start_sample is computed appropriately + context = const_metadata.context + probe_model = const_metadata.context.device.probe.model + seq = const_metadata.context.sequence + raw_seq = const_metadata.context.raw_sequence + medium = const_metadata.context.medium + rx_aperture_center_element = np.array(seq.rx_aperture_center_element) + + self.n_tx, self.n_rx, self.n_samples = const_metadata.input_shape + self.is_iq = const_metadata.is_iq_data + if self.is_iq: + buffer_dtype = self.xp.complex64 + else: + buffer_dtype = self.xp.float32 + + # -- Output buffer + self.buffer = self.xp.zeros((self.n_tx, self.n_rx * self.n_samples), + dtype=buffer_dtype) + + # -- Delays + acq_fs = (const_metadata.context.device.sampling_frequency + / seq.downsampling_factor) + fs = const_metadata.data_description.sampling_frequency + fc = seq.pulse.center_frequency + n_periods = seq.pulse.n_periods + if seq.speed_of_sound is not None: + c = seq.speed_of_sound + else: + c = medium.speed_of_sound + tx_angle = 0 # TODO use appropriate tx angle + start_sample = seq.rx_sample_range[0] + rx_aperture_origin = _get_rx_aperture_origin(seq) + + _, _, tx_delay_center = arrus.kernels.imaging.compute_tx_parameters( + seq, probe_model, c) + + burst_factor = n_periods / (2 * fc) + # -start_sample compensates the fact, that the data indices always start from 0 + initial_delay = - start_sample / acq_fs + if seq.init_delay == "tx_start": + burst_factor = n_periods / (2 * fc) + _, _, tx_delay_center = arrus.kernels.imaging.compute_tx_parameters( + seq, probe_model, c) + initial_delay += tx_delay_center + burst_factor + elif not seq.init_delay == "tx_center": + raise ValueError(f"Unrecognized init_delay value: {initial_delay}") + + radial_distance = ( + (start_sample / acq_fs + np.arange(0, self.n_samples) / fs) + * c / 2 + ) + x_distance = (radial_distance * np.sin(tx_angle)).reshape(1, -1) + z_distance = radial_distance * np.cos(tx_angle).reshape(1, -1) + + origin_offset = (rx_aperture_origin[0] + - (seq.rx_aperture_center_element[0])) + # New coordinate system: origin: rx aperture center + element_position = ((np.arange(0, self.n_rx) + origin_offset) + * probe_model.pitch) + element_position = element_position.reshape((self.n_rx, 1)) + if not probe_model.is_convex_array(): + element_angle = np.zeros((self.n_rx, 1)) + element_x = element_position + element_z = np.zeros((self.n_rx, 1)) + else: + element_angle = element_position / probe_model.curvature_radius + element_x = probe_model.curvature_radius * np.sin(element_angle) + element_z = probe_model.curvature_radius * ( + np.cos(element_angle) - 1) + + tx_distance = radial_distance + rx_distance = np.sqrt( + (x_distance - element_x) ** 2 + (z_distance - element_z) ** 2) + + self.t = (tx_distance + rx_distance) / c + initial_delay + self.delays = self.t * fs # in number of samples + total_n_samples = self.n_rx * self.n_samples + # Move samples outside the available area + self.delays[np.isclose(self.delays, self.n_samples-1)] = self.n_samples-1 + self.delays[self.delays > self.n_samples-1] = total_n_samples + 1 + # (RF data will also be unrolled to a vect. n_rx*n_samples elements, + # row-wise major order). + self.delays = self.xp.asarray(self.delays) + self.delays += self.xp.arange(0, self.n_rx).reshape(self.n_rx, 1) \ + * self.n_samples + self.delays = self.delays.reshape(-1, self.n_samples * self.n_rx) \ + .astype(self.xp.float32) + # Apodization + lambd = c / fc + max_tang = math.tan( + math.asin(min(1, 2 / 3 * lambd / probe_model.pitch))) + rx_tang = np.abs(np.tan(np.arctan2(x_distance - element_x, + z_distance - element_z) - element_angle)) + rx_apodization = (rx_tang < max_tang).astype(np.float32) + rx_apod_sum = np.sum(rx_apodization, axis=0) + rx_apod_sum[rx_apod_sum == 0] = 1 + rx_apodization = rx_apodization/(rx_apod_sum.reshape(1, self.n_samples)) + self.rx_apodization = self.xp.asarray(rx_apodization) + # IQ correction + self.t = self.xp.asarray(self.t) + self.iq_correction = self.xp.exp(1j * 2 * np.pi * fc * self.t) \ + .astype(self.xp.complex64) + # Create new output shape + return const_metadata.copy(input_shape=(self.n_tx, self.n_samples)) + + def __call__(self, data): + data = data.copy().reshape(self.n_tx, self.n_rx * self.n_samples) + + self.interp1d_func(data, self.delays, self.buffer) + out = self.buffer.reshape((self.n_tx, self.n_rx, self.n_samples)) + if self.is_iq: + out = out * self.iq_correction + out = out * self.rx_apodization + out = self.xp.sum(out, axis=1) + return out.reshape((self.n_tx, self.n_samples)) + + +class EnvelopeDetection: + """ + Envelope detection (Hilbert transform). + + Currently this op works only for I/Q data (complex64). + """ + + def __init__(self, num_pkg=None): + self.xp = num_pkg + + def set_pkgs(self, num_pkg, **kwargs): + self.xp = num_pkg + + def _prepare(self, const_metadata: arrus.metadata.ConstMetadata): + return const_metadata.copy(is_iq_data=False) + + def __call__(self, data): + if data.dtype != self.xp.complex64: raise ValueError( - "Bad db_range: max_db (now max_db = {}) " - "should be larger than min_db (now min_db = {})" - .format(max_db, min_db) - ) - - # check if 'rf' or 'iq' data on input - is_iqdata = isinstance(rf_image[1, 1], np.complex) - - dx = x_grid[1]-x_grid[0] - dy = y_grid[1]-y_grid[0] - - # calculate envelope - if is_iqdata: - amplitude_image = np.abs(rf_image) - else: - amplitude_image = np.abs(signal.hilbert(rf_image, axis=0)) - - # convert do dB - max_image_value = np.max(amplitude_image) - bmode_image = np.log10(amplitude_image/max_image_value)*20 - - # calculate ticks and labels - n_samples, n_lines = rf_image.shape - image_height = (n_samples-1)*dy - image_height = y_grid[-1]-y_grid[0] - # max_depth = image_depth + depth0 - # max_depth = z_grid[-1] - # image_width = (n_lines - 1)*dx - image_width = x_grid[-1]-x_grid[0] - image_proportion = image_height/image_width - - n_xticks = 4 - n_yticks = int(round(n_xticks*image_proportion)) - - xticks = np.linspace(0, n_lines-1, n_xticks) - xtickslabels = np.linspace(x_grid[0], x_grid[-1], n_xticks)*1e3 - xtickslabels = np.round(xtickslabels, 1) - - yticks = np.linspace(0, n_samples-1, n_yticks) - ytickslabels = np.linspace(y_grid[0], y_grid[-1], n_yticks)*1e3 - ytickslabels = np.round(ytickslabels, 1) - - # calculate data aspect for proper image proportions - data_aspect = dy/dx - - # show the image - - plt.imshow(bmode_image, - interpolation='bicubic', - aspect=data_aspect, - cmap='gray', - vmin=db_range[0], vmax=db_range[1] - ) - - plt.xticks(xticks, xtickslabels) - plt.yticks(yticks, ytickslabels) - - cbar = plt.colorbar() - cbar.ax.get_yaxis().labelpad=10 - cbar.ax.set_ylabel('[dB]', rotation=90) - plt.xlabel('[mm]') - plt.ylabel('[mm]') -# plt.show() - - -def compute_tx_delays(angles, focus, pitch, c=1490, n_chanels=128): + f"Data type {data.dtype} is currently not supported.") + return self.xp.abs(data) + + +class Transpose: """ - Computes Tx delays using given parameters. - - - :param angles: Transmission angles [rad]. - Can be a number or a list (for multiple angles). - :param focus: Focal length [m]. - :param pitch: Pitch [m] - :param c: Speed of sound [m/s]. Default value is 1490. - :param n_chanels: Number of channels/transducers. Default value is 128. - :return: Ndarray of delays. - Its shape is (number of angles, number of channels). + Data transposition. """ - # transducer indexes - x_i = np.linspace(0, n_chanels-1, n_chanels) + def __init__(self, axes=None): + self.axes = axes + self.xp = None - # transducer coordinates - x_c = x_i*pitch + def set_pkgs(self, num_pkg, **kwargs): + self.xp = num_pkg - angles = np.array(angles) - n_angles = angles.size - if n_angles != 0: - # reducing possible singleton dimensions of 'angles' - angles = np.squeeze(angles) - if angles.shape == (): - angles = np.array([angles]) + def _prepare(self, const_metadata): + input_shape = const_metadata.input_shape + axes = list(range(len(input_shape)))[::-1] if self.axes is None else self.axes + output_shape = tuple(input_shape[ax] for ax in axes) + return const_metadata.copy(input_shape=output_shape) - # allocating memory for delays - delays = np.zeros(shape=(n_angles, n_chanels)) + def __call__(self, data): + return self.xp.transpose(data, self.axes) - # calculating delays for each angle - for i_angle in range(0, n_angles): - this_angle = angles[i_angle] - this_delays = x_c*np.sin(this_angle)/c - if this_angle < 0: - this_delays = this_delays-this_delays[-1] - delays[i_angle, :] = this_delays - else: - delays = np.zeros(shape=(1, n_chanels)) - focus = np.array(focus) - if focus.size == 0: - return delays +class ScanConversion: + """ + Scan conversion (interpolation to target mesh). + + Currently linear interpolation is used by default, values outside + the input mesh will be set to 0.0. + + Currently the op is implement for CPU only. + :param x_grid: a vector of grid points along OX axis [m] + :param z_grid: a vector of grid points along OZ axis [m] + """ - elif focus.size == 1: - xf = (n_chanels-1)*pitch/2 - yf = focus + def __init__(self, x_grid, z_grid): + self.dst_points = None + self.dst_shape = None + self.x_grid = x_grid.reshape(1, -1) + self.z_grid = z_grid.reshape(1, -1) + self.is_gpu = False + + def set_pkgs(self, num_pkg, **kwargs): + if num_pkg != np: + self.is_gpu = True + # Ignoring provided num. package - currently CPU implementation is + # available only. + + def _prepare(self, const_metadata: arrus.metadata.ConstMetadata): + probe = const_metadata.context.device.probe.model + medium = const_metadata.context.medium + data_desc = const_metadata.data_description + + if not probe.is_convex_array(): + raise ValueError( + "Scan conversion currently works for convex probes data only.") - elif focus.size == 2: - xf = focus[0] + (n_chanels-1)*pitch/2 - yf = focus[1] + n_samples, _ = const_metadata.input_shape + seq = const_metadata.context.sequence + custom_data = const_metadata.context.custom_data - else: - print('Bad focus value, set to [] (plane wave)') - return delays + acq_fs = (const_metadata.context.device.sampling_frequency + / seq.downsampling_factor) + fs = data_desc.sampling_frequency - # distance between origin of coordinate system and focus - s0 = np.sqrt(yf**2+xf**2) - focus_sign = np.sign(yf) + start_sample = seq.rx_sample_range[0] - # cosinus of the angle between array (y=0) and focus position vector - if s0 == 0: - cos_alpha = 0 - else: - cos_alpha = xf/s0 + if seq.speed_of_sound is not None: + c = seq.speed_of_sound + else: + c = medium.speed_of_sound - # distances between elements and focus - si = np.sqrt(s0**2 + x_c**2 - 2*s0*x_c*cos_alpha) + tx_ap_cent_ang, _, _ = arrus.kernels.imaging.get_tx_aperture_center_coords(seq, probe) - # focusing delays - delays_foc = (s0-si)/c - delays_foc = delays_foc*focus_sign + z_grid_moved = self.z_grid.T + probe.curvature_radius - np.max( + probe.element_pos_z) - # set min(delays_foc) as delay==0 - d0 = np.min(delays_foc) - delays_foc = delays_foc - d0 + self.radGridIn = ( + (start_sample / acq_fs + np.arange(0, n_samples) / fs) + * c / 2) - # full delays - delays = delays + delays_foc + self.azimuthGridIn = tx_ap_cent_ang + azimuthGridOut = np.arctan2(self.x_grid, z_grid_moved) + radGridOut = (np.sqrt(self.x_grid ** 2 + z_grid_moved ** 2) + - probe.curvature_radius) - return delays + dst_points = np.dstack((radGridOut, azimuthGridOut)) + w, h, d = dst_points.shape + self.dst_points = dst_points.reshape((w * h, d)) + self.dst_shape = len(self.z_grid.squeeze()), len(self.x_grid.squeeze()) + return const_metadata.copy(input_shape=self.dst_shape) + def __call__(self, data): + if self.is_gpu: + data = data.get() + data[np.isnan(data)] = 0.0 + self.interpolator = scipy.interpolate.RegularGridInterpolator( + (self.radGridIn, self.azimuthGridIn), data, method="linear", + bounds_error=False, fill_value=0) + return self.interpolator(self.dst_points).reshape(self.dst_shape) -def calculate_envelope(rf): + +class LogCompression: """ - The function calculate envelope using hilbert transform - :param rf: - :return: envelope image + Converts to decibel scale. """ - envelope = np.abs(signal.hilbert(rf, axis=0)) - return envelope + def __init__(self): + pass + def set_pkgs(self, **kwargs): + # Intentionally ignoring num. package - + # currently numpy is available only. + pass -def rf2iq(rf, fc, fs, decimation_factor): - """ - Demodulation and decimation from rf signal to iq (quadrature) signal. + def _prepare(self, const_metadata: arrus.metadata.ConstMetadata): + return const_metadata - :param rf: array of rf signals - :param fc: carrier frequency - :param fs: sampling frequency - :param decimation_factor: decimation factor - :return: array of decimated iq signals + def __call__(self, data): + if not isinstance(data, np.ndarray): + data = data.get() + data[data == 0] = 1e-9 + return 20 * np.log10(data) - """ - s = rf.shape - n_dim = len(s) - n_samples = s[0] +class DynamicRangeAdjustment: + + def __init__(self, min=20, max=80): + self.min = min + self.max = max + self.xp = None + + def set_pkgs(self, num_pkg, **kwargs): + self.xp = num_pkg - if n_dim > 1: - n_channels = s[1] - else: - n_channels = 1 - rf = rf[..., np.newaxis] + def _prepare(self, const_metadata: arrus.metadata.ConstMetadata): + return const_metadata - if n_dim > 2: - n_transmissions = s[2] - else: - n_transmissions = 1 - rf = rf[..., np.newaxis] - # creating time array - ts = 1/fs - t = np.linspace(0, (n_samples-1)*ts, n_samples) - t = t[..., np.newaxis, np.newaxis] + def __call__(self, data): + return self.xp.clip(data, a_min=self.min, a_max=self.max) - # demodulation - iq = rf*np.exp(0-1j*2*np.pi*fc*t) +class ToGrayscaleImg: + def __init__(self): + self.xp = None - # low-pass filtration (assuming 150% band) - f_up_cut = fc*1.5/2 + def set_pkgs(self, num_pkg, **kwargs): + self.xp = num_pkg - # ir - filter_order = 8 - b, a = signal.butter(filter_order, - f_up_cut, - btype='low', - analog=False, - output='ba', - fs=fs - ) + def _prepare(self, const_metadata: arrus.metadata.ConstMetadata): + return const_metadata - # this scaling of amplitude is to make envelopes from iq and rf similar - iq = 2*signal.filtfilt(b, a, iq, axis=0) + def __call__(self, data): + data = data - self.xp.min(data) + data = data/self.xp.max(data)*255 + return data.astype(self.xp.uint8) - # decimation - if decimation_factor > 1: - iq = signal.decimate(iq, decimation_factor, axis=0) - else: - print('decimation factor <= 1, no decimation') - iq = np.squeeze(iq) +def _get_rx_aperture_origin(sequence): + rx_aperture_size = sequence.rx_aperture_size + rx_aperture_center_element = np.array(sequence.rx_aperture_center_element) + rx_aperture_origin = np.round(rx_aperture_center_element - + (rx_aperture_size - 1) / 2 + 1e-9) + return rx_aperture_origin - return iq diff --git a/api/python/arrus/utils/interpolate.py b/api/python/arrus/utils/interpolate.py new file mode 100644 index 000000000..2eac6a2dd --- /dev/null +++ b/api/python/arrus/utils/interpolate.py @@ -0,0 +1,74 @@ +""" +Cupy implementation of the scipy.interpolate.interp1d function. +The gpu function is currently very limited - it allows only for linear +interpolation, with constant value (equal 0.0) when extrapolating data. +""" +import cupy as cp + + +# TODO(pjarosik) move to .cuh +_interp1d_kernel_str = r''' + #include + extern "C" __global__ + void interp1d_kernel_%%dtype_name%%( + const %%dtype%%* __restrict__ data, const size_t dataWidth, const size_t dataHeight, + const float* __restrict__ samples, + %%dtype%%* __restrict__ output, const size_t outputWidth, const size_t outputHeight){ + + int xt = blockDim.x * blockIdx.x + threadIdx.x; + + if(xt >= outputWidth) { + return; + } + + float samplePos = samples[xt]; + int sampleNr = floorf(samplePos); + float ratio = samplePos - sampleNr; + + for(int i = 0; i < outputHeight; ++i) { + size_t outputOffset = i*outputWidth; + size_t dataOffset = i*dataWidth; + + if( (sampleNr < 0) + || (sampleNr >= dataWidth) + || (sampleNr == dataWidth-1 && ratio != 0.0f)) { // right border + epsilon + // extrapolation + output[xt+outputOffset] = 0; + } + else { + // interpolation + output[xt+outputOffset] = (1-ratio)*data[sampleNr+dataOffset] + + ratio*data[sampleNr+1+dataOffset]; + } + } + }''' + +_interp1d_kernel_complex64 = cp.RawKernel(_interp1d_kernel_str + .replace("%%dtype%%", + "complex") + .replace("%%dtype_name%%", + "complex64"), + "interp1d_kernel_complex64") +_interp1d_kernel_float32 = cp.RawKernel(_interp1d_kernel_str + .replace("%%dtype%%", "float") + .replace("%%dtype_name%%", "float32"), + "interp1d_kernel_float32") + + +def interp1d(input_data, samples, output_data): + samples = samples.squeeze() + if samples.ndim > 1: + raise ValueError("'samples' should be a 1D vector.") + blockSize = (512,) + output_height, output_width = output_data.shape + input_height, input_width = input_data.shape + gridSize = (int((output_width - 1) // blockSize[0] + 1),) + params = (input_data, input_width, input_height, + samples, + output_data, output_width, output_height) + if input_data.dtype == cp.complex64: + return _interp1d_kernel_complex64(gridSize, blockSize, params) + elif input_data.dtype == cp.float32: + return _interp1d_kernel_float32(gridSize, blockSize, params) + else: + raise ValueError(f"Unsupported data type: {input_data.dtype}") \ No newline at end of file diff --git a/api/python/arrus/utils/us4r.py b/api/python/arrus/utils/us4r.py new file mode 100644 index 000000000..012f64c21 --- /dev/null +++ b/api/python/arrus/utils/us4r.py @@ -0,0 +1,224 @@ +import dataclasses +import numpy as np + +import arrus.metadata +import arrus.exceptions +from arrus.utils.us4r_remap_gpu import get_default_grid_block_size, run_remap + + +@dataclasses.dataclass +class Transfer: + src_frame: int + src_range: tuple + dst_frame: int + dst_range: tuple + + +def get_batch_data(data, metadata, frame_nr): + batch_size = metadata.data_description.custom["frame_channel_mapping"].batch_size + n_samples = metadata.context.raw_sequence.get_n_samples() + if len(n_samples) > 1: + raise ValueError("This function doesn't support tx/rx sequences with variable number of samples") + n_samples = next(iter(n_samples)) + + # TODO here is an assumption, that each output frame has exactly the same number of samples + # This might not be the case in the future. + # Data from the first module. + firstm_n_scanlines = metadata.custom["frame_metadata_view"].shape[0] + # Number of scanlines in a single RF frame + assert firstm_n_scanlines % batch_size == 0, "Incorrect number of the result scanlines and samples." + firstm_n_scanlines_frame = firstm_n_scanlines // batch_size + + # Number of sample for the first module + firstm_n_samples_frame = firstm_n_scanlines_frame * n_samples + + first = data[frame_nr*firstm_n_samples_frame: + (frame_nr+1)*firstm_n_samples_frame, :] + + # Data from the second module. + # FIXME: here is an assumption, that there are no rx nops in the sequence + # This may not be the case in the future + offset = firstm_n_scanlines * n_samples # the number of samples + total_n_scanlines = np.max(metadata.data_description.custom["frame_channel_mapping"].frames+1)*batch_size + secondm_n_scanlines = total_n_scanlines - firstm_n_scanlines + assert secondm_n_scanlines % batch_size == 0, "Incorrect number of " \ + "the result scanlines " \ + "and samples." + assert firstm_n_scanlines == secondm_n_scanlines + secondm_n_scanlines_frame = secondm_n_scanlines // batch_size + secondm_n_samples_frame = secondm_n_scanlines_frame * n_samples + + second = data[frame_nr*secondm_n_samples_frame+offset: + (frame_nr+1)*secondm_n_samples_frame+offset, :] + return np.concatenate((first, second), axis=0) + + +def get_batch_metadata(metadata, frame_nr): + batch_size = metadata.data_description.custom["frame_channel_mapping"].batch_size + # Number of scanlines in the first module + n_scanlines_total = metadata.custom["frame_metadata_view"].shape[0] + n_samples_in_scanline = n_scanlines_total // batch_size + frame_metadata_view = metadata.custom["frame_metadata_view"][ + frame_nr*n_samples_in_scanline:(frame_nr+1)*n_samples_in_scanline] + new_metadata = arrus.metadata.Metadata(context=metadata.context, + data_desc=metadata.data_description, + custom={"frame_metadata_view" : + frame_metadata_view}) + return new_metadata + + +def group_transfers(frame_channel_mapping): + result = [] + frame_mapping = frame_channel_mapping.frames + channel_mapping = frame_channel_mapping.channels + + if frame_mapping.size == 0 or channel_mapping.size == 0: + raise RuntimeError("Empty frame channel mappings") + + # Number of logical frames + n_frames, n_channels = channel_mapping.shape + + for dst_frame in range(n_frames): + current_dst_range = None + + prev_src_frame = None + prev_src_channel = None + current_src_frame = None + current_src_range = None + + for dst_channel in range(n_channels): + src_frame = frame_mapping[dst_frame, dst_channel] + src_channel = channel_mapping[dst_frame, dst_channel] + + if src_channel < 0: + # Omit current channel. + # Negative src channel means, that the given channel + # is not available and should be treated as missing. + continue + + if (prev_src_frame is None # the first transfer + # new src frame + or src_frame != prev_src_frame + # a gap in current frame + or src_channel != prev_src_channel+1): + # Close current source range + if current_src_frame is not None: + transfer = Transfer( + src_frame=current_src_frame, + src_range=tuple(current_src_range), + dst_frame=dst_frame, + dst_range=tuple(current_dst_range) + ) + result.append(transfer) + # Start a new range + current_src_frame = src_frame + # [start, end) + current_src_range = [src_channel, src_channel + 1] + current_dst_range = [dst_channel, dst_channel + 1] + else: + # Continue current range + current_src_range[1] = src_channel + 1 + current_dst_range[1] = dst_channel + 1 + prev_src_frame = src_frame + prev_src_channel = src_channel + # End a range for current frame. + current_src_range = int(current_src_range[0]), int(current_src_range[1]) + transfer = Transfer( + src_frame=int(current_src_frame), + src_range=tuple(current_src_range), + dst_frame=dst_frame, + dst_range=tuple(current_dst_range) + ) + result.append(transfer) + return result + + +def remap(output_array, input_array, transfers): + input_array = input_array + for t in transfers: + dst_l, dst_r = t.dst_range + src_l, src_r = t.src_range + output_array[t.dst_frame, :, dst_l:dst_r] = \ + input_array[t.src_frame, :, src_l:src_r] + + +class RemapToLogicalOrder: + """ + Remaps the order of the data to logical order defined by the us4r device. + + If the batch size was equal 1, the raw ultrasound RF data with shape. + (n_frames, n_samples, n_channels). + A single metadata object will be returned. + + If the batch size was > 1, the the raw ultrasound RF data with shape + (n_us4oems*n_samples*n_frames*n_batches, 32) will be reordered to + (batch_size, n_frames, n_samples, n_channels). A list of metadata objects + will be returned. + """ + + def __init__(self, num_pkg=None): + self._transfers = None + self._output_buffer = None + self.xp = num_pkg + self.remap = None + + def set_pkgs(self, num_pkg, **kwargs): + self.xp = num_pkg + + def _is_prepared(self): + return self._transfers is not None and self._output_buffer is not None + + def _prepare(self, const_metadata: arrus.metadata.ConstMetadata): + xp = self.xp + # get shape, create an array with given shape + # create required transfers + # perform the transfers + fcm = const_metadata.data_description.custom["frame_channel_mapping"] + n_frames, n_channels = fcm.frames.shape + n_samples_set = {op.rx.get_n_samples() + for op in const_metadata.context.raw_sequence.ops} + if len(n_samples_set) > 1: + raise arrus.exceptions.IllegalArgumentError( + f"Each tx/rx in the sequence should acquire the same number of " + f"samples (actual: {n_samples_set})") + n_samples = next(iter(n_samples_set)) + self.output_shape = (n_frames, n_samples, n_channels) + self._output_buffer = xp.zeros(shape=self.output_shape, dtype=xp.int16) + + n_samples_raw, n_channels_raw = const_metadata.input_shape + self._input_shape = (n_samples_raw//n_samples, n_samples, + n_channels_raw) + self.batch_size = fcm.batch_size + + if xp == np: + # CPU + self._transfers = group_transfers(fcm) + + def cpu_remap_fn(data): + remap(self._output_buffer, + data.reshape(self._input_shape), + transfers=self._transfers) + self._remap_fn = cpu_remap_fn + else: + # GPU + print("Using new GPU kernel") + import cupy as cp + self._fcm_frames = cp.asarray(fcm.frames) + self._fcm_channels = cp.asarray(fcm.channels) + self.grid_size, self.block_size = get_default_grid_block_size(self._fcm_frames, n_samples) + + def gpu_remap_fn(data): + run_remap( + self.grid_size, self.block_size, + [self._output_buffer, data, + self._fcm_frames, self._fcm_channels, + n_frames, n_samples, n_channels]) + + self._remap_fn = gpu_remap_fn + + return const_metadata.copy(input_shape=self.output_shape) + + def __call__(self, data): + self._remap_fn(data) + return self._output_buffer + diff --git a/api/python/arrus/utils/us4r_remap_gpu.py b/api/python/arrus/utils/us4r_remap_gpu.py new file mode 100644 index 000000000..434e2f0e6 --- /dev/null +++ b/api/python/arrus/utils/us4r_remap_gpu.py @@ -0,0 +1,47 @@ +import cupy as cp + +_arrus_remap_str = r''' + // Naive implementation of data remapping (physical -> logical order). + extern "C" + __global__ void arrus_remap(short* out, short* in, + const short* fcmFrames, + const char* fcmChannels, + const unsigned nFrames, const unsigned nSamples, const unsigned nChannels) + { + int x = blockIdx.x * 32 + threadIdx.x; // logical channel + int y = blockIdx.y * 32 + threadIdx.y; // logical sample + int z = blockIdx.z; // logical frame + if(x >= nChannels || y >= nSamples || z >= nFrames) { + // outside the range + return; + } + int indexOut = x + y*nChannels + z*nChannels*nSamples; + int physicalChannel = fcmChannels[x + nChannels*z]; + if(physicalChannel < 0) { + // channel is turned off + return; + } + int physicalFrame = fcmFrames[x + nChannels*z]; + // 32 - number of channels in the physical mapping + int indexIn = physicalChannel + y*32 + physicalFrame*32*nSamples; + out[indexOut] = in[indexIn]; + }''' + + +remap_kernel = cp.RawKernel(_arrus_remap_str, "arrus_remap") + + +def get_default_grid_block_size(fcm_frames, n_samples): + # Note the kernel implementation + block_size = (32, 32) + n_frames, n_channels = fcm_frames.shape + grid_size = (int((n_channels - 1) // block_size[0] + 1), int((n_samples - 1) // block_size[1] + 1), n_frames) + return (grid_size, block_size) + + +def run_remap(grid_size, block_size, params): + """ + :param params: a list: data_out, data_in, fcm_frames, fcm_channels, n_frames, n_samples, n_channels + """ + return remap_kernel(grid_size, block_size, params) + diff --git a/api/python/arrus/validation.py b/api/python/arrus/validation.py index 1687167ef..b94deac5a 100644 --- a/api/python/arrus/validation.py +++ b/api/python/arrus/validation.py @@ -1,20 +1,25 @@ from collections.abc import Iterable +import arrus.exceptions + def assert_equal(value, expected, parameter_name): if value != expected: - raise InvalidParameterError(parameter_name, f"should be " + raise arrus.exceptions.IllegalArgumentError(parameter_name, + f"should be " f"equal {expected}") def assert_not_none(value, parameter_name): if value is None: - raise InvalidParameterError(parameter_name, "should be not None") + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "should be not None") def assert_none(value, parameter_name): if value is not None: - raise InvalidParameterError(parameter_name, "should not be provided") + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "should not be provided") def assert_shape(array, expected_shape, parameter_name, strict=False): @@ -23,14 +28,15 @@ def assert_shape(array, expected_shape, parameter_name, strict=False): else: shape = array.flatten().shape if shape != expected_shape: - raise InvalidParameterError(parameter_name, "expected shape: %s " % - expected_shape) + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "expected shape: %s " % + expected_shape) def assert_not_greater_than(value, maximum, parameter_name): if value > maximum: - raise InvalidParameterError(parameter_name, - "should not be greater than %d" % maximum) + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "should not be greater than %d" % maximum) def assert_in_range(actual, expected, parameter_name): @@ -39,10 +45,11 @@ def assert_in_range(actual, expected, parameter_name): else: a_start, a_end = actual e_start, e_end = expected - if not(a_start >= e_start and a_end <= e_end): - raise InvalidParameterError(parameter_name, - "%s expected in range %s" % - (str(actual), str(expected))) + if not (a_start >= e_start and a_end <= e_end): + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "%s expected in range %s" % + ( + str(actual), str(expected))) def assert_one_of(value, collection, parameter_name): @@ -50,30 +57,25 @@ def assert_one_of(value, collection, parameter_name): assert isinstance(collection, Iterable) s = set(collection) if value not in s: - raise InvalidParameterError(parameter_name, "should be one of: %s" % str(s)) + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "should be one of: %s" % str( + s)) def assert_non_negative(value, parameter_name): if value < 0: - raise InvalidParameterError(parameter_name, "should be non-negative") + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "should be non-negative") def assert_positive(value, parameter_name): if value <= 0: - raise InvalidParameterError(parameter_name, "should be positive") + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "should be positive") def assert_type(o, t, parameter_name): if not isinstance(o, t): - raise InvalidParameterError(parameter_name, - "should be of type: %s" % str(t)) - - -class InvalidParameterError(ValueError): - MSG_PATTERN = "Invalid parameter '%s': %s" - - def __init__(self, param, msg): - super().__init__(InvalidParameterError.MSG_PATTERN % (param, msg)) - - - + raise arrus.exceptions.IllegalArgumentError(parameter_name, + "should be of type: %s" % str( + t)) diff --git a/api/python/examples/acquire_batch.py b/api/python/examples/acquire_batch.py new file mode 100644 index 000000000..33907cfbb --- /dev/null +++ b/api/python/examples/acquire_batch.py @@ -0,0 +1,179 @@ +import arrus +import numpy as np +import matplotlib.pyplot as plt +import cupy as cp +import pickle +import time +import argparse +import os +from datetime import datetime + +from arrus.ops.imaging import LinSequence +from arrus.ops.us4r import Pulse + +from arrus.utils.imaging import ( + Pipeline, + BandpassFilter, + QuadratureDemodulation, + Decimation, + RxBeamforming, + EnvelopeDetection, + Transpose, + ScanConversion, + LogCompression, + DynamicRangeAdjustment, + ToGrayscaleImg +) +from arrus.utils.us4r import ( + RemapToLogicalOrder, + get_batch_data, + get_batch_metadata +) + +# Acquisition parameters. +VOLTAGE = 50 +TX_RX_SEQUENCE = LinSequence( + tx_aperture_center_element=np.arange(8, 183), + tx_aperture_size=64, + tx_focus=20e-3, + pulse=Pulse(center_frequency=8e6, n_periods=3.5, inverse=False), + rx_aperture_center_element=np.arange(8, 183), + rx_aperture_size=64, + rx_sample_range=(0, 2048), + pri=100e-6, + tgc_start=14, + tgc_slope=2e2, + downsampling_factor=2, + speed_of_sound=1480) + + +arrus.set_clog_level(arrus.logging.INFO) +arrus.add_log_file("test.log", arrus.logging.TRACE) + + +def init_display(aperture_size, n_samples): + fig, ax = plt.subplots() + fig.set_size_inches((7, 7)) + ax.set_xlabel("OX") + ax.set_ylabel("OZ") + image_w, image_h = aperture_size, n_samples + canvas = plt.imshow(np.zeros((image_w, image_h)), + vmin=np.iinfo(np.uint8).min, + vmax=np.iinfo(np.uint8).max, + cmap="gray") + fig.show() + return fig, ax, canvas + + +prev_timestamp = 0 + + +def display_data(frame_number, data, metadata, imaging_pipeline, figure, + ax, canvas): + global prev_timestamp + bmode, metadata = imaging_pipeline(cp.asarray(data), metadata) + frame_metadata = metadata.custom["frame_metadata_view"][0, :].copy().view(np.int8) + trigger_counter = frame_metadata[0:8].view(np.uint64).item() + timestamp = frame_metadata[8:16].view(np.uint64).item() / 65e6 + pulse_counter = frame_metadata[16:20].view(np.uint32).item() + outa_counter = frame_metadata[20:24].view(np.uint32).item() + outb_counter = frame_metadata[24:28].view(np.uint32).item() + canvas.set_data(bmode) + ax.set_aspect("auto") + + diff = timestamp - prev_timestamp + prev_timestamp = timestamp + ax.set_xlabel(f"OX,\n frame: {frame_number}, " + f"\n trigger counter: {trigger_counter}, " + + "timestamp: %.3f (diff: %.5f), pulse: %d, " % (timestamp, diff, pulse_counter) + + f"outa: {outa_counter}, " + f"outb: {outb_counter}") + figure.canvas.flush_events() + plt.draw() + + +def acquire_data(cfg_path, batch_size, output_directory, timestamp): + # Here starts communication with the device. + session = arrus.session.Session(cfg_path) + us4r = session.get_device("/Us4R:0") + # Set initial voltage on the us4r-lite device. + us4r.set_hv_voltage(VOLTAGE) + # Upload sequence on the us4r-lite device. + buffer = us4r.upload(TX_RX_SEQUENCE, mode="sync", + host_buffer_size=batch_size, + rx_batch_size=batch_size) + # Start the device. + us4r.start() + print("Acquiring data.") + data, metadata = buffer.tail() + np.save(os.path.join(output_directory, f"rf_{timestamp}.npy"), data) + with open(os.path.join(output_directory, f"metadata_{timestamp}.pkl"), + "wb") as f: + pickle.dump(metadata, f) + buffer.release_tail() + us4r.stop() + print("Data acquired.") + + +def main(): + parser = argparse.ArgumentParser( + description="The script acquires a sequence of RF data and saves " + "them to given directory.") + + parser.add_argument("--cfg", dest="cfg", + help="Path to session configuration file.", + required=True) + parser.add_argument("--batch_size", dest="batch_size", + help="Number of frame to acquire and save.", + required=True, type=int) + parser.add_argument("--output_directory", dest="output_directory", + help="Directory where to save the data " + "The filename will have a format " + "rf_{current time}.npy.", + required=True) + args = parser.parse_args() + timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + + acquire_data(args.cfg, args.batch_size, args.output_directory, timestamp) + + # Open and reconstruct image. + session = arrus.session.Session(mock={}) + gpu = session.get_device("/GPU:0") + x_grid = np.arange(-50, 50, 0.2)*1e-3 + z_grid = np.arange(0, 60, 0.2)*1e-3 + pipeline = Pipeline( + steps=( + RemapToLogicalOrder(), + Transpose(axes=(0, 2, 1)), + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=4, cic_order=2), + RxBeamforming(), + EnvelopeDetection(), + Transpose(), + ScanConversion(x_grid=x_grid, z_grid=z_grid), + LogCompression(), + DynamicRangeAdjustment(min=20, max=80), + ToGrayscaleImg()), + placement=gpu) + + fig, ax, canvas = init_display(len(x_grid), len(z_grid)) + + print("Loading saved data.") + data_file_path = os.path.join(args.output_directory, + f"rf_{timestamp}.npy") + metadata_file_path = os.path.join(args.output_directory, + f"metadata_{timestamp}.pkl") + batch_data = np.load(data_file_path) + batch_metadata = pickle.load(open(metadata_file_path, 'rb')) + + print("Displaying the data") + batch_size = batch_metadata.data_description.custom["frame_channel_mapping"].batch_size + for i in range(batch_size): + data = get_batch_data(batch_data, batch_metadata, i) + metadata = get_batch_metadata(batch_metadata, i) + display_data(i, data, metadata, pipeline, fig, ax, canvas) + + +if __name__ == "__main__": + main() diff --git a/api/python/examples/channels_mask_test.py b/api/python/examples/channels_mask_test.py new file mode 100644 index 000000000..73b219e24 --- /dev/null +++ b/api/python/examples/channels_mask_test.py @@ -0,0 +1,135 @@ +import numpy as np +import argparse + +import arrus +from arrus.ops.imaging import LinSequence +from arrus.ops.us4r import Pulse +import arrus.utils.us4r +import arrus.logging +from arrus.logging import (TRACE, DEBUG, INFO, ERROR) + + +def check_channels_mask(rf, channel_mask, threshold): + """ + This function is for test element masking. + The probe transmit and receive using single element and low voltage, + and check if there is a signal from transmission in recorded data, + and if in neighbouring channels signal is high. + If not, the element is treated as masked. + This procedure is repeated for all elements. + Non-zero samples in masked channels or + too high signals in neigbouring channels raise warnings. + The test is ok when no warnings shows in the command line. + (The threshold 500 was ok on tests with mabprobe). + """ + # skip first n samples + n_skipped = 10 + n_frames, n_samples, n_channels = rf.shape + mid = int(np.ceil(n_channels/2)-1) + stripped_rf = rf[:, n_skipped:, :] + + # mid is a channel number from subaperture corresponding to tested channel + + print(channel_mask) + + for i, channel in enumerate(channel_mask): + arrus.logging.log(INFO, f"Checking channel {channel}") + nonmasked = False + + # All samples (also the first 10 samples) should be smaller than the + # given threshold + # We try to detect a peak that occurs in the first of couple samples. + masked_line_max = np.max(rf[i, :, mid]) + if masked_line_max >= threshold: + arrus.logging.log(ERROR, + f"Too high signal ({masked_line_max}) detected in the " + f"masked channel.") + nonmasked = True + + # check if neighboring elements did not receive high signal + # (ommit first couple of samples which usually contains a large peak) + mx = np.amax(np.absolute(stripped_rf[i, :, :])) + if mx >= threshold: + arrus.logging.log(ERROR, + f"Too high signal ({mx}) detected in one of " + f"neighboring channels of channel #{channel}.") + nonmasked = True + + if nonmasked: + arrus.logging.log(ERROR, f"The channel {channel} is not masked!") + else: + arrus.logging.log(INFO, f"The channel {channel} is masked.") + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description="The script checks if the given channels are actually " + "masked.") + + parser.add_argument("--cfg", dest="cfg", help="Path to configuration file.", + required=True) + parser.add_argument("--channels_off", dest="channels_off", + help="A list of channels that are expected to be " + "turned off.", + type=int, nargs="+") + parser.add_argument("--threshold", dest="threshold", + help="A sample value threshold that determines if the " + "given channel is turned off.", + required=False, type=float, default=550) + parser.add_argument("--dump_file", dest="dump_file", + help="A file to which the examined data should " + "be saved.", + required=False, default=None) + + # arrus.set_clog_level(arrus.logging.TRACE) + arrus.add_log_file("channels_mask_test.log", arrus.logging.TRACE) + + args = parser.parse_args() + cfg_path = args.cfg + expected_channels_off = args.channels_off + threshold = args.threshold + dump_file = args.dump_file + + seq = LinSequence( + tx_aperture_center_element=np.array(expected_channels_off), + tx_aperture_size=1, + tx_focus=20e-3, + pulse=Pulse(center_frequency=5e6, n_periods=1, inverse=False), + rx_aperture_center_element=np.array(expected_channels_off), + rx_aperture_size=64, + rx_sample_range=(0, 256), + pri=2000e-6, + downsampling_factor=1, + tgc_start=14, + tgc_slope=0, + speed_of_sound=1490) + + session = arrus.Session(cfg_path) + + us4r = session.get_device("/Us4R:0") + us4r.set_hv_voltage(5) + buffer, const_metadata = us4r.upload(seq, host_buffer_size=2) + + us4r.start() + data, metadata = buffer.tail() + remap_step = arrus.utils.us4r.RemapToLogicalOrder() + remap_step.set_pkgs(num_pkg=np) + remap_step._prepare(const_metadata) + remapped_data = remap_step(data) + print("Calling channels mask check.") + check_channels_mask(rf=remapped_data, + channel_mask=expected_channels_off, + threshold=threshold) + print("Atfter channels mask check.") + buffer.release_tail() + if dump_file is not None: + arrus.logging.log(INFO, f"Saving data to {dump_file}") + np.save(f"{dump_file}.npy", remapped_data) + np.save(f"{dump_file}_raw.npy", data) + np.save(f"{dump_file}_fcm_frames.npy", + metadata.data_description.custom["frame_channel_mapping"].frames) + np.save(f"{dump_file}_fcm_channels.npy", + metadata.data_description.custom["frame_channel_mapping"].channels) + us4r.stop() + diff --git a/api/python/examples/classical_beamforming.py b/api/python/examples/classical_beamforming.py new file mode 100644 index 000000000..f6d987ea2 --- /dev/null +++ b/api/python/examples/classical_beamforming.py @@ -0,0 +1,257 @@ +import numpy as np +import argparse +import matplotlib.pyplot as plt +from arrus.ops.imaging import LinSequence +from arrus.ops.us4r import Pulse +import arrus.logging +import arrus.utils.us4r +import time +import pickle +import dataclasses +import cupy as cp +import copy + +from arrus.utils.imaging import ( + Pipeline, + BandpassFilter, + QuadratureDemodulation, + Decimation, + RxBeamforming, + EnvelopeDetection, + Transpose, + ScanConversion, + LogCompression, + DynamicRangeAdjustment, + ToGrayscaleImg +) + +from arrus.utils.us4r import RemapToLogicalOrder + +arrus.set_clog_level(arrus.logging.TRACE) +arrus.add_log_file("test.log", arrus.logging.TRACE) + + +def init_display(aperture_size, n_samples): + fig, ax = plt.subplots() + fig.set_size_inches((7, 7)) + ax.set_xlabel("OX") + ax.set_ylabel("OZ") + image_w, image_h = aperture_size, n_samples + canvas = plt.imshow(np.zeros((image_w, image_h)), + vmin=np.iinfo(np.uint8).min, + vmax=np.iinfo(np.uint8).max, + cmap="gray") + fig.show() + return fig, ax, canvas + +metadatas = [] +bmodes = [] + +def display_data(frame_number, data, metadata, imaging_pipeline, figure, ax, canvas): + # TODO use the imaging pipeline + print(f"Displaying frame {frame_number}") + metadatas.append(metadata.custom["frame_metadata_view"].copy()) + bmode = imaging_pipeline(cp.asarray(data)) + bmodes.append(bmode) + # canvas.set_data(bmode) + # ax.set_aspect("auto") + # figure.canvas.flush_events() + # plt.draw() + + +rf_data = [] +rf_metadata = [] + + +def save_raw_data(frame_number, data, metadata): + print(f"Data shape: {data.shape}") + arrus.logging.log(arrus.logging.INFO, f"Saving frame {frame_number}") + np.save(f"rf_{frame_number}.npy", data) + with open(f"metadata_{frame_number}.pkl", "wb") as file: + pickle.dump(metadata, file) + + +def copy_raw_data(frame_number, data, metadata): + global rf_data, rf_metadata + rf_data.append(data.copy()) + frame_metadata = metadata.custom["frame_metadata_view"].copy() + custom_data = copy.copy(metadata.custom) + custom_data["frame_metadata_view"] = frame_metadata + metadata = metadata.copy(custom=custom_data) + rf_metadata.append(metadata) + + +def create_bmode_imaging_pipeline(decimation_factor=4, cic_order=2, + x_grid=None, z_grid=None): + if x_grid is None: + x_grid = np.arange(-50, 50, 0.4)*1e-3 + if z_grid is None: + z_grid = np.arange(0, 60, 0.4)*1e-3 + + return Pipeline( + steps=( + RemapToLogicalOrder(), + Transpose(axes=(0, 2, 1)), + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=decimation_factor, + cic_order=cic_order), + RxBeamforming(), + EnvelopeDetection(), + Transpose(), + ScanConversion(x_grid=x_grid, z_grid=z_grid), + LogCompression(), + DynamicRangeAdjustment(min=5, max=120), + ToGrayscaleImg())) + + +def get_rf_iq_data(buffer, buffer_size): + iq_rec = iq_reconstruct(decimation_factor=4, cic_order=2) + iq_data_list = [] + for i in range(buffer_size): + data, metadata = buffer.tail() + iq_data, iq_metadata = iq_rec(cp.asarray(data), metadata) + iq_data_list.append((iq_data.get(), iq_metadata.get())) + buffer.release_tail() + return iq_data_list + + +def iq_reconstruct(decimation_factor=4, cic_order=2): + return Pipeline( + steps=( + RemapToLogicalOrder(), + Transpose(axes=(0, 2, 1)), + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=decimation_factor, + cic_order=cic_order), + RxBeamforming())) + + +iq_data = [] +iq_metadata = [] + +current_voltage = 5 + +def save_iq_data(frame_number, data, metadata, iq_rec): + global iq_data, iq_metadata + iq, iq_m = iq_rec(cp.asarray(data), metadata) + + iq_data.append(iq.get()) + frame_metadata = metadata.custom["frame_metadata_view"].copy() + custom_data = copy.copy(metadata.custom) + custom_data["frame_metadata_view"] = frame_metadata + metadata = metadata.copy(custom=custom_data) + iq_metadata.append(metadata) + + +def main(): + parser = argparse.ArgumentParser( + description="The script acquires a sequence of RF data or " + "reconstructs b-mode images.") + + parser.add_argument("--cfg", dest="cfg", + help="Path to session configuration file.", + required=True) + parser.add_argument("--action", dest="action", + help="An action to perform.", + required=True, choices=["nop", "save", "img", "save_mem", "save_iq"]) + parser.add_argument("--n", dest="n", + help="How many times should the operation be performed.", + required=False, type=int, default=100) + parser.add_argument("--host_buffer_size", dest="host_buffer_size", + help="Host buffer size.", required=False, type=int, + default=2) + parser.add_argument("--rx_batch_size", dest="rx_batch_size", + help="Rx batch size.", required=False, type=int, + default=1) + args = parser.parse_args() + + x_grid = np.arange(-50, 50, 0.4)*1e-3 + z_grid = np.arange(0, 60, 0.4)*1e-3 + + seq = LinSequence( + tx_aperture_center_element=np.arange(8, 183), + tx_aperture_size=64, + tx_focus=30e-3, + pulse=Pulse(center_frequency=8e6, n_periods=3.5, inverse=False), + rx_aperture_center_element=np.arange(8, 183), + rx_aperture_size=64, + rx_sample_range=(0, 2048), + pri=200e-6, + tgc_start=14, + tgc_slope=2e2, + downsampling_factor=2, + speed_of_sound=1490, + sri=500e-3) + + bmode_imaging = create_bmode_imaging_pipeline(x_grid=x_grid, z_grid=z_grid) + iq_rec = iq_reconstruct(4, 2) + + if args.action == "img": + fig, ax, canvas = init_display(len(z_grid), len(x_grid)) + + action_func = { + "nop": None, + "save": save_raw_data, + "save_mem": copy_raw_data, + "img": lambda frame_number, data, metadata: display_data( + frame_number, data, metadata, bmode_imaging, fig, ax, canvas), + "save_iq": lambda frame_number, data, metadata: save_iq_data( + frame_number, data, metadata, iq_rec), + }[args.action] + + # Here starts communication with the device. + session = arrus.session.Session(args.cfg) + + us4r = session.get_device("/Us4R:0") + gpu = session.get_device("/GPU:0") + + # Set the pipeline to be executed on the GPU + bmode_imaging.set_placement(gpu) + iq_rec.set_placement(gpu) + + # Set initial voltage on the us4r-lite device. + # Upload sequence on the us4r-lite device. + buffer, const_metadata = us4r.upload(seq, host_buffer_size=args.host_buffer_size, + rx_buffer_size=args.host_buffer_size, + rx_batch_size=args.rx_batch_size) + + const_metadata = bmode_imaging.initialize(const_metadata) + # Start the device. + us4r.start() + times = [] + arrus.logging.log(arrus.logging.INFO, f"Running {args.n} iterations.") + for i in range(args.n): + start = time.time() + data, metadata = buffer.tail() + print(data.ctypes.data) + + if action_func is not None: + action_func(i, data, metadata) + + buffer.release_tail() + times.append(time.time()-start) + arrus.logging.log(arrus.logging.INFO, + f"Done, average acquisition + processing time: {np.mean(times)} [s]") + + # rf_iq_data_buffer = get_rf_iq_data(buffer, 100) + if args.action == "save_mem": + print("Saving data to rf.npy i metadata.pkl") + global rf_data, rf_metadata + np.save("rf.npy", np.stack(rf_data)) + with open("metadata.pkl", "wb") as f: + pickle.dump(rf_metadata, f) + if args.action == "save_iq": + global iq_data, iq_metadata + np.save("rf_iq.npy", np.stack(iq_data)) + with open("metadata.pkl", "wb") as f: + pickle.dump(iq_metadata, f) + + print("Stopping the device.") + us4r.stop() + print("Device stopped.") + + +if __name__ == "__main__": + main() diff --git a/api/python/examples/classical_beamforming_keyboard.py b/api/python/examples/classical_beamforming_keyboard.py new file mode 100644 index 000000000..0a45e7caa --- /dev/null +++ b/api/python/examples/classical_beamforming_keyboard.py @@ -0,0 +1,299 @@ +import numpy as np +import argparse +import matplotlib.pyplot as plt +from arrus.ops.imaging import LinSequence +from arrus.ops.us4r import Pulse +import arrus.logging +import arrus.utils.us4r +import time +import pickle +import dataclasses +import cupy as cp +import copy +import keyboard + +from arrus.utils.imaging import ( + Pipeline, + BandpassFilter, + QuadratureDemodulation, + Decimation, + RxBeamforming, + EnvelopeDetection, + Transpose, + ScanConversion, + LogCompression, + DynamicRangeAdjustment, + ToGrayscaleImg +) + +from arrus.utils.us4r import RemapToLogicalOrder + +arrus.set_clog_level(arrus.logging.TRACE) +arrus.add_log_file("test.log", arrus.logging.TRACE) + + +def init_display(aperture_size, n_samples): + fig, ax = plt.subplots() + fig.set_size_inches((7, 7)) + ax.set_xlabel("OX") + ax.set_ylabel("OZ") + image_w, image_h = aperture_size, n_samples + canvas = plt.imshow(np.zeros((image_w, image_h)), + vmin=np.iinfo(np.uint8).min, + vmax=np.iinfo(np.uint8).max, + cmap="gray") + fig.show() + return fig, ax, canvas + + +def display_data(frame_number, data, metadata, imaging_pipeline, figure, ax, canvas): + # TODO use the imaging pipeline + print(f"Displaying frame {frame_number}") + bmode, metadata = imaging_pipeline(cp.asarray(data), metadata) + canvas.set_data(bmode) + ax.set_aspect("auto") + figure.canvas.flush_events() + plt.draw() + + +rf_data = [] +rf_metadata = [] + + +def save_raw_data(frame_number, data, metadata): + print(f"Data shape: {data.shape}") + arrus.logging.log(arrus.logging.INFO, f"Saving frame {frame_number}") + np.save(f"rf_{frame_number}.npy", data) + with open(f"metadata_{frame_number}.pkl", "wb") as file: + pickle.dump(metadata, file) + + +def copy_raw_data(frame_number, data, metadata): + global rf_data, rf_metadata + rf_data.append(data.copy()) + frame_metadata = metadata.custom["frame_metadata_view"].copy() + custom_data = copy.copy(metadata.custom) + custom_data["frame_metadata_view"] = frame_metadata + metadata = metadata.copy(custom=custom_data) + rf_metadata.append(metadata) + + +def create_bmode_imaging_pipeline(decimation_factor=4, cic_order=2, + x_grid=None, z_grid=None): + if x_grid is None: + x_grid = np.arange(-50, 50, 0.4)*1e-3 + if z_grid is None: + z_grid = np.arange(0, 60, 0.4)*1e-3 + + return Pipeline( + steps=( + RemapToLogicalOrder(), + Transpose(axes=(0, 2, 1)), + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=decimation_factor, + cic_order=cic_order), + RxBeamforming(), + EnvelopeDetection(), + Transpose(), + ScanConversion(x_grid=x_grid, z_grid=z_grid), + LogCompression(), + DynamicRangeAdjustment(min=5, max=120), + ToGrayscaleImg())) + + +def get_rf_iq_data(buffer, buffer_size): + iq_rec = iq_reconstruct(decimation_factor=4, cic_order=2) + iq_data_list = [] + for i in range(buffer_size): + data, metadata = buffer.tail() + iq_data, iq_metadata = iq_rec(cp.asarray(data), metadata) + iq_data_list.append((iq_data.get(), iq_metadata.get())) + buffer.release_tail() + return iq_data_list + + +def iq_reconstruct(decimation_factor=4, cic_order=2): + return Pipeline( + steps=( + RemapToLogicalOrder(), + Transpose(axes=(0, 2, 1)), + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=decimation_factor, + cic_order=cic_order), + RxBeamforming())) + + +iq_data = [] +iq_metadata = [] + +current_voltage = 30 +current_tgc = 14 + + +def save_iq_data(frame_number, data, metadata, iq_rec): + global iq_data, iq_metadata + iq, iq_m = iq_rec(cp.asarray(data), metadata) + + iq_data.append(iq.get()) + frame_metadata = metadata.custom["frame_metadata_view"].copy() + custom_data = copy.copy(metadata.custom) + custom_data["frame_metadata_view"] = frame_metadata + metadata = metadata.copy(custom=custom_data) + iq_metadata.append(metadata) + + +def main(): + parser = argparse.ArgumentParser( + description="The script acquires a sequence of RF data or " + "reconstructs b-mode images.") + + parser.add_argument("--cfg", dest="cfg", + help="Path to session configuration file.", + required=True) + parser.add_argument("--action", dest="action", + help="An action to perform.", + required=True, choices=["nop", "save", "img", "save_mem", "save_iq"]) + parser.add_argument("--n", dest="n", + help="How many times should the operation be performed.", + required=False, type=int, default=100) + parser.add_argument("--host_buffer_size", dest="host_buffer_size", + help="Host buffer size.", required=False, type=int, + default=2) + parser.add_argument("--rx_batch_size", dest="rx_batch_size", + help="Rx batch size.", required=False, type=int, + default=1) + args = parser.parse_args() + + x_grid = np.arange(-50, 50, 0.4)*1e-3 + z_grid = np.arange(0, 60, 0.4)*1e-3 + + seq = LinSequence( + tx_aperture_center_element=np.arange(8, 183), + tx_aperture_size=64, + tx_focus=30e-3, + pulse=Pulse(center_frequency=8e6, n_periods=3.5, inverse=False), + rx_aperture_center_element=np.arange(8, 183), + rx_aperture_size=64, + rx_sample_range=(0, 2048), + pri=100e-6, + tgc_start=current_tgc, + tgc_slope=0, + downsampling_factor=2, + speed_of_sound=1490) + + bmode_imaging = create_bmode_imaging_pipeline(x_grid=x_grid, z_grid=z_grid) + iq_rec = iq_reconstruct(4, 2) + + if args.action == "img": + fig, ax, canvas = init_display(len(z_grid), len(x_grid)) + + action_func = { + "nop": None, + "save": save_raw_data, + "save_mem": copy_raw_data, + "img": lambda frame_number, data, metadata: display_data( + frame_number, data, metadata, bmode_imaging, fig, ax, canvas), + "save_iq": lambda frame_number, data, metadata: save_iq_data( + frame_number, data, metadata, iq_rec), + }[args.action] + + # Here starts communication with the device. + session = arrus.session.Session(args.cfg) + + us4r = session.get_device("/Us4R:0") + gpu = session.get_device("/GPU:0") + + # Set the pipeline to be executed on the GPU + bmode_imaging.set_placement(gpu) + iq_rec.set_placement(gpu) + + # Set initial voltage on the us4r-lite device. + # Upload sequence on the us4r-lite device. + buffer = us4r.upload(seq, mode="sync", + host_buffer_size=args.host_buffer_size, + rx_batch_size=args.rx_batch_size) + + def increase_voltage(ev): + print("Increasing voltage") + global current_voltage + new_voltage = current_voltage + 5 + if current_voltage > 90: + print("maximum voltage set") + return + current_voltage = new_voltage + us4r.set_hv_voltage(current_voltage) + + def decrease_voltage(ev): + print("Decreasing voltage") + global current_voltage + new_voltage = current_voltage - 5 + if current_voltage < 5: + print("minimum voltage set") + return + current_voltage = new_voltage + us4r.set_hv_voltage(current_voltage) + + def increase_tgc(ev): + print("Increasing TGC") + global current_tgc + new_tgc = current_tgc + 5 + if new_tgc > 54: + print("maximum tgc already set") + return + current_tgc = new_tgc + print(f"Setting tgc start: {current_tgc}") + us4r.set_tgc(arrus.ops.tgc.LinearTgc(start=current_tgc, slope=0)) + + def decrease_tgc(ev): + print("decreasing TGC") + global current_tgc + new_tgc = current_tgc - 5 + if current_tgc < 14: + print("minimum tgc set") + return + current_tgc = new_tgc + us4r.set_tgc(arrus.ops.tgc.LinearTgc(start=current_tgc, slope=0)) + + keyboard.on_press_key("1", increase_voltage) + keyboard.on_press_key("2", decrease_voltage) + + keyboard.on_press_key("3", increase_tgc) + keyboard.on_press_key("4", decrease_tgc) + + # Start the device. + us4r.start() + times = [] + arrus.logging.log(arrus.logging.INFO, f"Running {args.n} iterations.") + for i in range(args.n): + start = time.time() + data, metadata = buffer.head() + + if action_func is not None: + action_func(i, data, metadata) + buffer.release_tail() + times.append(time.time()-start) + arrus.logging.log(arrus.logging.INFO, + f"Done, average acquisition + processing time: {np.mean(times)} [s]") + + # rf_iq_data_buffer = get_rf_iq_data(buffer, 100) + if args.action == "save_mem": + print("Saving data to rf.npy i metadata.pkl") + global rf_data, rf_metadata + np.save("rf.npy", np.stack(rf_data)) + with open("metadata.pkl", "wb") as f: + pickle.dump(rf_metadata, f) + if args.action == "save_iq": + global iq_data, iq_metadata + np.save("rf_iq.npy", np.stack(iq_data)) + with open("metadata.pkl", "wb") as f: + pickle.dump(iq_metadata, f) + + print("Stopping the device.") + us4r.stop() + print("Device stopped.") + + +if __name__ == "__main__": + main() diff --git a/api/python/examples/classical_beamforming_reconstruction.py b/api/python/examples/classical_beamforming_reconstruction.py new file mode 100644 index 000000000..e59d1a1c9 --- /dev/null +++ b/api/python/examples/classical_beamforming_reconstruction.py @@ -0,0 +1,108 @@ +import argparse +import pickle +import matplotlib.pyplot as plt +import numpy as np +import cupy as cp + +import arrus +import arrus.ops.us4r +from arrus.utils.us4r import RemapToLogicalOrder +from arrus.utils.imaging import ( + Pipeline, + BandpassFilter, + QuadratureDemodulation, + Decimation, + RxBeamforming, + EnvelopeDetection, + Transpose, + ScanConversion, + LogCompression, + DynamicRangeAdjustment, + ToGrayscaleImg +) + + +def main(): + parser = argparse.ArgumentParser( + description="The script reconstructs image from the given data.") + + parser.add_argument("--rf", dest="rf", help="Path to rf data file.", + required=False) + parser.add_argument("--metadata", dest="metadata", + help="Path to metadata file.", + required=False) + parser.add_argument("--matlab_dataset", dest="matlab_dataset", + help="Path to matlab dataset (v0.4.7).", + required=False) + args = parser.parse_args() + + is_numpy_data = args.rf is not None and args.metadata is not None + is_matlab_data = args.matlab_dataset is not None + if not (is_numpy_data ^ is_matlab_data): + raise ValueError("Exactly one of the following datasets should be " + "provided: numpy rf data and metadata, " + "matlab dataset.") + + if is_numpy_data: + data = np.load(args.rf) + metadata = pickle.load(open(args.metadata, 'rb')) + mock = {} # No matlab mock + elif is_matlab_data: + import h5py + dataset = h5py.File("data.mat", mode="r") + dataset = { + "rf": np.array(dataset["rf"][:5, :, :, :]), + "sys": dataset["sys"], + "seq": dataset["seq"] + } + mock = {"Us4R:0": dataset} + + # Create session with not Us4R device. + sess = arrus.Session(mock=mock) + gpu = sess.get_device("/GPU:0") + + if is_numpy_data: + initial_steps = [ + RemapToLogicalOrder(), + Transpose(axes=(0, 2, 1)) + ] + elif is_matlab_data: + seq = arrus.ops.us4r.TxRxSequence([], []) + us4r = sess.get_device("/Us4R:0") + buffer = us4r.upload(seq) + data, metadata = buffer.tail() + initial_steps = [] + + # Actual imaging starts here. + x_grid = np.arange(-50, 50, 0.4)*1e-3 + z_grid = np.arange(0, 60, 0.4)*1e-3 + + pipeline = Pipeline( + steps=initial_steps + [ + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=4, cic_order=2), + RxBeamforming(), + EnvelopeDetection(), + Transpose(), + ScanConversion(x_grid=x_grid, z_grid=z_grid), + LogCompression(), + DynamicRangeAdjustment(), + ToGrayscaleImg() + ], + placement=gpu) + reconstructed_data, metadata = pipeline(cp.asarray(data), metadata) + + fig, ax = plt.subplots() + fig.set_size_inches((7, 7)) + ax.imshow(reconstructed_data, cmap="gray") + ax.set_aspect('auto') + fig.show() + plt.show() + + +if __name__ == "__main__": + main() + + + diff --git a/api/python/examples/custom_tx_rx_sequence.py b/api/python/examples/custom_tx_rx_sequence.py new file mode 100644 index 000000000..edc0e67e7 --- /dev/null +++ b/api/python/examples/custom_tx_rx_sequence.py @@ -0,0 +1,109 @@ +import arrus.session +import numpy as np +import matplotlib.pyplot as plt +from arrus.ops.us4r import ( + TxRxSequence, + TxRx, + Tx, + Rx, + Pulse +) +import arrus.logging +import arrus.utils.us4r + +arrus.logging.set_clog_level(arrus.logging.TRACE) +arrus.logging.add_log_file("test.log", arrus.logging.TRACE) + +session = arrus.session.Session( + r"C:\Users\pjarosik\src\x-files\customers\nanoecho\nanoecho_magprobe_002.prototxt") + +rx_aperture = np.zeros((192,), dtype=np.bool) +rx_aperture[:192] = True + +distance = np.arange(start=round(400/1), + stop=4096, + step=round(150/1))/65e6*1490 +tgc_curve = 14 + distance*2e2 + +seq = TxRxSequence( + ops=[ + TxRx( + tx=Tx( + aperture=np.ones((192, ), dtype=np.bool), + delays=(np.arange(0, 192)*1e-8).astype(np.float32), + excitation=Pulse( + center_frequency=5e6, + n_periods=3.5, + inverse=True + ) + ), + rx=Rx( + aperture=rx_aperture, + sample_range=(0, 4096), + downsampling_factor=1 + ), + pri=1000e-6), + TxRx( + tx=Tx( + aperture=np.ones((192, ), dtype=np.bool), + delays=np.zeros((192, ), dtype=np.float32), + excitation=Pulse( + center_frequency=5e6, + n_periods=3.5, + inverse=True + ) + ), + rx=Rx( + aperture=rx_aperture, + sample_range=(0, 4096), + downsampling_factor=1 + ), + pri=1000e-6), + TxRx( + tx=Tx( + aperture=np.ones((192, ), dtype=np.bool), + delays=np.flip((np.arange(0, 192)*1e-8).astype(np.float32)), + excitation=Pulse( + center_frequency=5e6, + n_periods=3.5, + inverse=True + ) + ), + rx=Rx( + aperture=rx_aperture, + sample_range=(0, 4096), + downsampling_factor=1 + ), + pri=1000e-6), + ], + tgc_curve=np.array(tgc_curve) +) + +us4r = session.get_device("/Us4R:0") +us4r.set_hv_voltage(30) +buffer = us4r.upload(seq) + +print("Starting the device.") + +def display_data(data): + fig, ax = plt.subplots() + fig.set_size_inches((7, 7)) + ax.imshow(data) + ax.set_aspect('auto') + fig.show() + + +us4r.start() +data, metadata = buffer.tail() +remap_step = arrus.utils.us4r.RemapToLogicalOrder() +remap_step.set_pkgs(num_pkg=np) +remapped_data, metadata = remap_step(data, metadata) + +# print("Saving data") +# np.save("test.npy", data) +# print("Data saved") +# +# buffer.release_tail() +# +# print("Stopping the device.") +# us4r.stop() diff --git a/api/python/examples/display_batch.py b/api/python/examples/display_batch.py new file mode 100644 index 000000000..3c0386881 --- /dev/null +++ b/api/python/examples/display_batch.py @@ -0,0 +1,135 @@ +import arrus +import numpy as np +import matplotlib.pyplot as plt +import cupy as cp +import pickle +import time +import argparse +import os +from datetime import datetime + +from arrus.ops.imaging import LinSequence +from arrus.ops.us4r import Pulse + +from arrus.utils.imaging import ( + Pipeline, + BandpassFilter, + QuadratureDemodulation, + Decimation, + RxBeamforming, + EnvelopeDetection, + Transpose, + ScanConversion, + LogCompression, + DynamicRangeAdjustment, + ToGrayscaleImg +) +from arrus.utils.us4r import ( + RemapToLogicalOrder, + get_batch_data, + get_batch_metadata +) + + +arrus.set_clog_level(arrus.logging.INFO) +arrus.add_log_file("test.log", arrus.logging.TRACE) + + +def init_display(aperture_size, n_samples): + fig, ax = plt.subplots() + fig.set_size_inches((7, 7)) + ax.set_xlabel("OX") + ax.set_ylabel("OZ") + image_w, image_h = aperture_size, n_samples + canvas = plt.imshow(np.zeros((image_w, image_h)), + vmin=np.iinfo(np.uint8).min, + vmax=np.iinfo(np.uint8).max, + cmap="gray") + fig.show() + return fig, ax, canvas + + +prev_timestamp = 0 + + +def display_data(frame_number, data, metadata, imaging_pipeline, figure, + ax, canvas): + global prev_timestamp + bmode, metadata = imaging_pipeline(cp.asarray(data), metadata) + frame_metadata = metadata.custom["frame_metadata_view"][0, :].copy().view(np.int8) + trigger_counter = frame_metadata[0:8].view(np.uint64).item() + timestamp = frame_metadata[8:16].view(np.uint64).item() / 65e6 + pulse_counter = frame_metadata[16:20].view(np.uint32).item() + outa_counter = frame_metadata[20:24].view(np.uint32).item() + outb_counter = frame_metadata[24:28].view(np.uint32).item() + canvas.set_data(bmode) + ax.set_aspect("auto") + + diff = timestamp - prev_timestamp + prev_timestamp = timestamp + ax.set_xlabel(f"OX,\n frame: {frame_number}, " + f"\n trigger counter: {trigger_counter}, " + + "timestamp: %.3f (diff: %.5f), pulse: %d, " % (timestamp, diff, pulse_counter) + + f"outa: {outa_counter}, " + f"outb: {outb_counter}") + figure.canvas.flush_events() + plt.draw() + + +def main(): + parser = argparse.ArgumentParser( + description="The script acquires a sequence of RF data and saves " + "them to given directory.") + + parser.add_argument("--timestamp", dest="timestamp", + help="Timestamp of the file to display.", + required=True) + parser.add_argument("--directory", dest="directory", + help="Directory where to save the data were saved.", + required=True) + + args = parser.parse_args() + timestamp = args.timestamp + directory = args.directory + + # Open and reconstruct image. + session = arrus.session.Session(mock={}) + gpu = session.get_device("/GPU:0") + x_grid = np.arange(-50, 50, 0.2)*1e-3 + z_grid = np.arange(0, 60, 0.2)*1e-3 + pipeline = Pipeline( + steps=( + RemapToLogicalOrder(), + Transpose(axes=(0, 2, 1)), + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=4, cic_order=2), + RxBeamforming(), + EnvelopeDetection(), + Transpose(), + ScanConversion(x_grid=x_grid, z_grid=z_grid), + LogCompression(), + DynamicRangeAdjustment(min=20, max=80), + ToGrayscaleImg()), + placement=gpu) + + fig, ax, canvas = init_display(len(x_grid), len(z_grid)) + + print("Loading saved data.") + data_file_path = os.path.join(directory, f"rf_{timestamp}.npy") + metadata_file_path = os.path.join(directory, f"metadata_{timestamp}.pkl") + + batch_data = np.load(data_file_path) + batch_metadata = pickle.load(open(metadata_file_path, 'rb')) + + print("Displaying the data") + batch_size = batch_metadata.data_description.custom["frame_channel_mapping"]\ + .batch_size + for i in range(batch_size): + data = get_batch_data(batch_data, batch_metadata, i) + metadata = get_batch_metadata(batch_metadata, i) + display_data(i, data, metadata, pipeline, fig, ax, canvas) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/api/python/examples/iq_reconstruct.py b/api/python/examples/iq_reconstruct.py new file mode 100644 index 000000000..b773cf259 --- /dev/null +++ b/api/python/examples/iq_reconstruct.py @@ -0,0 +1,48 @@ +import arrus +import numpy as np +import arrus.utils.us4r +import pickle +import cupy as cp + +from arrus.utils.imaging import ( + Pipeline, + BandpassFilter, + QuadratureDemodulation, + Decimation, + RxBeamforming, + EnvelopeDetection, + Transpose, + ScanConversion, + LogCompression, + DynamicRangeAdjustment, + ToGrayscaleImg +) + +from arrus.utils.us4r import RemapToLogicalOrder, get_batch_data, get_batch_metadata + +iq_reconstruct = Pipeline( + steps=( + RemapToLogicalOrder(), + Transpose(axes=(0, 2, 1)), + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=4, cic_order=2), + RxBeamforming())) + + +data = np.load(f"C:\\Users\\pjarosik\\Desktop\\test_rf.npy") +metadata = pickle.load(open(f"C:\\Users\\pjarosik\\Desktop\\test_metadata.pkl", 'rb')) + +session = arrus.session.Session(mock={}) +gpu = session.get_device("/GPU:0") + + +# Set the pipeline to be executed on the GPU +iq_reconstruct.set_placement(gpu) + +raw_data = [] + +for i in range(1000): + j = i % 100 + iq_data, iq_metadata = iq_reconstruct(cp.asarray(data[j]), metadata[j]) + diff --git a/api/python/examples/mock_example.py b/api/python/examples/mock_example.py new file mode 100644 index 000000000..eea96bbe3 --- /dev/null +++ b/api/python/examples/mock_example.py @@ -0,0 +1,235 @@ +import numpy as np +import arrus +# import cupy as cp +import matplotlib.pyplot as plt +import h5py + +from arrus.ops.imaging import ( + LinSequence +) +from arrus.ops.us4r import ( + Pulse +) + +from arrus.utils.imaging import ( + Pipeline, + BandpassFilter, + QuadratureDemodulation, + Decimation, + RxBeamforming, + EnvelopeDetection, + Transpose, + ScanConversion, + LogCompression, + DynamicRangeAdjustment, + ToGrayscaleImg +) + +# Read the dataset do display. +print("Reading data...") +dataset = h5py.File("data.mat", mode="r") + +dataset = { + "rf": np.array(dataset["rf"][:5, :, :, :]), + "sys": dataset["sys"], + "seq": dataset["seq"] +} +print("...done.") + +# Create new session to communicate with the system. +# Session constructor configures all the necessary devices; in case of the mock, +# that means to load data from the provided dataset only. +# A non-mocked session will read a configuration file and create handles +# to the actual devices that should be available to user. +print("Creating session.") +sess = arrus.Session(mock={ + "Us4R:0": dataset +}) + +print("Session created.") + +# Session provides handles to system devices. What devices are available +# depends on the session configuration file. +# We will send you an appropriate session configuration file once you receive +# the us4r-lite hardware. +# The `Us4R` is an us4r lite device. +us4r = sess.get_device("/Us4R:0") +gpu = sess.get_device("/CPU:0") + +# Set HV voltage [0.5*Vpp]; +# maximum value: 90 (can be limited for specific probes in the session +# configuration file). +us4r.set_hv_voltage(30) + +# Tx/Rx sequence to perform on the us4r device. +sequence = LinSequence( + # Transmit a signal for an aperture centered in element 0, 1, ... 191 + # Note: this should not exceed the number of probe elements. + tx_aperture_center_element=np.arange(0, 192), + # The aperture should contain 64 elements. + tx_aperture_size=64, + # The beam should be focused on 30 mm depth. + tx_focus=30e-3, # [m] + # Transmit a sine wave with center frequency 4MHz, 2 periods, no inverse. + pulse=Pulse(center_frequency=4e6, n_periods=2, inverse=False), + # Receive echo data with aperture centered in elements 0, 1, ..., 191 + # Note: rx_aperture_center_element should have the length as + # the tx_aperture_center_element vector. + rx_aperture_center_element=np.arange(0, 192), + # Record data using 64 elements + rx_aperture_size=64, + # Downsampling factor: an integers that divides the output data sampling + # frequency, i.e. the output sampling frequency is + # 65e6/n, where n can be 1, 2, ..., 5. One means no downsampling. + downsampling_factor=1, + # Pulse repetition interval - the time between successive signal transmits. + pri=200e-6, + # Sample range: [start, end) sample + rx_sample_range=(0, 4096), + # Linear TGC curve start value. + tgc_start=14, + # Linear TGC curve slope. + tgc_slope=2e2 +) + +# Remember to upload th sequence on the us4r device. +# The provided buffer will contain acquired RF data. +# The buffer is a read-only circular queue (only us4r device can write to this +# buffer). +# Currently `us4r.upload` is just a nop. +buffer = us4r.upload(sequence) + +# Output image grid: +x_grid = np.arange(-50, 50, 0.4)*1e-3 +z_grid = np.arange(0, 60, 0.4)*1e-3 + +# Define bmode image reconstruction pipeline. +# You can find source and docstrings of each step in arrus.utils.imaging +# module. + +bmode_imaging = Pipeline( + placement=gpu, + steps=( + # Filter the data using bandpass filter, + # default bandwidth: [0.5*fc, 1.5*fc], where fc is center frequency. + # Currently FIR filter is available only. + # The data is filtered along the last axis. + # + # input: nd array. + # output: nd array with the same shape and data type + BandpassFilter(), + # Converts to I/Q samples. + # + # input: nd array + # output: nd array with the same shape and dtype=xp.complex64 + QuadratureDemodulation(), + # Decimate data (CIC filter is also used). + # + # input: nd array + # output: nd array with the last axis `decimation_factor`-times smaller + Decimation(decimation_factor=4, cic_order=2), + # Delay and sum; reconstruct scanlines from the provided echo data. + # + # input: nd array, shape: n_emissions, n_rx, n_samples + # output: nd array, shape: n_emissions, n_samples + RxBeamforming(), + # Extracts envelope from the RF data. + # + # input nd array, dtype=xp.complex64 + # output: nd array, dtype=xp.float32 + EnvelopeDetection(), + # Transpose the provided image. + # + # input: nd array + # output: nd array with the reversed axes + Transpose(), + # Interpolate the RF data to output b-mode image grid. + # + # Note! Currently implemented only for CPU. + # + # input: nd array, shape: n_samples, n_emissions + # output: nd array, shape: len(z_grid), len(x_grid) + ScanConversion(x_grid=x_grid, z_grid=z_grid), + # Convert to decibel scale. + LogCompression(), + DynamicRangeAdjustment(min=20, max=80), + ToGrayscaleImg() + ) +) + +# Display data with matplotlib +fig, ax = plt.subplots() +fig.set_size_inches((7, 7)) +ax.set_xlabel("OX") +ax.set_ylabel("OZ") +image_w, image_h = len(x_grid), len(z_grid) +canvas = plt.imshow(np.zeros((image_w, image_h)), + vmin=np.iinfo(np.uint8).min, + vmax=np.iinfo(np.uint8).max, + cmap="gray") +fig.show() + +# Here starts the data acquisition and processing. +# Starts currently uploaded tx/rx sequence. +us4r.start() +# The buffer is now populated with RF data (and some additional metadata). + +# Get data from the buffer, process and display (100 frames). +for i in range(100): + # Get data and metadata from the buffer. + # buffer.pop copies data from the buffer and returns new numpy ndarray. + # The buffer.pop releases current buffer element. + # Note: Most likely in the futurewe will add a target 'target_device' + # parameter which will allow to copy the RF data directly into GPU memory. + + # To avoid data copying the user can use a pair of instructions: + # - buffer.tail() (returns a numpy array that wraps a pointer to the memory + # area with data acquired by the the us4r-lite device) + # - buffer.release_tail() (notify the us4r-lite device that the + # data is not needed anymore and memory area can be reused by the + # us4r-lite device for the next acquisitions) + + print("Acquiring data") + # 2 elements + data, metadata = buffer.tail() + + + # The processing happens here + + # The metadata structure contains all the information necessary to + # reconstruct b-mode image from the RF data + # (e.g. probe's pitch, tx aperture position, etc.). + # You can find the source and docstrings of the metadata in + # arrus.metadata module. + if i == 0: + # Data acquisition context is constant after starting the us4r.device + # (you have to stop the device if you want e.g. change some lin sequence + # parameters), thus metadata.context field + # is constant; + # + # The metadata.context can be saved after acquiring the first frame; + # then you can ignore this field for consecutive fields. + print(metadata.context) + print(metadata.data_description) + + # process + # gpu_data = cp.asarray(data) + # We've just copied the data from the us4r-lite buffer, we can release + # the current buffer element. + # Reconstruct bmode image. + # Note: metadata.data_description describes data produced at a given step; + # e.g. metadata.data_description.sampling_frequency can change after + # `Decimation` operation. + bmode, metadata = bmode_imaging(data, metadata) + print(bmode.dtype) + # display + canvas.set_data(bmode) + ax.set_aspect("auto") + fig.canvas.flush_events() + plt.draw() + print(f"Custom metadata: {metadata.custom}") + + buffer.release_tail() + +# Stop the execution of the tx/rx sequence. +us4r.stop() diff --git a/api/python/examples/post.py b/api/python/examples/post.py new file mode 100644 index 000000000..8ed4f2a7b --- /dev/null +++ b/api/python/examples/post.py @@ -0,0 +1,19 @@ +import numpy as np +import matplotlib.pyplot as plt + +x = np.load('test2.npy', allow_pickle=True) +y = np.zeros((4096, 192), dtype=np.int16) + +y[:, :32] = x[:4096, :] +y[:, 32:64] = x[3*4096:4*4096, :] +y[:, 64:96] = x[1*4096:2*4096, :] +y[:, 96:128] = x[4*4096:5*4096, :] +y[:, 128:160] = x[2*4096:3*4096, :] +y[:, 160:192] = x[5*4096:6*4096, :] + +fig, ax = plt.subplots() + +fig.set_size_inches((7, 7)) +ax.imshow(y) +ax.set_aspect('auto') +fig.show() \ No newline at end of file diff --git a/api/python/examples/requirements.txt b/api/python/examples/requirements.txt new file mode 100644 index 000000000..d80563b24 --- /dev/null +++ b/api/python/examples/requirements.txt @@ -0,0 +1,2 @@ +matplotlib +h5py \ No newline at end of file diff --git a/api/python/examples/show_batch_file_data.ipynb b/api/python/examples/show_batch_file_data.ipynb new file mode 100644 index 000000000..fb051f75e --- /dev/null +++ b/api/python/examples/show_batch_file_data.ipynb @@ -0,0 +1,314 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing raw RF files from the disk." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import arrus\n", + "import numpy as np\n", + "import pickle\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## load the data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- remember to change the path of the input files" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "data = np.load(f\"C:\\\\Users\\\\pjarosik\\\\data\\\\rf_2020_11_24_09_44_37.npy\")\n", + "metadata = pickle.load(open(f\"C:\\\\Users\\\\pjarosik\\\\data\\\\metadata_2020_11_24_09_44_37.pkl\", 'rb'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## check frame counter and timestamps " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- checks if the frame counter is a sequence of consecutive values (175 scanlines distance)\n", + "- check the timestamp difference between consecutive frames. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import arrus.metadata\n", + "import numpy as np\n", + "from arrus.utils.us4r import get_batch_data, get_batch_metadata\n", + "\n", + "batch_size = metadata.data_description.custom[\"frame_channel_mapping\"].batch_size\n", + "splitted_metadata = []\n", + "\n", + "\n", + "for i in range(batch_size):\n", + " splitted_metadata.append(get_batch_metadata(metadata, i))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "trigger_counters = []\n", + "timestamps = []\n", + "for m in splitted_metadata:\n", + " trigger_counters.append(m.custom[\"frame_metadata_view\"][0].copy().view(np.int8)[0:8].view(np.uint64).item())\n", + " timestamps.append(m.custom[\"frame_metadata_view\"][0].copy().view(np.int8)[8:16].view(np.uint64).item()/65e6)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175,\n", + " 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175,\n", + " 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175,\n", + " 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175,\n", + " 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175,\n", + " 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175,\n", + " 175, 175, 175, 175, 175, 175, 175, 175, 175, 175, 175])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.diff(trigger_counters)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325, 0.017325,\n", + " 0.017325, 0.017325, 0.017325, 0.017325, 0.017325])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.diff(timestamps)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Display b-mode images of the acquired data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- frame_nr is the number of frame to be displayed" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from arrus.utils.imaging import (\n", + " Pipeline,\n", + " BandpassFilter,\n", + " QuadratureDemodulation,\n", + " Decimation,\n", + " RxBeamforming,\n", + " EnvelopeDetection,\n", + " Transpose,\n", + " ScanConversion,\n", + " LogCompression,\n", + " DynamicRangeAdjustment,\n", + " ToGrayscaleImg\n", + ")\n", + "from arrus.utils.us4r import RemapToLogicalOrder\n", + "\n", + "sess = arrus.Session(mock={})\n", + "\n", + "x_grid = np.arange(-50, 50, 0.2)*1e-3\n", + "z_grid = np.arange(0, 60, 0.2)*1e-3\n", + "\n", + "imaging = Pipeline(\n", + " steps=(\n", + " RemapToLogicalOrder(),\n", + " Transpose(axes=(0, 2, 1)),\n", + " BandpassFilter(),\n", + " QuadratureDemodulation(),\n", + " Decimation(decimation_factor=4, cic_order=2),\n", + " RxBeamforming(),\n", + " EnvelopeDetection(),\n", + " Transpose(),\n", + " ScanConversion(x_grid=x_grid, z_grid=z_grid),\n", + " LogCompression(),\n", + " DynamicRangeAdjustment(min=5, max=120),\n", + " ToGrayscaleImg()),\n", + " placement=sess.get_device(\"/CPU:0\"))\n", + "\n", + "frame_nr = 0\n", + "rf_data, rf_metadata = get_batch_data(data, metadata, frame_nr), get_batch_metadata(metadata, frame_nr)\n", + "bmode, bmode_metadata = imaging(rf_data, rf_metadata)\n", + "plt.imshow(bmode, cmap=\"gray\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Frame shape: (175, 512)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + ":15: RuntimeWarning: divide by zero encountered in log\n", + " plt.imshow(np.log(np.abs(iq_data.T)), cmap=\"gray\")\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "iq_reconstruction_only_pipeline = Pipeline(\n", + " steps=(\n", + " RemapToLogicalOrder(),\n", + " Transpose(axes=(0, 2, 1)),\n", + " BandpassFilter(),\n", + " QuadratureDemodulation(),\n", + " Decimation(decimation_factor=4, cic_order=2),\n", + " RxBeamforming()),\n", + " placement=sess.get_device('/CPU:0'))\n", + "\n", + "frame_nr = 0\n", + "rf_data, rf_metadata = get_batch_data(data, metadata, frame_nr), get_batch_metadata(metadata, frame_nr)\n", + "\n", + "iq_data, iq_metadata = iq_reconstruction_only_pipeline(rf_data, rf_metadata)\n", + "plt.imshow(np.log(np.abs(iq_data.T)), cmap=\"gray\")\n", + "\n", + "print(f\"Frame shape: {iq_data.shape}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/api/python/setup.py.in b/api/python/setup.py.in index 01a14ea05..f25ada907 100644 --- a/api/python/setup.py.in +++ b/api/python/setup.py.in @@ -24,12 +24,11 @@ setuptools.setup( "Topic :: Scientific/Engineering :: Medical Science Apps." ], install_requires=[ - "numpy>=1.17.4", - "PyYAML>=5.1.2", + "numpy>=1.19.3", "scipy>=1.3.1" ], package_data={ - '${PYTHON_PACKAGE_NAME}': ['devices/*.pyd', 'devices/*.lib'] + '${PYTHON_PACKAGE_NAME}': ['_py_core.pyd', "*.dll"] }, - python_requires='>=3.7', + python_requires='>=3.7' ) diff --git a/api/python/wrappers/core.i b/api/python/wrappers/core.i new file mode 100644 index 000000000..89fb1ba26 --- /dev/null +++ b/api/python/wrappers/core.i @@ -0,0 +1,283 @@ +%include stdint.i +%include exception.i +%include std_shared_ptr.i +%include std_string.i +%include std_unordered_set.i +%include std_vector.i +%include std_pair.i +%include std_optional.i + +%{ +#include "arrus/core/api/ops/us4r/Rx.h" +#include "arrus/core/api/ops/us4r/Tx.h" +#include "arrus/core/api/ops/us4r/TxRxSequence.h" +#include "arrus/core/api/common/types.h" +using namespace ::arrus; +%}; + +// TODO try not declaring explicitly the below types +namespace std { +%template(VectorBool) vector; +%template(VectorFloat) vector; +%template(PairUint32) pair; +%template(PairChannelIdx) pair; + +}; + +// ------------------------------------------ EXCEPTION HANDLING +%exception { + try { + $action + } + // TODO throw arrus specific exceptions + catch(const ::arrus::DeviceNotFoundException& e) { + SWIG_exception(SWIG_ValueError, e.what()); + } + catch(const ::arrus::IllegalArgumentException& e) { + SWIG_exception(SWIG_ValueError, e.what()); + } + catch(const ::arrus::IllegalStateException& e) { + SWIG_exception(SWIG_RuntimeError, e.what()); + } + catch(const ::arrus::TimeoutException& e) { + SWIG_exception(SWIG_RuntimeError, e.what()); + } + catch(const std::exception &e) { + SWIG_exception(SWIG_RuntimeError, e.what()); + } + catch(...) { + SWIG_exception(SWIG_UnknownError, "Unknown exception."); + } +} + +%module core + +%{ +#include +#include + +#include "arrus/core/api/common/types.h" +#include "arrus/common/logging/impl/Logging.h" +#include "arrus/core/api/io/settings.h" +#include "arrus/core/api/session/Session.h" +#include "arrus/core/api/common/logging.h" +#include "arrus/core/api/devices/us4r/Us4R.h" +#include "arrus/core/api/ops/us4r/TxRxSequence.h" + +using namespace ::arrus; +%} + +// Naive assumption that only classes starts with capital letter. +// TODO try enabling underscore option +// However, it is interferring with other swig features, like %template +//%rename("%(undercase)s", notregexmatch$name="^[A-Z].*$") ""; + +%nodefaultctor; + +// TO let know swig about any DLL export macros. +%include "arrus/core/api/common/macros.h" +%include "arrus/core/api/common/types.h" + +// ------------------------------------------ LOGGING +%shared_ptr(arrus::Logger) + +%include "arrus/core/api/common/LogSeverity.h" +%include "arrus/core/api/common/Logger.h" + +%inline %{ + std::shared_ptr<::arrus::Logging> LOGGING_FACTORY; + + // TODO consider moving the below function to %init + void initLoggingMechanism(const ::arrus::LogSeverity level) { + LOGGING_FACTORY = std::make_shared<::arrus::Logging>(); + LOGGING_FACTORY->addClog(level); + ::arrus::setLoggerFactory(LOGGING_FACTORY); + } + + void addLogFile(const std::string &filepath, const ::arrus::LogSeverity level) { + std::shared_ptr logFileStream = + // append to the end of the file + std::make_shared(filepath.c_str(), std::ios_base::app); + LOGGING_FACTORY->addTextSink(logFileStream, level); + } + + void setClogLevel(const ::arrus::LogSeverity level) { + LOGGING_FACTORY->setClogLevel(level); + } + + arrus::Logger::SharedHandle getLogger() { + ::arrus::Logger::SharedHandle logger = LOGGING_FACTORY->getLogger(); + return logger; + } +%} + +// ------------------------------------------ SESSION +%{ +#include "arrus/core/api/session/Session.h" +using namespace ::arrus::session; + +%}; +// TODO consider using unique_ptr anyway (https://stackoverflow.com/questions/27693812/how-to-handle-unique-ptrs-with-swig) + +%shared_ptr(arrus::session::Session); +%ignore createSession; +%include "arrus/core/api/session/Session.h" + +%inline %{ + +std::shared_ptr createSessionSharedHandle(const std::string& filepath) { + std::shared_ptr res = createSession(filepath); + return res; +} +%}; + +// ------------------------------------------ DEVICES +// Us4R +%{ +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/api/devices/Device.h" +#include "arrus/core/api/devices/DeviceWithComponents.h" +#include "arrus/core/api/devices/us4r/Us4R.h" +#include "arrus/core/api/devices/probe/ProbeModelId.h" +#include "arrus/core/api/devices/probe/Probe.h" +#include "arrus/core/api/devices/probe/ProbeModel.h" +#include "arrus/core/api/devices/us4r/FrameChannelMapping.h" +#include "arrus/core/api/devices/us4r/HostBuffer.h" +using namespace arrus::devices; +%}; + +%ignore operator<<(std::ostream &os, const DeviceId &id); +%include "arrus/core/api/devices/DeviceId.h" +%include "arrus/core/api/devices/Device.h" +%include "arrus/core/api/devices/DeviceWithComponents.h" +%include "arrus/core/api/devices/us4r/Us4R.h" +%include "arrus/core/api/devices/probe/ProbeModelId.h" +%include "arrus/core/api/devices/probe/ProbeModel.h" +%include "arrus/core/api/devices/probe/Probe.h" +%shared_ptr(arrus::devices::FrameChannelMapping); +%shared_ptr(arrus::devices::HostBuffer); +%include "arrus/core/api/devices/us4r/FrameChannelMapping.h" +%include "arrus/core/api/devices/us4r/HostBuffer.h" + +namespace std { + %template(UploadResult) pair, std::shared_ptr>; + %template(FrameChannelMappingElement) pair; +}; + +%inline %{ +arrus::devices::Us4R *castToUs4r(arrus::devices::Device *device) { + auto ptr = dynamic_cast(device); + if(!ptr) { + throw std::runtime_error("Given device is not an us4r handle."); + } + return ptr; +} +// TODO(pjarosik) remote the bellow functions when possible + +unsigned short getNumberOfElements(const arrus::devices::ProbeModel &probe) { + const auto &nElements = probe.getNumberOfElements(); + if(nElements.size() > 1) { + throw ::arrus::IllegalArgumentException("The python API currently cannot be use with 3D probes."); + } + return nElements[0]; +} + +double getPitch(const arrus::devices::ProbeModel &probe) { + const auto &pitch = probe.getPitch(); + if(pitch.size() > 1) { + throw ::arrus::IllegalArgumentException("The python API currently cannot be use with 3D probes."); + } + return pitch[0]; +} +%}; + +// ------------------------------------------ COMMON +// Turn on globally value wrappers +%feature("valuewrapper"); + +%ignore arrus::Tuple::operator[]; + +%include "arrus/core/api/common/Tuple.h" +%include "arrus/core/api/common/Interval.h" + +// ------------------------------------------ OPERATIONS + + +// Us4R +%feature("valuewrapper"); +%{ +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/api/ops/us4r/Pulse.h" +#include "arrus/core/api/ops/us4r/Rx.h" +#include "arrus/core/api/ops/us4r/Tx.h" +#include "arrus/core/api/ops/us4r/TxRxSequence.h" +#include +using namespace arrus::ops::us4r; +%}; + + +%feature("valuewrapper") TxRx; +%include "arrus/core/api/ops/us4r/tgc.h" +%include "arrus/core/api/ops/us4r/Pulse.h" +%include "arrus/core/api/ops/us4r/Rx.h" +%include "arrus/core/api/ops/us4r/Tx.h" +%include "arrus/core/api/ops/us4r/TxRxSequence.h" + + +%include "std_vector.i" +%include "typemaps.i" + +namespace std { +%template(TxRxVector) vector; +}; + +%inline %{ + +void TxRxVectorPushBack(std::vector &txrxs, + arrus::ops::us4r::TxRx &txrx) { + txrxs.push_back(txrx); +} + +%}; + + +// ------------------------------------------ SETTINGS +// TODO wrap std optional +// TODO test creating settings +// TODO test reading settings +// TODO feature autodoc +// Turn on globally value wrappers +%feature("valuewrapper"); +%{ +#include "arrus/core/api/devices/us4r/RxSettings.h" +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterModelId.h" +#include "arrus/core/api/devices/probe/ProbeSettings.h" +#include "arrus/core/api/devices/probe/ProbeModel.h" +#include "arrus/core/api/devices/probe/ProbeModelId.h" +#include "arrus/core/api/devices/us4r/HVSettings.h" +#include "arrus/core/api/devices/us4r/HVModelId.h" +#include "arrus/core/api/devices/us4r/Us4RSettings.h" +#include "arrus/core/api/session/SessionSettings.h" + +using namespace ::arrus::devices; +%}; + +%include "arrus/core/api/devices/us4r/RxSettings.h" +%include "arrus/core/api/devices/us4r/Us4OEMSettings.h" +%ignore operator<<(std::ostream &os, const ProbeAdapterModelId &id); +%include "arrus/core/api/devices/us4r/ProbeAdapterModelId.h" +%include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +%ignore operator<<(std::ostream &os, const ProbeModelId &id); +%include "arrus/core/api/devices/probe/ProbeModelId.h" +%include "arrus/core/api/devices/probe/ProbeModel.h" +%include "arrus/core/api/devices/probe/ProbeSettings.h" +%ignore operator<<(std::ostream &os, const HVModelId &id); +%include "arrus/core/api/devices/us4r/HVModelId.h" +%include "arrus/core/api/devices/us4r/HVSettings.h" +%include "arrus/core/api/devices/us4r/Us4RSettings.h" +%include "arrus/core/api/session/SessionSettings.h" + +// ------------------------------------------ IO +%include "arrus/core/api/io/settings.h" diff --git a/api/python/wrappers/idbarlite.i b/api/python/wrappers/idbarlite.i deleted file mode 100644 index 6285f6d9c..000000000 --- a/api/python/wrappers/idbarlite.i +++ /dev/null @@ -1,23 +0,0 @@ -%include "stdint.i" -%include exception.i -%include windows.i - -%exception { - try { - $action - } catch(const std::exception &e) { - SWIG_exception(SWIG_RuntimeError, e.what()); - } -} -%module idbarlite -%ignore dbarlite::DBARLite::Write; -%ignore dbarlite::DBARLite::Read; -%ignore dbarlite::DBARLite::WriteAndRead; -%ignore dbarlite::DBARLiteException; -%{ -#include -#include "iI2CMaster.h" -#include "idbarLite.h" -%} -%include iI2CMaster.h -%include idbarLite.h diff --git a/api/python/wrappers/ihv256.i b/api/python/wrappers/ihv256.i deleted file mode 100644 index c1ac8b102..000000000 --- a/api/python/wrappers/ihv256.i +++ /dev/null @@ -1,21 +0,0 @@ -%include "stdint.i" -%include exception.i -%include windows.i - -%exception { - try { - $action - } catch(const std::exception &e) { - SWIG_exception(SWIG_RuntimeError, e.what()); - } -} - -%module ihv256 -%{ -#include -#include "iI2CMaster.h" -#include "ihv256.h" -%} - -%include iI2CMaster.h -%include ihv256.h diff --git a/api/python/wrappers/ius4oem.i b/api/python/wrappers/ius4oem.i deleted file mode 100644 index 76171fd6e..000000000 --- a/api/python/wrappers/ius4oem.i +++ /dev/null @@ -1,157 +0,0 @@ -%include stdint.i -%include exception.i -%include windows.i - -%exception { - try { - $action - } catch(const std::exception &e) { - SWIG_exception(SWIG_RuntimeError, e.what()); - } catch(...) { - std::cout << "Unhandled type of exception! Check logs for more information" << std::endl; - } -} - -%module(directors="1") ius4oem - -%include -%shared_ptr(IUs4OEM) - -%include "carrays.i" -%array_functions(unsigned short, uint16Array); -%array_functions(double, doubleArray); - - -%ignore us4oem::Us4OEMException; -%ignore us4oem::afe58jd18::Register195; -%ignore us4oem::afe58jd18::Register196; -%ignore us4oem::afe58jd18::Register203; -%ignore us4oem::afe58jd18::REGISTER_ADDRESS; -// TODO(pjarosik) should not be part of the ius4oem interface! -%ignore AttachCVSeries; -%{ -#include -#include -#include -#include "iI2CMaster.h" -#include -#include -#include -#include -#include "core/api/DataAcquiredEvent.h" - -static constexpr size_t NCH = IUs4OEM::NCH; -%} -%include afe58jd18Registers.h -%include ius4oem.h -%include iI2CMaster.h -%include "core/api/Event.h" -%include "core/api/DataAcquiredEvent.h" - -%feature("director") ScheduleReceiveCallback; - -%inline %{ - -// TODO (pjarosik) move this callback to some other place -class ScheduleReceiveCallback { -public: - virtual void run(const arrus::DataAcquiredEvent& event) const = 0; - virtual ~ScheduleReceiveCallback() {}; -}; - -void ScheduleReceiveWithCallback(IUs4OEM* that, const size_t address, - const size_t length, - const size_t start, - const size_t decimation, - ScheduleReceiveCallback& callback) { - auto fn = [&callback, address, length] () { - // TODO(pjarosik) consider creating event outside the lambda function, to reduce interrupt handling time. - const arrus::DataAcquiredEvent& event = arrus::DataAcquiredEvent(address, length); - - PyGILState_STATE gstate = PyGILState_Ensure(); - try { - callback.run(event); - } catch(const std::exception &e) { - std::cerr << e.what() << std::endl; - } - PyGILState_Release(gstate); - }; - throw std::runtime_error("Schedule receive function is not implemented in this release."); -// that->ScheduleReceive(address, length, start, decimation, fn); -} - -void ScheduleReceiveWithoutCallback(IUs4OEM* that, - const size_t address, - const size_t length, - const size_t start, - const size_t decimation) { - throw std::runtime_error("Schedule receive function is not implemented in this release."); -// that->ScheduleReceive(firing, address, length, start, decimation); -} - - -// TODO(pjarosik) fix below in more elegant way -void TransferRXBufferToHostLocation(IUs4OEM* that, unsigned long long dstAddress, size_t length, size_t srcAddress) { - that->TransferRXBufferToHost((unsigned char*) dstAddress, length, srcAddress+0x1'0000'0000); -} - -II2CMaster* castToII2CMaster(IUs4OEM* ptr) { - return dynamic_cast(ptr); -} - -std::shared_ptr getUs4OEMPtr(unsigned idx) { - return std::shared_ptr(GetUs4OEM(idx)); -} - -void EnableReceiveDelayed(IUs4OEM* ptr) { - throw std::runtime_error("Enable receive function is not implemented in this release."); -// ptr->EnableReceive(); -// std::this_thread::sleep_for(std::chrono::milliseconds(1)); -} - -void setTxApertureCustom(IUs4OEM* that, const unsigned short* enabled, const size_t length, const unsigned short firing) { - std::bitset aperture; - for(int i=0; i < length; ++i) { - if(enabled[i]) { - aperture.set(i); - } - } - that->SetTxAperture(aperture, firing); -} - -void setActiveChannelGroupCustom( - IUs4OEM* that, - const unsigned short* enabled, - const size_t length, - const unsigned short firing) -{ - std::bitset mask; - for(int i=0; i < length; ++i) { - if(enabled[i]) { - mask.set(i); - } - } - that->SetActiveChannelGroup(mask, firing); -} - -void setRxApertureCustom(IUs4OEM* that, const unsigned short* enabled, const size_t length, const unsigned short firing) -{ - std::bitset aperture; - for(int i=0; i < length; ++i) { - if(enabled[i]) { - aperture.set(i); - } - } - that->SetRxAperture(aperture, firing); -} - -void setTGCSamplesCustom(IUs4OEM* that, const double* input, - const size_t length, const unsigned short firing) -{ - std::vector samples(length); - for(int i=0; i < length; ++i) { - samples[i] = input[i]; - } - that->TGCSetSamples(samples, firing); -} -%} diff --git a/api/python/wrappers/std_optional.i b/api/python/wrappers/std_optional.i new file mode 100644 index 000000000..f7e040da8 --- /dev/null +++ b/api/python/wrappers/std_optional.i @@ -0,0 +1,18 @@ +%typemap(in) std::optional %{ + if($input == Py_None) { + $1 = std::optional(); + } + else { + $1 = std::optional((float)PyFloat_AsDouble($input)); + } +%} + +%typemap(out) std::optional %{ + if($1) { + $result = PyFloat_FromDouble(*$1); + } + else { + $result = Py_None; + Py_INCREF(Py_None); + } +%} diff --git a/api/python/wrappers/std_unique_ptr.i b/api/python/wrappers/std_unique_ptr.i new file mode 100644 index 000000000..a56d17ad4 --- /dev/null +++ b/api/python/wrappers/std_unique_ptr.i @@ -0,0 +1,32 @@ +// Based on stackoverflow/flexo's answer +namespace std { + %feature("novaluewrapper") unique_ptr; + + template + struct unique_ptr { + typedef Type* pointer; + + explicit unique_ptr( pointer Ptr ); + unique_ptr (unique_ptr&& Right); + template unique_ptr( unique_ptr&& Right ); + unique_ptr( const unique_ptr& Right) = delete; + + pointer operator-> () const; + pointer release (); + void reset (pointer __p=pointer()); + void swap (unique_ptr &__u); + pointer get () const; + operator bool () const; + + ~unique_ptr(); + }; +} + +%define wrap_unique_ptr(Name, Type) + %template(Name) std::unique_ptr; + %newobject std::unique_ptr::release; + + %typemap(out) std::unique_ptr %{ + $result = SWIG_NewPointerObj(new $1_ltype(std::move($1)), $&1_descriptor, SWIG_POINTER_OWN); + %} +%enddef \ No newline at end of file diff --git a/arrus/common/asserts.h b/arrus/common/asserts.h new file mode 100644 index 000000000..2f7f09755 --- /dev/null +++ b/arrus/common/asserts.h @@ -0,0 +1,110 @@ +#ifndef ARRUS_COMMON_ASSERTS_H +#define ARRUS_COMMON_ASSERTS_H + +#include "arrus/core/api/common/exceptions.h" + +#define ARRUS_REQUIRES_TRUE(CONDITION, MSG) \ +do { \ + if (!(CONDITION)) { \ + throw ::arrus::ArrusException((MSG)); \ + } \ +} while(0) + + +#define ARRUS_REQUIRES_TRUE_E(CONDITION, EXCEPTION) \ +do { \ + if (!(CONDITION)) { \ + throw (EXCEPTION); \ + } \ +} while(0) + +#define ARRUS_REQUIRES_EQUAL(A, B, EXCEPTION) \ +do { \ + if (!((A) == (B))) { \ + throw EXCEPTION; \ + } \ +} while(0) + +#define ARRUS_REQUIRES_EQUAL_IAE(A, B) \ + ARRUS_REQUIRES_EQUAL(A, B, IllegalArgumentException(#A " != " #B)) + +#define ARRUS_REQUIRES_NON_EMPTY_IAE(coll) \ +do { \ + if (coll.empty()) { \ + throw IllegalArgumentException(#coll " cannot be empty"); \ + } \ +} while(0) + +#define ARRUS_REQUIRES_TRUE_FOR_ARGUMENT(CONDITION, MSG) \ +do { \ + if (!(CONDITION)) { \ + throw ::arrus::IllegalArgumentException((MSG)); \ + } \ +} while(0) + +#define ARRUS_REQUIRES_NO_THROW(EXPR, IN_EXCEPTION_TYPE, OUT_EXCEPTION) \ +do { \ + try { \ + EXPR; \ + } catch(const IN_EXCEPTION_TYPE&) { \ + throw (OUT_EXCEPTION); \ + } \ +} while(0) + +/** + * Check if A >= B, otherwise throws arrus::IllegalArgumentException. + */ +#define ARRUS_REQUIRES_AT_LEAST(A, B, MSG) \ +do { \ + if (!((A) >= (B))) { \ + throw ::arrus::IllegalArgumentException((MSG)); \ + } \ +} while(0) + +/** + * Check if A >= B, otherwise throws arrus::IllegalArgumentException. + */ +#define ARRUS_REQUIRES_AT_MOST(A, B, MSG) \ +do { \ + if (!((A) <= (B))) { \ + throw ::arrus::IllegalArgumentException((MSG)); \ + } \ +} while(0) + +/** + * Check if A >= B, otherwise throws arrus::IllegalArgumentException. + */ +#define ARRUS_REQUIRES_IN_CLOSED_INTERVAL(value, min, max, MSG) \ +do { \ + if (!(((value) >= (min) && (value) <= (max)))) { \ + throw ::arrus::IllegalArgumentException((MSG)); \ + } \ +} while(0) + +#define ARRUS_REQUIRES_IN_CLOSED_INTERVAL_E(value, min, max, exception) \ +do { \ + if (!(((value) >= (min) && (value) <= (max)))) { \ + throw exception; \ + } \ +} while(0) + +#define ARRUS_REQUIRES_DATA_TYPE_E(value, dtype, exception) \ + ARRUS_REQUIRES_IN_CLOSED_INTERVAL_E(value, \ + (std::numeric_limits::min)(), \ + (std::numeric_limits::max)(), \ + exception) + +#define ARRUS_REQUIRES_DATA_TYPE(value, dtype, msg) \ + ARRUS_REQUIRES_DATA_TYPE_E(value, dtype, ::arrus::IllegalArgumentException(msg)) \ + +#define ARRUS_WAIT_FOR_CV_OPTIONAL_TIMEOUT(cv, lock, timeout, exceptionMsg) \ + if(timeout > -1) { \ + auto status = cv.wait_for(lock ,std::chrono::milliseconds(timeout)); \ + if(status == std::cv_status::timeout) { \ + throw TimeoutException(exceptionMsg); \ + } \ + } \ + else { \ + cv.wait(lock); \ + } +#endif //ARRUS_COMMON_ASSERTS_H diff --git a/arrus/common/compiler.h b/arrus/common/compiler.h new file mode 100644 index 000000000..b4a390d9e --- /dev/null +++ b/arrus/common/compiler.h @@ -0,0 +1,28 @@ +#ifndef ARRUS_COMMON_COMPILER_H +#define ARRUS_COMMON_COMPILER_H + +#define IGNORE_UNUSED(x) do {(void)(x);} while(0) + +#ifdef _MSC_VER +#define COMPILER_PUSH_DIAGNOSTIC_STATE __pragma(warning(push)) +#define COMPILER_POP_DIAGNOSTIC_STATE __pragma(warning(pop)) +#define COMPILER_IGNORE_UNUSED __pragma(warning(disable: 4100 4101)) +#define COMPILER_DISABLE_MSVC_WARNINGS(...) __pragma(warning(disable: __VA_ARGS__)) + +#else +#define COMPILER_PUSH_DIAGNOSTIC_STATE _Pragma("GCC diagnostic push") +#define COMPILER_POP_DIAGNOSTIC_STATE _Pragma("GCC diagnostic pop") +#define COMPILER_IGNORE_UNUSED _Pragma("GCC diagnostic ignored \"-Wunused-parameter\"") _Pragma("GCC diagnostic ignored \"-Wunused-variable\"") +#define COMPILER_DISABLE_MSVC_WARNINGS(codes) +#endif + +namespace arrus { + +template +inline bool isInstanceOf(const InType *in){ + return dynamic_cast(in) != nullptr; +} + +} + +#endif //ARRUS_COMMON_COMPILER_H diff --git a/arrus/common/format.h b/arrus/common/format.h new file mode 100644 index 000000000..9984672e4 --- /dev/null +++ b/arrus/common/format.h @@ -0,0 +1,112 @@ +#ifndef ARRUS_COMMON_FORMAT_H +#define ARRUS_COMMON_FORMAT_H + +// String formatting and parsing utilities. +// Currently wraps fmt library calls. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "arrus/core/api/common/Tuple.h" +#include "arrus/core/api/common/Interval.h" + +namespace arrus { + +template +auto format(Args &&... args) { + return fmt::format(std::forward(args)...); +} + +/** + * Returns true if the given string contains numeric characters only. + * + * @param num string to verify + * @return true if the given string contains numeric characters only, + * false otherwise. + */ +inline bool isDigitsOnly(const std::string &num) { + return std::all_of(num.begin(), num.end(), isdigit); +} + +/** + * General purpose 'toString' function basing on ostream << operator. + */ +template +std::string toString(const T &t) { + std::ostringstream ss; + ss << t; + return ss.str(); +} + +template +inline std::string toString(const std::vector &values) { + std::vector vStr(values.size()); + std::transform(std::begin(values), std::end(values), std::begin(vStr), + [](auto v) { return std::to_string(v); }); + return boost::algorithm::join(vStr, ", "); +} + +template +inline std::string toString( + const gsl::span &values) { + std::vector vStr(values.size()); + std::transform(std::begin(values), std::end(values), std::begin(vStr), + [](auto v) { return std::to_string(v); }); + return boost::algorithm::join(vStr, ", "); +} + +template +inline std::string toStringTransform( + const std::vector &values, + const std::function &func) { + std::vector vStr(values.size()); + std::transform(std::begin(values), std::end(values), std::begin(vStr), + [&func](T v) { return func(v); }); + return boost::algorithm::join(vStr, ", "); +} + +template +inline std::string toString(const ::std::set &values) { + std::vector vStr(values.size()); + std::transform(std::begin(values), std::end(values), std::begin(vStr), + [](auto v) { return std::to_string(v); }); + return boost::algorithm::join(vStr, ", "); +} + +template +inline std::string toString(const ::std::unordered_set &values) { + std::vector vStr(values.size()); + std::transform(std::begin(values), std::end(values), std::begin(vStr), + [](auto v) { return std::to_string(v); }); + return boost::algorithm::join(vStr, ", "); +} + +template +inline std::string toString(const std::optional value) { + if(value.has_value()) { + return std::to_string(value.value()); + } else return "(no value)"; +} + +template +inline std::string toString(const Tuple tuple) { + return ::arrus::format("Tuple({})", toString(tuple.getValues())); +} + +template +inline std::string toString(const Interval i) { + return ::arrus::format("Interval: start: {}, right: {}", + i.start(), i.end()); +} + +} + +#endif //ARRUS_COMMON_FORMAT_H diff --git a/arrus/common/logging/impl/LogSeverity.cpp b/arrus/common/logging/impl/LogSeverity.cpp new file mode 100644 index 000000000..34188d07d --- /dev/null +++ b/arrus/common/logging/impl/LogSeverity.cpp @@ -0,0 +1,25 @@ +#include "arrus/core/api/common/LogSeverity.h" + +namespace arrus { + +std::ostream &operator<<(std::ostream &stream, arrus::LogSeverity level) { + static const char *enumStrs[] = + { + "TRACE", + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "FATAL" + }; + + if((unsigned) (level) < sizeof(enumStrs) / sizeof(*enumStrs)) { + stream << enumStrs[(unsigned int) level]; + } else { + stream << static_cast(level); + } + return stream; +} + +} + diff --git a/arrus/common/logging/impl/LoggerImpl.h b/arrus/common/logging/impl/LoggerImpl.h new file mode 100644 index 000000000..618e15907 --- /dev/null +++ b/arrus/common/logging/impl/LoggerImpl.h @@ -0,0 +1,61 @@ +#ifndef ARRUS_COMMON_LOGGING_IMPL_LOGGERIMPL_H +#define ARRUS_COMMON_LOGGING_IMPL_LOGGERIMPL_H + +#include +#include +#include +#include +#include + +#include "arrus/core/api/common/Logger.h" + +namespace arrus { + +/** + * Basic logger instance that can be used in the arrus library. + * + * Currently, it is a simple wrapper over boost::severity_logger_mt. + * + * This class should not be available publicly. + */ +class LoggerImpl : public Logger { +public: + + LoggerImpl() = default; + + /** + * Creates a logger with DeviceId attribute set. + * + * @param attributes attributes to set + */ + explicit LoggerImpl(const std::vector &attributes) { + for(auto &[key, value] : attributes) { + logger.add_attribute(key, + boost::log::attributes::constant( + value)); + } + } + + /** + * Logs a given string message with given severity level. + * + * @param severity severity attached to the message + * @param msg message to log + */ + void + log(const LogSeverity severity, const std::string &msg) override { + BOOST_LOG_SEV(logger, severity) << msg; + } + + void + setAttribute(const std::string &key, const std::string &value) override { + logger.add_attribute(key, boost::log::attributes::constant( + value)); + } + +private: + boost::log::sources::severity_logger_mt logger; +}; +} + +#endif //ARRUS_COMMON_LOGGING_IMPL_LOGGERIMPL_H diff --git a/arrus/common/logging/impl/Logging.cpp b/arrus/common/logging/impl/Logging.cpp new file mode 100644 index 000000000..95401d0ba --- /dev/null +++ b/arrus/common/logging/impl/Logging.cpp @@ -0,0 +1,83 @@ +#include +#include + + + +#include "arrus/core/api/common/LogSeverity.h" +#include "arrus/common/logging/impl/Logging.h" + + +BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", arrus::LogSeverity) +BOOST_LOG_ATTRIBUTE_KEYWORD(deviceIdLogAttr, "DeviceId", std::string) + +namespace arrus { + +typedef boost::log::sinks::synchronous_sink< + boost::log::sinks::text_ostream_backend> textSink; + +static boost::shared_ptr +addTextSinkBoostPtr(const boost::shared_ptr &ostream, + LogSeverity minSeverity, bool autoFlush) { + + boost::shared_ptr sink = boost::make_shared(); + + sink->locked_backend()->add_stream(ostream); + sink->locked_backend()->auto_flush(autoFlush); + sink->set_filter(severity >= minSeverity); + + namespace expr = boost::log::expressions; + + boost::log::formatter formatter = expr::stream + << "[" + << expr::format_date_time( + "TimeStamp", + "%Y-%m-%d %H:%M:%S") + << "]" + << expr::if_(expr::has_attr(deviceIdLogAttr)) + [ + expr::stream << "[" << deviceIdLogAttr << "]" + ] + << " " << severity << ": " + << expr::smessage; + sink->set_formatter(formatter); + boost::log::core::get()->add_sink(sink); + return sink; +} + +Logging::Logging() { + boost::log::add_common_attributes(); +} + +void +Logging::addTextSink(std::shared_ptr &ostream, + LogSeverity minSeverity, bool autoFlush) { + boost::shared_ptr boostPtr = boost::shared_ptr( + ostream.get(), + [ostream](std::ostream *) mutable { ostream.reset(); }); + addTextSinkBoostPtr(boostPtr, minSeverity, autoFlush); +} + +void Logging::addClog(LogSeverity severity) { + boost::shared_ptr stream(&std::clog, boost::null_deleter()); + this->clogSink = addTextSinkBoostPtr(stream, severity, false); +} + +void Logging::setClogLevel(LogSeverity level) { + if(this->clogSink == nullptr) { + this->addClog(level); + } else { + this->clogSink->set_filter(severity >= level); + } +} + +Logger::Handle Logging::getLogger() { + return std::make_unique(); +} + +Logger::Handle +Logging::getLogger(const std::vector &attributes) { + return std::make_unique(attributes); +} + + +} diff --git a/arrus/common/logging/impl/Logging.h b/arrus/common/logging/impl/Logging.h new file mode 100644 index 000000000..11eb6a044 --- /dev/null +++ b/arrus/common/logging/impl/Logging.h @@ -0,0 +1,69 @@ +#ifndef ARRUS_COMMON_LOGGING_IMPL_LOGGING_H +#define ARRUS_COMMON_LOGGING_IMPL_LOGGING_H + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "arrus/core/api/common/LogSeverity.h" +#include "arrus/core/api/common/LoggerFactory.h" +#include "arrus/common/logging/impl/LoggerImpl.h" + +namespace arrus { + +/** + * Log settings used in the arrus package. + */ +class Logging: public LoggerFactory { +public: + Logging(); + /** + * Adds a given given filename + * + * @param filename a path to the output log file + * @param severity severity level of the records that will be stored in the + * given output file + */ + void addTextSink(std::shared_ptr &ostream, LogSeverity severity, + bool autoFlush = false); + + /** + * Sets a minimum severity level for messages printed to the standard output. + * + * @param severity severity level to apply + */ + void addClog(LogSeverity severity); + + /** + * Sets logging level for clog. + * + * Adds clog if necessary. + * + * @param level level to set + */ + void setClogLevel(LogSeverity level); + + Logger::Handle getLogger() override; + + Logger::Handle + getLogger(const std::vector &attributes) override; + + + Logging(Logging const &) = delete; + void operator=(Logging const &) = delete; + Logging(Logging const &&) = delete; + void operator=(Logging const &&) = delete; +private: + boost::shared_ptr> clogSink; +}; +} + +#endif //ARRUS_COMMON_LOGGING_IMPL_LOGGING_H diff --git a/arrus/common/utils.h b/arrus/common/utils.h new file mode 100644 index 000000000..b4c21e008 --- /dev/null +++ b/arrus/common/utils.h @@ -0,0 +1,28 @@ +#ifndef ARRUS_COMMON_UTILS_H +#define ARRUS_COMMON_UTILS_H + +#include + +#include "arrus/common/asserts.h" +#include "arrus/common/format.h" + +namespace arrus { + +template +V safeCast(const T &in, const std::string& paramName, + const std::string& requiredTypeName) { + ARRUS_REQUIRES_DATA_TYPE_E( + in, V, std::runtime_error( + ::arrus::format("Data type mismatch: value '{}' cannot be " + "safely casted to type {}.", + paramName, requiredTypeName))); + + return static_cast(in); +} + +#define ARRUS_SAFE_CAST(value, dtype) \ + ::arrus::safeCast((value), #value, #dtype) + +} + +#endif //ARRUS_COMMON_UTILS_H diff --git a/arrus/core/CMakeLists.txt b/arrus/core/CMakeLists.txt new file mode 100644 index 000000000..746171223 --- /dev/null +++ b/arrus/core/CMakeLists.txt @@ -0,0 +1,263 @@ +set(TARGET_NAME arrus-core) + +################################################################################ +# protobuf +################################################################################ +protobuf_generate_cpp(PROTO_SRC PROTO_HDRS + io/proto/Dictionary.proto + io/proto/session/SessionSettings.proto + io/proto/common/IntervalInteger.proto + io/proto/common/IntervalDouble.proto + io/proto/common/LinearFunction.proto + io/proto/devices/probe/ProbeModel.proto + io/proto/devices/us4r/ProbeAdapterModel.proto + io/proto/devices/us4r/ProbeToAdapterConnection.proto + io/proto/devices/us4r/RxSettings.proto + io/proto/devices/us4r/Us4OEMSettings.proto + io/proto/devices/us4r/HVSettings.proto + io/proto/devices/us4r/Us4RSettings.proto + ) +################################################################################ +# Target +################################################################################ +set(SRC_FILES + api/common/exceptions.h + api/common/logging.h + api/common/types.h + api/common/Tuple.h + api/common/Interval.h + api/devices/DeviceWithComponents.h + api/devices/Device.h + api/devices/TriggerGenerator.h + api/devices/DeviceId.h + api/devices/probe/Probe.h + api/devices/probe/ProbeModel.h + api/devices/probe/ProbeModelId.h + api/devices/probe/ProbeSettings.h + api/devices/us4r/ProbeAdapter.h + api/devices/us4r/ProbeAdapterModelId.h + api/devices/us4r/ProbeAdapterSettings.h + api/devices/us4r/Us4OEM.h + api/devices/us4r/Us4OEMSettings.h + api/devices/us4r/Us4R.h + api/devices/us4r/Us4RSettings.h + api/devices/us4r/RxSettings.h + api/session/Session.h + api/session/SessionSettings.h + api/ops/us4r/Pulse.h + + common/hash.h + common/collections.h + + common/validation.h + common/logging.h + common/logging.cpp + + api/common/Logger.h + api/common/LogSeverity.h + api/common/LoggerFactory.h + ../common/compiler.h + ../common/asserts.h + ../common/format.h + + devices/utils.h + devices/DeviceId.cpp + devices/DeviceId.h + devices/SettingsValidator.h + devices/TxRxParameters.h + devices/TxRxParameters.cpp + + + devices/us4r/us4oem/Us4OEMFactory.h + devices/us4r/us4oem/Us4OEMFactoryImpl.h + devices/us4r/us4oem/Us4OEMImpl.h + devices/us4r/us4oem/Us4OEMImpl.cpp + devices/us4r/us4oem/Us4OEMSettingsValidator.h + devices/us4r/us4oem/Us4OEMSettings.h + devices/us4r/us4oem/Us4OEMSettings.cpp + + devices/us4r/Us4RFactory.h + devices/us4r/Us4RFactoryImpl.h + devices/us4r/Us4RImpl.h + devices/us4r/Us4RSettingsValidator.h + devices/us4r/Us4RSettingsConverter.h + devices/us4r/Us4RSettingsConverterImpl.h + devices/us4r/Us4RSettings.h + devices/us4r/Us4RSettings.cpp + devices/us4r/RxSettings.h + devices/us4r/RxSettings.cpp + + devices/us4r/probeadapter/ProbeAdapterFactory.h + devices/us4r/probeadapter/ProbeAdapterFactoryImpl.h + devices/us4r/probeadapter/ProbeAdapterImpl.h + devices/us4r/probeadapter/ProbeAdapterImpl.cpp + devices/us4r/probeadapter/ProbeAdapterSettingsValidator.h + devices/us4r/probeadapter/ProbeAdapterSettings.h + devices/us4r/probeadapter/ProbeAdapterSettings.cpp + + devices/probe/ProbeFactory.h + devices/probe/ProbeFactoryImpl.h + devices/probe/ProbeImpl.h + devices/probe/ProbeImpl.cpp + devices/probe/ProbeSettingsValidator.h + devices/probe/ProbeSettings.h + devices/probe/ProbeSettings.cpp + devices/probe/ProbeModel.h + devices/probe/ProbeModel.cpp + + api/io/settings.h + io/settings.cpp + io/validators/ProbeModelProtoValidator.h + io/validators/ProbeAdapterModelProtoValidator.h + io/validators/RxSettingsProtoValidator.h + io/validators/SessionSettingsProtoValidator.h + io/validators/DictionaryProtoValidator.h + io/validators/ProbeToAdapterConnectionProtoValidator.h + io/SettingsDictionary.h + + session/SessionImpl.cpp + session/SessionSettings.h + session/SessionSettings.cpp + + devices/us4r/external/ius4oem/IUs4OEMFactory.h + devices/us4r/external/ius4oem/IUs4OEMFactoryImpl.h + devices/us4r/external/ius4oem/LNAGainValueMap.h + devices/us4r/external/ius4oem/PGAGainValueMap.h + devices/us4r/external/ius4oem/ActiveTerminationValueMap.h + devices/us4r/external/ius4oem/LPFCutoffValueMap.h + devices/us4r/external/ius4oem/DTGCAttenuationValueMap.h + devices/us4r/external/ius4oem/Us4RLoggerWrapper.h + devices/us4r/external/ius4oem/IUs4OEMInitializer.h + devices/us4r/external/ius4oem/IUs4OEMInitializerImpl.h + devices/us4r/Us4ROutputBuffer.h + devices/us4r/Us4RImpl.cpp + devices/us4r/common.h + devices/us4r/common.cpp + + session/SessionImpl.h + session/SessionImpl.cpp + common/tests.h + common/interpolate.h + # TODO(pjarosik) cleanup below + api/common/macros.h + api/framework/Variable.h + api/framework/Tensor.h + api/framework/Constant.h + framework/graph/Graph.h + api/framework/Op.h + api/devices.h + api/framework.h + api/ops/us4r/tgc.h + api/examples.h + devices/UltrasoundDevice.h api/devices/us4r/HVSettings.h api/devices/us4r/HVModelId.h devices/us4r/hv/HV256Impl.h devices/us4r/hv/HV256Factory.h devices/us4r/hv/HV256Impl.cpp devices/us4r/hv/HV256FactoryImpl.h devices/us4r/hv/HV256FactoryImpl.cpp api/devices/us4r/FrameChannelMapping.h devices/us4r/FrameChannelMappingImpl.cpp devices/us4r/FrameChannelMappingImpl.h common/aperture.h external/eigen/Tensor.h devices/us4r/us4oem/Us4OEMImplBase.h devices/us4r/probeadapter/ProbeAdapterImplBase.h devices/probe/ProbeImplBase.h external/eigen/Dense.h ../common/utils.h devices/us4r/DataTransfer.h api/devices/us4r/HostBuffer.h api/framework/FifoBuffer.h devices/us4r/us4oem/Us4OEMBuffer.h devices/us4r/Us4RBuffer.h) + +set_source_files_properties(${SRC_FILES} PROPERTIES COMPILE_FLAGS + "${ARRUS_CPP_STRICT_COMPILE_OPTIONS}") + +# We do not use strict compile options (-wall, /w4, etc.) for auto generated files, +# because some of those files generate compile warnings (e.g. protobuf files). + +add_library(${TARGET_NAME} SHARED ${SRC_FILES} ${PROTO_SRC} ${PROTO_HDRS}) +################################################################################ +# Compile definitions +################################################################################ +target_compile_definitions(${TARGET_NAME} + PRIVATE + "BOOST_ALL_NO_LIB" + ) + +################################################################################ +# Include directories +################################################################################ +target_include_directories(${TARGET_NAME} + PRIVATE + ${ARRUS_ROOT_DIR} + ${CMAKE_CURRENT_BINARY_DIR}) + +################################################################################ +# Dependencies +################################################################################ +target_link_libraries(${TARGET_NAME} + PRIVATE + Us4::US4OEM + Us4::HV256 + Us4::DBARLite + Boost::Boost + protobuf::libprotobuf + fmt::fmt + Eigen3::Eigen3 + Microsoft.GSL::GSL) +################################################################################ +# Target compile options +################################################################################ +# strict compile options defined for on the source level +target_compile_options(${TARGET_NAME} PRIVATE + ${ARRUS_CPP_COMMON_COMPILE_OPTIONS}) +target_compile_definitions(${TARGET_NAME} PRIVATE + ${ARRUS_CPP_COMMON_COMPILE_DEFINITIONS} + # MSVC dll export declspec + ARRUS_CPP_API_BUILD_STAGE) +################################################################################ +# Tests +################################################################################ +if (ARRUS_RUN_TESTS) + find_package(GTest REQUIRED) + + set(ARRUS_CORE_DEVICES_TESTS_SRCS + devices/DeviceId.cpp common/logging.cpp) + # core::devices test + create_core_test(devices/DeviceIdTest.cpp devices/DeviceId.cpp) + create_core_test(devices/utilsTest.cpp) + create_core_test(devices/us4r/us4oem/Us4OEMSettingsValidatorTest.cpp "${ARRUS_CORE_DEVICES_TESTS_SRCS}") + create_core_test(devices/us4r/probeadapter/ProbeAdapterSettingsValidatorTest.cpp devices/DeviceId.cpp) + create_core_test(devices/probe/ProbeSettingsValidatorTest.cpp devices/DeviceId.cpp) + set(US4OEM_FACTORY_IMPL_TEST_DEPS devices/DeviceId.cpp common/logging.cpp devices/us4r/us4oem/Us4OEMImpl.cpp + devices/TxRxParameters.cpp devices/us4r/FrameChannelMappingImpl.cpp) + create_core_test(devices/us4r/us4oem/Us4OEMFactoryImplTest.cpp "${US4OEM_FACTORY_IMPL_TEST_DEPS}") +# # create_core_test(devices/us4r/Us4RFactoryImplTest.cpp devic) + create_core_test(devices/us4r/Us4RSettingsConverterImplTest.cpp devices/DeviceId.cpp) + create_core_test(devices/us4r/external/ius4oem/IUs4OEMInitializerImplTest.cpp) +# create_core_test(devices/us4r/Us4ROutputBufferTest.cpp "common/logging.cpp") + create_core_test(devices/us4r/commonTest.cpp "devices/us4r/common.cpp;devices/TxRxParameters.cpp") +# + set(US4OEM_IMPL_TEST_DEPS common/logging.cpp devices/us4r/us4oem/Us4OEMImpl.cpp + devices/us4r/common.cpp + devices/TxRxParameters.cpp devices/DeviceId.cpp devices/us4r/FrameChannelMappingImpl.cpp) + create_core_test(devices/us4r/us4oem/Us4OEMImplTest.cpp "${US4OEM_IMPL_TEST_DEPS}") + + set(ADAPTER_IMPL_TEST_DEPS common/logging.cpp devices/us4r/probeadapter/ProbeAdapterImpl.cpp + devices/us4r/common.cpp + devices/TxRxParameters.cpp devices/DeviceId.cpp devices/us4r/FrameChannelMappingImpl.cpp) + create_core_test(devices/us4r/probeadapter/ProbeAdapterImplTest.cpp "${ADAPTER_IMPL_TEST_DEPS}") + # core::io tests + set(ARRUS_CORE_IO_TEST_DATA ${CMAKE_CURRENT_SOURCE_DIR}/io/test-data) + create_core_test( + io/settingsTest.cpp + "" # no additional source files + "protobuf::libprotobuf;arrus-core" + "-DARRUS_TEST_DATA_PATH=\"${ARRUS_CORE_IO_TEST_DATA}\"") +endif () + +################################################################################ +# Installation +################################################################################ +install( + TARGETS + ${TARGET_NAME} + DESTINATION + ${ARRUS_LIB_INSTALL_DIR} +) + +################################################################################ +# Examples +################################################################################ +add_executable(core-example + examples/CoreExample.cpp + ../common/logging/impl/Logging.cpp + ../common/logging/impl/LogSeverity.cpp) +target_link_libraries(core-example + PRIVATE + arrus-core + Boost::Boost + fmt::fmt) +target_include_directories(core-example PRIVATE ${ARRUS_ROOT_DIR}) diff --git a/arrus/core/api/common/Interval.h b/arrus/core/api/common/Interval.h new file mode 100644 index 000000000..af6302bc6 --- /dev/null +++ b/arrus/core/api/common/Interval.h @@ -0,0 +1,44 @@ +#ifndef ARRUS_CORE_API_COMMON_INTERVAL_H +#define ARRUS_CORE_API_COMMON_INTERVAL_H + +#include + +#include "arrus/core/api/common/exceptions.h" + +namespace arrus { + +template +class Interval { +public: + Interval(const T &start, const T &end) + : Interval(std::make_pair(start, end)) {} + + explicit Interval(const std::pair &interval) { + if(interval.first > interval.second) { + throw IllegalArgumentException("Start should not be greater " + "than the end of the interval."); + } + this->interval = {interval.first, interval.second}; + } + + const T &start() const { return interval.first; } + + const T &end() const { return interval.second; } + + std::pair asPair() const {return interval;} + + bool operator==(const Interval &rhs) const { + return interval == rhs.interval; + } + + bool operator!=(const Interval &rhs) const { + return !(rhs == *this); + } + +private: + std::pair interval; +}; + +} + +#endif //ARRUS_CORE_API_COMMON_INTERVAL_H diff --git a/arrus/core/api/common/LogSeverity.h b/arrus/core/api/common/LogSeverity.h new file mode 100644 index 000000000..5e2beb11d --- /dev/null +++ b/arrus/core/api/common/LogSeverity.h @@ -0,0 +1,20 @@ +#ifndef ARRUS_CORE_API_COMMON_LOGSEVERITY_H +#define ARRUS_CORE_API_COMMON_LOGSEVERITY_H + +#include + +namespace arrus { + +enum class LogSeverity { + TRACE, + DEBUG, + INFO, + WARNING, + ERROR, + FATAL +}; +std::ostream &operator<<(std::ostream &stream, arrus::LogSeverity level); + +} + +#endif //ARRUS_CORE_API_COMMON_LOGSEVERITY_H diff --git a/arrus/core/api/common/Logger.h b/arrus/core/api/common/Logger.h new file mode 100644 index 000000000..abdc0c26f --- /dev/null +++ b/arrus/core/api/common/Logger.h @@ -0,0 +1,47 @@ +#ifndef ARRUS_CORE_API_COMMON_LOGGER_H +#define ARRUS_CORE_API_COMMON_LOGGER_H + +#include +#include "LogSeverity.h" + +namespace arrus { + +/** + * Basic logger instance that can be used in the arrus library. + * + * Currently, it is a simple wrapper over boost::severity_logger_mt. + * + * This class should not be available publicly. + */ +class Logger { +public: + using Handle = std::unique_ptr; + using SharedHandle = std::shared_ptr; + + using Attribute = std::pair; + + /** + * Logs a given string message with given severity level. + * + * @param severity severity attached to the message + * @param msg message to log + */ + virtual void log(const LogSeverity severity, const std::string &msg) = 0; + + /** + * Sets logger attribute with given value. + * + * This function can be used e.g. to set device id of the device logger. + * + * @param key attribute's name + * @param value value to set + */ + virtual void + setAttribute(const std::string &key, const std::string &value) = 0; + + virtual ~Logger() = default; +}; + +} + +#endif //ARRUS_CORE_API_COMMON_LOGGER_H diff --git a/arrus/core/api/common/LoggerFactory.h b/arrus/core/api/common/LoggerFactory.h new file mode 100644 index 000000000..955b3c62c --- /dev/null +++ b/arrus/core/api/common/LoggerFactory.h @@ -0,0 +1,21 @@ +#ifndef ARRUS_CORE_API_COMMON_LOGGERFACTORY_H +#define ARRUS_CORE_API_COMMON_LOGGERFACTORY_H + +#include +#include + +#include "Logger.h" + +namespace arrus { + +class LoggerFactory { +public: + virtual Logger::Handle getLogger() = 0; + + virtual Logger::Handle + getLogger(const std::vector &attributes) = 0; +}; + +} + +#endif //ARRUS_CORE_API_COMMON_LOGGERFACTORY_H diff --git a/arrus/core/api/common/Tuple.h b/arrus/core/api/common/Tuple.h new file mode 100644 index 000000000..d2c9c93ac --- /dev/null +++ b/arrus/core/api/common/Tuple.h @@ -0,0 +1,68 @@ +#ifndef ARRUS_CORE_API_COMMON_TUPLE_H +#define ARRUS_CORE_API_COMMON_TUPLE_H + +#include +#include +#include + +namespace arrus { + +/** + * A tuple of values. + */ +template +class Tuple { +public: + Tuple(const std::initializer_list &values) : values(values) {} + + explicit Tuple(const std::vector &values) : values(values) {} + + const T &operator[](size_t i) const { + return values[i]; + } + + const T &get(size_t i) const { + return this[i]; + } + + size_t size() const { + return values.size(); + } + + const std::vector &getValues() const { + return values; + } + + size_t product() const { + return std::reduce( + std::begin(values), std::end(values), size_t(1), + [](auto v1, auto v2) -> size_t { + return v1 * v2; + } + ); + } + + size_t sum() const { + return std::reduce( + std::begin(values), std::end(values), size_t(0), + [](auto v1, auto v2) -> size_t { + return v1 + v2; + } + ); + } + + bool operator==(const Tuple &rhs) const { + return values == rhs.values; + } + + bool operator!=(const Tuple &rhs) const { + return !(rhs == *this); + } + +private: + std::vector values; +}; + +} + +#endif //ARRUS_CORE_API_COMMON_TUPLE_H diff --git a/arrus/core/api/common/exceptions.h b/arrus/core/api/common/exceptions.h new file mode 100644 index 000000000..0383ccad5 --- /dev/null +++ b/arrus/core/api/common/exceptions.h @@ -0,0 +1,38 @@ +#ifndef ARRUS_CORE_COMMON_EXCEPTIONS_H +#define ARRUS_CORE_COMMON_EXCEPTIONS_H + +#include +#include "arrus/core/api/devices/DeviceId.h" + +namespace arrus { + +class ArrusException : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class IllegalArgumentException : public ArrusException { +public: + using ArrusException::ArrusException; +}; + +class DeviceNotFoundException : public IllegalArgumentException { +public: + explicit DeviceNotFoundException(const arrus::devices::DeviceId &id) + : IllegalArgumentException("Device " + id.toString() + " not found.") {} +}; + +class IllegalStateException : public ArrusException { +public: + using ArrusException::ArrusException; +}; + +class TimeoutException : public ArrusException { +public: + using ArrusException::ArrusException; +}; + + +} + +#endif //ARRUS_CORE_COMMON_EXCEPTIONS_H diff --git a/arrus/core/api/common/logging.h b/arrus/core/api/common/logging.h new file mode 100644 index 000000000..9ed5d9c1f --- /dev/null +++ b/arrus/core/api/common/logging.h @@ -0,0 +1,23 @@ +#ifndef ARRUS_CORE_API_COMMON_LOGGING_H +#define ARRUS_CORE_API_COMMON_LOGGING_H + +#include + +#include "arrus/core/api/common/macros.h" +#include "arrus/core/api/common/LoggerFactory.h" + +namespace arrus { + /** + * Sets a logger factory in arrus package. + * + * The provided logger factory will be used to generate + * default and component specific loggers. The logger factory + * should be available through the life-time of the application. + * + * @param factory logger factory to set + */ + ARRUS_CPP_EXPORT + void setLoggerFactory(const std::shared_ptr& factory); +} + +#endif //ARRUS_CORE_COMMON_LOGGING_H diff --git a/arrus/core/api/common/macros.h b/arrus/core/api/common/macros.h new file mode 100644 index 000000000..40fcad4b0 --- /dev/null +++ b/arrus/core/api/common/macros.h @@ -0,0 +1,22 @@ +#ifndef ARRUS_CORE_API_COMMON_MACROS_H +#define ARRUS_CORE_API_COMMON_MACROS_H + +// Platform agnostic __declspec handling. +// Use ARRUS_CPP_EXPORT to mark specific structures/functions/etc. +// that should be accessible by the dll user. +// For systems other than windows, ARRUS_CPP is just an empty +// macro. +#define ARRUS_PATH_KEY "ARRUS_PATH" +#if defined(_WIN32) && !defined(ARRUS_CORE_UNIT_TESTS) + +#if defined(ARRUS_CPP_API_BUILD_STAGE) +#define ARRUS_CPP_EXPORT __declspec(dllexport) +#else +#define ARRUS_CPP_EXPORT __declspec(dllimport) +#endif + +#else +#define ARRUS_CPP_EXPORT +#endif + +#endif //ARRUS_CORE_API_COMMON_MACROS_H diff --git a/arrus/core/api/common/types.h b/arrus/core/api/common/types.h new file mode 100644 index 000000000..32cbf0405 --- /dev/null +++ b/arrus/core/api/common/types.h @@ -0,0 +1,26 @@ +#ifndef ARRUS_CORE_TYPES_H +#define ARRUS_CORE_TYPES_H + +#include +#include + +namespace arrus { +// Data types +using uint8 = uint8_t; +using uint16 = uint16_t; +using uint32 = int32_t; +using int8 = int8_t; +using int16 = int16_t; +using int32 = int32_t; + +using float32 = float; +using float64 = double; + +using ChannelIdx = uint16; +typedef std::vector BitMask; +using Voltage = uint8; + +template using PtrHandle = T *; +} + +#endif //ARRUS_CORE_TYPES_H diff --git a/arrus/core/api/devices.h b/arrus/core/api/devices.h new file mode 100644 index 000000000..2d4734f96 --- /dev/null +++ b/arrus/core/api/devices.h @@ -0,0 +1,5 @@ +#ifndef ARRUS_CORE_API_DEVICES_H +#define ARRUS_CORE_API_DEVICES_H + + +#endif //ARRUS_CORE_API_DEVICES_H diff --git a/arrus/core/api/devices/Device.h b/arrus/core/api/devices/Device.h new file mode 100644 index 000000000..760963576 --- /dev/null +++ b/arrus/core/api/devices/Device.h @@ -0,0 +1,32 @@ +#ifndef ARRUS_CORE_API_DEVICES_DEVICE_H +#define ARRUS_CORE_API_DEVICES_DEVICE_H + +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/devices/DeviceId.h" + +#include + +namespace arrus::devices { +class Device { +public: + using Handle = std::unique_ptr; + using RawHandle = PtrHandle; + + DeviceId getDeviceId() const { + return id; + } + + virtual ~Device() = default; + +protected: + explicit Device(const DeviceId &id): id(id) {} + + DeviceId id; +}; + +} + + + + +#endif //ARRUS_CORE_API_DEVICES_DEVICE_H diff --git a/arrus/core/api/devices/DeviceId.h b/arrus/core/api/devices/DeviceId.h new file mode 100644 index 000000000..4cf1d3dce --- /dev/null +++ b/arrus/core/api/devices/DeviceId.h @@ -0,0 +1,92 @@ +#ifndef ARRUS_CORE_API_DEVICES_DEVICEID_H +#define ARRUS_CORE_API_DEVICES_DEVICEID_H + +#include +#include + +#include "arrus/core/api/common/macros.h" + +namespace arrus::devices { + +/** + * Device types available in the system. + */ +enum class DeviceType { + Us4R, + Us4OEM, + ProbeAdapter, + Probe, + GPU, + CPU, + HV +}; + +/** + * Converts string to DeviceType. + * + * @param deviceTypeStr string representation of device type enum. + * @return device type enum + */ +ARRUS_CPP_EXPORT +DeviceType parseToDeviceTypeEnum(const std::string &deviceTypeStr); + +/** + * Converts DeviceType to string. + * + * @param deviceType device type enum to convert + * @return string representation of device type + */ +ARRUS_CPP_EXPORT +std::string toString(DeviceType deviceTypeEnum); + +/** + * Device ordinal number, e.g. GPU 0, GPU 1, Us4OEM 0, Us4OEM 1 etc. + */ +using Ordinal = unsigned short; + +/** + * Device identifier. + */ +class DeviceId { +public: + DeviceId(const DeviceType dt, + const Ordinal ordinal) + : deviceType(dt), ordinal(ordinal) {} + + DeviceType getDeviceType() const { + return deviceType; + } + + Ordinal getOrdinal() const { + return ordinal; + } + + bool operator==(const DeviceId &rhs) const { + return deviceType == rhs.deviceType + && ordinal == rhs.ordinal; + } + + bool operator!=(const DeviceId &rhs) const { + return !(rhs == *this); + } + + ARRUS_CPP_EXPORT + friend std::ostream &operator<<(std::ostream &os, const DeviceId &id); + + std::string toString() const { + std::ostringstream ss; + ss << *this; + return ss.str(); + } + + ARRUS_CPP_EXPORT + static DeviceId parse(const std::string &deviceId); + +private: + DeviceType deviceType; + Ordinal ordinal; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_DEVICEID_H diff --git a/arrus/core/api/devices/DeviceWithComponents.h b/arrus/core/api/devices/DeviceWithComponents.h new file mode 100644 index 000000000..0dbff52db --- /dev/null +++ b/arrus/core/api/devices/DeviceWithComponents.h @@ -0,0 +1,25 @@ +#ifndef ARRUS_CORE_API_DEVICES_DEVICEWITHCOMPONENTS_H +#define ARRUS_CORE_API_DEVICES_DEVICEWITHCOMPONENTS_H + +#include + +#include "arrus/core/api/devices/Device.h" + +namespace arrus::devices { + +class DeviceWithComponents : public Device { +public: + using Device::Device; + + /** + * Returns a raw handle to the component of this device. + * + * @param path path to the component + * @return a handle to the component + */ + virtual Device::RawHandle getDevice(const std::string& path) = 0; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_DEVICEWITHCOMPONENTS_H diff --git a/arrus/core/api/devices/TriggerGenerator.h b/arrus/core/api/devices/TriggerGenerator.h new file mode 100644 index 000000000..fa4baadc5 --- /dev/null +++ b/arrus/core/api/devices/TriggerGenerator.h @@ -0,0 +1,17 @@ +#ifndef ARRUS_CORE_API_DEVICES_TRIGGERGENERATOR_H +#define ARRUS_CORE_API_DEVICES_TRIGGERGENERATOR_H + +namespace arrus::devices { + +/** + * A device that has ability to generate triggers. + */ +class TriggerGenerator { +public: + virtual void startTrigger() = 0; + virtual void stopTrigger() = 0; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_TRIGGERGENERATOR_H diff --git a/arrus/core/api/devices/probe/Probe.h b/arrus/core/api/devices/probe/Probe.h new file mode 100644 index 000000000..0bb257db2 --- /dev/null +++ b/arrus/core/api/devices/probe/Probe.h @@ -0,0 +1,31 @@ +#ifndef ARRUS_CORE_API_DEVICES_PROBE_PROBE_H +#define ARRUS_CORE_API_DEVICES_PROBE_PROBE_H + +#include "../Device.h" +#include "arrus/core/api/devices/probe/ProbeModel.h" + +namespace arrus::devices { + +class Probe : public Device { +public: + using Handle = std::unique_ptr; + using RawHandle = Probe*; + + virtual ~Probe() = default; + + virtual const arrus::devices::ProbeModel &getModel() const = 0; + + Probe(Probe const&) = delete; + Probe(Probe const&&) = delete; + void operator=(Probe const&) = delete; + void operator=(Probe const&&) = delete; +protected: + explicit Probe(const DeviceId &id): Device(id) {} + +}; + +} + + + +#endif //ARRUS_CORE_API_DEVICES_PROBE_PROBE_H diff --git a/arrus/core/api/devices/probe/ProbeModel.h b/arrus/core/api/devices/probe/ProbeModel.h new file mode 100644 index 000000000..8498e0ed5 --- /dev/null +++ b/arrus/core/api/devices/probe/ProbeModel.h @@ -0,0 +1,75 @@ +#ifndef ARRUS_CORE_API_DEVICES_PROBE_PROBEMODEL_H +#define ARRUS_CORE_API_DEVICES_PROBE_PROBEMODEL_H + +#include +#include + +#include "arrus/core/api/common/Tuple.h" +#include "arrus/core/api/common/Interval.h" +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/common/exceptions.h" +#include "arrus/core/api/devices/probe/ProbeModelId.h" + +namespace arrus::devices { + +/** + * A specification of the probe model. + */ +class ProbeModel { +public: + + using ElementIdxType = ChannelIdx; + + ProbeModel(ProbeModelId modelId, + const Tuple &numberOfElements, + const Tuple &pitch, + // Float, because carrier frequency can be set only to specific values + const Interval &txFrequencyRange, + const Interval &voltageRange, + const double curvatureRadius) + : modelId(std::move(modelId)), numberOfElements(numberOfElements), + pitch(pitch), txFrequencyRange(txFrequencyRange), voltageRange(voltageRange), + curvatureRadius(curvatureRadius) { + + if(numberOfElements.size() != pitch.size()) { + throw IllegalArgumentException( + "Number of elements and pitch should have the same size."); + } + } + + const ProbeModelId &getModelId() const { + return modelId; + } + + const Tuple &getNumberOfElements() const { + return numberOfElements; + } + + const Tuple &getPitch() const { + return pitch; + } + + const Interval &getTxFrequencyRange() const { + return txFrequencyRange; + } + + const Interval &getVoltageRange() const { + return voltageRange; + } + + double getCurvatureRadius() const { + return curvatureRadius; + } + +private: + ProbeModelId modelId; + Tuple numberOfElements; + Tuple pitch; + Interval txFrequencyRange; + Interval voltageRange; + double curvatureRadius; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_PROBE_PROBEMODEL_H diff --git a/arrus/core/api/devices/probe/ProbeModelId.h b/arrus/core/api/devices/probe/ProbeModelId.h new file mode 100644 index 000000000..b28969b9f --- /dev/null +++ b/arrus/core/api/devices/probe/ProbeModelId.h @@ -0,0 +1,45 @@ +#ifndef ARRUS_CORE_API_DEVICES_PROBE_PROBEMODELID_H +#define ARRUS_CORE_API_DEVICES_PROBE_PROBEMODELID_H + +#include +#include +#include +#include + +namespace arrus::devices { + +class ProbeModelId { +public: + explicit ProbeModelId(std::string manufacturer, std::string name) + : manufacturer(std::move(manufacturer)), name(std::move(name)) {} + + const std::string &getName() const { + return name; + } + + const std::string &getManufacturer() const { + return manufacturer; + } + + friend std::ostream &operator<<(std::ostream &os, const ProbeModelId &id) { + os << "ProbeModel(" + << "manufacturer: " << id.manufacturer + << " name: " << id.name + << ")"; + return os; + } + + std::string toString() const { + std::stringstream sstr; + sstr << *this; + return sstr.str(); + } + +private: + std::string manufacturer; + std::string name; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_PROBE_PROBEMODELID_H diff --git a/arrus/core/api/devices/probe/ProbeSettings.h b/arrus/core/api/devices/probe/ProbeSettings.h new file mode 100644 index 000000000..f8f302bb7 --- /dev/null +++ b/arrus/core/api/devices/probe/ProbeSettings.h @@ -0,0 +1,39 @@ +#ifndef ARRUS_CORE_API_DEVICES_PROBE_PROBESETTINGS_H +#define ARRUS_CORE_API_DEVICES_PROBE_PROBESETTINGS_H + +#include +#include + +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/devices/probe/ProbeModel.h" + +namespace arrus::devices { +class ProbeSettings { +public: + /** + * + * @param model + * @param channelMapping flattened channel mappings. For 2-D array channel + * mapping is row major order. + */ + ProbeSettings(ProbeModel model, + std::vector channelMapping) + : model(std::move(model)), + channelMapping(std::move(channelMapping)) {} + + const std::vector &getChannelMapping() const { + return channelMapping; + } + + const ProbeModel &getModel() const { + return model; + } + +private: + ProbeModel model; + /** A probe channel mapping to the underlying device. */ + std::vector channelMapping; +}; +} + +#endif //ARRUS_CORE_API_DEVICES_PROBE_PROBESETTINGS_H diff --git a/arrus/core/api/devices/us4r/FrameChannelMapping.h b/arrus/core/api/devices/us4r/FrameChannelMapping.h new file mode 100644 index 000000000..582eb783b --- /dev/null +++ b/arrus/core/api/devices/us4r/FrameChannelMapping.h @@ -0,0 +1,39 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_FRAMECHANNELMAPPING_H +#define ARRUS_CORE_API_DEVICES_US4R_FRAMECHANNELMAPPING_H + +#include + +#include "arrus/core/api/common/types.h" + +namespace arrus::devices { + +class FrameChannelMapping { +public: + using Handle = std::unique_ptr; + using SharedHandle = std::shared_ptr; + using FrameNumber = uint16; + constexpr static int8 UNAVAILABLE = -1; + /** + * Returns physical frame number and channel number for a given, + * logical, frame number and a **rx aperture** channel. + * + * @param frame logical frame number + * @param channel logical channel number + * @return actual frame number and channel number + */ + // TODO use FrameNumber typedef (simplified current implementation for swig) + virtual std::pair getLogical(FrameNumber frame, ChannelIdx channel) = 0; + + virtual FrameNumber getNumberOfLogicalFrames() = 0; + virtual ChannelIdx getNumberOfLogicalChannels() = 0; + + static bool isChannelUnavailable(int8 channelNumber) { + return channelNumber == UNAVAILABLE; + } + + virtual ~FrameChannelMapping() = default; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_FRAMECHANNELMAPPING_H diff --git a/arrus/core/api/devices/us4r/HVModelId.h b/arrus/core/api/devices/us4r/HVModelId.h new file mode 100644 index 000000000..ec0a45659 --- /dev/null +++ b/arrus/core/api/devices/us4r/HVModelId.h @@ -0,0 +1,47 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_HVMODELID_H +#define ARRUS_CORE_API_DEVICES_US4R_HVMODELID_H + +#include +#include +#include + +namespace arrus::devices { + +class HVModelId { + +public: + HVModelId(std::string manufacturer, std::string name) + : manufacturer(std::move(manufacturer)), + name(std::move(name)) {} + + const std::string &getManufacturer() const { + return manufacturer; + } + + const std::string &getName() const { + return name; + } + + friend std::ostream & + operator<<(std::ostream &os, const HVModelId &id) { + os << "HVModelId(" + << "manufacturer: " << id.manufacturer << " name: " << id.name + << ")"; + return os; + } + + std::string toString() const { + std::stringstream sstr; + sstr << *this; + return sstr.str(); + } + +private: + std::string manufacturer; + std::string name; + +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_HVMODELID_H diff --git a/arrus/core/api/devices/us4r/HVSettings.h b/arrus/core/api/devices/us4r/HVSettings.h new file mode 100644 index 000000000..85da27e56 --- /dev/null +++ b/arrus/core/api/devices/us4r/HVSettings.h @@ -0,0 +1,25 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_HVSETTINGS_H +#define ARRUS_CORE_API_DEVICES_US4R_HVSETTINGS_H + +#include + +#include "arrus/core/api/devices/us4r/HVModelId.h" + +namespace arrus::devices { + +class HVSettings { +public: + explicit HVSettings(HVModelId modelId) + : modelId(std::move(modelId)) {} + + const HVModelId &getModelId() const { + return modelId; + } + +private: + HVModelId modelId; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_HVSETTINGS_H diff --git a/arrus/core/api/devices/us4r/HostBuffer.h b/arrus/core/api/devices/us4r/HostBuffer.h new file mode 100644 index 000000000..8ca857ff8 --- /dev/null +++ b/arrus/core/api/devices/us4r/HostBuffer.h @@ -0,0 +1,47 @@ +#ifndef ARRUS_CORE_API_OPS_US4R_HOSTBUFFER_H +#define ARRUS_CORE_API_OPS_US4R_HOSTBUFFER_H + +#include "arrus/core/api/common/types.h" + +namespace arrus::devices { + +class HostBuffer { +public: + using Handle = std::unique_ptr; + using SharedHandle = std::shared_ptr; + static constexpr long long INF_TIMEOUT = -1; + + virtual ~HostBuffer() = default; + + /** + * @param timeout -1 means infinity timeout + * @return + */ + virtual int16* tail(long long timeout) = 0; + + virtual int16* head(long long timeout) = 0; + + virtual size_t tailAddress(long long timeout) { + return (size_t)tail(timeout); + } + + virtual size_t headAddress(long long timeout) { + return (size_t)head(timeout); + } + + virtual void releaseTail(long long timeout) = 0; + + virtual unsigned short getNumberOfElements() const = 0; + + virtual size_t getElementSize() const = 0; + + virtual int16* getElement(size_t i) = 0; + + virtual size_t getElementAddress(size_t i) { + return (size_t)getElement(i); + } +}; + +} + +#endif //ARRUS_CORE_API_OPS_US4R_HOSTBUFFER_H diff --git a/arrus/core/api/devices/us4r/ProbeAdapter.h b/arrus/core/api/devices/us4r/ProbeAdapter.h new file mode 100644 index 000000000..5b9405a8f --- /dev/null +++ b/arrus/core/api/devices/us4r/ProbeAdapter.h @@ -0,0 +1,31 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTER_H +#define ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTER_H + +#include +#include "arrus/core/api/devices/Device.h" + +namespace arrus::devices { + +class ProbeAdapter : public Device { +public: + using Handle = std::unique_ptr; + using RawHandle = PtrHandle; + + ~ProbeAdapter() override = default; + + ProbeAdapter(ProbeAdapter const&) = delete; + ProbeAdapter(ProbeAdapter const&&) = delete; + void operator=(ProbeAdapter const&) = delete; + void operator=(ProbeAdapter const&&) = delete; + + [[nodiscard]] virtual ChannelIdx getNumberOfChannels() const = 0; + + virtual void setTgcCurve(const std::vector &curve) = 0; + +protected: + explicit ProbeAdapter(const DeviceId &id): Device(id) {} +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTER_H diff --git a/arrus/core/api/devices/us4r/ProbeAdapterModelId.h b/arrus/core/api/devices/us4r/ProbeAdapterModelId.h new file mode 100644 index 000000000..71ad4294d --- /dev/null +++ b/arrus/core/api/devices/us4r/ProbeAdapterModelId.h @@ -0,0 +1,44 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTERMODELID_H +#define ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTERMODELID_H + +#include +#include +#include + +namespace arrus::devices { + +class ProbeAdapterModelId { +public: + explicit ProbeAdapterModelId(std::string manufacturer, std::string name) + : manufacturer(std::move(manufacturer)), name(std::move(name)) {} + + const std::string &getName() const { + return name; + } + + const std::string &getManufacturer() const { + return manufacturer; + } + + friend std::ostream & + operator<<(std::ostream &os, const ProbeAdapterModelId &id) { + os << "ProbeAdapterModelId(" + << "manufacturer: " << id.manufacturer << " name: " << id.name + << ")"; + return os; + } + + std::string toString() const { + std::stringstream sstr; + sstr << *this; + return sstr.str(); + } + +private: + std::string manufacturer; + std::string name; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTERMODELID_H diff --git a/arrus/core/api/devices/us4r/ProbeAdapterSettings.h b/arrus/core/api/devices/us4r/ProbeAdapterSettings.h new file mode 100644 index 000000000..66f382824 --- /dev/null +++ b/arrus/core/api/devices/us4r/ProbeAdapterSettings.h @@ -0,0 +1,45 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTERSETTINGS_H +#define ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTERSETTINGS_H + +#include +#include +#include + +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterModelId.h" + +namespace arrus::devices { +class ProbeAdapterSettings { +public: + using Us4OEMOrdinal = Ordinal; + using ChannelAddress = std::pair; + using ChannelMapping = std::vector; + + ProbeAdapterSettings(ProbeAdapterModelId modelId, + ChannelIdx numberOfChannels, ChannelMapping mapping) + : modelId(std::move(modelId)), nChannels(numberOfChannels), + mapping(std::move(mapping)) {} + + const ProbeAdapterModelId &getModelId() const { + return modelId; + } + + ChannelIdx getNumberOfChannels() const { + return nChannels; + } + + const ChannelMapping &getChannelMapping() const { + return mapping; + } + + + +private: + ProbeAdapterModelId modelId; + ChannelIdx nChannels; + ChannelMapping mapping; +}; +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_PROBEADAPTERSETTINGS_H diff --git a/arrus/core/api/devices/us4r/RxSettings.h b/arrus/core/api/devices/us4r/RxSettings.h new file mode 100644 index 000000000..e1af2143c --- /dev/null +++ b/arrus/core/api/devices/us4r/RxSettings.h @@ -0,0 +1,64 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_RXSETTINGS_H +#define ARRUS_CORE_API_DEVICES_US4R_RXSETTINGS_H + +#include +#include +#include + +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/ops/us4r/tgc.h" + +namespace arrus::devices { + +class RxSettings { +public: + using TGCSample = arrus::ops::us4r::TGCSampleValue; + using TGCCurve = arrus::ops::us4r::TGCCurve; + + RxSettings( + const std::optional &dtgcAttenuation, uint16 pgaGain, + uint16 lnaGain, TGCCurve tgcSamples, uint32 lpfCutoff, + const std::optional &activeTermination) + : dtgcAttenuation(dtgcAttenuation), pgaGain(pgaGain), + lnaGain(lnaGain), tgcSamples(std::move(tgcSamples)), + lpfCutoff(lpfCutoff), activeTermination(activeTermination) {} + + const std::optional &getDtgcAttenuation() const { + return dtgcAttenuation; + } + + uint16 getPgaGain() const { + return pgaGain; + } + + uint16 getLnaGain() const { + return lnaGain; + } + + const TGCCurve &getTgcSamples() const { + return tgcSamples; + } + + uint32 getLpfCutoff() const { + return lpfCutoff; + } + + const std::optional &getActiveTermination() const { + return activeTermination; + } + + + +private: + std::optional dtgcAttenuation; + uint16 pgaGain; + uint16 lnaGain; + + TGCCurve tgcSamples; + uint32 lpfCutoff; + std::optional activeTermination; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_RXSETTINGS_H diff --git a/arrus/core/api/devices/us4r/Us4OEM.h b/arrus/core/api/devices/us4r/Us4OEM.h new file mode 100644 index 000000000..66df7a319 --- /dev/null +++ b/arrus/core/api/devices/us4r/Us4OEM.h @@ -0,0 +1,31 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_US4OEM_H +#define ARRUS_CORE_API_DEVICES_US4R_US4OEM_H + +#include +#include "arrus/core/api/devices/Device.h" +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/devices/TriggerGenerator.h" + +namespace arrus::devices { + +class Us4OEM : public Device, public TriggerGenerator { +public: + using Handle = std::unique_ptr; + using RawHandle = PtrHandle; + + ~Us4OEM() override = default; + + virtual double getSamplingFrequency() = 0; + + Us4OEM(Us4OEM const&) = delete; + Us4OEM(Us4OEM const&&) = delete; + void operator=(Us4OEM const&) = delete; + void operator=(Us4OEM const&&) = delete; +protected: + explicit Us4OEM(const DeviceId &id): Device(id) {} +}; + + +} + +#endif // ARRUS_CORE_API_DEVICES_US4R_US4OEM_H diff --git a/arrus/core/api/devices/us4r/Us4OEMSettings.h b/arrus/core/api/devices/us4r/Us4OEMSettings.h new file mode 100644 index 000000000..bc35496f1 --- /dev/null +++ b/arrus/core/api/devices/us4r/Us4OEMSettings.h @@ -0,0 +1,79 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_US4OEMSETTINGS_H +#define ARRUS_CORE_API_DEVICES_US4R_US4OEMSETTINGS_H + +#include +#include +#include +#include +#include +#include + +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/devices/us4r/RxSettings.h" + +namespace arrus::devices { + +/** + * Us4OEM settings. + * + * Contains all raw parameters used to configure module. + */ +class Us4OEMSettings { +public: + using ChannelMapping = std::vector; + + /** + * Us4OEM Settings constructor. + * + * @param activeChannelGroups determines which groups of channels should be + * 'active'. When the 'channel is active', Us4OEM can transmit/receive + * a signal through this channel. + * If the size of the group is equal `n`, and the number of module's + * channels is `m`, `activeChannelGroups[0]` turns on/off channels + * `0,1,..,(n-1)`, `activeChannelGroups[1]` turns on/off channels + * `n,(n+1),..,(2n-1)`, and so on. The value `m' is always divisible + * by `n`. The array `activeChannelGroups` should have exactly + * `m/n` elements. + * @param channelMapping channel permutation to apply on a given Us4OEM. + * channelMapping[i] = j, where `i` is the virtual(logical) channel number, + * `j` is the physical channel number. + * @param rxSettings initial rx settings to apply + * @param channelMask channels that should be always turned off, + * CHANNEL NUMBERS STARTS FROM 0 + */ + Us4OEMSettings(ChannelMapping channelMapping, + BitMask activeChannelGroups, + RxSettings rxSettings, + std::unordered_set channelsMask) + : channelMapping(std::move(channelMapping)), + activeChannelGroups(std::move(activeChannelGroups)), + rxSettings(std::move(rxSettings)), + channelsMask(std::move(channelsMask)){} + + + const std::vector &getChannelMapping() const { + return channelMapping; + } + + const BitMask &getActiveChannelGroups() const { + return activeChannelGroups; + } + + const RxSettings &getRxSettings() const { + return rxSettings; + } + + const std::unordered_set &getChannelsMask() const { + return channelsMask; + } + +private: + std::vector channelMapping; + BitMask activeChannelGroups; + RxSettings rxSettings; + std::unordered_set channelsMask; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_US4OEMSETTINGS_H diff --git a/arrus/core/api/devices/us4r/Us4R.h b/arrus/core/api/devices/us4r/Us4R.h new file mode 100644 index 000000000..821741db8 --- /dev/null +++ b/arrus/core/api/devices/us4r/Us4R.h @@ -0,0 +1,90 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4R_H +#define ARRUS_CORE_DEVICES_US4R_US4R_H + +#include + +#include "arrus/core/api/devices/Device.h" +#include "arrus/core/api/devices/DeviceWithComponents.h" +#include "arrus/core/api/devices/us4r/Us4OEM.h" +#include "arrus/core/api/devices/us4r/ProbeAdapter.h" +#include "arrus/core/api/devices/probe/Probe.h" +#include "arrus/core/api/ops/us4r/TxRxSequence.h" +#include "FrameChannelMapping.h" +#include "HostBuffer.h" + + +namespace arrus::devices { + +/** + * Us4R system: a group of Us4OEM modules and related components. + */ +class Us4R : public DeviceWithComponents { +public: + using Handle = std::unique_ptr; + static constexpr long long INF_TIMEOUT = -1; + + explicit Us4R(const DeviceId &id): DeviceWithComponents(id) {} + + ~Us4R() override = default; + + /** + * Returns a handle to Us4OEM identified by given ordinal number. + * + * @param ordinal ordinal number of the us4oem to get + * @return a handle to the us4oem module + */ + virtual Us4OEM::RawHandle getUs4OEM(Ordinal ordinal) = 0; + + /** + * Returns a handle to an adapter identified by given ordinal number. + * + * @param ordinal ordinal number of the adapter to get + * @return a handle to the adapter device + */ + virtual ProbeAdapter::RawHandle getProbeAdapter(Ordinal ordinal) = 0; + + /** + * Returns a handle to a probe identified by given ordinal number. + * + * @param ordinal ordinal number of the probe to get + * @return a handle to the probe + */ + virtual arrus::devices::Probe* getProbe(Ordinal ordinal) = 0; + + /** + * Uploads a given + * + * @param seq + * @param rxBufferSize + * @param hostBufferSize + * @return + */ + virtual std::pair< + std::shared_ptr, + std::shared_ptr + > + upload(const ::arrus::ops::us4r::TxRxSequence &seq, unsigned short rxBufferSize, unsigned short hostBufferSize) = 0; + + virtual void setVoltage(Voltage voltage) = 0; + + virtual void disableHV() = 0; + + /** + * Sets tgc curve points asynchronously. + * + * @param tgcCurvePoints tgc curve points to set. + */ + virtual void setTgcCurve(const std::vector& tgcCurvePoints) = 0; + + virtual void start() = 0; + virtual void stop() = 0; + + Us4R(Us4R const&) = delete; + Us4R(Us4R const&&) = delete; + void operator=(Us4R const&) = delete; + void operator=(Us4R const&&) = delete; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4R_H diff --git a/arrus/core/api/devices/us4r/Us4RSettings.h b/arrus/core/api/devices/us4r/Us4RSettings.h new file mode 100644 index 000000000..36cbce05e --- /dev/null +++ b/arrus/core/api/devices/us4r/Us4RSettings.h @@ -0,0 +1,95 @@ +#ifndef ARRUS_CORE_API_DEVICES_US4R_US4RSETTINGS_H +#define ARRUS_CORE_API_DEVICES_US4R_US4RSETTINGS_H + +#include +#include +#include + +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +#include "arrus/core/api/devices/us4r/RxSettings.h" +#include "arrus/core/api/devices/us4r/HVSettings.h" +#include "arrus/core/api/devices/probe/ProbeSettings.h" +#include "arrus/core/api/devices/DeviceId.h" + +namespace arrus::devices { + +class Us4RSettings { +public: + explicit Us4RSettings( + std::vector us4OemSettings, + std::optional hvSettings) + : us4oemSettings(std::move(us4OemSettings)), + hvSettings(std::move(hvSettings)) {} + + Us4RSettings( + ProbeAdapterSettings probeAdapterSettings, + ProbeSettings probeSettings, + RxSettings rxSettings, + std::optional hvSettings, + std::vector channelsMask, + std::vector> us4oemChannelsMask) + : probeAdapterSettings(std::move(probeAdapterSettings)), + probeSettings(std::move(probeSettings)), + rxSettings(std::move(rxSettings)), + hvSettings(std::move(hvSettings)), + channelsMask(std::move(channelsMask)), + us4oemChannelsMask(std::move(us4oemChannelsMask)){} + + const std::vector &getUs4OEMSettings() const { + return us4oemSettings; + } + + const std::optional & + getProbeAdapterSettings() const { + return probeAdapterSettings; + } + + const std::optional &getProbeSettings() const { + return probeSettings; + } + + const std::optional &getRxSettings() const { + return rxSettings; + } + + const std::optional &getHVSettings() const { + return hvSettings; + } + + const std::vector &getChannelsMask() const { + return channelsMask; + } + + const std::vector> &getUs4OEMChannelsMask() const { + return us4oemChannelsMask; + } + +private: + /* A list of settings for Us4OEMs. + * First element configures Us4OEM:0, second: Us4OEM:1, etc. */ + std::vector us4oemSettings; + /** Probe adapter settings. Optional - when not set, at least one + * Us4OEMSettings must be set. When is set, the list of Us4OEM + * settings should be empty. */ + std::optional probeAdapterSettings{}; + /** ProbeSettings to set. Optional - when is set, ProbeAdapterSettings also + * must be available.*/ + std::optional probeSettings{}; + /** Required when no Us4OEM settings are set. */ + std::optional rxSettings; + /** Optional (us4r devices may have externally controlled hv suppliers. */ + std::optional hvSettings; + /** A list of channels that should be turned off in the us4r system. + * Note that the **channel numbers start from 0**.*/ + std::vector channelsMask; + /** A list of channels masks to apply on given us4oems. + * Currently us4oem channels are used for double check only. + * The administrator has to provide us4oem channels masks that confirms to + * the system us4r channels, and this way we reduce the chance of mistake. */ + std::vector> us4oemChannelsMask; +}; + +} + +#endif //ARRUS_CORE_API_DEVICES_US4R_US4RSETTINGS_H diff --git a/arrus/core/api/examples.h b/arrus/core/api/examples.h new file mode 100644 index 000000000..720f19442 --- /dev/null +++ b/arrus/core/api/examples.h @@ -0,0 +1,57 @@ +#include "arrus/core/api/session/Session.h" +#include "arrus/core/api/io/settings.h" +#include "arrus/core/api/ops/us4r/TxRxSequence.h" +#include "arrus/core/api/framework.h" + + +int main() { + using arrus::ops::us4r::TxRxSequence; + using arrus::ops::us4r::Pulse; + using arrus::ops::us4r::Rx; + using arrus::ops::us4r::Tx; + + using arrus::DeviceId; + using arrus::DeviceType; + + using arrus::Variable; + + arrus::SessionSettings settings = arrus::io::readSessionSettings( + "test.prototxt"); + arrus::Session::Handle session = arrus::createSession(settings); + + auto us4rId = DeviceId(DeviceType::Us4R, 0); + auto gpuId = DeviceId(DeviceType::GPU, 0); + + auto seq = TxRxSequence( + us4rId, + {{Tx({true, false}, {0.0, 0.0}, + Pulse(10e6, 1.5, false)), + Rx({true, false}, {0, 2048}, 1)} + }, + 160e-6, + {1.0f, 2.0f}); + arrus::CircularQueue::Element rf = seq.getData(); +// arrus::CircularQueue::Element planeWaveAngles = seq.getMetadata("focus"); + + // Imaging pipeline. + arrus::Tensor iq = arrus::ops::downConversion(gpuId, rf); + arrus::Tensor rfImg = arrus::ops::reconstructRFImage(gpuId, iq); + arrus::Tensor envelope = arrus::ops::hilbert(gpuId, rfImg); + arrus::Tensor bmode = arrus::ops::obtainBModeImage(gpuId, envelope); + // TODO scan conversion + + // Dumping pipeline. + arrus::Op save = arrus::ops::io::SaveOp(rf, "/home/pjarosik/tmp/directory/"); + + // Start acquisition on the us4r. + session.run(seq.start()); + while(true) { + if(mode == "bmode") { + auto const &image = session.run(bmode); + // actually should return a list of output values of given operation/tensor evaluation + } + else { + session.run(save); // Ignoring empty results. + } + } +} diff --git a/arrus/core/api/framework.h b/arrus/core/api/framework.h new file mode 100644 index 000000000..031af8ffb --- /dev/null +++ b/arrus/core/api/framework.h @@ -0,0 +1,16 @@ +#ifndef ARRUS_CORE_API_FRAMEWORK_H +#define ARRUS_CORE_API_FRAMEWORK_H + +#include "arrus/core/api/framework/Op.h" +#include "arrus/core/api/framework/Tensor.h" +#include "arrus/core/api/framework/Constant.h" +#include "arrus/core/api/framework/Variable.h" + +namespace arrus { +using arrus::framework::Op; +using arrus::framework::Tensor; +using arrus::framework::Constant; +using arrus::framework::Variable; +} + +#endif //ARRUS_CORE_API_FRAMEWORK_H diff --git a/arrus/core/api/framework/Constant.h b/arrus/core/api/framework/Constant.h new file mode 100644 index 000000000..7ef75bb11 --- /dev/null +++ b/arrus/core/api/framework/Constant.h @@ -0,0 +1,12 @@ +#ifndef ARRUS_CORE_API_FRAMEWORK_CONSTANT_H +#define ARRUS_CORE_API_FRAMEWORK_CONSTANT_H + +namespace arrus::framework { + +class Constant { + +}; + +} + +#endif //ARRUS_CORE_API_FRAMEWORK_CONSTANT_H diff --git a/arrus/core/api/framework/FifoBuffer.h b/arrus/core/api/framework/FifoBuffer.h new file mode 100644 index 000000000..c1c8405db --- /dev/null +++ b/arrus/core/api/framework/FifoBuffer.h @@ -0,0 +1,19 @@ +#ifndef ARRUS_ARRUS_CORE_API_FRAMEWORK_FIFOBUFFER_H +#define ARRUS_ARRUS_CORE_API_FRAMEWORK_FIFOBUFFER_H + +namespace arrus::framework { + +class FifoBuffer { + virtual ~FifoBuffer() = default; + + + enum class Mode { + SYNC, + /** Async mode - lock-free from the producer's point of view. */ + ASYNC + }; +}; + +} + +#endif //ARRUS_ARRUS_CORE_API_FRAMEWORK_FIFOBUFFER_H diff --git a/arrus/core/api/framework/Op.h b/arrus/core/api/framework/Op.h new file mode 100644 index 000000000..66e27fd69 --- /dev/null +++ b/arrus/core/api/framework/Op.h @@ -0,0 +1,12 @@ +#ifndef ARRUS_CORE_API_FRAMEWORK_OP_H +#define ARRUS_CORE_API_FRAMEWORK_OP_H + +namespace arrus::framework { + +class Op { + +}; + +} + +#endif //ARRUS_CORE_API_FRAMEWORK_OP_H diff --git a/arrus/core/api/framework/Tensor.h b/arrus/core/api/framework/Tensor.h new file mode 100644 index 000000000..11f076feb --- /dev/null +++ b/arrus/core/api/framework/Tensor.h @@ -0,0 +1,12 @@ +#ifndef ARRUS_CORE_API_FRAMEWORK_TENSOR_H +#define ARRUS_CORE_API_FRAMEWORK_TENSOR_H + +namespace arrus::framework { + +class Tensor { + +}; + +} + +#endif //ARRUS_CORE_API_FRAMEWORK_TENSOR_H diff --git a/arrus/core/api/framework/Variable.h b/arrus/core/api/framework/Variable.h new file mode 100644 index 000000000..0a963e385 --- /dev/null +++ b/arrus/core/api/framework/Variable.h @@ -0,0 +1,12 @@ +#ifndef ARRUS_CORE_API_FRAMEWORK_VARIABLE_H +#define ARRUS_CORE_API_FRAMEWORK_VARIABLE_H + +namespace arrus::framework { + +class Variable { + +}; + +} + +#endif //ARRUS_CORE_API_FRAMEWORK_VARIABLE_H diff --git a/arrus/core/api/io/settings.h b/arrus/core/api/io/settings.h new file mode 100644 index 000000000..6ca752c89 --- /dev/null +++ b/arrus/core/api/io/settings.h @@ -0,0 +1,15 @@ +#ifndef ARRUS_CORE_API_IO_SETTINGS_H +#define ARRUS_CORE_API_IO_SETTINGS_H + +#include +#include "arrus/core/api/common/macros.h" +#include "arrus/core/api/session/SessionSettings.h" + +namespace arrus::io { + +ARRUS_CPP_EXPORT +arrus::session::SessionSettings readSessionSettings(const std::string &file); + +} + +#endif //ARRUS_CORE_API_IO_SETTINGS_H diff --git a/arrus/core/api/ops/us4r/Pulse.h b/arrus/core/api/ops/us4r/Pulse.h new file mode 100644 index 000000000..9fc34f4cd --- /dev/null +++ b/arrus/core/api/ops/us4r/Pulse.h @@ -0,0 +1,43 @@ +#ifndef ARRUS_CORE_API_OPS_US4R_PULSE_H +#define ARRUS_CORE_API_OPS_US4R_PULSE_H + +namespace arrus::ops::us4r { + +class Pulse { + +public: + Pulse(float centerFrequency, float nPeriods, bool inverse) : + centerFrequency(centerFrequency), nPeriods(nPeriods), + inverse(inverse) {} + + float getCenterFrequency() const { + return centerFrequency; + } + + float getNPeriods() const { + return nPeriods; + } + + bool isInverse() const { + return inverse; + } + + bool operator==(const Pulse &rhs) const { + return centerFrequency == rhs.centerFrequency + && nPeriods == rhs.nPeriods + && inverse == rhs.inverse; + } + + bool operator!=(const Pulse &rhs) const { + return !(rhs == *this); + } + +private: + float centerFrequency; + float nPeriods; + bool inverse; +}; + +} + +#endif //ARRUS_CORE_API_OPS_US4R_PULSE_H diff --git a/arrus/core/api/ops/us4r/Rx.h b/arrus/core/api/ops/us4r/Rx.h new file mode 100644 index 000000000..ffc16732e --- /dev/null +++ b/arrus/core/api/ops/us4r/Rx.h @@ -0,0 +1,57 @@ +#ifndef ARRUS_CORE_API_OPS_US4R_RX_H +#define ARRUS_CORE_API_OPS_US4R_RX_H + +#include + +#include "arrus/core/api/common/Interval.h" +#include "arrus/core/api/common/Tuple.h" +#include "arrus/core/api/common/types.h" + +namespace arrus::ops::us4r { + +/** + * An operation that performs a single data reception (Rx). + */ +class Rx { +public: + /** + * Rx constructor. + * + * @param aperture receive aperture to use; + * aperture[i] = true means that the i-th channel should be turned on + * @param rxSampleRange [start, end) range of samples to acquire, starts from 0 + * @param downsamplingFactor the factor by which the sampling frequency should be divided, an integer + */ + Rx(std::vector aperture, std::pair sampleRange, + unsigned int downsamplingFactor = 1, + std::pair padding = {(ChannelIdx)0, (ChannelIdx) 0}) + : aperture(std::move(aperture)), sampleRange(std::move(sampleRange)), + downsamplingFactor(downsamplingFactor), + padding(std::move(padding)) {} + + const std::vector &getAperture() const { + return aperture; + } + + const std::pair &getSampleRange() const { + return sampleRange; + } + + unsigned getDownsamplingFactor() const { + return downsamplingFactor; + } + + const std::pair &getPadding() const { + return padding; + } + +private: + std::vector aperture; + std::pair sampleRange; + unsigned downsamplingFactor; + std::pair padding; +}; + +} + +#endif //ARRUS_CORE_API_OPS_US4R_RX_H diff --git a/arrus/core/api/ops/us4r/Tx.h b/arrus/core/api/ops/us4r/Tx.h new file mode 100644 index 000000000..e435e1818 --- /dev/null +++ b/arrus/core/api/ops/us4r/Tx.h @@ -0,0 +1,42 @@ +#ifndef ARRUS_CORE_API_OPS_US4R_TX_H +#define ARRUS_CORE_API_OPS_US4R_TX_H + +#include + +#include "Pulse.h" +#include "arrus/core/api/common/types.h" + +namespace arrus::ops::us4r { + +/** + * A single pulse transmission. + */ +class Tx { +public: + Tx(std::vector aperture, std::vector delays, const Pulse &excitation) + : aperture(std::move(aperture)), + delays(std::move(delays)), + excitation(excitation) {} + + const std::vector &getAperture() const { + return aperture; + } + + const std::vector &getDelays() const { + return delays; + } + + const Pulse &getExcitation() const { + return excitation; + } + +private: + std::vector aperture; + std::vector delays; + Pulse excitation; +}; + + +} + +#endif //ARRUS_CORE_API_OPS_US4R_TX_H diff --git a/arrus/core/api/ops/us4r/TxRxSequence.h b/arrus/core/api/ops/us4r/TxRxSequence.h new file mode 100644 index 000000000..f7fb787ea --- /dev/null +++ b/arrus/core/api/ops/us4r/TxRxSequence.h @@ -0,0 +1,94 @@ +#ifndef ARRUS_CORE_API_OPS_US4R_TXRXSEQUENCE_H +#define ARRUS_CORE_API_OPS_US4R_TXRXSEQUENCE_H + +#include +#include + +#include "arrus/core/api/devices/Device.h" +#include "arrus/core/api/framework.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/api/ops/us4r/Tx.h" +#include "arrus/core/api/ops/us4r/Rx.h" + +namespace arrus::ops::us4r { + +class TxRx { +public: + // TODO(pjarosik) remove default constructor!!! Currently required by py swig wrapper + TxRx() + :tx(std::vector{}, std::vector{}, + Pulse(0, 0, false)), + rx(std::vector{}, + std::make_pair((unsigned int)0, (unsigned int)0)), + pri(0.0f) + {} + + TxRx(Tx tx, Rx rx, float pri) : tx(std::move(tx)), rx(std::move(rx)), pri(pri) {} + + const Tx &getTx() const { + return tx; + } + + const Rx &getRx() const { + return rx; + } + + float getPri() const { + return pri; + } + +private: + Tx tx; + Rx rx; + float pri; +}; + +class TxRxSequence { +public: + static constexpr float NO_SRI = -1; + /** + * Tx/Rx sequence to execute on Us4R device. + * + * @param sequence a list of tx/rxs that compose a given sequence + * @param tgcCurve tgc curve to apply + * @param sri frame repetition interval - the total time that a given sequence should take. Should be not smaller + */ + TxRxSequence(std::vector sequence, TGCCurve tgcCurve, float sri = NO_SRI) + : txrxs(std::move(sequence)), tgcCurve(std::move(tgcCurve)), sri(sri) {} + + /** + * Sequence of operations to perform. + */ + const std::vector &getOps() const { + return txrxs; + } + + /** + * Initial TGC curve points. + */ + const TGCCurve &getTgcCurve() const { + return tgcCurve; + } + + /** + * Returns frame repetition interval (the total time the given sequence should actually take). + * nullopt means that the frame acquistion time should be determined by total PRI only. + */ + const std::optional getSri() const { + if(sri.value() != NO_SRI) { + return sri; + } + else { + return std::optional(); + } + } + +private: + std::vector txrxs; + TGCCurve tgcCurve; + std::optional sri; +}; + +} + +#endif //ARRUS_CORE_API_OPS_US4R_TXRXSEQUENCE_H diff --git a/arrus/core/api/ops/us4r/tgc.h b/arrus/core/api/ops/us4r/tgc.h new file mode 100644 index 000000000..1650b43a8 --- /dev/null +++ b/arrus/core/api/ops/us4r/tgc.h @@ -0,0 +1,13 @@ +#ifndef ARRUS_CORE_API_OPS_US4R_TGC_H +#define ARRUS_CORE_API_OPS_US4R_TGC_H + +#include + +namespace arrus::ops::us4r { + +using TGCSampleValue = float; +using TGCCurve = std::vector; + +} + +#endif //ARRUS_CORE_API_OPS_US4R_TGC_H diff --git a/arrus/core/api/session/Session.h b/arrus/core/api/session/Session.h new file mode 100644 index 000000000..a9cc3a28a --- /dev/null +++ b/arrus/core/api/session/Session.h @@ -0,0 +1,56 @@ +#ifndef ARRUS_CORE_API_SESSION_SESSION_H +#define ARRUS_CORE_API_SESSION_SESSION_H + +#include "arrus/core/api/common/macros.h" +#include "arrus/core/api/devices/Device.h" +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/api/session/SessionSettings.h" + +namespace arrus::session { + +class Session { +public: + using Handle = std::unique_ptr; + + /** + * Returns a handle to device with given Id. + * + * @param deviceId device identifier + * @return a device handle + */ + virtual arrus::devices::Device * + getDevice(const std::string &deviceId) = 0; + + /** + * Returns a handle to device with given Id. + * + * @param deviceId device identifier + * @return a device handle + */ + virtual arrus::devices::Device * + getDevice(const arrus::devices::DeviceId &deviceId) = 0; + + virtual ~Session() = default; +}; + +/** +* Creates a new session with the provided configuration. +* +* @param sessionSettings session settings to set. +* @return a unique handle to session +*/ +ARRUS_CPP_EXPORT +Session::Handle createSession(const SessionSettings &sessionSettings); + +/** +* Reads given configuration file and returns a handle to new session. +* +* @param filepath a path to session settings +* @return a unique handle to session +*/ +ARRUS_CPP_EXPORT +Session::Handle createSession(const std::string& filepath); +} + + +#endif //ARRUS_CORE_API_SESSION_SESSION_H \ No newline at end of file diff --git a/arrus/core/api/session/SessionSettings.h b/arrus/core/api/session/SessionSettings.h new file mode 100644 index 000000000..9f92c7999 --- /dev/null +++ b/arrus/core/api/session/SessionSettings.h @@ -0,0 +1,24 @@ +#ifndef ARRUS_CORE_API_SESSION_SESSIONSETTINGS_H +#define ARRUS_CORE_API_SESSION_SESSIONSETTINGS_H + +#include +#include + +#include "arrus/core/api/devices/us4r/Us4RSettings.h" + +namespace arrus::session { +class SessionSettings { +public: + explicit SessionSettings(arrus::devices::Us4RSettings us4RSettings) : + us4RSettings(std::move(us4RSettings)) {} + + const arrus::devices::Us4RSettings &getUs4RSettings() const { + return us4RSettings; + } + +private: + arrus::devices::Us4RSettings us4RSettings; +}; +} + +#endif //ARRUS_CORE_API_SESSION_SESSIONSETTINGS_H diff --git a/core/api/DataAcquiredEvent.h b/arrus/core/callbacks/DataAcquiredEvent.h similarity index 100% rename from core/api/DataAcquiredEvent.h rename to arrus/core/callbacks/DataAcquiredEvent.h diff --git a/core/api/Event.h b/arrus/core/callbacks/Event.h similarity index 100% rename from core/api/Event.h rename to arrus/core/callbacks/Event.h diff --git a/core/api/EventCallback.h b/arrus/core/callbacks/EventCallback.h similarity index 100% rename from core/api/EventCallback.h rename to arrus/core/callbacks/EventCallback.h diff --git a/arrus/core/common/aperture.h b/arrus/core/common/aperture.h new file mode 100644 index 000000000..9abde39a1 --- /dev/null +++ b/arrus/core/common/aperture.h @@ -0,0 +1,19 @@ +#ifndef ARRUS_CORE_COMMON_APERTURE_H +#define ARRUS_CORE_COMMON_APERTURE_H + +#include +#include + +#include "arrus/core/api/common/types.h" + +namespace arrus { + +inline +ChannelIdx getNumberOfActiveChannels(const BitMask &aperture) { + return static_cast( + std::accumulate(std::begin(aperture), std::end(aperture),0)); +} + +} + +#endif //ARRUS_CORE_COMMON_APERTURE_H diff --git a/arrus/core/common/collections.h b/arrus/core/common/collections.h new file mode 100644 index 000000000..1a5a69f92 --- /dev/null +++ b/arrus/core/common/collections.h @@ -0,0 +1,185 @@ +#ifndef ARRUS_CORE_COMMON_COLLECTIONS_H +#define ARRUS_CORE_COMMON_COLLECTIONS_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace arrus { + +/** + * Returns an array of range [start, end). + */ +template +inline std::vector getRange(T start, T end, T step = 1) { + std::vector values; + for(T i = start; i < end; i += step) { + values.push_back(i); + } + return values; +} + +template +inline std::vector castTo(std::vector values) { + std::vector result(values.size()); + std::transform( + std::begin(values), std::end(values), + std::begin(result), + [](In &value) { return Out(value); } + ); + return result; +} + +template +inline std::vector castTo(const Iterator begin, const Iterator end) { + std::vector result; + std::transform(begin, end, std::back_inserter(result), + [](auto &value) { return Out(value); }); + return result; +} + +/** + * Returns an array that holds given value n times. + */ +template +inline std::vector getNTimes(const T value, size_t n) { + std::vector values; + for(size_t i = 0; i < n; ++i) { + values.push_back(value); + } + return values; +} + +template +inline size_t countUnique(const std::vector &values) { + return std::unordered_set(std::begin(values), std::end(values)).size(); +} + +template +inline bool +setContains(const std::unordered_set &set, const T &value) { + return set.find(value) != set.end(); +} + +template +inline std::vector> +zip(const std::vector &a, const std::vector &b) { + if(a.size() != b.size()) { + throw ::arrus::IllegalArgumentException("Zipped vectors should " + "have the same size."); + } + std::vector> res; + res.reserve(a.size()); + for(const auto &[x, y] : boost::combine(a, b)) { + res.emplace_back(x, y); + } + return res; +} + +template +inline std::vector +generate(size_t nElements, std::function transformation) { + std::vector result; + result.reserve(nElements); + for(size_t i = 0; i < nElements; ++i) { + result.emplace_back(transformation(i)); + } + return result; +} + +template +inline std::vector +concat(const std::vector &a, const std::vector &b) { + std::vector result; + result.reserve(a.size() + b.size()); + result.insert(std::begin(result), std::begin(a), std::end(a)); + result.insert(std::end(result), std::begin(b), std::end(b)); + return result; + +} + +template +inline std::vector +concat(const std::vector> &a) { + std::vector result; + size_t totalSize = 0; + for(const auto &v : a) { + totalSize += v.size(); + } + result.reserve(totalSize); + for(const auto &vec : a) { + result.insert(std::end(result), std::begin(vec), std::end(vec)); + } + return result; +} + +template +inline std::vector +permute(const std::vector &input, const std::vector &perm) { + std::vector output(perm.size()); + for(size_t i = 0; i < static_cast(perm.size()); ++i) { + output[perm[i]] = input[i]; + } + return output; +} + +template +inline std::bitset +toBitset(const std::vector &in) { + std::bitset result; + for(size_t i = 0; i < size; ++i) { + result[i] = in[i]; + } + return result; +} + +template +inline bool containsKey(Map map, const K &key) { + return map.find(key) != std::end(map); +} + +template +inline void setValuesInRange(std::vector &container, size_t start, size_t end, const T &value) { + for(size_t i = start; i < end; ++i) { + container[i] = value; + } +} + +template +inline void setValuesInRange(std::bitset &container, size_t start, size_t end, const bool &value) { + for(size_t i = start; i < end; ++i) { + container[i] = value; + } +} + +template +inline void setValuesInRange(std::vector &container, size_t start, size_t end, + const std::function &generator) { + for(size_t i = start; i < end; ++i) { + container[i] = generator(i); + } +} + +template +inline void setValuesInRange(std::vector &container, size_t start, size_t end, + const std::vector& source) { + if(source.size != end-start) { + throw IllegalArgumentException( + arrus::format("Source vector should have exactly {} elements " + "when assing it to the selected range.", + end-start)); + } + for(size_t i = start; i < end; ++i) { + container[i] = source[i-start]; + } +} + +} + +#endif //ARRUS_CORE_COMMON_COLLECTIONS_H diff --git a/arrus/core/common/hash.h b/arrus/core/common/hash.h new file mode 100644 index 000000000..73a22af3f --- /dev/null +++ b/arrus/core/common/hash.h @@ -0,0 +1,42 @@ +#ifndef ARRUS_CORE_COMMON_HASH_H +#define ARRUS_CORE_COMMON_HASH_H + +#include + +#include + +namespace arrus { + +// Hash +inline void hash_combine_seed(std::size_t &) {} + +template +inline void hash_combine_seed(std::size_t &seed, const T &v, Rest... rest) { + boost::hash_combine(seed, v); + hash_combine_seed(seed, rest...); +} + +template +inline std::size_t hash_combine(const T &v, Rest... rest) { + std::size_t seed = 0; + hash_combine_seed(seed, v, rest...); + return seed; +} + +#define GET_HASHER_NAME(type) type##Hasher + +#define MAKE_HASHER(type, ...) \ + struct GET_HASHER_NAME(type) { \ + std::size_t operator()(const type &t) const { \ + return arrus::hash_combine(__VA_ARGS__); \ + } \ + }; +template +struct ContainerHash { + std::size_t operator()(const Container &c) const { + return boost::hash_range(std::begin(c), std::end(c)); + } +}; +} + +#endif //ARRUS_CORE_COMMON_HASH_H diff --git a/arrus/core/common/interpolate.h b/arrus/core/common/interpolate.h new file mode 100644 index 000000000..b7a2e503d --- /dev/null +++ b/arrus/core/common/interpolate.h @@ -0,0 +1,64 @@ +#ifndef ARRUS_CORE_COMMON_INTERPOLATE_H +#define ARRUS_CORE_COMMON_INTERPOLATE_H + +#include +#include + +#include "arrus/common/format.h" +#include "arrus/common/asserts.h" + +namespace arrus { + +/** + * Linear interpolation in 1D. + * + * @param x a sorted list + * @param y + * @param xi a list of interpolated values, may not be sorted + * @return yi + */ +template +std::vector interpolate1d(const std::vector &x, const std::vector &y, + const std::vector &xi) { + + ARRUS_REQUIRES_TRUE(!x.empty(), "Interpolation 1D: " + "sample points list should not be empty."); + ARRUS_REQUIRES_TRUE(x.size() == y.size(), + "Interpolation 1D: x.size != y.size"); + ARRUS_REQUIRES_TRUE(!xi.empty(), "Interpolation 1D: " + "query points list should not be empty."); + + std::vector result(xi.size()); + int i = 0; + for(auto value : xi) { + auto it = std::lower_bound(std::begin(x), std::end(x), value); + if(it == std::end(x)) { + throw IllegalArgumentException(arrus::format( + "Interpolation 1D: value {} is out of range [{}, {}].", + value, *std::begin(x), *std::prev(std::end(x)))); + } else if(it == std::begin(x)) { + if(*it == *std::begin(x)) { + result[i] = y[0]; + } else { + // value is lower than the first element of x + throw IllegalArgumentException(arrus::format( + "Interp 1D: value {} is out of range [{}, {}].", + value, *std::begin(x), *std::prev(std::end(x)))); + } + } else { + auto pos = std::distance(std::begin(x), it); + auto x2 = x[pos], y2 = y[pos]; + auto x1 = x[pos-1], y1 = y[pos-1]; + + auto slope = (y2-y1)/(x2-x1); + auto intercept = y1 - slope*x1; + auto res = slope*value + intercept; + result[i] = res; + } + ++i; + } + return result; +} +} + +#endif //ARRUS_CORE_COMMON_INTERPOLATE_H diff --git a/arrus/core/common/interpolateTest.cpp b/arrus/core/common/interpolateTest.cpp new file mode 100644 index 000000000..af0672721 --- /dev/null +++ b/arrus/core/common/interpolateTest.cpp @@ -0,0 +1 @@ +// TODO (implement) \ No newline at end of file diff --git a/arrus/core/common/logging.cpp b/arrus/core/common/logging.cpp new file mode 100644 index 000000000..1c0219e09 --- /dev/null +++ b/arrus/core/common/logging.cpp @@ -0,0 +1,30 @@ +#include "logging.h" +#include "arrus/core/api/common/exceptions.h" + +namespace arrus { + +std::shared_ptr loggerFactory; +Logger::SharedHandle defaultLogger; + +void setLoggerFactory(const std::shared_ptr& factory) { + loggerFactory = factory; + defaultLogger = factory->getLogger(); +} + +std::shared_ptr getLoggerFactory() { + if(loggerFactory == nullptr) { + throw IllegalStateException("Logging mechanism is not initialized, " + "register logger factory first."); + } + return loggerFactory; +} + +Logger::SharedHandle getDefaultLogger() { + if(defaultLogger == nullptr) { + throw IllegalStateException("Logging mechanism is not initialized, " + "register logger factory first."); + } + return defaultLogger; +} + +} \ No newline at end of file diff --git a/arrus/core/common/logging.h b/arrus/core/common/logging.h new file mode 100644 index 000000000..21d9e2b48 --- /dev/null +++ b/arrus/core/common/logging.h @@ -0,0 +1,36 @@ +#ifndef ARRUS_CORE_COMMON_LOGGING_H +#define ARRUS_CORE_COMMON_LOGGING_H + +#include "arrus/core/api/common/LoggerFactory.h" +#include "arrus/core/api/common/logging.h" + +namespace arrus { + +extern std::shared_ptr loggerFactory; + +std::shared_ptr getLoggerFactory(); + +Logger::SharedHandle getDefaultLogger(); + +#define INIT_ARRUS_DEVICE_LOGGER(logger, devId) \ + logger->setAttribute("DeviceId", devId) \ + +#define ARRUS_LOG(logger, severity, msg) \ + (logger)->log(severity, msg) + +#define ARRUS_LOG_DEFAULT(severity, msg) \ + getDefaultLogger()->log(severity, msg) + +#define DEFAULT_TEST_LOG_LEVEL arrus::LogSeverity::DEBUG + +#define ARRUS_INIT_TEST_LOG_LEVEL(ComponentType, level) \ +do{ \ + auto loggingMechanism = std::make_shared(); \ + loggingMechanism->addClog(level); \ + arrus::setLoggerFactory(loggingMechanism); \ +} while(0) +} + +#define ARRUS_INIT_TEST_LOG(ComponentType) \ + ARRUS_INIT_TEST_LOG_LEVEL(ComponentType, DEFAULT_TEST_LOG_LEVEL) +#endif //ARRUS_CORE_COMMON_LOGGING_H diff --git a/arrus/core/common/tests.h b/arrus/core/common/tests.h new file mode 100644 index 000000000..7ba41910d --- /dev/null +++ b/arrus/core/common/tests.h @@ -0,0 +1,27 @@ +#ifndef ARRUS_CORE_COMMON_TESTS_H +#define ARRUS_CORE_COMMON_TESTS_H + +#include + +namespace arrus { + +#define ARRUS_STRUCT_INIT_LIST(Type, initList) \ + [&]() { \ + Type x; \ + (initList); \ + return x; \ + }() + +#define ARRUS_EXPECT_TENSORS_EQ(a, b) \ + do { \ + Eigen::Tensor eq = ((a) == (b)).all(); \ + EXPECT_TRUE(eq(0)); \ + } while(0) + +# define ARRUS_PRINT_DIFFERENCES(a, b) \ + do { \ + a. \ + } while(0) +} + +#endif //ARRUS_CORE_COMMON_TESTS_H diff --git a/arrus/core/common/validation.h b/arrus/core/common/validation.h new file mode 100644 index 000000000..a85d3f681 --- /dev/null +++ b/arrus/core/common/validation.h @@ -0,0 +1,364 @@ +#ifndef ARRUS_CORE_COMMON_VALIDATION_H +#define ARRUS_CORE_COMMON_VALIDATION_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "arrus/core/api/common/exceptions.h" +#include "arrus/common/format.h" + +namespace arrus { + +template +class Validator { + public: + explicit Validator(std::string componentName) + : componentName(std::move(componentName)) {} + + virtual void validate(const T &obj) = 0; + + std::vector getErrors(const std::string ¶meter) { + std::vector result; + auto range = errors.equal_range(parameter); + for(auto i = range.first; i != range.second; ++i) { + result.push_back(i->second); + } + return result; + } + + [[nodiscard]] const std::multimap & + getErrors() const { + return errors; + } + + [[nodiscard]] const std::string &getComponentName() const { + return componentName; + } + + template + void copyErrorsFrom(const Validator &validator) { + for(auto&[key, value] : validator.getErrors()) { + errors.emplace(validator.getComponentName() + key, value); + } + } + + bool hasErrors() { + return !errors.empty(); + } + + void throwOnErrors() { + if(!errors.empty()) { + // Generate message with errors. + std::stringstream ss; + decltype(errors.equal_range("")) r; + int c = 0; + for(auto i = std::begin(errors); + i != std::end(errors); i = r.second) { + if(c > 0) { + ss << ". "; + } + ss << "parameter '" << i->first << "': "; + r = errors.equal_range(i->first); + int cc = 0; + for(auto j = r.first; j != r.second; ++j) { + if(cc > 0) { + ss << ". "; + } + ss << j->second; + cc++; + } + c++; + } + std::string message = arrus::format( + "One or more problems have been found " + "with {}: {}", + componentName, ss.str()); + + throw IllegalArgumentException(message); + } + } + + protected: + + /** + * Checks is given value is equal to 'expected'. + */ + template + void + expectEqual(const std::string ¶meter, const U &value, const U &expected, + const std::string &msg = "") { + if(value != expected) { + errors.emplace(parameter, + arrus::format( + "Value '{}{}' should be equal '{}' " + "(found: '{}')", + parameter, + msg, + expected, + value + )); + } + } + + + + /** + * Checks is given value is equal to 'expected'. + */ + template + void + expectAtMost(const std::string ¶meter, const U &value, + const U &expected, const std::string &msg = "") { + if(value > expected) { + errors.emplace(parameter, + arrus::format( + "Value '{}{}' should be at most '{}' " + "(found: '{}')", + parameter, msg, expected, value + )); + } + } + + /** + * Checks is given value is divisible by divider. + */ + template + void + expectDivisible( + const std::string ¶meter, const U &value, const U ÷r, + const std::string &msg = "") { + if(value % divider != 0) { + errors.emplace(parameter, + arrus::format( + "Value '{}{}' should be divisible by '{}' " + "(actually is: '{}')", + parameter, + msg, + divider, + value + )); + } + } + + template + void + expectAllDataType(const std::string ¶meter, const Container &container, + const std::string &msg = "") { + constexpr auto min = (std::numeric_limits::min)(); + constexpr auto max = (std::numeric_limits::max)(); + std::set invalidValues; + + for(auto const &value : container) { + if(!(value >= min && value <= max)) { + invalidValues.insert(value); + } + } + + if(!invalidValues.empty()) { + errors.emplace( + parameter, arrus::format( + "Value(s) '{}{}' should be in range [{}, {}] (found: '{}')", + parameter, msg, min, max, toString(invalidValues) + ) + ); + } + } + + /** + * Checks if all given values are in range [min, max]. + */ + template + void + expectAllInRange(const std::string ¶meter, const std::vector &values, + const U &min, const U &max, const std::string &msg = "") { + expectAllInRange(parameter, std::begin(values), std::end(values), + min, max, msg); + } + + /** + * Checks if all given values are in range [min, max]. + */ + template + void + expectAllInRange(const std::string ¶meter, + const Iterator begin, const Iterator end, + const U &min, const U &max, const std::string &msg = "") { + + std::set invalidValues; + + for(auto it = begin; it != end; ++it) { + auto value = *it; + if(!(value >= min && value <= max)) { + invalidValues.insert(value); + } + } + + if(!invalidValues.empty()) { + errors.emplace( + parameter, + arrus::format("Value(s) '{}{}' should be in range [{}, {}] " + "(found: '{}')", + parameter, msg, + min, max, toString(invalidValues) + ) + ); + } + } + + template + void + expectAllPositive(const std::string ¶meter, + const std::vector &values) { + std::set invalidValues; + for(auto value : values) { + if(value <= 0) { + invalidValues.insert(value); + } + } + if(!invalidValues.empty()) { + errors.emplace( + parameter, + arrus::format("Value(s) '{}' should be positive " + "(found: '{}')", + parameter, toString(invalidValues) + ) + ); + } + } + + /** + * Checks if given value is in range [min, max] for given data type. + */ + template + void + expectDataType(const std::string ¶meter, const U &value, + const std::string &msg = "") { + constexpr auto min = (std::numeric_limits::min)(); + constexpr auto max = (std::numeric_limits::max)(); + if(!(value >= min && value <= max)) { + errors.emplace( + parameter, + arrus::format( + "Value '{}{}' should be in range [{}, {}] " + "(found: '{}')", + parameter, msg, min, max, value)); + } + } + + + + /** + * Checks if given value is in range [min, max]. + */ + template + void + expectInRange(const std::string ¶meter, const U &value, const U &min, + const U &max, const std::string &msg = "") { + if(!(value >= min && value <= max)) { + errors.emplace(parameter, + arrus::format( + "Value '{}{}' should be in range [{}, {}] " + "(found: '{}')", + parameter, + msg, + min, max, + value + )); + } + } + + + + template + void + expectOneOf(const std::string ¶meter, U value, Container dictionary, + const std::string &msg = "") { + if(dictionary.find(value) == dictionary.end()) { + // Concatenate and sort dictionary values. + std::vector stringRepresentation; + std::transform(std::begin(dictionary), std::end(dictionary), + std::back_inserter(stringRepresentation), + [](auto &val) { + return boost::lexical_cast((U) val); + }); + errors.emplace(parameter, arrus::format( + "Value '{}{}' should be one of: '{}' (found: '{}')", + parameter, + msg, + boost::algorithm::join(stringRepresentation, ", "), + value + )); + } + } + + template + void + expectUnique(const std::string ¶meter, std::vector values, + const std::string &msg = "") { + std::unordered_set set(std::begin(values), std::end(values)); + if(set.size() != values.size()) { + errors.emplace(parameter, arrus::format( + "Parameter '{}{}' contains non-unique values. (got: '{}')", + parameter, msg, ::arrus::toString(values) + )); + + } + } + + void expectTrue(const std::string ¶meter, + bool condition, const std::string &msg) { + if(!condition) { + errors.emplace(parameter, arrus::format("{}", msg)); + } + } + + void expectFalse(const std::string ¶meter, + bool condition, const std::string &msg) { + if(condition) { + errors.emplace(parameter, arrus::format("{}", msg)); + } + } + + + private: + // parameter name -> messages + std::multimap errors; + std::string componentName; +}; + +// Macros +#define ARRUS_VALIDATOR_EXPECT_IN_RANGE(value, min, max) \ + expectInRange(#value, value, min, max) + +#define ARRUS_VALIDATOR_EXPECT_IN_RANGE_M(value, min, max, msg) \ + expectInRange(#value, value, min, max, msg); + +#define ARRUS_VALIDATOR_EXPECT_ALL_IN_RANGE_V(vector, min, max) \ + expectAllInRange(#vector, vector, min, max) + +#define ARRUS_VALIDATOR_EXPECT_ALL_IN_RANGE_VM(vector, min, max, msg) \ + expectAllInRange(#vector, vector, min, max, msg) + +#define ARRUS_VALIDATOR_EXPECT_ALL_IN_RANGE_IM(coll, min, max, msg) \ + expectAllInRange(#coll, std::begin(coll), std::end(coll), min, max, msg) + +#define ARRUS_VALIDATOR_EXPECT_EQUAL_M(value, expected, msg) \ + expectEqual(#value, value, expected, msg) + +#define ARRUS_VALIDATOR_EXPECT_TRUE_M(condition, msg) \ + expectTrue((#condition), (condition), msg) + +#define ARRUS_VALIDATOR_EXPECT_DIVISIBLE_M(value, divider, msg) \ + expectDivisible(#value, value, divider, msg) +} + +#endif //ARRUS_CORE_COMMON_VALIDATION_H diff --git a/arrus/core/devices/DeviceId.cpp b/arrus/core/devices/DeviceId.cpp new file mode 100644 index 000000000..a3248a74c --- /dev/null +++ b/arrus/core/devices/DeviceId.cpp @@ -0,0 +1,127 @@ +#include +#include +#include + +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/api/common/exceptions.h" +#include "arrus/common/format.h" +#include "arrus/common/asserts.h" + +namespace arrus::devices { + +static const std::unordered_map + DEVICE_TYPE_ENUM_STRINGS = { + {DeviceType::Us4R, "Us4R"}, + {DeviceType::Us4OEM, "Us4OEM"}, + {DeviceType::ProbeAdapter,"ProbeAdapter"}, + {DeviceType::Probe, "Probe"}, + {DeviceType::GPU, "GPU"}, + {DeviceType::CPU, "CPU"}, + {DeviceType::HV, "HV"} +}; + +/** + * String representation of Device Type Enum. + * Helper class to implement bi-directional translation enum -> string, + * string -> enum. + */ +class DeviceTypeEnumStringRepr { +public: + DeviceTypeEnumStringRepr(const DeviceTypeEnumStringRepr &) = delete; + + void operator=(const DeviceTypeEnumStringRepr &) = delete; + + static DeviceTypeEnumStringRepr &getInstance() { + static DeviceTypeEnumStringRepr instance; + return instance; + } + + std::string toString(const DeviceType deviceTypeEnum) { + return reprs.right.at(deviceTypeEnum); + } + + DeviceType parse(const std::string &deviceTypeStr) { + return reprs.left.at(deviceTypeStr); + } + + std::vector keys() { + std::vector result; + + std::transform(reprs.left.begin(), reprs.left.end(), + std::back_inserter(result), + [](auto const &p) { return p.first; }); + return result; + } + +private: + DeviceTypeEnumStringRepr() { + for (const auto& [e, str] : DEVICE_TYPE_ENUM_STRINGS) { + reprs.insert({str, e}); + } + } + + boost::bimap reprs; +}; + +DeviceType parseToDeviceTypeEnum(const std::string &deviceTypeStr) { + try { + return DeviceTypeEnumStringRepr::getInstance().parse(deviceTypeStr); + } + catch (const std::out_of_range&) { + std::vector availableKeys = + DeviceTypeEnumStringRepr::getInstance().keys(); + std::sort(availableKeys.begin(), availableKeys.end()); + const auto availableKeysMsg = + boost::algorithm::join(availableKeys,", "); + throw IllegalArgumentException( + arrus::format("Unrecognized device type: {}, " + "allowed types: {}", deviceTypeStr, + availableKeysMsg)); + } +} + +std::string toString(const DeviceType deviceTypeEnum) { + return DeviceTypeEnumStringRepr::getInstance().toString(deviceTypeEnum); +} + +// DeviceId. + +DeviceId DeviceId::parse(const std::string &deviceId) { + std::vector deviceIdComponents; + boost::algorithm::split(deviceIdComponents, deviceId, + boost::is_any_of(":")); + + if (deviceIdComponents.size() != 2) { + throw IllegalArgumentException(arrus::format( + "Device id should be have format: deviceType:ordinal " + "(got: '{}')", deviceId + )); + } + auto deviceTypeStr = deviceIdComponents[0]; + auto ordinalStr = deviceIdComponents[1]; + boost::trim(deviceTypeStr); + boost::trim(ordinalStr); + // Device Type. + DeviceType deviceTypeEnum = parseToDeviceTypeEnum(deviceTypeStr); + + // Device Ordinal. + ARRUS_REQUIRES_TRUE_FOR_ARGUMENT(isDigitsOnly(ordinalStr), + arrus::format("Invalid device number: {}", ordinalStr) + ); + Ordinal ordinal; + ARRUS_REQUIRES_NO_THROW( + ordinal = boost::lexical_cast(ordinalStr), + boost::bad_lexical_cast, + arrus::IllegalArgumentException( + arrus::format("Invalid device number: {}", ordinalStr) + ) + ); + return DeviceId(deviceTypeEnum, ordinal); +} + +std::ostream& operator<<(std::ostream &os, const DeviceId &id) { + os << toString(id.deviceType) << ":" << id.ordinal; + return os; +} + +} \ No newline at end of file diff --git a/arrus/core/devices/DeviceId.h b/arrus/core/devices/DeviceId.h new file mode 100644 index 000000000..6a78a3e46 --- /dev/null +++ b/arrus/core/devices/DeviceId.h @@ -0,0 +1,16 @@ +#ifndef ARRUS_CORE_DEVICES_DEVICEIDHASHER_H +#define ARRUS_CORE_DEVICES_DEVICEIDHASHER_H + +#include +#include + +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/common/hash.h" + +namespace arrus::devices { + +MAKE_HASHER(DeviceId, t.getDeviceType(), t.getOrdinal()) + +} + +#endif //ARRUS_CORE_DEVICES_DEVICEIDHASHER_H diff --git a/arrus/core/devices/DeviceIdTest.cpp b/arrus/core/devices/DeviceIdTest.cpp new file mode 100644 index 000000000..2f1785ebd --- /dev/null +++ b/arrus/core/devices/DeviceIdTest.cpp @@ -0,0 +1,166 @@ +// Device Id class test. +#include +#include +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/api/common/exceptions.h" + +namespace { + +using namespace arrus::devices; + +// DeviceId + +// DeviceId::parse + +struct ParseCorrectParams { + std::string idString; + DeviceId expectedId; + + friend std::ostream & + operator<<(std::ostream &os, const ParseCorrectParams &state) { + os << "idString: " << state.idString << " expectedId: " + << state.expectedId; + return os; + } +}; + +class DeviceIdCorrectParseTest + : public testing::TestWithParam { +}; + +TEST_P(DeviceIdCorrectParseTest, ParsesCorrectDeviceIds) { + const DeviceId id = DeviceId::parse(GetParam().idString); + EXPECT_EQ(GetParam().expectedId, id); +} + +INSTANTIATE_TEST_CASE_P + +(SimpleCases, DeviceIdCorrectParseTest, + testing::Values( + ParseCorrectParams{"Us4OEM:0", DeviceId(DeviceType::Us4OEM, 0)}, + ParseCorrectParams{"Us4OEM:1", DeviceId(DeviceType::Us4OEM, 1)}, + ParseCorrectParams{"ProbeAdapter:0", + DeviceId(DeviceType::ProbeAdapter, 0)}, + ParseCorrectParams{"Probe:3", DeviceId(DeviceType::Probe, 3)}, + ParseCorrectParams{"CPU:4", DeviceId(DeviceType::CPU, 4)}, + ParseCorrectParams{"GPU:0", DeviceId(DeviceType::GPU, 0)}, + ParseCorrectParams{"GPU:3", DeviceId(DeviceType::GPU, 3)} + )); + +INSTANTIATE_TEST_CASE_P + +(TestComponentTrimming, DeviceIdCorrectParseTest, + testing::Values( + // Make sure that id components are trimmed before creating the id. + // Any number of leading/trailing whitespaces is accepted. + ParseCorrectParams{"Us4OEM : 0", DeviceId(DeviceType::Us4OEM, 0)}, + ParseCorrectParams{" Us4OEM : 1 ", + DeviceId(DeviceType::Us4OEM, 1)}, + ParseCorrectParams{" GPU:2 ", DeviceId(DeviceType::GPU, 2)} + )); + +struct ParseIncorrectParams { + std::string idString; + + friend std::ostream & + operator<<(std::ostream &os, const ParseIncorrectParams &state) { + os << "idString: " << state.idString; + return os; + } +}; + +class DeviceIdIncorrectParseTest + : public testing::TestWithParam { +}; + +TEST_P(DeviceIdIncorrectParseTest, DeclineIncorrectIds) { + EXPECT_THROW(DeviceId::parse(GetParam().idString), + ::arrus::IllegalArgumentException); +} + +INSTANTIATE_TEST_CASE_P + +(TestUnknownDevice, DeviceIdIncorrectParseTest, + testing::Values( + ParseIncorrectParams{"Unknown: 0"}, + ParseIncorrectParams{"Unknown: 4"}, + ParseIncorrectParams{"Abcdefghijklmnopqsrtwxyz:4"} + )); + +INSTANTIATE_TEST_CASE_P + +(TestInvalidCapitalization, DeviceIdIncorrectParseTest, + testing::Values( + // We require exact case name: Us4OEM + ParseIncorrectParams{"US4OEM: 0"}, + ParseIncorrectParams{"gpu:1"} +)); + +INSTANTIATE_TEST_CASE_P + +(TestNonAlphanumericCharacters, DeviceIdIncorrectParseTest, + testing::Values( + // We require exact case name: Us4OEM + ParseIncorrectParams{"us_4oem:1"}, + ParseIncorrectParams{"GPU :1_2"}, + ParseIncorrectParams{"GPU :abc2"}, + ParseIncorrectParams{"GPU :2abc"}, + ParseIncorrectParams{"GPU :2 abc"} + )); + +INSTANTIATE_TEST_CASE_P + +(TestMissingIdComponents, DeviceIdIncorrectParseTest, + testing::Values( + ParseIncorrectParams{""}, + ParseIncorrectParams{" : "}, + ParseIncorrectParams{"GPU:"}, + ParseIncorrectParams{" : 0"}, + ParseIncorrectParams{" GPU 0 "}, + ParseIncorrectParams{"GPU::0"} + )); + +INSTANTIATE_TEST_CASE_P + +(TestInvalidOrdinalRange, DeviceIdIncorrectParseTest, + testing::Values( + ParseIncorrectParams{"GPU:-1"}, + ParseIncorrectParams{"GPU:1000000"} + )); + +// DeviceId::toString + + +// przetestowac, jak zachowa sie konwersja, gdy podamy DeviceType spoza zakresu enum + +struct ToStringCorrectParams { + DeviceId id; + std::string expectedString; + + friend std::ostream & + operator<<(std::ostream &os, const ToStringCorrectParams &state) { + os << " id: " << state.id + << "expected string: " << state.expectedString; + return os; + } +}; + +class DeviceIdToStringCorrectTest + : public testing::TestWithParam { +}; + +TEST_P(DeviceIdToStringCorrectTest, ToStringCorrectIds) { + EXPECT_EQ(GetParam().id.toString(), GetParam().expectedString); +} + +INSTANTIATE_TEST_CASE_P + +(SimpleCases, DeviceIdToStringCorrectTest, + testing::Values( + ToStringCorrectParams{DeviceId(DeviceType::Us4OEM, 0), "Us4OEM:0"}, + ToStringCorrectParams{DeviceId(DeviceType::GPU, 1), "GPU:1"} + )); + +} + + diff --git a/arrus/core/devices/SettingsValidator.h b/arrus/core/devices/SettingsValidator.h new file mode 100644 index 000000000..1954cdb2d --- /dev/null +++ b/arrus/core/devices/SettingsValidator.h @@ -0,0 +1,18 @@ +#ifndef ARRUS_CORE_DEVICES_DEVICESETTINGSVALIDATOR_H +#define ARRUS_CORE_DEVICES_DEVICESETTINGSVALIDATOR_H + +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/common/validation.h" + +namespace arrus::devices { + +template +class SettingsValidator : public Validator { +public: + explicit SettingsValidator(const DeviceId &id) + : Validator(id.toString() + " settings") {} +}; + +} + +#endif //ARRUS_CORE_DEVICES_DEVICESETTINGSVALIDATOR_H diff --git a/arrus/core/devices/TxRxParameters.cpp b/arrus/core/devices/TxRxParameters.cpp new file mode 100644 index 000000000..4fb9b0fad --- /dev/null +++ b/arrus/core/devices/TxRxParameters.cpp @@ -0,0 +1,29 @@ +#include "TxRxParameters.h" + +#include "arrus/core/api/common/types.h" +#include "arrus/core/common/collections.h" + +namespace arrus::devices { + +const TxRxParameters TxRxParameters::US4OEM_NOP = TxRxParameters( + std::vector(128, false), + std::vector(128, 0), + ops::us4r::Pulse(1e6, 1, false), + std::vector(128, false), + Interval(0, 64), + 0, 0); + + + + +uint16 getNumberOfNoRxNOPs(const TxRxParamsSequence &seq) { + uint16 res = 0; + for(const auto &op : seq) { + if(!op.isRxNOP()) { + ++res; + } + } + return res; +} + +} \ No newline at end of file diff --git a/arrus/core/devices/TxRxParameters.h b/arrus/core/devices/TxRxParameters.h new file mode 100644 index 000000000..80b8cfd25 --- /dev/null +++ b/arrus/core/devices/TxRxParameters.h @@ -0,0 +1,170 @@ +#ifndef ARRUS_CORE_DEVICES_TXRXPARAMETERS_H +#define ARRUS_CORE_DEVICES_TXRXPARAMETERS_H + +#include +#include +#include + +#include "arrus/core/api/common/Interval.h" +#include "arrus/core/api/common/Tuple.h" +#include "arrus/core/api/common/types.h" +#include "arrus/common/format.h" +#include "arrus/core/api/ops/us4r/Pulse.h" + +namespace arrus::devices { + +class TxRxParameters { +public: + static const TxRxParameters US4OEM_NOP; + + static TxRxParameters createRxNOPCopy(const TxRxParameters& op) { + return TxRxParameters( + op.txAperture, + op.txDelays, + op.txPulse, + BitMask(op.rxAperture.size(), false), + op.rxSampleRange, + op.rxDecimationFactor, + op.pri, + op.rxPadding + ); + } + + /** + * + * ** tx aperture, tx delays and rx aperture should have the same size + * (tx delays is NOT limited to the tx aperture active elements - + * the whole array must be provided).** + * + * @param txAperture + * @param txDelays + * @param txPulse + * @param rxAperture + * @param rxSampleRange [start, end) range of samples to acquire, starts from 0 + * @param rxDecimationFactor + * @param pri + * @param rxPadding how many 0-channels padd from the left and right + */ + TxRxParameters(std::vector txAperture, + std::vector txDelays, + const ops::us4r::Pulse &txPulse, + std::vector rxAperture, + Interval rxSampleRange, + uint32 rxDecimationFactor, float pri, + Tuple rxPadding = {0, 0}) + : txAperture(std::move(txAperture)), txDelays(std::move(txDelays)), + txPulse(txPulse), + rxAperture(std::move(rxAperture)), rxSampleRange(std::move(rxSampleRange)), + rxDecimationFactor(rxDecimationFactor), pri(pri), + rxPadding(std::move(rxPadding)){} + + [[nodiscard]] const std::vector &getTxAperture() const { + return txAperture; + } + + [[nodiscard]] const std::vector &getTxDelays() const { + return txDelays; + } + + [[nodiscard]] const ops::us4r::Pulse &getTxPulse() const { + return txPulse; + } + + [[nodiscard]] const std::vector &getRxAperture() const { + return rxAperture; + } + + [[nodiscard]] const Interval &getRxSampleRange() const { + return rxSampleRange; + } + + [[nodiscard]] uint32 getNumberOfSamples() const { + return rxSampleRange.end() - rxSampleRange.start(); + } + + [[nodiscard]] int32 getRxDecimationFactor() const { + return rxDecimationFactor; + } + + [[nodiscard]] float getPri() const { + return pri; + } + + [[nodiscard]] const Tuple &getRxPadding() const { + return rxPadding; + } + + [[nodiscard]] bool isNOP() const { + auto atLeastOneTxActive = std::reduce( + std::begin(txAperture), + std::end(txAperture), + false, [](auto a, auto b) {return a | b;}); + auto atLeastOneRxActive = std::reduce( + std::begin(rxAperture), + std::end(rxAperture), + false, [](auto a, auto b) {return a | b;}); + return !atLeastOneTxActive && !atLeastOneRxActive; + } + + [[nodiscard]] bool isRxNOP() const { + auto atLeastOneRxActive = std::reduce( + std::begin(rxAperture), + std::end(rxAperture), + false, [](auto a, auto b) {return a | b;}); + return !atLeastOneRxActive; + } + + friend std::ostream & + operator<<(std::ostream &os, const TxRxParameters ¶meters) { + os << "Tx/Rx: "; + os << "TX: "; + os << "aperture: " << ::arrus::toString(parameters.getTxAperture()) + << ", delays: " << ::arrus::toString(parameters.getTxDelays()) + << ", center frequency: " << parameters.getTxPulse().getCenterFrequency() + << ", n. periods: " << parameters.getTxPulse().getNPeriods() + << ", inverse: " << parameters.getTxPulse().isInverse(); + os << "; RX: "; + os << "aperture: " << ::arrus::toString(parameters.getRxAperture()); + os << "sample range: " << parameters.getRxSampleRange().start() << ", " + << parameters.getRxSampleRange().end(); + os << ", fs divider: " << parameters.getRxDecimationFactor(); + os << std::endl; + return os; + } + + bool operator==(const TxRxParameters &rhs) const { + return txAperture == rhs.txAperture && + txDelays == rhs.txDelays && + txPulse == rhs.txPulse && + rxAperture == rhs.rxAperture && + rxSampleRange == rhs.rxSampleRange && + rxDecimationFactor == rhs.rxDecimationFactor && + pri == rhs.pri; + } + + bool operator!=(const TxRxParameters &rhs) const { + return !(rhs == *this); + } +private: + ::std::vector txAperture; + ::std::vector txDelays; + ::arrus::ops::us4r::Pulse txPulse; + ::std::vector rxAperture; + // TODO change to a simple pair + Interval rxSampleRange; + int32 rxDecimationFactor; + float pri; + Tuple rxPadding; +}; + + +using TxRxParamsSequence = std::vector; + +/** + * Returns the number of actual ops, that is, a the number of ops excluding RxNOPs. + */ +uint16 getNumberOfNoRxNOPs(const TxRxParamsSequence &seq); + +} + +#endif //ARRUS_CORE_DEVICES_TXRXPARAMETERS_H diff --git a/arrus/core/devices/UltrasoundDevice.h b/arrus/core/devices/UltrasoundDevice.h new file mode 100644 index 000000000..8cfd6a401 --- /dev/null +++ b/arrus/core/devices/UltrasoundDevice.h @@ -0,0 +1,34 @@ +#ifndef ARRUS_CORE_DEVICES_ULTRASOUNDDEVICE_H +#define ARRUS_CORE_DEVICES_ULTRASOUNDDEVICE_H + +#include + +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/common/Interval.h" +#include "arrus/core/devices/TxRxParameters.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/api/devices/us4r/FrameChannelMapping.h" +#include "arrus/core/devices/us4r/DataTransfer.h" +#include "arrus/core/devices/us4r/Us4ROutputBuffer.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMBuffer.h" + +namespace arrus::devices { + +class UltrasoundDevice { +public: + virtual ~UltrasoundDevice() = default; + + virtual void start() = 0; + + virtual void stop() = 0; + + virtual Interval getAcceptedVoltageRange() = 0; + + virtual void syncTrigger() = 0; + + virtual void setTgcCurve(const ::arrus::ops::us4r::TGCCurve &tgcCurve) = 0; +}; + +} + +#endif //ARRUS_CORE_DEVICES_ULTRASOUNDDEVICE_H diff --git a/arrus/core/devices/probe/ProbeFactory.h b/arrus/core/devices/probe/ProbeFactory.h new file mode 100644 index 000000000..e9c6825c3 --- /dev/null +++ b/arrus/core/devices/probe/ProbeFactory.h @@ -0,0 +1,16 @@ +#ifndef ARRUS_CORE_DEVICES_PROBE_PROBEFACTORY_H +#define ARRUS_CORE_DEVICES_PROBE_PROBEFACTORY_H + +#include "arrus/core/api/devices/probe/ProbeSettings.h" +#include "arrus/core/devices/probe/ProbeImplBase.h" + +namespace arrus::devices { +class ProbeFactory { +public: + virtual ProbeImplBase::Handle + getProbe(const ProbeSettings &settings, + ProbeAdapterImplBase::RawHandle adapter) = 0; +}; +} + +#endif //ARRUS_CORE_DEVICES_PROBE_PROBEFACTORY_H diff --git a/arrus/core/devices/probe/ProbeFactoryImpl.h b/arrus/core/devices/probe/ProbeFactoryImpl.h new file mode 100644 index 000000000..bb9c351de --- /dev/null +++ b/arrus/core/devices/probe/ProbeFactoryImpl.h @@ -0,0 +1,44 @@ +#ifndef ARRUS_CORE_DEVICES_PROBE_PROBEFACTORYIMPL_H +#define ARRUS_CORE_DEVICES_PROBE_PROBEFACTORYIMPL_H + +#include + +#include "arrus/core/devices/probe/ProbeSettingsValidator.h" +#include "arrus/core/api/devices/Device.h" +#include "arrus/core/api/devices/us4r/ProbeAdapter.h" +#include "arrus/common/asserts.h" +#include "arrus/common/format.h" +#include "arrus/core/api/common/exceptions.h" +#include "arrus/core/devices/probe/ProbeFactory.h" +#include "ProbeImpl.h" + +namespace arrus::devices { + +class ProbeFactoryImpl : public ProbeFactory { +public: + ProbeImplBase::Handle getProbe(const ProbeSettings &settings, + ProbeAdapterImplBase::RawHandle adapter) override { + DeviceId id(DeviceType::Probe, 0); + ProbeSettingsValidator validator(id.getOrdinal()); + validator.validate(settings); + validator.throwOnErrors(); + + // Additionally, verify destination channels (should be in range + // available for the given adapter). + for(auto value : settings.getChannelMapping()) { + ARRUS_REQUIRES_IN_CLOSED_INTERVAL( + value, 0, adapter->getNumberOfChannels(), + ::arrus::format("Destination channel address: {} " + "exceeds maximum number of channels ({})" + " of the underlying probe adapter.", + value, adapter->getNumberOfChannels()) + ); + } + return std::make_unique(id, settings.getModel(), adapter, + settings.getChannelMapping()); + } +}; + +} + +#endif //ARRUS_CORE_DEVICES_PROBE_PROBEFACTORYIMPL_H diff --git a/arrus/core/devices/probe/ProbeImpl.cpp b/arrus/core/devices/probe/ProbeImpl.cpp new file mode 100644 index 000000000..5c7f26c8b --- /dev/null +++ b/arrus/core/devices/probe/ProbeImpl.cpp @@ -0,0 +1,120 @@ +#include "ProbeImpl.h" + +#include "arrus/common/asserts.h" +#include "arrus/core/common/validation.h" + +namespace arrus::devices { + +ProbeImpl::ProbeImpl(const DeviceId &id, ProbeModel model, + ProbeAdapterImplBase::RawHandle adapter, + std::vector channelMapping) + : ProbeImplBase(id), logger{getLoggerFactory()->getLogger()}, + model(std::move(model)), adapter(adapter), + channelMapping(std::move(channelMapping)) { + + INIT_ARRUS_DEVICE_LOGGER(logger, id.toString()); +} + +class ProbeTxRxValidator : public Validator { +public: + ProbeTxRxValidator(const std::string &componentName, const ProbeModel &modelRef) + : Validator(componentName), modelRef(modelRef) {} + + void validate(const TxRxParamsSequence &txRxs) override { + + auto numberOfChannels = modelRef.getNumberOfElements().product(); + auto &txFrequencyRange = modelRef.getTxFrequencyRange(); + + for(size_t firing = 0; firing < txRxs.size(); ++firing) { + const auto &op = txRxs[firing]; + auto firingStr = ::arrus::format(" (firing {})", firing); + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getTxAperture().size(), numberOfChannels, firingStr); + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getRxAperture().size(), numberOfChannels, firingStr); + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getTxDelays().size(), numberOfChannels, firingStr); + ARRUS_VALIDATOR_EXPECT_IN_RANGE_M( + op.getTxPulse().getCenterFrequency(), + txFrequencyRange.start(), txFrequencyRange.end(), + firingStr); + } + } + +private: + const ProbeModel &modelRef; +}; + +std::tuple +ProbeImpl::setTxRxSequence(const std::vector &seq, + const ops::us4r::TGCCurve &tgcSamples, + uint16 rxBufferSize, uint16 rxBatchSize, + std::optional sri) { + // Validate input sequence + ProbeTxRxValidator validator( + ::arrus::format("tx rx sequence for {}", getDeviceId().toString()), model); + validator.validate(seq); + validator.throwOnErrors(); + + // set tx rx sequence + std::vector adapterSeq; + + auto probeNumberOfElements = model.getNumberOfElements().product(); + + for(const auto &op: seq) { + logger->log(LogSeverity::TRACE, arrus::format( + "Setting tx/rx {}", ::arrus::toString(op))); + + BitMask txAperture(adapter->getNumberOfChannels()); + BitMask rxAperture(adapter->getNumberOfChannels()); + std::vector txDelays(adapter->getNumberOfChannels()); + + ARRUS_REQUIRES_TRUE( + op.getTxAperture().size() == op.getRxAperture().size() + && op.getTxAperture().size() == op.getTxDelays().size() + && op.getTxAperture().size() == probeNumberOfElements, + arrus::format("Probe's tx, rx apertures and tx delays " + "array should have the same size: {}", + model.getNumberOfElements().product())); + + for(size_t pch = 0; pch < op.getTxAperture().size(); ++pch) { + auto ach = channelMapping[pch]; + txAperture[ach] = op.getTxAperture()[pch]; + rxAperture[ach] = op.getRxAperture()[pch]; + txDelays[ach] = op.getTxDelays()[pch]; + } + adapterSeq.emplace_back(txAperture, txDelays, op.getTxPulse(), + rxAperture, op.getRxSampleRange(), + op.getRxDecimationFactor(), op.getPri(), + op.getRxPadding()); + } + + return adapter->setTxRxSequence(adapterSeq, tgcSamples, rxBufferSize, + rxBatchSize, sri); +} + +Interval ProbeImpl::getAcceptedVoltageRange() { + return model.getVoltageRange(); +} + +void ProbeImpl::start() { + adapter->start(); +} + +void ProbeImpl::stop() { + adapter->stop(); +} + +void ProbeImpl::syncTrigger() { + adapter->syncTrigger(); +} + +void ProbeImpl::registerOutputBuffer(Us4ROutputBuffer *buffer, const Us4RBuffer::Handle &us4rBuffer) { + adapter->registerOutputBuffer(buffer, us4rBuffer); +} + +void ProbeImpl::setTgcCurve(const std::vector &tgcCurve) { + adapter->setTgcCurve(tgcCurve); +} + +} \ No newline at end of file diff --git a/arrus/core/devices/probe/ProbeImpl.h b/arrus/core/devices/probe/ProbeImpl.h new file mode 100644 index 000000000..bc66ef42a --- /dev/null +++ b/arrus/core/devices/probe/ProbeImpl.h @@ -0,0 +1,61 @@ +#ifndef ARRUS_CORE_DEVICES_PROBE_PROBEIMPL_H +#define ARRUS_CORE_DEVICES_PROBE_PROBEIMPL_H + +#include "arrus/core/api/devices/probe/ProbeModel.h" +#include "arrus/core/devices/probe/ProbeImplBase.h" +#include "arrus/core/api/devices/us4r/ProbeAdapter.h" +#include "arrus/core/devices/TxRxParameters.h" +#include "arrus/core/devices/UltrasoundDevice.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/common/logging.h" +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterImplBase.h" + +namespace arrus::devices { + +class ProbeImpl : public ProbeImplBase { + +public: + using Handle = std::unique_ptr; + using RawHandle = PtrHandle; + + ProbeImpl(const DeviceId &id, ProbeModel model, + ProbeAdapterImplBase::RawHandle adapter, + std::vector channelMapping); + + const ProbeModel &getModel() const override { + return model; + } + + /** + * tx and rx aperture are expected to be provided in flattened format. + * + * @param seq + * @param tgcSamples + */ + std::tuple + setTxRxSequence(const std::vector &seq, + const ops::us4r::TGCCurve &tgcSamples, uint16 rxBufferSize, + uint16 rxBatchSize, std::optional sri) override; + + Interval getAcceptedVoltageRange() override; + + void start() override; + + void stop() override; + + void syncTrigger() override; + + void registerOutputBuffer(Us4ROutputBuffer *buffer, const Us4RBuffer::Handle &us4rBuffer) override; + + void setTgcCurve(const std::vector &tgcCurve) override; + +private: + Logger::Handle logger; + ProbeModel model; + ProbeAdapterImplBase::RawHandle adapter; + std::vector channelMapping; +}; + +} + +#endif //ARRUS_CORE_DEVICES_PROBE_PROBEIMPL_H diff --git a/arrus/core/devices/probe/ProbeImplBase.h b/arrus/core/devices/probe/ProbeImplBase.h new file mode 100644 index 000000000..94e6b8d9a --- /dev/null +++ b/arrus/core/devices/probe/ProbeImplBase.h @@ -0,0 +1,28 @@ +#ifndef ARRUS_CORE_DEVICES_PROBE_PROBEIMPLBASE_H +#define ARRUS_CORE_DEVICES_PROBE_PROBEIMPLBASE_H + +#include "arrus/core/api/devices/probe/Probe.h" +#include "arrus/core/devices/us4r/Us4RBuffer.h" +#include "arrus/core/devices/UltrasoundDevice.h" + +namespace arrus::devices { + +class ProbeImplBase : public Probe, public UltrasoundDevice { +public: + using Handle = std::unique_ptr; + using RawHandle = ProbeImplBase*; + using Probe::Probe; + + virtual + std::tuple + setTxRxSequence(const std::vector &seq, + const ops::us4r::TGCCurve &tgcSamples, uint16 rxBufferSize, + uint16 rxBatchSize, std::optional sri) = 0; + + virtual + void registerOutputBuffer(Us4ROutputBuffer *, const Us4RBuffer::Handle&) = 0; +}; + +} + +#endif //ARRUS_CORE_DEVICES_PROBE_PROBEIMPLBASE_H diff --git a/arrus/core/devices/probe/ProbeModel.cpp b/arrus/core/devices/probe/ProbeModel.cpp new file mode 100644 index 000000000..1db21a53a --- /dev/null +++ b/arrus/core/devices/probe/ProbeModel.cpp @@ -0,0 +1,17 @@ +#include "ProbeModel.h" + +#include "arrus/common/format.h" + + +namespace arrus::devices { +std::ostream &operator<<(std::ostream &os, const ProbeModel &model) { + os << "modelId: " << model.getModelId().getName() << ", " + << model.getModelId().getManufacturer() + << " numberOfElements: " + << ::arrus::toString(model.getNumberOfElements()) + << " pitch: " << ::arrus::toString(model.getPitch()) + << " txFrequencyRange: " << ::arrus::toString(model.getTxFrequencyRange()); + return os; +} +} + diff --git a/arrus/core/devices/probe/ProbeModel.h b/arrus/core/devices/probe/ProbeModel.h new file mode 100644 index 000000000..368d458ab --- /dev/null +++ b/arrus/core/devices/probe/ProbeModel.h @@ -0,0 +1,10 @@ +#ifndef ARRUS_CORE_DEVICES_PROBE_PROBEMODEL_H +#define ARRUS_CORE_DEVICES_PROBE_PROBEMODEL_H + +#include "arrus/core/api/devices/probe/ProbeModel.h" + +namespace arrus::devices { +std::ostream &operator<<(std::ostream &os, const ProbeModel &model); +} + +#endif //ARRUS_CORE_DEVICES_PROBE_PROBEMODEL_H diff --git a/arrus/core/devices/probe/ProbeSettings.cpp b/arrus/core/devices/probe/ProbeSettings.cpp new file mode 100644 index 000000000..2a02eee56 --- /dev/null +++ b/arrus/core/devices/probe/ProbeSettings.cpp @@ -0,0 +1,15 @@ +#include "ProbeSettings.h" + +#include "arrus/common/format.h" +#include "arrus/core/devices/probe/ProbeModel.h" + +namespace arrus::devices { + +std::ostream & +operator<<(std::ostream &os, const ProbeSettings &settings) { + os << "model: " << settings.getModel() << " channelMapping: " + << ::arrus::toString(settings.getChannelMapping()); + return os; +} + +} diff --git a/arrus/core/devices/probe/ProbeSettings.h b/arrus/core/devices/probe/ProbeSettings.h new file mode 100644 index 000000000..2c67a11b2 --- /dev/null +++ b/arrus/core/devices/probe/ProbeSettings.h @@ -0,0 +1,10 @@ +#ifndef ARRUS_CORE_DEVICES_PROBE_PROBESETTINGS_H +#define ARRUS_CORE_DEVICES_PROBE_PROBESETTINGS_H + +#include "arrus/core/api/devices/probe/ProbeSettings.h" + +namespace arrus::devices { +std::ostream &operator<<(std::ostream &os, const ProbeSettings &settings); +} + +#endif //ARRUS_CORE_DEVICES_PROBE_PROBESETTINGS_H diff --git a/arrus/core/devices/probe/ProbeSettingsValidator.h b/arrus/core/devices/probe/ProbeSettingsValidator.h new file mode 100644 index 000000000..269438c02 --- /dev/null +++ b/arrus/core/devices/probe/ProbeSettingsValidator.h @@ -0,0 +1,54 @@ +#ifndef ARRUS_CORE_DEVICES_PROBE_PROBESETTINGSVALIDATOR_H +#define ARRUS_CORE_DEVICES_PROBE_PROBESETTINGSVALIDATOR_H + +#include "arrus/core/api/devices/probe/ProbeSettings.h" +#include "arrus/core/devices/SettingsValidator.h" + +namespace arrus::devices { + +class ProbeSettingsValidator : public SettingsValidator { + +public: + explicit ProbeSettingsValidator(const Ordinal ordinal) + : SettingsValidator(DeviceId(DeviceType::Probe, ordinal)) {} + + void validate(const ProbeSettings &obj) override { + // verify id + auto &id = obj.getModel().getModelId(); + expectTrue("modelId", !id.getManufacturer().empty(), + "manufacturer name should not be empty."); + expectTrue("modelId", !id.getName().empty(), + "device name should not be empty."); + + // verify other parameters + expectAtMost("numberOfElements", + obj.getModel().getNumberOfElements().size(), (size_t) 2, + "(size)"); + expectEqual("numberOfElements", + obj.getModel().getNumberOfElements().size(), + obj.getModel().getPitch().size(), + " (size, comparing with pitch)"); + // Validating model. + expectAllPositive("pitch", obj.getModel().getPitch().getValues()); + expectAllPositive( + "txFrequencyRange", + {obj.getModel().getTxFrequencyRange().start(), + obj.getModel().getTxFrequencyRange().end()}); + expectAllInRange( + "numberOfElements", + obj.getModel().getNumberOfElements().getValues(), + (ProbeModel::ElementIdxType) 1, + std::numeric_limits::max() + ); + expectEqual( + "nElements", + obj.getChannelMapping().size(), + (size_t) obj.getModel().getNumberOfElements().product() + ); + expectUnique("channelMapping", obj.getChannelMapping()); + } +}; + +} + +#endif //ARRUS_CORE_DEVICES_PROBE_PROBESETTINGSVALIDATOR_H diff --git a/arrus/core/devices/probe/ProbeSettingsValidatorTest.cpp b/arrus/core/devices/probe/ProbeSettingsValidatorTest.cpp new file mode 100644 index 000000000..49653e5e7 --- /dev/null +++ b/arrus/core/devices/probe/ProbeSettingsValidatorTest.cpp @@ -0,0 +1,123 @@ +#include +#include + +#include "arrus/common/format.h" +#include "arrus/core/common/tests.h" +#include "arrus/core/common/collections.h" +#include "arrus/core/devices/probe/ProbeSettingsValidator.h" + +namespace { +using namespace arrus; +using namespace arrus::devices; + +struct TestProbeSettings { + ProbeModelId modelId{"test", "test"}; + Tuple numberOfElements{192}; + Tuple pitch{0.3e-3}; + Interval txFrequencyRange = {1e6, 10e6}; + Interval voltageRange = {0, 90}; + std::vector channelMapping = + arrus::getRange(0, 192); + + [[nodiscard]] ProbeSettings toProbeSettings() const { + return ProbeSettings{ + ProbeModel{modelId, numberOfElements, pitch, txFrequencyRange, voltageRange, 0.0}, + channelMapping}; + } + + friend std::ostream & + operator<<(std::ostream &os, const TestProbeSettings &settings) { + os << "modelId: " << settings.modelId + << " numberOfElements: " << toString(settings.numberOfElements) + << " pitch: " << toString(settings.pitch) + << " txFrequencyRange: " << toString(settings.txFrequencyRange) + << " channelMapping: " << toString(settings.channelMapping); + return os; + } +}; + +class CorrectProbeSettingsTest + : public testing::TestWithParam { +}; + +TEST_P(CorrectProbeSettingsTest, AcceptsCorrect) { + ProbeSettingsValidator validator(0); + TestProbeSettings val = GetParam(); + validator.validate(val.toProbeSettings()); + EXPECT_NO_THROW(validator.throwOnErrors()); + validator.throwOnErrors(); +} + +INSTANTIATE_TEST_CASE_P + +(ValidProbeSettings, CorrectProbeSettingsTest, + testing::Values( + // 1-D, all channels + TestProbeSettings{}, + // 1-D, subset of the underlying adapter is used + ARRUS_STRUCT_INIT_LIST(TestProbeSettings, ( + x.numberOfElements = {96}, + x.channelMapping = arrus::concat( + getRange(0, 48), + getRange(144, 192) + )) + ), + // 2-D, all channels are used + ARRUS_STRUCT_INIT_LIST(TestProbeSettings, ( + x.numberOfElements = {8, 8}, + x.pitch = {0.3e-3, 0.3e-3}, + x.channelMapping = getRange(0, 64) + )), + // 2-D, some of the channels are used + ARRUS_STRUCT_INIT_LIST(TestProbeSettings, ( + x.numberOfElements = {16, 16}, + x.pitch = {0.3e-3, 0.3e-3}, + x.channelMapping = arrus::concat( + getRange(0, 128), + getRange(512, 640) + ) + )) + )); + + +class IncorrectProbeSettingsTest + : public testing::TestWithParam { +}; + +TEST_P(IncorrectProbeSettingsTest, RejectsIncorrect) { + ProbeSettingsValidator validator(0); + TestProbeSettings val = GetParam(); + validator.validate(val.toProbeSettings()); + EXPECT_THROW(validator.throwOnErrors(), ::arrus::IllegalArgumentException); +} + +INSTANTIATE_TEST_CASE_P + +(InvalidProbeSettings, IncorrectProbeSettingsTest, + testing::Values( + // Testing probe model + // - Negative pitch + ARRUS_STRUCT_INIT_LIST(TestProbeSettings, ( + x.pitch = {-.3e-3} + )), + // - Negative frequency + ARRUS_STRUCT_INIT_LIST(TestProbeSettings, ( + x.txFrequencyRange = {-10e6, 10e6} + )), + // - Invalid number of channels in mapping + ARRUS_STRUCT_INIT_LIST(TestProbeSettings, ( + x.numberOfElements = {96}, + x.channelMapping = getRange(0, 192) + )), + // - Non-unique channels + ARRUS_STRUCT_INIT_LIST(TestProbeSettings, ( + x.numberOfElements = {32}, + x.channelMapping = ::arrus::concat( + getRange(0, 2), + getRange(0, 30) + ) + )) + )); + +} + diff --git a/arrus/core/devices/us4r/DataTransfer.h b/arrus/core/devices/us4r/DataTransfer.h new file mode 100644 index 000000000..1b4ce527e --- /dev/null +++ b/arrus/core/devices/us4r/DataTransfer.h @@ -0,0 +1,42 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_DATATRANSFER_H +#define ARRUS_CORE_DEVICES_US4R_DATATRANSFER_H + +#include + +namespace arrus::devices { + +class DataTransfer { +public: + DataTransfer(std::function transferFunc, + size_t size, size_t srcAddress, + uint16 firing) + : transferFunc(std::move(transferFunc)), + size(size), srcAddress(srcAddress), + firing(firing) {} + + [[nodiscard]] const std::function &getTransferFunc() const { + return transferFunc; + } + + [[nodiscard]] size_t getSize() const { + return size; + } + + [[nodiscard]] size_t getSrcAddress() const { + return srcAddress; + } + + [[nodiscard]] uint16 getFiring() const { + return firing; + } + +private: + std::function transferFunc; + size_t size; + size_t srcAddress; + uint16 firing; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_DATATRANSFER_H diff --git a/arrus/core/devices/us4r/FrameChannelMappingImpl.cpp b/arrus/core/devices/us4r/FrameChannelMappingImpl.cpp new file mode 100644 index 000000000..645f374c9 --- /dev/null +++ b/arrus/core/devices/us4r/FrameChannelMappingImpl.cpp @@ -0,0 +1,61 @@ +#include "FrameChannelMappingImpl.h" + +#include + +#include "arrus/common/asserts.h" +#include "arrus/core/api/common/exceptions.h" + +namespace arrus::devices { + +FrameChannelMappingImpl::FrameChannelMappingImpl(FrameMapping &frameMapping, + ChannelMapping &channelMapping) + : frameMapping(std::move(frameMapping)), channelMapping(std::move(channelMapping)) { + + ARRUS_REQUIRES_TRUE_E(frameMapping.rows() == channelMapping.rows() + && frameMapping.cols() == channelMapping.cols(), + ArrusException("Frame and channel mapping arrays should have the " + "same shape")); +} + +std::pair +FrameChannelMappingImpl::getLogical(FrameNumber frame, ChannelIdx channel) { + auto physicalFrame = frameMapping(frame, channel); + auto physicalChannel = channelMapping(frame, channel); + return {physicalFrame, physicalChannel}; +} + +FrameChannelMapping::FrameNumber FrameChannelMappingImpl::getNumberOfLogicalFrames() { + assert(frameMapping.rows() >= 0 + && frameMapping.rows() <= std::numeric_limits::max()); + return static_cast(frameMapping.rows()); +} + +ChannelIdx FrameChannelMappingImpl::getNumberOfLogicalChannels() { + assert(frameMapping.cols() >= 0 + && frameMapping.cols() <= std::numeric_limits::max()); + return static_cast(frameMapping.cols()); +} + +FrameChannelMappingImpl::~FrameChannelMappingImpl() = default; + +void +FrameChannelMappingBuilder::setChannelMapping(FrameNumber logicalFrame, ChannelIdx logicalChannel, + FrameNumber physicalFrame, int8 physicalChannel) { + frameMapping(logicalFrame, logicalChannel) = physicalFrame; + channelMapping(logicalFrame, logicalChannel) = physicalChannel; +} + +FrameChannelMappingImpl::Handle FrameChannelMappingBuilder::build() { + return std::make_unique(this->frameMapping, this->channelMapping); +} + +FrameChannelMappingBuilder::FrameChannelMappingBuilder(FrameNumber nFrames, ChannelIdx nChannels) + : frameMapping(FrameChannelMappingImpl::FrameMapping(nFrames, nChannels)), + channelMapping(FrameChannelMappingImpl::ChannelMapping(nFrames, nChannels)) { + // Creates empty frame mapping. + frameMapping.fill(0); + channelMapping.fill(FrameChannelMapping::UNAVAILABLE); +} + +} + diff --git a/arrus/core/devices/us4r/FrameChannelMappingImpl.h b/arrus/core/devices/us4r/FrameChannelMappingImpl.h new file mode 100644 index 000000000..3a1466d6b --- /dev/null +++ b/arrus/core/devices/us4r/FrameChannelMappingImpl.h @@ -0,0 +1,63 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_FRAMECHANNELMAPPINGIMPL_H +#define ARRUS_CORE_DEVICES_US4R_FRAMECHANNELMAPPINGIMPL_H + +#include +#include +#include + +#include "arrus/core/api/devices/us4r/FrameChannelMapping.h" + + +namespace arrus::devices { + +class FrameChannelMappingImpl : public FrameChannelMapping { +public: + using Handle = std::unique_ptr; + using FrameMapping = Eigen::Matrix; + using ChannelMapping = Eigen::Matrix; + + /** + * Takes ownership for the provided frames. + */ + FrameChannelMappingImpl(FrameMapping &frameMapping, ChannelMapping &channelMapping); + + /** + * @param frame logical frame to acquire + * @param channel channel in the logical frame to acquire + * @return frame and channel number of the physical signal data (the one returned by us4r device) + */ + std::pair getLogical(FrameNumber frame, ChannelIdx channel) override; + + FrameNumber getNumberOfLogicalFrames() override; + + ChannelIdx getNumberOfLogicalChannels() override; + + ~FrameChannelMappingImpl() override; + +private: + // logical (frame, number) -> physical (frame, number) + FrameMapping frameMapping; + ChannelMapping channelMapping; +}; + +class FrameChannelMappingBuilder { +public: + using FrameNumber = FrameChannelMapping::FrameNumber; + + FrameChannelMappingBuilder(FrameNumber nFrames, ChannelIdx nChannels); + + void setChannelMapping(FrameNumber logicalFrame, ChannelIdx logicalChannel, + FrameNumber physicalFrame, int8 physicalChannel); + + FrameChannelMappingImpl::Handle build(); + +private: + // logical (frame, number) -> physical (frame, number) + FrameChannelMappingImpl::FrameMapping frameMapping; + FrameChannelMappingImpl::ChannelMapping channelMapping; +}; + +} + + +#endif //ARRUS_CORE_DEVICES_US4R_FRAMECHANNELMAPPINGIMPL_H diff --git a/arrus/core/devices/us4r/RxSettings.cpp b/arrus/core/devices/us4r/RxSettings.cpp new file mode 100644 index 000000000..65c872dad --- /dev/null +++ b/arrus/core/devices/us4r/RxSettings.cpp @@ -0,0 +1,19 @@ +#include "RxSettings.h" + +#include "arrus/common/format.h" + +namespace arrus::devices { + +std::ostream & +operator<<(std::ostream &os, const RxSettings &settings) { + os << "dtgcAttenuation: " << ::arrus::toString(settings.getDtgcAttenuation()) + << " pgaGain: " << settings.getPgaGain() + << " lnaGain: " << settings.getLnaGain() + << " tgcSamples: " << ::arrus::toString(settings.getTgcSamples()) + << " lpfCutoff: " << settings.getLpfCutoff() + << " activeTermination: " << ::arrus::toString(settings.getActiveTermination()); + return os; +} + +} + diff --git a/arrus/core/devices/us4r/RxSettings.h b/arrus/core/devices/us4r/RxSettings.h new file mode 100644 index 000000000..82d02a285 --- /dev/null +++ b/arrus/core/devices/us4r/RxSettings.h @@ -0,0 +1,12 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_RXSETTINGS_H +#define ARRUS_CORE_DEVICES_US4R_RXSETTINGS_H + +#include "arrus/core/api/devices/us4r/RxSettings.h" + +namespace arrus::devices { + +std::ostream &operator<<(std::ostream &os, const RxSettings &settings); + +} + +#endif //ARRUS_CORE_DEVICES_US4R_RXSETTINGS_H diff --git a/arrus/core/devices/us4r/Us4RBuffer.h b/arrus/core/devices/us4r/Us4RBuffer.h new file mode 100644 index 000000000..c6d599386 --- /dev/null +++ b/arrus/core/devices/us4r/Us4RBuffer.h @@ -0,0 +1,104 @@ +#ifndef ARRUS_ARRUS_CORE_DEVICES_US4R_US4RBUFFER_H +#define ARRUS_ARRUS_CORE_DEVICES_US4R_US4RBUFFER_H + +#include +#include +#include + +#include "arrus/core/devices/us4r/us4oem/Us4OEMBuffer.h" + +namespace arrus::devices { + +class Us4RBufferElement { +public: + explicit Us4RBufferElement(std::vector us4OemElements) + : us4oemElements(std::move(us4OemElements)) {} + + [[nodiscard]] const Us4OEMBufferElement &getUs4oemElement(const size_t ordinal) const { + return us4oemElements[ordinal]; + } + + [[nodiscard]] const std::vector &getUs4oemElements() const { + return us4oemElements; + } + + [[nodiscard]] size_t getNumberOfUs4oems() const { + return us4oemElements.size(); + } + +private: + std::vector us4oemElements; +}; + +class Us4RBuffer { +public: + using Handle = std::unique_ptr; + + explicit Us4RBuffer(std::vector elements) + : elements(std::move(elements)) {} + + [[nodiscard]] const Us4RBufferElement &getElement(const size_t i) const { + return elements[i]; + } + + [[nodiscard]] size_t getNumberOfElements() const { + return elements.size(); + } + + [[nodiscard]] bool empty() { + return elements.empty(); + } + + size_t getElementSize() { + size_t result = 0; + for(auto &element: elements[0].getUs4oemElements()) { + result += element.getSize(); + } + return result; + } + + [[nodiscard]] Us4OEMBuffer getUs4oemBuffer(Ordinal ordinal) const { + std::vector us4oemBufferElements; + for(const auto &element : elements) { + us4oemBufferElements.push_back(element.getUs4oemElement(ordinal)); + } + return Us4OEMBuffer(us4oemBufferElements); + } + +private: + std::vector elements; +}; + +class Us4RBufferBuilder { +public: + + void pushBackUs4oemBuffer(const Us4OEMBuffer &us4oemBuffer) { + if(!elements.empty() && elements.size() != us4oemBuffer.getNumberOfElements()) { + throw arrus::ArrusException("Each Us4OEM rx buffer should have the same number of elements."); + } + if(elements.empty()) { + elements = std::vector>(us4oemBuffer.getNumberOfElements()); + } + for(size_t i = 0; i < us4oemBuffer.getNumberOfElements(); ++i) { + elements[i].push_back(us4oemBuffer.getElement(i)); + } + } + + Us4RBuffer::Handle build() { + // Create buffer. + std::vector us4rElements; + for(auto &element : elements) { + us4rElements.emplace_back(element); + } + return std::make_unique(us4rElements); + } + +private: + // element number -> us4oem ordinal -> part of the buffer element + std::vector> elements; + +}; + +} + +#endif //ARRUS_ARRUS_CORE_DEVICES_US4R_US4RBUFFER_H diff --git a/arrus/core/devices/us4r/Us4RFactory.h b/arrus/core/devices/us4r/Us4RFactory.h new file mode 100644 index 000000000..c7b634b6e --- /dev/null +++ b/arrus/core/devices/us4r/Us4RFactory.h @@ -0,0 +1,19 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4RFACTORY_H +#define ARRUS_CORE_DEVICES_US4R_US4RFACTORY_H + +#include "arrus/core/api/devices/us4r/Us4R.h" +#include "arrus/core/api/devices/us4r/Us4RSettings.h" + +namespace arrus::devices { + +class Us4RFactory { +public: + using Handle = std::unique_ptr; + + virtual Us4R::Handle + getUs4R(Ordinal ordinal, const Us4RSettings &settings) = 0; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4RFACTORY_H diff --git a/arrus/core/devices/us4r/Us4RFactoryImpl.h b/arrus/core/devices/us4r/Us4RFactoryImpl.h new file mode 100644 index 000000000..4cf5d833d --- /dev/null +++ b/arrus/core/devices/us4r/Us4RFactoryImpl.h @@ -0,0 +1,198 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4RFACTORYIMPL_H +#define ARRUS_CORE_DEVICES_US4R_US4RFACTORYIMPL_H + +#include +#include +#include + + +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializer.h" +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterFactory.h" +#include "arrus/core/devices/probe/ProbeFactory.h" +#include "arrus/common/asserts.h" +#include "arrus/core/devices/us4r/Us4RFactory.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImpl.h" +#include "arrus/core/devices/us4r/Us4RImpl.h" +#include "arrus/core/devices/us4r/Us4RSettingsValidator.h" +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidator.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMFactory.h" + +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h" +#include "arrus/core/devices/us4r/hv/HV256Factory.h" +#include "arrus/core/devices/us4r/Us4RSettingsConverter.h" + +namespace arrus::devices { + +class Us4RFactoryImpl : public Us4RFactory { +public: + Us4RFactoryImpl(std::unique_ptr us4oemFactory, + std::unique_ptr adapterFactory, + std::unique_ptr probeFactory, + std::unique_ptr ius4oemFactory, + std::unique_ptr ius4oemInitializer, + std::unique_ptr us4RSettingsConverter, + std::unique_ptr hvFactory) + : ius4oemFactory(std::move(ius4oemFactory)), + ius4oemInitializer(std::move(ius4oemInitializer)), + us4oemFactory(std::move(us4oemFactory)), + us4RSettingsConverter(std::move(us4RSettingsConverter)), + probeAdapterFactory(std::move(adapterFactory)), + probeFactory(std::move(probeFactory)), + hvFactory(std::move(hvFactory)) {} + + + Us4R::Handle + getUs4R(Ordinal ordinal, const Us4RSettings &settings) override { + DeviceId id(DeviceType::Us4R, ordinal); + + // Validate us4r settings (general). + Us4RSettingsValidator validator(ordinal); + validator.validate(settings); + validator.throwOnErrors(); + + if(settings.getProbeAdapterSettings().has_value()) { + // Probe, Adapter -> Us4OEM settings. + // Adapter + auto &probeAdapterSettings = + settings.getProbeAdapterSettings().value(); + ProbeAdapterSettingsValidator adapterValidator(0); + adapterValidator.validate(probeAdapterSettings); + adapterValidator.throwOnErrors(); + // Probe + auto &probeSettings = + settings.getProbeSettings().value(); + // TODO validate probe settings + auto &rxSettings = + settings.getRxSettings().value(); + // Rx settings will be validated by a specific device + // (Us4OEMs validator) + + + // Convert to Us4OEM settings + auto[us4OEMSettings, adapterSettings] = + us4RSettingsConverter->convertToUs4OEMSettings( + probeAdapterSettings, probeSettings, rxSettings, + settings.getChannelsMask()); + + // verify if the generated us4oemSettings.channelsMask is equal to us4oemChannelsMask field + validateChannelsMasks(us4OEMSettings, settings.getUs4OEMChannelsMask()); + + auto[us4oems, masterIUs4OEM] = getUs4OEMs(us4OEMSettings); + std::vector us4oemPtrs(us4oems.size()); + std::transform( + std::begin(us4oems), std::end(us4oems), + std::begin(us4oemPtrs), + [](const Us4OEMImplBase::Handle &ptr) { return ptr.get(); }); + // Create adapter. + ProbeAdapterImplBase::Handle adapter = + probeAdapterFactory->getProbeAdapter(adapterSettings, + us4oemPtrs); + // Create probe. + ProbeImplBase::Handle probe = probeFactory->getProbe(probeSettings, + adapter.get()); + + auto hv = getHV(settings.getHVSettings(), masterIUs4OEM); + return std::make_unique(id, std::move(us4oems), adapter, probe, std::move(hv)); + } else { + // Custom Us4OEMs only + auto[us4oems, masterIUs4OEM] = getUs4OEMs(settings.getUs4OEMSettings()); + auto hv = getHV(settings.getHVSettings(), masterIUs4OEM); + return std::make_unique(id, std::move(us4oems), std::move(hv)); + } + } + +private: + + void validateChannelsMasks(const std::vector &us4oemSettings, + const std::vector> &us4oemChannelsMasks) { + ARRUS_REQUIRES_TRUE_E( + us4oemSettings.size() == us4oemChannelsMasks.size(), + ::arrus::IllegalArgumentException( + ::arrus::format("There should be exactly {} us4oem channels masks " + "in the system configuration.", us4oemSettings.size()) + ) + ); + + for(int i = 0; i < us4oemSettings.size(); ++i) { + auto &setting = us4oemSettings[i]; + + std::unordered_set us4oemMask( + std::begin(us4oemChannelsMasks[i]), std::end(us4oemChannelsMasks[i])); + + ARRUS_REQUIRES_TRUE_E( + setting.getChannelsMask() == us4oemMask, + ::arrus::IllegalArgumentException( + ::arrus::format( + "The provided us4r channels masks does not match the provided us4oem channels masks, " + "for us4oem {}", i)) + ); + } + } + + + /** + * @return a pair: us4oems, master ius4oem + */ + std::pair, IUs4OEM *> + getUs4OEMs(const std::vector &us4oemCfgs) { + ARRUS_REQUIRES_AT_LEAST(us4oemCfgs.size(), 1, + "At least one us4oem should be configured."); + auto nUs4oems = static_cast(us4oemCfgs.size()); + + // Initialize Us4OEMs. + // We need to initialize Us4OEMs on a Us4R system level. + // This is because Us4OEM initialization procedure needs to consider + // existence of some master module (by default it's the 'Us4OEM:0'). + // Check the initializeModules function to see why. + std::vector ius4oems = + ius4oemFactory->getModules(nUs4oems); + + // Modifies input list - sorts ius4oems by ID in ascending order. + ius4oemInitializer->initModules(ius4oems); + auto master = ius4oems[0].get(); + + // Create Us4OEMs. + Us4RImpl::Us4OEMs us4oems; + ARRUS_REQUIRES_EQUAL(ius4oems.size(), us4oemCfgs.size(), + ArrusException( + "Values are not equal: ius4oem size, " + "us4oem settings size")); + + for(unsigned i = 0; i < ius4oems.size(); ++i) { + us4oems.push_back( + us4oemFactory->getUs4OEM( + static_cast(i), + ius4oems[i], us4oemCfgs[i]) + ); + } + return {std::move(us4oems), master}; + } + + std::optional getHV(const std::optional &settings, IUs4OEM *master) { + if(settings.has_value()) { + const auto &hvSettings = settings.value(); + auto &manufacturer = hvSettings.getModelId().getManufacturer(); + auto &name = hvSettings.getModelId().getName(); + ARRUS_REQUIRES_EQUAL(name, "hv256", IllegalArgumentException( + ::arrus::format("Only us4us HV256 is supported only (got {})", name))); + ARRUS_REQUIRES_EQUAL(manufacturer, "us4us", IllegalArgumentException( + ::arrus::format("Only us4us HV256 is supported only (got {})", name))); + + return hvFactory->getHV256(hvSettings, master); + } else { + return std::nullopt; + } + } + + std::unique_ptr ius4oemFactory; + std::unique_ptr ius4oemInitializer; + std::unique_ptr us4oemFactory; + std::unique_ptr us4RSettingsConverter; + std::unique_ptr probeAdapterFactory; + std::unique_ptr probeFactory; + std::unique_ptr hvFactory; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4RFACTORYIMPL_H diff --git a/arrus/core/devices/us4r/Us4RFactoryImplTest.cpp b/arrus/core/devices/us4r/Us4RFactoryImplTest.cpp new file mode 100644 index 000000000..100ee322f --- /dev/null +++ b/arrus/core/devices/us4r/Us4RFactoryImplTest.cpp @@ -0,0 +1,2 @@ +#include + diff --git a/arrus/core/devices/us4r/Us4RImpl.cpp b/arrus/core/devices/us4r/Us4RImpl.cpp new file mode 100644 index 000000000..e86ae7ec7 --- /dev/null +++ b/arrus/core/devices/us4r/Us4RImpl.cpp @@ -0,0 +1,200 @@ +#include "Us4RImpl.h" + +#include +#include +#include + +namespace arrus::devices { + +using ::arrus::ops::us4r::TxRxSequence; +using ::arrus::ops::us4r::Tx; +using ::arrus::ops::us4r::Rx; +using ::arrus::ops::us4r::Pulse; + +UltrasoundDevice *Us4RImpl::getDefaultComponent() { + // NOTE! The implementation of this function determines + // validation behaviour of SetVoltage function. + // The safest option is to prefer using Probe only, + // with an option to choose us4oem + // (but the user has to specify it explicitly in settings). + // Currently there should be no option to set TxRxSequence + // on an adapter directly. + if(probe.has_value()) { + return probe.value().get(); + } else { + return us4oems[0].get(); + } +} + +Us4RImpl::Us4RImpl(const DeviceId &id, + Us4RImpl::Us4OEMs us4oems, + ProbeAdapterImplBase::Handle &probeAdapter, + ProbeImplBase::Handle &probe, + std::optional hv) + : Us4R(id), logger{getLoggerFactory()->getLogger()}, + us4oems(std::move(us4oems)), + probeAdapter(std::move(probeAdapter)), + probe(std::move(probe)), + hv(std::move(hv)) { + + INIT_ARRUS_DEVICE_LOGGER(logger, id.toString()); + +} + +void Us4RImpl::setVoltage(Voltage voltage) { + logger->log(LogSeverity::INFO, + ::arrus::format("Setting voltage {}", voltage)); + ARRUS_REQUIRES_TRUE(hv.has_value(), "No HV have been set."); + // Validate. + auto *device = getDefaultComponent(); + auto voltageRange = device->getAcceptedVoltageRange(); + + auto minVoltage = voltageRange.start(); + auto maxVoltage = voltageRange.end(); + + if(voltage < minVoltage || voltage > maxVoltage) { + throw IllegalArgumentException( + ::arrus::format("Unaccepted voltage '{}', " + "should be in range: [{}, {}]", + voltage, minVoltage, maxVoltage)); + } + hv.value()->setVoltage(voltage); +} + +void Us4RImpl::disableHV() { + logger->log(LogSeverity::INFO, "Disabling HV"); + ARRUS_REQUIRES_TRUE(hv.has_value(), "No HV have been set."); + hv.value()->disable(); +} + +std::pair +Us4RImpl::upload(const ops::us4r::TxRxSequence &seq, + unsigned short rxBufferNElements, unsigned short hostBufferNElements) { + + ARRUS_REQUIRES_EQUAL( + getDefaultComponent(), probe.value().get(), + ::arrus::IllegalArgumentException( + "Currently TxRx sequence upload is available for system with probes only.")); + + if((hostBufferNElements % rxBufferNElements) != 0) { + throw ::arrus::IllegalArgumentException( + ::arrus::format("The size of the host buffer {} must be equal or a multiple " + "of the size of the rx buffer {}.", hostBufferNElements, rxBufferNElements)); + } + std::unique_lock guard(deviceStateMutex); + + if(this->state == State::STARTED) { + throw ::arrus::IllegalStateException( + "The device is running, uploading sequence is forbidden."); + } + + auto[rxBuffer, fcm] = uploadSequence(seq, rxBufferNElements, 1); + + ARRUS_REQUIRES_TRUE(!rxBuffer->empty(), "Us4R Rx buffer cannot be empty."); + + auto &element = rxBuffer->getElement(0); + std::vector elementPartSizes(element.getNumberOfUs4oems(), 0); + std::transform( + std::begin(element.getUs4oemElements()), + std::end(element.getUs4oemElements()), + std::begin(elementPartSizes), + [](const Us4OEMBufferElement& transfer) { + return transfer.getSize(); + }); + + // This might be quite time consuming operation - it might be good idea to + // check if the buffer properties has changed and reuse the old buffer + // if possible. + if(this->buffer) { + this->buffer.reset(); + } + this->buffer = std::make_shared( + elementPartSizes, hostBufferNElements); + getProbeImpl()->registerOutputBuffer(this->buffer.get(), rxBuffer); + return {this->buffer, std::move(fcm)}; +} + +void Us4RImpl::start() { + std::unique_lock guard(deviceStateMutex); + logger->log(LogSeverity::INFO, "Starting us4r."); + if(this->buffer == nullptr) { + throw ::arrus::IllegalArgumentException("Call upload function first."); + } + if(this->state == State::STARTED) { + throw ::arrus::IllegalStateException("Device is already running."); + } + this->buffer->resetState(); + this->getDefaultComponent()->start(); + this->state = State::STARTED; +} + +void Us4RImpl::stop() { + this->stopDevice(); +} + +void Us4RImpl::stopDevice() { + std::unique_lock guard(deviceStateMutex); + if(this->state != State::STARTED) { + logger->log(LogSeverity::INFO, "Device Us4R is already stopped."); + } + else { + logger->log(LogSeverity::DEBUG, "Stopping system."); + this->getDefaultComponent()->stop(); + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + logger->log(LogSeverity::DEBUG, "Stopped."); + } + if(this->buffer != nullptr) { + this->buffer->shutdown(); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + this->state = State::STOPPED; +} + +Us4RImpl::~Us4RImpl() { + getDefaultLogger()->log(LogSeverity::DEBUG, + "Closing connection with Us4R."); + this->stopDevice(); + getDefaultLogger()->log(LogSeverity::INFO, "Connection to Us4R closed."); +} + +std::tuple +Us4RImpl::uploadSequence(const ops::us4r::TxRxSequence &seq, + uint16_t rxBufferSize, uint16_t rxBatchSize) { + std::vector actualSeq; + // Convert to intermediate representation (TxRxParameters). + size_t opIdx = 0; + for(const auto &txrx : seq.getOps()) { + auto &tx = txrx.getTx(); + auto &rx = txrx.getRx(); + + Interval sampleRange(rx.getSampleRange().first, rx.getSampleRange().second); + Tuple padding({rx.getPadding().first, rx.getPadding().second}); + + actualSeq.push_back( + TxRxParameters( + tx.getAperture(), + tx.getDelays(), + tx.getExcitation(), + rx.getAperture(), + sampleRange, + rx.getDownsamplingFactor(), + txrx.getPri(), + padding) + ); + ++opIdx; + } + return getProbeImpl()->setTxRxSequence(actualSeq, seq.getTgcCurve(), rxBufferSize, rxBatchSize, seq.getSri()); + +} + +void Us4RImpl::syncTrigger() { + this->getDefaultComponent()->syncTrigger(); +} + +void Us4RImpl::setTgcCurve(const std::vector &tgcCurvePoints) { + this->getDefaultComponent()->setTgcCurve(tgcCurvePoints); +} + + + +} \ No newline at end of file diff --git a/arrus/core/devices/us4r/Us4RImpl.h b/arrus/core/devices/us4r/Us4RImpl.h new file mode 100644 index 000000000..39fddc39e --- /dev/null +++ b/arrus/core/devices/us4r/Us4RImpl.h @@ -0,0 +1,146 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4RIMPL_H +#define ARRUS_CORE_DEVICES_US4R_US4RIMPL_H + +#include +#include + +#include +#include + +#include "arrus/common/asserts.h" +#include "arrus/core/devices/utils.h" +#include "arrus/core/api/common/exceptions.h" +#include "arrus/core/api/devices/us4r/Us4R.h" +#include "arrus/core/api/devices/DeviceWithComponents.h" +#include "arrus/core/common/logging.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImpl.h" +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterImplBase.h" +#include "arrus/core/devices/probe/ProbeImplBase.h" +#include "arrus/core/devices/us4r/hv/HV256Impl.h" +#include "arrus/core/devices/us4r/Us4RBuffer.h" + +namespace arrus::devices { + +class Us4RImpl : public Us4R { +public: + using Us4OEMs = std::vector; + + enum class State { + STARTED, STOPPED + }; + + ~Us4RImpl() override; + + Us4RImpl(const DeviceId &id, Us4OEMs us4oems, std::optional hv) + : Us4R(id), us4oems(std::move(us4oems)), hv(std::move(hv)) {} + + Us4RImpl(const DeviceId &id, + Us4OEMs us4oems, + ProbeAdapterImplBase::Handle &probeAdapter, + ProbeImplBase::Handle &probe, + std::optional hv); + + Us4RImpl(Us4RImpl const &) = delete; + + Us4RImpl(Us4RImpl const &&) = delete; + + Device::RawHandle getDevice(const std::string &path) override { + auto[root, tail] = getPathRoot(path); + boost::algorithm::trim(root); + boost::algorithm::trim(tail); + if(!tail.empty()) { + throw IllegalArgumentException( + arrus::format( + "Us4R devices allows access only to the top-level " + "devices (got relative path: '{}')", path) + ); + } + DeviceId componentId = DeviceId::parse(root); + return getDevice(componentId); + } + + Device::RawHandle getDevice(const DeviceId &deviceId) { + auto ordinal = deviceId.getOrdinal(); + switch(deviceId.getDeviceType()) { + case DeviceType::Us4OEM: + return getUs4OEM(ordinal); + case DeviceType::ProbeAdapter: + return getProbeAdapter(ordinal); + case DeviceType::Probe: + return getProbe(ordinal); + default: + throw DeviceNotFoundException(deviceId); + } + } + + Us4OEM::RawHandle getUs4OEM(Ordinal ordinal) override { + if(ordinal >= us4oems.size()) { + throw DeviceNotFoundException( + DeviceId(DeviceType::Us4OEM, ordinal)); + } + return us4oems.at(ordinal).get(); + } + + + ProbeAdapter::RawHandle getProbeAdapter(Ordinal ordinal) override { + if(ordinal > 0 || !probeAdapter.has_value()) { + throw DeviceNotFoundException( + DeviceId(DeviceType::ProbeAdapter, ordinal)); + } + return probeAdapter.value().get(); + } + + Probe::RawHandle getProbe(Ordinal ordinal) override { + if(ordinal > 0 || !probe.has_value()) { + throw DeviceNotFoundException( + DeviceId(DeviceType::Probe, ordinal)); + } + return probe.value().get(); + } + + std::pair< + std::shared_ptr, + std::shared_ptr + > + upload(const ops::us4r::TxRxSequence &seq, + unsigned short rxBufferNElements, unsigned short hostBufferNElements) override; + + void start() override; + + void stop() override; + + void setVoltage(Voltage voltage); + + void disableHV(); + + void setTgcCurve(const std::vector &tgcCurvePoints) override; + +private: + std::mutex deviceStateMutex; + Logger::Handle logger; + Us4OEMs us4oems; + std::optional probeAdapter; + std::optional probe; + std::optional hv; + // will be used outside + // TODO extract output buffer to some external class + std::shared_ptr buffer; + State state{State::STOPPED}; + UltrasoundDevice *getDefaultComponent(); + + void stopDevice(); + + void syncTrigger(); + + std::tuple + uploadSequence(const ops::us4r::TxRxSequence &seq, uint16_t rxBufferSize, + uint16_t rxBatchSize); + + ProbeImplBase::RawHandle getProbeImpl() { + return probe.value().get(); + } +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4RIMPL_H diff --git a/arrus/core/devices/us4r/Us4ROutputBuffer.h b/arrus/core/devices/us4r/Us4ROutputBuffer.h new file mode 100644 index 000000000..643d018f5 --- /dev/null +++ b/arrus/core/devices/us4r/Us4ROutputBuffer.h @@ -0,0 +1,304 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4ROUTPUTBUFFER_H +#define ARRUS_CORE_DEVICES_US4R_US4ROUTPUTBUFFER_H + +#include +#include +#include +#include + +#include "arrus/core/api/devices/us4r/HostBuffer.h" +#include "arrus/core/api/common/types.h" +#include "arrus/core/api/common/exceptions.h" +#include "arrus/common/asserts.h" +#include "arrus/common/format.h" +#include "arrus/core/common/logging.h" + + +namespace arrus::devices { + +/** + * Us4R system's output circular FIFO buffer. + * + * The buffer has the following relationships: + * - buffer contains **elements** + * - the **element** is filled by many us4oems (with given ordinal) + * + * An example of the element is a single RF frame required to reconstruct + * a single b-mode image. + * + * The state of each buffer element is determined by the field `accumulators: + * - accumulators[element] == 0 means that the buffer element was processed and is ready for new data from the producer. + * - accumulators[element] > 0 && accumulators[element] != filledAccumulator means that the buffer element is partially confirmed by some of us4oems + * - accumulators[element] == filledAccumulator means that the buffer element is ready to be processed by a consumer. + */ +class Us4ROutputBuffer: public HostBuffer { +public: + static constexpr size_t DATA_ALIGNMENT = 4096; + using AccumulatorType = uint16; + + /** + * Buffer's constructor. + * + * @param us4oemOutputSizes number of samples to allocate for each of the + * us4oem output. That is, the i-th element describes how many bytes will + * be written by i-th us4oem. + */ + Us4ROutputBuffer(const std::vector &us4oemOutputSizes, uint16 nElements) + : elementSize(0), nElements(nElements), tailIdx(0), + currentNumberOfElements(0), + accumulators(nElements), isAccuClear(nElements), + us4oemPositions(us4oemOutputSizes.size()), + filledAccumulator((1ul << (size_t) us4oemOutputSizes.size()) - 1) { + + this->initialize(); + // Buffer allocation. + ARRUS_REQUIRES_TRUE(us4oemOutputSizes.size() <= 16, + "Currently Us4R data buffer supports up to 16 us4oem modules."); + size_t us4oemOffset = 0; + + Ordinal us4oemOrdinal = 0; + for(auto s : us4oemOutputSizes) { + this->us4oemOffsets.emplace_back(us4oemOffset); + us4oemOffset += s; + if(s == 0) { + // We should not expect any response from modules, do not acquire any data. + filledAccumulator &= ~(1ul << us4oemOrdinal); + } + ++us4oemOrdinal; + } + elementSize = us4oemOffset; + dataBuffer = reinterpret_cast(operator new[](elementSize * nElements, std::align_val_t(DATA_ALIGNMENT))); + getDefaultLogger()->log(LogSeverity::DEBUG, ::arrus::format("Allocated {} ({}, {}) bytes of memory, address: {}", + elementSize*nElements, elementSize, nElements, (size_t)dataBuffer)); + } + + ~Us4ROutputBuffer() override { + ::operator delete(dataBuffer, std::align_val_t(DATA_ALIGNMENT)); + getDefaultLogger()->log(LogSeverity::DEBUG, "Released the output buffer."); + } + + [[nodiscard]] uint16 getNumberOfElements() const override { + return nElements; + } + + int16 *getElement(size_t elementNumber) override { + return reinterpret_cast(reinterpret_cast(dataBuffer) + elementNumber*elementSize); + } + + uint8 *getAddress(uint16 elementNumber, Ordinal us4oem) { + return reinterpret_cast(dataBuffer) + elementNumber*elementSize + us4oemOffsets[us4oem]; + } + + /** + * Returns a total size of the buffer, the number of **uint16** values. + */ + [[nodiscard]] size_t getElementSize() const override { + return elementSize; + } + + /** + * Signals the readiness of new data acquired by the n-th Us4OEM module. + * + * This function should be called by us4oem interrupt callbacks. + * + * @param n us4oem ordinal number + * @param timeout number of milliseconds the thread will wait for + * accumulator or queue clearance (each separately), 0 means no timeout + * + * @throws TimeoutException when accumulator clearance of push operation + * reaches the timeout + * @return true if the buffer signal was successful, false otherwise (e.g. the queue was shut down). + */ + bool signal(Ordinal n, int firing, long long timeout = -1) { + std::unique_lock guard(mutex); + if(this->state != State::RUNNING) { + getDefaultLogger()->log(LogSeverity::TRACE, "Signal queue shutdown."); + return false; + } + auto &accumulator = accumulators[firing]; + getDefaultLogger()->log(LogSeverity::TRACE, + ::arrus::format("Signal, position: {}, accumulator: {}", firing, accumulator)); + + while(accumulator & (1ul << n)) { + // wait till the bit will be cleared + getDefaultLogger()->log( + LogSeverity::TRACE, + arrus::format("Us4OEM:{} signal thread is waiting for accumulator clearance: {}", n, firing)); + ARRUS_WAIT_FOR_CV_OPTIONAL_TIMEOUT( + isAccuClear[firing], guard, timeout, + ::arrus::format("Us4OEM:{} Timeout while waiting for queue element clearance.", n)) + if(this->state != State::RUNNING) { + getDefaultLogger()->log(LogSeverity::TRACE, "Signal queue shutdown."); + return false; + } + } + accumulator |= 1ul << n; + bool isElementReady = (accumulator & filledAccumulator) == filledAccumulator; + if(isElementReady) { + guard.unlock(); + queueEmpty.notify_one(); + } + return true; + } + + /** + * This function just waits till the given element will be cleared by a consumer. + */ + bool waitForRelease(Ordinal n, int firing, long long timeout) { + std::unique_lock guard(mutex); + if(this->state != State::RUNNING) { + return false; + } + + auto &accumulator = accumulators[firing]; + + while(accumulator != 0) { + // wait till the bit will be cleared + getDefaultLogger()->log( + LogSeverity::TRACE, + arrus::format("Us4OEM:{} signal thread is waiting for accumulator clearance: {}", n, firing)); + ARRUS_WAIT_FOR_CV_OPTIONAL_TIMEOUT( + isAccuClear[firing], guard, timeout, + ::arrus::format("Us4OEM:{} Timeout while waiting for queue element clearance.", n)) + if(this->state != State::RUNNING) { + return false; + } + } + return true; + } + + /** + * Releases the front data from further data acquisition. + * + * This function should be called by data processing thread when + * the data is no more needed. + * + * @param timeout a number of milliseconds the thread will wait when + * the queue is empty; nullptr means no timeout. + */ + void releaseTail(long long timeout) override { + std::unique_lock guard(mutex); + validateState(); + auto releasedIdx = tailIdx; + while(accumulators[releasedIdx] != filledAccumulator) { + ARRUS_WAIT_FOR_CV_OPTIONAL_TIMEOUT( + queueEmpty, guard, timeout, + "Timeout while waiting for new data queue.") + validateState(); + } + accumulators[releasedIdx] = 0; + tailIdx = (tailIdx + 1) % nElements; + guard.unlock(); + isAccuClear[releasedIdx].notify_all(); + } + + /** + * Returns a pointer to the front element of the buffer. + * + * The method should be called by data processing thread. + * + * @param timeout a number of milliseconds the thread will wait when + * the input queue is empty; nullptr means no timeout. + * @return a pointer to the front of the queue + */ + short *tail(long long timeout) override { + std::unique_lock guard(mutex); + validateState(); + while(accumulators[tailIdx] != filledAccumulator) { + ARRUS_WAIT_FOR_CV_OPTIONAL_TIMEOUT( + queueEmpty, guard, timeout, + "Timeout while waiting for new data queue.") + validateState(); + } + return (int16*)((uint8*)dataBuffer + tailIdx * elementSize); + } + + void markAsInvalid() { + // TODO this function should have the "highest priority" possible + std::unique_lock guard(mutex); + this->state = State::INVALID; + guard.unlock(); + queueEmpty.notify_all(); + for(auto &cv: isAccuClear) { + cv.notify_all(); + } + } + + void shutdown() { + std::unique_lock guard(mutex); + this->state = State::SHUTDOWN; + guard.unlock(); + queueEmpty.notify_all(); + for(auto &cv: isAccuClear) { + cv.notify_all(); + } + } + + void resetState() { + this->state = State::INVALID; + this->initialize(); + this->state = State::RUNNING; + } + + void initialize() { + this->tailIdx = 0; + accumulators = std::vector(this->nElements); + isAccuClear = std::vector(this->nElements); + for(auto &pos : us4oemPositions) { + pos = 0; + } + } + + int16 *head(long long int) override { + throw ::arrus::ArrusException("Not implemented."); + } + +private: + size_t elementSize; + /** Us4OEM output address relative to the data buffer element address. */ + std::vector us4oemOffsets; + /** Total size in the number of elements. */ + uint16 nElements; + int16 *dataBuffer; + uint16 tailIdx; + /** Currently occupied size of the buffer. */ + uint16 currentNumberOfElements; + + std::condition_variable queueEmpty; + + std::mutex mutex; + // frame number -> accumulator + std::vector accumulators; + /** A pattern of the filled accumulator, which indicates that the + * whole element is ready. */ + AccumulatorType filledAccumulator; + // frame number -> condition variable to notify, that accu is clear + std::vector isAccuClear; + // us4oem module id -> current writing position for this us4oem + std::vector us4oemPositions; + + // State management + enum class State {RUNNING, SHUTDOWN, INVALID}; + State state{State::RUNNING}; + + /** + * Throws IllegalStateException when the buffer is in invalid state. + * + * @return true if the queue execution should continue, false otherwise. + */ + void validateState() { + if(this->state == State::INVALID) { + throw ::arrus::IllegalStateException( + "The buffer is in invalid state " + "(probably some data transfer overflow happened)."); + } + else if(this->state == State::SHUTDOWN) { + throw ::arrus::IllegalStateException( + "The data buffer has been turned off."); + } + } +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4ROUTPUTBUFFER_H diff --git a/arrus/core/devices/us4r/Us4ROutputBufferTest.cpp b/arrus/core/devices/us4r/Us4ROutputBufferTest.cpp new file mode 100644 index 000000000..11bbe2f7f --- /dev/null +++ b/arrus/core/devices/us4r/Us4ROutputBufferTest.cpp @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include +#include + +#include "arrus/core/common/tests.h" +#include "arrus/core/common/collections.h" +#include "arrus/core/api/common/types.h" +#include "arrus/common/logging/impl/Logging.h" +#include "arrus/core/devices/us4r/Us4ROutputBuffer.h" + +namespace { + +using namespace arrus; +using namespace arrus::devices; + +struct TestCase { + TestCase(Ordinal nus4Oems, uint16 nFrames, uint16 numberOfQueueElements, + double producerSleepTimeDispersion, double consumerSleepTimeDispersion) + : nus4oems(nus4Oems), nFrames(nFrames), + numberOfQueueElements(numberOfQueueElements), + producerSleepTimeDispersion(producerSleepTimeDispersion), + consumerSleepTimeDispersion(consumerSleepTimeDispersion) {} + + Ordinal nus4oems = 1; + uint16 nFrames = 10; + uint16 numberOfQueueElements = 3; + double producerSleepTimeDispersion = 0; + double consumerSleepTimeDispersion = 0; +}; + +class Us4ROutputBufferTest + : public testing::TestWithParam { +}; + +TEST_P(Us4ROutputBufferTest, TestSingleConsumerMultipleProducersWithCallback) { + // Tests + constexpr uint16 LOG_FREQ = 1000; + Ordinal nus4oems = GetParam().nus4oems; + uint16 nFrames = GetParam().nFrames; + uint16 nElements = GetParam().numberOfQueueElements; + constexpr uint32 N_SAMPLES = 64; + constexpr size_t OUTPUT_SIZE = N_SAMPLES * 32; //in number of array elements + + std::vector outputSizes = getNTimes(N_SAMPLES, nus4oems); + Us4ROutputBuffer buffer(outputSizes, nElements); + + // rng for time sleeps + double producerSleepTimeDisp = GetParam().producerSleepTimeDispersion; + double consumerSleepTimeDisp = GetParam().consumerSleepTimeDispersion; + + auto producer = [&](Ordinal n) { + std::random_device rd{}; + std::mt19937 rng{rd()}; + std::normal_distribution<> rDistr{0.0, 1.0}; + uint16 outputNumber = 0; + while(outputNumber < nFrames) { + if(producerSleepTimeDisp > 0.0) { + uint32 sleepTimeMs = std::abs(rDistr(rng)) * producerSleepTimeDisp * 1000; + std::this_thread::sleep_for(std::chrono::milliseconds(sleepTimeMs)); + } + try { + if(n == 0 && outputNumber % LOG_FREQ == 0) { + getDefaultLogger()->log( + arrus::LogSeverity::DEBUG, + ::arrus::format("Saving frame {}", outputNumber)); + } + auto func = [&] { + uint8 *data = buffer.getAddress( + (outputNumber % nElements), n); + for(size_t i = 0; i < OUTPUT_SIZE; ++i) { + data[i] = outputNumber; + } + ++outputNumber; + }; + buffer.signal(n, firing); + } catch(const std::exception &e) { + std::cerr << e.what() << std::endl; + } + } + }; + + // run threads + std::vector us4oems(nus4oems); + for(int i = 0; i < nus4oems; ++i) { + us4oems[i] = std::thread(producer, static_cast(i)); + } + + std::random_device rd{}; + std::mt19937 rng{rd()}; + std::normal_distribution<> rDistr{0.0, 1.0}; + uint16 frameNumber = 0; + while(frameNumber < nFrames) { + if(consumerSleepTimeDisp > 0.0) { + uint32 sleepTimeMs = std::abs(rDistr(rng)) * consumerSleepTimeDisp * 1000; + std::this_thread::sleep_for(std::chrono::milliseconds(sleepTimeMs)); + } + uint16 *d = buffer.tail(); + size_t size = buffer.getElementSize(); + if(frameNumber % LOG_FREQ == 0) { + getDefaultLogger()->log(arrus::LogSeverity::DEBUG, + ::arrus::format( + "Reading frame {}, size {}", + frameNumber, size)); + } + for(size_t i = 0; i < size; ++i) { + ASSERT_EQ(d[i], frameNumber); + } + ++frameNumber; + buffer.releaseTail(INFINITY); + } + + for(std::thread &us4oem: us4oems) { + us4oem.join(); + } +} +} +#define TEST_CASE_PARAMETERS_SET1(nus4oems) \ + TestCase(nus4oems, 20, 7, 0.1, 0), \ + TestCase(nus4oems, 20, 7, 0, 0.1), \ + TestCase(nus4oems, 20, 7, 0.1, 0.1), \ + TestCase(nus4oems, 1, 2, 0, 0), \ + TestCase(nus4oems, 100, 2, 0, 0), \ + TestCase(nus4oems, 10, 100, 0, 0), \ + TestCase(nus4oems, 100, 10, 0, 0), \ + TestCase(nus4oems, 10000, 2, 0, 0) + +INSTANTIATE_TEST_CASE_P +(SingleProducerCallback, Us4ROutputBufferTest, + testing::Values( + TEST_CASE_PARAMETERS_SET1(1) + )); + +INSTANTIATE_TEST_CASE_P +(TwoProducersCallback, Us4ROutputBufferTest, + testing::Values( + TEST_CASE_PARAMETERS_SET1(2) + )); + +INSTANTIATE_TEST_CASE_P +(ThreeProducersCallback, Us4ROutputBufferTest, + testing::Values( + TEST_CASE_PARAMETERS_SET1(3) + )); + +INSTANTIATE_TEST_CASE_P +(FourProducersCallback, Us4ROutputBufferTest, + testing::Values( + TEST_CASE_PARAMETERS_SET1(4) + )); + +INSTANTIATE_TEST_CASE_P +(EightProducersCallback, Us4ROutputBufferTest, + testing::Values( + TEST_CASE_PARAMETERS_SET1(8) +)); + +INSTANTIATE_TEST_CASE_P +(SixteenProducersCallback, Us4ROutputBufferTest, + testing::Values( + TEST_CASE_PARAMETERS_SET1(16) +)); + +int main(int argc, char **argv) { + ARRUS_INIT_TEST_LOG_LEVEL(arrus::Logging, LogSeverity::DEBUG); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + + diff --git a/arrus/core/devices/us4r/Us4RSettings.cpp b/arrus/core/devices/us4r/Us4RSettings.cpp new file mode 100644 index 000000000..2de5be9c5 --- /dev/null +++ b/arrus/core/devices/us4r/Us4RSettings.cpp @@ -0,0 +1,61 @@ +#include "Us4RSettings.h" + +#include "arrus/core/devices/us4r/us4oem/Us4OEMSettings.h" +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterSettings.h" +#include "arrus/core/devices/probe/ProbeSettings.h" +#include "arrus/core/devices/us4r/RxSettings.h" + +namespace arrus::devices { + +template +static inline void printOptionalValue(const std::optional value, + std::ostream &os) { + if(value.has_value()) { + os << value.value(); + } else { + os << "(no value)"; + }; +} + +std::ostream & +operator<<(std::ostream &os, const Us4RSettings &settings) { + os << "us4oemSettings: "; + int i = 0; + for(const auto &us4oemSetting : settings.getUs4OEMSettings()) { + os << "Us4OEM:" << i++ << ": "; + os << us4oemSetting << "; "; + } + + auto &probeAdapterSettings = settings.getProbeAdapterSettings(); + auto &probeSettings = settings.getProbeSettings(); + auto &rxSettings = settings.getRxSettings(); + auto &channelsMask = settings.getChannelsMask(); + auto &us4oemChannelsMasks = settings.getUs4OEMChannelsMask(); + + os << " probeAdapterSettings: "; + printOptionalValue(probeAdapterSettings, os); + os << " probeSettings: "; + printOptionalValue(probeSettings, os); + os << " rxSettings: "; + printOptionalValue(rxSettings, os); + + os << " channels mask: "; + for(auto channel : channelsMask) { + os << (int)channel << ", "; + } + + os << " us4oem channels mask: "; + i = 0; + for(const auto& vec : us4oemChannelsMasks) { + os << "us4oem " << i << ": "; + for(auto channel : vec) { + os << (int)channel << ", "; + } + ++i; + } + return os; +} + +} + + diff --git a/arrus/core/devices/us4r/Us4RSettings.h b/arrus/core/devices/us4r/Us4RSettings.h new file mode 100644 index 000000000..3c6513983 --- /dev/null +++ b/arrus/core/devices/us4r/Us4RSettings.h @@ -0,0 +1,14 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4RSETTINGS_H +#define ARRUS_CORE_DEVICES_US4R_US4RSETTINGS_H + +#include "arrus/core/api/devices/us4r/Us4RSettings.h" + +namespace arrus::devices { + +std::ostream &operator<<(std::ostream &os, const Us4RSettings &settings); + + +} + + +#endif //ARRUS_CORE_DEVICES_US4R_US4RSETTINGS_H diff --git a/arrus/core/devices/us4r/Us4RSettingsConverter.h b/arrus/core/devices/us4r/Us4RSettingsConverter.h new file mode 100644 index 000000000..0a23ca4b8 --- /dev/null +++ b/arrus/core/devices/us4r/Us4RSettingsConverter.h @@ -0,0 +1,34 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4RSETTINGSCONVERTER_H +#define ARRUS_CORE_DEVICES_US4R_US4RSETTINGSCONVERTER_H + + +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +#include "arrus/core/api/devices/probe/ProbeSettings.h" + +namespace arrus::devices { + +class Us4RSettingsConverter { +public: + /** + * Converts given Us4R settings to us4oem specific settings and + * appropriately remapped probe adapter settings. + * + * Use the returned settings when us4oems are connected in the us4r + * to given adapter and probe. + * + * Channels mask - a list of PROBE channels that has to be masked. + * Channel numbers starts from 0! + * This function converts the probe channels mask to us4oem channels masks. + */ + virtual + std::pair, ProbeAdapterSettings> + convertToUs4OEMSettings(const ProbeAdapterSettings &probeAdapterSettings, + const ProbeSettings &probeSettings, + const RxSettings &rxSettings, + const std::vector &channelsMask) = 0; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4RSETTINGSCONVERTER_H diff --git a/arrus/core/devices/us4r/Us4RSettingsConverterImpl.h b/arrus/core/devices/us4r/Us4RSettingsConverterImpl.h new file mode 100644 index 000000000..2fd61ef86 --- /dev/null +++ b/arrus/core/devices/us4r/Us4RSettingsConverterImpl.h @@ -0,0 +1,158 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4SETTINGSCONVERTERIMPL_H +#define ARRUS_CORE_DEVICES_US4R_US4SETTINGSCONVERTERIMPL_H + +#include "arrus/common/asserts.h" +#include "arrus/common/utils.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImpl.h" +#include "arrus/core/devices/us4r/Us4RSettingsConverter.h" + +namespace arrus::devices { + +class Us4RSettingsConverterImpl : public Us4RSettingsConverter { +public: + std::pair, ProbeAdapterSettings> + convertToUs4OEMSettings(const ProbeAdapterSettings &probeAdapterSettings, + const ProbeSettings &probeSettings, + const RxSettings &rxSettings, + const std::vector &channelsMask) override { + + // Assumption: + // for each module there is N_RX_CHANNELS*k elements in mapping + // each group of N_RX_CHANNELS contains elements grouped to a single bucket (i*32, (i+1)*32) + const auto &adapterSettingsMapping = + probeAdapterSettings.getChannelMapping(); + const auto &probeSettingsMapping = probeSettings.getChannelMapping(); + + // get number of us4oems from the probe adapter mapping + // Determined based on ADAPTER MAPPINGS + Ordinal nUs4OEMs = getNumberOfModules(adapterSettingsMapping); + // Convert to list of us4oem mappings and active channel groups + + auto const nRx = Us4OEMImpl::N_RX_CHANNELS; + auto const nTx = Us4OEMImpl::N_TX_CHANNELS; + auto const actChSize = Us4OEMImpl::ACTIVE_CHANNEL_GROUP_SIZE; + auto const nActChGroups = Us4OEMImpl::N_ACTIVE_CHANNEL_GROUPS; + + std::vector result; + std::vector currentRxGroup(nUs4OEMs); + std::vector currentRxGroupElement(nUs4OEMs); + + // physical mapping for us4oems + std::vector us4oemChannelMapping; + // logical mapping for adapter probe + ProbeAdapterSettings::ChannelMapping adapterChannelMapping; + + // Initialize mappings with 0, 1, 2, 3, ... 127 + for(int i = 0; i < (int) nUs4OEMs; ++i) { + Us4OEMSettings::ChannelMapping mapping(nTx); + for(ChannelIdx j = 0; j < nTx; ++j) { + mapping[j] = j; + } + us4oemChannelMapping.emplace_back(mapping); + } + // Map settings to: + // - internal us4oem mapping, + // - adapter channel mapping + for(auto[module, channel] : probeAdapterSettings.getChannelMapping()) { + // Channel mapping + const auto group = channel / nRx; + const auto element = currentRxGroupElement[module]; + if(element == 0) { + // Starting new group + currentRxGroup[module] = (ChannelIdx) group; + } else { + // Safety condition + ARRUS_REQUIRES_TRUE(group == currentRxGroup[module], + "Invalid probe adapter Rx channel mapping: " + "inconsistent groups of channel " + "(consecutive elements of N_RX_CHANNELS " + "are required)"); + } + auto logicalChannel = group * nRx + element; + us4oemChannelMapping[module][logicalChannel] = channel; + adapterChannelMapping.emplace_back(module, + ChannelIdx(logicalChannel)); + + currentRxGroupElement[module] = + (currentRxGroupElement[module] + 1) % nRx; + + } + + // Active channel groups for us4oems + std::vector activeChannelGroups; + // Initialize masks + for(Ordinal i = 0; i < nUs4OEMs; ++i) { + // all groups turned off + activeChannelGroups.emplace_back(nActChGroups); + } + for(const auto adapterChannel : probeSettingsMapping) { + auto[module, logicalChannel] = adapterChannelMapping[adapterChannel]; + auto us4oemChannel = us4oemChannelMapping[module][logicalChannel]; + // When at least one channel in group has mapping, the whole + // group of channels has to be active + activeChannelGroups[module][us4oemChannel / actChSize] = true; + } + + // CHANNELS MASKS PRODUCTION + // convert probe channels masks to adapter channels mask: + // - for each masked channel find the adapter's channel + // convert adapter channels mask to us4oem channels mask + // - for each masked channel n the adapter find module, channel + // (note that we need use here logical channels of the us4oem + // because we set tx apertures with logical channel (the physical channel is mapped by the IUs4OEM) + + std::vector> channelsMasks(nUs4OEMs); + + // probe channel -> adapter channel -> [module, us4oem LOGICAL channel] + for(const auto offProbeChannel : channelsMask) { + ARRUS_REQUIRES_TRUE_E(offProbeChannel < probeSettings.getModel().getNumberOfElements().product(), + ::arrus::IllegalArgumentException( + ::arrus::format("Channels mask element {} cannot exceed " + "the number of probe elements {}", + offProbeChannel, probeSettings.getModel(). + getNumberOfElements().product()) + )); + auto offAdapterChannel = probeSettingsMapping[offProbeChannel]; + auto[module, offUs4OEMLogicalChannel] = adapterChannelMapping[offAdapterChannel]; + channelsMasks[module].emplace(ARRUS_SAFE_CAST(offUs4OEMLogicalChannel, uint8)); + } + // END OF THE MASKS PRODUCTION + + + for(int i = 0; i < nUs4OEMs; ++i) { + result.push_back( + Us4OEMSettings( + us4oemChannelMapping[i], + activeChannelGroups[i], + rxSettings, + channelsMasks[i]) + ); + } + return {result, ProbeAdapterSettings( + probeAdapterSettings.getModelId(), + probeAdapterSettings.getNumberOfChannels(), + adapterChannelMapping + )}; + } + +private: + static Ordinal + getNumberOfModules( + const ProbeAdapterSettings::ChannelMapping &adapterMapping) { + std::vector mask( + std::numeric_limits::max()); + Ordinal count = 0; + for(auto[module, channel] : adapterMapping) { + if(!mask[module]) { + count++; + mask[module] = true; + } + } + return count; + } + +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4SETTINGSCONVERTERIMPL_H diff --git a/arrus/core/devices/us4r/Us4RSettingsConverterImplTest.cpp b/arrus/core/devices/us4r/Us4RSettingsConverterImplTest.cpp new file mode 100644 index 000000000..8eae87d3e --- /dev/null +++ b/arrus/core/devices/us4r/Us4RSettingsConverterImplTest.cpp @@ -0,0 +1,742 @@ +#include + +#include +#include + +#include "arrus/core/devices/us4r/Us4RSettingsConverterImpl.h" + +#include "arrus/core/common/tests.h" +#include "arrus/core/common/collections.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +#include "arrus/core/api/devices/probe/ProbeSettings.h" +#include "arrus/core/api/devices/us4r/RxSettings.h" + +namespace { + +using namespace arrus; +using namespace arrus::devices; + +using ChannelAddress = ProbeAdapterSettings::ChannelAddress; + +// -------- Mappings + +std::vector generateReversed(ChannelIdx a, ChannelIdx b) { + std::vector result; + for(int i = b-1; i >= a; --i) { + result.push_back(i); + } + return result; +} + +struct Mappings { + ProbeAdapterSettings::ChannelMapping adapterMapping; + + std::vector> expectedUs4OEMMappings; + ProbeAdapterSettings::ChannelMapping expectedAdapterMapping; + + friend std::ostream & + operator<<(std::ostream &os, const Mappings &mappings) { + os << "adapterMapping: "; + for(const auto &address : mappings.adapterMapping) { + os << "(" << address.first << ", " << address.second << ") "; + } + + os << "expectedOs4OEMMappings: "; + + int i = 0; + for(const auto &mapping: mappings.expectedUs4OEMMappings) { + os << "mapping for Us4OEM: " << i++ << " :"; + for(auto v : mapping) { + os << v << " "; + } + } + + os << "expectedAdapterMapping: "; + + for(const auto &address : mappings.expectedAdapterMapping) { + os << "(" << address.first << ", " << address.second << ") "; + } + return os; + } +}; + +class MappingsTest + : public testing::TestWithParam { +}; + +TEST_P(MappingsTest, CorrectlyConvertsMappingsToUs4OEMSettings) { + Us4RSettingsConverterImpl converter; + + Mappings mappings = GetParam(); + + ProbeAdapterSettings adapterSettings( + ProbeAdapterModelId("test", "test"), + mappings.adapterMapping.size(), + mappings.adapterMapping + ); + + ProbeSettings probeSettings( + ProbeModel(ProbeModelId("test", "test"), + {32}, + {0.3e-3}, {1e6, 10e6}, {0, 90}, 0.0), + getRange(0, 32) + ); + + RxSettings rxSettings({}, 24, 24, {}, 10e6, {}); + + auto[us4oemSettings, newAdapterSettings] = + converter.convertToUs4OEMSettings(adapterSettings, probeSettings, + rxSettings, std::vector()); + +// std::cerr << "output probe adapter settings: " << std::endl; +// for(auto [m, ch] : newAdapterSettings.getChannelMapping()) { +// std::cerr << "(" << m << "," << ch << ")"; +// } +// std::cerr << std::endl; + + EXPECT_EQ(us4oemSettings.size(), mappings.expectedUs4OEMMappings.size()); + + for(int i = 0; i < us4oemSettings.size(); ++i) { + EXPECT_EQ(us4oemSettings[i].getChannelMapping(), + mappings.expectedUs4OEMMappings[i]); + } + + EXPECT_EQ(newAdapterSettings.getChannelMapping(), + mappings.expectedAdapterMapping); +} + +INSTANTIATE_TEST_CASE_P + +(TestingMappings, MappingsTest, + testing::Values( + // NOTE: the assumption is here that unmapped us4oem channels + // are mapped by identity function. + // Two modules, 0: 0-32, 1: 0-32 + ARRUS_STRUCT_INIT_LIST(Mappings, ( + x.adapterMapping = generate(64, [](size_t i) { + return ChannelAddress{i / 32, i % 32}; + }), + x.expectedUs4OEMMappings = { + // 0: + getRange(0, 128), + // 1: + getRange(0, 128) + }, +// The same as the input adapter mapping + x.expectedAdapterMapping = + generate(64, [](size_t i) { + return ChannelAddress{i / 32, i % 32}; + }) + )), +// Two modules, 0: 0-128, 1: 0-64 + ARRUS_STRUCT_INIT_LIST(Mappings, ( + x.adapterMapping = generate(192, [](size_t i) { + return ChannelAddress{i / 128, i % 128}; + }), + x.expectedUs4OEMMappings = { + // 0: + getRange(0, 128), + // 1: + getRange(0, 128) + }, +// The same as the input adapter mapping + x.expectedAdapterMapping = + generate(192, [](size_t i) { + return ChannelAddress{i / 128, i % 128}; + }) + )), +// Two modules, 0: 0-31, 1: 63-32, 0: 64-95, 1: 127-96 +// Expected: us4oems: 0: 0-127, 1: 0-31, 63-32, 64-95, 127-96 +// Expected: probe adapter: 0: 0-31, 1: 32-63, 0: 64-95, 1: 96-127 + ARRUS_STRUCT_INIT_LIST(Mappings, ( + x.adapterMapping = generate(128, [](size_t i) { + Ordinal module = (i / 32) % 2; + ChannelIdx channel = i; + if(module == 1) { + channel = (i / 32 + 1) * 32 - 1 - (i % 32); + } + return ChannelAddress{module, channel}; + }), + x.expectedUs4OEMMappings = { + // 0: + getRange(0, 128), + // 1: + ::arrus::concat( + { + getRange(0, 32), + generateReversed(32, 64), + getRange(64, 96), + generateReversed(96, 128) + }) + }, +// The same as the input adapter mapping + x.expectedAdapterMapping = + generate(128, [](size_t i) { + Ordinal module = (i / 32) % 2; + ChannelIdx channel = i; + return ChannelAddress{module, channel}; + }) + )), +// Two modules, 0: 32-0, 1: 0-32, 0: 64-32, 1: 32-64 + ARRUS_STRUCT_INIT_LIST(Mappings, ( + x.adapterMapping = generate(128, [](size_t i) { + Ordinal module = (i / 32) % 2; + ChannelIdx channel = i; + if(module == 0) { + channel = (i / 32 / 2 + 1) * 32 - 1 - (i % 32); + } else { + channel = (i / 32 / 2) * 32 + (i % 32); + } + return ChannelAddress{module, channel}; + }), + x.expectedUs4OEMMappings = { + // 0: + ::arrus::concat( + { + generateReversed(0, 32), + generateReversed(32, 64), + getRange(64, 128) + }), + // 1: + getRange(0, 128) + + }, + x.expectedAdapterMapping = + generate(128, [](size_t i) { + Ordinal module = (i / 32) % 2; + ChannelIdx channel = (i / 32 / 2) * 32 + (i % 32); + return ChannelAddress{module, channel}; + }) + )), +// two modules, groups are shuffled for us4oem: 0: 64-32, 32-0 + ARRUS_STRUCT_INIT_LIST(Mappings, ( + x.adapterMapping = generate(128, [](size_t i) { + Ordinal module = (i / 32) % 2; + ChannelIdx channel = i; + if(module == 0) { + channel = (1- i / 32 / 2 + 1) * 32 - 1 - (i % 32); + } else { + channel = (i / 32 / 2) * 32 + (i % 32); + } + return ChannelAddress{module, channel}; + }), + x.expectedUs4OEMMappings = { + // 0: + ::arrus::concat( + { + generateReversed(0, 32), + generateReversed(32, 64), + getRange(64, 128) + }), + // 1: + getRange(0, 128) + + }, + x.expectedAdapterMapping = + generate(128, [](size_t i) + { + Ordinal module = (i / 32) % 2; + ChannelIdx channel = i; + if(module == 0) { + channel = (1- i / 32 / 2) * 32 + (i % 32); + } else { + channel = (i / 32 / 2) * 32 + (i % 32); + } + return ChannelAddress{module, channel}; + }) + )), +// Two modules, 0: 0, 1: 0, 0: 1, 1: 1, ..., 0:31, 1:31 + ARRUS_STRUCT_INIT_LIST(Mappings, ( + x.adapterMapping = generate(64, [](size_t i) { + return ChannelAddress{i % 2, i / 2}; + }), + x.expectedUs4OEMMappings = { + // 0: + getRange(0, 128), + // 1: + getRange(0, 128) + }, + x.expectedAdapterMapping = + generate(64, [](size_t i) { + return ChannelAddress{i % 2, i / 2}; + }) + )), +// Two modules, some randomly generated permutation + ARRUS_STRUCT_INIT_LIST(Mappings, ( + x.adapterMapping = +// Random mapping for modules 0, 1, 0, 1... + {{0, 17}, + {1, 13}, + {0, 11}, + {1, 17}, + {0, 21}, + {1, 16}, + {0, 20}, + {1, 1}, + {0, 9}, + {1, 23}, + {0, 26}, + {1, 20}, + {0, 5}, + {1, 12}, + {0, 16}, + {1, 21}, + {0, 10}, + {1, 14}, + {0, 28}, + {1, 5}, + {0, 15}, + {1, 31}, + {0, 2}, + {1, 9}, + {0, 12}, + {1, 26}, + {0, 8}, + {1, 22}, + {0, 18}, + {1, 19}, + {0, 23}, + {1, 3}, + {0, 29}, + {1, 7}, + {0, 13}, + {1, 4}, + {0, 0}, + {1, 18}, + {0, 19}, + {1, 15}, + {0, 6}, + {1, 27}, + {0, 24}, + {1, 28}, + {0, 4}, + {1, 11}, + {0, 27}, + {1, 0}, + {0, 30}, + {1, 2}, + {0, 31}, + {1, 10}, + {0, 25}, + {1, 29}, + {0, 14}, + {1, 25}, + {0, 1}, + {1, 24}, + {0, 3}, + {1, 6}, + {0, 22}, + {1, 30}, + {0, 7}, + {1, 8} + }, + x.expectedUs4OEMMappings = { + // 0: + ::arrus::concat( + { + std::vector({ + 17, 11, 21, 20, + 9, 26, 5, 16, + 10, 28, 15, 2, + 12, 8, 18, 23, + 29, 13, 0, 19, + 6, 24, 4, 27, + 30, 31, 25, 14, + 1, 3, 22, 7 + }), + ::arrus::getRange(32, 128) + }), + // 1: + ::arrus::concat( + { + std::vector { + 13, 17, 16, 1, + 23, 20, 12, 21, + 14, 5, 31, 9, + 26, 22, 19, 3, + 7, 4, 18, 15, + 27, 28, 11, 0, + 2, 10, 29, 25, + 24, 6, 30, 8 + }, + ::arrus::getRange(32, 128) + }) + }, + x.expectedAdapterMapping = + generate(64, [](size_t i) { + return ChannelAddress{i % 2, i / 2}; + }) + )) + )); + +// -------- Groups of active channels + +struct ActiveChannels { + ProbeAdapterSettings::ChannelMapping adapterMapping; + std::vector probeMapping; + + std::vector expectedUs4OEMMasks; + + friend std::ostream & + operator<<(std::ostream &os, const ActiveChannels &mappings) { + os << "adapterMapping: "; + for(const auto &address : mappings.adapterMapping) { + os << "(" << address.first << ", " << address.second << ") "; + } + + os << "probeMapping: "; + + for(auto value : mappings.probeMapping) { + os << value << " "; + } + + os << "expected groups masks: "; + + int i = 0; + for(const auto & mask: mappings.expectedUs4OEMMasks) { + os << "Us4OEM:" << i << " :"; + for(auto value : mask) { + os << (int) value << " "; + } + } + return os; + } +}; + +class ActiveChannelsTest + : public testing::TestWithParam { +}; + +TEST_P(ActiveChannelsTest, CorrectlyGeneratesActiveChannelGroups) { + Us4RSettingsConverterImpl converter; + + ActiveChannels testCase = GetParam(); + + ProbeAdapterSettings adapterSettings( + ProbeAdapterModelId("test", "test"), + testCase.adapterMapping.size(), + testCase.adapterMapping + ); + + ProbeSettings probeSettings( + ProbeModel(ProbeModelId("test", "test"), + {32}, + {0.3e-3}, {1e6, 10e6}, {0, 90}, 0.0), + testCase.probeMapping + ); + + RxSettings rxSettings({}, 24, 24, {}, 10e6, {}); + + auto[us4oemSettings, newAdapterSettings] = + converter.convertToUs4OEMSettings(adapterSettings, probeSettings, + rxSettings, std::vector()); + + EXPECT_EQ(us4oemSettings.size(), testCase.expectedUs4OEMMasks.size()); + + for(int i = 0; i < us4oemSettings.size(); ++i) { + EXPECT_EQ(us4oemSettings[i].getActiveChannelGroups(), + testCase.expectedUs4OEMMasks[i]); + } +} + +INSTANTIATE_TEST_CASE_P + +(TestingActiveChannelGroups, ActiveChannelsTest, + testing::Values( +// Esaote 1 like case, full adapter to probe mapping +// us4oem:0 :0-128, us4oem:1 : 0-64 + ARRUS_STRUCT_INIT_LIST(ActiveChannels, ( + x.adapterMapping = generate(192, [](size_t i) { + return ChannelAddress{i / 128, i % 128}; + }), + x.probeMapping = getRange(0, 192), + x.expectedUs4OEMMasks = { + // Us4OEM: 0 + getNTimes(true, 16), + // Us4OEM: 1 + ::arrus::concat({ + getNTimes(true, 8), + getNTimes(false, 8) + }) + } + )), +// Esaote 1 case, partial adapter to probe mapping + ARRUS_STRUCT_INIT_LIST(ActiveChannels, ( + x.adapterMapping = generate(192, [](size_t i) { + return ChannelAddress{i / 128, i % 128}; + }), + x.probeMapping = ::arrus::concat({ + getRange(0, 48), + getRange(144, 192), + }), + x.expectedUs4OEMMasks = { + // Us4OEM: 0 + ::arrus::concat({ + getNTimes(true, 6), + getNTimes(false, 10) + }), + // Us4OEM: 1 + ::arrus::concat({ + getNTimes(false, 2), + getNTimes(true, 6), + getNTimes(false, 8) + }) + } + )), +// esaote 1, but reverse the channels: 32-0, 64-32, .. for module 0; +// for module 1 keep the order as is +// partial adapter to probe mapping + ARRUS_STRUCT_INIT_LIST(ActiveChannels, ( + x.adapterMapping = generate(192, [](size_t i) { + Ordinal module; + ChannelIdx channel; + if(i < 128) { + ChannelIdx group = i / 32; + module = 0; + channel = (group+1) * 32 - (i % 32 + 1); + } else { + module = 1; + channel = i % 128; + } + return ChannelAddress{module, channel}; + }), + x.probeMapping = ::arrus::concat({ + getRange(0, 48), + getRange(144, 192) + }), + x.expectedUs4OEMMasks = { + // Us4OEM: 0 + ::arrus::concat({ + getNTimes(true, 4), + getNTimes(false, 2), + getNTimes(true, 2), + getNTimes(false, 8) + }), + // Us4OEM: 1 + ::arrus::concat({ + getNTimes(false, 2), + getNTimes(true, 6), + getNTimes(false, 8) + }) + } + )) +)); + +// -------- Channels masks + +std::vector getSL1543ChannelMapping() { + return ::arrus::getRange(0, 192); +} + +std::vector getEsaotePhaseArrayProbeMapping() { + std::vector result; + for(int i = 0; i < 48; ++i) { + result.push_back(i); + } + for(int i = 144; i < 192; ++i) { + result.push_back(i); + } + return result; +} + +std::vector getOneByOneProbeMapping() { + return ::arrus::getRange(0, 128); +} + +ProbeAdapterSettings::ChannelMapping getEsaote3ChannelMapping() { + ProbeAdapterSettings::ChannelMapping mapping; + for(int i = 0; i < 192; ++i) { + auto group = i / 32; + auto module = group % 2; + auto channel = i % 32 + 32*(i/64); + mapping.push_back({module, channel}); +// std::cerr << i << ", " << module << ", " << channel << std::endl; + } + return mapping; +} + +ProbeAdapterSettings::ChannelMapping getOneByOneChannelMapping() { + ProbeAdapterSettings::ChannelMapping mapping; + for(int i = 0; i < 128; ++i) { + mapping.emplace_back(i%2, i / 2); + } + return mapping; +} + +struct ChannelMaskingTestCase { + std::vector probeMapping; + ProbeAdapterSettings::ChannelMapping adapterMapping; + std::vector channelsMask; + + std::vector> expectedChannelsMasks; + + friend std::ostream & + operator<<(std::ostream &os, const ChannelMaskingTestCase &testCase) { + os << "probeMapping: "; + for(const auto &address : testCase.probeMapping) { + os << address << " "; + } + + os << "adapterMapping: "; + for(const auto &address : testCase.adapterMapping) { + os << "(" << address.first << ", " << address.second << ") "; + } + + os << "channelsMask: "; + for(const auto &address : testCase.channelsMask) { + os << address << " "; + } + + os << "expectedChannelsMasks: "; + + int i = 0; + for(const auto &cm: testCase.expectedChannelsMasks) { + os << "expected channels masks: " << i++ << " :"; + for(auto v : cm) { + os << v << " "; + } + } + return os; + } +}; + +class ChannelMaskingTest + : public testing::TestWithParam { +}; + +TEST_P(ChannelMaskingTest, CorrectlyMasksChannels) { + Us4RSettingsConverterImpl converter; + + ChannelMaskingTestCase mappings = GetParam(); + + ProbeAdapterSettings adapterSettings( + ProbeAdapterModelId("test", "test"), + mappings.adapterMapping.size(), + mappings.adapterMapping + ); + + ProbeSettings probeSettings( + ProbeModel( + ProbeModelId("test", "test"), + {(ChannelIdx)mappings.probeMapping.size()}, + {0.3e-3}, {1e6, 10e6}, {0, 90}, 0.0), + mappings.probeMapping + ); + + RxSettings rxSettings({}, 24, 24, {}, 10e6, {}); + + auto[us4oemSettings, newAdapterSettings] = + converter.convertToUs4OEMSettings(adapterSettings, probeSettings, + rxSettings, mappings.channelsMask); + + std::vector> channelsMasks; + std::transform( + std::begin(us4oemSettings), std::end(us4oemSettings), + std::back_inserter(channelsMasks), + [] (Us4OEMSettings &settings) { + return settings.getChannelsMask(); + }); + EXPECT_EQ(channelsMasks, mappings.expectedChannelsMasks); +} + +INSTANTIATE_TEST_CASE_P + +(TestingMappings, ChannelMaskingTest, + testing::Values( + // No channel masking + ARRUS_STRUCT_INIT_LIST(ChannelMaskingTestCase, ( + x.probeMapping = getSL1543ChannelMapping(), + x.adapterMapping = getEsaote3ChannelMapping(), + x.channelsMask = std::vector({}), + x.expectedChannelsMasks = { + {}, + {} + } + )), + ARRUS_STRUCT_INIT_LIST(ChannelMaskingTestCase, ( + x.probeMapping = getSL1543ChannelMapping(), + x.adapterMapping = getEsaote3ChannelMapping(), + x.channelsMask = std::vector({0, 7, 16, 32, 50, 90, 120, 159, 191}), + x.expectedChannelsMasks = { + {0, 7, 16, (90 % 32) + 32, (159 % 32) + 2 *32}, + {0, 50 % 32, (120 % 32) + 32, (191 % 32) + 2*32} + } + )), + ARRUS_STRUCT_INIT_LIST(ChannelMaskingTestCase, ( + x.probeMapping = getSL1543ChannelMapping(), + x.adapterMapping = getEsaote3ChannelMapping(), + x.channelsMask = std::vector({111}), + x.expectedChannelsMasks = { + {}, + {(111 % 32) + 32} + } + )), + ARRUS_STRUCT_INIT_LIST(ChannelMaskingTestCase, ( + x.probeMapping = getSL1543ChannelMapping(), + x.adapterMapping = getEsaote3ChannelMapping(), + x.channelsMask = std::vector({151}), + x.expectedChannelsMasks = { + {(151 % 32) + 2*32}, + {} + } + )), + ARRUS_STRUCT_INIT_LIST(ChannelMaskingTestCase, ( + x.probeMapping = getSL1543ChannelMapping(), + x.adapterMapping = getEsaote3ChannelMapping(), + x.channelsMask = std::vector({151, 153, 154}), + x.expectedChannelsMasks = { + {(151 % 32) + 2*32, (153 % 32) + 2*32, (154 % 32) + 2*32}, + {} + } + )), + ARRUS_STRUCT_INIT_LIST(ChannelMaskingTestCase, ( + x.probeMapping = getEsaotePhaseArrayProbeMapping(), + x.adapterMapping = getEsaote3ChannelMapping(), + x.channelsMask = std::vector({0, 1, 2, 30, 40, 47, 48, 49, 70, 77, 95}), + x.expectedChannelsMasks = { + {0, 1, 2, 30, 64+16, 64+17}, + {40 % 32, 47 % 32, 70, 77, 95} + } + )), + ARRUS_STRUCT_INIT_LIST(ChannelMaskingTestCase, ( + x.probeMapping = getOneByOneProbeMapping(), + x.adapterMapping = getOneByOneChannelMapping(), + x.channelsMask = std::vector({0, 1, 2, 30, 127}), + x.expectedChannelsMasks = { + {0, 1, 15}, + {0, 63} + } + )) +)); + +TEST(ChannelsMaskingTest, ChecksIfChannelMaskElementsDoNotExceedNumberOfProbeElements) { + Us4RSettingsConverterImpl converter; + + auto probeMapping = getSL1543ChannelMapping(); + auto adapterMapping = getEsaote3ChannelMapping(); + + ProbeAdapterSettings adapterSettings( + ProbeAdapterModelId("test", "test"), + adapterMapping.size(), + adapterMapping + ); + + ProbeSettings probeSettings( + ProbeModel( + ProbeModelId("test", "test"), + {32}, + {0.3e-3}, {1e6, 10e6}, {0, 90}, 0.0), + probeMapping + ); + + RxSettings rxSettings({}, 24, 24, {}, 10e6, {}); + + std::vector channelsMask({10, 20, 192}); + + EXPECT_THROW(converter.convertToUs4OEMSettings(adapterSettings, probeSettings, + rxSettings, channelsMask), + ::arrus::IllegalArgumentException); + +} +} + diff --git a/arrus/core/devices/us4r/Us4RSettingsValidator.h b/arrus/core/devices/us4r/Us4RSettingsValidator.h new file mode 100644 index 000000000..b23f94d6f --- /dev/null +++ b/arrus/core/devices/us4r/Us4RSettingsValidator.h @@ -0,0 +1,44 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4RSETTINGSVALIDATOR_H +#define ARRUS_CORE_DEVICES_US4R_US4RSETTINGSVALIDATOR_H + +#include "arrus/core/api/devices/us4r/Us4RSettings.h" +#include "arrus/core/devices/SettingsValidator.h" + +namespace arrus::devices { +class Us4RSettingsValidator : public SettingsValidator { +public: + explicit Us4RSettingsValidator(Ordinal moduleOrdinal) + : SettingsValidator( + DeviceId(DeviceType::Us4R, moduleOrdinal)) {} + + void validate(const Us4RSettings &obj) override { + if(obj.getUs4OEMSettings().empty()) { + expectTrue("probe adapter settings", + obj.getProbeAdapterSettings().has_value(), + "Probe adapter settings are required."); + expectTrue("probe settings", + obj.getProbeSettings().has_value(), + "Probe settings are required."); + expectTrue("tgc settings", + obj.getRxSettings().has_value(), + "Us4R TGC settings must be provided."); + } else { + expectFalse( + "probe adapter settings", + obj.getProbeAdapterSettings().has_value(), + "Probe settings should not be set " + "(at least one custom Us4OEM setting was detected)."); + + expectFalse( + "probe settings", + obj.getProbeSettings().has_value(), + "Probe settings should not be set " + "(at least one custom Us4OEM setting was detected)."); + } + // The exact TGC settings should be verified by the underlying Us4OEMs. + } + +}; +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4RSETTINGSVALIDATOR_H diff --git a/arrus/core/devices/us4r/common.cpp b/arrus/core/devices/us4r/common.cpp new file mode 100644 index 000000000..036ab7269 --- /dev/null +++ b/arrus/core/devices/us4r/common.cpp @@ -0,0 +1,181 @@ +#include "common.h" + +#include +#include +#include +#include +#include + +#include "arrus/core/common/aperture.h" + +namespace arrus::devices { + +static ChannelIdx getMaximumRxAperture(const std::vector &seqs) { + ChannelIdx maxElementSize = 0; + for(const auto& seq: seqs) { + for(const auto &op : seq) { + ChannelIdx n = getNumberOfActiveChannels(op.getRxAperture()); + if(n > maxElementSize) { + maxElementSize = n; + } + } + } + return maxElementSize; +} + +static FrameChannelMapping::FrameNumber +getNumberOfFrames(const std::vector &seqs) { + FrameChannelMapping::FrameNumber numberOfFrames = 0; + auto numberOfOps = seqs[0].size(); + for(size_t opIdx = 0; opIdx < numberOfOps; ++opIdx) { + for(const auto & seq : seqs) { + if(!seq[opIdx].isRxNOP()) { + ++numberOfFrames; + break; + } + } + } + return numberOfFrames; +} + +std::tuple< + std::vector, + Eigen::Tensor, + Eigen::Tensor +> +splitRxAperturesIfNecessary(const std::vector &seqs) { + using FrameNumber = FrameChannelMapping::FrameNumber; + // All sequences must have the same length. + ARRUS_REQUIRES_NON_EMPTY_IAE(seqs); + size_t seqLength = seqs[0].size(); + for(const auto &seq : seqs) { + ARRUS_REQUIRES_EQUAL_IAE(seqLength, seq.size()); + } + std::vector result; + + // Find the maximum rx aperture size + ChannelIdx maxRxApertureSize = getMaximumRxAperture(seqs); + FrameChannelMapping::FrameNumber numberOfFrames = getNumberOfFrames(seqs); + // (module, logical frame, logical rx channel) -> physical frame + Eigen::Tensor opDestOp(seqs.size(), numberOfFrames, maxRxApertureSize); + // (module, logical frame, logical rx channel) -> physical rx channel + Eigen::Tensor opDestChannel(seqs.size(), numberOfFrames, maxRxApertureSize); + opDestOp.setZero(); + opDestChannel.setConstant(FrameChannelMapping::UNAVAILABLE); + + constexpr ChannelIdx N_RX_CHANNELS = Us4OEMImpl::N_RX_CHANNELS; + constexpr ChannelIdx N_GROUPS = + Us4OEMImpl::N_ADDR_CHANNELS / Us4OEMImpl::N_RX_CHANNELS; + constexpr ChannelIdx N_ADDR_CHANNELS = Us4OEMImpl::N_ADDR_CHANNELS; + + for(const auto &seq : seqs) { + TxRxParamsSequence resSeq; + resSeq.reserve(seq.size()); + result.push_back(resSeq); + } + + // us4oem ordinal number -> current frame idx + std::vector currentFrameIdx(seqs.size(), 0); + for(size_t opIdx = 0; opIdx < seqLength; ++opIdx) { + for(size_t seqIdx = 0; seqIdx < seqs.size(); ++seqIdx) { + const auto &seq = seqs[seqIdx]; + const auto &op = seq[opIdx]; + + // Split rx aperture, if necessary. + // subaperture number starts from 1, 0 means that the channel + // should be inactive. + std::vector subapertureIdxs(op.getRxAperture().size()); + for(ChannelIdx ch = 0; ch < N_RX_CHANNELS; ++ch) { + ChannelIdx subaperture = 1; + for(ChannelIdx group = 0; group < N_GROUPS; ++group) { + ChannelIdx addrIdx = group * N_RX_CHANNELS + ch; + if(op.getRxAperture()[addrIdx]) { + // channel active + subapertureIdxs[addrIdx] = subaperture++; + } else { + // channel inactive + subapertureIdxs[addrIdx] = 0; + } + } + } + ChannelIdx maxSubapertureIdx = *std::max_element( + std::begin(subapertureIdxs), std::end(subapertureIdxs)); + if(maxSubapertureIdx > 1) { + // Split aperture into smaller subapertures. + std::vector rxSubapertures(maxSubapertureIdx); + for(auto &subaperture : rxSubapertures) { + subaperture.resize(N_ADDR_CHANNELS); + } + + long long opActiveChannel = 0; + std::vector subopActiveChannels(maxSubapertureIdx, 0); + for(size_t ch = 0; ch < subapertureIdxs.size(); ++ch) { + auto subapIdx = subapertureIdxs[ch]; + if(subapIdx > 0) { + rxSubapertures[subapIdx-1][ch] = true; + // FC mapping + // -1 because subapIdx starts from one + opDestOp(seqIdx, opIdx, opActiveChannel) = FrameNumber(currentFrameIdx[seqIdx] + subapIdx - 1); + ARRUS_REQUIRES_TRUE_E( + opActiveChannel <= (std::numeric_limits::max)(), + arrus::ArrusException( + "Number of active rx elements should not exceed 32.")); + opDestChannel(seqIdx, opIdx, opActiveChannel) = + static_cast(subopActiveChannels[subapIdx-1]); + ++opActiveChannel; + ++subopActiveChannels[subapIdx-1]; + } + } + // generate ops from subapertures + for(auto &subaperture : rxSubapertures) { + result[seqIdx].emplace_back( + op.getTxAperture(), op.getTxDelays(), op.getTxPulse(), + subaperture, // Modified + op.getRxSampleRange(), op.getRxDecimationFactor(), op.getPri(), + op.getRxPadding()); + } + } else { + // we have a single rx aperture, or all rx channels are empty, + // just pass the operator as is + // NOTE: we push_back even if the op is rx nop + result[seqIdx].push_back(op); + // FC mapping + ChannelIdx opActiveChannel = 0; + for(auto bit : op.getRxAperture()) { + if(bit) { + opDestOp(seqIdx, opIdx, opActiveChannel) = + currentFrameIdx[seqIdx]; + ARRUS_REQUIRES_TRUE_E( + opActiveChannel <= (std::numeric_limits::max)(), + arrus::ArrusException( + "Number of active rx elements should not exceed 32.")); + opDestChannel(seqIdx, opIdx, opActiveChannel) + = static_cast(opActiveChannel); + ++opActiveChannel; + } + } + } + currentFrameIdx[seqIdx] += maxSubapertureIdx; + } + // Check if all seqs have the same size. + // If not, pad them with a rx NOP. + std::vector currentSeqSizes; + std::transform(std::begin(result), std::end(result), + std::back_inserter(currentSeqSizes), + [](auto &v) { return v.size(); }); + size_t maxSize = *std::max_element(std::begin(currentSeqSizes), + std::end(currentSeqSizes)); + + for(auto& resSeq : result) { + if(resSeq.size() < maxSize) { + // create rxnop copy from the last element of this sequence + // note, that even if the last element is rx nop it should be added + // in this method in some of the code above. + resSeq.resize(maxSize, TxRxParameters::createRxNOPCopy(resSeq[resSeq.size()-1])); + } + } + } + return std::make_tuple(result, opDestOp, opDestChannel); +} + +} \ No newline at end of file diff --git a/arrus/core/devices/us4r/common.h b/arrus/core/devices/us4r/common.h new file mode 100644 index 000000000..2d200ae9a --- /dev/null +++ b/arrus/core/devices/us4r/common.h @@ -0,0 +1,48 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_COMMON_H +#define ARRUS_CORE_DEVICES_US4R_COMMON_H + +#include +#include + +#include "arrus/core/api/common/types.h" +#include "arrus/common/asserts.h" +#include "arrus/core/devices/TxRxParameters.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImpl.h" + +#include "arrus/core/external/eigen/Tensor.h" + +namespace arrus::devices { + +/** + * Splits each tx/rx operation into multiple ops so that each rx aperture + * does not include the same rx channel multiple times. + * + * This function is intended to be used for Us4OEM TxRxs only! + * + * Note: us4oems have 32 rx channels, however 128 rx channels are addressable; + * each addressable rx channel 'i' is connected to us4oem channel 'i modulo 32', + * so for example us4oem channel 0 can handle the output addressable channels + * 0, 32, 64, 96; only one of these channels can be set in a single Rx aperture. + * + * `Seqs` input parameter is a vector of sequences that will be loaded on + * us4oem:0, us4oem:1, etc. This function outputs updated sequences so that + * there are no conflicting rx channels. All of the output sequences have the + * same length - e.g. if seqs[0] first tx/rx operation must be split into + * 4 tx/rx ops, and seqs[1] first op must be split into 2 tx/rx ops only, + * the second sequence will extended by NOP TxRxParameters. + * + * @param seqs tx/rx sequences to recalculate + * @return recalculated sequences, + * a mapping (module, input op index, rx channel) -> output frame number, + * a mapping (module, input op index, rx channel) -> output frame rx channel + */ +std::tuple< + std::vector, + Eigen::Tensor, + Eigen::Tensor +> +splitRxAperturesIfNecessary(const std::vector &seqs); + +} + +#endif //ARRUS_CORE_DEVICES_US4R_COMMON_H diff --git a/arrus/core/devices/us4r/commonTest.cpp b/arrus/core/devices/us4r/commonTest.cpp new file mode 100644 index 000000000..77758bcec --- /dev/null +++ b/arrus/core/devices/us4r/commonTest.cpp @@ -0,0 +1,385 @@ +#include +#include "common.h" + +#include "arrus/core/common/tests.h" +#include "arrus/core/common/collections.h" + +namespace { +using namespace arrus; +using namespace arrus::devices; +using namespace arrus::ops::us4r; + +const Pulse PULSE(7e6, 3.5, true); +const Interval RX_SAMPLE_RANGE(0, 4095); +const uint32 DECIMATION_FACTOR = 3; +const float PRI = 300e-6; +const std::vector TX_APERTURE = getNTimes(true, 128); +const std::vector TX_DELAYS(128); + +constexpr int32 FCM_UNAVAILABLE_VALUE = static_cast(FrameChannelMapping::UNAVAILABLE); + +TxRxParameters getStdTxRxParameters(const std::vector &rxAperture) { + return TxRxParameters(TX_APERTURE, TX_DELAYS, PULSE, rxAperture, + RX_SAMPLE_RANGE, DECIMATION_FACTOR, PRI); +} + + +void verifyOps(const std::vector &expected, + const std::vector &actual) { + ASSERT_EQ(expected.size(), actual.size()); + EXPECT_EQ(expected, actual); +} + +#define ARRUS_SET_FCM(module, frame, channel, dstFrame, dstChannel) \ + expectedDstFrame(module, frame, channel) = dstFrame; \ + expectedDstChannel(module, frame, channel) = dstChannel; \ + + +TEST(SplitRxApertureIfNecessaryTest, SplitsSingleOperationCorrectly) { + std::vector rxAperture(128); + rxAperture[1] = rxAperture[16] = rxAperture[33] = rxAperture[97] = true; + + std::vector in = { + { + getStdTxRxParameters(rxAperture) + } + }; + auto [res, fcmDstFrame, fcmDstChannel] = splitRxAperturesIfNecessary(in); + + std::vector expectedRxAperture0(128); + expectedRxAperture0[1] = true; + expectedRxAperture0[16] = true; + std::vector expectedRxAperture1(128); + expectedRxAperture1[33] = true; + std::vector expectedRxAperture2(128); + expectedRxAperture2[97] = true; + std::vector expected{ + { + getStdTxRxParameters(expectedRxAperture0), + getStdTxRxParameters(expectedRxAperture1), + getStdTxRxParameters(expectedRxAperture2) + } + }; + verifyOps(expected, res); + + // FCM + Eigen::Tensor expectedDstFrame(1, 1, 4); + Eigen::Tensor expectedDstChannel(1, 1, 4); + expectedDstFrame(0, 0, 0) = 0; + expectedDstChannel(0, 0, 0) = 0; + + expectedDstFrame(0, 0, 1) = 0; + expectedDstChannel(0, 0, 1) = 1; + + expectedDstFrame(0, 0, 2) = 1; + expectedDstChannel(0, 0, 2) = 0; + expectedDstFrame(0, 0, 3) = 2; + expectedDstChannel(0, 0, 3) = 0; + + ARRUS_EXPECT_TENSORS_EQ(fcmDstFrame, expectedDstFrame); + ARRUS_EXPECT_TENSORS_EQ(fcmDstChannel, expectedDstChannel); +} + +TEST(SplitRxApertureIfNecessaryTest, DoesNotSplitOpIfNotNecessary) { + std::vector rxAperture(128); + rxAperture[1] = rxAperture[16] = rxAperture[31] = true; + + std::vector in = { + { + getStdTxRxParameters(rxAperture) + } + }; + auto [res, fcmDstFrame, fcmDstChannel] = splitRxAperturesIfNecessary(in); + + std::vector expected{ + { + getStdTxRxParameters(rxAperture) + } + }; + verifyOps(expected, res); + + // FCM + Eigen::Tensor expectedDstFrame(1, 1, 3); + Eigen::Tensor expectedDstChannel(1, 1, 3); + + expectedDstFrame(0, 0, 0) = 0; + expectedDstChannel(0, 0, 0) = 0; + + expectedDstFrame(0, 0, 1) = 0; + expectedDstChannel(0, 0, 1) = 1; + + expectedDstFrame(0, 0, 2) = 0; + expectedDstChannel(0, 0, 2) = 2; + ARRUS_EXPECT_TENSORS_EQ(fcmDstFrame, expectedDstFrame); + ARRUS_EXPECT_TENSORS_EQ(fcmDstChannel, expectedDstChannel); +} + +TEST(SplitRxApertureIfNecessaryTest, SplitsMultipleOpsCorrectly) { + std::vector rxAperture0(128); + rxAperture0[1] = rxAperture0[16] = rxAperture0[33] = rxAperture0[80] = true; + // expected output two apertures: (1, 16), (33, 80) + std::vector rxAperture1(128); + rxAperture1[0] = rxAperture1[96] = rxAperture1[16] = rxAperture1[48] = true; + // expected two apertures: (0, 16), (48, 96) + std::vector rxAperture2(128); + rxAperture2[63] = true; + // expected one aperture + std::vector rxAperture3(128); + rxAperture3[0] = rxAperture3[32] = rxAperture3[96] = rxAperture3[48] = true; + // expected three apertures: (0, 48), (32), (96) + + std::vector in = { + { + getStdTxRxParameters(rxAperture0), + getStdTxRxParameters(rxAperture1), + getStdTxRxParameters(rxAperture2), + getStdTxRxParameters(rxAperture3) + } + }; + auto [res, fcmDstFrame, fcmDstChannel] = splitRxAperturesIfNecessary(in); + + // IN op 0 + std::vector expRxAperture0(128); + expRxAperture0[1] = expRxAperture0[16] = true; + std::vector expRxAperture1(128); + expRxAperture1[33] = expRxAperture1[80] = true; + // IN op 1 + std::vector expRxAperture2(128); + expRxAperture2[0] = expRxAperture2[16] = true; + std::vector expRxAperture3(128); + expRxAperture3[48] = expRxAperture3[96] = true; + // IN op 2 + std::vector expRxAperture4(128); + expRxAperture4[63] = true; + // In op 3 + std::vector expRxAperture5(128); + expRxAperture5[0] = expRxAperture5[48] = true; + std::vector expRxAperture6(128); + expRxAperture6[32] = true; + std::vector expRxAperture7(128); + expRxAperture7[96] = true; + + std::vector expected{ + { + getStdTxRxParameters(expRxAperture0), + getStdTxRxParameters(expRxAperture1), + getStdTxRxParameters(expRxAperture2), + getStdTxRxParameters(expRxAperture3), + getStdTxRxParameters(expRxAperture4), + getStdTxRxParameters(expRxAperture5), + getStdTxRxParameters(expRxAperture6), + getStdTxRxParameters(expRxAperture7) + } + }; + verifyOps(expected, res); + + // FCM + Eigen::Tensor expectedDstFrame(1, 4, 4); + Eigen::Tensor expectedDstChannel(1, 4, 4); + + // frame 1.1: + ARRUS_SET_FCM(0, 0, 0, 0, 0); + ARRUS_SET_FCM(0, 0, 1, 0, 1); + + // frame 1.2: + ARRUS_SET_FCM(0, 0, 2, 1, 0); + ARRUS_SET_FCM(0, 0, 3, 1, 1); + + // frame 2.1: + ARRUS_SET_FCM(0, 1, 0, 2, 0); + ARRUS_SET_FCM(0, 1, 1, 2, 1); + // frame 2.2: + ARRUS_SET_FCM(0, 1, 2, 3, 0); + ARRUS_SET_FCM(0, 1, 3, 3, 1); + + // frame 3: + ARRUS_SET_FCM(0, 2, 0, 4, 0); + // There is no rx channels > 0 for 3rd op. + ARRUS_SET_FCM(0, 2, 1, 0, FCM_UNAVAILABLE_VALUE); + ARRUS_SET_FCM(0, 2, 2, 0, FCM_UNAVAILABLE_VALUE); + ARRUS_SET_FCM(0, 2, 3, 0, FCM_UNAVAILABLE_VALUE); + + // frame 4.1: + // 0 + ARRUS_SET_FCM(0, 3, 0, 5, 0); + // 32 + ARRUS_SET_FCM(0, 3, 1, 6, 0); + // frame 4.2: + // 48 + // NOTE! 48 is assigned to frame 5, because 32 mod 32 is already covered + // by 0 + ARRUS_SET_FCM(0, 3, 2, 5, 1); + // frame 4.3 + // 96 + ARRUS_SET_FCM(0, 3, 3, 7, 0); + + ARRUS_EXPECT_TENSORS_EQ(fcmDstFrame, expectedDstFrame); + ARRUS_EXPECT_TENSORS_EQ(fcmDstChannel, expectedDstChannel); +} + +TEST(SplitRxApertureIfNecessaryTest, SplitsFullRxApertureCorrectly) { + std::vector rxAperture = getNTimes(true, 128); + + std::vector in = { + { + getStdTxRxParameters(rxAperture) + } + }; + auto [res, fcmDstFrame, fcmDstChannel] = splitRxAperturesIfNecessary(in); + + std::vector expectedRxAperture0(128); + for(size_t i = 0; i < 32; ++i) { + expectedRxAperture0[i] = true; + } + std::vector expectedRxAperture1(128); + for(size_t i = 32; i < 64; ++i) { + expectedRxAperture1[i] = true; + } + std::vector expectedRxAperture2(128); + for(size_t i = 64; i < 96; ++i) { + expectedRxAperture2[i] = true; + } + std::vector expectedRxAperture3(128); + for(size_t i = 96; i < 128; ++i) { + expectedRxAperture3[i] = true; + } + std::vector expected{ + { + getStdTxRxParameters(expectedRxAperture0), + getStdTxRxParameters(expectedRxAperture1), + getStdTxRxParameters(expectedRxAperture2), + getStdTxRxParameters(expectedRxAperture3) + } + }; + verifyOps(expected, res); + + // FCM + Eigen::Tensor expectedDstFrame(1, 1, 128); + Eigen::Tensor expectedDstChannel(1, 1, 128); + + for(int32 i = 0; i < 32; ++i) { + ARRUS_SET_FCM(0, 0, i, 0, i); + } + for(int32 i = 32; i < 64; ++i) { + ARRUS_SET_FCM(0, 0, i, 1, i%32); + } + for(int32 i = 64; i < 96; ++i) { + ARRUS_SET_FCM(0, 0, i, 2, i%32); + } + for(int32 i = 96; i < 128; ++i) { + ARRUS_SET_FCM(0, 0, i, 3, i%32); + } + ARRUS_EXPECT_TENSORS_EQ(fcmDstFrame, expectedDstFrame); + ARRUS_EXPECT_TENSORS_EQ(fcmDstChannel, expectedDstChannel); +} + +// multiple sequences, each sequence should has the same size (padded with NOPs if necessary) +TEST(SplitRxApertureIfNecessaryTest, PadsWithNopsCorrectly) { + // Two ops, first and the second should be padded + TxRxParamsSequence seq0; + { + std::vector rxAperture0(128); + rxAperture0[0] = rxAperture0[1] = rxAperture0[32] = true; + std::vector rxAperture1(128); + rxAperture1[16] = rxAperture1[17] = true; + std::vector rxAperture2(128); + rxAperture2[0] = rxAperture2[1] = true; + + seq0.push_back(getStdTxRxParameters(rxAperture0)); + seq0.push_back(getStdTxRxParameters(rxAperture1)); + seq0.push_back(getStdTxRxParameters(rxAperture2)); + } + TxRxParamsSequence seq1; + { + std::vector rxAperture0(128); + rxAperture0[0] = rxAperture0[1] = rxAperture0[34] = true; + std::vector rxAperture1(128); + rxAperture1[16] = rxAperture1[17] = true; + std::vector rxAperture2(128); + rxAperture2[16] = rxAperture2[48] = true; + + seq1.push_back(getStdTxRxParameters(rxAperture0)); + seq1.push_back(getStdTxRxParameters(rxAperture1)); + seq1.push_back(getStdTxRxParameters(rxAperture2)); + } + + std::vector in = {seq0, seq1}; + auto [res, fcmDstFrame, fcmDstChannel] = splitRxAperturesIfNecessary(in); + + TxRxParamsSequence expectedSeq0; + { + std::vector rxAperture0(128); + rxAperture0[0] = rxAperture0[1] = true; + + std::vector rxAperture1(128); + rxAperture1[32] = true; + + std::vector rxAperture2(128); + rxAperture2[16] = rxAperture2[17] = true; + + std::vector rxAperture3(128); + rxAperture3[0] = rxAperture3[1] = true; + + expectedSeq0.push_back(getStdTxRxParameters(rxAperture0)); + expectedSeq0.push_back(getStdTxRxParameters(rxAperture1)); + expectedSeq0.push_back(getStdTxRxParameters(rxAperture2)); + expectedSeq0.push_back(getStdTxRxParameters(rxAperture3)); + expectedSeq0.push_back(TxRxParameters::createRxNOPCopy(getStdTxRxParameters(rxAperture3))); + } + + TxRxParamsSequence expectedSeq1; + { + std::vector rxAperture0(128); + rxAperture0[0] = rxAperture0[1] = rxAperture0[34] = true; + std::vector rxAperture2(128); + rxAperture2[16] = rxAperture2[17] = true; + std::vector rxAperture3(128); + rxAperture3[16] = true; + std::vector rxAperture4(128); + rxAperture4[48] = true; + + expectedSeq1.push_back(getStdTxRxParameters(rxAperture0)); + expectedSeq1.push_back(TxRxParameters::createRxNOPCopy(getStdTxRxParameters(rxAperture0))); + expectedSeq1.push_back(getStdTxRxParameters(rxAperture2)); + expectedSeq1.push_back(getStdTxRxParameters(rxAperture3)); + expectedSeq1.push_back(getStdTxRxParameters(rxAperture4)); + } + + std::vector expected{expectedSeq0, expectedSeq1}; + verifyOps(expected, res); + + // FCM + Eigen::Tensor expectedDstFrame(2, 3, 3); + Eigen::Tensor expectedDstChannel(2, 3, 3); + expectedDstFrame.setZero(); + expectedDstChannel.setConstant(FCM_UNAVAILABLE_VALUE); + // Module 0 + // Frame 1.1 + ARRUS_SET_FCM(0, 0, 0, 0, 0); + ARRUS_SET_FCM(0, 0, 1, 0, 1); + // Frame 1.2 + ARRUS_SET_FCM(0, 0, 2, 1, 0); + // Frame 2 + ARRUS_SET_FCM(0, 1, 0, 2, 0); + ARRUS_SET_FCM(0, 1, 1, 2, 1); + // Frame 3 + ARRUS_SET_FCM(0, 2, 0, 3, 0); + ARRUS_SET_FCM(0, 2, 1, 3, 1); + + // Module 1 + // Frame 1 + ARRUS_SET_FCM(1, 0, 0, 0, 0); + ARRUS_SET_FCM(1, 0, 1, 0, 1); + ARRUS_SET_FCM(1, 0, 2, 0, 2); + // Frame 2 + ARRUS_SET_FCM(1, 1, 0, 1, 0); + ARRUS_SET_FCM(1, 1, 1, 1, 1); + // Frame 3.1 + ARRUS_SET_FCM(1, 2, 0, 2, 0); + ARRUS_SET_FCM(1, 2, 1, 3, 0); + + ARRUS_EXPECT_TENSORS_EQ(fcmDstFrame, expectedDstFrame); + ARRUS_EXPECT_TENSORS_EQ(fcmDstChannel, expectedDstChannel); +} + +} \ No newline at end of file diff --git a/arrus/core/devices/us4r/external/ius4oem/ActiveTerminationValueMap.h b/arrus/core/devices/us4r/external/ius4oem/ActiveTerminationValueMap.h new file mode 100644 index 000000000..000d8d457 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/ActiveTerminationValueMap.h @@ -0,0 +1,66 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_ACTIVETERMINATIONVALUEMAP_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_ACTIVETERMINATIONVALUEMAP_H + +#include +#include +#include + +#include "arrus/core/api/common/types.h" + +namespace arrus::devices { + +class ActiveTerminationValueMap { + +public: + using ValueType = uint16; + + static ActiveTerminationValueMap &getInstance() { + static ActiveTerminationValueMap instance; + return instance; + } + + us4r::afe58jd18::GBL_ACTIVE_TERM + getEnumValue(const ValueType value) { + return valueMap.at(value); + } + + /** + * Returns a sorted set of available values. + */ + std::set getAvailableValues() const { + std::set values; + std::transform(std::begin(valueMap), std::end(valueMap), + std::inserter(values, std::end(values)), + [](auto &val) { + return val.first; + }); + return values; + } + + ActiveTerminationValueMap(ActiveTerminationValueMap const &) = delete; + + void operator=(ActiveTerminationValueMap const &) = delete; + + ActiveTerminationValueMap(ActiveTerminationValueMap const &&) = delete; + + void operator=(ActiveTerminationValueMap const &&) = delete; + +private: + std::unordered_map valueMap{}; + + ActiveTerminationValueMap() { + valueMap.emplace((ValueType)50, + us4r::afe58jd18::GBL_ACTIVE_TERM::GBL_ACTIVE_TERM_50); + valueMap.emplace((ValueType)100, + us4r::afe58jd18::GBL_ACTIVE_TERM::GBL_ACTIVE_TERM_100); + + valueMap.emplace((ValueType)200, + us4r::afe58jd18::GBL_ACTIVE_TERM::GBL_ACTIVE_TERM_200); + valueMap.emplace((ValueType)400, + us4r::afe58jd18::GBL_ACTIVE_TERM::GBL_ACTIVE_TERM_400); + } + +}; + +} +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_ACTIVETERMINATIONVALUEMAP_H diff --git a/arrus/core/devices/us4r/external/ius4oem/DTGCAttenuationValueMap.h b/arrus/core/devices/us4r/external/ius4oem/DTGCAttenuationValueMap.h new file mode 100644 index 000000000..b79890dd7 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/DTGCAttenuationValueMap.h @@ -0,0 +1,75 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_DTGCATTENUATIONVALUEMAP_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_DTGCATTENUATIONVALUEMAP_H + +#include +#include +#include + +#include "arrus/core/api/common/types.h" + +namespace arrus::devices { + +class DTGCAttenuationValueMap { + +public: + using ValueType = uint16; + + static DTGCAttenuationValueMap &getInstance() { + static DTGCAttenuationValueMap instance; + return instance; + } + + us4r::afe58jd18::DIG_TGC_ATTENUATION + getEnumValue(const ValueType value) { + return valueMap.at(value); + } + + /** + * Returns a sorted set of available values. + */ + std::set getAvailableValues() const { + std::set values; + std::transform(std::begin(valueMap), std::end(valueMap), + std::inserter(values, std::end(values)), + [](auto &val) { + return val.first; + }); + return values; + } + + DTGCAttenuationValueMap(DTGCAttenuationValueMap const &) = delete; + + void operator=(DTGCAttenuationValueMap const &) = delete; + + DTGCAttenuationValueMap(DTGCAttenuationValueMap const &&) = delete; + + void operator=(DTGCAttenuationValueMap const &&) = delete; + +private: + std::unordered_map valueMap; + + DTGCAttenuationValueMap() { + valueMap.emplace(ValueType(0), + us4r::afe58jd18::DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_0dB); + valueMap.emplace(ValueType(6), + us4r::afe58jd18::DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_6dB); + valueMap.emplace(ValueType(12), + us4r::afe58jd18::DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_12dB); + valueMap.emplace(ValueType(18), + us4r::afe58jd18::DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_18dB); + valueMap.emplace(ValueType(24), + us4r::afe58jd18::DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_24dB); + valueMap.emplace(ValueType(30), + us4r::afe58jd18::DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_30dB); + valueMap.emplace(ValueType(36), + us4r::afe58jd18::DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_36dB); + valueMap.emplace(ValueType(42), + us4r::afe58jd18::DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_42dB); + } + + +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_DTGCATTENUATIONVALUEMAP_H diff --git a/arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h new file mode 100644 index 000000000..15635c0f2 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h @@ -0,0 +1,26 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMFACTORY_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMFACTORY_H + +#include +#include + +#include "arrus/core/api/devices/DeviceId.h" + +namespace arrus::devices { + +using IUs4OEMHandle = std::unique_ptr; +using Ius4OEMRawHandle = IUs4OEM*; + +/** + * A simple wrapper over GetUs4OEM method available in Us4. + */ + +class IUs4OEMFactory { +public: + virtual IUs4OEMHandle getIUs4OEM(unsigned index) = 0; + virtual std::vector getModules(Ordinal nModules) = 0; +}; + + +} +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMFACTORY_H diff --git a/arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactoryImpl.h b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactoryImpl.h new file mode 100644 index 000000000..f01ee6c98 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactoryImpl.h @@ -0,0 +1,54 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMFACTORYIMPL_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMFACTORYIMPL_H + +#include "IUs4OEMFactory.h" + +#include +#include + +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/common/logging.h" +#include "arrus/core/devices/us4r/external/ius4oem/Us4RLoggerWrapper.h" + + +namespace arrus::devices { + +/** + * A simple wrapper over GetUs4OEM method available in Us4. + */ +class IUs4OEMFactoryImpl : public IUs4OEMFactory { +public: + IUs4OEMFactoryImpl() = default; + + IUs4OEMHandle getIUs4OEM(unsigned index) override { + Logger::SharedHandle arrusLogger = getLoggerFactory()->getLogger(); + us4r::Logger::SharedHandle logger = + std::make_shared(arrusLogger); + return IUs4OEMHandle(GetUs4OEM(index, logger)); + } + + std::vector getModules(Ordinal nModules) override { + std::vector us4oems; + + std::vector ordinals(nModules); + std::iota(std::begin(ordinals), std::end(ordinals), Ordinal(0)); + + // Create Us4OEM handles. + for(auto ordinal : ordinals) { + us4oems.push_back(getIUs4OEM(ordinal)); + } + return us4oems; + } + + IUs4OEMFactoryImpl(IUs4OEMFactoryImpl const &) = delete; + + void operator=(IUs4OEMFactoryImpl const &) = delete; + + IUs4OEMFactoryImpl(IUs4OEMFactoryImpl const &&) = delete; + + void operator=(IUs4OEMFactoryImpl const &&) = delete; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMFACTORYIMPL_H diff --git a/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializer.h b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializer.h new file mode 100644 index 000000000..5350e8b61 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializer.h @@ -0,0 +1,20 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMINITIALIZER_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMINITIALIZER_H + +#include "arrus/core/api/devices/DeviceId.h" +#include "IUs4OEMFactory.h" + +namespace arrus::devices { + +class IUs4OEMInitializer { +public: + /** + * Sorts the given list of us4oems (by device id) and initializes them. + */ + virtual void + initModules(std::vector &ius4oems) = 0; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMINITIALIZER_H diff --git a/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializerImpl.h b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializerImpl.h new file mode 100644 index 000000000..0d4d7acfd --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializerImpl.h @@ -0,0 +1,38 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMINITIALIZERIMPL_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMINITIALIZERIMPL_H + +#include "IUs4OEMInitializer.h" + +namespace arrus::devices { + +class IUs4OEMInitializerImpl : public IUs4OEMInitializer { +public: + + void initModules(std::vector &ius4oems) override { + // Reorder us4oems according to ids (us4oem with the lowest id is the + // first one, with the highest id - the last one). + // TODO(pjarosik) make the below sorting exception safe + // (currently will std::terminate on an exception). + std::sort(std::begin(ius4oems), std::end(ius4oems), + [](const IUs4OEMHandle &x, const IUs4OEMHandle &y) { + return x->GetID() < y->GetID(); + }); + + for(auto &u : ius4oems) { + u->Initialize(1); + } + // Perform successive initialization levels. + for(int level = 2; level <= 4; level++) { + ius4oems[0]->Synchronize(); + for(auto &u : ius4oems) { + u->Initialize(level); + } + } + // Us4OEMs are initialized here. + } + +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_IUS4OEMINITIALIZERIMPL_H diff --git a/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializerImplTest.cpp b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializerImplTest.cpp new file mode 100644 index 000000000..3cbf9e34a --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializerImplTest.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include "arrus/core/devices/us4r/tests/MockIUs4OEM.h" +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h" +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializerImpl.h" + +namespace { + +using namespace arrus::devices; +using ::testing::Return; +using ::testing::InSequence; + +// Test if the input array is approprietaly sorted +TEST(IUs4OEMInitializerImplTest, Us4OEMsSortedApproprietaly) { + std::vector ius4oems; + ius4oems.emplace_back(std::make_unique<::testing::NiceMock>()); + ius4oems.emplace_back(std::make_unique<::testing::NiceMock>()); + ius4oems.emplace_back(std::make_unique<::testing::NiceMock>()); + ON_CALL(GET_MOCK_PTR(ius4oems[0]), GetID) + .WillByDefault(Return(4)); + ON_CALL(GET_MOCK_PTR(ius4oems[1]), GetID) + .WillByDefault(Return(0)); + ON_CALL(GET_MOCK_PTR(ius4oems[2]), GetID) + .WillByDefault(Return(2)); + + IUs4OEMInitializerImpl initializer; + initializer.initModules(ius4oems); + + EXPECT_EQ(ius4oems[0]->GetID(), 0); + EXPECT_EQ(ius4oems[1]->GetID(), 2); + EXPECT_EQ(ius4oems[2]->GetID(), 4); +} + +// Test the order of initialization. + +TEST(IUs4OEMInitializerImplTest, Us4OEMsInitializedProperly) { + std::vector ius4oems; + + ius4oems.emplace_back(std::make_unique<::testing::NiceMock>()); + ius4oems.emplace_back(std::make_unique<::testing::NiceMock>()); + + ON_CALL(GET_MOCK_PTR(ius4oems[0]), GetID) + .WillByDefault(Return(4)); + ON_CALL(GET_MOCK_PTR(ius4oems[1]), GetID) + .WillByDefault(Return(0)); + + // The actual order: 1, 0 + { + InSequence seq; + + EXPECT_CALL(GET_MOCK_PTR(ius4oems[1]), Initialize(1)); + EXPECT_CALL(GET_MOCK_PTR(ius4oems[0]), Initialize(1)); + + EXPECT_CALL(GET_MOCK_PTR(ius4oems[1]), Synchronize()); + EXPECT_CALL(GET_MOCK_PTR(ius4oems[1]), Initialize(2)); + EXPECT_CALL(GET_MOCK_PTR(ius4oems[0]), Initialize(2)); + + EXPECT_CALL(GET_MOCK_PTR(ius4oems[1]), Synchronize()); + EXPECT_CALL(GET_MOCK_PTR(ius4oems[1]), Initialize(3)); + EXPECT_CALL(GET_MOCK_PTR(ius4oems[0]), Initialize(3)); + + EXPECT_CALL(GET_MOCK_PTR(ius4oems[1]), Synchronize()); + EXPECT_CALL(GET_MOCK_PTR(ius4oems[1]), Initialize(4)); + EXPECT_CALL(GET_MOCK_PTR(ius4oems[0]), Initialize(4)); + } + + IUs4OEMInitializerImpl initializer; + initializer.initModules(ius4oems); +} + + +} + + diff --git a/arrus/core/devices/us4r/external/ius4oem/LNAGainValueMap.h b/arrus/core/devices/us4r/external/ius4oem/LNAGainValueMap.h new file mode 100644 index 000000000..05ee83c58 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/LNAGainValueMap.h @@ -0,0 +1,63 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_LNAGAINVALUEMAPPER_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_LNAGAINVALUEMAPPER_H + +#include +#include +#include + +#include "arrus/core/api/common/types.h" + +namespace arrus::devices { + +class LNAGainValueMap { + +public: + using ValueType = uint16; + + static LNAGainValueMap &getInstance() { + static LNAGainValueMap instance; + return instance; + } + + us4r::afe58jd18::LNA_GAIN_GBL getEnumValue(const ValueType value) { + return valueMap.at(value); + } + + /** + * Returns a sorted set of available values. + */ + std::set getAvailableValues() const { + std::set values; + std::transform(std::begin(valueMap), std::end(valueMap), + std::inserter(values, std::end(values)), + [](auto &val) { + return val.first; + }); + return values; + } + + LNAGainValueMap(LNAGainValueMap const &) = delete; + + void operator=(LNAGainValueMap const &) = delete; + + LNAGainValueMap(LNAGainValueMap const &&) = delete; + + void operator=(LNAGainValueMap const &&) = delete; + +private: + std::unordered_map valueMap; + + LNAGainValueMap() { + valueMap.emplace(ValueType(12), + us4r::afe58jd18::LNA_GAIN_GBL::LNA_GAIN_GBL_12dB); + valueMap.emplace(ValueType(18), + us4r::afe58jd18::LNA_GAIN_GBL::LNA_GAIN_GBL_18dB); + valueMap.emplace(ValueType(24), + us4r::afe58jd18::LNA_GAIN_GBL::LNA_GAIN_GBL_24dB); + } + +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_LNAGAINVALUEMAPPER_H diff --git a/arrus/core/devices/us4r/external/ius4oem/LPFCutoffValueMap.h b/arrus/core/devices/us4r/external/ius4oem/LPFCutoffValueMap.h new file mode 100644 index 000000000..e858b4644 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/LPFCutoffValueMap.h @@ -0,0 +1,69 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_LPFCUTOFFVALUEMAP_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_LPFCUTOFFVALUEMAP_H + +#include +#include +#include + +#include "arrus/core/api/common/types.h" + +namespace arrus::devices { + +class LPFCutoffValueMap { + +public: + using LPFCutoffValueType = uint32; + + static LPFCutoffValueMap &getInstance() { + static LPFCutoffValueMap instance; + return instance; + } + + us4r::afe58jd18::LPF_PROG getEnumValue(const LPFCutoffValueType value) { + return valueMap.at(value); + } + + /** + * Returns a sorted set of available values. + */ + std::set getAvailableValues() const { + std::set values; + std::transform(std::begin(valueMap), std::end(valueMap), + std::inserter(values, std::end(values)), + [](auto &val) { + return val.first; + }); + return values; + } + + LPFCutoffValueMap(LPFCutoffValueMap const &) = delete; + + void operator=(LPFCutoffValueMap const &) = delete; + + LPFCutoffValueMap(LPFCutoffValueMap const &&) = delete; + + void operator=(LPFCutoffValueMap const &&) = delete; + +private: + std::unordered_map valueMap; + + LPFCutoffValueMap() { + valueMap.emplace(10000000, + us4r::afe58jd18::LPF_PROG::LPF_PROG_10MHz); + valueMap.emplace(15000000, + us4r::afe58jd18::LPF_PROG::LPF_PROG_15MHz); + valueMap.emplace(20000000, + us4r::afe58jd18::LPF_PROG::LPF_PROG_20MHz); + valueMap.emplace(30000000, + us4r::afe58jd18::LPF_PROG::LPF_PROG_30MHz); + valueMap.emplace(35000000, + us4r::afe58jd18::LPF_PROG::LPF_PROG_35MHz); + valueMap.emplace(50000000, + us4r::afe58jd18::LPF_PROG::LPF_PROG_50MHz); + } + +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_LPFCUTOFFVALUEMAP_H diff --git a/arrus/core/devices/us4r/external/ius4oem/PGAGainValueMap.h b/arrus/core/devices/us4r/external/ius4oem/PGAGainValueMap.h new file mode 100644 index 000000000..d806b1999 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/PGAGainValueMap.h @@ -0,0 +1,59 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_PGAGAINVALUEMAPPER_H +#define ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_PGAGAINVALUEMAPPER_H + +#include +#include +#include + +#include "arrus/core/api/common/types.h" + +namespace arrus::devices { + +class PGAGainValueMap { + +public: + using ValueType = uint16; + + static PGAGainValueMap &getInstance() { + static PGAGainValueMap instance; + return instance; + } + + us4r::afe58jd18::PGA_GAIN getEnumValue(const ValueType value) { + return valueMap.at(value); + } + + /** + * Returns a sorted set of available values. + */ + std::set getAvailableValues() const { + std::set values; + std::transform(std::begin(valueMap), std::end(valueMap), + std::inserter(values, std::end(values)), + [](auto &val) { + return val.first; + }); + return values; + } + + PGAGainValueMap(PGAGainValueMap const &) = delete; + + void operator=(PGAGainValueMap const &) = delete; + + PGAGainValueMap(PGAGainValueMap const &&) = delete; + + void operator=(PGAGainValueMap const &&) = delete; + +private: + std::unordered_map valueMap; + + PGAGainValueMap() { + valueMap.emplace(ValueType(24), us4r::afe58jd18::PGA_GAIN::PGA_GAIN_24dB); + valueMap.emplace(ValueType(30), us4r::afe58jd18::PGA_GAIN::PGA_GAIN_30dB); + } + +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_EXTERNAL_IUS4OEM_PGAGAINVALUEMAPPER_H diff --git a/arrus/core/devices/us4r/external/ius4oem/Us4RLoggerWrapper.h b/arrus/core/devices/us4r/external/ius4oem/Us4RLoggerWrapper.h new file mode 100644 index 000000000..3f20982b8 --- /dev/null +++ b/arrus/core/devices/us4r/external/ius4oem/Us4RLoggerWrapper.h @@ -0,0 +1,45 @@ +#ifndef ARRUS_CORE_US4R_EXTERNAL_IUS4OEM_US4RLOGGERWRAPPER_H +#define ARRUS_CORE_US4R_EXTERNAL_IUS4OEM_US4RLOGGERWRAPPER_H + +#include +#include + +// us4r +#include + +#include "arrus/core/api/common/Logger.h" + +namespace arrus::devices { + +class Us4RLoggerWrapper : public us4r::Logger { +public: + + Us4RLoggerWrapper(arrus::Logger::SharedHandle logger) + : logger(std::move(logger)) {} + + void + log(const us4r::LogSeverity severity, const std::string &msg) override { + logger->log(sevMap.at(severity), msg); + } + + void + setAttribute(const std::string &key, const std::string &value) override { + logger->setAttribute(key, value); + } + +private: + arrus::Logger::SharedHandle logger; + + static const inline std::unordered_map sevMap{ + {us4r::LogSeverity::TRACE, arrus::LogSeverity::TRACE}, + {us4r::LogSeverity::DEBUG, arrus::LogSeverity::DEBUG}, + {us4r::LogSeverity::INFO, arrus::LogSeverity::INFO}, + {us4r::LogSeverity::WARNING, arrus::LogSeverity::WARNING}, + {us4r::LogSeverity::ERROR, arrus::LogSeverity::ERROR}, + {us4r::LogSeverity::FATAL, arrus::LogSeverity::FATAL}, + }; +}; + +} + +#endif //ARRUS_CORE_US4R_EXTERNAL_IUS4OEM_US4RLOGGERWRAPPER_H diff --git a/arrus/core/devices/us4r/hv/HV256Factory.h b/arrus/core/devices/us4r/hv/HV256Factory.h new file mode 100644 index 000000000..f5c019682 --- /dev/null +++ b/arrus/core/devices/us4r/hv/HV256Factory.h @@ -0,0 +1,18 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_HV_HV256FACTORY_H +#define ARRUS_CORE_DEVICES_US4R_HV_HV256FACTORY_H + + +#include "arrus/core/api/devices/us4r/HVSettings.h" +#include "arrus/core/devices/us4r/hv/HV256Impl.h" +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h" + +namespace arrus::devices { + +class HV256Factory { +public: + virtual HV256Impl::Handle getHV256(const HVSettings &settings, IUs4OEM *master) = 0; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_HV_HV256FACTORY_H diff --git a/arrus/core/devices/us4r/hv/HV256FactoryImpl.cpp b/arrus/core/devices/us4r/hv/HV256FactoryImpl.cpp new file mode 100644 index 000000000..2521e2ab6 --- /dev/null +++ b/arrus/core/devices/us4r/hv/HV256FactoryImpl.cpp @@ -0,0 +1,24 @@ +#include "HV256FactoryImpl.h" + +#include +#include +#include + +#include "arrus/core/devices/us4r/external/ius4oem/Us4RLoggerWrapper.h" + +namespace arrus::devices { + + +HV256Impl::Handle HV256FactoryImpl::getHV256(const HVSettings &settings, IUs4OEM *master) { + std::unique_ptr dbarLite(GetDBARLite(dynamic_cast(master))); + Logger::SharedHandle arrusLogger = getLoggerFactory()->getLogger(); + us4r::Logger::Handle logger = + std::make_unique(arrusLogger); + + std::unique_ptr hv256(GetHV256(dbarLite->GetI2CHV(), std::move(logger))); + DeviceId id(DeviceType::HV, 0); + + return std::make_unique(id, settings.getModelId(), + std::move(dbarLite), std::move(hv256)); +} +} \ No newline at end of file diff --git a/arrus/core/devices/us4r/hv/HV256FactoryImpl.h b/arrus/core/devices/us4r/hv/HV256FactoryImpl.h new file mode 100644 index 000000000..df80340e3 --- /dev/null +++ b/arrus/core/devices/us4r/hv/HV256FactoryImpl.h @@ -0,0 +1,15 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_HV_HV256FACTORYIMPL_H +#define ARRUS_CORE_DEVICES_US4R_HV_HV256FACTORYIMPL_H + +#include "HV256Factory.h" + +namespace arrus::devices { + +class HV256FactoryImpl : public HV256Factory { +public: + HV256Impl::Handle getHV256(const HVSettings &settings, IUs4OEM *master) override; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_HV_HV256FACTORYIMPL_H diff --git a/arrus/core/devices/us4r/hv/HV256Impl.cpp b/arrus/core/devices/us4r/hv/HV256Impl.cpp new file mode 100644 index 000000000..79ac40a27 --- /dev/null +++ b/arrus/core/devices/us4r/hv/HV256Impl.cpp @@ -0,0 +1,16 @@ +#include "HV256Impl.h" + +#include + +namespace arrus::devices { + +HV256Impl::HV256Impl(const DeviceId &id, + HVModelId modelId, std::unique_ptr dbarLite, + std::unique_ptr hv256) + : Device(id), + logger(getLoggerFactory()->getLogger()), + modelId(std::move(modelId)), dbarLite(std::move(dbarLite)), hv256(std::move(hv256)) { + + INIT_ARRUS_DEVICE_LOGGER(logger, id.toString()); +} +} \ No newline at end of file diff --git a/arrus/core/devices/us4r/hv/HV256Impl.h b/arrus/core/devices/us4r/hv/HV256Impl.h new file mode 100644 index 000000000..c7c72df3f --- /dev/null +++ b/arrus/core/devices/us4r/hv/HV256Impl.h @@ -0,0 +1,64 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_HV_HV256IMPL_H +#define ARRUS_CORE_DEVICES_US4R_HV_HV256IMPL_H + +#include + +#include +#include + +#include "arrus/core/common/logging.h" +#include "arrus/core/api/devices/Device.h" +#include "arrus/core/api/devices/us4r/HVModelId.h" +#include "arrus/common/format.h" + +namespace arrus::devices { + +class HV256Impl : public Device { +public: + using Handle = std::unique_ptr; + + HV256Impl(const DeviceId &id, HVModelId modelId, + std::unique_ptr dbarLite, + std::unique_ptr hv256); + + void setVoltage(Voltage voltage) { + try { + hv256->EnableHV(); + hv256->SetHVVoltage(voltage); + } catch(std::exception &e) { + // TODO catch a specific exception + logger->log( + LogSeverity::INFO, + ::arrus::format( + "First attempt to set HV voltage failed with " + "message: '{}', trying once more.", + e.what())); + hv256->EnableHV(); + hv256->SetHVVoltage(voltage); + } + } + + void disable() { + try { + hv256->DisableHV(); + } catch(std::exception &e) { + logger->log(LogSeverity::INFO, + ::arrus::format( + "First attempt to disable high voltage failed with " + "message: '{}', trying once more.", + e.what())); + hv256->DisableHV(); + } + } + +private: + Logger::Handle logger; + HVModelId modelId; + std::unique_ptr dbarLite; + std::unique_ptr hv256; +}; + + +} + +#endif //ARRUS_CORE_DEVICES_US4R_HV_HV256IMPL_H diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterFactory.h b/arrus/core/devices/us4r/probeadapter/ProbeAdapterFactory.h new file mode 100644 index 000000000..3799e6b12 --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterFactory.h @@ -0,0 +1,20 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERFACTORY_H +#define ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERFACTORY_H + +#include "arrus/core/api/devices/us4r/ProbeAdapter.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/api/devices/us4r/Us4OEM.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImplBase.h" +#include "ProbeAdapterImplBase.h" + +namespace arrus::devices { +class ProbeAdapterFactory { +public: + virtual ProbeAdapterImplBase::Handle + getProbeAdapter(const ProbeAdapterSettings &settings, + const std::vector &us4oems) = 0; +}; +} + +#endif //ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERFACTORY_H diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterFactoryImpl.h b/arrus/core/devices/us4r/probeadapter/ProbeAdapterFactoryImpl.h new file mode 100644 index 000000000..785946d30 --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterFactoryImpl.h @@ -0,0 +1,50 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERFACTORYIMPL_H +#define ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERFACTORYIMPL_H + +#include + +#include "arrus/core/api/devices/probe/Probe.h" +#include "ProbeAdapterFactory.h" +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidator.h" +#include "ProbeAdapterImpl.h" + +namespace arrus::devices { + +class ProbeAdapterFactoryImpl : public ProbeAdapterFactory { +public: + ProbeAdapterImplBase::Handle + getProbeAdapter(const ProbeAdapterSettings &settings, + const std::vector &us4oems) override { + const DeviceId id(DeviceType::ProbeAdapter, 0); + ProbeAdapterSettingsValidator validator(id.getOrdinal()); + validator.validate(settings); + validator.throwOnErrors(); + + assertCorrectNumberOfUs4OEMs(settings, us4oems); + + return std::make_unique( + id, + settings.getModelId(), + us4oems, + settings.getNumberOfChannels(), + settings.getChannelMapping()); + } + +private: + static void assertCorrectNumberOfUs4OEMs( + const ProbeAdapterSettings &settings, + const std::vector &us4oems) { + std::unordered_set ordinals; + for(auto value : settings.getChannelMapping()) { + ordinals.insert(value.first); + } + ARRUS_REQUIRES_TRUE(ordinals.size() == us4oems.size(), + arrus::format("Incorrect number of us4oems " + "(provided {}, from settings {})", + us4oems.size(), ordinals.size())); + } +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERFACTORYIMPL_H diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterImpl.cpp b/arrus/core/devices/us4r/probeadapter/ProbeAdapterImpl.cpp new file mode 100644 index 000000000..fa5064942 --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterImpl.cpp @@ -0,0 +1,433 @@ +#include "ProbeAdapterImpl.h" + +#include "arrus/core/external/eigen/Dense.h" +#include "arrus/core/devices/us4r/common.h" +#include "arrus/core/common/validation.h" +#include "arrus/core/common/aperture.h" +#include "arrus/core/devices/us4r/FrameChannelMappingImpl.h" +#include "arrus/common/utils.h" + +#undef ERROR + +namespace arrus::devices { + +using namespace ::arrus::ops::us4r; + +ProbeAdapterImpl::ProbeAdapterImpl(DeviceId deviceId, + ProbeAdapterModelId modelId, + std::vector us4oems, + ChannelIdx numberOfChannels, + ChannelMapping channelMapping) + : ProbeAdapterImplBase(deviceId), logger(getLoggerFactory()->getLogger()), + modelId(std::move(modelId)), + us4oems(std::move(us4oems)), + numberOfChannels(numberOfChannels), + channelMapping(std::move(channelMapping)) { + + INIT_ARRUS_DEVICE_LOGGER(logger, id.toString()); +} + +class ProbeAdapterTxRxValidator : public Validator { +public: + ProbeAdapterTxRxValidator(const std::string &componentName, ChannelIdx nChannels) + : Validator(componentName), nChannels(nChannels) {} + + void validate(const TxRxParamsSequence &txRxs) override { + const auto nSamples = txRxs[0].getNumberOfSamples(); + size_t nActiveRxChannels = std::accumulate(std::begin(txRxs[0].getRxAperture()), + std::end(txRxs[0].getRxAperture()), 0) + + txRxs[0].getRxPadding().sum(); + for(size_t firing = 0; firing < txRxs.size(); ++firing) { + const auto &op = txRxs[firing]; + auto firingStr = ::arrus::format("firing {}", firing); + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getRxAperture().size(), size_t(nChannels), firingStr); + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getTxAperture().size(), size_t(nChannels), firingStr); + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getTxDelays().size(), size_t(nChannels), firingStr); + + ARRUS_VALIDATOR_EXPECT_TRUE_M(op.getNumberOfSamples() == nSamples, + "Each Rx should acquire the same number of samples."); + size_t currActiveRxChannels = std::accumulate(std::begin(txRxs[firing].getRxAperture()), + std::end(txRxs[firing].getRxAperture()), 0) + + txRxs[firing].getRxPadding().sum(); + ARRUS_VALIDATOR_EXPECT_TRUE_M(currActiveRxChannels == nActiveRxChannels, + "Each rx aperture should have the same size."); + if(hasErrors()) { + return; + } + } + } + +private: + ChannelIdx nChannels; +}; + +std::tuple +ProbeAdapterImpl::setTxRxSequence(const std::vector &seq, + const TGCCurve &tgcSamples, + uint16 rxBufferSize, uint16 batchSize, + std::optional sri) { + // Validate input sequence + ProbeAdapterTxRxValidator validator( + ::arrus::format("{} tx rx sequence", getDeviceId().toString()), + numberOfChannels); + validator.validate(seq); + validator.throwOnErrors(); + + // Split into multiple arrays. + // us4oem, op number -> aperture/delays + std::unordered_map> txApertures, rxApertures; + std::unordered_map>> txDelaysList; + + // Here is an assumption, that each operation has the same size rx aperture. + auto paddingSize = seq[0].getRxPadding().sum(); + auto rxApertureSize = getNumberOfActiveChannels(seq[0].getRxAperture()) + paddingSize; + auto nFrames = getNumberOfNoRxNOPs(seq); + + // -- Frame channel mapping stuff related to splitting each operation between available + // modules. + + // (logical frame, logical channel) -> physical module + // active rx channel number here refers to the LOCAL ordinal number of + // active channel in the rx aperture; e.g. for rx aperture [0, 32, 42], + // 0 has relative ordinal number 0, 32 has relative number 1, + // 42 has relative number 2. + Eigen::MatrixXi frameModule(nFrames, rxApertureSize); + frameModule.setConstant(FrameChannelMapping::UNAVAILABLE); + // (logical frame, logical channel) -> actual channel on a given us4oem + Eigen::MatrixXi frameChannel(nFrames, rxApertureSize); + frameChannel.setConstant(FrameChannelMapping::UNAVAILABLE); + + // Initialize helper arrays. + for(Ordinal ordinal = 0; ordinal < us4oems.size(); ++ordinal) { + txApertures.emplace(ordinal, std::vector(seq.size())); + rxApertures.emplace(ordinal, std::vector(seq.size())); + txDelaysList.emplace(ordinal, std::vector>(seq.size())); + } + + // Split Tx, Rx apertures and tx delays into sub-apertures specific for + // each us4oem module. + uint32 opNumber = 0; + + uint32 frameNumber = 0; + for(const auto &op : seq) { + logger->log(LogSeverity::TRACE, arrus::format("Setting tx/rx {}", ::arrus::toString(op))); + + const auto &txAperture = op.getTxAperture(); + const auto &rxAperture = op.getRxAperture(); + const auto &txDelays = op.getTxDelays(); + + // TODO change the below to an 'assert' + ARRUS_REQUIRES_TRUE(txAperture.size() == rxAperture.size() + && txAperture.size() == numberOfChannels, + arrus::format( + "Tx and Rx apertures should have a size: {}", + numberOfChannels)); + + for(Ordinal ordinal = 0; ordinal < us4oems.size(); ++ordinal) { + txApertures[ordinal][opNumber].resize(Us4OEMImpl::N_ADDR_CHANNELS); + rxApertures[ordinal][opNumber].resize(Us4OEMImpl::N_ADDR_CHANNELS); + txDelaysList[ordinal][opNumber].resize(Us4OEMImpl::N_ADDR_CHANNELS); + } + + size_t activeAdapterCh = 0; + bool isRxNop = true; + std::vector activeUs4oemCh(us4oems.size(), 0); + + // SPLIT tx/rx/delays between modules + for(size_t ach = 0; ach < numberOfChannels; ++ach) { + + // tx/rx/delays mapping stuff + auto cm = channelMapping[ach]; + Ordinal dstModule = cm.first; + ChannelIdx dstChannel = cm.second; + txApertures[dstModule][opNumber][dstChannel] = txAperture[ach]; + rxApertures[dstModule][opNumber][dstChannel] = rxAperture[ach]; + txDelaysList[dstModule][opNumber][dstChannel] = txDelays[ach]; + + // FC Mapping stuff + if(op.getRxAperture()[ach]) { + isRxNop = false; + frameModule(frameNumber, activeAdapterCh + op.getRxPadding()[0]) = dstModule; + frameChannel(frameNumber, activeAdapterCh + op.getRxPadding()[0]) = + static_cast(activeUs4oemCh[dstModule]); + ++activeAdapterCh; + ++activeUs4oemCh[dstModule]; + } + } + if(!isRxNop) { + ++frameNumber; + } + ++opNumber; + } + + // Create operations for each of the us4oem module. + std::vector seqs(us4oems.size()); + + for(Ordinal us4oemOrdinal = 0; us4oemOrdinal < us4oems.size(); ++us4oemOrdinal) { + auto &us4oemSeq = seqs[us4oemOrdinal]; + + uint16 i = 0; + for(const auto &op : seq) { + // Convert tx aperture to us4oem tx aperture + const auto &txAperture = txApertures[us4oemOrdinal][i]; + const auto &rxAperture = rxApertures[us4oemOrdinal][i]; + const auto &txDelays = txDelaysList[us4oemOrdinal][i]; + + // Intentionally not copying rx padding - us4oem do not allow rx padding. + us4oemSeq.emplace_back(txAperture, txDelays, op.getTxPulse(), + rxAperture, op.getRxSampleRange(), + op.getRxDecimationFactor(), op.getPri(), + Tuple({0, 0})); + ++i; + } + // keep operations with empty tx or rx aperture - they are still a part of the larger operation + } + // split operations if necessary + + auto[splittedOps, opDstSplittedOp, opDestSplittedCh] = splitRxAperturesIfNecessary(seqs); + + // set sequence on each us4oem + std::vector fcMappings; + FrameChannelMapping::FrameNumber totalNumberOfFrames = 0; + std::vector frameOffsets(seqs.size(), 0); + + // section -> us4oem -> transfer + std::vector> outputTransfers; + + Us4RBufferBuilder us4RBufferBuilder; + for(Ordinal us4oemOrdinal = 0; us4oemOrdinal < us4oems.size(); ++us4oemOrdinal) { + auto &us4oem = us4oems[us4oemOrdinal]; + auto[buffer, fcMapping] = us4oem->setTxRxSequence( + splittedOps[us4oemOrdinal], tgcSamples, rxBufferSize, batchSize, + sri); + frameOffsets[us4oemOrdinal] = totalNumberOfFrames; + totalNumberOfFrames += fcMapping->getNumberOfLogicalFrames(); + fcMappings.push_back(std::move(fcMapping)); + // fcMapping is not valid anymore here + us4RBufferBuilder.pushBackUs4oemBuffer(buffer); + } + + // generate FrameChannelMapping for the adapter output. + FrameChannelMappingBuilder outFcBuilder(nFrames, ARRUS_SAFE_CAST(rxApertureSize, ChannelIdx)); + FrameChannelMappingBuilder::FrameNumber frameIdx = 0; + for(const auto &op: seq) { + if(op.isRxNOP()) { + continue; + } + uint16 activeRxChIdx = 0; + for(auto bit : op.getRxAperture()) { + if(bit) { + // Frame channel mapping determined by distributing op on multiple devices + auto dstModule = frameModule(frameIdx, activeRxChIdx + op.getRxPadding()[0]); + auto dstModuleChannel = frameChannel(frameIdx, activeRxChIdx + op.getRxPadding()[0]); + + // if dstModuleChannel is unavailable, set channel mapping to -1 and continue + // unavailable dstModuleChannel means, that the given channel was virtual + // and has no assigned value. + ARRUS_REQUIRES_DATA_TYPE_E( + dstModuleChannel, int8, + ::arrus::ArrusException( + "Invalid dstModuleChannel data type, " + "rx aperture is outise.")); + if(FrameChannelMapping::isChannelUnavailable((int8) dstModuleChannel)) { + outFcBuilder.setChannelMapping( + frameIdx, activeRxChIdx + op.getRxPadding()[0], + 0, FrameChannelMapping::UNAVAILABLE); + } else { + // Otherwise, we have an actual channel. + ARRUS_REQUIRES_TRUE_E( + dstModule >= 0 && dstModuleChannel >= 0, + arrus::ArrusException("Dst module and dst channel " + "should be non-negative") + ); + + auto dstOp = opDstSplittedOp(dstModule, frameIdx, dstModuleChannel); + auto dstChannel = opDestSplittedCh(dstModule, frameIdx, dstModuleChannel); + FrameChannelMapping::FrameNumber destFrame = 0; + int8 destFrameChannel = -1; + if(!FrameChannelMapping::isChannelUnavailable(dstChannel)) { + auto res = fcMappings[dstModule]->getLogical(dstOp, dstChannel); + destFrame = res.first; + destFrameChannel = res.second; + } + outFcBuilder.setChannelMapping( + frameIdx, activeRxChIdx + op.getRxPadding()[0], + destFrame + frameOffsets[dstModule], + destFrameChannel); + } + ++activeRxChIdx; + } + } + ++frameIdx; + } + return {us4RBufferBuilder.build(), outFcBuilder.build()}; +} + +Ordinal ProbeAdapterImpl::getNumberOfUs4OEMs() { + return ARRUS_SAFE_CAST(this->us4oems.size(), Ordinal); +} + +void ProbeAdapterImpl::start() { +// EnableSequencer resets position of the us4oem sequencer. + for(auto &us4oem: this->us4oems) { + us4oem->enableSequencer(); + } + this->us4oems[0]->startTrigger(); +} + +void ProbeAdapterImpl::stop() { + this->us4oems[0]->stop(); +} + +void ProbeAdapterImpl::syncTrigger() { + this->us4oems[0]->syncTrigger(); +} + +void ProbeAdapterImpl::registerOutputBuffer(Us4ROutputBuffer *buffer, const Us4RBuffer::Handle &transfers) { + Ordinal us4oemOrdinal = 0; + for(auto &us4oem: us4oems) { + auto us4oemBuffer = transfers->getUs4oemBuffer(us4oemOrdinal); + registerOutputBuffer(buffer, us4oemBuffer, us4oem); + ++us4oemOrdinal; + } +} + +void ProbeAdapterImpl::registerOutputBuffer(Us4ROutputBuffer *outputBuffer, + const Us4OEMBuffer &transfers, + Us4OEMImplBase::RawHandle us4oem) { + // Each transfer should have the same size. + std::unordered_set sizes; + for(auto &transfer: transfers.getElements()) { + sizes.insert(transfer.getSize()); + } + if(sizes.size() > 1) { + throw ::arrus::ArrusException("Each transfer should have the same size."); + } + // This is the size of a single element produced by this us4oem. + const size_t elementSize = *std::begin(sizes); + if(elementSize == 0) { + // This us4oem will not transfer any data, so the buffer registration has no sense here. + return; + } + // Output buffer - assuming that the number of elements is a multiple of number of transfers + const auto rxBufferSize = ARRUS_SAFE_CAST(transfers.getNumberOfElements(), uint16); + const uint16 hostBufferSize = outputBuffer->getNumberOfElements(); + const Ordinal ordinal = us4oem->getDeviceId().getOrdinal(); + + // Prepare host buffers + uint16 hostElement = 0; + uint16 rxElement = 0; + + auto ius4oem = us4oem->getIUs4oem(); + + while(hostElement < hostBufferSize) { + auto dstAddress = outputBuffer->getAddress(hostElement, ordinal); + auto srcAddress = transfers.getElement(rxElement).getAddress(); + logger->log(LogSeverity::DEBUG, ::arrus::format("Preparing host buffer to {} from {}, size {}", + (size_t) dstAddress, (size_t) srcAddress, elementSize)); + ius4oem->PrepareHostBuffer(dstAddress, elementSize, srcAddress); + ++hostElement; + rxElement = (rxElement + 1) % rxBufferSize; + } + + // prepare transfers + uint16 transferIdx = 0; + uint16 startFiring = 0; + + for(auto &transfer: transfers.getElements()) { + auto dstAddress = outputBuffer->getAddress(transferIdx, ordinal); + auto srcAddress = transfer.getAddress(); + auto endFiring = transfer.getFiring(); + + ius4oem->PrepareTransferRXBufferToHost( + transferIdx, dstAddress, elementSize, srcAddress); + + ius4oem->ScheduleTransferRXBufferToHost( + endFiring, transferIdx, + [this, ius4oem, outputBuffer, ordinal, transferIdx, startFiring, + endFiring, srcAddress, elementSize, + rxBufferSize, hostBufferSize, + element = transferIdx]() mutable { + try { + auto dstAddress = outputBuffer->getAddress((uint16) element, ordinal); + ius4oem->MarkEntriesAsReadyForReceive(startFiring, endFiring); + logger->log(LogSeverity::DEBUG, ::arrus::format("Rx Released: {}, {}", startFiring, endFiring)); + + // Prepare transfer for the next iteration. + ius4oem->PrepareTransferRXBufferToHost( + transferIdx, dstAddress, elementSize, srcAddress); + ius4oem->ScheduleTransferRXBufferToHost(endFiring, transferIdx, nullptr); + + bool cont = outputBuffer->signal(ordinal, element, + HostBuffer::INF_TIMEOUT); // Also a callback function can be used here. + if(!cont) { + logger->log(LogSeverity::DEBUG, "Output buffer shut down."); + return; + } + // TODO the below should be called when buffer element is released. + cont = outputBuffer->waitForRelease(ordinal, element, HostBuffer::INF_TIMEOUT); + if(!cont) { + logger->log(LogSeverity::DEBUG, "Output buffer shut down"); + return; + } + ius4oem->MarkEntriesAsReadyForTransfer(startFiring, endFiring); + logger->log(LogSeverity::DEBUG, ::arrus::format("Host Released: {}, {}", startFiring, endFiring)); + element = (element + rxBufferSize) % hostBufferSize; + } catch(const std::exception &e) { + logger->log(LogSeverity::ERROR, "Us4OEM: " + + std::to_string(ordinal) + + " transfer callback ended with an exception: " + + e.what()); + } catch(...) { + logger->log(LogSeverity::ERROR, "Us4OEM: " + + std::to_string(ordinal) + + " transfer callback ended with unknown exception"); + } + + } + ); + // TODO register in the us4oem + startFiring = endFiring + 1; + ++transferIdx; + } + // Register overflow callbacks (mark output buffer as invalid) + + ius4oem->RegisterReceiveOverflowCallback([this, outputBuffer]() { + try { + this->logger->log(LogSeverity::ERROR, "Rx buffer overflow, stopping the device."); + this->getMasterUs4oem()->stop(); + outputBuffer->markAsInvalid(); + } catch (const std::exception &e) { + logger->log(LogSeverity::ERROR, "Receive overflow callback ended with an exception: " + + std::string(e.what())); + } catch (...) { + logger->log(LogSeverity::ERROR, "Receive overflow callback ended with unknown exception"); + } + + }); + + ius4oem->RegisterTransferOverflowCallback([this, outputBuffer]() { + try { + this->logger->log(LogSeverity::ERROR, "Host buffer overflow, stopping the device."); + this->getMasterUs4oem()->stop(); + outputBuffer->markAsInvalid(); + } catch (const std::exception &e) { + logger->log(LogSeverity::ERROR, "Receive overflow callback ended with an exception: " + + std::string(e.what())); + } catch (...) { + logger->log(LogSeverity::ERROR, "Receive overflow callback ended with unknown exception"); + } + + }); +} + +void ProbeAdapterImpl::setTgcCurve(const TGCCurve &curve) { + for(auto &us4oem: us4oems) { + us4oem->setTgcCurve(curve); + } +} + +} \ No newline at end of file diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterImpl.h b/arrus/core/devices/us4r/probeadapter/ProbeAdapterImpl.h new file mode 100644 index 000000000..407ea8985 --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterImpl.h @@ -0,0 +1,68 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERIMPL_H +#define ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERIMPL_H + +#include + +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterImplBase.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImpl.h" +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +#include "arrus/core/devices/TxRxParameters.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/common/asserts.h" +#include "arrus/core/common/logging.h" +#include "arrus/core/devices/us4r/Us4RBuffer.h" + +namespace arrus::devices { + +class ProbeAdapterImpl : public ProbeAdapterImplBase { +public: + using Handle = std::unique_ptr; + using RawHandle = PtrHandle; + + using ChannelAddress = ProbeAdapterSettings::ChannelAddress; + using ChannelMapping = ProbeAdapterSettings::ChannelMapping; + + ProbeAdapterImpl(DeviceId deviceId, ProbeAdapterModelId modelId, + std::vector us4oems, + ChannelIdx numberOfChannels, + ChannelMapping channelMapping); + + [[nodiscard]] ChannelIdx getNumberOfChannels() const override { + return numberOfChannels; + } + + std::tuple + setTxRxSequence(const std::vector &seq, + const ::arrus::ops::us4r::TGCCurve &tgcSamples, + uint16 rxBufferSize = 1, uint16 rxBatchSize = 1, + std::optional sri = std::nullopt) override; + + Ordinal getNumberOfUs4OEMs() override; + + void start() override; + + void stop() override; + + void syncTrigger() override; + + void registerOutputBuffer(Us4ROutputBuffer *buffer, const Us4RBuffer::Handle &transfers); + + void setTgcCurve(const arrus::ops::us4r::TGCCurve &curve) override; + +private: + Logger::Handle logger; + ProbeAdapterModelId modelId; + std::vector us4oems; + ChannelIdx numberOfChannels; + ChannelMapping channelMapping; + + void registerOutputBuffer(Us4ROutputBuffer *outputBuffer, const Us4OEMBuffer &transfers, Us4OEMImplBase::RawHandle us4oem); + + Us4OEMImplBase::RawHandle getMasterUs4oem() const { + return this->us4oems[0]; + } +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERIMPL_H diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterImplBase.h b/arrus/core/devices/us4r/probeadapter/ProbeAdapterImplBase.h new file mode 100644 index 000000000..1371b1a7b --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterImplBase.h @@ -0,0 +1,43 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERIMPLBASE_H +#define ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERIMPLBASE_H + +#include "arrus/core/api/devices/us4r/ProbeAdapter.h" +#include "arrus/core/api/devices/us4r/FrameChannelMapping.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/devices/TxRxParameters.h" +#include "arrus/core/devices/us4r/DataTransfer.h" +#include "arrus/core/api/devices/us4r/HostBuffer.h" +#include "arrus/core/devices/us4r/Us4ROutputBuffer.h" +#include "arrus/core/devices/us4r/Us4RBuffer.h" + +namespace arrus::devices { + +class ProbeAdapterImplBase : public ProbeAdapter { +public: + using ProbeAdapter::ProbeAdapter; + + using Handle = std::unique_ptr; + using RawHandle = PtrHandle; + + virtual + std::tuple + setTxRxSequence(const std::vector &seq, + const ::arrus::ops::us4r::TGCCurve &tgcSamples, + uint16 rxBufferSize, uint16 rxBatchSize, + std::optional sri) = 0; + + virtual + void registerOutputBuffer(Us4ROutputBuffer *buffer, const Us4RBuffer::Handle &transfers) = 0; + + virtual Ordinal getNumberOfUs4OEMs() = 0; + + virtual void start() = 0; + + virtual void stop() = 0; + + virtual void syncTrigger() = 0; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERIMPLBASE_H diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterImplTest.cpp b/arrus/core/devices/us4r/probeadapter/ProbeAdapterImplTest.cpp new file mode 100644 index 000000000..733220884 --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterImplTest.cpp @@ -0,0 +1,1096 @@ +#include +#include +#include + +#include "ProbeAdapterImpl.h" + +#include "arrus/core/devices/us4r/us4oem/Us4OEMImplBase.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/common/tests.h" +#include "arrus/common/logging/impl/Logging.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/common/collections.h" + +namespace { + +using namespace arrus; +using namespace arrus::devices; +using namespace arrus::ops::us4r; +using ::testing::_; +using ::testing::ElementsAre; +using ::testing::Property; +using ::testing::Eq; +using ::testing::Return; +using ::testing::ByMove; +using ::testing::AllOf; + +const ChannelIdx DEFAULT_NCHANNELS = 64; + +struct TestTxRxParams { + + TestTxRxParams() { + for(int i = 0; i < 32; ++i) { + rxAperture[i] = true; + } + } + + BitMask txAperture = getNTimes(true, DEFAULT_NCHANNELS); + std::vector txDelays = getNTimes(0.0f, DEFAULT_NCHANNELS); + ops::us4r::Pulse pulse{2.0e6f, 2.5f, true}; + BitMask rxAperture = getNTimes(false, DEFAULT_NCHANNELS); + uint32 decimationFactor = 1; + float pri = 100e-6f; + Interval sampleRange{0, 4096}; + Tuple rxPadding{0, 0}; + + [[nodiscard]] TxRxParameters getTxRxParameters() const { + return TxRxParameters(txAperture, txDelays, pulse, + rxAperture, sampleRange, + decimationFactor, pri, rxPadding); + } +}; + +BitMask getDefaultTxAperture(ChannelIdx nchannels) { + return BitMask(nchannels, true); +} + +BitMask getDefaultRxAperture(ChannelIdx nchannels) { + BitMask aperture(nchannels, false); + ::arrus::setValuesInRange(aperture, 0, Us4OEMImpl::N_RX_CHANNELS, true); + return aperture; +} + +std::vector getDefaultTxDelays(ChannelIdx nchannels) { + return getNTimes(0.0f, nchannels); +} + +std::tuple< + Us4OEMBuffer, + FrameChannelMapping::Handle> +createEmptySetTxRxResult(FrameChannelMapping::FrameNumber nFrames, ChannelIdx nChannels) { + FrameChannelMappingBuilder builder(nFrames, nChannels); + for(int i = 0; i < nFrames; ++i) { + for(int j = 0; j < nChannels; ++j) { + builder.setChannelMapping(i, j, i, j); + } + } + Us4OEMBuffer buffer({Us4OEMBufferElement(0, 10, 0)}); + return std::make_tuple(buffer, builder.build()); +} + +class MockUs4OEM : public Us4OEMImplBase { +public: + explicit MockUs4OEM(Ordinal id) + : Us4OEMImplBase(DeviceId(DeviceType::Us4OEM, id)) {} + + MOCK_METHOD( + (std::tuple), + setTxRxSequence, + (const TxRxParamsSequence &seq, const ::arrus::ops::us4r::TGCCurve &tgc, + uint16 rxBufferSize, uint16 batchSize, std::optional sri), + (override)); + MOCK_METHOD(Interval, getAcceptedVoltageRange, (), (override)); + MOCK_METHOD(double, getSamplingFrequency, (), (override)); + MOCK_METHOD(void, startTrigger, (), (override)); + MOCK_METHOD(void, stopTrigger, (), (override)); + MOCK_METHOD(void, start, (), (override)); + MOCK_METHOD(void, stop, (), (override)); + MOCK_METHOD(void, syncTrigger, (), (override)); + MOCK_METHOD(bool, isMaster, (), (override)); + MOCK_METHOD(void, setTgcCurve, (const ::arrus::ops::us4r::TGCCurve &tgc), (override)); + MOCK_METHOD(Ius4OEMRawHandle, getIUs4oem, (), (override)); + MOCK_METHOD(void, enableSequencer, (), (override)); +}; + +class AbstractProbeAdapterImplTest : public ::testing::Test { + using NiceMockHandle = std::unique_ptr<::testing::NiceMock>; +protected: + void SetUp() override { + us4oems.push_back(std::make_unique<::testing::NiceMock>(0)); + us4oemsPtr.push_back(us4oems[0].get()); + us4oems.push_back(std::make_unique<::testing::NiceMock>(1)); + us4oemsPtr.push_back(us4oems[1].get()); + probeAdapter = std::make_unique( + DeviceId(DeviceType::ProbeAdapter, 0), + ProbeAdapterModelId("test", "test"), + us4oemsPtr, getNChannels(), getChannelMapping()); + } + + virtual ProbeAdapterImpl::ChannelMapping getChannelMapping() = 0; + + virtual ChannelIdx getNChannels() { + return DEFAULT_NCHANNELS; + } + + std::vector us4oems; + std::vector us4oemsPtr; + ProbeAdapterImpl::Handle probeAdapter; + TGCCurve defaultTGCCurve; +}; + +class ProbeAdapter64ChannelsTest : public AbstractProbeAdapterImplTest { + // An adapter with 64 channels. + // 0-32 channels to us4oem:0 + // 32-64 channels to us4oem:1 + ProbeAdapterImpl::ChannelMapping getChannelMapping() override { + ProbeAdapterImpl::ChannelMapping mapping(getNChannels()); + for(ChannelIdx ch = 0; ch < getNChannels(); ++ch) { + mapping[ch] = {ch / 2, ch % 32}; + } + return mapping; + } +}; + +// ------------------------------------------ Test validation +TEST_F(ProbeAdapter64ChannelsTest, ChecksRxApertureSize) { + BitMask rxAperture(128, false); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + + // Throw: nchannels = 64, rx aperture size = 128 + EXPECT_THROW(probeAdapter->setTxRxSequence(seq, defaultTGCCurve), + IllegalArgumentException); +} + +TEST_F(ProbeAdapter64ChannelsTest, ChecksTxApertureSize) { + BitMask txAperture(32, false); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txAperture = txAperture)) + .getTxRxParameters() + }; + + // Throw: nchannels = 64, aperture size = 32 + EXPECT_THROW(probeAdapter->setTxRxSequence(seq, defaultTGCCurve), + IllegalArgumentException); +} + +TEST_F(ProbeAdapter64ChannelsTest, ChecksTxDelaysSize) { + std::vector txDelays(65, false); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txDelays = txDelays)) + .getTxRxParameters() + }; + + // Throw: nchannels = 64, number of delays = 65 + EXPECT_THROW(probeAdapter->setTxRxSequence(seq, defaultTGCCurve), + IllegalArgumentException); +} + +// ------------------------------------------ Test aperture/delays distribution + +class ProbeAdapterChannelMapping1Test : public AbstractProbeAdapterImplTest { + // An adapter with 64 channels. + // 0-32 channels to us4oem:0 + // 32-64 channels to us4oem:1 + ProbeAdapterImpl::ChannelMapping getChannelMapping() override { + ProbeAdapterImpl::ChannelMapping mapping(getNChannels()); + for(ChannelIdx ch = 0; ch < getNChannels(); ++ch) { + mapping[ch] = {ch / 32, ch % 32}; + } + return mapping; + } +}; + +#define EXPECT_SEQUENCE_PROPERTY_NFRAMES(deviceId, matcher, nFrames) \ + do { \ + \ + EXPECT_CALL(*(us4oems[deviceId].get()), setTxRxSequence(matcher, _, _, _, _)) \ + .WillOnce(Return(ByMove(createEmptySetTxRxResult(nFrames, 32)))); \ + } while(0) + +#define EXPECT_SEQUENCE_PROPERTY(deviceId, matcher) \ + EXPECT_SEQUENCE_PROPERTY_NFRAMES(deviceId, matcher, 1) + +#define SET_TX_RX_SEQUENCE(probeAdapter, seq) \ + probeAdapter->setTxRxSequence(seq, defaultTGCCurve) + +#define US4OEM_MOCK_SET_TX_RX_SEQUENCE() \ + setTxRxSequence(_, _, _, _, _) + + +TEST_F(ProbeAdapterChannelMapping1Test, DistributesTxAperturesCorrectly) { + BitMask txAperture(64, false); + ::arrus::setValuesInRange(txAperture, 20, 40, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txAperture = txAperture)) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 20, 32, true); + EXPECT_SEQUENCE_PROPERTY(0, ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp0))); + + BitMask expectedTxAp1(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp1, 0, 8, true); + EXPECT_SEQUENCE_PROPERTY(1, ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMapping1Test, DistributesRxAperturesCorrectly) { + BitMask rxAperture(64, false); + ::arrus::setValuesInRange(rxAperture, 15, 51, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 15, 32, true); + EXPECT_SEQUENCE_PROPERTY(0, + ElementsAre(Property(&TxRxParameters::getRxAperture, expectedTxAp0))); + + BitMask expectedTxAp1(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp1, 0, 19, true); + EXPECT_SEQUENCE_PROPERTY(1, + ElementsAre(Property(&TxRxParameters::getRxAperture, expectedTxAp1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMapping1Test, DistributesTxDelaysCorrectly) { + std::vector delays(64, 0.0f); + for(int i = 18; i < 44; ++i) { + delays[i] = i * 5e-6; + } + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txDelays = delays)) + .getTxRxParameters() + }; + + std::vector delays0(Us4OEMImpl::N_TX_CHANNELS, 0); + for(int i = 18; i < 32; ++i) { + delays0[i] = i * 5e-6; + } + EXPECT_SEQUENCE_PROPERTY(0, + ElementsAre(Property(&TxRxParameters::getTxDelays, delays0))); + + std::vector delays1(Us4OEMImpl::N_TX_CHANNELS, 0); + for(int i = 0; i < 44 - 32; ++i) { + delays1[i] = (i + 32) * 5e-6; + } + EXPECT_SEQUENCE_PROPERTY(1, + ElementsAre(Property(&TxRxParameters::getTxDelays, delays1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMapping1Test, DistributesTxAperturesCorrectlySingleUs4OEM0) { + BitMask txAperture(64, false); + ::arrus::setValuesInRange(txAperture, 10, 21, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txAperture = txAperture)) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 10, 21, true); + EXPECT_SEQUENCE_PROPERTY(0, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp0))); + + BitMask expectedTxAp1(Us4OEMImpl::N_ADDR_CHANNELS, false); + EXPECT_SEQUENCE_PROPERTY(1, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMapping1Test, DistributesTxAperturesCorrectlySingleUs4OEM1) { + BitMask txAperture(64, false); + ::arrus::setValuesInRange(txAperture, 42, 61, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txAperture = txAperture)) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_ADDR_CHANNELS, false); + EXPECT_SEQUENCE_PROPERTY(0, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp0))); + + BitMask expectedTxAp1(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp1, 10, 29, true); + EXPECT_SEQUENCE_PROPERTY(1, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +class ProbeAdapterChannelMappingEsaote3Test : public AbstractProbeAdapterImplTest { + // An adapter with 192 channels. + // 0-32, 64-96, 128-160 channels to us4oem:0 + // 32-64, 96-128, 160-192 channels to us4oem:1 +protected: + ProbeAdapterImpl::ChannelMapping getChannelMapping() override { + ProbeAdapterImpl::ChannelMapping mapping(getNChannels()); + for(ChannelIdx ch = 0; ch < getNChannels(); ++ch) { + auto group = ch / 32; + auto module = group % 2; + mapping[ch] = {module, ch % 32 + 32 * (group / 2)}; + } + return mapping; + } + + ChannelIdx getNChannels() override { + return 192; + } +}; + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, DistributesTxAperturesCorrectlySingleUs4OEM) { + BitMask txAperture(getNChannels(), false); + ::arrus::setValuesInRange(txAperture, 65, 80, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = txAperture, + x.rxAperture = getDefaultRxAperture(getNChannels()), + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 33, 48, true); + EXPECT_SEQUENCE_PROPERTY(0, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp0))); + + BitMask expectedTxAp1(Us4OEMImpl::N_TX_CHANNELS, false); + EXPECT_SEQUENCE_PROPERTY(1, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, DistributesTxAperturesCorrectlyTwoSubapertures) { + BitMask txAperture(getNChannels(), false); + ::arrus::setValuesInRange(txAperture, 128 + 14, 128 + 40, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = txAperture, + x.rxAperture = getDefaultRxAperture(getNChannels()), + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 64 + 14, 64 + 32, true); + EXPECT_SEQUENCE_PROPERTY(0, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp0))); + + BitMask expectedTxAp1(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp1, 64 + 0, 64 + 8, true); + EXPECT_SEQUENCE_PROPERTY(1, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, DistributesTxAperturesCorrectlyThreeSubapertures) { + BitMask txAperture(getNChannels(), false); + ::arrus::setValuesInRange(txAperture, 16, 80, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = txAperture, + x.rxAperture = getDefaultRxAperture(getNChannels()), + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 16, 48, true); + EXPECT_SEQUENCE_PROPERTY(0, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp0))); + + BitMask expectedTxAp1(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp1, 0, 32, true); + EXPECT_SEQUENCE_PROPERTY(1, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, DistributesTxAperturesWithGapsCorrectly) { + BitMask txAperture(getNChannels(), false); + ::arrus::setValuesInRange(txAperture, 0 + 8, 160 + 30, true); + + txAperture[0 + 14] = txAperture[0 + 17] + = txAperture[32 + 23] = txAperture[32 + 24] + = txAperture[64 + 25] = txAperture[160 + 7] = false; + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = txAperture, + x.rxAperture = getDefaultRxAperture(getNChannels()), + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 8, 96, true); + expectedTxAp0[0 + 14] = expectedTxAp0[0 + 17] = expectedTxAp0[32 + 25] = false; + EXPECT_SEQUENCE_PROPERTY(0, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp0))); + + BitMask expectedTxAp1(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp1, 0, 64 + 30, true); + expectedTxAp1[0 + 23] = expectedTxAp1[0 + 24] = expectedTxAp1[64 + 7] = false; + EXPECT_SEQUENCE_PROPERTY(1, + ElementsAre(Property(&TxRxParameters::getTxAperture, expectedTxAp1))); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, DistributesAperturesCorrectlyForMultipleRxApertures) { + BitMask txAperture(getNChannels(), false); + ::arrus::setValuesInRange(txAperture, 0 + 8, 160 + 30, true); + + BitMask rxAperture(getNChannels(), false); + ::arrus::setValuesInRange(rxAperture, 16, 96, true); + rxAperture[0 + 18] = rxAperture[32 + 23] = false; + // There should be two apertures: [16, 80], [80, 100] with two gaps: 18, 55 + + txAperture[0 + 14] = txAperture[0 + 17] + = txAperture[32 + 23] = txAperture[32 + 24] + = txAperture[64 + 25] = txAperture[160 + 7] = false; + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = txAperture, + x.rxAperture = rxAperture, + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 8, 96, true); + expectedTxAp0[0 + 14] = expectedTxAp0[0 + 17] = expectedTxAp0[32 + 25] = false; + + BitMask expectedRxAp00(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedRxAp00, 16, 32 + 80 - 64, true); + expectedRxAp00[18] = false; + // TODO(pjarosik) this should be done in a pretty more clever way, to minimize + // potential transfers that are needed + // Instead, the next one channel can be used here + expectedRxAp00[18 + 32] = true; + BitMask expectedRxAp01(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedRxAp01, 32 + 80 - 64, 64, true); + // 18+32 is already covered by op 0 + expectedRxAp01[18 + 32] = false; + + EXPECT_SEQUENCE_PROPERTY_NFRAMES( + + 0, + // Tx aperture should stay the same. + // Rx aperture should be adjusted appropriately. + ElementsAre( + AllOf( + Property(&TxRxParameters::getTxAperture, expectedTxAp0), + Property(&TxRxParameters::getRxAperture, expectedRxAp00) + ), + AllOf( + Property(&TxRxParameters::getTxAperture, expectedTxAp0), + Property(&TxRxParameters::getRxAperture, expectedRxAp01) + ) + ), + 2 + ); + + BitMask expectedTxAp1(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp1, 0, 64 + 30, true); + expectedTxAp1[0 + 23] = expectedTxAp1[0 + 24] = expectedTxAp1[64 + 7] = false; + + BitMask expectedRxAp10(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedRxAp10, 0, 32, true); + expectedRxAp10[23] = false; + + BitMask expectedRxAp11(Us4OEMImpl::N_ADDR_CHANNELS, false); + // second aperture should be empty + EXPECT_SEQUENCE_PROPERTY_NFRAMES( + 1, + ElementsAre( + AllOf( + Property(&TxRxParameters::getTxAperture, expectedTxAp1), + Property(&TxRxParameters::getRxAperture, expectedRxAp10) + ), + AllOf( + Property(&TxRxParameters::getTxAperture, expectedTxAp1), + Property(&TxRxParameters::getRxAperture, expectedRxAp11)) + ), + 2 + ); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, DistributesAperturesCorrectlyForMultipleRxAperturesForUs4OEM0) { + // It should keep tx aperture on the second module even if there is no rx aperture for this module + BitMask txAperture(getNChannels(), false); + ::arrus::setValuesInRange(txAperture, 0 + 9, 160 + 31, true); + + BitMask rxAperture(getNChannels(), false); + ::arrus::setValuesInRange(rxAperture, 16, 32, true); + ::arrus::setValuesInRange(rxAperture, 64 + 16, 64 + 32, true); + rxAperture[0 + 18] = rxAperture[64 + 23] = false; + + txAperture[0 + 14] = txAperture[0 + 17] + = txAperture[32 + 23] = txAperture[32 + 24] + = txAperture[64 + 25] = txAperture[160 + 7] = false; + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = txAperture, + x.rxAperture = rxAperture, + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + BitMask expectedTxAp0(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp0, 9, 96, true); + expectedTxAp0[0 + 14] = expectedTxAp0[0 + 17] = expectedTxAp0[32 + 25] = false; + + BitMask expectedRxAp00(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedRxAp00, 16, 32, true); + expectedRxAp00[18] = false; + expectedRxAp00[32 + 18] = true; + BitMask expectedRxAp01(Us4OEMImpl::N_ADDR_CHANNELS, false); + ::arrus::setValuesInRange(expectedRxAp01, 32 + 16, 32 + 32, true); + expectedRxAp01[32 + 23] = false; + expectedRxAp01[32 + 18] = false; + + EXPECT_SEQUENCE_PROPERTY_NFRAMES( + 0, + // Tx aperture should stay the same. + // Rx aperture should be adjusted appropriately. + ElementsAre( + AllOf( + Property(&TxRxParameters::getTxAperture, expectedTxAp0), + Property(&TxRxParameters::getRxAperture, expectedRxAp00) + ), + AllOf( + Property(&TxRxParameters::getTxAperture, expectedTxAp0), + Property(&TxRxParameters::getRxAperture, expectedRxAp01) + ) + ), + 2 + ); + + BitMask expectedTxAp1(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp1, 0, 64 + 31, true); + expectedTxAp1[0 + 23] = expectedTxAp1[0 + 24] = expectedTxAp1[64 + 7] = false; + + // rx apertures should be empty + BitMask expectedRxAp10(Us4OEMImpl::N_ADDR_CHANNELS, false); + BitMask expectedRxAp11(Us4OEMImpl::N_ADDR_CHANNELS, false); + + EXPECT_SEQUENCE_PROPERTY_NFRAMES( + 1, + ElementsAre( + AllOf( + Property(&TxRxParameters::getTxAperture, expectedTxAp1), + Property(&TxRxParameters::getRxAperture, expectedRxAp10) + ), + AllOf( + Property(&TxRxParameters::getTxAperture, expectedTxAp1), + Property(&TxRxParameters::getRxAperture, expectedRxAp11)) + ), + 2 + ); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, DistributesTxAperturesTwoOperations) { + BitMask txAperture0(getNChannels(), false); + ::arrus::setValuesInRange(txAperture0, 20, 64 + 20, true); + BitMask txAperture1(getNChannels(), false); + ::arrus::setValuesInRange(txAperture1, 23, 64 + 23, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = txAperture0, + x.rxAperture = getDefaultRxAperture(getNChannels()), + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters(), + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = txAperture1, + x.rxAperture = getDefaultRxAperture(getNChannels()), + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + BitMask expectedTxAp00(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp00, 20, 32 + 20, true); + BitMask expectedTxAp01(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp01, 23, 32 + 23, true); + EXPECT_SEQUENCE_PROPERTY_NFRAMES( + 0, + ElementsAre( + Property(&TxRxParameters::getTxAperture, expectedTxAp00), + Property(&TxRxParameters::getTxAperture, expectedTxAp01) + ), + 2 + ); + + BitMask expectedTxAp10(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp10, 0, 32, true); + BitMask expectedTxAp11(Us4OEMImpl::N_TX_CHANNELS, false); + ::arrus::setValuesInRange(expectedTxAp11, 0, 32, true); + EXPECT_SEQUENCE_PROPERTY_NFRAMES( + 1, + ElementsAre( + Property(&TxRxParameters::getTxAperture, expectedTxAp10), + Property(&TxRxParameters::getTxAperture, expectedTxAp11) + ), + 2 + ); + + SET_TX_RX_SEQUENCE(probeAdapter, seq); +} + +// ------------------------------------------ Test Frame Channel Mapping +TEST_F(ProbeAdapterChannelMappingEsaote3Test, ProducesCorrectFCMSingleDistributedOperation) { + BitMask rxAperture(getNChannels(), false); + ::arrus::setValuesInRange(rxAperture, 16, 72, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = getDefaultTxAperture(getNChannels()), + x.rxAperture = rxAperture, + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + FrameChannelMappingBuilder builder0(1, Us4OEMImpl::N_RX_CHANNELS); + for(int i = 0; i < 32; ++i) { + if(i < 24) { + builder0.setChannelMapping(0, i, 0, i); + } else { + builder0.setChannelMapping(0, i, 0, -1); + } + } + auto fcm0 = builder0.build(); + + FrameChannelMappingBuilder builder1(1, Us4OEMImpl::N_RX_CHANNELS); + for(int i = 0; i < 32; ++i) { + builder1.setChannelMapping(0, i, 0, i); + } + auto fcm1 = builder1.build(); + Us4OEMBuffer us4oemBuffer({Us4OEMBufferElement(0, 10, 0)}); + + std::tuple res0(us4oemBuffer, std::move(fcm0)); + std::tuple res1(us4oemBuffer, std::move(fcm1)); + + EXPECT_CALL(*(us4oems[0].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res0)))); + EXPECT_CALL(*(us4oems[1].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res1)))); + + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(probeAdapter, seq); + + EXPECT_EQ(1, fcm->getNumberOfLogicalFrames()); + EXPECT_EQ(72 - 16, fcm->getNumberOfLogicalChannels()); + + for(int i = 0; i < 16; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + EXPECT_EQ(0, frame); + EXPECT_EQ(channel, i); + } + + for(int i = 16; i < 16 + 32; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + EXPECT_EQ(1, frame); + EXPECT_EQ(channel, i - 16); + } + + for(int i = 16 + 32; i < 56; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + EXPECT_EQ(0, frame); + EXPECT_EQ(channel, i - 32); + } +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, ProducesCorrectFCMSingleDistributedOperationWithGaps) { + BitMask rxAperture(getNChannels(), false); + ::arrus::setValuesInRange(rxAperture, 16, 73, true); + // Channels 20, 30 and 40 were masked for given us4oem and data is missing + // Still, the input rx aperture stays as is. + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = getDefaultTxAperture(getNChannels()), + x.rxAperture = rxAperture, + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + FrameChannelMappingBuilder builder0(1, Us4OEMImpl::N_RX_CHANNELS); + for(int i = 0, j = -1; i < 32; ++i) { + int currentJ = -1; + // channels were marked by the us4oem that are missing + if(i != 20-16 && i != 30-16 && i <= 25) { + currentJ = ++j; + } + builder0.setChannelMapping(0, i, 0, currentJ); + } + auto fcm0 = builder0.build(); + + FrameChannelMappingBuilder builder1(1, Us4OEMImpl::N_RX_CHANNELS); + for(int i = 0, j = -1; i < 32; ++i) { + int currentJ = -1; + if(i != 40-32) { + currentJ = ++j; + } + builder1.setChannelMapping(0, i, 0, currentJ); + } + auto fcm1 = builder1.build(); + + Us4OEMBuffer us4oemBuffer({Us4OEMBufferElement(0, 10, 0)}); + + std::tuple res0(us4oemBuffer, std::move(fcm0)); + std::tuple res1(us4oemBuffer, std::move(fcm1)); + + EXPECT_CALL(*(us4oems[0].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res0)))); + EXPECT_CALL(*(us4oems[1].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res1)))); + + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(probeAdapter, seq); + + EXPECT_EQ(1, fcm->getNumberOfLogicalFrames()); + EXPECT_EQ(73-16, fcm->getNumberOfLogicalChannels()); + + std::vector expectedFrames; + for(int i = 16; i < 32; ++i) { + expectedFrames.push_back(0); + } + for(int i = 32; i < 64; ++i) { + expectedFrames.push_back(1); + } + for(int i = 64; i < 73; ++i) { + expectedFrames.push_back(0); + } + std::vector expectedChannels = { + 0, 1, 2, 3, -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, -1, 13, + 0, 1, 2, 3, 4, 5, 6, 7, -1, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, + 14, 15, 16, 17, 18, 19, 20, 21, 22 + }; + + for(int i = 0; i < 73-16; ++i) { + auto [frame, channel] = fcm->getLogical(0, i); + EXPECT_EQ(expectedFrames[i], frame); + EXPECT_EQ(expectedChannels[i], channel); + } +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, ProducesCorrectFCMForMultiOpRxAperture) { + BitMask rxAperture(getNChannels(), false); + ::arrus::setValuesInRange(rxAperture, 48, 128, true); + // RxNOP - the second operation on us4oem + // Ops: us4oem0: 32-64 (64-96), Rx NOP, us4oem1: 16-48, 48-64 + // Channel 99 (us4oem:1 channel 32+3) is masked and data is missing. + // Still, the input rx aperture stays as is. + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = getDefaultTxAperture(getNChannels()), + x.rxAperture = rxAperture, + x.txDelays = getDefaultTxDelays(getNChannels()) + )) + .getTxRxParameters() + }; + // FCM from the us4oem:0 + FrameChannelMappingBuilder builder0(1, Us4OEMImpl::N_RX_CHANNELS); + // The second op is Rx NOP. + for(int i = 0; i < 32; ++i) { + builder0.setChannelMapping(0, i, 0, i); + } + auto fcm0 = builder0.build(); + + FrameChannelMappingBuilder builder1(2, Us4OEMImpl::N_RX_CHANNELS); + // First frame: + for(int i = 0, j = -1; i < 32; ++i) { + int currentJ = -1; + if(i != 16+3) { + currentJ = ++j; + builder1.setChannelMapping(0, i, 0, currentJ); + } else { + builder1.setChannelMapping(0, i, 0, FrameChannelMapping::UNAVAILABLE); + } + } + // Second frame: + for(int i = 0; i < 32; ++i) { + if(i < 16) { + builder1.setChannelMapping(1, i, 1, i); + } + else { + builder1.setChannelMapping(1, i, 1, FrameChannelMapping::UNAVAILABLE); + } + } + auto fcm1 = builder1.build(); + + Us4OEMBuffer us4oemBuffer({Us4OEMBufferElement(0, 10, 0)}); + + std::tuple res0(us4oemBuffer, std::move(fcm0)); + std::tuple res1(us4oemBuffer, std::move(fcm1)); + + EXPECT_CALL(*(us4oems[0].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res0)))); + EXPECT_CALL(*(us4oems[1].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res1)))); + + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(probeAdapter, seq); + + EXPECT_EQ(1, fcm->getNumberOfLogicalFrames()); + EXPECT_EQ(128-48, fcm->getNumberOfLogicalChannels()); + + std::vector expectedFrames; + std::vector expectedChannels; + + // Us4OEM:1, frame 1, channels 0-16 + for(int i = 48; i < 64; ++i) { + expectedFrames.push_back(1); + expectedChannels.push_back(i-48); + } + // Us4OEM:0 + for(int i = 64; i < 96; ++i) { + expectedFrames.push_back(0); + expectedChannels.push_back(i-64); + } + // Us4OEM:1, frame 1, channels 16-32 + for(int i = 96; i < 96+15; ++i) { // 15 because there will be one -1 + expectedFrames.push_back(1); + if(i == 99 && expectedChannels[expectedChannels.size()-1] != FrameChannelMapping::UNAVAILABLE) { + expectedChannels.push_back(FrameChannelMapping::UNAVAILABLE); + --i; + } else { + expectedChannels.push_back(i-96+16); + } + } + // Us4OEM:1, frame 2 + for(int i = 96+16; i < 128; ++i) { + expectedFrames.push_back(2); + expectedChannels.push_back(i-(96+16)); + } + + // VALIDATE + for(int i = 0; i < 128-48; ++i) { + auto [frame, channel] = fcm->getLogical(0, i); + EXPECT_EQ(expectedFrames[i], frame); + EXPECT_EQ(expectedChannels[i], channel); + } +} + +// Currently padding impacts the output frame channel mapping +TEST_F(ProbeAdapterChannelMappingEsaote3Test, AppliesPaddingToFCMCorrectly) { + BitMask rxAperture(getNChannels(), false); + ::arrus::setValuesInRange(rxAperture, 0, 16, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = getDefaultTxAperture(getNChannels()), + x.rxAperture = rxAperture, + x.txDelays = getDefaultTxDelays(getNChannels()), + x.rxPadding = {16, 0} + )) + .getTxRxParameters() + }; + FrameChannelMappingBuilder builder0(1, Us4OEMImpl::N_RX_CHANNELS); + for(int i = 0; i < 32; ++i) { + if(i < 16) { + builder0.setChannelMapping(0, i, 0, i); + } else { + builder0.setChannelMapping(0, i, 0, -1); + } + } + auto fcm0 = builder0.build(); + + FrameChannelMappingBuilder builder1(1, Us4OEMImpl::N_RX_CHANNELS); + // No active channels + auto fcm1 = builder1.build(); + + Us4OEMBuffer us4oemBuffer({Us4OEMBufferElement(0, 10, 0)}); + std::tuple res0(us4oemBuffer, std::move(fcm0)); + std::tuple res1(us4oemBuffer, std::move(fcm1)); + + EXPECT_CALL(*(us4oems[0].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res0)))); + EXPECT_CALL(*(us4oems[1].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res1)))); + + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(probeAdapter, seq); + + EXPECT_EQ(1, fcm->getNumberOfLogicalFrames()); + EXPECT_EQ(32, fcm->getNumberOfLogicalChannels()); // 16 active + 16 rx padding + + for(int i = 0; i < 16; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + ASSERT_EQ(0, frame); + ASSERT_EQ(channel, FrameChannelMapping::UNAVAILABLE); + } + + for(int i = 16; i < 32; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + ASSERT_EQ(0, frame); + ASSERT_EQ(channel, i-16); + } +} + +// The same as above, but with aperture using two modules +TEST_F(ProbeAdapterChannelMappingEsaote3Test, AppliesPaddingToFCMCorrectlyRxApertureUsingTwoModules) { + BitMask rxAperture(getNChannels(), false); + ::arrus::setValuesInRange(rxAperture, 0, 49, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = getDefaultTxAperture(getNChannels()), + x.rxAperture = rxAperture, + x.txDelays = getDefaultTxDelays(getNChannels()), + x.rxPadding = {15, 0} + )) + .getTxRxParameters() + }; + FrameChannelMappingBuilder builder0(1, Us4OEMImpl::N_RX_CHANNELS); + for(int i = 0; i < 32; ++i) { + builder0.setChannelMapping(0, i, 0, i); + } + auto fcm0 = builder0.build(); + + FrameChannelMappingBuilder builder1(1, Us4OEMImpl::N_RX_CHANNELS); + for(int i = 0; i < 32; ++i) { + if(i < 17) { + builder1.setChannelMapping(0, i, 0, i); + } + else { + builder1.setChannelMapping(0, i, 0, FrameChannelMapping::UNAVAILABLE); + } + } + auto fcm1 = builder1.build(); + + Us4OEMBuffer us4oemBuffer({Us4OEMBufferElement(0, 10, 0)}); + std::tuple res0(us4oemBuffer, std::move(fcm0)); + std::tuple res1(us4oemBuffer, std::move(fcm1)); + + EXPECT_CALL(*(us4oems[0].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res0)))); + EXPECT_CALL(*(us4oems[1].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res1)))); + + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(probeAdapter, seq); + + EXPECT_EQ(1, fcm->getNumberOfLogicalFrames()); + EXPECT_EQ(64, fcm->getNumberOfLogicalChannels()); // 49 active + 15 rx padding + + for(int i = 0; i < 15; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + ASSERT_EQ(channel, FrameChannelMapping::UNAVAILABLE); + } + for(int i = 15; i < 15+32; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + ASSERT_EQ(0, frame); + ASSERT_EQ(channel, i - 15); + } + for(int i = 15+32; i < 64; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + ASSERT_EQ(1, frame); + ASSERT_EQ(channel, i-(15+32)); + } +} + +TEST_F(ProbeAdapterChannelMappingEsaote3Test, AppliesPaddingToFCMCorrectlyRightSide) { + BitMask rxAperture(getNChannels(), false); + ::arrus::setValuesInRange(rxAperture, 176, 192, true); + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.txAperture = getDefaultTxAperture(getNChannels()), + x.rxAperture = rxAperture, + x.txDelays = getDefaultTxDelays(getNChannels()), + x.rxPadding = {0, 16} + )) + .getTxRxParameters() + }; + FrameChannelMappingBuilder builder0(0, Us4OEMImpl::N_RX_CHANNELS); + // No output + auto fcm0 = builder0.build(); + + FrameChannelMappingBuilder builder1(1, Us4OEMImpl::N_RX_CHANNELS); + for(int i = 0; i < 32; ++i) { + if(i < 16) { + builder1.setChannelMapping(0, i, 0, i); + } + else { + builder1.setChannelMapping(0, i, 0, FrameChannelMapping::UNAVAILABLE); + } + } + auto fcm1 = builder1.build(); + + Us4OEMBuffer us4oemBuffer({Us4OEMBufferElement(0, 10, 0)}); + std::tuple res0(us4oemBuffer, std::move(fcm0)); + std::tuple res1(us4oemBuffer, std::move(fcm1)); + + EXPECT_CALL(*(us4oems[0].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res0)))); + EXPECT_CALL(*(us4oems[1].get()), US4OEM_MOCK_SET_TX_RX_SEQUENCE()) + .WillOnce(Return(ByMove(std::move(res1)))); + + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(probeAdapter, seq); + + EXPECT_EQ(1, fcm->getNumberOfLogicalFrames()); + EXPECT_EQ(32, fcm->getNumberOfLogicalChannels()); // 16 active + 16 rx padding + + for(int i = 0; i < 16; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + ASSERT_EQ(0, frame); + ASSERT_EQ(channel, i); + } + for(int i = 16; i < 32; ++i) { + auto[frame, channel] = fcm->getLogical(0, i); + ASSERT_EQ(channel, FrameChannelMapping::UNAVAILABLE); + } +} + +// ------------------------------------------ TODO Test that all other parameters are passed unmodified +} + + +int main(int argc, char **argv) { + ARRUS_INIT_TEST_LOG(arrus::Logging); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettings.cpp b/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettings.cpp new file mode 100644 index 000000000..b70e38b06 --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettings.cpp @@ -0,0 +1,19 @@ +#include "ProbeAdapterSettings.h" + +#include "arrus/common/format.h" + +namespace arrus::devices { + +std::ostream & +operator<<(std::ostream &os, const ProbeAdapterSettings &settings) { + os << "modelId: " << settings.getModelId().getName() << ", " + << settings.getModelId().getManufacturer() + << " nChannels: " << settings.getNumberOfChannels(); + os << " channelMapping: "; + for(auto [module, channel] : settings.getChannelMapping()) { + os << "(" << module << "," << channel << ")"; + } + return os; +} + +} diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettings.h b/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettings.h new file mode 100644 index 000000000..f3be0419d --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettings.h @@ -0,0 +1,11 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERSETTINGS_H +#define ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERSETTINGS_H + +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" + +namespace arrus::devices { +std::ostream & +operator<<(std::ostream &os, const ProbeAdapterSettings &settings); +} + +#endif //ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERSETTINGS_H diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidator.h b/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidator.h new file mode 100644 index 000000000..603b145a8 --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidator.h @@ -0,0 +1,130 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERSETTINGSVALIDATOR_H +#define ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERSETTINGSVALIDATOR_H + +#include +#include +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" + +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImpl.h" +#include "arrus/core/devices/SettingsValidator.h" +#include "arrus/core/common/collections.h" +#include "arrus/common/format.h" +#include "arrus/common/asserts.h" + +namespace arrus::devices { + +class ProbeAdapterSettingsValidator + : public SettingsValidator { + +public: + explicit ProbeAdapterSettingsValidator(const Ordinal ordinal) + : SettingsValidator( + DeviceId(DeviceType::ProbeAdapter, ordinal)) {} + + void validate(const ProbeAdapterSettings &obj) override { + auto &id = obj.getModelId(); + expectTrue("modelId", !id.getManufacturer().empty(), + "manufacturer name should not be empty."); + expectTrue("modelId", !id.getName().empty(), + "device name should not be empty."); + + using OEMMapping = Us4OEMSettings::ChannelMapping; + using OEMMappingElement = OEMMapping::value_type; + const auto N_RX = Us4OEMImpl::N_RX_CHANNELS; + // Make sure, that the number of channels is equal to + // the number of channel mapping elements. + expectEqual("channel mapping", + static_cast(obj.getChannelMapping().size()), + obj.getNumberOfChannels(), + " (size, compared to nChannels)"); + + // Get the number of us4oems + std::vector modules; + for(auto &[us4oem, channel] : obj.getChannelMapping()) { + modules.push_back(us4oem); + } + + // Check if contains consecutive ordinal numbers. + // Us4OEMs should have exactly ordinals: 0, 1, ... nmodules-1 + std::unordered_set modulesSet{std::begin(modules), + std::end(modules)}; + ARRUS_REQUIRES_TRUE( + modulesSet.size() >= std::numeric_limits::min() + && modulesSet.size() <= std::numeric_limits::max(), + arrus::format("Us4OEMs count should be in range {}, {}", + std::numeric_limits::min(), + std::numeric_limits::max())); + + for(Ordinal i = 0; i < (Ordinal) modulesSet.size(); ++i) { + expectTrue("channel mapping", + setContains(modulesSet, i), + arrus::format("Missing Us4OEM: {}", i)); + } + + if(hasErrors()) { + // Do not continue here, some errors may impact further validation + // correctness. + return; + } + + auto nModules = modulesSet.size(); + + // Split to us4oem channel mappings + std::vector us4oemMappings(nModules); + + for(auto[module, channel] : obj.getChannelMapping()) { + us4oemMappings[module].emplace_back(channel); + } + + unsigned us4oemOrdinal = 0; + + for(auto &mapping : us4oemMappings) { + // Make sure that the number of channels for each module is + // multiple of nRx + expectDivisible("channel mapping", + (ChannelIdx) mapping.size(), N_RX, + arrus::format(" size (for Us4OEM: {})", us4oemOrdinal)); + + auto nIt = mapping.size() / N_RX; + for(unsigned i = 0; i < nIt; ++i) { + std::unordered_set channelsSet( + std::begin(mapping) + i * N_RX, + std::begin(mapping) + (i + 1) * N_RX); + + // Make sure that the channel mappings are unique in given groups. + expectEqual( + "channel mapping", + (ChannelIdx) channelsSet.size(), N_RX, + arrus::format( + " (number of unique channel indices " + "for Us4OEM: {}, " + "for input range [{}, {}))", + us4oemOrdinal, i*N_RX, (i+1)*N_RX)); + + // Make sure, the mapping contains [0,31)*i*32 groups + // (where i can be 0, 1, 2, 3) + std::unordered_set groups; + for(auto v: channelsSet) { + groups.insert(v / N_RX); + } + bool isSingleGroup = groups.size() == 1; + + expectTrue( + "channel mapping", + isSingleGroup, + arrus::format( + "(for Us4OEM:{}): " + "Channels [{}, {}] should be in the single group " + "of 32 elements (i*32, (i+1)*32), where i " + "can be 0, 1, 2, 3,....", + us4oemOrdinal, i * N_RX, (i + 1) * N_RX)); + } + ++us4oemOrdinal; + } + } +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_PROBEADAPTER_PROBEADAPTERSETTINGSVALIDATOR_H diff --git a/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidatorTest.cpp b/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidatorTest.cpp new file mode 100644 index 000000000..f028b1ded --- /dev/null +++ b/arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidatorTest.cpp @@ -0,0 +1,219 @@ +#include +#include +#include + +#include "arrus/core/common/tests.h" +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterSettingsValidator.h" + +namespace xyz { + +using namespace ::arrus; +using namespace ::arrus::devices; +using ChannelAddress = ::arrus::devices::ProbeAdapterSettings::ChannelAddress; + +struct TestAdapterSettings { + ::arrus::devices::ProbeAdapterModelId modelId{"test", "test"}; + ChannelIdx nChannels = 64; + ProbeAdapterSettings::ChannelMapping channelMapping; + + [[nodiscard]] ProbeAdapterSettings toProbeAdapterSettings() const { + return ProbeAdapterSettings(modelId, nChannels, channelMapping); + } + + friend std::ostream & + operator<<(std::ostream &os, const TestAdapterSettings &settings) { + + std::function func = [](ChannelAddress v) { + return "(" + + std::to_string(v.first) + + ", " + + std::to_string(v.second) + ")"; + }; + os << " nChannels: " << settings.nChannels + << " channelMapping: " << + (arrus::toStringTransform( + settings.channelMapping, func)); + return os; + } + +}; + +class CorrectAdapterSettingsTest + : public testing::TestWithParam { +}; + +TEST_P(CorrectAdapterSettingsTest, AcceptsCorrect) { + ProbeAdapterSettingsValidator validator(0); + TestAdapterSettings val = GetParam(); + validator.validate(val.toProbeAdapterSettings()); + EXPECT_NO_THROW(validator.throwOnErrors()); +} + +INSTANTIATE_TEST_CASE_P + +(ValidProbeAdapterSettings, CorrectAdapterSettingsTest, + testing::Values( + // at given position i of the probe adapter: + // (us4oem ordinal, us4oem channel) + // (0, 0), (1, 0), (0, 1), (1, 1),... (0, 63), (1, 63) + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.channelMapping = arrus::generate( + 64, [](size_t i) { + std::cout << i % 2 << ", " << i / 2 << std::endl; + return std::make_pair(Ordinal(i % 2), ChannelIdx(i / 2)); + }) + )), + // (0, 0), (1, 0), (0, 1), (1, 1),... (0, 127), (1, 127) + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.nChannels = 128, + x.channelMapping = arrus::generate( + 128, [](size_t i) { + std::cout << i % 2 << ", " << i / 2 << std::endl; + return std::make_pair(Ordinal(i % 2), ChannelIdx(i / 2)); + }) + )), + // reverse channels + // (0, 63), (1, 63), (0, 62), (1, 62),..., (0, 0), (1, 0) + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.channelMapping = arrus::generate( + 64, [](size_t i) { + auto res = std::make_pair(Ordinal(i % 2), ChannelIdx(63 - i / 2)); + std::cout << res.first << ", " << res.second + << std::endl; + return res; + }) + )), + // reverse modules + // (1, 0), (0, 0), (1, 1), (0, 1),... (1, 63), (0, 63) + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.channelMapping = arrus::generate( + 64, [](size_t i) { + auto res = std::make_pair(Ordinal(1 - i % 2), ChannelIdx(i / 2)); + std::cout << res.first << ", " << res.second + << std::endl; + return res; + }) + )), + // some non-trivial groups + // us4oem:0: [0-32), [64-96) + // us4oem:1: [32-64), [96-128) + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.channelMapping = arrus::generate( + 64, [](size_t i) { + Ordinal module; + if((i / 32) % 2 == 0) { + module = 0; + } else { + module = 1; + } + auto res = std::make_pair(Ordinal(module), ChannelIdx(i)); + std::cout << res.first << ", " << res.second + << std::endl; + return res; + }) + )), + // single module + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.nChannels = 128, + x.channelMapping = arrus::generate( + 128, [](size_t i) { + return std::make_pair(Ordinal(0), ChannelIdx(i)); + }) + )), + // 8 modules + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.nChannels = 256, + x.channelMapping = arrus::generate( + 256, [](size_t i) { + return std::make_pair(Ordinal(i % 8), ChannelIdx(i / 8)); + }) + )) + )); + + +class IncorrectAdapterSettingsTest + : public testing::TestWithParam { +}; + +TEST_P(IncorrectAdapterSettingsTest, RejectIncorrect) { + ProbeAdapterSettingsValidator validator(0); + TestAdapterSettings val = GetParam(); + validator.validate(val.toProbeAdapterSettings()); + EXPECT_THROW(validator.throwOnErrors(), IllegalArgumentException); + try { + validator.throwOnErrors(); + } catch(const IllegalArgumentException &e) { + std::cerr << "The exception message: " << e.what() << std::endl; + } +} + +INSTANTIATE_TEST_CASE_P + +(InvalidProbeAdapterSettings, IncorrectAdapterSettingsTest, + testing::Values( + // Invalid size of the mapping - should be the same as nChannels + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.nChannels=128, + x.channelMapping = arrus::generate( + 64, [](size_t i) { + return std::make_pair(Ordinal(i % 2), ChannelIdx(i / 2)); + }) + )), + // Invalid us4oem ordinal numbers: should be consecutive + // Configuring us4oems 0 and 2 + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.nChannels=64, + x.channelMapping = arrus::generate( + 64, [](size_t i) { + return std::make_pair(Ordinal(2 * (i % 2)), ChannelIdx(i / 2)); + }) + )), + // One of the us4oems has incomplete (not divisible by number of Rx + // channels) mapping + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.nChannels=48, + x.channelMapping = arrus::generate( + 48, [](size_t i) { + Ordinal module = i < 32 ? 0 : 1; + ChannelIdx channel = i % 32; + return std::make_pair(Ordinal(module), ChannelIdx(channel)); + }) + )), + // One of the us4oems has non-unique set of group channels. + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.nChannels=64, + x.channelMapping = arrus::generate( + 64, [](size_t i) { + Ordinal module = i < 32 ? 0 : 1; + ChannelIdx channel; + if(i < 32) { + // module 0: ok + channel = i % 32; + } else { + // module 1: channels 0, 1, 2, ..., 30, 0 + channel = i % 31; + } + return std::make_pair(Ordinal(module), ChannelIdx(channel)); + }) + )), + // One of us4oems has mixed group of channels + ARRUS_STRUCT_INIT_LIST(TestAdapterSettings, ( + x.nChannels=64, + x.channelMapping = arrus::generate( + 64, [](size_t i) { + Ordinal module = i < 32 ? 0 : 1; + ChannelIdx channel = i % 32; + // module 0: channels are ok + if(i >= 32) { + // Module 1: channels 1, 2, ..., 33 + channel = (channel + 1) % 33; + } + return std::make_pair(Ordinal(module), ChannelIdx(channel)); + }) + )) + )); + + +} + + diff --git a/arrus/core/devices/us4r/tests/MockIUs4OEM.h b/arrus/core/devices/us4r/tests/MockIUs4OEM.h new file mode 100644 index 000000000..dd2c5b617 --- /dev/null +++ b/arrus/core/devices/us4r/tests/MockIUs4OEM.h @@ -0,0 +1,120 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_TESTS_MOCKIUS4OEM_H +#define ARRUS_CORE_DEVICES_US4R_TESTS_MOCKIUS4OEM_H + +#include +#include +#include + +class MockIUs4OEM : public IUs4OEM { +public: + MOCK_METHOD(unsigned int, GetID, (), (override)); + MOCK_METHOD(bool, IsPowereddown, (), (override)); + MOCK_METHOD(void, Initialize, (int), (override)); + MOCK_METHOD(void, Synchronize, (), (override)); + MOCK_METHOD(void, ScheduleReceive, + (const size_t firing, const size_t address, const size_t length, const uint32_t start, const uint32_t decimation, const size_t rxMapId, const std::function& callback), + (override)); + MOCK_METHOD(void, ClearScheduledReceive, (), (override)); + MOCK_METHOD(void, TransferRXBufferToHost, + (unsigned char * dstAddress, size_t length, size_t srcAddress), + (override)); + MOCK_METHOD(void, SetPGAGain, (us4r::afe58jd18::PGA_GAIN gain), (override)); + MOCK_METHOD(void, SetLPFCutoff, (us4r::afe58jd18::LPF_PROG cutoff), + (override)); + MOCK_METHOD(void, SetActiveTermination, + (us4r::afe58jd18::ACTIVE_TERM_EN endis, us4r::afe58jd18::GBL_ACTIVE_TERM term), + (override)); + MOCK_METHOD(void, SetLNAGain, (us4r::afe58jd18::LNA_GAIN_GBL gain), + (override)); + MOCK_METHOD(void, SetDTGC, + (us4r::afe58jd18::EN_DIG_TGC endis, us4r::afe58jd18::DIG_TGC_ATTENUATION att), + (override)); + MOCK_METHOD(void, InitializeTX, (), (override)); + MOCK_METHOD(void, SetNumberOfFirings, (const unsigned short nFirings), + (override)); + MOCK_METHOD(float, SetTxDelay, + (const unsigned char channel, const float value, const unsigned short firing), + (override)); + MOCK_METHOD(float, SetTxFreqency, + (const float frequency, const unsigned short firing), + (override)); + MOCK_METHOD(unsigned char, SetTxHalfPeriods, + (unsigned char nop, const unsigned short firing), (override)); + MOCK_METHOD(void, SetTxInvert, (bool onoff, const unsigned short firing), + (override)); + MOCK_METHOD(void, SetTxCw, (bool onoff, const unsigned short firing), + (override)); + MOCK_METHOD(void, SetRxAperture, + (const unsigned char origin, const unsigned char size, const unsigned short firing), + (override)); + MOCK_METHOD(void, SetTxAperture, + (const unsigned char origin, const unsigned char size, const unsigned short firing), + (override)); + MOCK_METHOD(void, SetRxAperture, + (const std::bitset& aperture, const unsigned short firing), + (override)); + MOCK_METHOD(void, SetTxAperture, + (const std::bitset& aperture, const unsigned short firing), + (override)); + MOCK_METHOD(void, SetActiveChannelGroup, + (const std::bitset& group, const unsigned short firing), + (override)); + MOCK_METHOD(void, SetRxTime, + (const float time, const unsigned short firing), (override)); + MOCK_METHOD(void, SetRxDelay, + (const float delay, const unsigned short firing), (override)); + MOCK_METHOD(void, EnableTransmit, (), (override)); + MOCK_METHOD(void, EnableSequencer, (), (override)); + MOCK_METHOD(void, SetRxChannelMapping, + ( const std::vector & mapping, const uint16_t rxMapId), + (override)); + MOCK_METHOD(void, SetTxChannelMapping, + (const unsigned char srcChannel, const unsigned char dstChannel), + (override)); + MOCK_METHOD(void, TGCEnable, (), (override)); + MOCK_METHOD(void, TGCDisable, (), (override)); + MOCK_METHOD(void, TGCSetSamples, + (const std::vector & samples, const int firing), + (override)); + MOCK_METHOD(void, TriggerStart, (), (override)); + MOCK_METHOD(void, TriggerStop, (), (override)); + MOCK_METHOD(void, TriggerSync, (), (override)); + MOCK_METHOD(void, SetNTriggers, (unsigned short n), (override)); + MOCK_METHOD(void, SetTrigger, + (unsigned int timeToNextTrigger, bool syncReq, unsigned short idx), + (override)); + MOCK_METHOD(void, UpdateFirmware, (const char * filename), (override)); + MOCK_METHOD(float, GetUpdateFirmwareProgress, (), (override)); + MOCK_METHOD(const char *, GetUpdateFirmwareStatus, (), (override)); + MOCK_METHOD(int, UpdateTxFirmware, + (const char * seaFilename, const char * sedFilename), + (override)); + MOCK_METHOD(float, GetUpdateTxFirmwareProgress, (), (override)); + MOCK_METHOD(const char *, GetUpdateTxFirmwareStatus, (), (override)); + MOCK_METHOD(void, SWTrigger, (), (override)); + MOCK_METHOD(void, SWNextTX, (), (override)); + MOCK_METHOD(void, EnableTestPatterns, (), (override)); + MOCK_METHOD(void, DisableTestPatterns, (), (override)); + MOCK_METHOD(void, SyncTestPatterns, (), (override)); + MOCK_METHOD(void, LockDMABuffer, (unsigned char * address, size_t length), + (override)); + MOCK_METHOD(void, ReleaseDMABuffer, (unsigned char * address), (override)); + MOCK_METHOD(void, ScheduleTransferRXBufferToHost, (const size_t, unsigned char *, size_t, size_t, + const std::function &)); + MOCK_METHOD(void, SyncTransfer, (), (override)); + MOCK_METHOD(void, ScheduleTransferRXBufferToHost, (const size_t,const size_t,const std::function &), (override)); + MOCK_METHOD(void, PrepareTransferRXBufferToHost, (const size_t,unsigned char *,size_t,size_t), (override)); + MOCK_METHOD(void, PrepareHostBuffer, (unsigned char *,size_t,size_t), (override)); + MOCK_METHOD(void, MarkEntriesAsReadyForReceive, (unsigned short,unsigned short), (override)); + MOCK_METHOD(void, MarkEntriesAsReadyForTransfer, (unsigned short,unsigned short), (override)); + MOCK_METHOD(void, RegisterReceiveOverflowCallback, (const std::function &), (override)); + MOCK_METHOD(void, RegisterTransferOverflowCallback, (const std::function &), (override)); + MOCK_METHOD(void, EnableWaitOnReceiveOverflow, (), (override)); + MOCK_METHOD(void, EnableWaitOnTransferOverflow, (), (override)); + MOCK_METHOD(void, SyncReceive, (), (override)); + MOCK_METHOD(void, ResetCallbacks, (), (override)); +}; + +#define GET_MOCK_PTR(sptr) *(MockIUs4OEM *) (sptr.get()) + +#endif //ARRUS_CORE_DEVICES_US4R_TESTS_MOCKIUS4OEM_H diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMBuffer.h b/arrus/core/devices/us4r/us4oem/Us4OEMBuffer.h new file mode 100644 index 000000000..67b10761b --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMBuffer.h @@ -0,0 +1,69 @@ +#ifndef ARRUS_ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMBUFFER_H +#define ARRUS_ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMBUFFER_H + +#include + +#include "arrus/core/api/common/types.h" + +namespace arrus::devices { + +/** + * A description of a single us4oem buffer element. + * + * An element is described by: + * - src address + * - size - size of the element (bytes) + * - firing - a firing which ends the acquiring Tx/Rx sequence + */ +class Us4OEMBufferElement { +public: + + Us4OEMBufferElement(size_t address, size_t size, unsigned short firing) + : address(address), size(size), firing(firing) {} + + [[nodiscard]] size_t getAddress() const { + return address; + } + + [[nodiscard]] size_t getSize() const { + return size; + } + + [[nodiscard]] uint16 getFiring() const { + return firing; + } + +private: + size_t address; + size_t size; + uint16 firing; +}; + +/** + * A class describing a structure of a buffer that is located in the Us4OEM + * memory. + */ +class Us4OEMBuffer { +public: + explicit Us4OEMBuffer(std::vector elements) + : elements(std::move(elements)) {} + + [[nodiscard]] const Us4OEMBufferElement &getElement(size_t i) const { + return elements[i]; + } + + [[nodiscard]] size_t getNumberOfElements() const { + return elements.size(); + } + + [[nodiscard]] const std::vector &getElements() const { + return elements; + } + +private: + std::vector elements; +}; + +} + +#endif //ARRUS_ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMBUFFER_H diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMFactory.h b/arrus/core/devices/us4r/us4oem/Us4OEMFactory.h new file mode 100644 index 000000000..765d23e59 --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMFactory.h @@ -0,0 +1,26 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMFACTORY_H +#define ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMFACTORY_H + +#include "arrus/core/api/devices/DeviceId.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImplBase.h" +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" + +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h" + +namespace arrus::devices { + +/** + * This class should not be part of the c++ api! Due to dependency on IUs4OEMHandle + * + * To get an access to single Us4OEM: create a single-module Us4R custom system. + */ +class Us4OEMFactory { +public: + virtual Us4OEMImplBase::Handle + getUs4OEM(Ordinal ordinal, IUs4OEMHandle &handle, + const Us4OEMSettings &settings) = 0; +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMFACTORY_H diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMFactoryImpl.h b/arrus/core/devices/us4r/us4oem/Us4OEMFactoryImpl.h new file mode 100644 index 000000000..cd96be187 --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMFactoryImpl.h @@ -0,0 +1,164 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMFACTORYIMPL_H +#define ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMFACTORYIMPL_H + +#include "arrus/core/api/devices/us4r/RxSettings.h" +#include "arrus/common/asserts.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImpl.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMFactory.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMSettingsValidator.h" +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h" +#include "arrus/core/devices/us4r/external/ius4oem/PGAGainValueMap.h" + +namespace arrus::devices { +class Us4OEMFactoryImpl : public Us4OEMFactory { +public: + Us4OEMFactoryImpl() = default; + + Us4OEMImplBase::Handle + getUs4OEM(Ordinal ordinal, IUs4OEMHandle &ius4oem, + const Us4OEMSettings &cfg) override { + + // Validate settings. + Us4OEMSettingsValidator validator(ordinal); + validator.validate(cfg); + validator.throwOnErrors(); + + // We assume here, that the ius4oem is already initialized. + + // Configure IUs4OEM + ChannelIdx chGroupSize = Us4OEMImpl::N_RX_CHANNELS; + ARRUS_REQUIRES_TRUE( + IUs4OEM::NCH % chGroupSize == 0, + arrus::format("Number of Us4OEM channels ({}) is not " + "divisible by the size of channel group ({})", + IUs4OEM::NCH, chGroupSize)); + ChannelIdx nChannelGroups = IUs4OEM::NCH / chGroupSize; + + // Tx channel mapping + // Convert to uint8_t + std::vector channelMapping; + ARRUS_REQUIRES_AT_MOST( + cfg.getChannelMapping().size(), + UINT8_MAX, + arrus::format("Maximum number of channels: {}", UINT8_MAX)); + + for(auto value : cfg.getChannelMapping()) { + ARRUS_REQUIRES_AT_MOST(value, (ChannelIdx) UINT8_MAX, arrus::format( + "Us4OEM channel index cannot exceed {}", + (ChannelIdx) UINT8_MAX)); + channelMapping.push_back(static_cast(value)); + } + + uint8_t virtualIdx = 0; + for(uint8_t physicalIdx : channelMapping) { + // src - physical channel + // dst - virtual channel + ius4oem->SetTxChannelMapping(virtualIdx++, physicalIdx); + } + // Rx channel mapping + // Check if the the permutation in channel mapping is the same + // for each group of 32 channels. If so, use the first 32 channels + // to set mapping. + // Otherwise store rxChannelMapping in Us4OEM handle for further usage. + + const bool isSinglePermutation = hasConsistentPermutations( + cfg.getChannelMapping(), chGroupSize, nChannelGroups); + + if(isSinglePermutation) { + ius4oem->SetRxChannelMapping( + std::vector( + std::begin(channelMapping), + std::begin(channelMapping) + chGroupSize), + 0); + } + // otherwise store the complete channel mapping array in Us4OEM handle + // (check the value returned by current method). + + // Other parameters + // TGC + const auto pgaGain = cfg.getRxSettings().getPgaGain(); + const auto lnaGain = cfg.getRxSettings().getLnaGain(); + ius4oem->SetPGAGain( + PGAGainValueMap::getInstance().getEnumValue(pgaGain)); + ius4oem->SetLNAGain( + LNAGainValueMap::getInstance().getEnumValue(lnaGain)); + // Convert TGC values to [0, 1] range + if(! cfg.getRxSettings().getTgcSamples().empty()) { + const auto maxGain = pgaGain + lnaGain; + const RxSettings::TGCCurve normalizedTGCSamples = getNormalizedTGCSamples( + cfg.getRxSettings().getTgcSamples(), + maxGain - Us4OEMImpl::TGC_ATTENUATION_RANGE, + static_cast(maxGain)); + + ius4oem->TGCEnable(); + // Currently firing parameter does not matter. + ius4oem->TGCSetSamples(normalizedTGCSamples, 0); + } + + // DTGC + if(cfg.getRxSettings().getDtgcAttenuation().has_value()) { + ius4oem->SetDTGC(us4r::afe58jd18::EN_DIG_TGC::EN_DIG_TGC_EN, + DTGCAttenuationValueMap::getInstance().getEnumValue( + cfg.getRxSettings().getDtgcAttenuation().value())); + } else { + // DTGC value does not matter + ius4oem->SetDTGC(us4r::afe58jd18::EN_DIG_TGC::EN_DIG_TGC_DIS, + us4r::afe58jd18::DIG_TGC_ATTENUATION:: + DIG_TGC_ATTENUATION_42dB); + } + + // Filtering + ius4oem->SetLPFCutoff(LPFCutoffValueMap::getInstance().getEnumValue( + cfg.getRxSettings().getLpfCutoff())); + + // Active termination + if(cfg.getRxSettings().getActiveTermination().has_value()) { + ius4oem->SetActiveTermination( + us4r::afe58jd18::ACTIVE_TERM_EN::ACTIVE_TERM_EN, + ActiveTerminationValueMap::getInstance().getEnumValue( + cfg.getRxSettings().getActiveTermination().value())); + } else { + ius4oem->SetActiveTermination( + us4r::afe58jd18::ACTIVE_TERM_EN::ACTIVE_TERM_DIS, + us4r::afe58jd18::GBL_ACTIVE_TERM::GBL_ACTIVE_TERM_50); + } + + + return std::make_unique( + DeviceId(DeviceType::Us4OEM, ordinal), + std::move(ius4oem), cfg.getActiveChannelGroups(), + channelMapping, pgaGain, lnaGain, + cfg.getChannelsMask()); + } + +private: + + static RxSettings::TGCCurve + getNormalizedTGCSamples(const RxSettings::TGCCurve &samples, + const RxSettings::TGCSample min, + const RxSettings::TGCSample max) { + RxSettings::TGCCurve result; + auto range = max - min; + std::transform(std::begin(samples), std::end(samples), + std::back_inserter(result), + [=](auto value) { return (value - min) / range; }); + return result; + } + + static bool + hasConsistentPermutations(const std::vector &vector, + ChannelIdx groupSize, + ChannelIdx nGroups) { + for(ChannelIdx group = 1; group < nGroups; ++group) { + for(ChannelIdx i = 0; i < groupSize; ++i) { + if(vector[i] != (vector[i + group * groupSize] % groupSize)) { + return false; + } + } + } + return true; + } +}; +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMFACTORYIMPL_H diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMFactoryImplTest.cpp b/arrus/core/devices/us4r/us4oem/Us4OEMFactoryImplTest.cpp new file mode 100644 index 000000000..af97ffc2d --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMFactoryImplTest.cpp @@ -0,0 +1,275 @@ +#include +#include + +#include + +#include "arrus/core/common/tests.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMFactoryImpl.h" + +#include "arrus/core/devices/us4r/us4oem/tests/CommonSettings.h" +#include "arrus/core/devices/us4r/tests/MockIUs4OEM.h" +#include "arrus/common/logging/impl/Logging.h" + +namespace { + +using namespace arrus; +using namespace arrus::devices; +using namespace us4r::afe58jd18; + +using ::testing::_; +using ::testing::FloatEq; +using ::testing::Eq; +using ::testing::Pointwise; +using ::testing::InSequence; +using ::testing::Lt; +using ::testing::Gt; + +// Below default parameters should be conformant with CommonSettings.h +struct ExpectedUs4RParameters { + PGA_GAIN pgaGain = PGA_GAIN::PGA_GAIN_30dB; + LNA_GAIN_GBL lnaGain = LNA_GAIN_GBL::LNA_GAIN_GBL_24dB; + DIG_TGC_ATTENUATION dtgcAttValue = DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_42dB; + EN_DIG_TGC dtgcAttEnabled = EN_DIG_TGC::EN_DIG_TGC_EN; + RxSettings::TGCCurve tgcSamplesNormalized = getRange(0.4, 0.65, 0.0125); + bool tgcEnabled = true; + LPF_PROG lpfCutoff = LPF_PROG::LPF_PROG_10MHz; + GBL_ACTIVE_TERM activeTerminationValue = GBL_ACTIVE_TERM::GBL_ACTIVE_TERM_50; + ACTIVE_TERM_EN activeTerminationEnabled = ACTIVE_TERM_EN::ACTIVE_TERM_EN; +}; + +class Us4OEMFactorySimpleParametersTest + : public testing::TestWithParam> { +}; + +TEST_P(Us4OEMFactorySimpleParametersTest, VerifyUs4OEMFactorySimpleParameters) { + std::unique_ptr ius4oem = std::make_unique<::testing::NiceMock>(); + ExpectedUs4RParameters us4rParameters = GetParam().second; + EXPECT_CALL(GET_MOCK_PTR(ius4oem), SetPGAGain(us4rParameters.pgaGain)); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), SetLNAGain(us4rParameters.lnaGain)); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetDTGC(us4rParameters.dtgcAttEnabled, + us4rParameters.dtgcAttValue)); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), SetLPFCutoff(us4rParameters.lpfCutoff)); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetActiveTermination(us4rParameters.activeTerminationEnabled, + us4rParameters.activeTerminationValue)); + Us4OEMFactoryImpl factory; + + factory.getUs4OEM(0, ius4oem, GetParam().first.getUs4OEMSettings()); +} + +INSTANTIATE_TEST_CASE_P + +(Us4OEMFactorySetsSimpleIUs4OEMProperties, Us4OEMFactorySimpleParametersTest, + testing::Values( + std::pair{TestUs4OEMSettings{}, + ExpectedUs4RParameters{}}, + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.pgaGain=24)), + ARRUS_STRUCT_INIT_LIST(ExpectedUs4RParameters, (x.pgaGain=PGA_GAIN::PGA_GAIN_24dB))}, + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.lnaGain=12)), + ARRUS_STRUCT_INIT_LIST(ExpectedUs4RParameters, (x.lnaGain=LNA_GAIN_GBL::LNA_GAIN_GBL_12dB))}, + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.lpfCutoff=(int) 15e6)), + ARRUS_STRUCT_INIT_LIST(ExpectedUs4RParameters, (x.lpfCutoff=LPF_PROG::LPF_PROG_15MHz))} + )); + +class Us4OEMFactoryOptionalParametersTest + : public testing::TestWithParam> { +}; + +TEST_P(Us4OEMFactoryOptionalParametersTest, + VerifyUs4OEMFactoryOptionalParameters) { + std::unique_ptr ius4oem = std::make_unique<::testing::NiceMock>(); + ExpectedUs4RParameters us4rParameters = GetParam().second; + if(GetParam().first.dtgcAttenuation.has_value()) { + // NO disable + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetDTGC(EN_DIG_TGC::EN_DIG_TGC_DIS, _)).Times(0); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetDTGC(EN_DIG_TGC::EN_DIG_TGC_EN, + us4rParameters.dtgcAttValue)); + } else { + // NO enable + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetDTGC(EN_DIG_TGC::EN_DIG_TGC_EN, _)).Times(0); + + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetDTGC(EN_DIG_TGC::EN_DIG_TGC_DIS, _)); + } + + if(GetParam().first.activeTermination.has_value()) { + // NO disable + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetActiveTermination(ACTIVE_TERM_EN::ACTIVE_TERM_DIS, + us4rParameters.activeTerminationValue)) + .Times(0); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetActiveTermination(ACTIVE_TERM_EN::ACTIVE_TERM_EN, + us4rParameters.activeTerminationValue)); + } else { + // NO enable + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetActiveTermination(ACTIVE_TERM_EN::ACTIVE_TERM_EN, _)) + .Times(0); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetActiveTermination(ACTIVE_TERM_EN::ACTIVE_TERM_DIS, _)); + } + Us4OEMFactoryImpl factory; + + factory.getUs4OEM(0, ius4oem, GetParam().first.getUs4OEMSettings()); +} + +INSTANTIATE_TEST_CASE_P + +(Us4OEMFactorySetsOptionalIUs4OEMProperties, + Us4OEMFactoryOptionalParametersTest, + testing::Values( + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.dtgcAttenuation=0)), + ARRUS_STRUCT_INIT_LIST(ExpectedUs4RParameters, + (x.dtgcAttValue=DIG_TGC_ATTENUATION::DIG_TGC_ATTENUATION_0dB))}, + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.dtgcAttenuation={})), + ExpectedUs4RParameters{}}, // Any value is accepted + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.activeTermination=200)), + ARRUS_STRUCT_INIT_LIST(ExpectedUs4RParameters, + (x.activeTerminationValue=GBL_ACTIVE_TERM::GBL_ACTIVE_TERM_200))}, + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.activeTermination={})), + ExpectedUs4RParameters{}} + )); + + +class Us4OEMFactoryTGCSamplesTest + : public testing::TestWithParam> { +}; + +TEST_P(Us4OEMFactoryTGCSamplesTest, VerifyUs4OEMFactorySimpleParameters) { + std::unique_ptr ius4oem = std::make_unique<::testing::NiceMock>(); + ExpectedUs4RParameters us4rParameters = GetParam().second; + RxSettings::TGCCurve tgcCurve = GetParam().first.getUs4OEMSettings().getRxSettings().getTgcSamples(); + + if(tgcCurve.empty()) { + EXPECT_CALL(GET_MOCK_PTR(ius4oem), TGCDisable()); + // NO TGC enable + EXPECT_CALL(GET_MOCK_PTR(ius4oem), TGCEnable()).Times(0); + } else { + EXPECT_CALL(GET_MOCK_PTR(ius4oem), TGCEnable()); + // NO TGC disable + EXPECT_CALL(GET_MOCK_PTR(ius4oem), TGCDisable()).Times(0); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + TGCSetSamples( + Pointwise( + FloatEq(), + us4rParameters.tgcSamplesNormalized), _)); + } + + Us4OEMFactoryImpl factory; + + factory.getUs4OEM(0, ius4oem, GetParam().first.getUs4OEMSettings()); +} + +INSTANTIATE_TEST_CASE_P + +(Us4OEMFactorySetsTGCSettings, + Us4OEMFactoryTGCSamplesTest, + testing::Values( + // NO TGC + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.tgcSamples={})), + ExpectedUs4RParameters{}}, + // TGC samples set + std::pair{ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.pgaGain=30, + x.lnaGain=24, + x.tgcSamples={30, 35, 40})), + ARRUS_STRUCT_INIT_LIST(ExpectedUs4RParameters, ( + x.tgcSamplesNormalized={0.4, 0.525,0.65}))} + )); + +// Mappings. + +TEST(Us4OEMFactoryTest, WorksForConsistentMapping) { + // Given + std::unique_ptr ius4oem = std::make_unique<::testing::NiceMock>(); + + // Mapping includes groups of 32 channel, each has the same permutation + std::vector channelMapping = getRange(0, 128, 1); + + for(int i = 0; i < 4; ++i) { + std::swap(channelMapping[i * 32], channelMapping[(i + 1) * 32 - 1]); + } + + TestUs4OEMSettings cfg; + cfg.channelMapping = std::vector( + std::begin(channelMapping), std::end(channelMapping)); + Us4OEMFactoryImpl factory; + // Expect + EXPECT_CALL(GET_MOCK_PTR(ius4oem), SetRxChannelMapping(_, _)).Times(0); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetRxChannelMapping( + std::vector( + std::begin(channelMapping), + std::begin(channelMapping) + 32), 0)) + .Times(1); + // Run + factory.getUs4OEM(0, ius4oem, cfg.getUs4OEMSettings()); +} + +TEST(Us4OEMFactoryTest, WorksForInconsistentMapping) { + // Given + std::unique_ptr ius4oem = std::make_unique<::testing::NiceMock>(); + + // Mapping includes groups of 32 channel, each has the same permutation + std::vector channelMapping = getRange(0, 128, 1); + + for(int i = 0; i < 2; ++i) { + std::swap(channelMapping[i * 32], channelMapping[(i + 1) * 32 - 1]); + } + + for(int i = 2; i < 4; ++i) { + std::swap(channelMapping[i * 32 + 1], channelMapping[(i + 1) * 32 - 2]); + } + + TestUs4OEMSettings cfg; + cfg.channelMapping = std::vector( + std::begin(channelMapping), std::end(channelMapping)); + Us4OEMFactoryImpl factory; + // Expect + EXPECT_CALL(GET_MOCK_PTR(ius4oem), SetRxChannelMapping(_, _)).Times(0); + // Run + factory.getUs4OEM(0, ius4oem, cfg.getUs4OEMSettings()); +} + +// Tx channel mapping +TEST(Us4OEMFactoryTest, WorksForTxChannelMapping) { + // Given + std::unique_ptr ius4oem = std::make_unique<::testing::NiceMock>(); + std::vector channelMapping = getRange(0, 128, 1); + TestUs4OEMSettings cfg; + cfg.channelMapping = channelMapping; + Us4OEMFactoryImpl factory; + // Expect + { + InSequence seq; + for(ChannelIdx i = 0; i < Us4OEMImpl::N_TX_CHANNELS; ++i) { + EXPECT_CALL(GET_MOCK_PTR(ius4oem), + SetTxChannelMapping(i, channelMapping[i])); + } + + } + // No other calls should be made + EXPECT_CALL(GET_MOCK_PTR(ius4oem), SetTxChannelMapping(Lt(0), _)) + .Times(0); + EXPECT_CALL(GET_MOCK_PTR(ius4oem), SetTxChannelMapping(Gt(127), _)) + .Times(0); + + // Run + factory.getUs4OEM(0, ius4oem, cfg.getUs4OEMSettings()); +} + +} + +int main(int argc, char **argv) { + ARRUS_INIT_TEST_LOG(arrus::Logging); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + + diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMImpl.cpp b/arrus/core/devices/us4r/us4oem/Us4OEMImpl.cpp new file mode 100644 index 000000000..028c7472f --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMImpl.cpp @@ -0,0 +1,576 @@ +#include "Us4OEMImpl.h" + +#include +#include +#include + +#include "arrus/common/format.h" +#include "arrus/common/utils.h" +#include "arrus/core/common/collections.h" +#include "arrus/common/asserts.h" +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" +#include "arrus/core/common/hash.h" +#include "arrus/core/common/interpolate.h" +#include "arrus/core/common/validation.h" +#include "arrus/core/devices/us4r/FrameChannelMappingImpl.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMBuffer.h" + +namespace arrus::devices { + +Us4OEMImpl::Us4OEMImpl(DeviceId id, IUs4OEMHandle ius4oem, + const BitMask &activeChannelGroups, + std::vector channelMapping, + uint16 pgaGain, uint16 lnaGain, + std::unordered_set channelsMask) + : Us4OEMImplBase(id), logger{getLoggerFactory()->getLogger()}, + ius4oem(std::move(ius4oem)), + channelMapping(std::move(channelMapping)), + channelsMask(std::move(channelsMask)), + pgaGain(pgaGain), lnaGain(lnaGain) { + + INIT_ARRUS_DEVICE_LOGGER(logger, id.toString()); + + // This class stores reordered active groups of channels, + // as presented in the IUs4OEM docs. + static const std::vector acgRemap = {0, 4, 8, 12, + 2, 6, 10, 14, + 1, 5, 9, 13, + 3, 7, 11, 15}; + auto acg = ::arrus::permute(activeChannelGroups, acgRemap); + ARRUS_REQUIRES_TRUE(acg.size() == activeChannelGroups.size(), + arrus::format( + "Invalid number of active channels mask elements; " + "the input has {}, expected: {}", acg.size(), + activeChannelGroups.size())); + this->activeChannelGroups = ::arrus::toBitset(acg); + + if(this->channelsMask.empty()) { + this->logger->log(LogSeverity::INFO, ::arrus::format( + "No channel masking will be applied for {}", ::arrus::toString(id))); + } else { + this->logger->log(LogSeverity::INFO, ::arrus::format( + "Following us4oem channels will be turned off: {}", + ::arrus::toString(this->channelsMask))); + } +} + +Us4OEMImpl::~Us4OEMImpl() { + try { + logger->log(LogSeverity::DEBUG, arrus::format("Destroying handle")); + } catch(const std::exception &e) { + std::cerr << arrus::format("Exception while calling us4oem destructor: {}", e.what()) + << std::endl; + } + logger->log(LogSeverity::DEBUG, arrus::format("Us4OEM handle destroyed.")); +} + +bool Us4OEMImpl::isMaster() { + return getDeviceId().getOrdinal() == 0; +} + +void Us4OEMImpl::startTrigger() { + if(isMaster()) { + ius4oem->TriggerStart(); + } +} + +void Us4OEMImpl::stopTrigger() { + if(isMaster()) { + ius4oem->TriggerStop(); + } +} + +class Us4OEMTxRxValidator : public Validator { +public: + using Validator::Validator; + + void validate(const TxRxParamsSequence &txRxs) override { + // Validation according to us4oem technote + const auto decimationFactor = txRxs[0].getRxDecimationFactor(); + const auto startSample = txRxs[0].getRxSampleRange().start(); + for(size_t firing = 0; firing < txRxs.size(); ++firing) { + const auto &op = txRxs[firing]; + if(!op.isNOP()) { + auto firingStr = ::arrus::format(" (firing {})", firing); + + // Tx + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getTxAperture().size(), size_t(Us4OEMImpl::N_TX_CHANNELS), + firingStr); + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getTxDelays().size(), size_t(Us4OEMImpl::N_TX_CHANNELS), + firingStr); + ARRUS_VALIDATOR_EXPECT_ALL_IN_RANGE_VM( + op.getTxDelays(), + Us4OEMImpl::MIN_TX_DELAY, Us4OEMImpl::MAX_TX_DELAY, + firingStr); + + // Tx - pulse + ARRUS_VALIDATOR_EXPECT_IN_RANGE_M( + op.getTxPulse().getCenterFrequency(), + Us4OEMImpl::MIN_TX_FREQUENCY, Us4OEMImpl::MAX_TX_FREQUENCY, + firingStr); + ARRUS_VALIDATOR_EXPECT_IN_RANGE_M( + op.getTxPulse().getNPeriods(), 0.0f, 32.0f, firingStr); + float ignore = 0.0f; + float fractional = std::modf(op.getTxPulse().getNPeriods(), &ignore); + ARRUS_VALIDATOR_EXPECT_TRUE_M( + (fractional == 0.0f || fractional == 0.5f), + (firingStr + ", n periods")); + + // Rx + ARRUS_VALIDATOR_EXPECT_EQUAL_M( + op.getRxAperture().size(), size_t(Us4OEMImpl::N_ADDR_CHANNELS), firingStr); + size_t numberOfActiveRxChannels = std::accumulate( + std::begin(op.getRxAperture()), std::end(op.getRxAperture()), 0); + ARRUS_VALIDATOR_EXPECT_IN_RANGE_M( + numberOfActiveRxChannels, size_t(0), size_t(32), firingStr); + uint32 numberOfSamples = op.getNumberOfSamples(); + ARRUS_VALIDATOR_EXPECT_IN_RANGE_M( + // should be enough for condition rxTime < 4000 [us] + numberOfSamples, Us4OEMImpl::MIN_NSAMPLES, Us4OEMImpl::MAX_NSAMPLES, firingStr); + ARRUS_VALIDATOR_EXPECT_DIVISIBLE_M( + numberOfSamples, 64, firingStr); + ARRUS_VALIDATOR_EXPECT_IN_RANGE_M( + op.getRxDecimationFactor(), 0, 5, firingStr); + ARRUS_VALIDATOR_EXPECT_IN_RANGE_M( + op.getPri(), + Us4OEMImpl::MIN_PRI, Us4OEMImpl::MAX_PRI, + firingStr); + ARRUS_VALIDATOR_EXPECT_TRUE_M( + op.getRxDecimationFactor() == decimationFactor, + "Decimation factor should be the same for all operations." + firingStr + ); + ARRUS_VALIDATOR_EXPECT_TRUE_M( + op.getRxSampleRange().start() == startSample, + "Start sample should be the same for all operations." + firingStr + ); + ARRUS_VALIDATOR_EXPECT_TRUE_M( + (op.getRxPadding() == ::arrus::Tuple{0, 0}), + ("Rx padding is not allowed for us4oems. " + firingStr) + ); + } + } + } +}; + +std::pair getTgcMinMax(uint16 pgaGain, uint16 lnaGain) { + float max = float(pgaGain) + float(lnaGain); + return std::make_pair(max - 40, max); +} + +class TGCCurveValidator : public Validator<::arrus::ops::us4r::TGCCurve> { +public: + TGCCurveValidator(const std::string &componentName, uint16 pgaGain, uint16 lnaGain) + : Validator(componentName), pgaGain(pgaGain), lnaGain(lnaGain) {} + + void validate(const ops::us4r::TGCCurve &tgcCurve) override { + if(pgaGain != 30 || lnaGain != 24) { + ARRUS_VALIDATOR_EXPECT_TRUE_M( + tgcCurve.empty(), + "Currently TGC is supported only for " + "PGA gain 30 dB, LNA gain 24 dB"); + } else { + ARRUS_VALIDATOR_EXPECT_IN_RANGE( + tgcCurve.size(), size_t(0), size_t(1022)); + auto[min, max] = getTgcMinMax(pgaGain, lnaGain); + ARRUS_VALIDATOR_EXPECT_ALL_IN_RANGE_V(tgcCurve, min, max); + } + } + +private: + uint16 pgaGain, lnaGain; +}; + +std::tuple +Us4OEMImpl::setTxRxSequence(const std::vector &seq, + const ops::us4r::TGCCurve &tgc, uint16 rxBufferSize, + uint16 batchSize, std::optional sri) { + // TODO initialize module: reset all parameters (turn off TGC, DTGC, ActiveTermination, etc.) + // Validate input sequence and parameters. + std::string deviceIdStr = getDeviceId().toString(); + Us4OEMTxRxValidator seqValidator(format("{} tx rx sequence", deviceIdStr)); + seqValidator.validate(seq); + seqValidator.throwOnErrors(); + + TGCCurveValidator tgcValidator(format("{} tgc samples", deviceIdStr), pgaGain, lnaGain); + tgcValidator.validate(tgc); + tgcValidator.throwOnErrors(); + + // General sequence parameters. + auto nOps = static_cast(seq.size()); + ARRUS_REQUIRES_AT_MOST(nOps*batchSize, 1024, ::arrus::format( + "Exceeded the maximum ({}) number of firings: {}", 1024, nOps)); + ARRUS_REQUIRES_AT_MOST(nOps * batchSize * rxBufferSize, 16384, + ::arrus::format( + "Exceeded the maximum ({}) number of triggers: {}", + 16384, nOps * batchSize * rxBufferSize)); + + ius4oem->SetNumberOfFirings(nOps*batchSize); + ius4oem->ClearScheduledReceive(); + ius4oem->ResetCallbacks(); + setTGC(tgc); + + auto[rxMappings, rxApertures, fcm] = setRxMappings(seq); + + // helper data + const std::bitset emptyAperture; + const std::bitset emptyChannelGroups; + + // Program Tx/rx sequence ("firings") + for(uint16 opIdx = 0; opIdx < seq.size(); ++opIdx) { + logger->log(LogSeverity::TRACE, format("Setting tx/rx: {}", opIdx)); + auto const &op = seq[opIdx]; + if(op.isNOP()) { + logger->log(LogSeverity::TRACE, + format("Setting tx/rx {}: NOP {}", opIdx, ::arrus::toString(op))); + } else { + logger->log(LogSeverity::DEBUG, + arrus::format("Setting tx/rx {}: {}", opIdx, ::arrus::toString(op))); + } + auto[startSample, endSample] = op.getRxSampleRange().asPair(); + float rxTime = getRxTime(endSample, op.getRxDecimationFactor()); + rxTime = std::max(rxTime, MIN_RX_TIME); + + if(op.isNOP()) { + ius4oem->SetActiveChannelGroup(emptyChannelGroups, opIdx); + // Intentionally filtering empty aperture to reduce possibility of a mistake. + auto txAperture = filterAperture(emptyAperture); + auto rxAperture = filterAperture(emptyAperture); + + // Intentionally validating the apertures, to reduce possibility of mistake. + validateAperture(txAperture); + ius4oem->SetTxAperture(txAperture, opIdx); + validateAperture(rxAperture); + ius4oem->SetRxAperture(rxAperture, opIdx); + } else { + // active channel groups already remapped in constructor + ius4oem->SetActiveChannelGroup(activeChannelGroups, opIdx); + + auto txAperture = filterAperture( + ::arrus::toBitset(op.getTxAperture())); + auto rxAperture = filterAperture(rxApertures[opIdx]); + + // Intentionally validating tx apertures, to reduce the risk of mistake channel activation + // (e.g. the masked one). + validateAperture(txAperture); + ius4oem->SetTxAperture(txAperture, opIdx); + validateAperture(rxAperture); + ius4oem->SetRxAperture(rxAperture, opIdx); + } + + // Delays + uint8 txChannel = 0; + for(bool bit : op.getTxAperture()) { + float txDelay = 0; + if(bit && !::arrus::setContains(this->channelsMask, txChannel)) { + txDelay = op.getTxDelays()[txChannel]; + } + ius4oem->SetTxDelay(txChannel, txDelay, opIdx); + ++txChannel; + } + ius4oem->SetTxFreqency(op.getTxPulse().getCenterFrequency(), opIdx); + ius4oem->SetTxHalfPeriods(static_cast(op.getTxPulse().getNPeriods() * 2), opIdx); + ius4oem->SetTxInvert(op.getTxPulse().isInverse(), opIdx); + ius4oem->SetRxTime(rxTime, opIdx); + ius4oem->SetRxDelay(Us4OEMImpl::RX_DELAY, opIdx); + } + + // Program data acquisitions ("ScheduleReceive" part) + // element == the result data frame of the given operations sequence + // Buffer elements. + // The below code fills the us4oem memory with the acquired data. + // us4oem rxdma output address + size_t outputAddress = 0; + size_t transferAddressStart = 0; + + uint16 firing = 0; + std::vector rxBufferElements; + for(uint16 batchIdx = 0; batchIdx < rxBufferSize; ++batchIdx) { + // Batch elements. + for(uint16 batchElementIdx = 0; batchElementIdx < batchSize; ++batchElementIdx) { + // Element operation. + for(uint16 opIdx = 0; opIdx < seq.size(); ++opIdx) { + firing = opIdx + (batchElementIdx * nOps) + (batchIdx * nOps * batchSize); + auto const &op = seq[opIdx]; + auto[startSample, endSample] = op.getRxSampleRange().asPair(); + size_t nSamples = endSample - startSample; + size_t nBytes = nSamples * N_RX_CHANNELS * sizeof(OutputDType); + auto rxMapId = rxMappings.find(opIdx)->second; + + ARRUS_REQUIRES_AT_MOST( + outputAddress + nBytes, DDR_SIZE, + ::arrus::format("Total data size cannot exceed 4GiB (device {})", getDeviceId().toString())); + + if(op.isRxNOP() && !this->isMaster()) { + // TODO reduce the size of data acquired for master rx nops to small number of samples + // (e.g. 64) + ius4oem->ScheduleReceive(firing, outputAddress, nSamples, + SAMPLE_DELAY + startSample, + op.getRxDecimationFactor() - 1, + rxMapId, nullptr); + } else { + // Also, allows rx nops for master module. + // Master module gathers frame metadata, so we cannot miss any of them + ius4oem->ScheduleReceive(firing, outputAddress, nSamples, + SAMPLE_DELAY + startSample, + op.getRxDecimationFactor() - 1, + rxMapId, nullptr); + outputAddress += nBytes; + } + } + } + // The size of the chunk. + auto size = outputAddress - transferAddressStart; + // Where the chunk starts. + auto srcAddress = transferAddressStart; + transferAddressStart = outputAddress; + rxBufferElements.emplace_back(srcAddress, size, firing); + } + ius4oem->EnableTransmit(); + + // Set frame repetition interval if possible. + float totalPri = 0.0f; + for(auto &op : seq) { + totalPri += op.getPri(); + } + std::optional lastPriExtend = std::nullopt; + if(sri.has_value()) { + if(totalPri < sri.value()) { + lastPriExtend = sri.value() - totalPri; + } else { + // TODO move this condition to sequence validator + throw IllegalArgumentException( + arrus::format("Sequence repetition interval {} cannot be set, " + "sequence total pri is equal {}", + sri.value(), totalPri)); + } + } + + // Program triggers + ius4oem->SetNTriggers(nOps * batchSize * rxBufferSize); + firing = 0; + for(uint16 batchIdx = 0; batchIdx < rxBufferSize; ++batchIdx) { + for(uint16 batchElementIdx = 0; batchElementIdx < batchSize; ++batchElementIdx) { + for(uint16 opIdx = 0; opIdx < seq.size(); ++opIdx) { + firing = (uint16) (opIdx + batchElementIdx * nOps + batchIdx * nOps * batchSize); + auto const &op = seq[opIdx]; + bool checkpoint = false; + float pri = op.getPri(); + if(opIdx == nOps - 1 && lastPriExtend.has_value()) { + pri += lastPriExtend.value(); + } + auto priMs = static_cast(pri * 1e6); + ius4oem->SetTrigger(priMs, checkpoint, firing); + } + } + } + return {Us4OEMBuffer(rxBufferElements), std::move(fcm)}; +} + +std::tuple< + std::unordered_map, + std::vector, + FrameChannelMapping::Handle> +Us4OEMImpl::setRxMappings(const std::vector &seq) { + // a map: op ordinal number -> rx map id + std::unordered_map result; + std::unordered_map, uint16, ContainerHash>> rxMappings; + + // FC mapping + auto numberOfOutputFrames = getNumberOfNoRxNOPs(seq); + if(this->isMaster()) { + // We transfer all master module frames due to possible metadata stored in the frame. + numberOfOutputFrames = ARRUS_SAFE_CAST(seq.size(), ChannelIdx); + } + FrameChannelMappingBuilder fcmBuilder(numberOfOutputFrames, N_RX_CHANNELS); + + // Rx apertures after taking into account possible conflicts in Rx channel + // mapping. + std::vector outputRxApertures; + + uint16 rxMapId = 0; + uint16 opId = 0; + uint16 noRxNopId = 0; + + for(const auto &op: seq) { + // Considering rx nops: rx channel mapping will be equal [0, 1,.. 31]. + + // Index of rx aperture channel (0, 1...32) -> us4oem physical channel + // nullopt means that given channel is missing (conflicting with some other channel or is masked) + std::vector> mapping; + std::unordered_set channelsUsed; + + // Convert rx aperture + channel mapping -> new rx aperture (with conflicting channels turned off). + std::bitset outputRxAperture; + + // Us4OEM channel number: values from 0-127 + uint8 channel = 0; + // Number of Us4OEM active channel, values from 0-31 + uint8 onChannel = 0; + + bool isRxNop = true; + for(const auto isOn : op.getRxAperture()) { + if(isOn) { + isRxNop = false; + ARRUS_REQUIRES_TRUE_E( + onChannel < N_RX_CHANNELS, + ArrusException("Up to 32 active rx channels can be set.")); + + // Physical channel number, values 0-31 + auto rxChannel = channelMapping[channel]; + rxChannel = rxChannel % N_RX_CHANNELS; + if(!setContains(channelsUsed, rxChannel) && !setContains(this->channelsMask, channel)) { + // This channel is OK. + // STRATEGY: if there are conflicting/masked rx channels, keep the + // first one (with the lowest channel number), turn off all + // the rest. Turn off conflicting channels. + outputRxAperture[channel] = true; + mapping.emplace_back(rxChannel); + channelsUsed.insert(rxChannel); + } else { + // This channel is not OK. + mapping.emplace_back(std::nullopt); + } + auto frameNumber = noRxNopId; + if(this->isMaster()) { + frameNumber = opId; + } + fcmBuilder.setChannelMapping(frameNumber, onChannel, frameNumber, (int8) (mapping.size() - 1)); + ++onChannel; + } + ++channel; + } + outputRxApertures.push_back(outputRxAperture); + + // Replace invalid channels with unused channels + std::list unusedChannels; + for(uint8 i = 0; i < N_RX_CHANNELS; ++i) { + if(!setContains(channelsUsed, i)) { + unusedChannels.push_back(i); + } + } + std::vector rxMapping; + for(auto &dstChannel: mapping) { + if(!dstChannel.has_value()) { + rxMapping.push_back(unusedChannels.front()); + unusedChannels.pop_front(); + } else { + rxMapping.push_back(dstChannel.value()); + } + } + // Move all the non-active channels to the end of mapping. + while(rxMapping.size() != 32) { + rxMapping.push_back(unusedChannels.front()); + unusedChannels.pop_front(); + } + + auto mappingIt = rxMappings.find(rxMapping); + if(mappingIt == std::end(rxMappings)) { + // Create new Rx channel mapping. + rxMappings.emplace(rxMapping, rxMapId); + result.emplace(opId, rxMapId); + // Set channel mapping + ARRUS_REQUIRES_TRUE(rxMapping.size() == N_RX_CHANNELS, + arrus::format( + "Invalid size of the RX " + "channel mapping to set: {}", + rxMapping.size())); + ARRUS_REQUIRES_TRUE( + rxMapId < 128, + arrus::format("128 different rx mappings can be loaded only" + ", deviceId: {}.", getDeviceId().toString())); + ius4oem->SetRxChannelMapping(rxMapping, rxMapId); + ++rxMapId; + } else { + // Use the existing one. + result.emplace(opId, mappingIt->second); + } + ++opId; + if(!isRxNop) { + ++noRxNopId; + } + } + return {result, outputRxApertures, fcmBuilder.build()}; +} + +double Us4OEMImpl::getSamplingFrequency() { + return Us4OEMImpl::SAMPLING_FREQUENCY; +} + +float Us4OEMImpl::getRxTime(size_t nSamples, uint32 decimationFactor) { + return nSamples / (Us4OEMImpl::SAMPLING_FREQUENCY/decimationFactor) + + Us4OEMImpl::RX_TIME_EPSILON; +} + +void Us4OEMImpl::setTGC(const ops::us4r::TGCCurve &tgc) { + if(tgc.empty()) { + ius4oem->TGCDisable(); + } else { + ius4oem->TGCEnable(); + + static const std::vector tgcChar = + {14.000f, 14.001f, 14.002f, 14.003f, 14.024f, 14.168f, 14.480f, 14.825f, + 15.234f, 15.770f, 16.508f, 17.382f, 18.469f, 19.796f, 20.933f, 21.862f, + 22.891f, 24.099f, 25.543f, 26.596f, 27.651f, 28.837f, 30.265f, 31.690f, + 32.843f, 34.045f, 35.543f, 37.184f, 38.460f, 39.680f, 41.083f, 42.740f, + 44.269f, 45.540f, 46.936f, 48.474f, 49.895f, 50.966f, 52.083f, 53.256f, + 54.0f}; + auto actualTGC = ::arrus::interpolate1d( + tgcChar, + ::arrus::getRange(14, 55, 1.0), + tgc); + for(auto &val: actualTGC) { + val = (val - 14.0f) / 40.0f; + } + // Currently setting firing parameter has no impact on the result + // because TGC can be set only once for the whole sequence. + ius4oem->TGCSetSamples(actualTGC, 0); + } +} + +std::bitset +Us4OEMImpl::filterAperture(std::bitset aperture) { + for(auto channel : this->channelsMask) { + aperture[channel] = false; + } + return aperture; +} + +void +Us4OEMImpl::validateAperture(const std::bitset &aperture) { + for(auto channel : this->channelsMask) { + if(aperture[channel]) { + throw ArrusException( + ::arrus::format("Attempted to set masked channel: {}", channel) + ); + } + } +} + +void Us4OEMImpl::start() { + this->startTrigger(); +} + +void Us4OEMImpl::stop() { + this->stopTrigger(); +} + +void Us4OEMImpl::syncTrigger() { + this->ius4oem->TriggerSync(); +} + +void Us4OEMImpl::setTgcCurve(const ops::us4r::TGCCurve &tgc) { + // Currently firing parameter doesn't matter. + this->setTGC(tgc); +} + +Ius4OEMRawHandle Us4OEMImpl::getIUs4oem() { + return ius4oem.get(); +} + +void Us4OEMImpl::enableSequencer() { + this->ius4oem->EnableSequencer(); +} + +} diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMImpl.h b/arrus/core/devices/us4r/us4oem/Us4OEMImpl.h new file mode 100644 index 000000000..ffa3fc7bf --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMImpl.h @@ -0,0 +1,144 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMIMPL_H +#define ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMIMPL_H + +#include +#include +#include + +#include "arrus/core/api/devices/us4r/FrameChannelMapping.h" +#include "arrus/common/format.h" +#include "arrus/core/common/logging.h" +#include "arrus/core/api/devices/us4r/Us4OEM.h" +#include "arrus/core/api/common/types.h" +#include "arrus/core/devices/TxRxParameters.h" +#include "arrus/core/devices/UltrasoundDevice.h" +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactory.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImplBase.h" +#include "arrus/core/devices/us4r/DataTransfer.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMBuffer.h" + + +namespace arrus::devices { + +/** + * Us4OEM wrapper implementation. + * + * This class stores reordered channels, as it is required in IUs4OEM docs. + */ +class Us4OEMImpl : public Us4OEMImplBase { +public: + using Handle = std::unique_ptr; + using RawHandle = PtrHandle; + + using OutputDType = int16; + using FiringIdx = uint16; + // voltage, +/- [V] amplitude, (ref: technote) + static constexpr Voltage MIN_VOLTAGE = 0; + static constexpr Voltage MAX_VOLTAGE = 90; // 180 vpp + + // TGC constants. + static constexpr float TGC_ATTENUATION_RANGE = 40.0f; + static constexpr float TGC_SAMPLING_FREQUENCY = 1e6; + static constexpr size_t TGC_N_SAMPLES = 1022; + + // Number of tx/rx channels. + static constexpr ChannelIdx N_TX_CHANNELS = 128; + static constexpr ChannelIdx N_RX_CHANNELS = 32; + static constexpr ChannelIdx N_ADDR_CHANNELS = N_TX_CHANNELS; + static constexpr ChannelIdx ACTIVE_CHANNEL_GROUP_SIZE = 8; + static constexpr ChannelIdx N_ACTIVE_CHANNEL_GROUPS = + N_TX_CHANNELS / ACTIVE_CHANNEL_GROUP_SIZE; + + static constexpr float MIN_TX_DELAY = 0.0f; + static constexpr float MAX_TX_DELAY = 16.96e-6f; + + static constexpr float MIN_TX_FREQUENCY = 1e6f; + static constexpr float MAX_TX_FREQUENCY = 20e6f; + + // Sampling + static constexpr float SAMPLING_FREQUENCY = 65e6; + static constexpr uint32 SAMPLE_DELAY = 240; + static constexpr float RX_DELAY = 0.0; + static constexpr float RX_TIME_EPSILON = static_cast(10e-6); + static constexpr uint32 MIN_NSAMPLES = 64; + static constexpr uint32 MAX_NSAMPLES = 16384; + // Data + static constexpr size_t DDR_SIZE = 1ull << 32u; + // Other + static constexpr float MIN_PRI = 80e-6f; + static constexpr float MIN_RX_TIME = MIN_PRI; + static constexpr float MAX_PRI = 1.0f; + + /** + * Us4OEMImpl constructor. + * + * @param ius4oem + * @param activeChannelGroups must contain exactly N_ACTIVE_CHANNEL_GROUPS elements + * @param channelMapping a vector of N_TX_CHANNELS destination channels; must contain + * exactly N_TX_CHANNELS numbers + */ + Us4OEMImpl(DeviceId id, IUs4OEMHandle ius4oem, + const BitMask &activeChannelGroups, + std::vector channelMapping, + uint16 pgaGain, uint16 lnaGain, + std::unordered_set channelsMask); + + ~Us4OEMImpl() override; + + bool isMaster() override; + + void startTrigger() override; + + void stopTrigger() override; + + void syncTrigger() override; + + std::tuple + setTxRxSequence(const std::vector &seq, const ops::us4r::TGCCurve &tgcSamples, + uint16 rxBufferSize, uint16 rxBatchSize, std::optional sri) override; + + double getSamplingFrequency() override; + + Interval getAcceptedVoltageRange() override { + return Interval(MIN_VOLTAGE, MAX_VOLTAGE); + } + + void start() override; + + void stop() override; + + void setTgcCurve(const ops::us4r::TGCCurve &tgc) override; + + Ius4OEMRawHandle getIUs4oem() override; + + void enableSequencer() override; + +private: + using Us4OEMBitMask = std::bitset; + + Logger::Handle logger; + IUs4OEMHandle ius4oem; + std::bitset activeChannelGroups; + std::vector channelMapping; + std::unordered_set channelsMask; + uint16 pgaGain, lnaGain; + + std::tuple< + std::unordered_map, + std::vector, + FrameChannelMapping::Handle> + setRxMappings(const std::vector &seq); + + static float getRxTime(size_t nSamples, uint32 decimationFactor); + + void setTGC(const ops::us4r::TGCCurve &tgc); + + std::bitset filterAperture(std::bitset aperture); + + void validateAperture(const std::bitset &aperture); +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMIMPL_H diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMImplBase.h b/arrus/core/devices/us4r/us4oem/Us4OEMImplBase.h new file mode 100644 index 000000000..a3d576a3f --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMImplBase.h @@ -0,0 +1,46 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMIMPLBASE_H +#define ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMIMPLBASE_H + +#include +#include "arrus/core/api/devices/us4r/FrameChannelMapping.h" +#include "arrus/core/api/devices/us4r/Us4OEM.h" +#include "arrus/core/devices/TxRxParameters.h" +#include "arrus/core/api/ops/us4r/tgc.h" +#include "arrus/core/devices/UltrasoundDevice.h" + +namespace arrus::devices { + +class Us4OEMImplBase : public Us4OEM, public UltrasoundDevice { +public: + using Handle = std::unique_ptr; + using RawHandle = PtrHandle; + + ~Us4OEMImplBase() override = default; + + Us4OEMImplBase(Us4OEMImplBase const&) = delete; + Us4OEMImplBase(Us4OEMImplBase const&&) = delete; + void operator=(Us4OEMImplBase const&) = delete; + void operator=(Us4OEMImplBase const&&) = delete; + + virtual void syncTrigger() = 0; + virtual bool isMaster() = 0; + + virtual + std::tuple + setTxRxSequence(const std::vector &seq, const ops::us4r::TGCCurve &tgcSamples, + uint16 rxBufferSize, uint16 rxBatchSize, std::optional sri) = 0; + + virtual void setTgcCurve(const ::arrus::ops::us4r::TGCCurve &tgc) = 0; + + // TODO expose "registerUs4OEMOutputBuffer" function, keep this class hermetic + virtual Ius4OEMRawHandle getIUs4oem() = 0; + + virtual void enableSequencer() = 0; + +protected: + explicit Us4OEMImplBase(const DeviceId &id) : Us4OEM(id) {} +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMIMPLBASE_H diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMImplTest.cpp b/arrus/core/devices/us4r/us4oem/Us4OEMImplTest.cpp new file mode 100644 index 000000000..ebb47f46a --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMImplTest.cpp @@ -0,0 +1,960 @@ +#include +#include +#include + +#include "Us4OEMImpl.h" +#include "arrus/core/common/tests.h" +#include "arrus/core/common/collections.h" +#include "arrus/core/devices/us4r/tests/MockIUs4OEM.h" +#include "arrus/common/logging/impl/Logging.h" +#include "arrus/core/api/ops/us4r/tgc.h" + +namespace { +using namespace arrus; +using namespace arrus::devices; +using namespace arrus::ops::us4r; +using ::testing::_; +using ::testing::Ge; +using ::testing::FloatEq; +using ::testing::Pointwise; + +MATCHER_P(FloatNearPointwise, tol, "") { + return std::abs(std::get<0>(arg) - std::get<1>(arg)) < tol; +} + +constexpr uint16 DEFAULT_PGA_GAIN = 30; +constexpr uint16 DEFAULT_LNA_GAIN = 24; + +struct TestTxRxParams { + + TestTxRxParams() { + for(int i = 0; i < 32; ++i) { + rxAperture[i] = true; + } + } + + BitMask txAperture = getNTimes(true, Us4OEMImpl::N_TX_CHANNELS);; + std::vector txDelays = getNTimes(0.0f, Us4OEMImpl::N_TX_CHANNELS); + ops::us4r::Pulse pulse{2.0e6f, 2.5f, true}; + BitMask rxAperture = getNTimes(false, Us4OEMImpl::N_ADDR_CHANNELS); + uint32 decimationFactor = 1; + float pri = 100e-6f; + Interval sampleRange{0, 4096}; + + [[nodiscard]] TxRxParameters getTxRxParameters() const { + return TxRxParameters(txAperture, txDelays, pulse, + rxAperture, sampleRange, + decimationFactor, pri); + } +}; + + +class Us4OEMImplEsaote3LikeTest : public ::testing::Test { +protected: + void SetUp() override { + std::unique_ptr ius4oem = std::make_unique<::testing::NiceMock>(); + ius4oemPtr = dynamic_cast(ius4oem.get()); + BitMask activeChannelGroups = {true, true, true, true, + true, true, true, true, + true, true, true, true, + true, true, true, true}; + std::vector channelMapping = getRange(0, 128); + uint16 pgaGain = DEFAULT_PGA_GAIN; + uint16 lnaGain = DEFAULT_LNA_GAIN; + us4oem = std::make_unique( + DeviceId(DeviceType::Us4OEM, 0), + std::move(ius4oem), activeChannelGroups, + channelMapping, + pgaGain, lnaGain, + std::unordered_set() + ); + } + + MockIUs4OEM *ius4oemPtr; + Us4OEMImpl::Handle us4oem; + TGCCurve defaultTGCCurve; + uint16 defaultRxBufferSize = 1; + uint16 defaultBatchSize = 1; + std::optional defaultSri = std::nullopt; +}; + + +#define SET_TX_RX_SEQUENCE_TGC(us4oem, seq, tgc) \ + us4oem->setTxRxSequence(seq, tgc, defaultRxBufferSize, defaultBatchSize, defaultSri) + +#define SET_TX_RX_SEQUENCE(us4oem, seq) SET_TX_RX_SEQUENCE_TGC(us4oem, seq, defaultTGCCurve) + +// ------------------------------------------ TESTING VALIDATION +TEST_F(Us4OEMImplEsaote3LikeTest, PreventsInvalidApertureSize) { + // Tx aperture. + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txAperture = getNTimes(true, Us4OEMImpl::N_TX_CHANNELS + 1))) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), IllegalArgumentException); + + // Rx aperture: total size + seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = getNTimes(true, Us4OEMImpl::N_TX_CHANNELS - 1))) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); + +// Rx aperture: number of active elements + BitMask rxAperture(128, false); + for(size_t i = 0; i < 33; ++i) { + rxAperture[i] = true; + } + seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); + + // Tx delays + seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txDelays = getNTimes(0.0f, Us4OEMImpl::N_TX_CHANNELS / 2))) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, PreventsInvalidTxDelays) { + // Tx delays + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txDelays = getNTimes(Us4OEMImpl::MAX_TX_DELAY + 1e-6f, Us4OEMImpl::N_TX_CHANNELS))) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); + + seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.txDelays = getNTimes(Us4OEMImpl::MIN_TX_DELAY - 1e-6f, Us4OEMImpl::N_TX_CHANNELS))) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, PreventsInvalidPri) { + // Tx delays + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pri = Us4OEMImpl::MAX_PRI + 1e-6f)) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); + + seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pri = Us4OEMImpl::MIN_PRI - 1e-6f)) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, PreventsInvalidNPeriodsOnly) { + // Tx delays + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pulse = Pulse(2e6, 1.3f, false))) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); + + seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pulse = Pulse(2e6, 1.5f, false))) + .getTxRxParameters() + }; + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, PreventsInvalidFrequency) { + // Tx delays + const auto maxFreq = Us4OEMImpl::MAX_TX_FREQUENCY; + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pulse = Pulse(std::nextafter(maxFreq, maxFreq + 1e6f), 1.0f, false))) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); + + seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pulse = Pulse(Us4OEMImpl::MIN_TX_FREQUENCY - 0.5e6f, 1.0f, false))) + .getTxRxParameters() + }; + EXPECT_THROW(SET_TX_RX_SEQUENCE(us4oem, seq), + IllegalArgumentException); +} +// TODO test memory overflow protection +// ------------------------------------------ Testing parameters set to IUs4OEM + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectRxMapping032) { + // Rx aperture 0-32 + BitMask rxAperture(128, false); + setValuesInRange(rxAperture, 0, 32, true); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + std::vector expectedRxMapping = getRange(0, 32); + EXPECT_CALL(*ius4oemPtr, SetRxChannelMapping(expectedRxMapping, 0)); + + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectRxMapping032Missing1518) { + // Rx aperture 0-32 + BitMask rxAperture(128, false); + setValuesInRange(rxAperture, 0, 32, true); + rxAperture[15] = false; + rxAperture[18] = false; + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + std::vector expectedRxMapping = getRange(0, 32); + // 0, 1, 2, .., 14, 16, 17, 19, 20, ..., 29, 15, 18 + setValuesInRange(expectedRxMapping, 0, 15, + [](size_t i) { return (uint8) (i); }); + setValuesInRange(expectedRxMapping, 15, 17, + [](size_t i) { return (uint8) (i + 1); }); + setValuesInRange(expectedRxMapping, 17, 30, + [](size_t i) { return (uint8) (i + 2); }); + expectedRxMapping[30] = 15; + expectedRxMapping[31] = 18; + + EXPECT_CALL(*ius4oemPtr, SetRxChannelMapping(expectedRxMapping, 0)); + + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectRxMapping1648) { + // Rx aperture 0-32 + BitMask rxAperture(128, false); + setValuesInRange(rxAperture, 16, 48, true); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + std::vector expectedRxMapping(32, 0); + setValuesInRange(expectedRxMapping, 0, 16, + [](size_t i) { return static_cast(i + 16); }); + setValuesInRange(expectedRxMapping, 16, 32, + [](size_t i) { return static_cast(i % 16); }); + EXPECT_CALL(*ius4oemPtr, SetRxChannelMapping(expectedRxMapping, 0)); + + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectNumberOfMappings) { + // Rx aperture 0-32 + BitMask rxAperture1(128, false); + setValuesInRange(rxAperture1, 0, 32, true); + BitMask rxAperture2(128, false); + setValuesInRange(rxAperture2, 16, 48, true); + BitMask rxAperture3(128, false); + setValuesInRange(rxAperture3, 32, 64, true); + + std::vector seq = { + // 1st tx/rx + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture1)) + .getTxRxParameters(), + // 2nd tx/rx + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture2)) + .getTxRxParameters(), + // 3rd tx/rx + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture3)) + .getTxRxParameters() + }; + std::vector expectedRxMapping1 = getRange(0, 32); + std::vector expectedRxMapping2(32, 0); + setValuesInRange(expectedRxMapping2, 0, 16, + [](size_t i) { return static_cast(i + 16); }); + setValuesInRange(expectedRxMapping2, 16, 32, + [](size_t i) { return static_cast(i % 16); }); + + EXPECT_CALL(*ius4oemPtr, SetRxChannelMapping(expectedRxMapping1, 0)); + EXPECT_CALL(*ius4oemPtr, SetRxChannelMapping(expectedRxMapping2, 1)); + + EXPECT_CALL(*ius4oemPtr, ScheduleReceive(0, _, _, _, _, 0, _)); + EXPECT_CALL(*ius4oemPtr, ScheduleReceive(1, _, _, _, _, 1, _)); + EXPECT_CALL(*ius4oemPtr, ScheduleReceive(2, _, _, _, _, 0, _)); + + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +class Us4OEMImplConflictingChannelsTest : public ::testing::Test { +protected: + void SetUp() override { + std::unique_ptr ius4oem = std::make_unique<::testing::NiceMock>(); + ius4oemPtr = dynamic_cast(ius4oem.get()); + BitMask activeChannelGroups = {true, true, true, true, + true, true, true, true, + true, true, true, true, + true, true, true, true}; + // Esaote 2 Us4OEM:0 channel mapping + std::vector channelMapping = castTo({26, 27, 25, 23, 28, 22, 20, 21, + 24, 18, 19, 15, 17, 16, 29, 13, + 11, 14, 30, 8, 12, 5, 10, 9, + 31, 7, 3, 6, 0, 2, 4, 1, + 56, 55, 54, 53, 57, 52, 51, 49, + 50, 48, 47, 46, 44, 45, 58, 42, + 43, 59, 40, 41, 60, 38, 61, 39, + 62, 34, 37, 63, 36, 35, 32, 33, + 92, 93, 89, 91, 88, 90, 87, 85, + 86, 84, 83, 82, 81, 80, 79, 77, + 78, 76, 95, 75, 74, 94, 73, 72, + 70, 64, 71, 68, 65, 69, 67, 66, + 96, 97, 98, 99, 100, 101, 102, 103, + 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127}); + uint16 pgaGain = DEFAULT_PGA_GAIN; + uint16 lnaGain = DEFAULT_LNA_GAIN; + us4oem = std::make_unique( + DeviceId(DeviceType::Us4OEM, 0), + std::move(ius4oem), activeChannelGroups, + channelMapping, + pgaGain, lnaGain, + std::unordered_set() + ); + } + + MockIUs4OEM *ius4oemPtr; + Us4OEMImpl::Handle us4oem; + TGCCurve defaultTGCCurve; + uint16 defaultRxBufferSize = 1; + uint16 defaultBatchSize = 1; + std::optional defaultSri = std::nullopt; +}; + +TEST_F(Us4OEMImplConflictingChannelsTest, TurnsOffConflictingChannels) { + BitMask rxAperture(128, false); + + // 11, 14, 30, 8, 12, 5, 10, 9, + // 31, 7, 3, 6, 0, 2, 4, 1, + // 56, 55, 54, 53, 57, 52, 51, 49, + // 50, 48, 47, 46, 44, 45, 58, 42, + + // 10 (10, 42), 12 (12, 44), 14 (14, 46) are conflicting: + + // (11, 14, 30, 8, 12, 5, 10, 9, + // 31, 7, 3, 6, 0, 2, 4, 1, + // 24, 23, 22, 21, 25, 20, 19, 17, + // 18, 16, 15, 14, 12, 13, 26, 10) + + setValuesInRange(rxAperture, 16, 48, true); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + + std::bitset expectedRxAperture; + setValuesInRange(expectedRxAperture, 16, 48, true); + expectedRxAperture[43] = false; + expectedRxAperture[44] = false; + expectedRxAperture[47] = false; + EXPECT_CALL(*ius4oemPtr, SetRxAperture(expectedRxAperture, 0)); + + // The channel mapping should stay unmodified + // 27, 28, 29 are not used (should be turned off) + std::vector expectedRxMapping = {11, 14, 30, 8, 12, 5, 10, 9, + 31, 7, 3, 6, 0, 2, 4, 1, + 24, 23, 22, 21, 25, 20, 19, 17, + 18, 16, 15, 27, 28, 13, 26, 29}; + + EXPECT_CALL(*ius4oemPtr, SetRxChannelMapping(expectedRxMapping, 0)); + + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +// active channel groups! (NOP, no NOP) + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectRxTimeAndDelay1) { + // Sample range -> rx delay + // end-start / sampling frequency + Interval sampleRange(0, 1024); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.sampleRange = sampleRange)) + .getTxRxParameters() + }; + EXPECT_CALL(*ius4oemPtr, SetRxDelay(Us4OEMImpl::RX_DELAY, 0)); + uint32 nSamples = sampleRange.end() - sampleRange.start(); + float minimumRxTime = float(nSamples) / Us4OEMImpl::SAMPLING_FREQUENCY; + EXPECT_CALL(*ius4oemPtr, SetRxTime(Ge(minimumRxTime), 0)); + EXPECT_CALL(*ius4oemPtr, ScheduleReceive(0, _, nSamples, Us4OEMImpl::SAMPLE_DELAY + sampleRange.start(), _, _, _)); + // ScheduleReceive: starting sample + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectRxTimeAndDelay2) { + // Sample range -> rx delay + // end-start / sampling frequency + Interval sampleRange(40, 1024 + 40); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.sampleRange = sampleRange)) + .getTxRxParameters() + }; + EXPECT_CALL(*ius4oemPtr, SetRxDelay(Us4OEMImpl::RX_DELAY, 0)); + uint32 nSamples = sampleRange.end() - sampleRange.start(); + float minimumRxTime = float(nSamples) / Us4OEMImpl::SAMPLING_FREQUENCY; + EXPECT_CALL(*ius4oemPtr, SetRxTime(Ge(minimumRxTime), 0)); + EXPECT_CALL(*ius4oemPtr, ScheduleReceive(0, _, nSamples, Us4OEMImpl::SAMPLE_DELAY + sampleRange.start(), _, _, _)); + // ScheduleReceive: starting sample + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectNumberOfTxHalfPeriods) { + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pulse = Pulse(3e6f, 1.5, true))) + .getTxRxParameters() + }; + EXPECT_CALL(*ius4oemPtr, SetTxHalfPeriods(3, 0)); + EXPECT_CALL(*ius4oemPtr, SetTxFreqency(3e6f, 0)); + EXPECT_CALL(*ius4oemPtr, SetTxInvert(true, 0)); + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectNumberOfTxHalfPeriods2) { + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pulse = Pulse(3e6, 3, false))) + .getTxRxParameters() + }; + EXPECT_CALL(*ius4oemPtr, SetTxHalfPeriods(6, 0)); + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, SetsCorrectNumberOfTxHalfPeriods3) { + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.pulse = Pulse(3e6, 30.5, false))) + .getTxRxParameters() + }; + EXPECT_CALL(*ius4oemPtr, SetTxHalfPeriods(61, 0)); + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, TurnsOffTGCWhenEmpty) { + std::vector seq = { + TestTxRxParams().getTxRxParameters() + }; + EXPECT_CALL(*ius4oemPtr, TGCDisable); + SET_TX_RX_SEQUENCE_TGC(us4oem, seq, {}); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, InterpolatesToTGCCharacteristicCorrectly) { + std::vector seq = { + TestTxRxParams().getTxRxParameters() + }; + TGCCurve tgc = {14.000f, 14.001f, 14.002f}; + + EXPECT_CALL(*ius4oemPtr, TGCEnable); + + TGCCurve expectedTgc = {14.0f, 15.0f, 16.0f}; + // normalized + for(float &i : expectedTgc) { + i = (i - 14.0f) / 40.f; + } + EXPECT_CALL(*ius4oemPtr, TGCSetSamples(expectedTgc, _)); + + SET_TX_RX_SEQUENCE_TGC(us4oem, seq, tgc); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, InterpolatesToTGCCharacteristicCorrectly2) { + std::vector seq = { + TestTxRxParams().getTxRxParameters() + }; + TGCCurve tgc = {14.000f, 14.0005f, 14.001f}; + + EXPECT_CALL(*ius4oemPtr, TGCEnable); + + TGCCurve expectedTgc = {14.0f, 14.5f, 15.0f}; + // normalized + for(float &i : expectedTgc) { + i = (i - 14.0f) / 40.f; + } + EXPECT_CALL(*ius4oemPtr, TGCSetSamples(Pointwise(FloatNearPointwise(1e-4), expectedTgc), _)); + SET_TX_RX_SEQUENCE_TGC(us4oem, seq, tgc); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, InterpolatesToTGCCharacteristicCorrectly3) { + std::vector seq = { + TestTxRxParams().getTxRxParameters() + }; + TGCCurve tgc = {14.000f, 14.0002f, 14.0007f, 14.001f, 14.0015f}; + + EXPECT_CALL(*ius4oemPtr, TGCEnable); + + TGCCurve expectedTgc = {14.0f, 14.2f, 14.7f, 15.0f, 15.5f}; + // normalized + for(float &i : expectedTgc) { + i = (i - 14.0f) / 40.f; + } + EXPECT_CALL(*ius4oemPtr, TGCSetSamples(Pointwise(FloatNearPointwise(1e-4), expectedTgc), _)); + SET_TX_RX_SEQUENCE_TGC(us4oem, seq, tgc); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, TurnsOffAllChannelsForNOP) { + std::vector seq = { + TxRxParameters::US4OEM_NOP + }; + // empty + std::bitset rxAperture, txAperture; + // empty + std::bitset activeChannelGroup; + EXPECT_CALL(*ius4oemPtr, SetRxAperture(rxAperture, 0)); + EXPECT_CALL(*ius4oemPtr, SetTxAperture(txAperture, 0)); + EXPECT_CALL(*ius4oemPtr, SetActiveChannelGroup(activeChannelGroup, 0)); + + SET_TX_RX_SEQUENCE(us4oem, seq); +} + +TEST_F(Us4OEMImplEsaote3LikeTest, TestFrameChannelMappingForNonconflictingRxMapping) { + BitMask rxAperture(128, false); + setValuesInRange(rxAperture, 0, 32, true); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(us4oem, seq); + + EXPECT_EQ(fcm->getNumberOfLogicalFrames(), 1); + + for(size_t i = 0; i < Us4OEMImpl::N_RX_CHANNELS; ++i) { + auto[dstFrame, dstChannel] = fcm->getLogical(0, i); + EXPECT_EQ(dstChannel, i); + EXPECT_EQ(dstFrame, 0); + } +} + +TEST_F(Us4OEMImplEsaote3LikeTest, TestFrameChannelMappingForNonconflictingRxMapping2) { + BitMask rxAperture(128, false); + setValuesInRange(rxAperture, 16, 48, true); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(us4oem, seq); + + EXPECT_EQ(fcm->getNumberOfLogicalFrames(), 1); + + for(size_t i = 0; i < Us4OEMImpl::N_RX_CHANNELS; ++i) { + auto[dstFrame, dstChannel] = fcm->getLogical(0, i); + EXPECT_EQ(dstChannel, i); + EXPECT_EQ(dstFrame, 0); + } +} + +TEST_F(Us4OEMImplEsaote3LikeTest, TestFrameChannelMappingIncompleteRxAperture) { + BitMask rxAperture(128, false); + setValuesInRange(rxAperture, 0, 32, true); + + rxAperture[31] = rxAperture[15] = false; + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(us4oem, seq); + + EXPECT_EQ(fcm->getNumberOfLogicalFrames(), 1); + + for(size_t i = 0; i < 30; ++i) { + auto[dstFrame, dstChannel] = fcm->getLogical(0, i); + EXPECT_EQ(dstChannel, i); + EXPECT_EQ(dstFrame, 0); + } +} + +TEST_F(Us4OEMImplConflictingChannelsTest, TestFrameChannelMappingForConflictingMapping) { + BitMask rxAperture(128, false); + // (11, 14, 30, 8, 12, 5, 10, 9, + // 31, 7, 3, 6, 0, 2, 4, 1, + // 24, 23, 22, 21, 25, 20, 19, 17, + // 18, 16, 15, 14, 12, 13, 26, 10) + setValuesInRange(rxAperture, 16, 48, true); + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + (x.rxAperture = rxAperture)) + .getTxRxParameters() + }; + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(us4oem, seq); + + for(size_t i = 0; i < Us4OEMImpl::N_RX_CHANNELS; ++i) { + auto[dstfr, dstch] = fcm->getLogical(0, i); + std::cerr << (int16) dstch << ", "; + } + std::cerr << std::endl; + + EXPECT_EQ(fcm->getNumberOfLogicalFrames(), 1); + // turned off channels should be zeroed, so we just expect 0-31 here + std::vector expectedDstChannels = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31 + }; + + for(size_t i = 0; i < Us4OEMImpl::N_RX_CHANNELS; ++i) { + auto[dstFrame, dstChannel] = fcm->getLogical(0, i); + EXPECT_EQ(dstChannel, expectedDstChannels[i]); + EXPECT_EQ(dstFrame, 0); + } +} + +// ------------------------------------------ TESTING CHANNEL MASKING + +class Us4OEMImplEsaote3ChannelsMaskTest : public ::testing::Test { +protected: + void SetUp() override { + ius4oem = std::make_unique<::testing::NiceMock>(); + ius4oemPtr = dynamic_cast(ius4oem.get()); + } + + Us4OEMImpl::Handle createHandle(const std::unordered_set &channelsMask) { + // This function can be called only once. + + BitMask activeChannelGroups = {true, true, true, true, + true, true, true, true, + true, true, true, true, + true, true, true, true}; + + std::vector channelMapping = getRange(0, 128); + + const uint16 pgaGain = DEFAULT_PGA_GAIN; + const uint16 lnaGain = DEFAULT_LNA_GAIN; + return std::make_unique( + DeviceId(DeviceType::Us4OEM, 0), + // NOTE: due to the below move this function can be called only once + std::move(ius4oem), activeChannelGroups, + channelMapping, + pgaGain, lnaGain, + channelsMask + ); + + } + + std::unique_ptr ius4oem; + MockIUs4OEM *ius4oemPtr; + TGCCurve defaultTGCCurve; + uint16 defaultRxBufferSize = 1; + uint16 defaultBatchSize = 1; + std::optional defaultSri = std::nullopt; +}; + +// no masking - no channels are turned off +TEST_F(Us4OEMImplEsaote3ChannelsMaskTest, DoesNothingWithAperturesWhenNoChannelMask) { + auto us4oem = createHandle(std::unordered_set({})); + + BitMask rxAperture(128, false); + BitMask txAperture(128, false); + std::vector txDelays(128, 0.0); + + txAperture[0] = txAperture[6] = txAperture[31] = txAperture[59] = true; + txDelays[0] = 1e-6; + txDelays[6] = 2e-6; + txDelays[31] = 4e-6; + txDelays[59] = 8e-6; + rxAperture[0] = rxAperture[7] = rxAperture[31] = rxAperture[60] = true; + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.rxAperture = rxAperture, + x.txAperture = txAperture, + x.txDelays = txDelays + )) + .getTxRxParameters() + }; + + auto expectedTxAperture = ::arrus::toBitset(txAperture); + auto expectedRxAperture = ::arrus::toBitset(rxAperture); + auto &expectedTxDelays = txDelays; + + EXPECT_CALL(*ius4oemPtr, SetRxAperture(expectedRxAperture, 0)); + EXPECT_CALL(*ius4oemPtr, SetTxAperture(expectedTxAperture, 0)); + for(int i = 0; i < expectedTxDelays.size(); ++i) { + EXPECT_CALL(*ius4oemPtr, SetTxDelay(i, expectedTxDelays[i], 0)); + } + + SET_TX_RX_SEQUENCE(us4oem, seq); +} + + +TEST_F(Us4OEMImplEsaote3ChannelsMaskTest, MasksProperlyASingleChannel) { + auto us4oem = createHandle(std::unordered_set({7})); + + BitMask rxAperture(128, false); + BitMask txAperture(128, false); + std::vector txDelays(128, 0.0); + + txAperture[0] = txAperture[7] = txAperture[33] = txAperture[95] = true; + txDelays[0] = txDelays[7] = txDelays[33] = txDelays[95] = 1e-6; + rxAperture[0] = rxAperture[7] = rxAperture[31] = rxAperture[60] = true; + + std::vector seq = { + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.rxAperture = rxAperture, + x.txAperture = txAperture, + x.txDelays = txDelays + )) + .getTxRxParameters() + }; + + auto expectedTxAperture = ::arrus::toBitset(txAperture); + expectedTxAperture[7] = false; + auto expectedRxAperture = ::arrus::toBitset(rxAperture); + expectedRxAperture[7] = false; + + std::vector expectedTxDelays(txDelays); + expectedTxDelays[7] = 0.0f; + + EXPECT_CALL(*ius4oemPtr, SetRxAperture(expectedRxAperture, 0)); + EXPECT_CALL(*ius4oemPtr, SetTxAperture(expectedTxAperture, 0)); + for(int i = 0; i < expectedTxDelays.size(); ++i) { + EXPECT_CALL(*ius4oemPtr, SetTxDelay(i, expectedTxDelays[i], 0)); + } + + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(us4oem, seq); + + EXPECT_EQ(fcm->getNumberOfLogicalFrames(), 1); + ASSERT_EQ(fcm->getNumberOfLogicalChannels(), Us4OEMImpl::N_RX_CHANNELS); + + std::vector expectedSrcChannels(Us4OEMImpl::N_RX_CHANNELS, -1); + expectedSrcChannels[0] = 0; + expectedSrcChannels[1] = 1; + expectedSrcChannels[2] = 2; + expectedSrcChannels[3] = 3; + + for(int i = 0; i < Us4OEMImpl::N_RX_CHANNELS; ++i) { + auto[srcFrame, srcChannel] = fcm->getLogical(0, i); + EXPECT_EQ(srcFrame, 0); + ASSERT_EQ(srcChannel, expectedSrcChannels[i]); + } +} + +#define MASK_CHANNELS(aperture, channelsToMask) \ +do { \ + for(auto channel: channelsToMask) { \ + aperture[channel] = false; \ + }\ +} while(0) + +#define SET_EXPECTED_MASKED_APERTURES(txAperture, rxAperture, channelsMask) \ + BitMask expectedTxAperture(txAperture); \ + MASK_CHANNELS(expectedTxAperture, channelsMask); \ + expectedTxApertures.push_back(::arrus::toBitset(expectedTxAperture)); \ + BitMask expectedRxAperture(rxAperture); \ + MASK_CHANNELS(expectedRxAperture, channelsMask); \ + expectedRxApertures.push_back(::arrus::toBitset(expectedRxAperture)); + +TEST_F(Us4OEMImplEsaote3ChannelsMaskTest, MasksProperlyASingleChannelForAllOperations) { + std::unordered_set channelsMask {7, 60, 93}; + auto us4oem = createHandle(channelsMask); + std::vector seq; + + const auto N_ADDR_CHANNELS = Us4OEMImpl::N_ADDR_CHANNELS; + + std::vector txApertures; + std::vector rxApertures; + std::vector> expectedTxApertures; + std::vector> expectedRxApertures; + + { + // Op 0: + // Given + BitMask txAperture(128, false); + BitMask rxAperture(128, false); + + txAperture[0] = txAperture[7] = txAperture[33] = txAperture[95] = true; + rxAperture[0] = rxAperture[7] = rxAperture[31] = rxAperture[60] = true; + + txApertures.push_back(txAperture); + rxApertures.push_back(rxAperture); + seq.push_back( + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.rxAperture = rxAperture, + x.txAperture = txAperture + )) + .getTxRxParameters()); + + // Expected: + SET_EXPECTED_MASKED_APERTURES(txAperture, rxAperture, channelsMask); + } + { + // Op 1: + BitMask rxAperture(128, false); + BitMask txAperture(128, false); + std::vector txDelays(128, 0.0); + + ::arrus::setValuesInRange(txAperture, 16, 64+16, true); + ::arrus::setValuesInRange(rxAperture, 48, 48+32, true); + + txApertures.push_back(txAperture); + rxApertures.push_back(rxAperture); + + seq.push_back( + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.rxAperture = rxAperture, + x.txAperture = txAperture, + x.txDelays = txDelays + )) + .getTxRxParameters()); + // Expected: + SET_EXPECTED_MASKED_APERTURES(txAperture, rxAperture, channelsMask); + } + { + // Op 2: + BitMask rxAperture(128, false); + BitMask txAperture(128, false); + std::vector txDelays(128, 0.0); + + ::arrus::setValuesInRange(txAperture, 0, 64, true); + ::arrus::setValuesInRange(rxAperture, 0, 32, true); + + txApertures.push_back(txAperture); + rxApertures.push_back(rxAperture); + + seq.push_back( + ARRUS_STRUCT_INIT_LIST( + TestTxRxParams, + ( + x.rxAperture = rxAperture, + x.txAperture = txAperture, + x.txDelays = txDelays + )) + .getTxRxParameters()); + // Expected: + SET_EXPECTED_MASKED_APERTURES(txAperture, rxAperture, channelsMask); + } + + ASSERT_EQ(expectedRxApertures.size(), expectedTxApertures.size()); + + for(int i = 0; i < expectedRxApertures.size(); ++i) { + EXPECT_CALL(*ius4oemPtr, SetRxAperture(expectedRxApertures[i], i)); + EXPECT_CALL(*ius4oemPtr, SetTxAperture(expectedTxApertures[i], i)); + } + + auto [buffer, fcm] = SET_TX_RX_SEQUENCE(us4oem, seq); + + // Validate generated FCM + ASSERT_EQ(fcm->getNumberOfLogicalFrames(), 3); + ASSERT_EQ(fcm->getNumberOfLogicalChannels(), Us4OEMImpl::N_RX_CHANNELS); + + { + // Frame 0 + + std::vector expectedSrcChannels(Us4OEMImpl::N_RX_CHANNELS, -1); + expectedSrcChannels[0] = 0; + expectedSrcChannels[1] = 1; + // rx aperture channel 1 is turned off (channel 7), but still we want to have it here + expectedSrcChannels[2] = 2; + // rx aperture channel 3 is missing (channel 60) + expectedSrcChannels[3] = 3; + + for(int i = 0; i < Us4OEMImpl::N_RX_CHANNELS; ++i) { + auto [srcFrame, srcChannel] = fcm->getLogical(0, i); + EXPECT_EQ(srcFrame, 0); + ASSERT_EQ(srcChannel, expectedSrcChannels[i]); + } + } + { + // Frame 1, 2 + for(int frame = 1; frame <= 2; ++frame) { + uint8 i = 0; + ChannelIdx rxChannelNumber = 0; + for(auto bit : rxApertures[frame]) { + if(bit) { + auto [srcFrame, srcChannel] = fcm->getLogical(frame, i); + std::cerr << frame << ", " << (int)i << ", " << srcFrame << ", " << (int)srcChannel << std::endl; + ASSERT_EQ(srcFrame, frame); + ASSERT_EQ(srcChannel, i++); + } + ++rxChannelNumber; + } + } + } +} +} + +int main(int argc, char **argv) { + std::cerr << "Starting" << std::endl; + ARRUS_INIT_TEST_LOG(arrus::Logging); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMSettings.cpp b/arrus/core/devices/us4r/us4oem/Us4OEMSettings.cpp new file mode 100644 index 000000000..0037c002c --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMSettings.cpp @@ -0,0 +1,17 @@ +#include "Us4OEMSettings.h" + +#include "arrus/common/format.h" +#include "arrus/core/devices/us4r/RxSettings.h" + +namespace arrus::devices { + +std::ostream & +operator<<(std::ostream &os, const Us4OEMSettings &settings) { + os << "channelMapping: " << ::arrus::toString(settings.getChannelMapping()) + << " activeChannelGroups: " + << ::arrus::toString(settings.getActiveChannelGroups()) + << " rxSettings: " << settings.getRxSettings(); + return os; +} + +} diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMSettings.h b/arrus/core/devices/us4r/us4oem/Us4OEMSettings.h new file mode 100644 index 000000000..9b1247b8a --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMSettings.h @@ -0,0 +1,10 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMSETTINGS_H +#define ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMSETTINGS_H + +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" + +namespace arrus::devices { +std::ostream & operator<<(std::ostream &os, const Us4OEMSettings &settings); +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMSETTINGS_H diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMSettingsValidator.h b/arrus/core/devices/us4r/us4oem/Us4OEMSettingsValidator.h new file mode 100644 index 000000000..eb09b7468 --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMSettingsValidator.h @@ -0,0 +1,131 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMSETTINGSVALIDATOR_H +#define ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMSETTINGSVALIDATOR_H + +#include + +#include "arrus/core/common/validation.h" +#include "arrus/core/common/logging.h" +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMImpl.h" +#include "arrus/core/devices/SettingsValidator.h" + +#include "arrus/core/devices/us4r/external/ius4oem/PGAGainValueMap.h" +#include "arrus/core/devices/us4r/external/ius4oem/LNAGainValueMap.h" +#include "arrus/core/devices/us4r/external/ius4oem/LPFCutoffValueMap.h" +#include "arrus/core/devices/us4r/external/ius4oem/DTGCAttenuationValueMap.h" +#include "arrus/core/devices/us4r/external/ius4oem/ActiveTerminationValueMap.h" + +namespace arrus::devices { + +class Us4OEMSettingsValidator : public SettingsValidator { +public: + explicit Us4OEMSettingsValidator(Ordinal moduleOrdinal) + : SettingsValidator( + DeviceId(DeviceType::Us4OEM, moduleOrdinal)) {} + + void validate(const Us4OEMSettings &obj) override { + constexpr ChannelIdx RX_SIZE = Us4OEMImpl::N_RX_CHANNELS; + constexpr ChannelIdx N_TX_CHANNELS = Us4OEMImpl::N_TX_CHANNELS; + constexpr ChannelIdx N_RX_GROUPS = N_TX_CHANNELS / RX_SIZE; + + // Active channel groups + expectEqual("active channel groups", + obj.getActiveChannelGroups().size(), + (size_t)Us4OEMImpl::N_ACTIVE_CHANNEL_GROUPS, + "(size)"); + + // Channel mapping: + // The size of the mapping: + // Us4OEM mapping should include all channels, we don't want + // the situation, where some o channels are not defined. + expectEqual("channel mapping", + obj.getChannelMapping().size(), + (size_t)N_TX_CHANNELS, + "(size)"); + + if(obj.getChannelMapping().size() == (size_t)N_TX_CHANNELS) { + auto &channelMapping = obj.getChannelMapping(); + + // Check if contains (possibly permuted) groups: + // 0-31, 32-63, 64-95, 96-127 + for(unsigned char group = 0; group < N_RX_GROUPS; ++group) { + std::unordered_set groupValues{ + std::begin(channelMapping) + group * RX_SIZE, + std::begin(channelMapping) + (group + 1) * RX_SIZE}; + + std::vector missingValues; + for(ChannelIdx j = group * RX_SIZE; + j < (ChannelIdx) (group + 1) * RX_SIZE; ++j) { + if(groupValues.find(j) == groupValues.end()) { + missingValues.push_back(j); + } + } + expectTrue( + "channel mapping", + missingValues.empty(), + arrus::format( + "Some of Us4OEM channels: '{}' " + "are missing in the group of channels [{}, {}]", + ::arrus::toString(missingValues), + group * RX_SIZE, (group + 1) * RX_SIZE + ) + ); + } + } + // TGC samples + if(obj.getRxSettings().getDtgcAttenuation().has_value()) { + expectOneOf( + "dtgc attenuation", + obj.getRxSettings().getDtgcAttenuation().value(), + DTGCAttenuationValueMap::getInstance().getAvailableValues() + ); + } + expectOneOf( + "pga gain", + obj.getRxSettings().getPgaGain(), + PGAGainValueMap::getInstance().getAvailableValues()); + expectOneOf( + "lna gain", + obj.getRxSettings().getLnaGain(), + LNAGainValueMap::getInstance().getAvailableValues()); + + if(!obj.getRxSettings().getTgcSamples().empty()) { + // Maximum/minimum number of samples. + expectInRange( + "tgc samples", + obj.getRxSettings().getTgcSamples().size(), + (size_t)1, (size_t)Us4OEMImpl::TGC_N_SAMPLES, + "(size)" + ); + + // Maximum/minimum value of a TGC sample. + auto tgcMax = float(obj.getRxSettings().getPgaGain() + + obj.getRxSettings().getLnaGain()); + auto tgcMin = std::max(0.0f, float(tgcMax - Us4OEMImpl::TGC_ATTENUATION_RANGE)); + expectAllInRange("tgc samples", + obj.getRxSettings().getTgcSamples(), tgcMin, + tgcMax); + } + + // Active termination. + if(obj.getRxSettings().getActiveTermination().has_value()) { + expectOneOf( + "active termination", + obj.getRxSettings().getActiveTermination().value(), + ActiveTerminationValueMap::getInstance().getAvailableValues() + ); + } + + // LPF cutoff. + expectOneOf( + "lpf cutoff", + obj.getRxSettings().getLpfCutoff(), + LPFCutoffValueMap::getInstance().getAvailableValues() + ); + } + +}; + +} + +#endif //ARRUS_CORE_DEVICES_US4R_US4OEM_US4OEMSETTINGSVALIDATOR_H diff --git a/arrus/core/devices/us4r/us4oem/Us4OEMSettingsValidatorTest.cpp b/arrus/core/devices/us4r/us4oem/Us4OEMSettingsValidatorTest.cpp new file mode 100644 index 000000000..8236585ea --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/Us4OEMSettingsValidatorTest.cpp @@ -0,0 +1,192 @@ +#include +#include +#include "arrus/core/common/tests.h" +#include "arrus/core/common/logging.h" +#include "arrus/core/common/collections.h" +#include "arrus/common/logging/impl/Logging.h" +#include "Us4OEMSettingsValidator.h" + +namespace { +using namespace arrus; +using namespace arrus::devices; + +#include "arrus/core/devices/us4r/us4oem/tests/CommonSettings.h" + + +class CorrectUs4OEMSettingsTest + : public testing::TestWithParam { +}; + +TEST_P(CorrectUs4OEMSettingsTest, ValidateCorrectUs4OEMSettings) { + Us4OEMSettingsValidator validator(0); + TestUs4OEMSettings val = GetParam(); + validator.validate(val.getUs4OEMSettings()); + EXPECT_NO_THROW(validator.throwOnErrors()); +} + +INSTANTIATE_TEST_CASE_P + +(ValidUs4OEMSettings, CorrectUs4OEMSettingsTest, + testing::Values( + TestUs4OEMSettings{}, + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.channelMapping={ + // 0-31 + 26, 27, 25, 23, 28, 22, 20, 21, + 24, 18, 19, 15, 17, 16, 29, 13, + 11, 14, 30, 8, 12, 5, 10, 9, + 31, 7, 3, 6, 0, 2, 4, 1, + // 32-63 + 56, 55, 54, 53, 57, 52, 51, 49, + 50, 48, 47, 46, 44, 45, 58, 42, + 43, 59, 40, 41, 60, 38, 61, 39, + 62, 34, 37, 63, 36, 35, 32, 33, + // 64-95 + 65, 67, 66, 69, 64, 68, 71, 70, + 72, 74, 73, 75, 76, 77, 78, 79, + 80, 82, 81, 83, 85, 84, 87, 86, + 88, 92, 89, 94, 90, 91, 95, 93, + // 96-127 + 96, 97, 98, 99, 100, 101, 102, 103, + 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127})), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.activeChannelGroups={ + false, false, false, true, true, true, true, true, + true, false, true, true, true, true, true, true})), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.dtgcAttenuation=6)), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.dtgcAttenuation={})), // Turn off + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.pgaGain=24)), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.lnaGain=24)), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.lpfCutoff=(int) 15e6)), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.activeTermination=200)), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.activeTermination={})), // Turn off + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.tgcSamples={14.0f, 20.0f, 25.0f})), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.tgcSamples={})) // Turn off + )); + +class InCorrectUs4OEMSettingsTest + : public testing::TestWithParam { +}; + +TEST_P(InCorrectUs4OEMSettingsTest, ValidateInCorrectUs4OEMSettings) { + Us4OEMSettingsValidator validator(0); + TestUs4OEMSettings val = GetParam(); + validator.validate(val.getUs4OEMSettings()); + EXPECT_THROW(validator.throwOnErrors(), IllegalArgumentException); + for(const auto& invalidParameter : GetParam().invalidParameters) { + EXPECT_FALSE(validator.getErrors(invalidParameter).empty()); + } +} + +INSTANTIATE_TEST_CASE_P + +(InvalidUs4OEMSettings, InCorrectUs4OEMSettingsTest, + testing::Values( + // Invalid size of the channel mapping + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.channelMapping = {0, 1, 2, 3, 4, 5, 6, 7, 8}, + x.invalidParameters = {"channel mapping"})), + // Invalid mapping (channel mapping are mixed between 32-element groups) + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, (x.channelMapping={ + // 0-31, missing 11 + 26, 27, 25, 23, 28, 22, 20, 21, + 24, 18, 19, 15, 17, 16, 29, 13, + 14, 30, 8, 12, 5, 10, 9, + 31, 7, 3, 6, 0, 2, 4, 1, + 42, + // 32-63, missing 42 + 56, 55, 54, 53, 57, 52, 51, 49, + 50, 48, 47, 46, 44, 45, 58, + 43, 59, 40, 41, 60, 38, 61, 39, + 62, 34, 37, 63, 36, 35, 32, 33, + 11, + // 64-95, missing 77 + 65, 67, 66, 69, 64, 68, 71, 70, + 72, 74, 73, 75, 76, 78, 79, + 80, 82, 81, 83, 85, 84, 87, 86, + 88, 92, 89, 94, 90, 91, 95, 93, + 123, + // 96-127, missing 123 + 96, 97, 98, 99, 100, 101, 102, 103, + 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 124, 125, 126, 127, + 77})), + + // Invalid number of active channel groups + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.activeChannelGroups = getNTimes(false, 15), + x.invalidParameters = {"active channel groups"})), + // Empty array of active channel groups + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.activeChannelGroups = {}, + x.invalidParameters = {"active channel groups"})), + // Invalid value + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.pgaGain = 777, + x.invalidParameters = {"pga gain"})), + // Invalid value + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.lnaGain = 666, + x.invalidParameters = {"lna gain"})), + // Invalid value + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.dtgcAttenuation = 123, + x.invalidParameters = {"dtgc attenuation"})), + // Invalid value + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.lpfCutoff = 1, + x.invalidParameters = {"lpf cutoff"})), + // Invalid value + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.activeTermination = 9999, + x.invalidParameters = {"active termination"})), + + // Invalid number of TGC samples + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.tgcSamples = getNTimes(40.0f, 1024), + x.invalidParameters = {"tgc samples"})), + // Invalid TGC samples values (1) below the range + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.pgaGain = 30, + x.lnaGain = 24, + x.tgcSamples = {13.0f}, + x.invalidParameters = {"tgc samples"})), + // Invalid TGC samples values (2) above the range + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.pgaGain = 30, + x.lnaGain = 24, + x.tgcSamples = {55.0f}, + x.invalidParameters = {"tgc samples"})), + // Invalid TGC samples values (3) below 0 + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.pgaGain = 24, + x.lnaGain = 12, + x.tgcSamples = {-1.0f}, + x.invalidParameters = {"tgc samples"})), + // Invalid TGC samples values (3) multiple wrong values + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.pgaGain = 24, + x.lnaGain = 24, + x.tgcSamples = {0, 15, 50.0f}, + x.invalidParameters = {"tgc samples"})), + ARRUS_STRUCT_INIT_LIST(TestUs4OEMSettings, ( + x.pgaGain = 11, + x.lnaGain = 22, + x.invalidParameters = {"pga gain", "lna gain"})) +)); + +// Test that multiple errors are signaled + +// Main +int main(int argc, char **argv) { + ARRUS_INIT_TEST_LOG(arrus::Logging); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +} + + + + + diff --git a/arrus/core/devices/us4r/us4oem/tests/CommonSettings.h b/arrus/core/devices/us4r/us4oem/tests/CommonSettings.h new file mode 100644 index 000000000..73bcecb6e --- /dev/null +++ b/arrus/core/devices/us4r/us4oem/tests/CommonSettings.h @@ -0,0 +1,50 @@ +#ifndef ARRUS_CORE_DEVICES_US4R_US4OEM_TESTS_COMMONS_H +#define ARRUS_CORE_DEVICES_US4R_US4OEM_TESTS_COMMONS_H + +#include "arrus/core/common/collections.h" +#include "arrus/core/api/devices/us4r/Us4OEMSettings.h" + +using namespace arrus; +using namespace arrus::devices; + +struct TestUs4OEMSettings { + std::vector channelMapping{getRange(0, 128)}; + BitMask activeChannelGroups{getNTimes(true, 16)}; + std::optional dtgcAttenuation{42}; + uint16 pgaGain{30}; + uint16 lnaGain{24}; + RxSettings::TGCCurve tgcSamples{getRange(30, 40, 0.5)}; + uint32 lpfCutoff{(int) 10e6}; + std::optional activeTermination{50}; + + std::vector invalidParameters; + + Us4OEMSettings getUs4OEMSettings() const { + return Us4OEMSettings(channelMapping, activeChannelGroups, + RxSettings( + dtgcAttenuation, pgaGain, + lnaGain, tgcSamples, + lpfCutoff, activeTermination), + std::unordered_set()); + } + + friend std::ostream & + operator<<(std::ostream &os, const TestUs4OEMSettings &settings) { + os << "channelMapping: " << toString(settings.channelMapping) + << " activeChannelGroups: " << toString(settings.activeChannelGroups) + << " dtgcAttenuation: " << toString(settings.dtgcAttenuation) + << " pgaGain: " << (int) settings.pgaGain + << " lnaGain: " << (int) settings.lnaGain + << " lpfCutoff: " << settings.lpfCutoff + << " activeTermination: " << toString(settings.activeTermination) + << " tgcSamples: " << toString(settings.tgcSamples); + + for(const auto &invalidParameter : settings.invalidParameters) { + os << " invalidParameter: " << invalidParameter; + } + return os; + } +}; + + +#endif //ARRUS_CORE_DEVICES_US4R_US4OEM_TESTS_COMMONS_H diff --git a/arrus/core/devices/utils.h b/arrus/core/devices/utils.h new file mode 100644 index 000000000..e0d2c79d3 --- /dev/null +++ b/arrus/core/devices/utils.h @@ -0,0 +1,41 @@ +#ifndef ARRUS_CORE_DEVICES_UTILS_H +#define ARRUS_CORE_DEVICES_UTILS_H + +#include +#include "arrus/common/format.h" +#include "arrus/core/api/common/exceptions.h" + +namespace arrus::devices { + +// Device path +constexpr char PATH_DELIMITER = '/'; + +inline +std::pair getPathRoot(const std::string &path) { + if(path.empty() || path[0] != PATH_DELIMITER) { + throw IllegalArgumentException( + ::arrus::format("Invalid path '{}', should start with '{}'", + path, PATH_DELIMITER)); + } + + std::string relPath = path.substr(1, path.size() - 1); + if(relPath.empty()) { + throw IllegalArgumentException( + arrus::format("Path should refer to some object " + "(got: '{}')", path)); + } + + size_t firstElementEnd = relPath.find(PATH_DELIMITER); + if(firstElementEnd == std::string::npos) { + return {relPath, ""}; + } else { + return { + relPath.substr(0, firstElementEnd), + relPath.substr(firstElementEnd, relPath.size()-firstElementEnd) + }; + } +} + +} + +#endif //ARRUS_CORE_DEVICES_UTILS_H diff --git a/arrus/core/devices/utilsTest.cpp b/arrus/core/devices/utilsTest.cpp new file mode 100644 index 000000000..0ad226719 --- /dev/null +++ b/arrus/core/devices/utilsTest.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include "arrus/core/api/common/exceptions.h" +#include "arrus/core/devices/utils.h" +#include "arrus/core/common/tests.h" + +namespace { + +struct GetPathRootTestCase { + std::string path; + std::pair expectedRootTail; + + friend std::ostream & + operator<<(std::ostream &os, const GetPathRootTestCase &aCase) { + os << "path: " << aCase.path << " expectedRootTail: " + << aCase.expectedRootTail.first + << ", " << aCase.expectedRootTail.second; + return os; + } +}; + +class GetPathRootTest + : public testing::TestWithParam { +}; + +TEST_P(GetPathRootTest, CorrectlyExtractsRootAndTail) { + GetPathRootTestCase tc = GetParam(); + auto[root, tail] = ::arrus::devices::getPathRoot(tc.path); + + EXPECT_EQ(root, tc.expectedRootTail.first); + EXPECT_EQ(tail, tc.expectedRootTail.second); +} + +INSTANTIATE_TEST_CASE_P + +(TestingCorrectGetPathRoot, GetPathRootTest, + testing::Values( + ARRUS_STRUCT_INIT_LIST(GetPathRootTestCase, ( + x.path = "/Us4R:0", + x.expectedRootTail = {"Us4R:0", ""} + )), + ARRUS_STRUCT_INIT_LIST(GetPathRootTestCase, ( + x.path = "/Us4R:0/Probe:0", + x.expectedRootTail = {"Us4R:0", "/Probe:0"} + )), + ARRUS_STRUCT_INIT_LIST(GetPathRootTestCase, ( + x.path = "/Us4R:0/Us4OEM:3", + x.expectedRootTail = {"Us4R:0", "/Us4OEM:3"} + )), + ARRUS_STRUCT_INIT_LIST(GetPathRootTestCase, ( + x.path = "/Us4R:0/Us4OEM:3/Sequencer:0", + x.expectedRootTail = {"Us4R:0", "/Us4OEM:3/Sequencer:0"} + )) + ) +); + +class GetPathRootInvalidInputTest + : public testing::TestWithParam { +}; + +TEST_P(GetPathRootInvalidInputTest, GetPathRootInvalidInputTest) { + GetPathRootTestCase tc = GetParam(); + EXPECT_THROW(::arrus::devices::getPathRoot(tc.path), + ::arrus::IllegalArgumentException); +} + +INSTANTIATE_TEST_CASE_P +(InvalidDataTest, GetPathRootInvalidInputTest, +testing::Values( + ARRUS_STRUCT_INIT_LIST(GetPathRootTestCase, (x.path = "")), + ARRUS_STRUCT_INIT_LIST(GetPathRootTestCase, (x.path = "/")) +)); + +} diff --git a/arrus/core/examples/CoreExample.cpp b/arrus/core/examples/CoreExample.cpp new file mode 100644 index 000000000..1a9070690 --- /dev/null +++ b/arrus/core/examples/CoreExample.cpp @@ -0,0 +1,85 @@ +#include +#include +#include +#include +#include + +#include "arrus/core/api/io/settings.h" +#include "arrus/core/api/session/Session.h" +#include "arrus/core/api/common/logging.h" +#include "arrus/common/logging/impl/Logging.h" +#include "arrus/core/api/devices/us4r/Us4R.h" +#include "arrus/core/api/ops/us4r/TxRxSequence.h" + +int main() noexcept { + using namespace ::arrus::ops::us4r; + try { + auto loggingMechanism = std::make_shared<::arrus::Logging>(); + std::shared_ptr ostream{ + std::shared_ptr(&std::cout, [](std::ostream *) {})}; + loggingMechanism->addTextSink(ostream, ::arrus::LogSeverity::TRACE); +// std::shared_ptr logFileStream = +// // append to the end of the file +// std::make_shared(R"(C:\Users\pjarosik\cpplog.txt)", std::ios_base::app); +// loggingMechanism->addTextSink(logFileStream, arrus::LogSeverity::TRACE); + + ::arrus::setLoggerFactory(loggingMechanism); + + auto settings = + ::arrus::io::readSessionSettings( + R"(C:\Users\pjarosik\src\x-files\customers\nanoecho\nanoecho_magprobe_002.prototxt)"); + auto session = ::arrus::session::createSession(settings); + auto us4r = (::arrus::devices::Us4R *) session->getDevice("/Us4R:0"); + + ::arrus::BitMask txAperture(192, true); + ::arrus::BitMask rxAperture(192, true); + std::vector delays(192, 0.0f); + + Pulse pulse(4e6, 2, false); + ::std::pair<::arrus::uint32, arrus::uint32> sampleRange{0, 4096}; + + std::vector txrxs; + + for(int i = 0; i < 175; ++i) { + arrus::BitMask aperture(192, false); + + auto origin = i-32; + + unsigned short leftPadding = 0, rightPadding = 0; + for(int j = 0; j < 64; ++j) { + auto idx = origin + j; + aperture[std::min(std::max(idx, 0), 191)] = true; + if(idx < 0) { + ++leftPadding; + } + if(idx > 191) { + ++rightPadding; + } + } + + txrxs.emplace_back(Tx(aperture, delays, pulse), Rx(aperture, sampleRange, 1, {leftPadding, rightPadding}), 100e-6); + } + + TxRxSequence seq(txrxs, {}, 200e-3); + us4r->setVoltage(30); + + auto[buffer, fcm] = us4r->upload(seq, 2, 2); + + us4r->start(); + for(int i = 0; i < 10; ++i) { + std::string msg = "i: " + std::to_string(i) + "\n"; + std::cout << msg; + int16_t* data = buffer->tail(::arrus::devices::HostBuffer::INF_TIMEOUT); +// std::cout << "Got data" << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + buffer->releaseTail(::arrus::devices::HostBuffer::INF_TIMEOUT); + } + us4r->stop(); + + } catch(const std::exception &e) { + std::cerr << e.what() << std::endl; + return -1; + } + + return 0; +} diff --git a/arrus/core/external/eigen/Dense.h b/arrus/core/external/eigen/Dense.h new file mode 100644 index 000000000..6abdf7e12 --- /dev/null +++ b/arrus/core/external/eigen/Dense.h @@ -0,0 +1,11 @@ +#ifndef ARRUS_CORE_EXTERNAL_EIGEN_DENSE_H +#define ARRUS_CORE_EXTERNAL_EIGEN_DENSE_H + +#include "arrus/common/compiler.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4554 4127) +#include +COMPILER_POP_DIAGNOSTIC_STATE + +#endif //ARRUS_CORE_EXTERNAL_EIGEN_DENSE_H diff --git a/arrus/core/external/eigen/Tensor.h b/arrus/core/external/eigen/Tensor.h new file mode 100644 index 000000000..9c7f792b5 --- /dev/null +++ b/arrus/core/external/eigen/Tensor.h @@ -0,0 +1,11 @@ +#ifndef ARRUS_CORE_EXTERNAL_EIGEN_TENSOR_H +#define ARRUS_CORE_EXTERNAL_EIGEN_TENSOR_H + +#include "arrus/common/compiler.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4554 4127) +#include +COMPILER_POP_DIAGNOSTIC_STATE + +#endif //ARRUS_CORE_EXTERNAL_EIGEN_TENSOR_H diff --git a/arrus/core/framework/graph/Graph.h b/arrus/core/framework/graph/Graph.h new file mode 100644 index 000000000..ef7dd487c --- /dev/null +++ b/arrus/core/framework/graph/Graph.h @@ -0,0 +1,4 @@ +#ifndef ARRUS_CORE_FRAMEWORK_GRAPH_GRAPH_H +#define ARRUS_CORE_FRAMEWORK_GRAPH_GRAPH_H + +#endif //ARRUS_CORE_FRAMEWORK_GRAPH_GRAPH_H diff --git a/arrus/core/io/SettingsDictionary.h b/arrus/core/io/SettingsDictionary.h new file mode 100644 index 000000000..5ccda5cc7 --- /dev/null +++ b/arrus/core/io/SettingsDictionary.h @@ -0,0 +1,77 @@ +#ifndef ARRUS_CORE_IO_SETTINGSDICTIONARY_H +#define ARRUS_CORE_IO_SETTINGSDICTIONARY_H + +#include +#include "arrus/core/api/devices/us4r/ProbeAdapterSettings.h" + +namespace arrus::io { + +using ::arrus::devices::ProbeAdapterSettings; +using ::arrus::devices::ProbeSettings; +using ::arrus::devices::ProbeModel; +using ::arrus::devices::ProbeModelId; +using ::arrus::devices::ProbeAdapterModelId; + +class SettingsDictionary { + public: + [[nodiscard]] ProbeAdapterSettings + getAdapterSettings(const ProbeAdapterModelId &adapterModelId) const { + return adaptersMap.at(convertIdToString(adapterModelId)); + } + + void insertAdapterSettings(ProbeAdapterSettings &&adapter) { + std::string key = convertIdToString(adapter.getModelId()); + adaptersMap.emplace(key, + std::forward(adapter)); + } + + [[nodiscard]] ProbeSettings + getProbeSettings(const ProbeModelId &probeModelId, + const ProbeAdapterModelId &adapterModelId) const { + std::string key = + convertIdToString(probeModelId) + + convertIdToString(adapterModelId); + return probesMap.at(key); + } + + void insertProbeSettings(ProbeSettings &&probe, + const ProbeAdapterModelId &adapterId) { + std::string key = + convertIdToString(probe.getModel().getModelId()) + + convertIdToString(adapterId); + probesMap.emplace(key, std::forward(probe)); + } + + [[nodiscard]] ProbeModel getProbeModel(const ProbeModelId &id) const { + return modelsMap.at(convertIdToString(id)); + } + + void insertProbeModel(const ProbeModel &probeModel) { + std::string key = convertIdToString(probeModel.getModelId()); + modelsMap.emplace(key, probeModel); + } + + template + static + std::string convertIdToString(const T &id) { + return id.getManufacturer() + id.getName(); + } + + template + static + std::string convertProtoIdToString(const T &id) { + return id.manufacturer() + id.name(); + } + + private: + // + // manufacturer + name -> adapter + std::unordered_map adaptersMap; + // adapter manufacturer + a. name + probe manufacturer + p. name -> probe s. + std::unordered_map probesMap; + std::unordered_map modelsMap; +}; + +} + +#endif //ARRUS_CORE_IO_SETTINGSDICTIONARY_H diff --git a/arrus/core/io/proto/Dictionary.proto b/arrus/core/io/proto/Dictionary.proto new file mode 100644 index 000000000..3fac67a9f --- /dev/null +++ b/arrus/core/io/proto/Dictionary.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package arrus.proto; + +import "io/proto/devices/probe/ProbeModel.proto"; +import "io/proto/devices/us4r/ProbeAdapterModel.proto"; +import "io/proto/devices/us4r/ProbeToAdapterConnection.proto"; + +message Dictionary { + repeated ProbeModel probe_models = 1; + repeated ProbeAdapterModel probe_adapter_models = 2; + repeated ProbeToAdapterConnection probe_to_adapter_connections = 3; +} diff --git a/arrus/core/io/proto/common/IntervalDouble.proto b/arrus/core/io/proto/common/IntervalDouble.proto new file mode 100644 index 000000000..2cf9ba81f --- /dev/null +++ b/arrus/core/io/proto/common/IntervalDouble.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package arrus.proto; + +// A closed interval [min, max] +message IntervalDouble { + double begin = 1; + double end = 2; +} diff --git a/arrus/core/io/proto/common/IntervalInteger.proto b/arrus/core/io/proto/common/IntervalInteger.proto new file mode 100644 index 000000000..702be5ed3 --- /dev/null +++ b/arrus/core/io/proto/common/IntervalInteger.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package arrus.proto; + +// A closed interval [min, max] +message IntervalInteger { + int32 begin = 1; + int32 end = 2; +} diff --git a/arrus/core/io/proto/common/LinearFunction.proto b/arrus/core/io/proto/common/LinearFunction.proto new file mode 100644 index 000000000..72e196414 --- /dev/null +++ b/arrus/core/io/proto/common/LinearFunction.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package arrus.proto; + +message LinearFunction { + double intercept = 1; + double slope = 2; +} \ No newline at end of file diff --git a/arrus/core/io/proto/default.dict b/arrus/core/io/proto/default.dict new file mode 100644 index 000000000..01c034b9e --- /dev/null +++ b/arrus/core/io/proto/default.dict @@ -0,0 +1,261 @@ +adapter_models: [ + { + id: { + manufacturer: "us4us" + name: "esaote" + } + n_channels: 192 + us4oem_channels_regions: [ + { + us4oem: 0 + channels: [31, 30, 29, 28, 27, 26, 25, 24, + 23, 22, 21, 20, 19, 18, 17, 15, + 16, 14, 13, 12, 11, 10, 9, 8, + 7, 6, 5, 4, 3, 2, 1, 0, + 63, 62, 61, 60, 59, 58, 57, 56, + 55, 54, 53, 52, 51, 50, 49, 47, + 48, 46, 45, 44, 43, 42, 41, 40, + 39, 38, 37, 36, 35, 34, 33, 32, + 95, 94, 93, 92, 91, 90, 89, 88, + 87, 86, 85, 84, 83, 82, 81, 79, + 80, 78, 77, 76, 75, 74, 73, 72, + 71, 70, 69, 68, 67, 66, 65, 64, + 127, 126, 125, 124, 123, 122, 121, 120, + 119, 118, 117, 116, 115, 114, 112, 113, + 111, 110, 109, 108, 107, 106, 105, 104, + 103, 102, 101, 100, 99, 98, 97, 96] + }, + { + us4oem: 1 + channels: [0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63] + } + ] + }, + { + id: { + manufacturer: "us4us" + name: "esaote2" + } + n_channels: 192 + us4oem_channels_regions: [ + { + us4oem: 0 + channels: [26, 27, 25, 23, 28, 22, 20, 21, + 24, 18, 19, 15, 17, 16, 29, 13, + 11, 14, 30, 8, 12, 5, 10, 9, + 31, 7, 3, 6, 0, 2, 4, 1] + }, + { + us4oem: 1 + channels: [ 4, 3, 7, 5, 6, 2, 8, 9, + 1, 11, 0, 10, 13, 12, 15, 14, + 16, 17, 19, 18, 20, 25, 21, 22, + 23, 31, 24, 27, 30, 26, 28, 29] + }, + { + us4oem: 0 + channels: [56, 55, 54, 53, 57, 52, 51, 49, + 50, 48, 47, 46, 44, 45, 58, 42, + 43, 59, 40, 41, 60, 38, 61, 39, + 62, 34, 37, 63, 36, 35, 32, 33] + }, + { + us4oem: 1 + channels: [35, 34, 36, 38, 33, 37, 39, 40, + 32, 41, 42, 43, 44, 45, 46, 47, + 49, 48, 50, 52, 51, 55, 53, 54, + 58, 56, 59, 57, 62, 61, 60, 63] + }, + { + us4oem: 0 + channels: [92, 93, 89, 91, 88, 90, 87, 85, + 86, 84, 83, 82, 81, 80, 79, 77, + 78, 76, 95, 75, 74, 94, 73, 72, + 70, 64, 71, 68, 65, 69, 67, 66] + }, + { + us4oem: 1 + channels: [65, 67, 66, 69, 64, 68, 71, 70, + 72, 74, 73, 75, 76, 77, 78, 79, + 80, 82, 81, 83, 85, 84, 87, 86, + 88, 92, 89, 94, 90, 91, 95, 93] + } + ] + }, + { + id: { + manufacturer: "us4us" + name: "ultrasonix" + } + n_channels: 128 + us4oem_channels_regions: [ + { + # adapter channel 0: us4oem0, 0, + # adapter channel 1: us4oem0, 1, + # ... + # adapter channel 31: us4oem0, 31 + us4oem: 0 + channels: [0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31] + }, + { + # adapter channel 32: us4oem1, 63, + # adapter channel 33: us4oem1, 62, + # ... + # adapter channel 63: us4oem1, 32, + us4oem: 1 + channels: [63, 62, 61, 60, 59, 58, 57, 56, + 55, 54, 53, 52, 51, 50, 49, 48, + 47, 46, 45, 44, 43, 42, 41, 40, + 39, 38, 37, 36, 35, 34, 33, 32] + }, + # adapter channel 64, us4oem0, 64, + # adapter channel 65, us4oem0, 65, + # ... + { + us4oem: 0 + channels: [64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95] + }, + { + us4oem: 1 + channels: [127, 126, 125, 124, 123, 122, 121, 120, + 119, 118, 117, 116, 115, 114, 113, 112, + 111, 110, 109, 108, 107, 106, 105, 104, + 103, 102, 101, 100, 99, 98, 97, 96] + } + ] + } +] +probe_models: [ + { + id: { + manufacturer: "esaote" + name: "sl1543" + } + n_channels: 192, + pitch: 0.245e-3 + }, + { + id: { + name: "al2442" + manufacturer: "esaote" + } + n_channels: 192, + pitch: 0.21e-3 + }, + { + id: { + name: "sp2430" + manufacturer: "esaote" + } + n_channels: 96, + pitch: 0.22e-3 + }, + { + id: { + name: "l14-5/38" + manufacturer: "ultrasonix" + } + n_channels: 128 + pitch: 0.3048e-3 + } +] + +probe_to_adapter_connections = [ + { + probe_model_id: { + manufacturer: "esaote" + name: "sl1543" + } + adapter_model_id: [ + { + manufacturer: "us4us" + name: "esaote" + }, + { + manufacturer: "us4us" + name: "esaote2" + } + ] + adapter_channels_range: { + start: 0 + end: 192 + } + }, + { + probe_model_id: { + manufacturer: "esaote" + name: "al2442" + } + adapter_model_id: [ + { + manufacturer: "us4us" + name: "esaote" + }, + { + manufacturer: "us4us" + name: "esaote2" + } + ] + adapter_channels_range: { + start: 0 + end: 192 + } + }, + { + probe_model_id: { + manufacturer: "esaote" + name: "sp2430" + } + adapter_model_id: [ + { + manufacturer: "us4us" + name: "esaote" + }, + { + manufacturer: "us4us" + name: "esaote2" + } + ] + adapter_channels_range: [ + { + start: 0 + end: 48 + }, + { + start: 144, + end: 192 + } + ] + }, + { + probe_model_id: { + manufacturer: "ultrasonix" + name: "l14-5/38" + } + adapter_model_id: [ + { + manufacturer: "us4us" + name: "ultrasonix" + }, + ] + adapter_channels_range: { + start: 0 + end: 128 + } + }, + + +] diff --git a/arrus/core/io/proto/devices/probe/ProbeModel.proto b/arrus/core/io/proto/devices/probe/ProbeModel.proto new file mode 100644 index 000000000..9627dbf37 --- /dev/null +++ b/arrus/core/io/proto/devices/probe/ProbeModel.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package arrus.proto; + +import "io/proto/common/IntervalDouble.proto"; +import "io/proto/common/IntervalInteger.proto"; + + +message ProbeModel { + message Id { + string manufacturer = 1; + string name = 2; + } + Id id = 1; + repeated uint32 n_elements = 2; + repeated double pitch = 3; + IntervalDouble tx_frequency_range = 4; + // Acceptable voltage range +/-, [0.5*Vpp] + IntervalInteger voltage_range = 5; + // Curvature radius; 0 means no curvature. + double curvature_radius = 6; +} \ No newline at end of file diff --git a/arrus/core/io/proto/devices/us4r/HVSettings.proto b/arrus/core/io/proto/devices/us4r/HVSettings.proto new file mode 100644 index 000000000..b20e13452 --- /dev/null +++ b/arrus/core/io/proto/devices/us4r/HVSettings.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package arrus.proto; + +message HVSettings { + message Id { + string manufacturer = 1; + string name = 2; + } + Id model_id = 1; +} \ No newline at end of file diff --git a/arrus/core/io/proto/devices/us4r/ProbeAdapterModel.proto b/arrus/core/io/proto/devices/us4r/ProbeAdapterModel.proto new file mode 100644 index 000000000..23455087b --- /dev/null +++ b/arrus/core/io/proto/devices/us4r/ProbeAdapterModel.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package arrus.proto; + +import "io/proto/common/IntervalInteger.proto"; + +message ProbeAdapterModel { + + message Id { + string manufacturer = 1; + string name = 2; + } + + message ChannelMappingRegion { + uint32 us4oem = 1; + repeated uint32 channels = 2; + // Closed interval of channel numbers [first, last] + IntervalInteger region = 3; + } + + message ChannelMapping { + repeated uint32 us4oems = 1; + repeated uint32 channels = 2; + } + + Id id = 1; + uint32 n_channels = 2; + ChannelMapping channel_mapping = 3; + repeated ChannelMappingRegion channel_mapping_regions = 4; +} \ No newline at end of file diff --git a/arrus/core/io/proto/devices/us4r/ProbeToAdapterConnection.proto b/arrus/core/io/proto/devices/us4r/ProbeToAdapterConnection.proto new file mode 100644 index 000000000..f9d94f3b1 --- /dev/null +++ b/arrus/core/io/proto/devices/us4r/ProbeToAdapterConnection.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package arrus.proto; + +import "io/proto/devices/probe/ProbeModel.proto"; +import "io/proto/devices/us4r/ProbeAdapterModel.proto"; +import "io/proto/common/IntervalInteger.proto"; + +message ProbeToAdapterConnection { + ProbeModel.Id probe_model_id = 1; + repeated ProbeAdapterModel.Id probe_adapter_model_id = 2; + + // Channel mapping - one of: + repeated uint32 channel_mapping = 3; + repeated IntervalInteger channel_mapping_ranges = 4; +} \ No newline at end of file diff --git a/arrus/core/io/proto/devices/us4r/RxSettings.proto b/arrus/core/io/proto/devices/us4r/RxSettings.proto new file mode 100644 index 000000000..47210fb88 --- /dev/null +++ b/arrus/core/io/proto/devices/us4r/RxSettings.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package arrus.proto; + +import "io/proto/common/LinearFunction.proto"; + +message RxSettings { + // optional + oneof dtgcAttenuation_ { + uint32 dtgc_attenuation = 2; + } + uint32 pga_gain = 3; + uint32 lna_gain = 4; + LinearFunction tgc_curve_linear = 5; + repeated double tgc_samples = 6; + + uint32 lpf_cutoff = 7; + + oneof activeTermination_ { + uint32 active_termination = 9; + } +} \ No newline at end of file diff --git a/arrus/core/io/proto/devices/us4r/Us4OEMSettings.proto b/arrus/core/io/proto/devices/us4r/Us4OEMSettings.proto new file mode 100644 index 000000000..8f2a36ef7 --- /dev/null +++ b/arrus/core/io/proto/devices/us4r/Us4OEMSettings.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package arrus.proto; + +import "io/proto/devices/us4r/RxSettings.proto"; + +message Us4OEMSettings { + repeated uint32 channel_mapping = 1; + repeated bool active_channel_groups = 2; + RxSettings rx_settings = 3; +} \ No newline at end of file diff --git a/arrus/core/io/proto/devices/us4r/Us4RSettings.proto b/arrus/core/io/proto/devices/us4r/Us4RSettings.proto new file mode 100644 index 000000000..27e322c32 --- /dev/null +++ b/arrus/core/io/proto/devices/us4r/Us4RSettings.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package arrus.proto; + +import "io/proto/devices/probe/ProbeModel.proto"; +import "io/proto/devices/us4r/ProbeAdapterModel.proto"; +import "io/proto/devices/us4r/ProbeToAdapterConnection.proto"; +import "io/proto/devices/us4r/RxSettings.proto"; +import "io/proto/devices/us4r/Us4OEMSettings.proto"; +import "io/proto/devices/us4r/HVSettings.proto"; + +message Us4RSettings { + + // Only one of the following is available: (probe, adapter, rxsettings) + // or a list of us4oem settings. + + oneof one_of_probe_representation { + ProbeModel.Id probe_id = 1; + ProbeModel probe = 2; + } + + oneof one_of_adapter_representation { + ProbeAdapterModel.Id adapter_id = 3; + ProbeAdapterModel adapter = 4; + } + + + message ChannelsMask { + // The channel number of the probe that should be turned of. + // The number refers to + repeated uint32 channels = 1; + } + + ChannelsMask channels_mask = 5; + ProbeToAdapterConnection probe_to_adapter_connection = 6; + RxSettings rx_settings = 7; + repeated Us4OEMSettings us4oems = 8; + HVSettings hv = 9; + + repeated ChannelsMask us4oem_channels_mask = 10; +} \ No newline at end of file diff --git a/arrus/core/io/proto/session/SessionSettings.proto b/arrus/core/io/proto/session/SessionSettings.proto new file mode 100644 index 000000000..5dac326c0 --- /dev/null +++ b/arrus/core/io/proto/session/SessionSettings.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package arrus.proto; + +import "io/proto/devices/us4r/Us4RSettings.proto"; + +message SessionSettings { + Us4RSettings us4r = 1; + string dictionary_file = 2; +} + diff --git a/arrus/core/io/settings.cpp b/arrus/core/io/settings.cpp new file mode 100644 index 000000000..9f5a21a6b --- /dev/null +++ b/arrus/core/io/settings.cpp @@ -0,0 +1,457 @@ +#include "arrus/core/api/io/settings.h" +#include +#include +#include +#include +#include + +#include "arrus/core/common/logging.h" +#include "arrus/core/session/SessionSettings.h" +#include "arrus/common/utils.h" + +#ifdef _MSC_VER + +#include +#define ARRUS_OPEN_FILE _open + +#elif ARRUS_LINUX + +#include +#define ARRUS_OPEN_FILE open + +#endif + +#include "arrus/common/asserts.h" +#include "arrus/common/format.h" +#include "arrus/common/compiler.h" +#include "arrus/core/common/validation.h" +#include "arrus/core/io/validators/SessionSettingsProtoValidator.h" +#include "arrus/core/io/validators/DictionaryProtoValidator.h" +#include "arrus/core/io/SettingsDictionary.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4127) + +#include +#include +// TODO(146) should point to arrus/core/io/... +#include "io/proto/session/SessionSettings.pb.h" +#include "io/proto/Dictionary.pb.h" + +COMPILER_POP_DIAGNOSTIC_STATE + + +namespace arrus::io { + +namespace ap = arrus::proto; + +using namespace ::arrus::devices; +using namespace ::arrus::session; + +template +std::unique_ptr readProtoTxt(const std::string &filepath) { + int fd = ARRUS_OPEN_FILE(filepath.c_str(), O_RDONLY); + ARRUS_REQUIRES_TRUE( + fd != 0, arrus::format("Could not open file {}", filepath)); + google::protobuf::io::FileInputStream input(fd); + input.SetCloseOnDelete(true); + auto result = std::make_unique(); + google::protobuf::TextFormat::Parse(&input, result.get()); + return result; +} + +ProbeAdapterSettings +readAdapterSettings(const ap::ProbeAdapterModel &proto) { + ProbeAdapterModelId id(proto.id().manufacturer(), proto.id().name()); + // Safe, should be verified by probe adapter proto validator. + auto nChannels = static_cast(proto.n_channels()); + + ProbeAdapterSettings::ChannelMapping channelMapping; + using ChannelAddress = ProbeAdapterSettings::ChannelAddress; + + if(proto.has_channel_mapping()) { + const auto &mapping = proto.channel_mapping(); + const auto &us4oems = mapping.us4oems(); + const auto &inChannels = mapping.channels(); + + auto modules = ::arrus::castTo( + std::begin(us4oems), std::end(us4oems)); + auto channels = ::arrus::castTo( + std::begin(inChannels), std::end(inChannels)); + + ARRUS_REQUIRES_EQUAL(modules.size(), channels.size(), + IllegalArgumentException( + "Us4oems and channels lists should have " + "the same size")); + channelMapping = std::vector{modules.size()}; + for(int i = 0; i < modules.size(); ++i) { + channelMapping[i] = {modules[i], channels[i]}; + } + } else if(!proto.channel_mapping_regions().empty()) { + std::vector modules; + std::vector channels; + for(auto const ®ion : proto.channel_mapping_regions()) { + auto module = static_cast(region.us4oem()); + + if(region.has_region()) { + + ChannelIdx begin = ARRUS_SAFE_CAST(region.region().begin(), ChannelIdx); + ChannelIdx end = ARRUS_SAFE_CAST(region.region().end(), ChannelIdx); + + for(ChannelIdx ch = begin; ch <= end; ++ch) { + channelMapping.emplace_back(module, ch); + } + } + else { + // Just channels. + for(auto channel : region.channels()) { + channelMapping.emplace_back( + module, static_cast(channel)); + } + } + + } + } + return ProbeAdapterSettings(id, nChannels, channelMapping); +} + + +ProbeModel readProbeModel(const proto::ProbeModel &proto) { + ProbeModelId id{proto.id().manufacturer(), proto.id().name()}; + using ElementIdxType = ProbeModel::ElementIdxType; + + auto nElementsVec = ::arrus::castTo( + std::begin(proto.n_elements()), std::end(proto.n_elements())); + // TODO move + Tuple nElements{nElementsVec}; + + std::vector pitchVec(proto.pitch().size()); + std::copy(std::begin(proto.pitch()), std::end(proto.pitch()), std::begin(pitchVec)); + Tuple pitch{pitchVec}; + + double curvatureRadius = proto.curvature_radius(); + + ::arrus::Interval txFreqRange{static_cast(proto.tx_frequency_range().begin()), + static_cast(proto.tx_frequency_range().end())}; + ::arrus::Interval voltageRange{static_cast(proto.voltage_range().begin()), + static_cast(proto.voltage_range().end())}; + return ProbeModel(id, nElements, pitch, txFreqRange, voltageRange, curvatureRadius); +} + + +std::vector readProbeConnectionChannelMapping( + const ap::ProbeToAdapterConnection &connection) { + + const auto &channelMapping = connection.channel_mapping(); + const auto &ranges = connection.channel_mapping_ranges(); + + if(!channelMapping.empty()) { + return castTo(std::begin(channelMapping), + std::end(channelMapping)); + } else if(!ranges.empty()) { + std::vector result; + for(auto const &range: ranges) { + for(int i = range.begin(); i <= range.end(); ++i) { + result.push_back(static_cast(i)); + } + } + return result; + } else { + throw ArrusException("NYI"); + } +} + +SettingsDictionary +readDictionary(const ap::Dictionary *proto) { + SettingsDictionary result; + + if(proto == nullptr) { + return result; + } + + for(auto const &adapter : proto->probe_adapter_models()) { + result.insertAdapterSettings(readAdapterSettings(adapter)); + } + + // index connections + std::unordered_multimap connections; + + for(const ap::ProbeToAdapterConnection &conn : proto->probe_to_adapter_connections()) { + std::string key = + SettingsDictionary::convertProtoIdToString(conn.probe_model_id()); + const ap::ProbeToAdapterConnection *ptr = &conn; + connections.emplace(key, ptr); + } + + // Read probes. + for(auto const &probe : proto->probe_models()) { + const ProbeModel probeModel = readProbeModel(probe); + result.insertProbeModel(probeModel); + + std::string key = + SettingsDictionary::convertProtoIdToString(probe.id()); + auto range = connections.equal_range(key); + for(auto it = range.first; it != range.second; ++it) { + auto conn = it->second; + std::vector channelMapping = + readProbeConnectionChannelMapping(*conn); + + for(auto const &adapterProtoId : conn->probe_adapter_model_id()) { + const ProbeAdapterModelId adapterId( + adapterProtoId.manufacturer(), + adapterProtoId.name()); + result.insertProbeSettings( + ProbeSettings(probeModel, channelMapping), adapterId); + } + } + } + return result; +} + +RxSettings readRxSettings(const proto::RxSettings &proto) { + std::optional dtgcAtt; + if(proto.dtgcAttenuation__case() == proto::RxSettings::kDtgcAttenuation) { + // dtgc attenuation is set + dtgcAtt = static_cast(proto.dtgc_attenuation()); + } + auto pgaGain = static_cast(proto.pga_gain()); + auto lnaGain = static_cast(proto.lna_gain()); + + RxSettings::TGCCurve tgcSamples = castTo( + std::begin(proto.tgc_samples()), std::end(proto.tgc_samples())); + + uint32 lpfCutoff = proto.lpf_cutoff(); + + std::optional activeTermination; + if(proto.activeTermination__case() == + proto::RxSettings::kActiveTermination) { + activeTermination = static_cast(proto.active_termination()); + } + + return RxSettings(dtgcAtt, pgaGain, lnaGain, tgcSamples, lpfCutoff, + activeTermination); +} + +ProbeAdapterSettings readOrGetAdapterSettings(const proto::Us4RSettings &us4r, + const SettingsDictionary &dictionary) { + if(us4r.has_adapter()) { + return readAdapterSettings(us4r.adapter()); + } else if(us4r.has_adapter_id()) { + ProbeAdapterModelId id{us4r.adapter_id().manufacturer(), + us4r.adapter_id().name()}; + try { + return dictionary.getAdapterSettings(id); + } catch(const std::out_of_range &) { + throw IllegalArgumentException( + arrus::format("Adapter with id {} not found.", id.toString())); + } + } else { + throw ArrusException("NYI"); + } +} + +ProbeSettings readOrGetProbeSettings(const proto::Us4RSettings &us4r, + const ProbeAdapterModelId &adapterId, + const SettingsDictionary &dictionary) { + if(us4r.has_probe()) { + ProbeModel model = readProbeModel(us4r.probe()); + std::vector channelMapping = + readProbeConnectionChannelMapping( + us4r.probe_to_adapter_connection()); + return ProbeSettings(model, channelMapping); + } else if(us4r.has_probe_id()) { + ProbeModelId id{us4r.probe_id().manufacturer(), us4r.probe_id().name()}; + if(us4r.has_probe_to_adapter_connection()) { + std::vector channelMapping = + readProbeConnectionChannelMapping( + us4r.probe_to_adapter_connection()); + try { + ProbeModel model = dictionary.getProbeModel(id); + return ProbeSettings(model, channelMapping); + } catch(std::out_of_range &) { + throw IllegalArgumentException( + arrus::format("Probe with id {} not found.", + id.toString())); + } + } else { + try { + return dictionary.getProbeSettings(id, adapterId); + } catch(std::out_of_range &) { + throw IllegalArgumentException( + arrus::format( + "Probe settings for probe with id {}" + " adapter with id {} not found.", id.toString())); + } + } + } else { + throw ArrusException("NYI"); + } +} + +template +std::vector readChannelsMask(const proto::Us4RSettings_ChannelsMask &mask) { + auto &channels = mask.channels(); + + // validate + for(auto channel : channels) { + ARRUS_REQUIRES_DATA_TYPE(channel, T, arrus::format( + "Channel mask should contain only values from uint16 range " + "(found: '{}')", channel)); + } + std::vector result; + + for(auto channel : channels) { + result.push_back(static_cast(channel)); + } + return result; +} + + +Us4RSettings readUs4RSettings(const proto::Us4RSettings &us4r, + const SettingsDictionary &dictionary) { + std::optional hvSettings; + if(us4r.has_hv()) { + auto &manufacturer = us4r.hv().model_id().manufacturer(); + auto &name = us4r.hv().model_id().name(); + ARRUS_REQUIRES_NON_EMPTY_IAE(manufacturer); + ARRUS_REQUIRES_NON_EMPTY_IAE(name); + hvSettings = HVSettings(HVModelId(manufacturer, name)); + } + if(!us4r.us4oems().empty()) { + // Us4OEMs are provided. + std::vector us4oemSettings; + + std::vector> us4oemChannelsMask; + us4oemChannelsMask.resize(us4r.us4oems().size()); + + if(!us4r.us4oem_channels_mask().empty() + && us4r.us4oems().size() != us4r.us4oem_channels_mask().size()) { + throw ::arrus::IllegalArgumentException( + "The number of us4oem channels " + "masks should be the same as the number of us4oems."); + } + + int i = 0; + for(auto &mask: us4r.us4oem_channels_mask()) { + auto channelsMask = readChannelsMask(mask); + + us4oemChannelsMask[i] = std::unordered_set( + std::begin(channelsMask), std::end(channelsMask)); + ++i; + } + + for(auto const &us4oem : us4r.us4oems()) { + auto rxSettings = readRxSettings(us4oem.rx_settings()); + auto channelMapping = castTo( + std::begin(us4oem.channel_mapping()), + std::end(us4oem.channel_mapping())); + auto activeChannelGroups = castTo( + std::begin(us4oem.active_channel_groups()), + std::end(us4oem.active_channel_groups())); + us4oemSettings.emplace_back( + channelMapping, activeChannelGroups, rxSettings, + us4oemChannelsMask[i]); + } + return Us4RSettings(us4oemSettings, hvSettings); + } else { + ProbeAdapterSettings adapterSettings = + readOrGetAdapterSettings(us4r, dictionary); + ProbeSettings probeSettings = readOrGetProbeSettings( + us4r, adapterSettings.getModelId(), dictionary); + RxSettings rxSettings = readRxSettings(us4r.rx_settings()); + + // ensure that user provided channels mask + // TODO(pjarosik) consider removing this check in the future + if(!us4r.has_channels_mask()) { + throw IllegalArgumentException( + "Us4r settings field 'channels_mask' is required. " + "Set empty array of channels if you want to turn off channel masking."); + } + + if(us4r.us4oem_channels_mask().empty()) { + throw IllegalArgumentException( + "Us4r settings field 'us4oem_channels_mask is required. " + "Set empty array of channels for each of the module explicitly if you want " + "to turn of channel masking."); + } + + std::vector channelsMask = + readChannelsMask(us4r.channels_mask()); + std::vector> us4oemChannelsMask; + for(auto &mask: us4r.us4oem_channels_mask()) { + us4oemChannelsMask.push_back(readChannelsMask(mask)); + } + + return Us4RSettings(adapterSettings, probeSettings, rxSettings, + hvSettings, channelsMask, us4oemChannelsMask); + } +} + + + +SessionSettings readSessionSettings(const std::string &filepath) { + auto logger = ::arrus::getDefaultLogger(); + // Read and validate session. + std::filesystem::path sessionSettingsPath{filepath}; + if(!std::filesystem::is_regular_file(sessionSettingsPath)) { + throw IllegalArgumentException( + ::arrus::format("File not found {}.", filepath)); + } + std::unique_ptr s = + readProtoTxt(filepath); + + //Validate. + SessionSettingsProtoValidator validator( + "session settings in " + filepath); + validator.validate(s); + validator.throwOnErrors(); + + // Read and validate Dictionary. + std::unique_ptr d; + if(!s->dictionary_file().empty()) { + std::string dictionaryPathStr; + // 1. Try to use the parent directory of session settings. + auto dictP = sessionSettingsPath.parent_path() / s->dictionary_file(); + if(std::filesystem::is_regular_file(dictP)) { + dictionaryPathStr = dictP.u8string(); + } else { + // 2. Try to use ARRUS_PATH, if available. + const char *arrusP = std::getenv(ARRUS_PATH_KEY); + if(arrusP != nullptr) { + std::filesystem::path arrusDicP{arrusP}; + arrusDicP = arrusDicP / s->dictionary_file(); + if(std::filesystem::is_regular_file(arrusDicP)) { + dictionaryPathStr = arrusDicP.u8string(); + } else { + throw IllegalArgumentException( + ::arrus::format("Invalid path to dictionary: {}", + s->dictionary_file())); + } + } else { + throw IllegalArgumentException( + ::arrus::format("Invalid path to dictionary: {}", + s->dictionary_file())); + } + } + d = readProtoTxt(dictionaryPathStr); + DictionaryProtoValidator dictionaryValidator("dictionary"); + dictionaryValidator.validate(d); + dictionaryValidator.throwOnErrors(); + } + + SettingsDictionary dictionary = readDictionary(d.get()); + + Us4RSettings us4rSettings = readUs4RSettings(s->us4r(), dictionary); + // TODO std move + + SessionSettings sessionSettings(us4rSettings); + + logger->log(LogSeverity::DEBUG, + arrus::format("Read settings from '{}': {}", + filepath, ::arrus::toString(sessionSettings))); + + return sessionSettings; +} + + +} \ No newline at end of file diff --git a/arrus/core/io/settingsTest.cpp b/arrus/core/io/settingsTest.cpp new file mode 100644 index 000000000..a7e26989f --- /dev/null +++ b/arrus/core/io/settingsTest.cpp @@ -0,0 +1,220 @@ +#include +#include +#include + +#include "arrus/common/logging/impl/Logging.h" +#include "arrus/core/common/logging.h" +#include "arrus/core/api/io/settings.h" +#include "arrus/core/common/collections.h" + +using namespace ::arrus; +using namespace ::arrus::session; +using namespace ::arrus::devices; +using namespace arrus::ops::us4r; + +// ARRUS_TEST_DATA_PATH is defined in cmake. + +std::vector generateReversed(ChannelIdx a, ChannelIdx b) { + std::vector result; + for(int i = b - 1; i >= a; --i) { + result.push_back(i); + } + return result; +} + +TEST(ReadingProtoTxtFile, readsUs4RPrototxtSettingsCorrectly) { + auto filepath = std::filesystem::path(ARRUS_TEST_DATA_PATH) / + std::filesystem::path("us4r.prototxt"); + ::arrus::session::SessionSettings settings = arrus::io::readSessionSettings( + filepath.string()); + auto const &us4rSettings = settings.getUs4RSettings(); + EXPECT_TRUE(us4rSettings.getUs4OEMSettings().empty()); + + EXPECT_EQ(us4rSettings.getChannelsMask(), std::vector({5, 10, 15})); + EXPECT_EQ(us4rSettings.getUs4OEMChannelsMask(), std::vector>({{5, 10}, {15}})); + + // Probe settings + // Probe model + auto const &probeSettings = us4rSettings.getProbeSettings(); + auto const &probeModel = probeSettings->getModel(); + EXPECT_EQ(probeModel.getModelId().getManufacturer(), "esaote"); + EXPECT_EQ(probeModel.getModelId().getName(), "sl1543"); + // 1-d, linear array. + EXPECT_EQ(probeModel.getNumberOfElements().size(), 1); + EXPECT_EQ(probeModel.getNumberOfElements()[0], 192); + EXPECT_EQ(probeModel.getPitch().size(), 1); + EXPECT_DOUBLE_EQ(probeModel.getPitch()[0], 0.245e-3); + EXPECT_DOUBLE_EQ(probeModel.getTxFrequencyRange().start(), 1e6); + EXPECT_DOUBLE_EQ(probeModel.getTxFrequencyRange().end(), 10e6); + + // Probe channel mapping + EXPECT_EQ(probeSettings->getChannelMapping(), getRange(0, 192)); + + // Probe adapter settigns + auto const &adapterSettings = us4rSettings.getProbeAdapterSettings(); + EXPECT_EQ(adapterSettings->getModelId().getManufacturer(), "us4us"); + EXPECT_EQ(adapterSettings->getModelId().getName(), "esaote2"); + EXPECT_EQ(adapterSettings->getNumberOfChannels(), 192); + EXPECT_EQ(adapterSettings->getChannelMapping(), + std::vector( + { + { 0, 26 }, { 0, 27 }, { 0, 25 }, { 0, 23 }, { 0, 28 }, { 0, 22 }, { 0, 20 }, { 0, 21 }, + { 0, 24 }, { 0, 18 }, { 0, 19 }, { 0, 15 }, { 0, 17 }, { 0, 16 }, { 0, 29 }, { 0, 13 }, + { 0, 11 }, { 0, 14 }, { 0, 30 }, { 0, 8 }, { 0, 12 }, { 0, 5 }, { 0, 10 }, { 0, 9 }, + { 0, 31 }, { 0, 7 }, { 0, 3 }, { 0, 6 }, { 0, 0 }, { 0, 2 }, { 0, 4 }, { 0, 1 }, + { 1, 4 }, { 1, 3 }, { 1, 7 }, { 1, 5 }, { 1, 6 }, { 1, 2 }, { 1, 8 }, { 1, 9 }, + { 1, 1 }, { 1, 11 }, { 1, 0 }, { 1, 10 }, { 1, 13 }, { 1, 12 }, { 1, 15 }, { 1, 14 }, + { 1, 16 }, { 1, 17 }, { 1, 19 }, { 1, 18 }, { 1, 20 }, { 1, 25 }, { 1, 21 }, { 1, 22 }, + { 1, 23 }, { 1, 31 }, { 1, 24 }, { 1, 27 }, { 1, 30 }, { 1, 26 }, { 1, 28 }, { 1, 29 }, + { 0, 56 }, { 0, 55 }, { 0, 54 }, { 0, 53 }, { 0, 57 }, { 0, 52 }, { 0, 51 }, { 0, 49 }, + { 0, 50 }, { 0, 48 }, { 0, 47 }, { 0, 46 }, { 0, 44 }, { 0, 45 }, { 0, 58 }, { 0, 42 }, + { 0, 43 }, { 0, 59 }, { 0, 40 }, { 0, 41 }, { 0, 60 }, { 0, 38 }, { 0, 61 }, { 0, 39 }, + { 0, 62 }, { 0, 34 }, { 0, 37 }, { 0, 63 }, { 0, 36 }, { 0, 35 }, { 0, 32 }, { 0, 33 }, + { 1, 35 }, { 1, 34 }, { 1, 36 }, { 1, 38 }, { 1, 33 }, { 1, 37 }, { 1, 39 }, { 1, 40 }, + { 1, 32 }, { 1, 41 }, { 1, 42 }, { 1, 43 }, { 1, 44 }, { 1, 45 }, { 1, 46 }, { 1, 47 }, + { 1, 49 }, { 1, 48 }, { 1, 50 }, { 1, 52 }, { 1, 51 }, { 1, 55 }, { 1, 53 }, { 1, 54 }, + { 1, 58 }, { 1, 56 }, { 1, 59 }, { 1, 57 }, { 1, 62 }, { 1, 61 }, { 1, 60 }, { 1, 63 }, + { 0, 92 }, { 0, 93 }, { 0, 89 }, { 0, 91 }, { 0, 88 }, { 0, 90 }, { 0, 87 }, { 0, 85 }, + { 0, 86 }, { 0, 84 }, { 0, 83 }, { 0, 82 }, { 0, 81 }, { 0, 80 }, { 0, 79 }, { 0, 77 }, + { 0, 78 }, { 0, 76 }, { 0, 95 }, { 0, 75 }, { 0, 74 }, { 0, 94 }, { 0, 73 }, { 0, 72 }, + { 0, 70 }, { 0, 64 }, { 0, 71 }, { 0, 68 }, { 0, 65 }, { 0, 69 }, { 0, 67 }, { 0, 66 }, + { 1, 65 }, { 1, 67 }, { 1, 66 }, { 1, 69 }, { 1, 64 }, { 1, 68 }, { 1, 71 }, { 1, 70 }, + { 1, 72 }, { 1, 74 }, { 1, 73 }, { 1, 75 }, { 1, 76 }, { 1, 77 }, { 1, 78 }, { 1, 79 }, + { 1, 80 }, { 1, 82 }, { 1, 81 }, { 1, 83 }, { 1, 85 }, { 1, 84 }, { 1, 87 }, { 1, 86 }, + { 1, 88 }, { 1, 92 }, { 1, 89 }, { 1, 94 }, { 1, 90 }, { 1, 91 }, { 1, 95 }, { 1, 93 } + } + )); + // Rx settings + auto const &rxSettings = us4rSettings.getRxSettings(); + EXPECT_FALSE(rxSettings->getDtgcAttenuation().has_value()); + EXPECT_EQ(rxSettings->getPgaGain(), 30); + EXPECT_EQ(rxSettings->getLnaGain(), 24); + EXPECT_EQ(rxSettings->getTgcSamples(), + std::vector({14, 15, 16})); + EXPECT_EQ(rxSettings->getLpfCutoff(), 10000000); + EXPECT_EQ(rxSettings->getActiveTermination(), 200); +} + +TEST(ReadingProtoTxtFile, readsCustomUs4RPrototxtSettingsCorrectly) { + auto filepath = std::filesystem::path(ARRUS_TEST_DATA_PATH) / + std::filesystem::path("custom_us4r.prototxt"); + SessionSettings settings = arrus::io::readSessionSettings( + filepath.string()); + auto const &us4rSettings = settings.getUs4RSettings(); + EXPECT_TRUE(us4rSettings.getUs4OEMSettings().empty()); + + EXPECT_EQ(us4rSettings.getChannelsMask(), std::vector({0, 15, 30})); + EXPECT_EQ(us4rSettings.getUs4OEMChannelsMask(), std::vector>({{0, 15, 30},{}})); + + // Probe settings + // Probe model + auto const &probeSettings = us4rSettings.getProbeSettings(); + auto const &probeModel = probeSettings->getModel(); + EXPECT_EQ(probeModel.getModelId().getManufacturer(), "acme"); + EXPECT_EQ(probeModel.getModelId().getName(), "my_custom_probe"); + // 1-d, linear array. + EXPECT_EQ(probeModel.getNumberOfElements().size(), 1); + EXPECT_EQ(probeModel.getNumberOfElements()[0], 32); + EXPECT_EQ(probeModel.getPitch().size(), 1); + EXPECT_DOUBLE_EQ(probeModel.getPitch()[0], 0.21e-3); + EXPECT_DOUBLE_EQ(probeModel.getTxFrequencyRange().start(), 1e6); + EXPECT_DOUBLE_EQ(probeModel.getTxFrequencyRange().end(), 40e6); + + // Probe channel mapping + EXPECT_EQ(probeSettings->getChannelMapping(), + concat(getRange(0, 16), + getRange(48, 64))); + + // Probe adapter settigns + auto const &adapterSettings = us4rSettings.getProbeAdapterSettings(); + EXPECT_EQ(adapterSettings->getModelId().getManufacturer(), "acme"); + EXPECT_EQ(adapterSettings->getModelId().getName(), "my_custom_adapter"); + EXPECT_EQ(adapterSettings->getNumberOfChannels(), 64); + EXPECT_EQ(adapterSettings->getChannelMapping(), + std::vector( + { + { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 }, { 0, 2 }, { 1, 2 }, { 0, 3 }, { 1, 3 }, + { 0, 4 }, { 1, 4 }, { 0, 5 }, { 1, 5 }, { 0, 6 }, { 1, 6 }, { 0, 7 }, { 1, 7 }, + { 0, 8 }, { 1, 8 }, { 0, 9 }, { 1, 9 }, { 0, 10 }, { 1, 10 }, { 0, 11 }, { 1, 11 }, + { 0, 12 }, { 1, 12 }, { 0, 13 }, { 1, 13 }, { 0, 14 }, { 1, 14 }, { 0, 15 }, { 1, 15 }, + { 0, 16 }, { 1, 16 }, { 0, 17 }, { 1, 17 }, { 0, 18 }, { 1, 18 }, { 0, 19 }, { 1, 19 }, + { 0, 20 }, { 1, 20 }, { 0, 21 }, { 1, 21 }, { 0, 22 }, { 1, 22 }, { 0, 23 }, { 1, 23 }, + { 0, 24 }, { 1, 24 }, { 0, 25 }, { 1, 25 }, { 0, 26 }, { 1, 26 }, { 0, 27 }, { 1, 27 }, + { 0, 28 }, { 1, 28 }, { 0, 29 }, { 1, 29 }, { 0, 30 }, { 1, 30 }, { 0, 31 }, { 1, 31 } + } + )); + // Rx settings + auto const &rxSettings = us4rSettings.getRxSettings(); + EXPECT_EQ(rxSettings->getDtgcAttenuation(), 0); + EXPECT_EQ(rxSettings->getPgaGain(), 24); + EXPECT_EQ(rxSettings->getLnaGain(), 12); + EXPECT_EQ(rxSettings->getTgcSamples(), + std::vector({20, 21, 22})); + EXPECT_EQ(rxSettings->getLpfCutoff(), 1000000); + EXPECT_FALSE(rxSettings->getActiveTermination().has_value()); +} + +TEST(ReadingProtoTxtFile, readUs4OEMsPrototxtSettingsCorrectly) { + auto filepath = std::filesystem::path(ARRUS_TEST_DATA_PATH) / + std::filesystem::path("us4oems.prototxt"); + SessionSettings settings = arrus::io::readSessionSettings( + filepath.string()); + auto const &us4rSettings = settings.getUs4RSettings(); + EXPECT_FALSE(us4rSettings.getProbeAdapterSettings().has_value()); + EXPECT_FALSE(us4rSettings.getProbeSettings().has_value()); + EXPECT_FALSE(us4rSettings.getRxSettings().has_value()); + + // us4oem:0 + auto const &us4oem0 = us4rSettings.getUs4OEMSettings()[0]; + EXPECT_EQ(us4oem0.getChannelMapping(), + ::arrus::getRange(0, 128)); + EXPECT_EQ(us4oem0.getActiveChannelGroups(), std::vector( + { + true, true, true, true, + true, true, true, true, + true, true, true, true, + false, false, false, false + })); + // Rx settings + auto const &rxSettings0 = us4oem0.getRxSettings(); + EXPECT_EQ(rxSettings0.getDtgcAttenuation(), 24); + EXPECT_EQ(rxSettings0.getLnaGain(), 12); + EXPECT_EQ(rxSettings0.getPgaGain(), 24); + EXPECT_TRUE(rxSettings0.getTgcSamples().empty()); + EXPECT_EQ(rxSettings0.getLpfCutoff(), 10000000); + EXPECT_FALSE(rxSettings0.getActiveTermination().has_value()); + + // us4oem:1 + auto const &us4oem1 = us4rSettings.getUs4OEMSettings()[1]; + EXPECT_EQ(us4oem1.getChannelMapping(), + generateReversed(0, 128)); + EXPECT_EQ(us4oem1.getActiveChannelGroups(), std::vector( + { + false, false, false, false, + true, true, true, true, + true, true, true, true, + true, true, true, true + })); + // Rx settings + auto const &rxSettings1 = us4oem1.getRxSettings(); + EXPECT_FALSE(rxSettings1.getDtgcAttenuation().has_value()); + EXPECT_EQ(rxSettings1.getLnaGain(), 30); + EXPECT_EQ(rxSettings1.getPgaGain(), 24); + EXPECT_EQ(rxSettings1.getTgcSamples(), + std::vector({14.5, 15.5, 16.5, 17.5})); + EXPECT_EQ(rxSettings1.getLpfCutoff(), 1000000); + EXPECT_EQ(rxSettings1.getActiveTermination(), 500); +} + +TEST(ReadingProtoTxtFile, throwsExceptionOnNoChannelsMask) { + auto filepath = std::filesystem::path(ARRUS_TEST_DATA_PATH) / + std::filesystem::path("custom_us4r_no_channels_mask.prototxt"); + EXPECT_THROW(arrus::io::readSessionSettings(filepath.string()), IllegalArgumentException); +} + +int main(int argc, char **argv) { + ARRUS_INIT_TEST_LOG(arrus::Logging); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + diff --git a/arrus/core/io/test-data/custom_us4r.prototxt b/arrus/core/io/test-data/custom_us4r.prototxt new file mode 100644 index 000000000..3f0727fd8 --- /dev/null +++ b/arrus/core/io/test-data/custom_us4r.prototxt @@ -0,0 +1,64 @@ +dictionary_file: "dictionary.prototxt" + +us4r: { + probe: { + id: { + manufacturer: "acme" + name: "my_custom_probe" + } + n_elements: 32, + pitch: 0.21e-3, + tx_frequency_range: { + begin: 1e6, + end: 40e6 + } + } + adapter: { + id: { + manufacturer: "acme" + name: "my_custom_adapter" + } + n_channels: 64 + channel_mapping: { + us4oems: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1] + channels: [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31] + } + } + + probe_to_adapter_connection: { + channel_mapping_ranges: [ + { + begin: 0 + end: 15 + }, + { + begin: 48 + end: 63 + } + ] + } + + # Default initial values. + rx_settings: { + dtgc_attenuation: 0 + lna_gain: 12 + pga_gain: 24 + tgc_samples: [20, 21, 22] + lpf_cutoff: 1000000 + } + + channels_mask: { + channels: [0, 15, 30] + } + + us4oem_channels_mask: [ + { + channels: [0, 15, 30] + }, + { + channels: [] + } + ] +} + + diff --git a/arrus/core/io/test-data/custom_us4r_no_channels_mask.prototxt b/arrus/core/io/test-data/custom_us4r_no_channels_mask.prototxt new file mode 100644 index 000000000..29616ce4a --- /dev/null +++ b/arrus/core/io/test-data/custom_us4r_no_channels_mask.prototxt @@ -0,0 +1,49 @@ +dictionary_file: "dictionary.prototxt" + +us4r: { + probe: { + id: { + manufacturer: "acme" + name: "my_custom_probe" + } + n_elements: 32, + pitch: 0.21e-3, + tx_frequency_range: { + begin: 1e6, + end: 40e6 + } + } + adapter: { + id: { + manufacturer: "acme" + name: "my_custom_adapter" + } + n_channels: 64 + channel_mapping: { + us4oems: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1] + channels: [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31] + } + } + + probe_to_adapter_connection: { + channel_mapping_ranges: [ + { + begin: 0 + end: 15 + }, + { + begin: 48 + end: 63 + } + ] + } + + # Default initial values. + rx_settings: { + dtgc_attenuation: 0 + lna_gain: 12 + pga_gain: 24 + tgc_samples: [20, 21, 22] + lpf_cutoff: 1000000 + } +} \ No newline at end of file diff --git a/arrus/core/io/test-data/dictionary.prototxt b/arrus/core/io/test-data/dictionary.prototxt new file mode 100644 index 000000000..a88a676fe --- /dev/null +++ b/arrus/core/io/test-data/dictionary.prototxt @@ -0,0 +1,490 @@ +probe_adapter_models: [ + { + id: { + manufacturer: "us4us" + name: "esaote" + } + n_channels: 192 + channel_mapping_regions: [ + { + us4oem: 0 + channels: [31, 30, 29, 28, 27, 26, 25, 24, + 23, 22, 21, 20, 19, 18, 17, 15, + 16, 14, 13, 12, 11, 10, 9, 8, + 7, 6, 5, 4, 3, 2, 1, 0, + 63, 62, 61, 60, 59, 58, 57, 56, + 55, 54, 53, 52, 51, 50, 49, 47, + 48, 46, 45, 44, 43, 42, 41, 40, + 39, 38, 37, 36, 35, 34, 33, 32, + 95, 94, 93, 92, 91, 90, 89, 88, + 87, 86, 85, 84, 83, 82, 81, 79, + 80, 78, 77, 76, 75, 74, 73, 72, + 71, 70, 69, 68, 67, 66, 65, 64, + 127, 126, 125, 124, 123, 122, 121, 120, + 119, 118, 117, 116, 115, 114, 112, 113, + 111, 110, 109, 108, 107, 106, 105, 104, + 103, 102, 101, 100, 99, 98, 97, 96] + }, + { + us4oem: 1 + channels: [0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63] + } + ] + }, + { + id: { + manufacturer: "us4us" + name: "esaote2" + } + n_channels: 192 + channel_mapping_regions: [ + { + us4oem: 0 + channels: [26, 27, 25, 23, 28, 22, 20, 21, + 24, 18, 19, 15, 17, 16, 29, 13, + 11, 14, 30, 8, 12, 5, 10, 9, + 31, 7, 3, 6, 0, 2, 4, 1] + }, + { + us4oem: 1 + channels: [ 4, 3, 7, 5, 6, 2, 8, 9, + 1, 11, 0, 10, 13, 12, 15, 14, + 16, 17, 19, 18, 20, 25, 21, 22, + 23, 31, 24, 27, 30, 26, 28, 29] + }, + { + us4oem: 0 + channels: [56, 55, 54, 53, 57, 52, 51, 49, + 50, 48, 47, 46, 44, 45, 58, 42, + 43, 59, 40, 41, 60, 38, 61, 39, + 62, 34, 37, 63, 36, 35, 32, 33] + }, + { + us4oem: 1 + channels: [35, 34, 36, 38, 33, 37, 39, 40, + 32, 41, 42, 43, 44, 45, 46, 47, + 49, 48, 50, 52, 51, 55, 53, 54, + 58, 56, 59, 57, 62, 61, 60, 63] + }, + { + us4oem: 0 + channels: [92, 93, 89, 91, 88, 90, 87, 85, + 86, 84, 83, 82, 81, 80, 79, 77, + 78, 76, 95, 75, 74, 94, 73, 72, + 70, 64, 71, 68, 65, 69, 67, 66] + }, + { + us4oem: 1 + channels: [65, 67, 66, 69, 64, 68, 71, 70, + 72, 74, 73, 75, 76, 77, 78, 79, + 80, 82, 81, 83, 85, 84, 87, 86, + 88, 92, 89, 94, 90, 91, 95, 93] + } + ] + }, + + { + id: { + manufacturer: "us4us" + name: "esaote3" + } + n_channels: 192 + channel_mapping_regions: [ + { + us4oem: 0 + region: { + begin: 0, + end: 31 + } + }, + { + us4oem: 1 + region: { + begin: 0, + end: 31 + } + }, + { + us4oem: 0 + region: { + begin: 32, + end: 63 + } + }, + { + us4oem: 1 + region: { + begin: 32, + end: 63 + } + }, + { + us4oem: 0 + region: { + begin: 64, + end: 95 + } + }, + { + us4oem: 1 + region: { + begin: 64, + end: 95 + } + } + ] + }, + { + id: { + manufacturer: "us4us" + name: "ultrasonix" + } + n_channels: 128 + channel_mapping_regions: [ + { + # adapter channel 0: us4oem0, 0, + # adapter channel 1: us4oem0, 1, + # ... + # adapter channel 31: us4oem0, 31 + us4oem: 0 + channels: [0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31] + }, + { + # adapter channel 32: us4oem1, 63, + # adapter channel 33: us4oem1, 62, + # ... + # adapter channel 63: us4oem1, 32, + us4oem: 1 + channels: [63, 62, 61, 60, 59, 58, 57, 56, + 55, 54, 53, 52, 51, 50, 49, 48, + 47, 46, 45, 44, 43, 42, 41, 40, + 39, 38, 37, 36, 35, 34, 33, 32] + }, + # adapter channel 64, us4oem0, 64, + # adapter channel 65, us4oem0, 65, + # ... + { + us4oem: 0 + channels: [64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95] + }, + { + us4oem: 1 + channels: [127, 126, 125, 124, 123, 122, 121, 120, + 119, 118, 117, 116, 115, 114, 113, 112, + 111, 110, 109, 108, 107, 106, 105, 104, + 103, 102, 101, 100, 99, 98, 97, 96] + } + ] + }, + { + id: { + manufacturer: "us4us" + name: "atl/philips" + } + n_channels: 128 + channel_mapping_regions: [ + { + us4oem: 0 + channels: [31, 30, 29, 28, 27, 26, 25, 24, + 23, 22, 21, 20, 19, 18, 17, 15, + 16, 14, 13, 12, 11, 10, 9, 8, + 7, 6, 5, 4, 3, 2, 1, 0] + }, + { + us4oem: 1 + channels: [32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 48, + 47, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63] + }, + { + us4oem: 0 + channels: [95, 94, 93, 92, 91, 90, 89, 88, + 87, 86, 85, 84, 83, 82, 81, 79, + 80, 78, 77, 76, 75, 74, 73, 72, + 71, 70, 69, 68, 67, 66, 65, 64] + }, + { + us4oem: 1 + channels: [96, 97, 98, 99, 100, 101, 102, 103, + 104, 105, 106, 107, 108, 109, 110, 112, + 111, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127] + } + ] + } +] +probe_models: [ + { + id: { + manufacturer: "esaote" + name: "sl1543" + } + n_elements: 192, + pitch: 0.245e-3, + tx_frequency_range: { + begin: 1e6, + end: 10e6 + }, + voltage_range: { + begin: 0, + end: 75 + } + }, + { + id: { + manufacturer: "esaote" + name: "al2442" + } + n_elements: 192, + pitch: 0.21e-3, + tx_frequency_range: { + begin: 1e6, + end: 10e6 + }, + voltage_range: { + begin: 0, + end: 75 + } + }, + { + id: { + manufacturer: "esaote" + name: "sp2430" + } + n_elements: 96, + pitch: 0.22e-3, + tx_frequency_range: { + begin: 1e6, + end: 10e6 + }, + voltage_range: { + begin: 0, + end: 75 + } + }, + { + id: { + manufacturer: "ultrasonix" + name: "l14-5/38" + } + n_elements: 128 + pitch: 0.3048e-3, + tx_frequency_range: { + begin: 1e6, + end: 10e6 + }, + voltage_range: { + begin: 0, + end: 75 + } + }, + + { + id: { + manufacturer: "olympus" + name: "5L128" + } + n_elements: 128, + pitch: 0.6e-3, + tx_frequency_range: { + begin: 1e6, + end: 10e6 + }, + voltage_range: { + begin: 0, + end: 115 + } + }, + { + id: { + manufacturer: "vermon" + name: "la/20/128" + } + n_elements: 128, + pitch: 0.1e-3, + tx_frequency_range: { + begin: 10e6, + end: 30e6 + }, + voltage_range: { + begin: 0, + end: 15 + } + }, + { + id: { + manufacturer: "atl/philips" + name: "l7-4" + } + n_elements: 128, + pitch: 0.298e-3, + tx_frequency_range: { + begin: 1e6, + end: 10e6 + }, + voltage_range: { + begin: 0, + end: 75 + } + } +] + +probe_to_adapter_connections: [ + { + probe_model_id: { + manufacturer: "esaote" + name: "sl1543" + } + probe_adapter_model_id: [ + { + manufacturer: "us4us" + name: "esaote" + }, + { + manufacturer: "us4us" + name: "esaote2" + }, + { + manufacturer: "us4us" + name: "esaote3" + } + ] + channel_mapping_ranges: { + begin: 0 + end: 191 + } + }, + { + probe_model_id: { + manufacturer: "esaote" + name: "al2442" + } + probe_adapter_model_id: [ + { + manufacturer: "us4us" + name: "esaote" + }, + { + manufacturer: "us4us" + name: "esaote2" + }, + { + manufacturer: "us4us" + name: "esaote3" + } + ] + channel_mapping_ranges: { + begin: 0 + end: 191 + } + }, + { + probe_model_id: { + manufacturer: "esaote" + name: "sp2430" + } + probe_adapter_model_id: [ + { + manufacturer: "us4us" + name: "esaote" + }, + { + manufacturer: "us4us" + name: "esaote2" + }, + { + manufacturer: "us4us" + name: "esaote3" + } + ] + channel_mapping_ranges: [ + { + begin: 0 + end: 47 + }, + { + begin: 144, + end: 191 + } + ] + }, + { + probe_model_id: { + manufacturer: "ultrasonix" + name: "l14-5/38" + } + probe_adapter_model_id: [ + { + manufacturer: "us4us" + name: "ultrasonix" + } + ] + channel_mapping_ranges: { + begin: 0 + end: 127 + } + }, + { + probe_model_id: { + manufacturer: "olympus" + name: "5L128" + } + probe_adapter_model_id: [ + { + manufacturer: "us4us" + name: "esaote3" + } + ] + channel_mapping_ranges: { + begin: 0 + end: 127 + } + }, + { + probe_model_id: { + manufacturer: "vermon" + name: "la/20/128" + } + probe_adapter_model_id: [ + { + manufacturer: "us4us" + name: "atl/philips" + } + ] + channel_mapping_ranges: { + begin: 0 + end: 127 + } + }, + { + probe_model_id: { + manufacturer: "atl/philips" + name: "l7-4" + } + probe_adapter_model_id: [ + { + manufacturer: "us4us" + name: "atl/philips" + } + ] + channel_mapping_ranges: { + begin: 0 + end: 127 + } + } + +] diff --git a/arrus/core/io/test-data/us4oems.prototxt b/arrus/core/io/test-data/us4oems.prototxt new file mode 100644 index 000000000..ec2c19b3f --- /dev/null +++ b/arrus/core/io/test-data/us4oems.prototxt @@ -0,0 +1,25 @@ +us4r: { + us4oems: [ + { + channel_mapping: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127] + active_channel_groups: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0] + rx_settings: { + dtgc_attenuation: 24 + lna_gain: 12 + pga_gain: 24 + lpf_cutoff: 10000000 + } + }, + { + channel_mapping: [127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + active_channel_groups: [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + rx_settings: { + lna_gain: 30 + pga_gain: 24 + lpf_cutoff: 1000000 + tgc_samples: [14.5, 15.5, 16.5, 17.5] + active_termination: 500 + } + } + ] +} diff --git a/arrus/core/io/test-data/us4r.prototxt b/arrus/core/io/test-data/us4r.prototxt new file mode 100644 index 000000000..fcf6f12ee --- /dev/null +++ b/arrus/core/io/test-data/us4r.prototxt @@ -0,0 +1,44 @@ +dictionary_file: "dictionary.prototxt" + +us4r: { + probe_id: { + manufacturer: "esaote" + name: "sl1543" + } + + adapter_id: { + manufacturer: "us4us" + name: "esaote2" + } + + # Default initial values. + rx_settings: { + lna_gain: 24 + pga_gain: 30 + tgc_samples: [14, 15, 16] + lpf_cutoff: 10000000 + active_termination: 200 + } + + hv: { + model_id { + manufacturer: "us4us" + name: "hv256" + } + } + + channels_mask: { + channels: [5, 10, 15] + } + + us4oem_channels_mask: [ + { + channels: [5, 10] + }, + { + channels: 15 + } + ] +} + + diff --git a/arrus/core/io/validators/DictionaryProtoValidator.h b/arrus/core/io/validators/DictionaryProtoValidator.h new file mode 100644 index 000000000..184c5bc96 --- /dev/null +++ b/arrus/core/io/validators/DictionaryProtoValidator.h @@ -0,0 +1,122 @@ +#ifndef ARRUS_CORE_IO_VALIDATORS_DICTIONARYPROTOVALIDATOR_H +#define ARRUS_CORE_IO_VALIDATORS_DICTIONARYPROTOVALIDATOR_H + +#include +#include +#include +#include + +#include "arrus/common/compiler.h" +#include "arrus/core/common/collections.h" +#include "arrus/core/common/validation.h" + +#include "arrus/core/io/validators/ProbeModelProtoValidator.h" +#include "arrus/core/io/validators/ProbeAdapterModelProtoValidator.h" +#include "arrus/core/io/validators/ProbeToAdapterConnectionProtoValidator.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4127) + +#include "io/proto/Dictionary.pb.h" + +COMPILER_POP_DIAGNOSTIC_STATE + +namespace arrus::io { + +class DictionaryProtoValidator + : public Validator> { + +public: + using Validator::Validator; + + template + inline bool hasId(T model) { + return model.has_id() && !model.id().name().empty() + && !model.id().manufacturer().empty(); + } + + void + validate(const std::unique_ptr &obj) override { + using ModelId = std::pair; + std::unordered_set> adapterIds; + std::unordered_set> probeIds; + + // Validate probes + int i = 0; + for(auto &probe : obj->probe_models()) { + std::string fieldName = arrus::format("probe_model:{}", i); + expectTrue(fieldName, hasId(probe), + "id (including its components) should not empty"); + if(hasId(probe)) { + ModelId probeId = {probe.id().manufacturer(), + probe.id().name()}; + + expectTrue(fieldName, + probeIds.find(probeId) == probeIds.end(), + "id already used."); + + probeIds.emplace(probeId); + + ProbeModelProtoValidator probeValidator(fieldName); + probeValidator.validate(probe); + copyErrorsFrom(probeValidator); + } + ++i; + } + + i = 0; + for(auto &adapter: obj->probe_adapter_models()) { + std::string fieldName = arrus::format("probe_adapter_model:{}", i); + expectTrue(fieldName, hasId(adapter), + "id (including its components) should not empty"); + if(hasId(adapter)) { + ModelId adapterId = {adapter.id().manufacturer(), + adapter.id().name()}; + expectTrue(fieldName, + adapterIds.find(adapterId) == adapterIds.end(), + "id already used."); + + adapterIds.emplace(adapterId); + + ProbeAdapterModelProtoValidator adapterValidator(fieldName); + adapterValidator.validate(adapter); + copyErrorsFrom(adapterValidator); + ++i; + } + + } + i = 0; + for(auto &conn : obj->probe_to_adapter_connections()) { + std::string fieldName = + arrus::format("probe_adapter_connection:{}", i); + + // Verify if the probe model with given id actually exists. + ModelId probeModelId = {conn.probe_model_id().manufacturer(), + conn.probe_model_id().name()}; + + expectTrue(fieldName, probeIds.find(probeModelId) != probeIds.end(), + arrus::format("Undefined probe id: {}, {}", + probeModelId.first, probeModelId.second)); + + // Verify if the adapter models with given ids actually exist. + for(auto &probeAdapterModelId: conn.probe_adapter_model_id()) { + ModelId id = {probeAdapterModelId.manufacturer(), + probeAdapterModelId.name()}; + expectTrue(fieldName, adapterIds.find(id) != adapterIds.end(), + arrus::format("Undefined adapter id: {}, {}", + id.first, id.second)); + } + + ProbeToAdapterConnectionProtoValidator connValidator(fieldName); + connValidator.validate(conn); + copyErrorsFrom(connValidator); + + ++i; + } + } + +}; + +} + +#endif //ARRUS_CORE_IO_VALIDATORS_DICTIONARYPROTOVALIDATOR_H diff --git a/arrus/core/io/validators/ProbeAdapterModelProtoValidator.h b/arrus/core/io/validators/ProbeAdapterModelProtoValidator.h new file mode 100644 index 000000000..92331b2af --- /dev/null +++ b/arrus/core/io/validators/ProbeAdapterModelProtoValidator.h @@ -0,0 +1,76 @@ +#ifndef ARRUS_CORE_IO_VALIDATORS_PROBEADAPTERMODELPROTOVALIDATOR_H +#define ARRUS_CORE_IO_VALIDATORS_PROBEADAPTERMODELPROTOVALIDATOR_H + +#include "arrus/common/compiler.h" +#include "arrus/core/common/validation.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4127) + +#include "io/proto/devices/us4r/ProbeAdapterModel.pb.h" + +namespace arrus::io { + +class ProbeAdapterModelProtoValidator + : public Validator { + public: + + explicit ProbeAdapterModelProtoValidator(const std::string &componentName) + : Validator(componentName) {} + + void validate(const arrus::proto::ProbeAdapterModel &obj) override { + using namespace arrus::devices; + bool hasChannelMapping = obj.has_channel_mapping(); + bool hasChannelMappingsRegions = !obj.channel_mapping_regions().empty(); + + // Data types + expectDataType("n_channels", obj.n_channels()); + if(hasChannelMapping) { + auto const &ordinals = obj.channel_mapping().us4oems(); + auto const &channels = obj.channel_mapping().channels(); + expectAllDataType( + "channel_mapping.us4oems", ordinals); + expectAllDataType( + "channel_mapping.channels", channels); + } + if(hasChannelMappingsRegions) { + for(auto const ®ion: obj.channel_mapping_regions()) { + expectTrue( + "channel_mapping_regions", + region.has_region() ^ !region.channels().empty(), + "Exactly one of the following should be provided: region, channels" + ); + + expectDataType("channel_mapping_regions.us4oem", + region.us4oem()); + if(region.has_region()) { + + expectDataType( + "channel_mapping_regions.region.begin", + region.region().begin()); + + expectDataType( + "channel_mapping_regions.region.end", + region.region().end()); + + } else { + expectAllDataType( + "channel_mapping_regions.channels", region.channels()); + } + + } + } + + // Semantic + expectTrue("channel mapping", + hasChannelMapping ^ hasChannelMappingsRegions, + "Exactly one of the following should be set for " + "probe adapter model: (channel mappings, channel " + "mapping regions)"); + } + +}; + +} + +#endif //ARRUS_CORE_IO_VALIDATORS_PROBEADAPTERMODELPROTOVALIDATOR_H diff --git a/arrus/core/io/validators/ProbeModelProtoValidator.h b/arrus/core/io/validators/ProbeModelProtoValidator.h new file mode 100644 index 000000000..f6523d6b3 --- /dev/null +++ b/arrus/core/io/validators/ProbeModelProtoValidator.h @@ -0,0 +1,40 @@ +#ifndef ARRUS_CORE_IO_VALIDATORS_PROBEMODELPROTOVALIDATOR_H +#define ARRUS_CORE_IO_VALIDATORS_PROBEMODELPROTOVALIDATOR_H + +#include "arrus/common/compiler.h" +#include "arrus/core/common/validation.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4127) + +#include "io/proto/devices/probe/ProbeModel.pb.h" + +COMPILER_POP_DIAGNOSTIC_STATE + +namespace arrus::io { + +class ProbeModelProtoValidator : public Validator { +public: + explicit ProbeModelProtoValidator(const std::string &componentName) + : Validator(componentName) {} + + void validate(const arrus::proto::ProbeModel &obj) override { + // Data type + expectAllDataType("n_elements", obj.n_elements()); + auto &freqRange = obj.tx_frequency_range(); + expectTrue("tx_frequency_range", freqRange.begin() <= freqRange.end(), + "tx freq range begin should be <= tx freq range end."); + + auto &voltageRange = obj.voltage_range(); + expectTrue("voltage_range", voltageRange.begin() <= voltageRange.end(), + "voltage range begin should be <= voltage range end."); + + expectDataType("voltage_range.begin", voltageRange.begin()); + expectDataType("voltage_range.end", voltageRange.end()); + } +}; + +} + + +#endif //ARRUS_CORE_IO_VALIDATORS_PROBEMODELPROTOVALIDATOR_H diff --git a/arrus/core/io/validators/ProbeToAdapterConnectionProtoValidator.h b/arrus/core/io/validators/ProbeToAdapterConnectionProtoValidator.h new file mode 100644 index 000000000..67385b6cc --- /dev/null +++ b/arrus/core/io/validators/ProbeToAdapterConnectionProtoValidator.h @@ -0,0 +1,55 @@ +#ifndef ARRUS_CORE_IO_VALIDATORS_PROBETOADAPTERCONNECTIONPROTOVALIDATOR_H +#define ARRUS_CORE_IO_VALIDATORS_PROBETOADAPTERCONNECTIONPROTOVALIDATOR_H + +#include "arrus/common/compiler.h" +#include "arrus/core/common/validation.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4127) + +#include "io/proto/devices/us4r/ProbeToAdapterConnection.pb.h" + +COMPILER_POP_DIAGNOSTIC_STATE + +namespace arrus::io { + +class ProbeToAdapterConnectionProtoValidator + : public Validator { + public: + + using Validator::Validator; + + void validate(const proto::ProbeToAdapterConnection &obj) override { + bool hasChannelMapping = !obj.channel_mapping().empty(); + bool hasMappingIntervals = !obj.channel_mapping_ranges().empty(); + + // data types + if(hasChannelMapping) { + expectAllDataType( + "channel_mapping", + obj.channel_mapping()); + } + + if(hasMappingIntervals) { + for(auto const &range : obj.channel_mapping_ranges()) { + expectTrue("range", range.begin() <= range.end(), + "should be begin <= end."); + expectDataType("region.begin", range.begin()); + expectDataType("region.end", range.end()); + } + } + + // semantic + expectTrue("probe_to_adapter_connection", + hasChannelMapping ^ hasMappingIntervals, + "Exactly one of the following should set: " + "channel_mapping, channel_mappign_ranges" + ); + } + +}; + + +} + +#endif //ARRUS_CORE_IO_VALIDATORS_PROBETOADAPTERCONNECTIONPROTOVALIDATOR_H diff --git a/arrus/core/io/validators/RxSettingsProtoValidator.h b/arrus/core/io/validators/RxSettingsProtoValidator.h new file mode 100644 index 000000000..df0113e56 --- /dev/null +++ b/arrus/core/io/validators/RxSettingsProtoValidator.h @@ -0,0 +1,38 @@ +#ifndef ARRUS_CORE_IO_VALIDATORS_RXSETTINGSPROTOVALIDATOR_H +#define ARRUS_CORE_IO_VALIDATORS_RXSETTINGSPROTOVALIDATOR_H + +#include "arrus/common/compiler.h" +#include "arrus/core/common/validation.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4127) + +#include "io/proto/devices/us4r/RxSettings.pb.h" + +COMPILER_POP_DIAGNOSTIC_STATE + +namespace arrus::io { + +class RxSettingsProtoValidator : public Validator { + using Validator::Validator; + + public: + void validate(const arrus::proto::RxSettings &obj) override { + + if(obj.dtgcAttenuation__case() == proto::RxSettings::kDtgcAttenuation) { + expectDataType("dtgc_attenuation", obj.dtgc_attenuation()); + } + expectDataType("pga_gain", obj.pga_gain()); + expectDataType("lna_gain", obj.lna_gain()); + + if(obj.activeTermination__case() == + proto::RxSettings::kActiveTermination) { + expectDataType("active_termination", obj.active_termination()); + } + } + +}; + +} + +#endif //ARRUS_CORE_IO_VALIDATORS_RXSETTINGSPROTOVALIDATOR_H diff --git a/arrus/core/io/validators/SessionSettingsProtoValidator.h b/arrus/core/io/validators/SessionSettingsProtoValidator.h new file mode 100644 index 000000000..818530cca --- /dev/null +++ b/arrus/core/io/validators/SessionSettingsProtoValidator.h @@ -0,0 +1,159 @@ +#ifndef ARRUS_CORE_IO_VALIDATORS_SESSIONSETTINGSPROTOVALIDATOR_H +#define ARRUS_CORE_IO_VALIDATORS_SESSIONSETTINGSPROTOVALIDATOR_H + +#include "arrus/common/compiler.h" +#include "arrus/core/common/validation.h" + +#include "arrus/core/io/validators/ProbeModelProtoValidator.h" +#include "arrus/core/io/validators/ProbeAdapterModelProtoValidator.h" +#include "arrus/core/io/validators/ProbeToAdapterConnectionProtoValidator.h" +#include "arrus/core/io/validators/RxSettingsProtoValidator.h" + +COMPILER_PUSH_DIAGNOSTIC_STATE +COMPILER_DISABLE_MSVC_WARNINGS(4127) + +#include "io/proto/session/SessionSettings.pb.h" + +COMPILER_POP_DIAGNOSTIC_STATE + + +namespace arrus::io { + +class SessionSettingsProtoValidator + : public Validator> { + + public: + explicit SessionSettingsProtoValidator(const std::string &name) + : Validator(name) {} + + void validate( + const std::unique_ptr &obj) override { + expectTrue("us4r", obj->has_us4r(), "us4r settings are required"); + + // TODO extract us4r settings validator + + if(hasErrors()) { + return; + } + + auto &us4r = obj->us4r(); + + bool hasUs4oemSettings = !us4r.us4oems().empty(); + bool hasProbeSettings = us4r.has_probe() || us4r.has_probe_id(); + bool hasAdapterSettings = us4r.has_adapter() || us4r.has_adapter_id(); + bool hasProbeAdapterSettings = hasProbeSettings || hasAdapterSettings || + us4r.has_probe_to_adapter_connection() || + us4r.has_rx_settings(); + + expectTrue("us4r", hasUs4oemSettings ^ hasProbeAdapterSettings, + "Exactly one of the following should be set in us4r " + "settings: a list of us4oem settings or: (probe settings, " + "adapter settings, probe<->adapter connection, rx settings)" + ); + + if(hasErrors()) { + return; + } + + if(hasUs4oemSettings) { + int i = 0; + for(auto &settings : us4r.us4oems()) { + std::string fieldName = "Us4OEM:" + std::to_string(i); + expectTrue(fieldName, settings.has_rx_settings(), + "Rx settings are required."); + + expectAllDataType( + fieldName, settings.channel_mapping(), "channel_mapping"); + + RxSettingsProtoValidator validator(fieldName); + validator.validate(settings.rx_settings()); + copyErrorsFrom(validator); + ++i; + } + } else if(hasProbeAdapterSettings) { + bool hasAllProbeSettings = hasProbeSettings && hasAdapterSettings && + us4r.has_rx_settings(); + + expectTrue("us4r", hasAllProbeSettings, + "All of the following fields are required: " + "(probe settings, adapter settings, rx settings)"); + + if(us4r.has_probe() || us4r.has_adapter()) { + // Custom probe or adapter + expectTrue("us4r", us4r.has_probe_to_adapter_connection(), + "Probe to adapter connection is required " + "for custom probe and adapter definitions."); + } + + if(hasErrors()) { + return; + } + + if(us4r.has_probe()) { + expectTrue("probe_id", !us4r.has_probe_id(), + "Probe Id should not be set " + "(custom probe already set)."); + ProbeModelProtoValidator probeValidator("custom probe"); + auto &probe = us4r.probe(); + probeValidator.validate(probe); + copyErrorsFrom(probeValidator); + } else { + expectTrue("probe_id", us4r.has_probe_id(), + "Probe id or custom probe def. is required."); + } + + if(us4r.has_adapter()) { + expectTrue("adapter_id", !us4r.has_adapter_id(), + "Adapter Id should not be set " + "(custom adapter already set)."); + + ProbeAdapterModelProtoValidator adapterValidator( + "custom adapter"); + auto &adapter = us4r.adapter(); + adapterValidator.validate(adapter); + copyErrorsFrom(adapterValidator); + } else { + expectTrue("adapter_id", us4r.has_adapter_id(), + "Adapter id or custom adapter def. is required."); + } + + if(us4r.has_probe() && us4r.has_adapter()) { + expectTrue("probe_to_adapter_connection", + us4r.has_probe_to_adapter_connection(), + "Custom probe and adapter are set, " + "custom probe to adapter connection is required."); + } + + // otherwise it should overwrite the settings from dictionary. + + if(us4r.has_probe_to_adapter_connection()) { + // probe and adapter ids are forbidden (to avoid any additional + // confusion). + auto &conn = us4r.probe_to_adapter_connection(); + expectTrue("probe_to_adapter_connection", + !conn.has_probe_model_id(), + "Probe model id is forbidden for custom " + "probe connections."); + + expectTrue("probe_to_adapter_connection", + conn.probe_adapter_model_id().empty(), + "Adapter id is forbidden for custom " + "probe connections."); + ProbeToAdapterConnectionProtoValidator connValidator( + "custom connection"); + connValidator.validate(conn); + copyErrorsFrom(connValidator); + } + + expectTrue("rx_settings", us4r.has_rx_settings(), + "Rx settings are required."); + RxSettingsProtoValidator rxSettingsValidator("rx_settings"); + rxSettingsValidator.validate(us4r.rx_settings()); + copyErrorsFrom(rxSettingsValidator); + } + } +}; + +} + +#endif //ARRUS_CORE_IO_VALIDATORS_SESSIONSETTINGSPROTOVALIDATOR_H diff --git a/arrus/core/session/SessionImpl.cpp b/arrus/core/session/SessionImpl.cpp new file mode 100644 index 000000000..d0383de8e --- /dev/null +++ b/arrus/core/session/SessionImpl.cpp @@ -0,0 +1,107 @@ +#include "arrus/core/session/SessionImpl.h" + +#include +#include + +#include + +#include "arrus/core/api/common/exceptions.h" +#include "arrus/common/format.h" +#include "arrus/common/compiler.h" +#include "arrus/core/devices/utils.h" + +#include "arrus/core/devices/us4r/Us4RFactoryImpl.h" +#include "arrus/core/devices/us4r/us4oem/Us4OEMFactoryImpl.h" +#include "arrus/core/devices/probe/ProbeFactoryImpl.h" +#include "arrus/core/devices/us4r/Us4RSettingsConverterImpl.h" +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMInitializerImpl.h" +#include "arrus/core/devices/us4r/probeadapter/ProbeAdapterFactoryImpl.h" +#include "arrus/core/devices/us4r/external/ius4oem/IUs4OEMFactoryImpl.h" +#include "arrus/core/devices/us4r/hv/HV256FactoryImpl.h" +#include "arrus/core/session/SessionSettings.h" +#include "arrus/core/api/io/settings.h" + +namespace arrus::session { + +using namespace arrus::devices; + +Session::Handle createSession(const SessionSettings &sessionSettings) { + return std::make_unique( + sessionSettings, + std::make_unique( + std::make_unique(), + std::make_unique(), + std::make_unique(), + std::make_unique(), + std::make_unique(), + std::make_unique(), + std::make_unique() + ) + ); +} + +Session::Handle createSession(const std::string& filepath) { + auto settings = arrus::io::readSessionSettings(filepath); + return createSession(settings); +} + +SessionImpl::SessionImpl(const SessionSettings &sessionSettings, + Us4RFactory::Handle us4RFactory) + : us4rFactory(std::move(us4RFactory)) { + getDefaultLogger()->log(LogSeverity::DEBUG, + arrus::format("Configuring session: {}", + toString(sessionSettings))); + devices = configureDevices(sessionSettings); +} + +arrus::devices::Device::RawHandle SessionImpl::getDevice(const std::string &path) { + // sanitize + std::string sanitizedPath{path}; + boost::algorithm::trim(sanitizedPath); + + // parse path + auto[root, tail] = ::arrus::devices::getPathRoot(sanitizedPath); + + auto deviceId = DeviceId::parse(root); + arrus::devices::Device::RawHandle rootDevice = getDevice(deviceId); + + if(tail.empty()) { + return rootDevice; + } else { + if(isInstanceOf(rootDevice)) { + return ((DeviceWithComponents *) rootDevice)->getDevice(tail); + } else { + throw IllegalArgumentException(arrus::format( + "Invalid path '{}', top-level devices can be accessed only.", + path + )); + } + } +} + +arrus::devices::Device::RawHandle SessionImpl::getDevice(const DeviceId &deviceId) { + try { + return devices.at(deviceId).get(); + } catch(const std::out_of_range &) { + throw IllegalArgumentException( + arrus::format("Device unavailable: {}", deviceId.toString())); + } +} + +SessionImpl::DeviceMap +SessionImpl::configureDevices(const SessionSettings &sessionSettings) { + DeviceMap result; + + // Configuring Us4R. + const Us4RSettings &us4RSettings = sessionSettings.getUs4RSettings(); + Us4R::Handle us4r = us4rFactory->getUs4R(0, us4RSettings); + result.emplace(us4r->getDeviceId(), std::move(us4r)); + return result; +} + +SessionImpl::~SessionImpl() { + ::arrus::getDefaultLogger()->log(LogSeverity::INFO, "Closing session."); +} + + +} \ No newline at end of file diff --git a/arrus/core/session/SessionImpl.h b/arrus/core/session/SessionImpl.h new file mode 100644 index 000000000..0ab1e3940 --- /dev/null +++ b/arrus/core/session/SessionImpl.h @@ -0,0 +1,51 @@ +#ifndef ARRUS_CORE_SESSION_SESSIONIMPL_H +#define ARRUS_CORE_SESSION_SESSIONIMPL_H + +#include +#include + +#include "arrus/core/api/session/Session.h" +#include "arrus/core/common/hash.h" +#include "arrus/core/devices/DeviceId.h" + +namespace arrus::session { + +class SessionImpl : public Session { +public: + SessionImpl( + const SessionSettings &sessionSettings, + arrus::devices::Us4RFactory::Handle us4RFactory); + + virtual ~SessionImpl(); + + arrus::devices::Device::RawHandle + getDevice(const std::string &deviceId) override; + + arrus::devices::Device::RawHandle + getDevice(const arrus::devices::DeviceId &deviceId) override; + + SessionImpl(SessionImpl const &) = delete; + + void operator=(SessionImpl const &) = delete; + + SessionImpl(SessionImpl const &&) = delete; + + void operator=(SessionImpl const &&) = delete; + +private: + using DeviceMap = std::unordered_map< + arrus::devices::DeviceId, + arrus::devices::Device::Handle, + GET_HASHER_NAME(arrus::devices::DeviceId)>; + + DeviceMap + configureDevices(const SessionSettings &sessionSettings); + + DeviceMap devices; + arrus::devices::Us4RFactory::Handle us4rFactory; +}; + + +} + +#endif //ARRUS_CORE_SESSION_SESSIONIMPL_H diff --git a/arrus/core/session/SessionSettings.cpp b/arrus/core/session/SessionSettings.cpp new file mode 100644 index 000000000..a68797086 --- /dev/null +++ b/arrus/core/session/SessionSettings.cpp @@ -0,0 +1,14 @@ +#include "SessionSettings.h" +#include "arrus/core/devices/us4r/Us4RSettings.h" + +namespace arrus::session { + +std::ostream & +operator<<(std::ostream &os, const SessionSettings &settings) { + os << "us4RSettings: " << settings.getUs4RSettings(); + return os; +} + +} + + diff --git a/arrus/core/session/SessionSettings.h b/arrus/core/session/SessionSettings.h new file mode 100644 index 000000000..d7db61179 --- /dev/null +++ b/arrus/core/session/SessionSettings.h @@ -0,0 +1,15 @@ +#ifndef ARRUS_CORE_SESSION_SESSIONSETTINGS_H +#define ARRUS_CORE_SESSION_SESSIONSETTINGS_H + +#include "arrus/core/api/session/SessionSettings.h" + +namespace arrus::session { + +std::ostream & +operator<<(std::ostream &os, const SessionSettings &settings); + +} + + +#endif //ARRUS_CORE_SESSION_SESSIONSETTINGS_H + diff --git a/cmake/FindUs4.cmake b/cmake/FindUs4.cmake index 8682a8040..efc2f3d0f 100644 --- a/cmake/FindUs4.cmake +++ b/cmake/FindUs4.cmake @@ -1,5 +1,9 @@ # Option: Us4_ROOT_DIR: a directory, where lib64 and include files are located. +if(NOT DEFINED Us4_ROOT_DIR) + message(FATAL_ERROR "Us4_ROOT_DIR should be provided.") +endif() + find_path(Us4_INCLUDE_DIR NAMES ius4oem.h PATHS "${Us4_ROOT_DIR}/include" diff --git a/cmake/common.cmake b/cmake/common.cmake new file mode 100644 index 000000000..c450cdf73 --- /dev/null +++ b/cmake/common.cmake @@ -0,0 +1,10 @@ +# Prepends a value to the Path env. variable and returns new value in output_var. +# This function is os independent. +function(prepend_env_path output_var_name value) + if(WIN32) + set(arrus_path_sep "\;") + elseif(UNIX) + set(arrus_path_sep ":") + endif() + set(${output_var_name} ${value}${arrus_path_sep}$ENV{PATH} PARENT_SCOPE) +endfunction() \ No newline at end of file diff --git a/cmake/python.cmake b/cmake/python.cmake index e29d99e3b..11f2e2597 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -74,7 +74,7 @@ function(install_arrus_package TARGET_NAME VENV_TARGET PACKAGE_TARGET INSTALLATI COMMAND ${INSTALL_VENV_EXECUTABLE} -m pip install #TODO(pjarosik) consider appending timestamp to project version - # in order to avoid unecessary reinstallation of arius dependencies + # in order to avoid unnecessary reinstallation of arius dependencies ${INSTALL_ARRUS_OPTIONS} DEPENDS ${VENV_TARGET} ${PACKAGE_TARGET} ${ARRUS_PACKAGE_STAMP} @@ -97,7 +97,7 @@ function(install_sphinx_package TARGET_NAME VENV_TARGET) COMMAND ${CMAKE_COMMAND} -E touch ${INSTALL_TIMESTAMP} COMMAND - ${INSTALL_VENV_EXECUTABLE} -m pip install sphinx sphinx_rtd_theme + ${INSTALL_VENV_EXECUTABLE} -m pip install sphinx sphinx_rtd_theme six "git+git://github.com/pjarosik/matlabdomain@master#egg=sphinxcontrib-matlabdomain" DEPENDS ${VENV_TARGET} diff --git a/cmake/tests.cmake b/cmake/tests.cmake new file mode 100644 index 000000000..a6d25f461 --- /dev/null +++ b/cmake/tests.cmake @@ -0,0 +1,53 @@ +function(create_core_test test_src) + # Optional arguments. + if(${ARGC} GREATER 1) + set(other_srcs ${ARGV1}) + endif() + if(${ARGC} GREATER 2) + set(other_deps ${ARGV2}) + endif() + if(${ARGC} GREATER 2) + set(compile_definitions ${ARGV3}) + endif() + + # TODO(pjarosik) make the below a parameter + if(NOT DEFINED ARRUS_CPP_COMMON_COMPILE_OPTIONS) + message(FATAL_ERROR "ARRUS_CPP_COMMON_COMPILE_OPTIONS must be set for test targets.") + endif() + + # replace / in test_src with _ + get_filename_component(target_name_file ${test_src} NAME_WE) + get_filename_component(target_name_dir ${test_src} DIRECTORY) + string(REPLACE "/" "_" target_name "${target_name_dir}/${target_name_file}") + + add_executable(${target_name} + ${test_src} + ../common/logging/impl/Logging.cpp + ../common/logging/impl/LogSeverity.cpp + ${other_srcs} + ) + target_link_libraries(${target_name} + GTest::GTest + Boost::Boost + fmt::fmt + Microsoft.GSL::GSL + Eigen3::Eigen3 + ${other_deps}) + target_include_directories( + ${target_name} + PRIVATE + ${ARRUS_ROOT_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/.. + ${CMAKE_CURRENT_BINARY_DIR} # Required for pb.h files (protobuf). + ${Us4_INCLUDE_DIR} # Required to mock us4 devices. TODO(pjarosik) do not depend tests on external libraries + ) + target_compile_options(${target_name} PRIVATE ${ARRUS_CPP_COMMON_COMPILE_OPTIONS}) + target_compile_definitions(${target_name} PRIVATE + ARRUS_CORE_UNIT_TESTS + _SILENCE_CXX17_ALLOCATOR_VOID_DEPRECATION_WARNING + ${compile_definitions}) + add_test(NAME ${test_src} COMMAND ${target_name}) + + prepend_env_path(ARRUS_TESTS_ENV_PATH ${Us4_LIB_DIR}) + set_tests_properties(${test_src} PROPERTIES ENVIRONMENT "PATH=${ARRUS_TESTS_ENV_PATH}") +endfunction() \ No newline at end of file diff --git a/conan/profiles/msvc_x64_debug b/conan/profiles/msvc_x64_debug new file mode 100644 index 000000000..bf1052c9d --- /dev/null +++ b/conan/profiles/msvc_x64_debug @@ -0,0 +1,9 @@ +include(default) + +[settings] +os=Windows +arch=x86_64 +compiler=Visual Studio +compiler.version=15 +compiler.runtime=MDd +build_type=Debug \ No newline at end of file diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 000000000..c0d89044c --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,15 @@ +[requires] +boost/1.70.0#4fc6b458f2dbbe748a3f7ca8742b997e +protobuf/3.11.4 +gtest/1.10.0 +fmt/7.0.1 +ms-gsl/2.1.0 +eigen/3.3.7 + + +[options] +boost:shared=True + +[generators] +cmake_find_package +virtualenv diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt deleted file mode 100644 index 4798703de..000000000 --- a/core/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -add_library(core INTERFACE) - -target_include_directories(core INTERFACE ..) \ No newline at end of file diff --git a/core/cfg/default.yaml b/core/cfg/default.yaml deleted file mode 100644 index 4928026f2..000000000 --- a/core/cfg/default.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# How many cards are available in the system. -nUs4OEMs: 2 -HV256: True -probes: -- sl1543: - # Name of the interface adapter used in the system. - interface: esaote - # Us4OEM apertures used by given probe. - pitch: 0.245e-3 - aperture: - # SL1543's aperture: 0:127 - - card: 0 - master: True - origin: 0 - size: 128 - # SL1543's aperture: 128:191 - - card: 1 - origin: 0 - size: 64 diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 575439814..ad3430982 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -16,7 +16,7 @@ set(DOC_FILES "content/installation/img/thunderbolt.png" "content/installation/img/uninstall_arius_drv.png" "content/examples/bmode_imaging.matlab.rst" - "content/examples/single_module.python.rst" + content/examples/mock_session_example.python.rst "content/examples/img/coordinate_system.jpeg" "content/examples/img/apertures.jpeg" "content/examples/img/delays.jpeg" diff --git a/docs/conf.python.py.in b/docs/conf.python.py.in index 088dd297a..8404fbbee 100644 --- a/docs/conf.python.py.in +++ b/docs/conf.python.py.in @@ -13,38 +13,6 @@ import sys import importlib -# -- Mocking modules libs ---------------------------------------------------- - -import types -# TODO(pjarosik) don't duplicate logic from test_tools.mock_import -# or just do not use mocking here -def mock_import(module_name: str, **kwargs): - mock_module = types.ModuleType(module_name) - for key, value in kwargs.items(): - setattr(mock_module, key, value) - sys.modules[module_name] = mock_module - module_path = module_name.split(".") - if len(module_path) > 1: - print(".".join(module_path[:-1])) - parent = importlib.import_module(".".join(module_path[:-1])) - setattr(parent, module_path[-1], mock_module) - sys.modules[module_name] = mock_module - return mock_module - -mock_import( - "arrus.devices.ius4oem", - IUs4OEM=None, - ScheduleReceiveCallback=object -) -mock_import( - "arrus.devices.idbarLite", - IDbarLite=None -) -mock_import( - "arrus.devices.ihv256", - IHV256=None -) - # -- Project information ----------------------------------------------------- project = 'ARRUS (${LANGUAGE})' diff --git a/docs/content/api.python.rst b/docs/content/api.python.rst index ef687c373..2a1eed5c8 100644 --- a/docs/content/api.python.rst +++ b/docs/content/api.python.rst @@ -4,151 +4,133 @@ API Reference ============= -.. caution:: - - We will do our best to maintain backward compatibility but please note - that ARRUS is currently under development and its API may be modified in the future. +Session +======= +.. autoclass:: arrus.Session + :members: Devices ======= **Do not create instances of the below classes directly**. Use ``session.get_device`` to acquire the appropriate device, for example: -``session.get_device('Us4OEM:0')``. +``session.get_device('/Us4OEM:0')``. -.. autoclass:: arrus.devices.us4oem.Us4OEM - :members: get_sampling_frequency, get_n_rx_channels, get_n_tx_channels +.. autoclass:: arrus.devices.us4r.Us4R + :members: :show-inheritance: -.. autoclass:: arrus.devices.hv256.HV256 +.. autoclass:: arrus.devices.gpu.GPU + :show-inheritance: + +.. autoclass:: arrus.devices.cpu.CPU :show-inheritance: Operations ========== -.. autoclass:: arrus.ops.TxRx - :members: - :show-inheritance: +B-mode imaging Tx/Rx sequences +------------------------------ -.. autoclass:: arrus.ops.Tx +.. autoclass:: arrus.ops.imaging.LinSequence :members: :show-inheritance: -.. autoclass:: arrus.ops.Rx - :members: - :show-inheritance: -.. autoclass:: arrus.ops.Sequence - :members: - :show-inheritance: +Custom Tx/Rx sequences +---------------------- -.. autoclass:: arrus.ops.Loop +.. autoclass:: arrus.ops.us4r.Pulse :members: :show-inheritance: -Parameters ----------- - -Excitation -~~~~~~~~~~ - -.. autoclass:: arrus.Excitation - :members: - -.. autoclass:: arrus.SineWave +.. autoclass:: arrus.ops.us4r.TxRx :members: :show-inheritance: -Aperture -~~~~~~~~ - -.. autoclass:: arrus.MaskAperture +.. autoclass:: arrus.ops.us4r.Tx :members: :show-inheritance: -.. autoclass:: arrus.RegionBasedAperture +.. autoclass:: arrus.ops.us4r.Rx :members: :show-inheritance: -.. autoclass:: arrus.SingleElementAperture +.. autoclass:: arrus.ops.us4r.TxRxSequence :members: :show-inheritance: -Session -======= +Output data +=========== -.. autoclass:: arrus.Session +An instance of the following class is returned by the ``us4r.upload`` function: + +.. autoclass:: arrus.devices.us4r.HostBuffer :members: + :show-inheritance: -Configuration -============= -The configuration to apply to each of the Arrus components. +Metadata +-------- -.. autoclass:: arrus.CustomUs4RCfg +.. autoclass:: arrus.metadata.Metadata :members: :show-inheritance: -.. autoclass:: arrus.Us4OEMCfg +.. autoclass:: arrus.metadata.EchoDataDescription :members: :show-inheritance: -.. autoclass:: arrus.SessionCfg +.. autoclass:: arrus.metadata.FrameAcquisitionContext :members: :show-inheritance: -.. autoclass:: arrus.ChannelMapping - :members: Utility functions ================= -Logging -------- -.. autofunction:: arrus.set_log_level - -.. autofunction:: arrus.add_log_file - -Legacy API Reference -==================== - -.. danger:: +B-mode imaging pipeline +----------------------- - The API below is deprecated and we discourage using it. The legacy API - will be removed in the near future. - -Interface ---------- +.. autoclass:: arrus.utils.imaging.Pipeline + :show-inheritance: -An interface contains all the information required to configure the probe adapter -provided with the us4OEM module. Use the following function to obtain the -appropriate interface. +.. autoclass:: arrus.utils.imaging.BandpassFilter + :show-inheritance: +.. autoclass:: arrus.utils.imaging.QuadratureDemodulation + :show-inheritance: -.. autofunction:: arrus.interface.get_interface +.. autoclass:: arrus.utils.imaging.Decimation + :show-inheritance: -Currently, ``esaote`` and ``ultrasonix`` interfaces are implemented. +.. autoclass:: arrus.utils.imaging.RxBeamforming + :show-inheritance: -.. autoclass:: arrus.interface.UltrasoundInterface - :members: +.. autoclass:: arrus.utils.imaging.EnvelopeDetection + :show-inheritance: -Interactive session -------------------- +.. autoclass:: arrus.utils.imaging.Transpose + :show-inheritance: -A session object allows you to obtain the appropriate device handler. +.. autoclass:: arrus.utils.imaging.ScanConversion + :show-inheritance: -.. autoclass:: arrus.session.InteractiveSession - :members: +.. autoclass:: arrus.utils.imaging.LogCompression + :show-inheritance: -Devices +Logging ------- +.. autofunction:: arrus.set_clog_level -Only the Us4OEM device handler is provided. - -.. autoclass:: arrus.devices.us4oem.Us4OEM - :members: - :noindex: - +.. autofunction:: arrus.add_log_file +The following log severity levels are available: +``arrus.logging.TRACE``, +``arrus.logging.DEBUG``, +``arrus.logging.INFO``, +``arrus.logging.WARNING``, +``arrus.logging.ERROR``, +``arrus.logging.FATAL`` diff --git a/docs/content/examples/mock_session_example.python.rst b/docs/content/examples/mock_session_example.python.rst new file mode 100644 index 000000000..7325ee435 --- /dev/null +++ b/docs/content/examples/mock_session_example.python.rst @@ -0,0 +1,294 @@ +======== +Examples +======== + +Mock session +------------ + +.. code-block:: python + + import numpy as np + import arrus + # import cupy as cp + import matplotlib.pyplot as plt + import h5py + + from arrus.ops.imaging import ( + LinSequence + ) + from arrus.ops.us4r import ( + Pulse + ) + + from arrus.utils.imaging import ( + Pipeline, + BandpassFilter, + QuadratureDemodulation, + Decimation, + RxBeamforming, + EnvelopeDetection, + Transpose, + ScanConversion, + LogCompression + ) + + # Read the dataset do display. + print("Reading data...") + dataset = h5py.File("data.mat", mode="r") + + dataset = { + "rf": np.array(dataset["rf"][:5, :, :, :]), + "sys": dataset["sys"], + "seq": dataset["seq"] + } + print("...done.") + + # Create new session to communicate with the system. + # Session constructor configures all the necessary devices; in case of the mock, + # that means to load data from the provided dataset only. + # A non-mocked session will read a configuration file and create handles + # to the actual devices that should be available to user. + print("Creating session.") + sess = arrus.Session(mock={ + "Us4R:0": dataset + }) + + print("Session created.") + + # Session provides handles to system devices. What devices are available + # depends on the session configuration file. + # We will send you an appropriate session configuration file once you receive + # the us4r-lite hardware. + # The `Us4R` is an us4r lite device. + us4r = sess.get_device("/Us4R:0") + gpu = sess.get_device("/CPU:0") + + # Set HV voltage [0.5*Vpp]; + # maximum value: 90 (can be limited for specific probes in the session + # configuration file). + us4r.set_hv_voltage(30) + + # Tx/Rx sequence to perform on the us4r device. + sequence = LinSequence( + # Transmit a signal for an aperture centered in element 0, 1, ... 191 + # Note: this should not exceed the number of probe elements. + tx_aperture_center_element=np.arange(0, 192), + # The aperture should contain 64 elements. + tx_aperture_size=64, + # The beam should be focused on 30 mm depth. + tx_focus=30e-3, # [m] + # Transmit a sine wave with center frequency 4MHz, 2 periods, no inverse. + pulse=Pulse(center_frequency=4e6, n_periods=2, inverse=False), + # Receive echo data with aperture centered in elements 0, 1, ..., 191 + # Note: rx_aperture_center_element should have the length as + # the tx_aperture_center_element vector. + rx_aperture_center_element=np.arange(0, 192), + # Record data using 64 elements + rx_aperture_size=64, + # Downsampling factor: an integers that divides the output data sampling + # frequency, i.e. the output sampling frequency is + # 65e6/n, where n can be 1, 2, ..., 5. One means no downsampling. + downsampling_factor=1, + # Pulse repetition interval - the time between successive signal transmits. + pri=200e-6, + # Sample range: [start, end) sample + rx_sample_range=(0, 4096), + # Linear TGC curve start value. + tgc_start=14, + # Linear TGC curve slope. + tgc_slope=2e2 + ) + + # Remember to upload th sequence on the us4r device. + # The provided buffer will contain acquired RF data. + # The buffer is a read-only circular queue (only us4r device can write to this + # buffer). + # Currently `us4r.upload` is just a nop. + buffer = us4r.upload(sequence) + + # Output image grid: + x_grid = np.arange(-50, 50, 0.4)*1e-3 + z_grid = np.arange(0, 60, 0.4)*1e-3 + + # Define bmode image reconstruction pipeline. + # You can find source and docstrings of each step in arrus.utils.imaging + # module. + bmode_imaging = Pipeline( + placement=gpu, + steps=( + # Filter the data using bandpass filter, + # default bandwidth: [0.5*fc, 1.5*fc], where fc is center frequency. + # Currently FIR filter is available only. + # The data is filtered along the last axis. + # + # input: nd array. + # output: nd array with the same shape and data type + BandpassFilter(), + # Converts to I/Q samples. + # + # input: nd array + # output: nd array with the same shape and dtype=xp.complex64 + QuadratureDemodulation(), + # Decimate data (CIC filter is also used). + # + # input: nd array + # output: nd array with the last axis `decimation_factor`-times smaller + Decimation(decimation_factor=4, cic_order=2), + # Delay and sum; reconstruct scanlines from the provided echo data. + # + # input: nd array, shape: n_emissions, n_rx, n_samples + # output: nd array, shape: n_emissions, n_samples + RxBeamforming(), + # Extracts envelope from the RF data. + # + # input nd array, dtype=xp.complex64 + # output: nd array, dtype=xp.float32 + EnvelopeDetection(), + # Transpose the provided image. + # + # input: nd array + # output: nd array with the reversed axes + Transpose(), + # Interpolate the RF data to output b-mode image grid. + # + # Note! Currently implemented only for CPU. + # + # input: nd array, shape: n_samples, n_emissions + # output: nd array, shape: len(z_grid), len(x_grid) + ScanConversion(x_grid=x_grid, z_grid=z_grid), + # Convert to decibel scale. + LogCompression() + ) + ) + + # Display data with matplotlib + fig, ax = plt.subplots() + fig.set_size_inches((7, 7)) + ax.set_xlabel("OX") + ax.set_ylabel("OZ") + image_w, image_h = len(x_grid), len(z_grid) + canvas = plt.imshow(np.zeros((image_w, image_h)), vmin=20, vmax=80, cmap="gray") + fig.show() + + # Here starts the data acquisition and processing. + # Starts currently uploaded tx/rx sequence. + us4r.start() + # The buffer is now populated with RF data (and some additional metadata). + + # Get data from the buffer, process and display (100 frames). + for i in range(100): + # Get data and metadata from the buffer. + # buffer.pop copies data from the buffer and returns new numpy ndarray. + # The buffer.pop releases current buffer element. + # Note: Most likely in the futurewe will add a target 'target_device' + # parameter which will allow to copy the RF data directly into GPU memory. + + # To avoid data copying the user can use a pair of instructions: + # - buffer.tail() (returns a numpy array that wraps a pointer to the memory + # area with data acquired by the the us4r-lite device) + # - buffer.release_tail() (notify the us4r-lite device that the + # data is not needed anymore and memory area can be reused by the + # us4r-lite device for the next acquisitions) + print("Acquiring data") + data, metadata = buffer.tail() + + # The metadata structure contains all the information necessary to + # reconstruct b-mode image from the RF data + # (e.g. probe's pitch, tx aperture position, etc.). + # You can find the source and docstrings of the metadata in + # arrus.metadata module. + if i == 0: + # Data acquisition context is constant after starting the us4r.device + # (you have to stop the device if you want e.g. change some lin sequence + # parameters), thus metadata.context field + # is constant; + # + # The metadata.context can be saved after acquiring the first frame; + # then you can ignore this field for consecutive fields. + print(metadata.context) + print(metadata.data_description) + + # process + # gpu_data = cp.asarray(data) + # We've just copied the data from the us4r-lite buffer, we can release + # the current buffer element. + buffer.release_tail() + + # Reconstruct bmode image. + # Note: metadata.data_description describes data produced at a given step; + # e.g. metadata.data_description.sampling_frequency can change after + # `Decimation` operation. + bmode, metadata = bmode_imaging(data, metadata) + # display + canvas.set_data(bmode) + ax.set_aspect("auto") + fig.canvas.flush_events() + plt.draw() + print(f"Custom metadata: {metadata.custom}") + + # Stop the execution of the tx/rx sequence. + us4r.stop() + + +Classical beamforming +--------------------- + +.. code-block:: python + + seq = LinSequence( + tx_aperture_center_element=np.arange(7, 182), + tx_aperture_size=64, + tx_focus=30e-3, + pulse=Pulse(center_frequency=5e6, n_periods=3.5, inverse=False), + rx_aperture_center_element=np.arange(7, 182), + rx_aperture_size=64, + rx_sample_range=(0, 4096), + pri=100e-6, + downsampling_factor=1, + tgc_start=14, + tgc_slope=2e2, + speed_of_sound=1490) + + bmode_imaging = Pipeline( + placement=gpu, + steps=( + BandpassFilter(), + QuadratureDemodulation(), + Decimation(decimation_factor=4, cic_order=2), + RxBeamforming(), + EnvelopeDetection(), + Transpose(), + ScanConversion(x_grid=x_grid, z_grid=z_grid), + LogCompression())) + + # Here starts communication with the device. + session = arrus.session.Session("cfg.prototxt") + + n = 100 + + us4r = session.get_device("/Us4R:0") + gpu = session.get_device("/GPU:0") + + # Set the pipeline to be executed on the GPU + bmode_imaging.set_placement(gpu) + # Set initial voltage on the us4r-lite device. + us4r.set_hv_voltage(30) + # Upload sequence on the us4r-lite device. + buffer = us4r.upload(seq, mode="sync") + + # Start the device. + us4r.start() + times = [] + arrus.logging.log(arrus.logging.INFO, f"Running {n} iterations.") + for i in range(n): + start = time.time() + data, metadata = buffer.tail() + if action_func is not None: + action_func(i, data, metadata) + buffer.release_tail() + times.append(time.time()-start) + + arrus.logging.log(arrus.logging.INFO, + f"Done, average acquisition + processing time: {np.mean(times)} [s]") + + us4r.stop() diff --git a/docs/content/examples/single_module.python.rst b/docs/content/examples/single_module.python.rst deleted file mode 100644 index 922fcabbc..000000000 --- a/docs/content/examples/single_module.python.rst +++ /dev/null @@ -1,155 +0,0 @@ -Communicating with the Us4OEM -========================= - -Acquiring echo signal data requires these three steps: - -1. configure a `session` with the Us4OEM device, -2. define operations (ops) that should be executed by the device, -3. execute operations within the session. - -All three steps are described below. - -Configuring session -------------------- - -User communicates with the Us4OEM device in a single `session`. -Session is an abstract object that represents a connection between client's -programming interface and the device. The session should be configured before -starting. In particular, it is essential to provide: - -- a description of the system which the user wants to connect to, -- Us4OEM device initialization parameters. - -The system description should be provided as an instance of the -:class:`arrus.CustomUs4RCfg` class. - -.. code-block:: python - - # US4R-LITE CONFIGURATION - system_cfg = CustomUs4RCfg( - n_us4oems=2, - is_hv256=True - ) - -Us4OEM initialization parameters should be set using the :class:`arrus.Us4OEMCfg` -class, e.g: - -.. code-block:: python - - us4oem_cfg = Us4OEMCfg( - channel_mapping="esaote", - active_channel_groups=[1]*16, - dtgc=0, - active_termination=200, - log_transfer_time=True - ) - -Finally, you can prepare a :class:`arrus.SessionCfg`: - -.. code-block:: python - - session_cfg = SessionCfg( - system=system_cfg, - devices={ - "Us4OEM:0": us4oem_cfg, - } - ) - - -Defining operations to perform ------------------------------- - -In ARRUS, the user defines the **operations** that will be executed on a particular -**device**. - -Us4OEM modules implement the :class:`arrus.ops.TxRx` operation, -that is, a single transmit and echo signal reception. The result of this -operation is stored directly in the module's DDR memory, and then transferred -to the PC for further processing. A single ``TxRx`` allows to transmit a signal -impulse using 128 channels at most and to receive echo data using a maximum of 32 -channels. - -A single ``TxRx`` operation is limited by the module's maximum number of Rx channels. -In most cases a :class:`arrus.ops.Sequence` of ``TxRx`` operations will be -desired. A single ``Sequence`` allows to execute a given collection of -``TxRx`` operations, store all acquired data in the module's DDR memory, -then transfer it to the computer's memory. For example, a ``Sequence`` of 4 -``TxRx`` operations with a shifted Rx aperture (stride 32) allows to acquire -data using 128 Rx channels: - -.. code-block:: python - - operations = [] - for i in range(4): - tx = Tx(excitation=SineWave(frequency=8.125e6, n_periods=1.5, - inverse=False), - aperture=RegionBasedAperture(origin=0, size=128), - pri=200e-6) - rx = Rx(n_samples=8192, - aperture=RegionBasedAperture(origin=i*32, size=32)) - tx_rx = TxRx(tx, rx) - operations.append(tx_rx) - tx_rx_sequence = Sequence(operations) - -In real-time imaging, the user would likely wish to execute a given operation -in a loop, until the system is explicitly stopped. In this case -:class:`arrus.ops.Loop` should be used. This operation repeats a given -``Sequence`` of ``TxRx`` ops, until the loop is explicitly stopped. -After each execution of the ``Sequence``, a ``callback`` function is called, -with the RF data provided as input. If the acquisition is to continue, -the ``callback`` function should return ``True``, ``False`` otherwise. - -.. code-block:: python - - def callback(data): - print("New data!") - return True - - sequence_loop = Loop(tx_rx_sequence) - - -Running operations ------------------ - -Operations can be executed within a :class:`arrus.Session`. - -In particular, to run the sequence of 4 ``TxRx`` operations: - -.. code-block:: python - - with arrus.Session(cfg=session_cfg) as sess: - us4oem = sess.get_device("/Us4OEM:0") - data = sess.run(tx_rx_sequence, feed_dict={'device': us4oem}) - -Please note that ``Session`` is a `python context manager class` with the -following semantic: when the context (an indented block of code) ends, all -running devices are stopped and the session is closed. - -A parameter ``feed_dict`` allows to fill the executed operation placeholders -with specific values. An example of such a placeholder is a ``device`` on which -the operation should be executed. The ``Loop`` operation requires an additional -feed value, the ``callback`` function, that should be called when data -acquisition is finished. - -.. code-block:: python - - with arrus.Session(cfg=session_cfg) as sess: - us4oem = sess.get_device("/Us4OEM:0") - sess.run(sequence_loop, feed_dict={'device': us4oem, - 'callback': callback} - - -Examples --------- - -The following examples are available in the ``python\examples\us4oem`` directory: - -- ``us4oem_x1_pwi_single.py``: using Us4OEM to transmit a single plane \ - wave and acquire echo data. -- ``us4oem_x1_sta_single.py``: using Us4OEM to perform a single STA sequence \ -- ``us4oem_x1_sta_multiple.py``: using Us4OEM to perform the STA sequence multiple\ - times; saves acquired RF data to a ``numpy`` file with a given frequency. -- ``us4oem_x1_sta_old_api.py``: an example using the old, legacy API. - -All examples require the ``matplotlib`` package to be installed. - diff --git a/docs/content/glossary.rst b/docs/content/glossary.rst new file mode 100644 index 000000000..f94103e60 --- /dev/null +++ b/docs/content/glossary.rst @@ -0,0 +1,11 @@ +Definitions: + +A **frame** is an output of a single Tx/Rx operation. +An example of a single frame is a 2-D frame which will produce a single scanline for linear scanning scheme. + +A **sequence of frames** (in short: **sequence**) is an output of a sequence of Tx/Rx operations. +An example of a sequence is 3-D array with shape (frame, sample, channel), from which a single b-mode image can be reconstructed. + +A **batch of sequences** (in short: a **batch**) is a collection of multiple sequences. + +An example of batch is a 4-D array with shape (sequence, frame, sample, channel), which can be used in a Doppler estimation methods. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a6c949fe1..56e716551 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,8 +8,8 @@ Welcome to ARRUS documentation! content/introduction content/installation/index - content/examples/single_module content/examples/bmode_imaging + content/examples/mock_session_example content/api .. toctree:: diff --git a/scripts/build.py b/scripts/build.py index 837671070..d662af2e2 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -21,15 +21,18 @@ def main(): parser.add_argument("--source_dir", dest="source_dir", type=str, required=False, default=os.environ.get(SRC_ENVIRON, None)) - parser.add_argument("--us4r_dir", dest="us4r_dir", type=str, required=False, default=None) + parser.add_argument("--verbose", dest="verbose", + required=False, default=False, + action="store_true") args = parser.parse_args() configuration = args.config src_dir = args.source_dir us4r_dir = args.us4r_dir + verbose = args.verbose if src_dir is None: raise ValueError("%s environment variable should be declared " @@ -38,12 +41,27 @@ def main(): build_dir = os.path.join(src_dir, "build") - cmake_cmd = [ + cmake_cmd = [] + join_cmd = False + if os.name == "nt": + join_cmd = True + cmd = os.path.join(build_dir, 'activate.bat') + cmake_cmd += [f'"{cmd}"', "&&"] + pass + else: + join_cmd = False + shell_source(f"{os.path.join(build_dir, 'activate.sh')}") + + build_dir = f'"{build_dir}"' + cmake_cmd += [ "cmake", "--build", build_dir, "--config", configuration ] + if verbose: + cmake_cmd += ["--verbose"] + print("Calling: %s" % (" ".join(cmake_cmd))) current_env = os.environ.copy() @@ -51,6 +69,8 @@ def main(): current_env["PATH"] = os.path.join(us4r_dir, "lib64") +os.pathsep + current_env["PATH"] print(f"Calling with Path {current_env['PATH']}") + if join_cmd: + cmake_cmd = " ".join(cmake_cmd) process = subprocess.Popen(cmake_cmd, env=current_env) process.wait() return_code = process.returncode @@ -62,5 +82,16 @@ def main(): assert_no_error(result) +def shell_source(script): + # Credits: + # https://stackoverflow.com/questions/7040592/calling-the-source-command-from-subprocess-popen#answer-12708396 + pipe = subprocess.Popen(". %s; env" % script, stdout=subprocess.PIPE, shell=True) + output = pipe.communicate()[0] + env = (line.decode("utf-8") for line in output.splitlines()) + env = (line.split("=", 1) for line in env) + env = (var for var in env if len(var) == 2) # // leave correct pairs only + env = dict(env) + os.environ.update(env) + if __name__ == "__main__": main() diff --git a/scripts/cfg_build.py b/scripts/cfg_build.py index 63a52ca11..132e536e1 100644 --- a/scripts/cfg_build.py +++ b/scripts/cfg_build.py @@ -20,7 +20,7 @@ def assert_no_error(return_code): def main(): parser = argparse.ArgumentParser(description="Configures build system.") parser.add_argument("--targets", dest="targets", - type=str, nargs="+", required=True) + type=str, nargs="*", required=False) parser.add_argument("--run_targets", dest="run_targets", type=str, nargs="*", required=False) parser.add_argument("--src_branch_name", dest="src_branch_name", type=str, @@ -38,10 +38,12 @@ def main(): run_targets = args.run_targets extra_options = args.options src_branch_name = args.src_branch_name - options = ["-DARRUS_BUILD_%s=ON" % target.upper() for target in targets] + options = [] + if targets is not None: + options += ["-DARRUS_BUILD_%s=ON" % target.upper() for target in targets] if run_targets is not None: options += ["-DARRUS_RUN_%s=ON" % t.upper() for t in run_targets] - options += ["-D%s" % o.upper() for o in extra_options] + options += ["-D%s" % o for o in extra_options] src_dir = args.source_dir us4r_install_dir = args.us4r_dir @@ -61,7 +63,12 @@ def main(): shutil.rmtree(build_dir, ignore_errors=True) os.makedirs(build_dir) - cmake_generator = "" + # Conan install. + cmd = ["conan", "install", src_dir, "-if", build_dir] + result = subprocess.call(cmd) + assert_no_error(result) + + # Cmake cfg generator. if os.name == "nt": cmake_generator = "Visual Studio 15 2017 Win64" else: diff --git a/scripts/test.py b/scripts/test.py index 153c3c163..2e9fdd035 100644 --- a/scripts/test.py +++ b/scripts/test.py @@ -32,7 +32,7 @@ def main(): os.chdir(build_dir) cmake_cmd = [ - "ctest", "-C", config + "ctest", "-C", config, "--verbose" ] print("Calling: %s" % (" ".join(cmake_cmd)))