diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfcc4a0..d6b2d25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,9 +22,9 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: actions/github-script@v8 + - uses: actions/github-script@v9 with: script: | core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); diff --git a/.github/workflows/build_binaries.yml b/.github/workflows/build_binaries.yml index 9b054a0..c0477b5 100644 --- a/.github/workflows/build_binaries.yml +++ b/.github/workflows/build_binaries.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 fetch-tags: true @@ -97,7 +97,7 @@ jobs: path: release_assets - name: Upload assets to GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: | release_assets/**/*.{deb,rpm,tar.gz,zip} diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 36d5fea..870ad38 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -34,12 +34,12 @@ jobs: - [windows-latest, AMD64, win_amd64, 0.0] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 # need git tags available for setuptools_scm to grab tags with: fetch-depth: 0 - - uses: actions/github-script@v8 + - uses: actions/github-script@v9 with: script: | core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); @@ -59,7 +59,7 @@ jobs: - name: Build Windows wheels if: runner.OS == 'Windows' - uses: pypa/cibuildwheel@v3.4.0 + uses: pypa/cibuildwheel@v4.1.1 with: package-dir: ${{github.workspace}}/python config-file: ${{github.workspace}}/python/pyproject.toml @@ -76,7 +76,7 @@ jobs: startswith(matrix.build-platform[2], 'manylinux_2_28') && ! ( startswith(matrix.build-platform[1], 'aarch64') && !startsWith(github.ref, 'refs/tags/') ) - uses: pypa/cibuildwheel@v3.4.0 + uses: pypa/cibuildwheel@v4.1.1 with: package-dir: ./python config-file: ./python/pyproject.toml @@ -107,7 +107,7 @@ jobs: - name: Build macOS wheels if: runner.os == 'macOS' - uses: pypa/cibuildwheel@v3.4.0 + uses: pypa/cibuildwheel@v4.1.1 with: package-dir: ./python config-file: ./python/pyproject.toml diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 3dc067e..1f8ad63 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 # need git tags available to get git version with: fetch-depth: 0 @@ -33,7 +33,7 @@ jobs: echo "commit_name=$GIT_COMMIT_NAME" >> $GITHUB_OUTPUT - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: 3.x diff --git a/.gitignore b/.gitignore index d2a5112..fbefb73 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,11 @@ build/ .zed/ .cache/ python/build/ +python/src/clibtokamap*.so +python/src/libtokamap/__pycache__/ +python/src/libtokamap/_version.py python/src/libtokamap.egg-info +python/tests/__pycache__/ python/dist/ python/setup.py .clangd diff --git a/CMakeLists.txt b/CMakeLists.txt index 89bde80..f547ef9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,12 +16,14 @@ project( LANGUAGES CXX ) -set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set( CMAKE_POSITION_INDEPENDENT_CODE ON ) if( "${PROJECT_SOURCE_DIR}" STREQUAL "${PROJECT_BINARY_DIR}" ) - message( SEND_ERROR "In-source builds are not allowed." ) + message( SEND_ERROR "In-source builds are not allowed." ) endif() +# find_package( xtensor REQUIRED ) + # Set project options include( StaticAnalyzers ) include( Utils ) @@ -34,18 +36,18 @@ option( ENABLE_DEBIAN_PACKAGING "Configure install files for debian packaging" O ## Generators if( UNIX AND NOT APPLE ) - set( DEFAULT_CPACK_GENERATORS "DEB;RPM;TGZ" ) + set( DEFAULT_CPACK_GENERATORS "DEB;RPM;TGZ" ) elseif( APPLE ) - set( DEFAULT_CPACK_GENERATORS "TGZ;ZIP" ) + set( DEFAULT_CPACK_GENERATORS "TGZ;ZIP" ) else() - set( DEFAULT_CPACK_GENERATORS "ZIP" ) + set( DEFAULT_CPACK_GENERATORS "ZIP" ) endif() set( ENABLE_CPACK_GENERATORS "${DEFAULT_CPACK_GENERATORS}" CACHE STRING "enable the cpack generators" ) add_clang_format_target() if( ENABLE_TESTING ) - enable_testing() + enable_testing() endif() # Specify C++ standard for all targets @@ -69,9 +71,9 @@ set( UTIL_SOURCES src/utils/typed_data_array.cpp ) if( MSVC ) - list( APPEND UTIL_SOURCES src/utils/windows_utils.cpp ) + list( APPEND UTIL_SOURCES src/utils/windows_utils.cpp ) else() - list( APPEND UTIL_SOURCES src/utils/unix_utils.cpp ) + list( APPEND UTIL_SOURCES src/utils/unix_utils.cpp ) endif() set( MAP_TYPE_SOURCES @@ -79,6 +81,7 @@ set( MAP_TYPE_SOURCES src/map_types/data_source_mapping.cpp src/map_types/dim_mapping.cpp src/map_types/expr_mapping.cpp + src/map_types/interp_mapping.cpp src/map_types/value_mapping.cpp ) @@ -114,6 +117,7 @@ set( MAP_TYPE_HEADERS src/map_types/data_source_mapping.hpp src/map_types/dim_mapping.hpp src/map_types/expr_mapping.hpp + src/map_types/interp_mapping.hpp src/map_types/map_arguments.hpp src/map_types/value_mapping.hpp ) @@ -141,7 +145,7 @@ set( EXT_HEADERS ) if( "${PROJECT_VERSION_TWEAK}" STREQUAL "" ) - set( PROJECT_VERSION_TWEAK 0 ) + set( PROJECT_VERSION_TWEAK 0 ) endif() configure_file( @@ -165,10 +169,10 @@ configure_file( add_library( libtokamap ${SOURCES} ) if( MSVC ) - target_compile_options( libtokamap PRIVATE /W4 /WX /bigobj /utf-8 ) - target_link_libraries( libtokamap PRIVATE Dbghelp ) + target_compile_options( libtokamap PRIVATE /W4 /WX /bigobj /utf-8 ) + target_link_libraries( libtokamap PRIVATE Dbghelp ) else() - target_compile_options( libtokamap PRIVATE -Wall -Wextra -Wpedantic -Werror ) + target_compile_options( libtokamap PRIVATE -Wall -Wextra -Wpedantic -Werror ) endif() target_include_directories( libtokamap @@ -184,7 +188,7 @@ target_include_directories( libtokamap $ ) if( ENABLE_PROFILING ) - target_compile_definitions( libtokamap PRIVATE LIBTOKAMAP_PROFILE_ENABLED ) + target_compile_definitions( libtokamap PRIVATE LIBTOKAMAP_PROFILE_ENABLED ) endif() target_sources( libtokamap PUBLIC FILE_SET HEADERS @@ -241,14 +245,14 @@ install( FILES # Install documentation if( ENABLE_DEBIAN_PACKAGING ) - add_custom_command( + add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/changelog" COMMAND ./scripts/git2debchangelog.sh | grep -v -e 'Merge branch' -e 'Merge pull' > ${CMAKE_CURRENT_BINARY_DIR}/changelog WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" COMMENT "Generating changelog" ) - add_custom_command( + add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" COMMAND gzip -cn9 changelog > changelog.gz WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" @@ -256,20 +260,20 @@ if( ENABLE_DEBIAN_PACKAGING ) COMMENT "Compressing changelog" ) - add_custom_target( changelog_gz ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" ) + add_custom_target( changelog_gz ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" ) - install( FILES + install( FILES "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/libtokamap-dev" ) - install( FILES + install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/libtokamap-dev" RENAME copyright ) else( ENABLE_DEBIAN_PACKAGING ) - install( FILES + install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/README.md DESTINATION "${CMAKE_INSTALL_DOCDIR}" @@ -278,11 +282,11 @@ endif( ENABLE_DEBIAN_PACKAGING ) # Unit testing setup if( ENABLE_TESTING ) - add_subdirectory( test ) + add_subdirectory( test ) endif() if( ENABLE_EXAMPLES ) - add_subdirectory( examples/simple_mapper ) + add_subdirectory( examples/simple_mapper ) endif() list( TRANSFORM SOURCES PREPEND ${CMAKE_CURRENT_LIST_DIR}/ ) diff --git a/examples/simple_mapper/mappings/example_v1/globals.json b/examples/simple_mapper/mappings/example_v1/globals.json index 68fb740..88cafa2 100644 --- a/examples/simple_mapper/mappings/example_v1/globals.json +++ b/examples/simple_mapper/mappings/example_v1/globals.json @@ -1,7 +1,7 @@ { - "DATA_SOURCE_CONFIG": { + "data_source_config": { "JSON": { - "ARGS": { + "args": { "file_name": "data_{{ shot }}.json" } } diff --git a/examples/simple_mapper/mappings/example_v1/magnetics/40/mappings.json b/examples/simple_mapper/mappings/example_v1/magnetics/40/mappings.json index 8cf79f3..e61fd46 100644 --- a/examples/simple_mapper/mappings/example_v1/magnetics/40/mappings.json +++ b/examples/simple_mapper/mappings/example_v1/magnetics/40/mappings.json @@ -2,91 +2,91 @@ "version": "1.0.0", "coil": { "map_type": "DIMENSION", - "DIM_PROBE": "_coils" + "dim_probe": "_coils" }, "_coils": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils" } }, "coil[#]/name": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils/{{ #0 }}/name" } }, "_coil[#]/radius": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils/{{ #0 }}/radius" } }, "coil[#]/area": { - "MAP_TYPE": "EXPR", - "EXPR": "pi * r^2", - "PARAMETERS": { + "map_type": "EXPR", + "expr": "pi * r^2", + "parameters": { "r": "_coil[#]/radius" } }, "coil[#]/position": { - "MAP_TYPE": "DIMENSION", - "DIM_PROBE": "_coil[#]/position" + "map_type": "DIMENSION", + "dim_probe": "_coil[#]/position" }, "_coil[#]/position": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils/{{ #0 }}/position" } }, "coil[#]/position[#]/r": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils/{{ #0 }}/position/{{ #1 }}/0" } }, "coil[#]/position[#]/z": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils/{{ #0 }}/position/{{ #1 }}/1" } }, "coil[#]/flux/time": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils/{{ #0 }}/flux/time" } }, "coil[#]/flux/data": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils/{{ #0 }}/flux/data" } }, "coil[#]/flux/data_scaled": { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": { "signal": "coils/{{ #0 }}/flux/data" }, - "SCALE": 2.0 + "scale": 2.0 }, "coil[#]/flux/dot_product": { - "MAP_TYPE": "CUSTOM", - "LIBRARY": "custom", - "FUNCTION": "dot_product", - "INPUTS": { + "map_type": "CUSTOM", + "library": "custom", + "function": "dot_product", + "inputs": { "lhs": "coil[#]/flux/time", "rhs": "coil[#]/flux/data" }, - "PARAMETERS": {} + "parameters": {} } } diff --git a/examples/simple_mapper/mappings/example_v1/mappings.cfg.json b/examples/simple_mapper/mappings/example_v1/mappings.cfg.json index f644dac..d80df45 100644 --- a/examples/simple_mapper/mappings/example_v1/mappings.cfg.json +++ b/examples/simple_mapper/mappings/example_v1/mappings.cfg.json @@ -4,6 +4,6 @@ "author": "John Doe", "version": "1.0.1" }, - "partitions": [{ "attribute": "shot", "selector": "max_below" }], + "partitions": [{ "attribute": "shot", "selector": "MAX_BELOW" }], "groups": ["magnetics"] } diff --git a/examples/simple_mapper/schemas/globals.schema.json b/examples/simple_mapper/schemas/globals.schema.json index b7a0124..6cd21f8 100644 --- a/examples/simple_mapper/schemas/globals.schema.json +++ b/examples/simple_mapper/schemas/globals.schema.json @@ -4,11 +4,11 @@ "description": "JSON Schema for v1.0 of mapping globals", "type": "object", "properties": { - "DATA_SOURCE_CONFIG": { + "data_source_config": { "type": "object", "patternProperties": { "^.*$": { - "ARGS": { "$ref": "#/$defs/args" } + "args": { "$ref": "#/$defs/args" } } } } diff --git a/examples/simple_mapper/schemas/mappings.cfg.schema.json b/examples/simple_mapper/schemas/mappings.cfg.schema.json index 822055d..6b12223 100644 --- a/examples/simple_mapper/schemas/mappings.cfg.schema.json +++ b/examples/simple_mapper/schemas/mappings.cfg.schema.json @@ -37,7 +37,7 @@ "type": "object", "properties": { "attribute": { "type": "string" }, - "selector": { "enum": ["max_below", "min_above", "exact", "closest"] } + "selector": { "enum": ["MAX_BELOW", "MIN_ABOVE", "EXACT", "CLOSEST"] } }, "additionalProperties": false, "required": ["attribute", "selector"] diff --git a/examples/simple_mapper/schemas/mappings.schema.json b/examples/simple_mapper/schemas/mappings.schema.json index b91bacf..5f4cd6c 100644 --- a/examples/simple_mapper/schemas/mappings.schema.json +++ b/examples/simple_mapper/schemas/mappings.schema.json @@ -27,26 +27,26 @@ }, "dimension": { "properties": { - "MAP_TYPE": { "const": "DIMENSION" }, - "DIM_PROBE": { "type": "string" }, - "COMMENT": { "type": "string" } + "map_type": { "const": "DIMENSION" }, + "dim_probe": { "type": "string" }, + "comment": { "type": "string" } }, - "required": ["MAP_TYPE", "DIM_PROBE"], + "required": ["map_type", "dim_probe"], "additionalProperties": false }, "value": { "properties": { - "MAP_TYPE": { "const": "VALUE" }, - "VALUE": { "type": ["number", "string", "array", "object"] }, - "COMMENT": { "type": "string" } + "map_type": { "const": "VALUE" }, + "value": { "type": ["number", "string", "array"] }, + "comment": { "type": "string" } }, - "required": ["MAP_TYPE", "VALUE"], + "required": ["map_type", "value"], "additionalProperties": false }, "data_source": { "properties": { - "MAP_TYPE": { "const": "DATA_SOURCE" }, - "ARGS": { + "map_type": { "const": "DATA_SOURCE" }, + "args": { "type": "object", "patternProperties": { "^.*$": { @@ -54,19 +54,19 @@ } } }, - "DATA_SOURCE": { "type": "string" }, - "OFFSET": { "type": ["string", "number"] }, - "SCALE": { "type": ["string", "number"] }, - "SLICE": { "type": "string" }, - "COMMENT": { "type": "string" } + "data_source": { "type": "string" }, + "offset": { "type": ["string", "number"] }, + "scale": { "type": ["string", "number"] }, + "slice": { "type": "string" }, + "comment": { "type": "string" } }, - "required": ["MAP_TYPE", "ARGS", "DATA_SOURCE"], + "required": ["map_type", "args", "data_source"], "additionalProperties": false }, "expression": { "properties": { - "MAP_TYPE": { "const": "EXPR" }, - "PARAMETERS": { + "map_type": { "const": "EXPR" }, + "parameters": { "type": "object", "patternProperties": { "^.*$": { @@ -74,18 +74,18 @@ } } }, - "EXPR": { "type": "string" }, - "COMMENT": { "type": "string" } + "expr": { "type": "string" }, + "comment": { "type": "string" } }, - "required": ["MAP_TYPE", "PARAMETERS", "EXPR"], + "required": ["map_type", "parameters", "expr"], "additionalProperties": false }, "custom": { "properties": { - "MAP_TYPE": { "const": "CUSTOM" }, - "LIBRARY": { "type": "string" }, - "FUNCTION": { "type": "string" }, - "INPUTS": { + "map_type": { "const": "CUSTOM" }, + "library": { "type": "string" }, + "function": { "type": "string" }, + "inputs": { "type": "object", "patternProperties": { "^.*$": { @@ -93,10 +93,10 @@ } } }, - "PARAMETERS": { "type": "object" }, - "COMMENT": { "type": "string" } + "parameters": { "type": "object" }, + "comment": { "type": "string" } }, - "required": ["MAP_TYPE", "LIBRARY", "FUNCTION", "INPUTS", "PARAMETERS"], + "required": ["map_type", "library", "function", "inputs", "parameters"], "additionalProperties": false } } diff --git a/include/libtokamap.hpp b/include/libtokamap.hpp index d382393..712d219 100644 --- a/include/libtokamap.hpp +++ b/include/libtokamap.hpp @@ -6,6 +6,7 @@ #include // IWYU pragma: export. #include // IWYU pragma: export. #include // IWYU pragma: export. +#include // IWYU pragma: export. #include // IWYU pragma: export. #include // IWYU pragma: export. #include // IWYU pragma: export. diff --git a/python/pyproject.toml b/python/pyproject.toml index 2e8e28a..0e2bf76 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" license = "MIT" requires-python = ">=3.11" dependencies = [ - "tokamap>=0.1.0", + "tokamap>=0.3.2", "numpy>=2.0", ] keywords = [ @@ -59,6 +59,11 @@ version_file = "src/libtokamap/_version.py" version_scheme = "post-release" local_scheme = "no-local-version" +[tool.pytest.ini_options] +markers = [ + "cache_regression: cache behavior regressions that document known failures on develop", +] + [tool.cibuildwheel] test-requires = ["pytest>=8"] test-command = "pytest {package}/tests -q" diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index 4fee4f8..ab6013c 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -24,6 +24,39 @@ def get(self, args: dict[str, str]) -> np.ndarray: raise ValueError(f"failed for {args['signal']}") +class RecordingDataSource(libtokamap.DataSource): + def __init__(self) -> None: + self.calls: list[dict[str, Any]] = [] + + def get(self, args: dict[str, Any]) -> np.ndarray: + self.calls.append(dict(args)) + return np.array([args["shot"]], dtype=np.int64) + + +class RuntimeAttributeDataSource(libtokamap.DataSource): + def get(self, args: dict[str, Any]) -> np.ndarray: + return np.array([args["run_id"]], dtype=np.int64) + + +class ArrayRecordingDataSource(libtokamap.DataSource): + def __init__(self) -> None: + self.calls: list[dict[str, Any]] = [] + + def get(self, args: dict[str, Any]) -> np.ndarray: + self.calls.append(dict(args)) + return np.array([10.0, 20.0, 30.0], dtype=np.float64) + + +class ConstantRecordingDataSource(libtokamap.DataSource): + def __init__(self, value: int) -> None: + self.value = value + self.calls: list[dict[str, Any]] = [] + + def get(self, args: dict[str, Any]) -> np.ndarray: + self.calls.append(dict(args)) + return np.array([self.value], dtype=np.int64) + + def int_codes(_inputs: dict[str, libtokamap.MappedValue], _params: dict[str, Any]) -> np.ndarray: return np.array([1, 2, 3], dtype=np.int32) @@ -146,16 +179,315 @@ def test_python_data_source_errors_are_wrapped(config_path: Path) -> None: mapper.map("example", "magnetics/coil", {"shot": 42}) +def test_python_data_source_receives_runtime_attributes( + config_path: Path, tmp_path: Path +) -> None: + mapping_dir = copy_example_mappings(tmp_path) + globals_path = mapping_dir / "example_v1/globals.json" + globals_json = json.loads(globals_path.read_text(encoding="utf-8")) + globals_json["DEG2RAD"] = 0.0174532925199 + globals_path.write_text(json.dumps(globals_json), encoding="utf-8") + + mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" + mappings = json.loads(mappings_path.read_text(encoding="utf-8")) + mappings["runtime_attrs"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "runtime_attrs"}, + } + mappings["explicit_args_override_runtime_attrs"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "runtime_attrs", "shot": 7}, + } + mappings_path.write_text(json.dumps(mappings), encoding="utf-8") + + write_config(config_path, mapping_dir, REPO_ROOT / "examples/simple_mapper/schemas") + data_source = RecordingDataSource() + mapper = libtokamap.Mapper(str(config_path)) + mapper.register_python_data_source("JSON", data_source) + mapper.register_custom_function("custom", "dot_product", dot_product) + + runtime_result = mapper.map("example", "magnetics/runtime_attrs", {"shot": 42}) + override_result = mapper.map( + "example", "magnetics/explicit_args_override_runtime_attrs", {"shot": 42} + ) + + np.testing.assert_array_equal(runtime_result, np.array([42], dtype=np.int64)) + np.testing.assert_array_equal(override_result, np.array([7], dtype=np.int64)) + assert data_source.calls[0]["shot"] == 42 + assert data_source.calls[0]["signal"] == "runtime_attrs" + assert "DEG2RAD" not in data_source.calls[0] + assert data_source.calls[1]["shot"] == 7 + + +@pytest.mark.cache_regression +@pytest.mark.skip(reason="skipping until caching is reworked") +def test_cache_distinguishes_indices_and_reuses_raw_data_source_fetch( + config_path: Path, tmp_path: Path +) -> None: + mapping_dir = copy_example_mappings(tmp_path) + mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" + mappings = json.loads(mappings_path.read_text(encoding="utf-8")) + mappings["cache_probe[#]"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "cache_probe_indexed"}, + "slice": "[{{ #0 }}]", + } + mappings_path.write_text(json.dumps(mappings), encoding="utf-8") + + config_path.write_text( + "\n".join( + [ + f'mapping_directory = "{mapping_dir.as_posix()}"', + f'schemas_directory = "{(REPO_ROOT / "examples/simple_mapper/schemas").as_posix()}"', + "cache_enabled = true", + ] + ), + encoding="utf-8", + ) + + data_source = ArrayRecordingDataSource() + mapper = libtokamap.Mapper(str(config_path)) + mapper.register_python_data_source("JSON", data_source) + mapper.register_custom_function("custom", "dot_product", dot_product) + + first = mapper.map("example", "magnetics/cache_probe[0]", {"shot": 42}) + second = mapper.map("example", "magnetics/cache_probe[1]", {"shot": 42}) + + assert first.item() == pytest.approx(10.0) + assert second.item() == pytest.approx(20.0) + assert len(data_source.calls) == 1 + + +@pytest.mark.cache_regression +@pytest.mark.skip(reason="skipping until caching is reworked") +def test_repeated_identical_data_source_request_reuses_raw_fetch( + config_path: Path, tmp_path: Path +) -> None: + mapping_dir = copy_example_mappings(tmp_path) + mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" + mappings = json.loads(mappings_path.read_text(encoding="utf-8")) + mappings["cache_probe"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "cache_probe_repeated"}, + } + mappings_path.write_text(json.dumps(mappings), encoding="utf-8") + + config_path.write_text( + "\n".join( + [ + f'mapping_directory = "{mapping_dir.as_posix()}"', + f'schemas_directory = "{(REPO_ROOT / "examples/simple_mapper/schemas").as_posix()}"', + "cache_enabled = true", + ] + ), + encoding="utf-8", + ) + + data_source = ArrayRecordingDataSource() + mapper = libtokamap.Mapper(str(config_path)) + mapper.register_python_data_source("JSON", data_source) + mapper.register_custom_function("custom", "dot_product", dot_product) + + first = mapper.map("example", "magnetics/cache_probe", {"shot": 42}) + second = mapper.map("example", "magnetics/cache_probe", {"shot": 42}) + + np.testing.assert_array_equal(first, np.array([10.0, 20.0, 30.0])) + np.testing.assert_array_equal(second, np.array([10.0, 20.0, 30.0])) + assert len(data_source.calls) == 1 + + +@pytest.mark.cache_regression +@pytest.mark.skip(reason="skipping until caching is reworked") +def test_sibling_mappings_with_same_data_source_args_reuse_raw_fetch( + config_path: Path, tmp_path: Path +) -> None: + mapping_dir = copy_example_mappings(tmp_path) + mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" + mappings = json.loads(mappings_path.read_text(encoding="utf-8")) + mappings["cache_probe_raw"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "cache_probe_sibling"}, + } + mappings["cache_probe_scaled"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "cache_probe_sibling"}, + "scale": 2.0, + } + mappings_path.write_text(json.dumps(mappings), encoding="utf-8") + + config_path.write_text( + "\n".join( + [ + f'mapping_directory = "{mapping_dir.as_posix()}"', + f'schemas_directory = "{(REPO_ROOT / "examples/simple_mapper/schemas").as_posix()}"', + "cache_enabled = true", + ] + ), + encoding="utf-8", + ) + + data_source = ArrayRecordingDataSource() + mapper = libtokamap.Mapper(str(config_path)) + mapper.register_python_data_source("JSON", data_source) + mapper.register_custom_function("custom", "dot_product", dot_product) + + raw = mapper.map("example", "magnetics/cache_probe_raw", {"shot": 42}) + scaled = mapper.map("example", "magnetics/cache_probe_scaled", {"shot": 42}) + + np.testing.assert_array_equal(raw, np.array([10.0, 20.0, 30.0])) + np.testing.assert_array_equal(scaled, np.array([20.0, 40.0, 60.0])) + assert len(data_source.calls) == 1 + + +def test_cache_disabled_repeated_data_source_request_fetches_each_time( + config_path: Path, tmp_path: Path +) -> None: + mapping_dir = copy_example_mappings(tmp_path) + mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" + mappings = json.loads(mappings_path.read_text(encoding="utf-8")) + mappings["uncached_probe"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "cache_probe_disabled"}, + } + mappings_path.write_text(json.dumps(mappings), encoding="utf-8") + + config_path.write_text( + "\n".join( + [ + f'mapping_directory = "{mapping_dir.as_posix()}"', + f'schemas_directory = "{(REPO_ROOT / "examples/simple_mapper/schemas").as_posix()}"', + "cache_enabled = false", + ] + ), + encoding="utf-8", + ) + + data_source = ArrayRecordingDataSource() + mapper = libtokamap.Mapper(str(config_path)) + mapper.register_python_data_source("JSON", data_source) + mapper.register_custom_function("custom", "dot_product", dot_product) + + mapper.map("example", "magnetics/uncached_probe", {"shot": 42}) + mapper.map("example", "magnetics/uncached_probe", {"shot": 42}) + + assert len(data_source.calls) == 2 + + +@pytest.mark.cache_regression +@pytest.mark.skip(reason="skipping until caching is reworked") +def test_data_source_cache_does_not_leak_between_mapper_instances( + config_path: Path, tmp_path: Path +) -> None: + mapping_dir = copy_example_mappings(tmp_path) + mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" + mappings = json.loads(mappings_path.read_text(encoding="utf-8")) + mappings["pollution_probe"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "cache_probe_pollution"}, + } + mappings_path.write_text(json.dumps(mappings), encoding="utf-8") + + config_path.write_text( + "\n".join( + [ + f'mapping_directory = "{mapping_dir.as_posix()}"', + f'schemas_directory = "{(REPO_ROOT / "examples/simple_mapper/schemas").as_posix()}"', + "cache_enabled = true", + ] + ), + encoding="utf-8", + ) + + first_source = ConstantRecordingDataSource(1) + first_mapper = libtokamap.Mapper(str(config_path)) + first_mapper.register_python_data_source("JSON", first_source) + first_mapper.register_custom_function("custom", "dot_product", dot_product) + np.testing.assert_array_equal( + first_mapper.map("example", "magnetics/pollution_probe", {"shot": 42}), + np.array([1], dtype=np.int64), + ) + + second_source = ConstantRecordingDataSource(2) + second_mapper = libtokamap.Mapper(str(config_path)) + second_mapper.register_python_data_source("JSON", second_source) + second_mapper.register_custom_function("custom", "dot_product", dot_product) + np.testing.assert_array_equal( + second_mapper.map("example", "magnetics/pollution_probe", {"shot": 42}), + np.array([2], dtype=np.int64), + ) + + third_source = ConstantRecordingDataSource(3) + third_mapper = libtokamap.Mapper(str(config_path)) + third_mapper.register_python_data_source("JSON", third_source) + third_mapper.register_custom_function("custom", "dot_product", dot_product) + np.testing.assert_array_equal( + third_mapper.map("example", "magnetics/pollution_probe", {"shot": 42}), + np.array([3], dtype=np.int64), + ) + assert len(third_source.calls) == 1 + + +@pytest.mark.cache_regression +@pytest.mark.skip(reason="skipping until caching is reworked") +def test_mapping_cache_key_includes_runtime_attributes( + config_path: Path, tmp_path: Path +) -> None: + mapping_dir = copy_example_mappings(tmp_path) + mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" + mappings = json.loads(mappings_path.read_text(encoding="utf-8")) + mappings["_runtime_attr"] = { + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "runtime_attr_key", "run_id": "{{ run_id }}"}, + } + mappings["runtime_attr_ref"] = { + "map_type": "EXPR", + "expr": "x", + "parameters": {"x": "_runtime_attr"}, + } + mappings_path.write_text(json.dumps(mappings), encoding="utf-8") + + config_path.write_text( + "\n".join( + [ + f'mapping_directory = "{mapping_dir.as_posix()}"', + f'schemas_directory = "{(REPO_ROOT / "examples/simple_mapper/schemas").as_posix()}"', + "cache_enabled = true", + ] + ), + encoding="utf-8", + ) + + data_source = RuntimeAttributeDataSource() + mapper = libtokamap.Mapper(str(config_path)) + mapper.register_python_data_source("JSON", data_source) + mapper.register_custom_function("custom", "dot_product", dot_product) + + first = mapper.map("example", "magnetics/_runtime_attr", {"shot": 42, "run_id": 1}) + second = mapper.map("example", "magnetics/_runtime_attr", {"shot": 42, "run_id": 2}) + + np.testing.assert_array_equal(first, np.array([1], dtype=np.int64)) + np.testing.assert_array_equal(second, np.array([2], dtype=np.int64)) + + def test_custom_function_errors_are_wrapped(config_path: Path, tmp_path: Path) -> None: mapping_dir = copy_example_mappings(tmp_path) mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" mappings = json.loads(mappings_path.read_text(encoding="utf-8")) mappings["coil[#]/flux/failing_custom"] = { - "MAP_TYPE": "CUSTOM", - "LIBRARY": "custom", - "FUNCTION": "failing_custom", - "INPUTS": {"lhs": "coil[#]/flux/time"}, - "PARAMETERS": {}, + "map_type": "CUSTOM", + "library": "custom", + "function": "failing_custom", + "inputs": {"lhs": "coil[#]/flux/time"}, + "parameters": {}, } mappings_path.write_text(json.dumps(mappings), encoding="utf-8") @@ -174,11 +506,11 @@ def test_custom_function_accepts_integer_numpy_return(config_path: Path, tmp_pat mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" mappings = json.loads(mappings_path.read_text(encoding="utf-8")) mappings["coil[#]/flux/int_codes"] = { - "MAP_TYPE": "CUSTOM", - "LIBRARY": "custom", - "FUNCTION": "int_codes", - "INPUTS": {}, - "PARAMETERS": {}, + "map_type": "CUSTOM", + "library": "custom", + "function": "int_codes", + "inputs": {}, + "parameters": {}, } mappings_path.write_text(json.dumps(mappings), encoding="utf-8") @@ -218,10 +550,10 @@ def test_invalid_slice_raises_processing_error(config_path: Path, tmp_path: Path mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" mappings = json.loads(mappings_path.read_text(encoding="utf-8")) mappings["bad_slice"] = { - "MAP_TYPE": "DATA_SOURCE", - "DATA_SOURCE": "JSON", - "ARGS": {"signal": "coils/0/flux/data"}, - "SLICE": "[::0]", + "map_type": "DATA_SOURCE", + "data_source": "JSON", + "args": {"signal": "coils/0/flux/data"}, + "slice": "[::0]", } mappings_path.write_text(json.dumps(mappings), encoding="utf-8") diff --git a/python/tests/test_callback_returns.py b/python/tests/test_callback_returns.py index 5b6f604..9be6a47 100644 --- a/python/tests/test_callback_returns.py +++ b/python/tests/test_callback_returns.py @@ -19,11 +19,11 @@ def make_custom_mapper(tmp_path, callback: Callback) -> libtokamap.Mapper: mappings_path = mapping_dir / "example_v1/magnetics/40/mappings.json" mappings = json.loads(mappings_path.read_text(encoding="utf-8")) mappings["coil[#]/flux/callback_result"] = { - "MAP_TYPE": "CUSTOM", - "LIBRARY": "custom", - "FUNCTION": "callback_result", - "INPUTS": {}, - "PARAMETERS": {}, + "map_type": "CUSTOM", + "library": "custom", + "function": "callback_result", + "inputs": {}, + "parameters": {}, } mappings_path.write_text(json.dumps(mappings), encoding="utf-8") diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..bd44ede --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,2 @@ +target +build diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..b7a6c5c --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,352 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "cc" +version = "1.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f81de88da10862f22b5b3a60f18f6f42bbe7cb8faa24845dd7b1e4e22190e77" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5edd58bf75c3fdfc80d79806403af626570662f7b6cc782a7fabe156166bd6d6" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd46bf2b541a4e0c2d5abba76607379ee05d68e714868e3cb406dc8d591ce2d2" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c79b68f6a3a8f809d39b38ae8af61305a6113819b19b262643b9c21353b92d9" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862b7fdb048ff9ef0779a0d0a03affd09746c4c875543746b640756be9cff2af" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libtokamap-rust" +version = "0.1.0" +dependencies = [ + "cxx", + "cxx-build", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "serde" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..83bd75a --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "libtokamap-rust" +version = "0.1.0" +edition = "2021" +authors = ["LibTokaMap Contributors"] +description = "Rust bindings for libtokamap C++ library" +license = "MIT" +repository = "https://github.com/your-org/libtokamap" +keywords = ["tokamak", "mapping", "physics", "data"] +categories = ["science", "api-bindings"] + +[dependencies] +cxx = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" + +[build-dependencies] +cxx-build = "1.0" + +[lib] +name = "libtokamap_rust" +crate-type = ["cdylib", "rlib"] + +[features] +default = [] diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..647d5ea --- /dev/null +++ b/rust/README.md @@ -0,0 +1,349 @@ +# libtokamap-rust + +Rust bindings for the libtokamap C++ library, providing safe and ergonomic access to tokamak data mapping functionality. + +## Overview + +libtokamap is a C++ library designed for mapping and processing tokamak experimental data. This Rust wrapper provides a safe, memory-safe interface to the core functionality while maintaining high performance. + +## Features + +- **Safe C++ Interop**: Uses the `cxx` crate for zero-cost, safe C++ integration +- **Memory Safety**: Automatic memory management with Rust's ownership system +- **Type Safety**: Strong typing for data arrays and configuration +- **Error Handling**: Comprehensive error types using `thiserror` +- **JSON Configuration**: Serde-based JSON configuration support +- **Data Source Management**: Register and manage multiple data sources +- **Custom Functions**: Load and use custom mapping functions +- **Thread Safety**: Safe to use across threads (when the underlying C++ library supports it) + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +libtokamap-rust = "0.1.0" +``` + +### Prerequisites + +1. **libtokamap C++ library**: Must be built and installed +2. **C++20 compiler**: Required for the C++ components +3. **CMake**: For building the C++ library +4. **System dependencies**: As required by libtokamap (HDF5, MDS+, etc.) + +### Building from Source + +1. First, build the libtokamap C++ library: + ```bash + cd libtokamap + mkdir build && cd build + cmake .. -DCMAKE_BUILD_TYPE=Release + make -j$(nproc) + make install + ``` + +2. Then build the Rust wrapper: + ```bash + cd rust + cargo build --release + ``` + +## Quick Start + +```rust +use libtokamap_rust::{MappingHandler, DataType, TokaMapError}; +use serde_json::json; + +fn main() -> Result<(), TokaMapError> { + // Create a new mapping handler + let mut handler = MappingHandler::new(); + + // Initialize with JSON configuration + let config = json!({ + "version": "1.0", + "experiments": { + "ITER": { + "mapping_dir": "/data/iter/mappings", + "groups": ["magnetics", "thomson", "ece"] + } + }, + "data_sources": {} + }); + + handler.init_with_json(&config)?; + + // Map some data + let result = handler.map_data( + "ITER", // experiment + "/magnetics/bpol_probe_01", // signal path + DataType::Float, // expected data type + 1, // rank (1D array) + Some(&json!({"shot": 12345, "time": 5.0})) // extra attributes + )?; + + // Work with the result + println!("Data type: {:?}", result.data_type()); + println!("Size: {}", result.size()); + println!("Shape: {:?}", result.shape()); + + // Convert to Rust vector + let float_data: Vec = result.to_vec()?; + println!("First value: {}", float_data[0]); + + Ok(()) +} +``` + +## API Reference + +### MappingHandler + +The main interface for data mapping operations. + +```rust +// Create a new handler +let mut handler = MappingHandler::new(); + +// Initialize from file +handler.init_with_path("config.json")?; + +// Initialize from JSON value +handler.init_with_json(&config_json)?; + +// Map data +let result = handler.map_data(experiment, path, data_type, rank, attributes)?; + +// Reset handler +handler.reset()?; +``` + +### Data Source Management + +```rust +// Register a data source factory from a shared library +handler.register_data_source_factory_from_lib( + "hdf5_factory", + "/path/to/hdf5_datasource.so" +)?; + +// Register a data source instance +let factory_args = [ + ("file_path".to_string(), json!("/data/experiment.h5")), + ("dataset_prefix".to_string(), json!("signals")) +].into_iter().collect(); + +handler.register_data_source_with_factory( + "my_hdf5_source", + "hdf5_factory", + &factory_args +)?; + +// Unregister when done +handler.unregister_data_source("my_hdf5_source")?; +``` + +### Custom Functions + +```rust +// Load custom function library +handler.load_custom_function_library("/path/to/custom_functions.so")?; + +// Unregister specific function +handler.unregister_custom_function("my_lib", "my_function")?; +``` + +### TypedDataArray + +Represents typed arrays returned from mapping operations. + +```rust +// Get metadata +let data_type = array.data_type(); +let size = array.size(); +let shape = array.shape(); +let rank = array.rank(); + +// Convert to Rust types +let float_vec: Vec = array.to_vec()?; +let double_vec: Vec = array.to_vec()?; + +// Access raw bytes +let bytes = array.as_bytes(); + +// Check if empty +if array.is_empty() { + println!("No data"); +} +``` + +## Data Types + +The library supports these data types: + +- `DataType::Char` - `i8` +- `DataType::UChar` - `u8` +- `DataType::Short` - `i16` +- `DataType::UShort` - `u16` +- `DataType::Int` - `i32` +- `DataType::UInt` - `u32` +- `DataType::Long` - `i64` (platform dependent) +- `DataType::ULong` - `u64` (platform dependent) +- `DataType::Int64` - `i64` +- `DataType::UInt64` - `u64` +- `DataType::Float` - `f32` +- `DataType::Double` - `f64` + +## Error Handling + +The library provides comprehensive error types: + +```rust +use libtokamap_rust::TokaMapError; + +match handler.init_with_path("config.json") { + Ok(_) => println!("Success!"), + Err(TokaMapError::Configuration(msg)) => eprintln!("Config error: {}", msg), + Err(TokaMapError::DataType(msg)) => eprintln!("Data type error: {}", msg), + Err(TokaMapError::Processing(msg)) => eprintln!("Processing error: {}", msg), + Err(TokaMapError::Parameter(msg)) => eprintln!("Parameter error: {}", msg), + Err(TokaMapError::DataSource(msg)) => eprintln!("Data source error: {}", msg), + Err(TokaMapError::Generic(msg)) => eprintln!("Generic error: {}", msg), +} +``` + +## Configuration Format + +The configuration uses JSON format compatible with the C++ library: + +```json +{ + "version": "1.0", + "dd_version": "3.0", + "mapping_directory": "/data/mappings", + "cache": { + "enabled": true, + "max_size_mb": 1024 + }, + "experiments": { + "ITER": { + "mapping_dir": "/data/iter/mappings", + "groups": ["magnetics", "thomson", "ece"], + "partition": [ + { + "attribute": "shot", + "selector": "exact" + }, + { + "attribute": "time", + "selector": "closest" + } + ] + } + }, + "data_sources": { + "mdsplus_factory": { + "type": "library", + "path": "/usr/local/lib/libmdsplus_datasource.so" + } + }, + "global_settings": { + "trace_enabled": false, + "default_timeout": 30 + } +} +``` + +## Examples + +Check the `examples` module for comprehensive usage examples: + +```rust +use libtokamap_rust::examples; + +// Run all examples +examples::run_all_examples(); + +// Or run specific examples +examples::basic_usage_example()?; +examples::data_source_registration_example()?; +examples::custom_functions_example()?; +``` + +## Thread Safety + +The `MappingHandler` is not `Send` or `Sync` by default due to the underlying C++ implementation. For multi-threaded usage, create separate handler instances per thread or use appropriate synchronization. + +## Performance Considerations + +- **Zero-copy Data Transfer**: Where possible, data is transferred without copying +- **Memory Ownership**: The Rust wrapper takes ownership of data from C++ +- **Caching**: Utilizes the C++ library's built-in caching mechanisms +- **Batch Operations**: Process multiple mappings efficiently + +## Troubleshooting + +### Common Issues + +1. **Library not found**: Ensure libtokamap is properly installed and in library path +2. **C++ version**: Requires C++20 compatible compiler +3. **Missing dependencies**: Install HDF5, MDS+, and other required libraries + +### Debug Build + +For debugging, use the debug build which includes additional error information: + +```bash +cargo build # Debug build +RUST_LOG=debug cargo test # Run tests with logging +``` + +### CMake Integration + +The build system automatically detects the libtokamap installation. If needed, you can specify paths: + +```bash +export LIBTOKAMAP_ROOT=/path/to/libtokamap/install +cargo build +``` + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests +5. Run the test suite: `cargo test` +6. Submit a pull request + +### Development Setup + +```bash +# Clone and build libtokamap +git clone https://github.com/your-org/libtokamap.git +cd libtokamap +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Debug -DENABLE_TESTING=ON +make -j$(nproc) + +# Build Rust wrapper +cd ../rust +cargo build +cargo test +``` + +## License + +This project is licensed under the MIT OR Apache-2.0 license - see the LICENSE files for details. + +## Changelog + +### 0.1.0 (TBD) +- Initial release +- Basic MappingHandler wrapper +- TypedDataArray support +- Data source management +- Custom function loading +- Comprehensive error handling +- Documentation and examples \ No newline at end of file diff --git a/rust/build.rs b/rust/build.rs new file mode 100644 index 0000000..d608755 --- /dev/null +++ b/rust/build.rs @@ -0,0 +1,50 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + // Get the path to the libtokamap source directory + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let libtokamap_root = PathBuf::from(&manifest_dir).parent().unwrap().to_path_buf(); + + // Include directories for libtokamap + let include_dirs = vec![ + libtokamap_root.join("src"), + libtokamap_root.join("ext_include"), + libtokamap_root.join("build").join("include"), + libtokamap_root.join("include"), + ]; + + // Build the C++ bridge + cxx_build::bridge("src/lib.rs") + .file("src/bridge.cpp") + .includes(&include_dirs) + .std("c++20") + .flag_if_supported("-Wall") + .flag_if_supported("-Wextra") + .flag_if_supported("-Werror") + .compile("libtokamap_rust_bridge"); + + // Link against libtokamap + println!("cargo:rustc-link-lib=tokamap"); + + // Add library search path + let lib_dir = libtokamap_root.join("build").join("lib"); + if lib_dir.exists() { + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + } + + // Also check install directory + let install_lib_dir = libtokamap_root.join("install").join("lib"); + if install_lib_dir.exists() { + println!( + "cargo:rustc-link-search=native={}", + install_lib_dir.display() + ); + } + + // Rerun if any of these files change + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=src/bridge.cpp"); + println!("cargo:rerun-if-changed=../src/handlers/mapping_handler.hpp"); + println!("cargo:rerun-if-changed=../src/handlers/mapping_handler.cpp"); +} diff --git a/rust/src/bridge.cpp b/rust/src/bridge.cpp new file mode 100644 index 0000000..7cc7b87 --- /dev/null +++ b/rust/src/bridge.cpp @@ -0,0 +1,211 @@ +#include "bridge.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libtokamap-rust/src/lib.rs.h" + +void MappingHandlerWrapper::reset() +{ + try { + handler.reset(); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Reset failed: ") + e.what()); + } +} + +void MappingHandlerWrapper::init_with_path(rust::Str config_path) +{ + try { + std::filesystem::path path(static_cast(config_path)); + handler.init(path); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Initialization with path failed: ") + e.what()); + } +} + +void MappingHandlerWrapper::init_with_json(rust::Str config_json) +{ + try { + nlohmann::json config = nlohmann::json::parse(static_cast(config_json)); + handler.init(config); + } catch (const nlohmann::json::parse_error& e) { + throw std::runtime_error(std::string("JSON parse error: ") + e.what()); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Initialization with JSON failed: ") + e.what()); + } +} + +RustTypedDataArray MappingHandlerWrapper::map_data(rust::Str experiment, rust::Str path, uint32_t data_type_index, + int32_t rank, + rust::Str extra_attributes) +{ + try { + // Parse extra attributes JSON + nlohmann::json attrs; + if (!extra_attributes.empty() && extra_attributes != "{}") { + attrs = nlohmann::json::parse(extra_attributes); + } + + // Convert data type index to DataType + libtokamap::DataType data_type = data_type_index_from_u32(data_type_index); + + // Call the C++ map function + auto cpp_array = + handler.map(static_cast(experiment), static_cast(path), data_type, rank, attrs); + + // Convert to Rust-compatible format + return convert_typed_data_array(cpp_array); + + } catch (const nlohmann::json::parse_error& e) { + throw std::runtime_error(std::string("JSON parse error in extra_attributes: ") + e.what()); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Mapping failed: ") + e.what()); + } +} + +void MappingHandlerWrapper::register_data_source_factory_with_lib(rust::Str factory_name, rust::Str library_path) +{ + try { + std::filesystem::path lib_path(static_cast(library_path)); + handler.register_data_source_factory(static_cast(factory_name), + static_cast(lib_path)); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Data source factory registration failed: ") + e.what()); + } +} + +void MappingHandlerWrapper::register_data_source_with_factory(rust::Str name, rust::Str factory_name, + const RustDataSourceFactoryArgs& args) +{ + try { + auto cpp_args = convert_factory_args(args); + handler.register_data_source(static_cast(name), static_cast(factory_name), cpp_args); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Data source registration failed: ") + e.what()); + } +} + +void MappingHandlerWrapper::unregister_data_source(rust::Str name) +{ + try { + handler.unregister_data_source(static_cast(name)); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Data source unregistration failed: ") + e.what()); + } +} + +void MappingHandlerWrapper::load_custom_function_library(rust::Str library_path) +{ + try { + std::filesystem::path lib_path(static_cast(library_path)); + handler.load_custom_function_library(lib_path); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Custom function library loading failed: ") + e.what()); + } +} + +void MappingHandlerWrapper::unregister_custom_function(rust::Str library_name, rust::Str function_name) +{ + try { + handler.unregister_custom_function(static_cast(library_name), + static_cast(function_name)); + } catch (const std::exception& e) { + throw std::runtime_error(std::string("Custom function unregistration failed: ") + e.what()); + } +} + +// Factory function +std::unique_ptr new_mapping_handler() { return std::make_unique(); } + +// Helper function to convert data type index to DataType +libtokamap::DataType data_type_index_from_u32(uint32_t data_type) +{ + constexpr uint32_t INT8_TYPE = 1; + constexpr uint32_t INT16_TYPE = 2; + constexpr uint32_t INT32_TYPE = 3; + constexpr uint32_t INT64_TYPE = 4; + constexpr uint32_t UINT8_TYPE = 5; + constexpr uint32_t UINT16_TYPE = 6; + constexpr uint32_t UINT32_TYPE = 7; + constexpr uint32_t UINT64_TYPE = 8; + constexpr uint32_t FLOAT_TYPE = 9; + constexpr uint32_t DOUBLE_TYPE = 10; + + switch (data_type) { + case INT8_TYPE: return libtokamap::DataType::Int8; + case INT16_TYPE: return libtokamap::DataType::Int16; + case INT32_TYPE: return libtokamap::DataType::Int32; + case INT64_TYPE: return libtokamap::DataType::Int64; + case UINT8_TYPE: return libtokamap::DataType::UInt8; + case UINT16_TYPE: return libtokamap::DataType::UInt16; + case UINT32_TYPE: return libtokamap::DataType::UInt32; + case UINT64_TYPE: return libtokamap::DataType::UInt64; + case FLOAT_TYPE: return libtokamap::DataType::Float; + case DOUBLE_TYPE: return libtokamap::DataType::Double; + default: return libtokamap::DataType::Unknown; + } +} + +// Helper function to convert C++ TypedDataArray to Rust format +RustTypedDataArray convert_typed_data_array(libtokamap::TypedDataArray& cpp_array) +{ + RustTypedDataArray rust_array{}; + + // Convert data type + rust_array.data_type = static_cast(cpp_array.data_type()); + + // Copy size and shape + rust_array.size = cpp_array.size(); + std::copy(cpp_array.shape().begin(), cpp_array.shape().end(), std::back_inserter(rust_array.shape)); + + // Copy raw data + size_t element_size = cpp_array.element_size(); + size_t total_bytes = rust_array.size * element_size; + std::copy(cpp_array.buffer(), cpp_array.buffer() + total_bytes, std::back_inserter(rust_array.data)); + + return rust_array; +} + +// Helper function to convert Rust factory args to C++ format +libtokamap::DataSourceFactoryArgs convert_factory_args(const RustDataSourceFactoryArgs& rust_args) +{ + libtokamap::DataSourceFactoryArgs cpp_args; + + try { + nlohmann::json json_args = nlohmann::json::parse(rust_args.json_string); + + // Convert JSON values to std::any + for (const auto& [key, value] : json_args.items()) { + if (value.is_string()) { + cpp_args[key] = value.get(); + } else if (value.is_number_integer()) { + cpp_args[key] = value.get(); + } else if (value.is_number_unsigned()) { + cpp_args[key] = value.get(); + } else if (value.is_number_float()) { + cpp_args[key] = value.get(); + } else if (value.is_boolean()) { + cpp_args[key] = value.get(); + } else { + // For complex types, store as JSON string + cpp_args[key] = value.dump(); + } + } + } catch (const nlohmann::json::parse_error& e) { + throw std::runtime_error(std::string("Failed to parse factory args JSON: ") + e.what()); + } + + return cpp_args; +} diff --git a/rust/src/bridge.hpp b/rust/src/bridge.hpp new file mode 100644 index 0000000..68fd5af --- /dev/null +++ b/rust/src/bridge.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Rust-compatible types (defined to match the Rust bridge) +struct RustTypedDataArray; +struct RustDataSourceFactoryArgs; + +// C++ wrapper class for MappingHandler to provide a clean interface for Rust +class MappingHandlerWrapper +{ + private: + libtokamap::MappingHandler handler; + + public: + MappingHandlerWrapper() = default; + ~MappingHandlerWrapper() = default; + + // Disable copy and move to keep it simple + MappingHandlerWrapper(const MappingHandlerWrapper&) = delete; + MappingHandlerWrapper& operator=(const MappingHandlerWrapper&) = delete; + MappingHandlerWrapper(MappingHandlerWrapper&&) = delete; + MappingHandlerWrapper& operator=(MappingHandlerWrapper&&) = delete; + + // Reset the handler + void reset(); + + // Initialize with config file path + void init_with_path(rust::Str config_path); + + // Initialize with JSON config string + void init_with_json(rust::Str config_json); + + // Map data + RustTypedDataArray map_data(rust::Str experiment, rust::Str path, uint32_t data_type_index, int32_t rank, + rust::Str extra_attributes); + + // Register data source factory with library path + void register_data_source_factory_with_lib(rust::Str factory_name, rust::Str library_path); + + // Register data source with factory + void register_data_source_with_factory(rust::Str name, rust::Str factory_name, + const RustDataSourceFactoryArgs& args); + + // Unregister data source + void unregister_data_source(rust::Str name); + + // Load custom function library + void load_custom_function_library(rust::Str library_path); + + // Unregister custom function + void unregister_custom_function(rust::Str library_name, rust::Str function_name); +}; + +// Factory function to create new MappingHandlerWrapper +std::unique_ptr new_mapping_handler(); + +// Helper functions for type conversion +libtokamap::DataType data_type_index_from_u32(uint32_t data_type); +RustTypedDataArray convert_typed_data_array(libtokamap::TypedDataArray& cpp_array); +libtokamap::DataSourceFactoryArgs convert_factory_args(const RustDataSourceFactoryArgs& rust_args); diff --git a/rust/src/examples.rs b/rust/src/examples.rs new file mode 100644 index 0000000..f7635dd --- /dev/null +++ b/rust/src/examples.rs @@ -0,0 +1,259 @@ +//! Examples of using the libtokamap Rust wrapper +//! +//! This module provides examples of how to use the MappingHandler +//! and other components of the libtokamap Rust wrapper. + +use crate::{DataSourceFactoryArgs, MappingHandler, TokaMapError}; +use serde_json::{json, Value}; + +/// Example of basic MappingHandler usage +pub fn basic_usage_example() -> Result<(), TokaMapError> { + // Create a new mapping handler + let mut handler = MappingHandler::new(); + + // Initialize with a configuration file + // handler.init_with_path("path/to/config.json")?; + + // Or initialize with a JSON configuration + let config = json!({ + "version": "1.0", + "experiments": { + "test_experiment": { + "mapping_dir": "/path/to/mappings", + "groups": ["group1", "group2"] + } + }, + "data_sources": {} + }); + + handler.init_with_json(&config)?; + + // Map some data (this would normally work with real data) + // let result = handler.map_data( + // "test_experiment", + // "/some/data/path", + // DataType::Float, + // 2, // rank + // Some(&json!({"time": 1000.0})) + // )?; + + println!("MappingHandler initialized successfully"); + Ok(()) +} + +/// Example of registering data sources +pub fn data_source_registration_example() -> Result<(), TokaMapError> { + let mut handler = MappingHandler::new(); + + // Register a data source factory from a dynamic library + handler + .register_data_source_factory_from_lib("hdf5_factory", "/path/to/hdf5_data_source.so")?; + + // Create factory arguments for the data source + let mut factory_args = DataSourceFactoryArgs::new(); + factory_args.insert("file_path".to_string(), json!("/data/experiment.h5")); + factory_args.insert("dataset_prefix".to_string(), json!("signals")); + + // Register a data source using the factory + handler.register_data_source_with_factory("hdf5_data_source", "hdf5_factory", &factory_args)?; + + println!("Data source registered successfully"); + + // Later, you can unregister the data source if needed + handler.unregister_data_source("hdf5_data_source")?; + + Ok(()) +} + +/// Example of loading and using custom functions +pub fn custom_functions_example() -> Result<(), TokaMapError> { + let mut handler = MappingHandler::new(); + + // Load a custom function library + handler.load_custom_function_library("/path/to/custom_functions.so")?; + + println!("Custom function library loaded successfully"); + + // Custom functions would be used automatically during mapping + // based on the mapping configuration + + // You can also unregister specific custom functions + handler.unregister_custom_function("my_library", "my_function")?; + + Ok(()) +} + +/// Example configuration for a tokamak experiment +pub fn tokamak_config_example() -> Value { + json!({ + "version": "1.0", + "dd_version": "3.0", + "mapping_directory": "/data/mappings", + "cache": { + "enabled": true, + "max_size_mb": 1024 + }, + "experiments": { + "ITER": { + "mapping_dir": "/data/iter/mappings", + "groups": ["magnetics", "thomson", "ece"], + "partition": [ + { + "attribute": "shot", + "selector": "exact" + }, + { + "attribute": "time", + "selector": "closest" + } + ] + }, + "JET": { + "mapping_dir": "/data/jet/mappings", + "groups": ["magnetics", "bolometry", "cxrs"], + "partition": [ + { + "attribute": "pulse", + "selector": "exact" + } + ] + } + }, + "data_sources": { + "mdsplus_factory": { + "type": "library", + "path": "/usr/local/lib/libmdsplus_datasource.so" + }, + "hdf5_factory": { + "type": "library", + "path": "/usr/local/lib/libhdf5_datasource.so" + } + }, + "global_settings": { + "trace_enabled": false, + "default_timeout": 30 + } + }) +} + +/// Example of working with TypedDataArray results +pub fn typed_data_array_example() -> Result<(), TokaMapError> { + let mut handler = MappingHandler::new(); + let config = tokamak_config_example(); + handler.init_with_json(&config)?; + + // This would normally return real data from a mapping operation + // let result = handler.map_data( + // "ITER", + // "/magnetics/bpol_probe_01", + // DataType::Float, + // 1, // 1D array + // Some(&json!({"shot": 12345, "time": 5.0})) + // )?; + + // Example of what you could do with the result: + // println!("Data type: {:?}", result.data_type()); + // println!("Size: {}", result.size()); + // println!("Shape: {:?}", result.shape()); + // println!("Rank: {}", result.rank()); + + // Convert to a Rust vector (if the types match) + // let float_data: Vec = result.to_vec()?; + // println!("First few values: {:?}", &float_data[..5.min(float_data.len())]); + + Ok(()) +} + +/// Example of error handling +pub fn error_handling_example() { + let mut handler = MappingHandler::new(); + + // Try to initialize with invalid JSON + let invalid_config = json!({ + "version": "1.0", + // Missing required fields + }); + + match handler.init_with_json(&invalid_config) { + Ok(_) => println!("Initialization succeeded"), + Err(TokaMapError::Configuration(msg)) => { + println!("Configuration error: {}", msg); + } + Err(TokaMapError::DataType(msg)) => { + println!("Data type error: {}", msg); + } + Err(TokaMapError::Processing(msg)) => { + println!("Processing error: {}", msg); + } + Err(TokaMapError::Parameter(msg)) => { + println!("Parameter error: {}", msg); + } + Err(TokaMapError::DataSource(msg)) => { + println!("Data source error: {}", msg); + } + Err(TokaMapError::Generic(msg)) => { + println!("Generic error: {}", msg); + } + Err(TokaMapError::Thread(msg)) => { + println!("Thread error: {}", msg); + } + } +} + +/// Run all examples (for testing) +pub fn run_all_examples() { + println!("Running libtokamap Rust wrapper examples...\n"); + + println!("1. Basic usage example:"); + if let Err(e) = basic_usage_example() { + println!(" Error: {}", e); + } + + println!("\n2. Data source registration example:"); + if let Err(e) = data_source_registration_example() { + println!(" Error: {}", e); + } + + println!("\n3. Custom functions example:"); + if let Err(e) = custom_functions_example() { + println!(" Error: {}", e); + } + + println!("\n4. TypedDataArray example:"); + if let Err(e) = typed_data_array_example() { + println!(" Error: {}", e); + } + + println!("\n5. Error handling example:"); + error_handling_example(); + + println!("\n6. Example tokamak configuration:"); + let config = tokamak_config_example(); + println!("{}", serde_json::to_string_pretty(&config).unwrap()); + + println!("\nExamples completed!"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tokamak_config_generation() { + let config = tokamak_config_example(); + assert!(config.is_object()); + assert_eq!(config["version"], "1.0"); + assert!(config["experiments"].is_object()); + } + + #[test] + fn test_examples_dont_panic() { + // These examples might fail due to missing files/configuration, + // but they shouldn't panic + let _ = basic_usage_example(); + let _ = data_source_registration_example(); + let _ = custom_functions_example(); + let _ = typed_data_array_example(); + error_handling_example(); // This one shouldn't fail + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..1e18461 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,390 @@ +//! Rust bindings for libtokamap C++ library +//! +//! This crate provides safe Rust bindings for the libtokamap C++ library, +//! which is used for tokamak data mapping and processing. + +use serde_json::Value as JsonValue; +use std::collections::HashMap; +use thiserror::Error; + +pub mod examples; + +/// Error types for the libtokamap Rust wrapper +#[derive(Error, Debug)] +pub enum TokaMapError { + #[error("Configuration error: {0}")] + Configuration(String), + #[error("Data type error: {0}")] + DataType(String), + #[error("Processing error: {0}")] + Processing(String), + #[error("Parameter error: {0}")] + Parameter(String), + #[error("Data source error: {0}")] + DataSource(String), + #[error("Generic error: {0}")] + Generic(String), + #[error("Thread error: {0}")] + Thread(String), +} + +/// Data types supported by TypedDataArray +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DataType { + Unknown, + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, + Float, + Double, +} + +/// A Rust representation of the C++ TypedDataArray +#[derive(Debug)] +pub struct TypedDataArray { + data_type: DataType, + size: usize, + shape: Vec, + data: Vec, +} + +impl TypedDataArray { + /// Get the data type of the array + pub fn data_type(&self) -> DataType { + self.data_type + } + + /// Get the size (number of elements) of the array + pub fn size(&self) -> usize { + self.size + } + + /// Get the shape of the array + pub fn shape(&self) -> &[usize] { + &self.shape + } + + /// Get the rank (number of dimensions) of the array + pub fn rank(&self) -> usize { + self.shape.len() + } + + /// Check if the array is empty + pub fn is_empty(&self) -> bool { + self.size == 0 + } + + /// Get the raw data as bytes + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + /// Convert to a vector of the specified type (if compatible) + pub fn to_vec(&self) -> Result, TokaMapError> { + let expected_size = std::mem::size_of::(); + let actual_element_size = self.data.len() / self.size; + + if expected_size != actual_element_size { + return Err(TokaMapError::DataType(format!( + "Type size mismatch: expected {}, got {}", + expected_size, actual_element_size + ))); + } + + let ptr = self.data.as_ptr() as *const T; + let slice = unsafe { std::slice::from_raw_parts(ptr, self.size) }; + Ok(slice.to_vec()) + } +} + +/// Arguments for data source factory functions +pub type DataSourceFactoryArgs = HashMap; + +#[cxx::bridge] +mod ffi { + /// Rust types that will be shared with C++ + #[derive(Debug)] + pub struct RustTypedDataArray { + pub data_type: u8, + pub size: usize, + pub shape: Vec, + pub data: Vec, + } + + #[derive(Debug)] + pub struct RustDataSourceFactoryArgs { + pub json_string: String, + } + + unsafe extern "C++" { + include!("libtokamap-rust/src/bridge.hpp"); + + /// C++ MappingHandler wrapper + type MappingHandlerWrapper; + + /// Create a new MappingHandler instance + fn new_mapping_handler() -> UniquePtr; + + /// Reset the MappingHandler + fn reset(self: Pin<&mut MappingHandlerWrapper>) -> Result<()>; + + /// Initialize with config file path + fn init_with_path(self: Pin<&mut MappingHandlerWrapper>, config_path: &str) -> Result<()>; + + /// Initialize with JSON config string + fn init_with_json(self: Pin<&mut MappingHandlerWrapper>, config_json: &str) -> Result<()>; + + /// Map data + fn map_data( + self: Pin<&mut MappingHandlerWrapper>, + experiment: &str, + path: &str, + data_type_index: u32, + rank: i32, + extra_attributes: &str, + ) -> Result; + + /// Register data source factory with library path + fn register_data_source_factory_with_lib( + self: Pin<&mut MappingHandlerWrapper>, + factory_name: &str, + library_path: &str, + ) -> Result<()>; + + /// Register data source with factory + fn register_data_source_with_factory( + self: Pin<&mut MappingHandlerWrapper>, + name: &str, + factory_name: &str, + args: &RustDataSourceFactoryArgs, + ) -> Result<()>; + + /// Unregister data source + fn unregister_data_source(self: Pin<&mut MappingHandlerWrapper>, name: &str) -> Result<()>; + + /// Load custom function library + fn load_custom_function_library( + self: Pin<&mut MappingHandlerWrapper>, + library_path: &str, + ) -> Result<()>; + + /// Unregister custom function + fn unregister_custom_function( + self: Pin<&mut MappingHandlerWrapper>, + library_name: &str, + function_name: &str, + ) -> Result<()>; + } +} + +/// Main Rust wrapper for the MappingHandler +pub struct MappingHandler { + inner: std::sync::Mutex>, +} + +unsafe impl Send for MappingHandler {} + +impl MappingHandler { + /// Create a new MappingHandler + pub fn new() -> Self { + Self { + inner: std::sync::Mutex::new(ffi::new_mapping_handler()), + } + } + + /// Reset the handler + pub fn reset(&mut self) -> Result<(), TokaMapError> { + self.inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .reset() + .map_err(|e| TokaMapError::Generic(format!("Reset failed: {}", e))) + } + + /// Initialize with a configuration file + pub fn init_with_path( + &mut self, + config_path: impl AsRef, + ) -> Result<(), TokaMapError> { + let path_str = config_path.as_ref().to_string_lossy(); + self.inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .init_with_path(&path_str) + .map_err(|e| TokaMapError::Configuration(format!("Initialization failed: {}", e))) + } + + /// Initialize with a JSON configuration + pub fn init_with_json(&mut self, config: &serde_json::Value) -> Result<(), TokaMapError> { + let config_str = serde_json::to_string(config).map_err(|e| { + TokaMapError::Configuration(format!("JSON serialization failed: {}", e)) + })?; + + self.inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .init_with_json(&config_str) + .map_err(|e| TokaMapError::Configuration(format!("Initialization failed: {}", e))) + } + + /// Map data with the specified parameters + pub fn map_data( + &mut self, + experiment: &str, + path: &str, + data_type: DataType, + rank: i32, + extra_attributes: Option<&serde_json::Value>, + ) -> Result { + let data_type_index = data_type as u32; + let attrs_str = match extra_attributes { + Some(attrs) => serde_json::to_string(attrs).map_err(|e| { + TokaMapError::Parameter(format!("Failed to serialize attributes: {}", e)) + })?, + None => "{}".to_string(), + }; + + let rust_array = self + .inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .map_data(experiment, path, data_type_index, rank, &attrs_str) + .map_err(|e| TokaMapError::Processing(format!("Mapping failed: {}", e)))?; + + Ok(TypedDataArray { + data_type: match rust_array.data_type { + 1 => DataType::Int8, + 2 => DataType::Int16, + 3 => DataType::Int32, + 4 => DataType::Int64, + 5 => DataType::UInt8, + 6 => DataType::UInt16, + 7 => DataType::UInt32, + 8 => DataType::UInt64, + 9 => DataType::Float, + 10 => DataType::Double, + _ => DataType::Unknown, + }, + size: rust_array.size, + shape: rust_array.shape, + data: rust_array.data, + }) + } + + /// Register a data source factory from a dynamic library + pub fn register_data_source_factory_from_lib( + &mut self, + factory_name: &str, + library_path: impl AsRef, + ) -> Result<(), TokaMapError> { + let path_str = library_path.as_ref().to_string_lossy(); + self.inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .register_data_source_factory_with_lib(factory_name, &path_str) + .map_err(|e| TokaMapError::DataSource(format!("Factory registration failed: {}", e))) + } + + /// Register a data source with a factory + pub fn register_data_source_with_factory( + &mut self, + name: &str, + factory_name: &str, + args: &DataSourceFactoryArgs, + ) -> Result<(), TokaMapError> { + let args_json = serde_json::to_string(args).map_err(|e| { + TokaMapError::Parameter(format!("Failed to serialize factory args: {}", e)) + })?; + + let rust_args = ffi::RustDataSourceFactoryArgs { + json_string: args_json, + }; + + self.inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .register_data_source_with_factory(name, factory_name, &rust_args) + .map_err(|e| { + TokaMapError::DataSource(format!("Data source registration failed: {}", e)) + }) + } + + /// Unregister a data source + pub fn unregister_data_source(&mut self, name: &str) -> Result<(), TokaMapError> { + self.inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .unregister_data_source(name) + .map_err(|e| { + TokaMapError::DataSource(format!("Data source unregistration failed: {}", e)) + }) + } + + /// Load custom function library + pub fn load_custom_function_library( + &mut self, + library_path: impl AsRef, + ) -> Result<(), TokaMapError> { + let path_str = library_path.as_ref().to_string_lossy(); + self.inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .load_custom_function_library(&path_str) + .map_err(|e| { + TokaMapError::Generic(format!("Custom function library loading failed: {}", e)) + }) + } + + /// Unregister a custom function + pub fn unregister_custom_function( + &mut self, + library_name: &str, + function_name: &str, + ) -> Result<(), TokaMapError> { + self.inner + .lock() + .map_err(|_| TokaMapError::Thread("mutex lock failed".to_string()))? + .pin_mut() + .unregister_custom_function(library_name, function_name) + .map_err(|e| { + TokaMapError::Generic(format!("Custom function unregistration failed: {}", e)) + }) + } +} + +impl Default for MappingHandler { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mapping_handler_creation() { + let handler = MappingHandler::new(); + // Just test that we can create the handler + assert!(!handler.inner.lock().unwrap().is_null()); + } + + #[test] + fn test_data_type_enum() { + assert_eq!(DataType::Float as u32, 9); + assert_eq!(DataType::Double as u32, 10); + } +} diff --git a/rust/tests/integration_tests.rs b/rust/tests/integration_tests.rs new file mode 100644 index 0000000..e7fd887 --- /dev/null +++ b/rust/tests/integration_tests.rs @@ -0,0 +1,292 @@ +//! Integration tests for the libtokamap Rust wrapper +//! +//! These tests verify that the Rust wrapper correctly interfaces with +//! the underlying C++ libtokamap library. + +use libtokamap_rust::{DataType, MappingHandler, TokaMapError}; +use serde_json::json; + +#[test] +fn test_mapping_handler_creation() { + let handler = MappingHandler::new(); + // Just verify that we can create a handler without panicking + drop(handler); +} + +#[test] +fn test_handler_reset() { + let mut handler = MappingHandler::new(); + + // Reset should work even on uninitialized handler + match handler.reset() { + Ok(_) => {} + Err(_) => { + // Reset might fail if handler isn't initialized, which is acceptable + } + } +} + +#[test] +fn test_json_config_initialization() { + let mut handler = MappingHandler::new(); + + let config = json!({ + "version": "1.0", + "experiments": {}, + "data_sources": {} + }); + + // This might fail due to missing actual mapping files, but should not panic + let result = handler.init_with_json(&config); + + // We don't assert success because we may not have real mapping data + // but we verify the call doesn't panic and returns a proper Result + match result { + Ok(_) => println!("Initialization succeeded"), + Err(TokaMapError::Configuration(_)) => println!("Expected configuration error"), + Err(e) => println!("Initialization failed with: {}", e), + } +} + +#[test] +fn test_invalid_json_config() { + let mut handler = MappingHandler::new(); + + // Test with completely invalid config + let invalid_config = json!({ + "invalid_field": "invalid_value" + }); + + let result = handler.init_with_json(&invalid_config); + assert!(result.is_err(), "Should fail with invalid configuration"); + + if let Err(e) = result { + assert!(matches!(e, TokaMapError::Configuration(_))); + } +} + +#[test] +fn test_path_initialization_with_nonexistent_file() { + let mut handler = MappingHandler::new(); + + // Test with non-existent file + let result = handler.init_with_path("/nonexistent/path/config.json"); + assert!(result.is_err(), "Should fail with non-existent config file"); + + if let Err(e) = result { + assert!(matches!(e, TokaMapError::Configuration(_))); + } +} + +#[test] +fn test_data_source_operations() { + let mut handler = MappingHandler::new(); + + // Test registering a data source factory with non-existent library + let result = + handler.register_data_source_factory_from_lib("test_factory", "/nonexistent/library.so"); + + // Should fail but not panic + assert!(result.is_err()); + if let Err(e) = result { + assert!(matches!(e, TokaMapError::DataSource(_))); + } + + // Test unregistering non-existent data source + let result = handler.unregister_data_source("nonexistent_source"); + + // May succeed or fail depending on implementation, but shouldn't panic + match result { + Ok(_) => {} + Err(TokaMapError::DataSource(_)) => {} + Err(e) => panic!("Unexpected error type: {}", e), + } +} + +#[test] +fn test_custom_function_operations() { + let mut handler = MappingHandler::new(); + + // Test loading non-existent custom function library + let result = handler.load_custom_function_library("/nonexistent/functions.so"); + + // Should fail but not panic + assert!(result.is_err()); + if let Err(e) = result { + assert!(matches!(e, TokaMapError::Generic(_))); + } + + // Test unregistering non-existent custom function + let result = handler.unregister_custom_function("nonexistent_lib", "nonexistent_func"); + + // May succeed or fail, but shouldn't panic + match result { + Ok(_) => {} + Err(TokaMapError::Generic(_)) => {} + Err(e) => panic!("Unexpected error type: {}", e), + } +} + +#[test] +fn test_data_mapping_without_initialization() { + let mut handler = MappingHandler::new(); + + // Try to map data without initialization + let result = handler.map_data("test_experiment", "/test/path", DataType::Float, 1, None); + + // Should fail because handler is not initialized + assert!(result.is_err()); +} + +#[test] +fn test_data_type_enum_values() { + // Test that DataType enum values are as expected + assert_eq!(DataType::Unknown as u32, 0); + assert_eq!(DataType::Int8 as u32, 1); + assert_eq!(DataType::Int16 as u32, 2); + assert_eq!(DataType::Int32 as u32, 3); + assert_eq!(DataType::Int64 as u32, 4); + assert_eq!(DataType::UInt8 as u32, 5); + assert_eq!(DataType::UInt16 as u32, 6); + assert_eq!(DataType::UInt32 as u32, 7); + assert_eq!(DataType::UInt64 as u32, 8); + assert_eq!(DataType::Float as u32, 9); + assert_eq!(DataType::Double as u32, 10); +} + +#[test] +fn test_error_types() { + // Test that error types can be created and match correctly + let config_error = TokaMapError::Configuration("test".to_string()); + assert!(matches!(config_error, TokaMapError::Configuration(_))); + + let data_type_error = TokaMapError::DataType("test".to_string()); + assert!(matches!(data_type_error, TokaMapError::DataType(_))); + + let processing_error = TokaMapError::Processing("test".to_string()); + assert!(matches!(processing_error, TokaMapError::Processing(_))); + + let parameter_error = TokaMapError::Parameter("test".to_string()); + assert!(matches!(parameter_error, TokaMapError::Parameter(_))); + + let data_source_error = TokaMapError::DataSource("test".to_string()); + assert!(matches!(data_source_error, TokaMapError::DataSource(_))); + + let generic_error = TokaMapError::Generic("test".to_string()); + assert!(matches!(generic_error, TokaMapError::Generic(_))); +} + +#[test] +fn test_factory_args_creation() { + use libtokamap_rust::DataSourceFactoryArgs; + + let mut args = DataSourceFactoryArgs::new(); + args.insert("string_arg".to_string(), json!("test_value")); + args.insert("int_arg".to_string(), json!(42)); + args.insert("float_arg".to_string(), json!(3.14)); + args.insert("bool_arg".to_string(), json!(true)); + + assert_eq!(args.len(), 4); + assert!(args.contains_key("string_arg")); + assert!(args.contains_key("int_arg")); + assert!(args.contains_key("float_arg")); + assert!(args.contains_key("bool_arg")); +} + +#[test] +fn test_multiple_handlers() { + // Test that we can create multiple handlers + let handler1 = MappingHandler::new(); + let handler2 = MappingHandler::new(); + let handler3 = MappingHandler::new(); + + // All should be valid + drop(handler1); + drop(handler2); + drop(handler3); +} + +#[cfg(feature = "integration_with_real_data")] +#[test] +fn test_with_real_config() { + // This test only runs if we have access to real configuration + // Enable with: cargo test --features integration_with_real_data + + use std::env; + + let config_path = env::var("LIBTOKAMAP_TEST_CONFIG") + .expect("LIBTOKAMAP_TEST_CONFIG environment variable not set"); + + let mut handler = MappingHandler::new(); + let result = handler.init_with_path(&config_path); + + match result { + Ok(_) => { + println!("Successfully initialized with real config"); + + // Try a real mapping operation + if let Ok(mapped_data) = handler.map_data( + "test_experiment", + "/test/path", + DataType::Float, + 1, + Some(&json!({"test_attr": "test_value"})), + ) { + println!("Successfully mapped data: {} elements", mapped_data.size()); + println!("Data type: {:?}", mapped_data.data_type()); + println!("Shape: {:?}", mapped_data.shape()); + } + } + Err(e) => { + println!("Failed to initialize with real config: {}", e); + } + } +} + +// Benchmark tests (only run with --release) +#[cfg(test)] +mod benchmarks { + use super::*; + use std::time::Instant; + + #[test] + #[ignore] // Run with: cargo test --release -- --ignored + fn bench_handler_creation() { + let start = Instant::now(); + let iterations = 1000; + + for _ in 0..iterations { + let _handler = MappingHandler::new(); + } + + let duration = start.elapsed(); + println!("Created {} handlers in {:?}", iterations, duration); + println!("Average time per creation: {:?}", duration / iterations); + } + + #[test] + #[ignore] + fn bench_config_parsing() { + let config = json!({ + "version": "1.0", + "experiments": { + "test1": {"mapping_dir": "/test1", "groups": ["g1", "g2"]}, + "test2": {"mapping_dir": "/test2", "groups": ["g3", "g4"]}, + "test3": {"mapping_dir": "/test3", "groups": ["g5", "g6"]}, + }, + "data_sources": {} + }); + + let start = Instant::now(); + let iterations = 100; + + for _ in 0..iterations { + let mut handler = MappingHandler::new(); + let _ = handler.init_with_json(&config); + } + + let duration = start.elapsed(); + println!("Parsed config {} times in {:?}", iterations, duration); + println!("Average time per parse: {:?}", duration / iterations); + } +} diff --git a/scripts/git2debchangelog.sh b/scripts/git2debchangelog.sh index ca9bcf6..bc026cb 100755 --- a/scripts/git2debchangelog.sh +++ b/scripts/git2debchangelog.sh @@ -6,26 +6,25 @@ # become changelog entries. Merge commits are not excluded, so you probably # have to clean up the result manually. -RE_VERSION='^v\?[0-9]\+\([.-][0-9]\+\)*' +RE_VERSION='^[0-9]\+\(\.[0-9]\+\)\+$' # Assume the name of the current directory is the package name PACKAGE=${PWD##*/} function logentry() { - local previous=$1 - local version=$2 - echo "$PACKAGE ($version) unstable; urgency=low" - echo - git --no-pager log --format="%w(80,0,4) * %s" $previous${previous:+..}$version - echo - git --no-pager log --format=" -- %an <%ae> %aD" -n 1 $version - echo + local previous=$1 + local version=$2 + echo "$PACKAGE ($version) unstable; urgency=low" + echo + git --no-pager log --format="%w(80,0,4) * %s" $previous${previous:+..}$version + echo + git --no-pager log --format=" -- %an <%ae> %aD" -n 1 $version + echo } git tag --sort "-version:refname" | grep "$RE_VERSION" | ( - read version; while read previous; do - logentry $previous $version - version="$previous" - done - logentry "" $version + read version; while read previous; do + logentry $previous $version + version="$previous" + done + logentry "" $version ) - diff --git a/src/handlers/mapping_handler.cpp b/src/handlers/mapping_handler.cpp index d02c394..0e0003d 100644 --- a/src/handlers/mapping_handler.cpp +++ b/src/handlers/mapping_handler.cpp @@ -30,6 +30,7 @@ #include "map_types/data_source_mapping.hpp" #include "map_types/dim_mapping.hpp" #include "map_types/expr_mapping.hpp" +#include "map_types/interp_mapping.hpp" #include "map_types/map_arguments.hpp" #include "map_types/value_mapping.hpp" #include "utils/algorithm.hpp" @@ -149,28 +150,6 @@ void validate(const nlohmann::json& json, const valijson::Schema& schema) } } -void uppercase_keys(nlohmann::json& data) -{ - for (auto& entry : data) { - nlohmann::json new_entry; - if (entry.is_object()) { - for (const auto& [key, value] : entry.items()) { - new_entry[libtokamap::to_upper_copy(key)] = value; - } - entry = new_entry; - } - } -} - -[[nodiscard]] nlohmann::json load_json(const std::filesystem::path& file_path, bool to_upper = false) -{ - auto json = load_json_file(file_path); - if (to_upper) { - uppercase_keys(json); - } - return json; -} - struct MappingConfigMetadata { std::string experiment; std::string author; @@ -330,7 +309,8 @@ libtokamap::TypedDataArray libtokamap::MappingHandler::map(const ExperimentName& } const libtokamap::MapArguments map_arguments{mappings, attributes, data_type, rank, - m_trace_enabled, m_cache_enabled, m_ram_cache.get()}; + m_trace_enabled, m_cache_enabled, m_ram_cache.get(), + extra_attributes}; LIBTOKAMAP_PROFILER_ATTR(profiler, "cache_hit", false); auto result = mappings.at(map_path)->map(map_arguments); @@ -348,7 +328,7 @@ void apply_config(std::unordered_map& args, nlohman { if (plugin_config_map.contains(plugin_name)) { const auto& plugin_config = plugin_config_map[plugin_name].get(); - const auto& plugin_args = plugin_config["ARGS"].get(); + const auto& plugin_args = plugin_config["args"].get(); for (const auto& [name, arg] : plugin_args.items()) { if (!args.contains(name)) { // don't overwrite mapping arguments with global values @@ -381,7 +361,7 @@ std::optional get_float_value(const std::string& name, const nlohmann::js void init_value_mapping(libtokamap::MappingStore& map_store, const libtokamap::MappingName& mapping_name, const nlohmann::json& value) { - const auto& value_json = value.at("VALUE"); + const auto& value_json = value.at("value"); map_store.emplace(mapping_name, std::make_unique(value_json)); } @@ -389,25 +369,25 @@ void init_data_source_mapping(libtokamap::MappingStore& map_store, const libtoka const nlohmann::json& value, const nlohmann::json& group_attributes, const libtokamap::DataSourceRegistry& data_sources) { - if (!value.contains("DATA_SOURCE")) { - throw libtokamap::ConfigurationError{"required DATA_SOURCE argument not provided in DATA_SOURCE mapping '" + + if (!value.contains("data_source")) { + throw libtokamap::ConfigurationError{"required data_source argument not provided in DATA_SOURCE mapping '" + mapping_name + "'"}; } - std::string data_source_name = value["DATA_SOURCE"].get(); + std::string data_source_name = value["data_source"].get(); libtokamap::to_upper(data_source_name); - if (!value.contains("ARGS")) { - throw libtokamap::ConfigurationError{"required ARGS argument not provided in DATA_SOURCE mapping '" + + if (!value.contains("args")) { + throw libtokamap::ConfigurationError{"required args argument not provided in DATA_SOURCE mapping '" + mapping_name + "'"}; } - auto args = value["ARGS"].get(); - auto offset = get_float_value("OFFSET", value, group_attributes); - auto scale = get_float_value("SCALE", value, group_attributes); - auto slice = value.contains("SLICE") ? std::optional{value.at("SLICE").get()} + auto args = value["args"].get(); + auto offset = get_float_value("offset", value, group_attributes); + auto scale = get_float_value("scale", value, group_attributes); + auto slice = value.contains("slice") ? std::optional{value.at("slice").get()} : std::optional{}; - if (group_attributes.contains("DATA_SOURCE_CONFIG")) { - const auto& plugin_config_map = group_attributes.at("DATA_SOURCE_CONFIG"); + if (group_attributes.contains("data_source_config")) { + const auto& plugin_config_map = group_attributes.at("data_source_config"); apply_config(args, plugin_config_map, data_source_name); } @@ -423,7 +403,7 @@ void init_data_source_mapping(libtokamap::MappingStore& map_store, const libtoka void init_dim_mapping(libtokamap::MappingStore& map_store, const libtokamap::MappingName& mapping_name, const nlohmann::json& value, libtokamap::MappingCounts& mapping_counts) { - auto dim_probe = value["DIM_PROBE"].get(); + auto dim_probe = value["dim_probe"].get(); map_store.emplace(mapping_name, std::make_unique(dim_probe)); mapping_counts.increment(dim_probe); } @@ -431,23 +411,48 @@ void init_dim_mapping(libtokamap::MappingStore& map_store, const libtokamap::Map void init_expr_mapping(libtokamap::MappingStore& map_store, const libtokamap::MappingName& mapping_name, const nlohmann::json& value, libtokamap::MappingCounts& mapping_counts) { - auto expr = value["EXPR"].get(); - auto parameters = value["PARAMETERS"].get>(); + auto expr = value["expr"].get(); + auto parameters = value["parameters"].get>(); map_store.emplace(mapping_name, std::make_unique(expr, parameters)); for (const auto& [_key, param_value] : parameters) { mapping_counts.increment(param_value); } } +void init_interp_mapping(libtokamap::MappingStore& map_store, const libtokamap::MappingName& mapping_name, + const nlohmann::json& value, libtokamap::MappingCounts& mapping_counts) +{ + for (const auto& required : {"input", "base", "target", "type"}) { + if (!value.contains(required)) { + throw libtokamap::ConfigurationError{"Required " + std::string{required} + + " argument not provided in INTERP mapping '" + mapping_name + "'"}; + } + } + + const auto input = value["input"].get(); + const auto base = value["base"].get(); + const auto target = value["target"].get(); + const auto interp_type = value["type"].get(); + + if (interp_type == libtokamap::InterpType::UNKNOWN) { + throw libtokamap::ConfigurationError{"Unknown interpolation type in INTERP mapping '" + mapping_name + "'"}; + } + + map_store.emplace(mapping_name, std::make_unique(input, base, target, interp_type)); + mapping_counts.increment(input); + mapping_counts.increment(base); + mapping_counts.increment(target); +} + void init_custom_mapping(libtokamap::MappingStore& map_store, const libtokamap::MappingName& mapping_name, const nlohmann::json& value, const std::vector& library_functions, libtokamap::MappingCounts& mapping_counts) { std::vector library_paths = {}; - auto library_name = value["LIBRARY"].get(); - auto function_name = value["FUNCTION"].get(); - auto input_map = value["INPUTS"].get(); - auto params = value["PARAMETERS"]; + auto library_name = value["library"].get(); + auto function_name = value["function"].get(); + auto input_map = value["inputs"].get(); + auto params = value["parameters"]; map_store.emplace(mapping_name, std::make_unique(library_functions, library_name, function_name, input_map, params)); for (const auto& [_key, input_value] : input_map) { @@ -480,15 +485,15 @@ libtokamap::MappingStore libtokamap::MappingHandler::init_mappings(const nlohman { libtokamap::MappingStore map_store; for (const auto& [mapping_name, value] : data.items()) { - if (!value.contains("MAP_TYPE")) { - throw libtokamap::MappingError{"required MAP_TYPE argument not found in mapping '" + mapping_name + "'"}; + if (!value.contains("map_type")) { + throw libtokamap::MappingError{"required map_type argument not found in mapping '" + mapping_name + "'"}; } if (map_store.contains(mapping_name)) { throw libtokamap::MappingError{"duplicate mapping found '" + mapping_name + "'"}; } using libtokamap::MappingType; - switch (value["MAP_TYPE"].get()) { + switch (value["map_type"].get()) { case MappingType::VALUE: init_value_mapping(map_store, mapping_name, value); break; @@ -501,6 +506,9 @@ libtokamap::MappingStore libtokamap::MappingHandler::init_mappings(const nlohman case MappingType::EXPR: init_expr_mapping(map_store, mapping_name, value, m_mapping_counts); break; + case MappingType::INTERP: + init_interp_mapping(map_store, mapping_name, value, m_mapping_counts); + break; case MappingType::CUSTOM: init_custom_mapping(map_store, mapping_name, value, m_library_functions, m_mapping_counts); break; @@ -629,7 +637,7 @@ void libtokamap::MappingHandler::load_experiment(const ExperimentName& experimen const auto& mapping_dir = experiment_mapping.root_path; - auto top_level_globals = load_json(mapping_dir / "globals.json"); + auto top_level_globals = load_json_file(mapping_dir / "globals.json"); parse_globals(top_level_globals); validate(top_level_globals, m_globals_schema); experiment_mapping.top_level_globals = top_level_globals; @@ -643,13 +651,12 @@ void libtokamap::MappingHandler::load_experiment(const ExperimentName& experimen MappingPair mapping_pair; - mapping_pair.globals = load_json(partition_directory / "globals.json"); + mapping_pair.globals = load_json_file(partition_directory / "globals.json"); parse_globals(mapping_pair.globals); validate(mapping_pair.globals, m_globals_schema); mapping_pair.globals.update(top_level_globals); - constexpr bool to_upper = true; - auto mappings_json = load_json(partition_directory / "mappings.json", to_upper); + auto mappings_json = load_json_file(partition_directory / "mappings.json"); parse_mappings(mappings_json); validate(mappings_json, m_mappings_schema); mapping_pair.mappings = init_mappings(mappings_json, mapping_pair.globals); diff --git a/src/map_types/base_mapping.hpp b/src/map_types/base_mapping.hpp index 15036e7..b3d35b0 100644 --- a/src/map_types/base_mapping.hpp +++ b/src/map_types/base_mapping.hpp @@ -9,13 +9,13 @@ namespace libtokamap { -enum class MappingType : uint8_t { UNKNOWN, VALUE, DATA_SOURCE, SLICE, EXPR, CUSTOM, DIM }; +enum class MappingType : uint8_t { UNKNOWN, VALUE, DATA_SOURCE, EXPR, CUSTOM, DIM, INTERP }; NLOHMANN_JSON_SERIALIZE_ENUM(MappingType, {{MappingType::UNKNOWN, ""}, // will default to this on no match {MappingType::VALUE, "VALUE"}, {MappingType::DATA_SOURCE, "DATA_SOURCE"}, - {MappingType::SLICE, "SLICE"}, {MappingType::EXPR, "EXPR"}, + {MappingType::INTERP, "INTERP"}, {MappingType::CUSTOM, "CUSTOM"}, {MappingType::DIM, "DIMENSION"}}) diff --git a/src/map_types/data_source_mapping.cpp b/src/map_types/data_source_mapping.cpp index b4c7367..f264b9c 100644 --- a/src/map_types/data_source_mapping.cpp +++ b/src/map_types/data_source_mapping.cpp @@ -27,6 +27,11 @@ libtokamap::TypedDataArray libtokamap::DataSourceMapping::map(const MapArguments TypedDataArray array; DataSourceArgs args = m_data_source_args; + for (const auto& [key, value] : arguments.runtime_attributes.items()) { + if (!args.contains(key) && value.is_primitive() && !value.is_null()) { + args[key] = value; + } + } for (auto& [key, value] : args) { if (value.is_string()) { value = libtokamap::render(value.get(), arguments.global_data); diff --git a/src/map_types/dim_mapping.cpp b/src/map_types/dim_mapping.cpp index 4552edf..4948392 100644 --- a/src/map_types/dim_mapping.cpp +++ b/src/map_types/dim_mapping.cpp @@ -14,7 +14,7 @@ libtokamap::TypedDataArray libtokamap::DimMapping::map(const MapArguments& argum LIBTOKAMAP_PROFILER(profiler); if (!arguments.entries.contains(m_dim_probe)) { - throw libtokamap::MappingError{"invalid DIM_PROBE '" + m_dim_probe + "'"}; + throw libtokamap::MappingError{"invalid dim_probe '" + m_dim_probe + "'"}; } LIBTOKAMAP_PROFILER_ATTR(profiler, "dim_probe", m_dim_probe); @@ -31,7 +31,7 @@ libtokamap::TypedDataArray libtokamap::DimMapping::map(const MapArguments& argum // // Special case for scalar arrays // return TypedDataArray{static_cast(1)}; //} - //throw libtokamap::MappingError{"cannot use DIM_PROBE on rank 0 mapping '" + m_dim_probe + "'"}; + //throw libtokamap::MappingError{"cannot use dim_probe on rank 0 mapping '" + m_dim_probe + "'"}; } auto result = TypedDataArray{static_cast(array.shape()[dim_index])}; diff --git a/src/map_types/expr_mapping.hpp b/src/map_types/expr_mapping.hpp index dfa236e..7f20157 100644 --- a/src/map_types/expr_mapping.hpp +++ b/src/map_types/expr_mapping.hpp @@ -21,7 +21,7 @@ namespace libtokamap /** * @class ExprMapping - * @brief ExprMapping class to the hold the EXPR MAP_TYPE after parsing from the + * @brief ExprMapping class to hold expression mappings after parsing from the * JSON mapping file * * The class holds an expression std::string 'm_expr' for evaluation and diff --git a/src/map_types/interp_mapping.cpp b/src/map_types/interp_mapping.cpp new file mode 100644 index 0000000..0da7f0c --- /dev/null +++ b/src/map_types/interp_mapping.cpp @@ -0,0 +1,184 @@ +#include "map_types/interp_mapping.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "exceptions/exceptions.hpp" +#include "map_types/map_arguments.hpp" +#include "utils/typed_data_array.hpp" + +namespace +{ + +using libtokamap::DataType; +using libtokamap::InterpType; +using libtokamap::TypedDataArray; + +std::vector to_doubles(const TypedDataArray& array, const std::string& name) +{ + if (array.data_type() == DataType::Float) { + const auto* data = array.data(); + return std::vector{data, data + array.size()}; + } + if (array.data_type() == DataType::Double) { + return array.to_vector(); + } + throw libtokamap::DataTypeError{"INTERP mapping '" + name + "' must be floating point, got " + + libtokamap::data_type_name(array.data_type())}; +} + +void check_increasing(const std::vector& base, const std::string& name) +{ + for (size_t idx = 1; idx < base.size(); ++idx) { + if (base[idx] <= base[idx - 1]) { + throw libtokamap::ProcessingError{"INTERP base '" + name + + "' must be strictly increasing, but is not at index " + + std::to_string(idx)}; + } + } +} + +/** + * @brief Indices of the two base points either side of the given point + * + * Points beyond either end of the base are clamped rather than extrapolated, + * and come back as a pair of equal indices for the caller to take as-is + */ +std::pair bracket(double point, const std::vector& base) +{ + // clamp, no extrapolate + if (point <= base.front()) { + return {0, 0}; + } + if (point >= base.back()) { + const size_t last = base.size() - 1; + return {last, last}; + } + + const auto upper = std::ranges::upper_bound(base, point); + const size_t high = static_cast(upper - base.begin()); + return {high - 1, high}; +} + +/** + * @brief Straight line between each pair of bracketing base points + * + * Every target point is an independent lookup into the base, so the targets + * need not be ordered + */ +std::vector interpolate_linear(const std::vector& target, const std::vector& base, + const std::vector& input) +{ + std::vector interpolated(target.size()); + std::ranges::transform(target, interpolated.begin(), [&](double point) { + const auto [low, high] = bracket(point, base); + if (low == high) { + return input[low]; + } + // fraction of the way between the bracketing base points + const double fraction = (point - base[low]) / (base[high] - base[low]); + return std::lerp(input[low], input[high], fraction); + }); + return interpolated; +} + +/** + * @brief Interpolate the input onto the target points, the base being the + * reference the input is defined against + * + * Each interpolation type owns its own loop, so that types needing a setup pass + * over the whole base (splines, for instance) have somewhere to do it once + * + * @param interp_type which interpolation to run, eg. linear + * @param target points to evaluate at, in any order + * @param base axis the input is sampled on, strictly increasing and the same + * length as the input + * @param input data values being interpolated, one per base point + * @return one interpolated value per target point, in target order + */ +std::vector interpolate(InterpType interp_type, const std::vector& target, + const std::vector& base, const std::vector& input) +{ + switch (interp_type) { + case InterpType::LINEAR: + return interpolate_linear(target, base, input); + case InterpType::UNKNOWN: + throw libtokamap::ProcessingError{"Unknown interpolation type"}; + // AJP: add more later + } + LIBTOKAMAP_UNREACHABLE +} + +} // namespace + +libtokamap::TypedDataArray libtokamap::InterpMapping::map_interp_args(const MapArguments& arguments, + const std::string& name) const +{ + if (!arguments.entries.contains(name)) { + throw libtokamap::MappingError{"Mapping '" + name + "' referenced by INTERP mapping not found"}; + } + return arguments.entries.at(name)->map(arguments); +} + +libtokamap::TypedDataArray libtokamap::InterpMapping::map(const MapArguments& arguments) const +{ + + const auto input_array = map_interp_args(arguments, m_input); + const auto base_array = map_interp_args(arguments, m_base); + const auto target_array = map_interp_args(arguments, m_target); + + // empty check + if (input_array.empty() || base_array.empty() || target_array.empty()) { + throw libtokamap::ProcessingError{"One (or more) of the INTERP parameters are empty"}; + } + + // rank 1D check + if (input_array.rank() != 1 || base_array.rank() != 1 || target_array.rank() != 1) { + throw libtokamap::ProcessingError{"Only 1D interpolation is supported" + " - please ensure all INTERP parameters are rank 1"}; + } + + // input size match == base size check + if (input_array.size() != base_array.size()) { + throw libtokamap::ProcessingError{"INTERP data '" + m_input + "' has " + std::to_string(input_array.size()) + + " elements but base '" + m_base + "' has " + + std::to_string(base_array.size())}; + } + + // Error for single point array check + if (base_array.size() < 2) { + throw libtokamap::ProcessingError{"INTERP base '" + m_base + + "' must have at least 2 points, got " + + std::to_string(base_array.size())}; + } + + const auto input = to_doubles(input_array, m_input); + const auto base = to_doubles(base_array, m_base); + const auto target = to_doubles(target_array, m_target); + + // ascending check + check_increasing(base, m_base); + + const auto interpolated = interpolate(m_interp_type, target, base, input); + + // use float if originally float + if (input_array.data_type() == DataType::Float) { + std::vector result; + result.reserve(interpolated.size()); + + std::transform( + interpolated.begin(), + interpolated.end(), + std::back_inserter(result), + [](double value) { return static_cast(value); }); + + return TypedDataArray{std::move(result)}; + } + return TypedDataArray{std::move(interpolated)}; +} diff --git a/src/map_types/interp_mapping.hpp b/src/map_types/interp_mapping.hpp new file mode 100644 index 0000000..1be029e --- /dev/null +++ b/src/map_types/interp_mapping.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include "map_types/base_mapping.hpp" +#include "map_types/map_arguments.hpp" +#include "utils/typed_data_array.hpp" + +namespace libtokamap +{ + +enum class InterpType : short { UNKNOWN, LINEAR /*CUBIC, SPLINE, NEAREST*/ }; + +NLOHMANN_JSON_SERIALIZE_ENUM(InterpType, {{InterpType::UNKNOWN, ""}, // will default to this on no match + {InterpType::LINEAR, "LINEAR"}}) + +class InterpMapping : public Mapping +{ + public: + InterpMapping() = delete; + InterpMapping(std::string input, std::string base, std::string target, InterpType interp_type) + : m_input{std::move(input)}, m_base{std::move(base)}, m_target{std::move(target)}, m_interp_type{interp_type} {}; + + [[nodiscard]] TypedDataArray map(const MapArguments& arguments) const override; + + private: + std::string m_input; + std::string m_base; + std::string m_target; + InterpType m_interp_type; + + [[nodiscard]] TypedDataArray map_interp_args(const MapArguments& arguments, const std::string& name) const; +}; + +} // namespace libtokamap diff --git a/src/map_types/map_arguments.hpp b/src/map_types/map_arguments.hpp index 1eaca78..183ec8c 100644 --- a/src/map_types/map_arguments.hpp +++ b/src/map_types/map_arguments.hpp @@ -24,13 +24,30 @@ struct MapArguments { bool trace_enabled; bool cache_enabled; RamCache* ram_cache; + const nlohmann::json& runtime_attributes; explicit MapArguments(const std::unordered_map>& entries, const nlohmann::json& global_data, const DataType data_type, const int rank, const bool trace_enabled, const bool cache_enabled, RamCache* ram_cache) + : MapArguments(entries, global_data, data_type, rank, trace_enabled, cache_enabled, ram_cache, + empty_runtime_attributes()) + { + } + + explicit MapArguments(const std::unordered_map>& entries, + const nlohmann::json& global_data, const DataType data_type, const int rank, + const bool trace_enabled, const bool cache_enabled, RamCache* ram_cache, + const nlohmann::json& runtime_attributes) : entries{entries}, global_data{global_data}, data_type{data_type}, rank{rank}, trace_enabled{trace_enabled}, - cache_enabled{cache_enabled}, ram_cache{ram_cache} + cache_enabled{cache_enabled}, ram_cache{ram_cache}, runtime_attributes{runtime_attributes} + { + } + + private: + static const nlohmann::json& empty_runtime_attributes() { + static const nlohmann::json empty = nlohmann::json::object(); + return empty; } }; diff --git a/src/utils/syntax_parser.cpp b/src/utils/syntax_parser.cpp index f59869d..e9c4e17 100644 --- a/src/utils/syntax_parser.cpp +++ b/src/utils/syntax_parser.cpp @@ -57,10 +57,6 @@ void walk_json(nlohmann::json& root) stack.pop(); for (const auto& element : current->items()) { - if (element.key() == "MAP_TYPE") { - continue; // Skip MAP_TYPE elements - } - auto& node = element.value(); if (node.is_string()) { node = libtokamap::process_string_node(node); @@ -105,13 +101,13 @@ nlohmann::json libtokamap::expand_syntactic_sugar(nlohmann::json input) std::string str = input; if (!str.empty() && str[0] == '@') { str = str.substr(1); - input = {{"MAP_TYPE", "FORWARD"}, {"VALUE", str}}; + input = {{"map_type", "FORWARD"}, {"value", str}}; } else { - input = {{"MAP_TYPE", "VALUE"}, {"VALUE", str}}; + input = {{"map_type", "VALUE"}, {"value", str}}; } } else if (input.is_primitive()) { // parse simple non-string value - input = {{"MAP_TYPE", "VALUE"}, {"VALUE", input}}; + input = {{"map_type", "VALUE"}, {"value", input}}; } // walk object looking for strings with #N diff --git a/src/utils/typed_data_array.hpp b/src/utils/typed_data_array.hpp index 147f371..ca5acaa 100644 --- a/src/utils/typed_data_array.hpp +++ b/src/utils/typed_data_array.hpp @@ -8,12 +8,20 @@ #include #include #include -#include #include #include #include #include #include +#if __cplusplus >= 202002L +# include +# define TM_BIT_CAST std::bit_cast +#else +# define TM_BIT_CAST reinterpret_cast +#endif + +// #include +// #include #include "exceptions/exceptions.hpp" #include "utils/compiler.hpp" @@ -204,7 +212,7 @@ class TypedDataArray : m_data_type{data_type_of()}, m_size{array.size()}, m_shape{std::move(shape)}, m_owning{true} { m_buffer = static_cast(malloc(m_size * sizeof(T))); - std::memcpy(m_buffer, reinterpret_cast(array.data()), m_size * sizeof(T)); + std::memcpy(m_buffer, TM_BIT_CAST(array.data()), m_size * sizeof(T)); if (m_shape.empty()) { m_shape.push_back(m_size); } @@ -216,9 +224,9 @@ class TypedDataArray { if (m_owning) { m_buffer = static_cast(malloc(m_size * sizeof(T))); - std::memcpy(m_buffer, reinterpret_cast(array), m_size * sizeof(T)); + std::memcpy(m_buffer, TM_BIT_CAST(array), m_size * sizeof(T)); } else { - m_buffer = reinterpret_cast(array); + m_buffer = TM_BIT_CAST(array); } } @@ -226,7 +234,7 @@ class TypedDataArray explicit TypedDataArray(const T value) : m_data_type{data_type_of()}, m_size{1}, m_owning{true} { m_buffer = static_cast(malloc(sizeof(T))); - std::memcpy(m_buffer, reinterpret_cast(&value), sizeof(T)); + std::memcpy(m_buffer, TM_BIT_CAST(&value), sizeof(T)); } explicit TypedDataArray(const std::string& value) @@ -263,7 +271,7 @@ class TypedDataArray throw libtokamap::DataTypeError{"invalid type given to apply"}; } - auto* data = reinterpret_cast(m_buffer); + auto* data = TM_BIT_CAST(m_buffer); for (size_t idx = 0; idx < m_size; ++idx) { data[idx] = static_cast((static_cast(data[idx]) * scale_factor) + offset); } @@ -294,10 +302,10 @@ class TypedDataArray new_size *= len; } - auto* array = reinterpret_cast(m_buffer); + auto* array = TM_BIT_CAST(m_buffer); auto* new_buffer = static_cast(malloc(sizeof(T) * new_size)); - auto* new_array = reinterpret_cast(new_buffer); + auto* new_array = TM_BIT_CAST(new_buffer); auto offsets = compute_offsets(m_shape, subsets); size_t idx = 0; @@ -351,8 +359,8 @@ class TypedDataArray new_array.m_buffer = new char[m_size * sizeof(To)]; new_array.m_owning = true; - From* data = std::bit_cast(m_buffer); - std::copy(data, data + m_size, std::bit_cast(new_array.m_buffer)); + From* data = TM_BIT_CAST(m_buffer); + std::copy(data, data + m_size, TM_BIT_CAST(new_array.m_buffer)); return new_array; } @@ -363,7 +371,7 @@ class TypedDataArray if (m_data_type != data_type_of()) { throw libtokamap::DataTypeError{"invalid type given to span"}; } - return std::span{reinterpret_cast(m_buffer), m_size}; + return std::span{TM_BIT_CAST(m_buffer), m_size}; } #endif @@ -372,7 +380,7 @@ class TypedDataArray if (m_data_type != data_type_of()) { throw libtokamap::DataTypeError{"invalid type given to data"}; } - return reinterpret_cast(m_buffer); + return TM_BIT_CAST(m_buffer); } template [[nodiscard]] std::vector to_vector() const @@ -380,7 +388,7 @@ class TypedDataArray if (m_data_type != data_type_of()) { throw libtokamap::DataTypeError{"invalid type given to to_vector"}; } - const T* ptr = reinterpret_cast(m_buffer); + const T* ptr = TM_BIT_CAST(m_buffer); return std::vector{ptr, ptr + m_size}; } @@ -445,6 +453,24 @@ class TypedDataArray void set_trace(nlohmann::json trace) { m_trace = std::move(trace); } [[nodiscard]] const nlohmann::json& trace() const { return m_trace; } + // template + // xt::xarray as_xtensor() { + // if (m_data_type != data_type_of()) { + // throw libtokamap::DataTypeError{"invalid type given to as_xtensor"}; + // } + // T* data = TM_BIT_CAST(m_buffer); + // return xt::adapt(data, m_size, xt::no_ownership{}, m_shape); + // } + + // template + // xt::xarray to_xtensor() { + // if (m_data_type != data_type_of()) { + // throw libtokamap::DataTypeError{"invalid type given to into_xtensor"}; + // } + // T* data = TM_BIT_CAST(m_buffer); + // return xt::adapt(data, m_size, xt::acquire_ownership{}, m_shape); + // } + private: char* m_buffer = nullptr; DataType m_data_type; diff --git a/src/utils/types.hpp b/src/utils/types.hpp index 2983fb9..b7f2f39 100644 --- a/src/utils/types.hpp +++ b/src/utils/types.hpp @@ -48,10 +48,10 @@ enum class DirectorySelector : uint8_t { Undefined, MaxBelow, MinAbove, Exact, C NLOHMANN_JSON_SERIALIZE_ENUM(libtokamap::DirectorySelector, { {libtokamap::DirectorySelector::Undefined, nullptr}, - {libtokamap::DirectorySelector::MaxBelow, "max_below"}, - {libtokamap::DirectorySelector::MinAbove, "min_above"}, - {libtokamap::DirectorySelector::Exact, "exact"}, - {libtokamap::DirectorySelector::Closest, "closest"}, + {libtokamap::DirectorySelector::MaxBelow, "MAX_BELOW"}, + {libtokamap::DirectorySelector::MinAbove, "MIN_ABOVE"}, + {libtokamap::DirectorySelector::Exact, "EXACT"}, + {libtokamap::DirectorySelector::Closest, "CLOSEST"}, }) struct MappingPartition { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 80bb9d0..ed1957d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,6 +14,7 @@ set( TEST_SOURCES src/data_source_mapping_test.cpp src/dim_mapping_test.cpp src/indices_test.cpp + src/interp_mapping_test.cpp src/mapping_locator_test.cpp src/parse_slices_test.cpp src/render_test.cpp @@ -57,10 +58,10 @@ target_link_libraries( libtokamap_tests ) if( ENABLE_COVERAGE ) - include( Coverage ) - add_llvm_coverage( libtokamap_tests ${CMAKE_CURRENT_BINARY_DIR}/libtokamap_tests ) - # target_compile_options( libtokamap_tests PRIVATE -fprofile-instr-generate -fcoverage-mapping ) - # target_link_options( libtokamap_tests PRIVATE -fprofile-instr-generate ) + include( Coverage ) + add_llvm_coverage( libtokamap_tests ${CMAKE_CURRENT_BINARY_DIR}/libtokamap_tests ) + # target_compile_options( libtokamap_tests PRIVATE -fprofile-instr-generate -fcoverage-mapping ) + # target_link_options( libtokamap_tests PRIVATE -fprofile-instr-generate ) endif() # CTest integration diff --git a/test/src/dim_mapping_test.cpp b/test/src/dim_mapping_test.cpp index 584456f..e9ea1b6 100644 --- a/test/src/dim_mapping_test.cpp +++ b/test/src/dim_mapping_test.cpp @@ -58,12 +58,11 @@ make_map_arguments(std::unordered_map(test_json.at("DIM_PROBE")); + auto mapping = std::make_unique(test_json.at("dim_probe")); REQUIRE(mapping != nullptr); } @@ -200,7 +199,7 @@ TEST_CASE("DimMapping returns expected dimension sizes", "[dim_mapping]") TEST_CASE("DimMapping error handling", "[dim_mapping_errors]") { - SECTION("Throws error when DIM_PROBE not found") + SECTION("Throws error when dim_probe not found") { auto dim_mapping = std::make_unique("nonexistent_probe"); @@ -209,6 +208,6 @@ TEST_CASE("DimMapping error handling", "[dim_mapping_errors]") REQUIRE_THROWS_AS(dim_mapping->map(map_args), MappingError); REQUIRE_THROWS_WITH(dim_mapping->map(map_args), - Catch::Matchers::ContainsSubstring("invalid DIM_PROBE 'nonexistent_probe'")); + Catch::Matchers::ContainsSubstring("invalid dim_probe 'nonexistent_probe'")); } } diff --git a/test/src/interp_mapping_test.cpp b/test/src/interp_mapping_test.cpp new file mode 100644 index 0000000..e47be61 --- /dev/null +++ b/test/src/interp_mapping_test.cpp @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include +#include + +#include "exceptions/exceptions.hpp" +#include "map_types/base_mapping.hpp" +#include "map_types/interp_mapping.hpp" +#include "map_types/map_arguments.hpp" +#include "map_types/value_mapping.hpp" +#include "utils/typed_data_array.hpp" + +#include +#include +#include + +using namespace libtokamap; + +namespace +{ + +using MapEntries = std::unordered_map>; + +MapArguments make_map_arguments(MapEntries& entries) +{ + static nlohmann::json empty_global_data = nlohmann::json::object(); + + constexpr bool trace_enabled = false; + constexpr bool cache_enabled = false; + constexpr RamCache* ram_cache = nullptr; + + return MapArguments(entries, empty_global_data, DataType::Float, 1, trace_enabled, cache_enabled, ram_cache); +} + +MapEntries make_entries(const nlohmann::json& signal, const nlohmann::json& time, const nlohmann::json& new_time) +{ + MapEntries entries; + entries["signal"] = std::make_unique(signal); + entries["time"] = std::make_unique(time); + entries["new_time"] = std::make_unique(new_time); + return entries; +} + +InterpMapping make_interp_mapping() +{ + return InterpMapping{"signal", "time", "new_time", InterpType::LINEAR}; +} + +} // namespace + +TEST_CASE("InterpMapping interpolates onto a new time base", "[interp_mapping]") +{ + // throw a few situations, on the nose, out of bounds, inbetween, all the rest + auto entries = make_entries( + {10.0, 20.0, 30.0, 40.0}, + {0.0, 1.0, 2.0, 3.0}, + {-1.0, 0.0, 0.5, 1.5, 2.0, 3.0, 5.0} + ); + MapArguments map_args = make_map_arguments(entries); + + const auto result = make_interp_mapping().map(map_args); + + REQUIRE(result.rank() == 1); + REQUIRE(result.size() == 7); + REQUIRE(result.data_type() == DataType::Float); + + // points outside the base are clamped rather than extrapolated + const std::vector expected{10.0F, 10.0F, 15.0F, 25.0F, 30.0F, 40.0F, 40.0F}; + const auto actual = result.to_vector(); + for (size_t idx = 0; idx < expected.size(); ++idx) { + INFO("index " << idx); + REQUIRE(actual[idx] == Catch::Approx(expected[idx])); + } +} + +TEST_CASE("InterpMapping rejects arguments it cannot interpolate", "[interp_mapping_errors]") +{ + SECTION("non floating point data") + { + auto entries = make_entries({10, 20}, {0.0, 1.0}, {0.5}); + MapArguments map_args = make_map_arguments(entries); + + REQUIRE_THROWS_AS(make_interp_mapping().map(map_args), DataTypeError); + REQUIRE_THROWS_WITH(make_interp_mapping().map(map_args), + Catch::Matchers::ContainsSubstring("must be floating point")); + } + + SECTION("input and base of different lengths") + { + auto entries = make_entries({10.0, 20.0, 30.0}, {0.0, 1.0}, {0.5}); + MapArguments map_args = make_map_arguments(entries); + + REQUIRE_THROWS_AS(make_interp_mapping().map(map_args), ProcessingError); + } + + SECTION("base that is not strictly increasing") + { + auto entries = make_entries({10.0, 20.0, 30.0, 40.0}, {0.0, 2.0, 1.0, 3.0}, {0.5}); + MapArguments map_args = make_map_arguments(entries); + + REQUIRE_THROWS_AS(make_interp_mapping().map(map_args), ProcessingError); + REQUIRE_THROWS_WITH(make_interp_mapping().map(map_args), + Catch::Matchers::ContainsSubstring("must be strictly increasing")); + } + + SECTION("a base with only one point") + { + auto entries = make_entries({10.0}, {0.0}, {0.5}); + MapArguments map_args = make_map_arguments(entries); + + REQUIRE_THROWS_AS(make_interp_mapping().map(map_args), ProcessingError); + REQUIRE_THROWS_WITH(make_interp_mapping().map(map_args), + Catch::Matchers::ContainsSubstring("must have at least 2 points")); + } +} diff --git a/test/src/syntax_parser_test.cpp b/test/src/syntax_parser_test.cpp index 444d95a..81b5224 100644 --- a/test/src/syntax_parser_test.cpp +++ b/test/src/syntax_parser_test.cpp @@ -3,13 +3,39 @@ #include "utils/syntax_parser.hpp" +namespace +{ + +bool contains_key_recursive(const nlohmann::json& node, const std::string& key) +{ + if (node.is_object()) { + if (node.contains(key)) { + return true; + } + for (const auto& value : node) { + if (contains_key_recursive(value, key)) { + return true; + } + } + } else if (node.is_array()) { + for (const auto& value : node) { + if (contains_key_recursive(value, key)) { + return true; + } + } + } + return false; +} + +} // namespace + TEST_CASE("Parse forward mapping", "[syntax_parser]") { SECTION("single element") { nlohmann::json input = "@FOO"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "FORWARD" }, - { "VALUE", "FOO" } + { "map_type", "FORWARD" }, + { "value", "FOO" } }; REQUIRE(result == expected); } @@ -18,8 +44,8 @@ TEST_CASE("Parse forward mapping", "[syntax_parser]") { nlohmann::json input = "@/A/B/C"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "FORWARD" }, - { "VALUE", "/A/B/C" } + { "map_type", "FORWARD" }, + { "value", "/A/B/C" } }; REQUIRE(result == expected); } @@ -30,8 +56,8 @@ TEST_CASE("Parse value mapping", "[syntax_parser]") { nlohmann::json input = "foo"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", "foo" } + { "map_type", "VALUE" }, + { "value", "foo" } }; REQUIRE(result == expected); } @@ -40,8 +66,8 @@ TEST_CASE("Parse value mapping", "[syntax_parser]") { nlohmann::json input = 3; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", 3 } + { "map_type", "VALUE" }, + { "value", 3 } }; REQUIRE(result == expected); } @@ -50,11 +76,29 @@ TEST_CASE("Parse value mapping", "[syntax_parser]") { nlohmann::json input = 3.14; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", 3.14 } + { "map_type", "VALUE" }, + { "value", 3.14 } }; REQUIRE(result == expected); } + + SECTION("does not emit legacy uppercase keys for integer value sugar") { + nlohmann::json input = 1; + nlohmann::json result = libtokamap::expand_syntactic_sugar(input); + REQUIRE(result.at("map_type") == "VALUE"); + REQUIRE(result.at("value") == 1); + REQUIRE_FALSE(contains_key_recursive(result, "MAP_TYPE")); + REQUIRE_FALSE(contains_key_recursive(result, "VALUE")); + } + + SECTION("does not emit legacy uppercase keys for string value sugar") { + nlohmann::json input = "hello"; + nlohmann::json result = libtokamap::expand_syntactic_sugar(input); + REQUIRE(result.at("map_type") == "VALUE"); + REQUIRE(result.at("value") == "hello"); + REQUIRE_FALSE(contains_key_recursive(result, "MAP_TYPE")); + REQUIRE_FALSE(contains_key_recursive(result, "VALUE")); + } } // walk for strings @@ -67,8 +111,8 @@ TEST_CASE("Parse indices expansion", "[syntax_parser]") { nlohmann::json input = "{{ #3 }}"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", "{{ indices.3 }}" } + { "map_type", "VALUE" }, + { "value", "{{ indices.3 }}" } }; REQUIRE(result == expected); } @@ -78,8 +122,8 @@ TEST_CASE("Parse indices expansion", "[syntax_parser]") { nlohmann::json input = "foo/{{ #0 }}/bar"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", "foo/{{ indices.0 }}/bar" } + { "map_type", "VALUE" }, + { "value", "foo/{{ indices.0 }}/bar" } }; REQUIRE(result == expected); } @@ -88,23 +132,23 @@ TEST_CASE("Parse indices expansion", "[syntax_parser]") { nlohmann::json input = "foo/{{ #0 }}/bar/{{ #1 }}/baz"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", "foo/{{ indices.0 }}/bar/{{ indices.1 }}/baz" } + { "map_type", "VALUE" }, + { "value", "foo/{{ indices.0 }}/bar/{{ indices.1 }}/baz" } }; REQUIRE(result == expected); } SECTION("simple index as nested field") { nlohmann::json input = { - { "MAP_TYPE", "PLUGIN" }, - { "ARGS", { + { "map_type", "PLUGIN" }, + { "args", { { "signal", "{{ #0 }}" }, } } }; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "PLUGIN" }, - { "ARGS", { + { "map_type", "PLUGIN" }, + { "args", { { "signal", "{{ indices.0 }}" }, } } }; @@ -116,8 +160,8 @@ TEST_CASE("Parse indices expansion", "[syntax_parser]") { nlohmann::json input = "{{ foo[#2] }}"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", "{{ at(foo, indices.2) }}" } + { "map_type", "VALUE" }, + { "value", "{{ at(foo, indices.2) }}" } }; REQUIRE(result == expected); } @@ -126,8 +170,8 @@ TEST_CASE("Parse indices expansion", "[syntax_parser]") { nlohmann::json input = "{{ foo[#1].bar }}"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", "{{ at(foo, indices.1).bar }}" } + { "map_type", "VALUE" }, + { "value", "{{ at(foo, indices.1).bar }}" } }; REQUIRE(result == expected); } @@ -136,8 +180,8 @@ TEST_CASE("Parse indices expansion", "[syntax_parser]") { nlohmann::json input = "{{ (foo[#0].bar)[#1] }}"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", "{{ at(at(foo, indices.0).bar, indices.1) }}" } + { "map_type", "VALUE" }, + { "value", "{{ at(at(foo, indices.0).bar, indices.1) }}" } }; REQUIRE(result == expected); } @@ -146,8 +190,8 @@ TEST_CASE("Parse indices expansion", "[syntax_parser]") { nlohmann::json input = "{{ (foo[#0].bar)[#1].baz }}"; nlohmann::json result = libtokamap::expand_syntactic_sugar(input); nlohmann::json expected = { - { "MAP_TYPE", "VALUE" }, - { "VALUE", "{{ at(at(foo, indices.0).bar, indices.1).baz }}" } + { "map_type", "VALUE" }, + { "value", "{{ at(at(foo, indices.0).bar, indices.1).baz }}" } }; REQUIRE(result == expected); } diff --git a/test/src/value_mapping_test.cpp b/test/src/value_mapping_test.cpp index 71bd7c7..ba16348 100644 --- a/test/src/value_mapping_test.cpp +++ b/test/src/value_mapping_test.cpp @@ -38,40 +38,32 @@ libtokamap::MapArguments make_map_arguments(const DataType data_type, const int TEST_CASE("ValueMapping can be constructed from JSON", "[value_mapping]") { - // Setup test fixture - nlohmann::json test_json = {{"MAP_TYPE", "VALUE"}, {"VALUE", 42}}; + nlohmann::json test_json = {{"map_type", "VALUE"}, {"value", 42}}; SECTION("Constructor works with integer value") { - auto mapping = std::make_unique(test_json); + auto mapping = std::make_unique(test_json.at("value")); REQUIRE(mapping != nullptr); } SECTION("Constructor works with float value") { - test_json["VALUE"] = 3.14; - auto mapping = std::make_unique(test_json); + test_json["value"] = 3.14; + auto mapping = std::make_unique(test_json.at("value")); REQUIRE(mapping != nullptr); } SECTION("Constructor works with string value") { - test_json["VALUE"] = "test_string"; - auto mapping = std::make_unique(test_json); + test_json["value"] = "test_string"; + auto mapping = std::make_unique(test_json.at("value")); REQUIRE(mapping != nullptr); } SECTION("Constructor works with array value") { - test_json["VALUE"] = {1, 2, 3, 4, 5}; - auto mapping = std::make_unique(test_json); - REQUIRE(mapping != nullptr); - } - - SECTION("Constructor works with object value") - { - test_json["VALUE"] = {{"key1", "value1"}, {"key2", 2}}; - auto mapping = std::make_unique(test_json); + test_json["value"] = {1, 2, 3, 4, 5}; + auto mapping = std::make_unique(test_json.at("value")); REQUIRE(mapping != nullptr); } } @@ -79,13 +71,13 @@ TEST_CASE("ValueMapping can be constructed from JSON", "[value_mapping]") TEST_CASE("ValueMapping returns expected data for different 0D types", "[value_mapping_0D]") { - nlohmann::json test_json = {{"VALUE", 42}}; + nlohmann::json test_json = {{"value", 42}}; SECTION("Integer values are correctly returned") { - test_json["VALUE"] = 42; + test_json["value"] = 42; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Int32, 0); @@ -99,9 +91,9 @@ TEST_CASE("ValueMapping returns expected data for different 0D types", "[value_m SECTION("Negative integer values are correctly returned") { - test_json["VALUE"] = -42; + test_json["value"] = -42; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Int32, 0); @@ -115,9 +107,9 @@ TEST_CASE("ValueMapping returns expected data for different 0D types", "[value_m SECTION("String values are correctly returned") { - test_json["VALUE"] = "Hello World!"; + test_json["value"] = "Hello World!"; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Int8, 1); @@ -131,9 +123,9 @@ TEST_CASE("ValueMapping returns expected data for different 0D types", "[value_m SECTION("Float values are correctly returned") { - test_json["VALUE"] = 42.75; + test_json["value"] = 42.75; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Float, 0); @@ -147,9 +139,9 @@ TEST_CASE("ValueMapping returns expected data for different 0D types", "[value_m SECTION("Negative float values are correctly returned") { - test_json["VALUE"] = -42.75; + test_json["value"] = -42.75; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Float, 0); @@ -165,14 +157,14 @@ TEST_CASE("ValueMapping returns expected data for different 0D types", "[value_m TEST_CASE("ValueMapping returns expected data for different 1D types", "[value_mapping_1D]") { - nlohmann::json test_json = {{"VALUE", {0, 0, 0}}}; + nlohmann::json test_json = {{"value", {0, 0, 0}}}; SECTION("1D integer arrays are correctly returned") { std::vector test_vector_1d{1, 2, 3, 4}; - test_json["VALUE"] = test_vector_1d; + test_json["value"] = test_vector_1d; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Int32, 1); @@ -189,9 +181,9 @@ TEST_CASE("ValueMapping returns expected data for different 1D types", "[value_m SECTION("1D negative integer arrays are correctly returned") { std::vector test_vector_1d{-1, 2, -3, 4}; - test_json["VALUE"] = test_vector_1d; + test_json["value"] = test_vector_1d; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Int32, 1); @@ -208,9 +200,9 @@ TEST_CASE("ValueMapping returns expected data for different 1D types", "[value_m SECTION("1D float arrays are correctly returned") { std::vector test_vector_1d{0.1, 0.2, 0.3, 0.4}; - test_json["VALUE"] = test_vector_1d; + test_json["value"] = test_vector_1d; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Float, 1); @@ -227,9 +219,9 @@ TEST_CASE("ValueMapping returns expected data for different 1D types", "[value_m SECTION("1D negative float arrays are correctly returned") { std::vector test_vector_1d{0.1, -0.2, 0.3, -0.4}; - test_json["VALUE"] = test_vector_1d; + test_json["value"] = test_vector_1d; - const auto& value_json = test_json.at("VALUE"); + const auto& value_json = test_json.at("value"); auto mapping = std::make_unique(value_json); MapArguments map_args = make_map_arguments(DataType::Float, 1);