diff --git a/.github/conda_pgm_env.yml b/.github/conda_pgm_env.yml index 1845c03ec8..6c3be6c6cf 100644 --- a/.github/conda_pgm_env.yml +++ b/.github/conda_pgm_env.yml @@ -5,7 +5,7 @@ name: conda-pgm-env dependencies: # build env - - python=3.12 + - python=3.14 - pip - scikit-build-core # build deps @@ -16,6 +16,7 @@ dependencies: - numpy - cmake - ninja + - cli11 # test deps - pytest - pytest-cov diff --git a/.github/workflows/build-test-release.yml b/.github/workflows/build-test-release.yml index d87186b1a7..fbd2ec3486 100644 --- a/.github/workflows/build-test-release.yml +++ b/.github/workflows/build-test-release.yml @@ -278,10 +278,15 @@ jobs: cmake --install build/ - name: Build python - run: python -m pip install . --no-build-isolation --no-deps -C wheel.cmake=false + run: | + python conda_build_preparation.py + python -m pip install . --no-build-isolation --no-deps -C wheel.cmake=false - name: Test - run: pytest + run: | + pytest + power-grid-model --help + power-grid-model --version github-release: name: Create and release assets to GitHub diff --git a/CMakeLists.txt b/CMakeLists.txt index 1bb14a5b44..8535425704 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ find_package(Boost REQUIRED) find_package(Eigen3 CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(msgpack-cxx REQUIRED) +find_package(CLI11 CONFIG REQUIRED) if(NOT WIN32) # thread diff --git a/cmake/pgm_version.cmake b/cmake/pgm_version.cmake index 816b6f651f..9cdd26eadc 100644 --- a/cmake/pgm_version.cmake +++ b/cmake/pgm_version.cmake @@ -13,3 +13,10 @@ string( PGM_VERSION "${_PGM_VERSION_STRIPPED}" ) +string( + REGEX REPLACE + "^([0-9]+)\\.[0-9]+(\\.[0-9]+)?.*" + "\\1" + PGM_VERSION_MAJOR + "${PGM_VERSION}" +) diff --git a/conda_build_preparation.py b/conda_build_preparation.py new file mode 100644 index 0000000000..7a6d1497dc --- /dev/null +++ b/conda_build_preparation.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +# This script modifies the pyproject.toml file to remove specific sections +# [project.entry-points."cmake.root"] and [project.scripts] +# and deletes the run_pgm_cli.py file from the source directory. +# It is intended to be run as part of the conda build preparation process. +# So that conda environment will not be confused with PyPI style Python shim and entry points. + +import re +from pathlib import Path + +# Read the root pyproject.toml +pyproject_path = Path(__file__).parent / "pyproject.toml" +content = pyproject_path.read_text() + +# Remove [project.entry-points."cmake.root"] section +content = re.sub(r'\n\[project\.entry-points\."cmake\.root"\].*?(?=\n\[|\Z)', "", content, flags=re.DOTALL) + +# Remove [project.scripts] section +content = re.sub(r"\n\[project\.scripts\].*?(?=\n\[|\Z)", "", content, flags=re.DOTALL) + +# Write back to pyproject.toml +pyproject_path.write_text(content) + +# Remove run_pgm_cli.py file +run_pgm_cli_path = ( + Path(__file__).parent / "src" / "power_grid_model" / "_core" / "power_grid_model_c" / "run_pgm_cli.py" +) +run_pgm_cli_path.unlink(missing_ok=True) diff --git a/power_grid_model_c/CMakeLists.txt b/power_grid_model_c/CMakeLists.txt index 698351326f..5559e72375 100644 --- a/power_grid_model_c/CMakeLists.txt +++ b/power_grid_model_c/CMakeLists.txt @@ -5,3 +5,4 @@ add_subdirectory("power_grid_model") add_subdirectory("power_grid_model_c") add_subdirectory("power_grid_model_cpp") +add_subdirectory("power_grid_model_cli") diff --git a/power_grid_model_c/power_grid_model_c/CMakeLists.txt b/power_grid_model_c/power_grid_model_c/CMakeLists.txt index 14a8ad9868..4fea23cee5 100644 --- a/power_grid_model_c/power_grid_model_c/CMakeLists.txt +++ b/power_grid_model_c/power_grid_model_c/CMakeLists.txt @@ -31,6 +31,8 @@ file( target_link_libraries(power_grid_model_c PRIVATE power_grid_model) +target_compile_definitions(power_grid_model_c PRIVATE PGM_VERSION="${PGM_VERSION}") + target_sources( power_grid_model_c PUBLIC @@ -44,7 +46,7 @@ set_target_properties( power_grid_model_c PROPERTIES VERSION ${PGM_VERSION} - SOVERSION ${PGM_VERSION} + SOVERSION ${PGM_VERSION_MAJOR} INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO TRUE ) diff --git a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/handle.h b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/handle.h index c06b5f43d4..4ca0d958eb 100644 --- a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/handle.h +++ b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/handle.h @@ -104,6 +104,13 @@ PGM_API char const** PGM_batch_errors(PGM_Handle const* handle); */ PGM_API void PGM_clear_error(PGM_Handle* handle); +/** + * @brief Get the version of the Power Grid Model library. + * + * @return A pointer to a zero-terminated string representing the version. + */ +PGM_API char const* PGM_version(void); + #ifdef __cplusplus } #endif diff --git a/power_grid_model_c/power_grid_model_c/src/handle.cpp b/power_grid_model_c/power_grid_model_c/src/handle.cpp index 1bfb5e3763..20dee456d9 100644 --- a/power_grid_model_c/power_grid_model_c/src/handle.cpp +++ b/power_grid_model_c/power_grid_model_c/src/handle.cpp @@ -18,6 +18,8 @@ using namespace power_grid_model; using power_grid_model_c::clear_error; using power_grid_model_c::compile_time_safe_cast; + +constexpr char const* version = PGM_VERSION; } // namespace // create and destroy handle @@ -52,3 +54,4 @@ char const** PGM_batch_errors(PGM_Handle const* handle) { return handle_ref.batch_errs_c_str.data(); } void PGM_clear_error(PGM_Handle* handle) { clear_error(handle); } +char const* PGM_version(void) { return version; } diff --git a/power_grid_model_c/power_grid_model_cli/CMakeLists.txt b/power_grid_model_c/power_grid_model_cli/CMakeLists.txt new file mode 100644 index 0000000000..279d232703 --- /dev/null +++ b/power_grid_model_c/power_grid_model_cli/CMakeLists.txt @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +add_executable(power_grid_model_cli + main.cpp + cli_options.cpp + pgm_calculation.cpp +) + +target_link_libraries(power_grid_model_cli PRIVATE power_grid_model_c) +target_link_libraries(power_grid_model_cli PRIVATE power_grid_model_cpp) +target_link_libraries(power_grid_model_cli PRIVATE CLI11::CLI11) + +set_property(TARGET power_grid_model_cli PROPERTY INSTALL_RPATH_USE_LINK_PATH FALSE) +set_property(TARGET power_grid_model_cli PROPERTY OUTPUT_NAME "power-grid-model") +if(APPLE) + set_property(TARGET power_grid_model_cli PROPERTY INSTALL_RPATH "@loader_path/../${CMAKE_INSTALL_LIBDIR}") +elseif(UNIX) # Linux, BSD (not Windows/macOS) + set_property(TARGET power_grid_model_cli PROPERTY INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}") +endif() + + +target_compile_definitions( + power_grid_model_cli + PRIVATE PGM_ENABLE_EXPERIMENTAL +) + +install( + TARGETS power_grid_model_cli + EXPORT power_grid_modelTargets + COMPONENT power_grid_model +) diff --git a/power_grid_model_c/power_grid_model_cli/cli_functions.hpp b/power_grid_model_c/power_grid_model_cli/cli_functions.hpp new file mode 100644 index 0000000000..871dd758a8 --- /dev/null +++ b/power_grid_model_c/power_grid_model_cli/cli_functions.hpp @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include + +#include +#include +#include +#include +#include + +namespace power_grid_model_cpp { + +struct CLIResult { + int exit_code; + bool should_exit; + + operator bool() const { return should_exit || exit_code != 0; } +}; + +// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) +struct ClIOptions { + std::filesystem::path input_file; + std::vector batch_update_file; + std::filesystem::path output_file; + PGM_SerializationFormat input_serialization_format{PGM_json}; + std::vector batch_update_serialization_format; + bool is_batch{false}; + + double system_frequency{50.0}; + + Idx calculation_type{PGM_power_flow}; + Idx calculation_method{PGM_default_method}; + bool symmetric_calculation{static_cast(PGM_symmetric)}; + double error_tolerance{1e-8}; + Idx max_iterations{20}; + Idx threading{-1}; + Idx short_circuit_voltage_scaling{PGM_short_circuit_voltage_scaling_maximum}; + Idx tap_changing_strategy{PGM_tap_changing_strategy_disabled}; + + bool use_msgpack_output_serialization{false}; + Idx output_json_indent{2}; + bool use_compact_serialization{false}; + + std::string output_dataset_name; + MetaDataset const* output_dataset{nullptr}; + std::map> output_component_attribute_filters; + + bool verbose{false}; + + friend std::ostream& operator<<(std::ostream& os, ClIOptions const& options); +}; + +CLIResult parse_cli_options(int argc, char** argv, ClIOptions& options); + +void pgm_calculation(ClIOptions const& cli_options); + +} // namespace power_grid_model_cpp diff --git a/power_grid_model_c/power_grid_model_cli/cli_options.cpp b/power_grid_model_c/power_grid_model_cli/cli_options.cpp new file mode 100644 index 0000000000..d75686fc23 --- /dev/null +++ b/power_grid_model_c/power_grid_model_cli/cli_options.cpp @@ -0,0 +1,260 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include "cli_functions.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace power_grid_model_cpp { + +using EnumMap = std::map; + +struct CLIPostCallback { + // NOLINTBEGIN(cppcoreguidelines-avoid-const-or-ref-data-members) + ClIOptions& options; + CLI::Option& msgpack_flag; + CLI::Option& compact_flag; + std::vector const& output_components; + std::vector const& output_attributes; + // NOLINTEND(cppcoreguidelines-avoid-const-or-ref-data-members) + + void operator()() { + set_default_values(); + set_output_dataset(); + add_component_output_filter(); + add_attribute_output_filter(); + } + + static PGM_SerializationFormat get_serialization_format(std::string const& argument_type, + std::filesystem::path const& path) { + std::ifstream file{path, std::ios::binary}; + if (!file.is_open()) { + throw CLI::ValidationError(argument_type, "Unable to open file: " + path.string()); + } + uint8_t header; + file.read(reinterpret_cast(&header), 1); + if (!file) { + throw CLI::ValidationError(argument_type, "Unable to read from file: " + path.string()); + } + // Check for fixmap (0x80-0x8f), map16 (0xde), or map32 (0xdf) + bool const is_msgpack = (header >= 0x80 && header <= 0x8f) || header == 0xde || header == 0xdf; + return is_msgpack ? PGM_msgpack : PGM_json; + } + + void set_default_values() { + // detect if input file is msgpack + options.input_serialization_format = get_serialization_format("input", options.input_file); + // detect if batch update file is provided + options.is_batch = !options.batch_update_file.empty(); + // detect if batch update file is msgpack + options.batch_update_serialization_format.resize(options.batch_update_file.size()); + std::transform(options.batch_update_file.cbegin(), options.batch_update_file.cend(), + options.batch_update_serialization_format.begin(), + [](auto const& path) { return get_serialization_format("batch-update", path); }); + // default msgpack output if input or any of the batch updates is msgpack and user did not specify output format + if (msgpack_flag.count() == 0 && (options.input_serialization_format == PGM_msgpack || + std::ranges::any_of(options.batch_update_serialization_format, + [](auto format) { return format == PGM_msgpack; }))) { + options.use_msgpack_output_serialization = true; + } + // default compact serialization if msgpack output and user did not specify compact option + if (compact_flag.count() == 0 && options.use_msgpack_output_serialization) { + options.use_compact_serialization = true; + } + } + + void set_output_dataset() { + if (options.calculation_type == PGM_power_flow || options.calculation_type == PGM_state_estimation) { + if (options.symmetric_calculation) { + options.output_dataset_name = "sym_output"; + } else { + options.output_dataset_name = "asym_output"; + } + } else { + // options.calculation_type == PGM_short_circuit + options.output_dataset_name = "sc_output"; + } + options.output_dataset = MetaData::get_dataset_by_name(options.output_dataset_name); + } + + void add_component_output_filter() { + for (auto const& comp_name : output_components) { + try { + auto const* const component = MetaData::get_component_by_name(options.output_dataset_name, comp_name); + options.output_component_attribute_filters[component] = {}; + } catch (PowerGridError const&) { + throw CLI::ValidationError("output-component", "Component '" + comp_name + "' not found in dataset '" + + options.output_dataset_name + "'."); + } + } + } + + void add_attribute_output_filter() { + for (auto const& attr_full_name : output_attributes) { + auto dot_pos = attr_full_name.find('.'); + if (dot_pos == std::string::npos || dot_pos == 0 || dot_pos == attr_full_name.size() - 1) { + throw CLI::ValidationError("output-attribute", "Attribute '" + attr_full_name + + "' is not in the format 'component.attribute'."); + } + auto comp_name = attr_full_name.substr(0, dot_pos); + auto attr_name = attr_full_name.substr(dot_pos + 1); + MetaComponent const* component = nullptr; + try { + component = MetaData::get_component_by_name(options.output_dataset_name, comp_name); + } catch (PowerGridError const&) { + throw CLI::ValidationError("output-attribute", "Component '" + comp_name + "' not found in dataset '" + + options.output_dataset_name + "'."); + } + MetaAttribute const* attribute = nullptr; + try { + attribute = MetaData::get_attribute_by_name(options.output_dataset_name, comp_name, attr_name); + } catch (PowerGridError const&) { + std::string error_msg = "Attribute '"; + error_msg += attr_name; + error_msg += "' not found in component '"; + error_msg += comp_name; + error_msg += "' of dataset '"; + error_msg += options.output_dataset_name; + error_msg += "'."; + throw CLI::ValidationError("output-attribute", error_msg); + } + options.output_component_attribute_filters[component].insert(attribute); + } + } +}; + +CLIResult parse_cli_options(int argc, char** argv, ClIOptions& options) { + std::string const version_str = std::string("Power Grid Model CLI\n Version: ") + PGM_version(); + CLI::App app{version_str}; + + CLI::Validator const existing_parent_dir_validator{ + [](std::string& input) { + std::filesystem::path const p{input}; + auto parent = p.has_parent_path() ? p.parent_path() : std::filesystem::path{"."}; + if (parent.empty() || !std::filesystem::exists(parent) || !std::filesystem::is_directory(parent)) { + return std::string("The parent directory of the specified path does not exist or is not a directory."); + } + return std::string{}; + }, + "ExistingParentDirectory"}; + + app.set_version_flag("-v,--version", PGM_version()); + app.add_option("-i,--input", options.input_file, "Input file path")->required()->check(CLI::ExistingFile); + app.add_option("-b,--batch-update", options.batch_update_file, + "Batch update file path. Can be specified multiple times.\n" + "If multiple files are specified, the core will intepret them as the cartesian product of all " + "combinations of all scenarios in the list of batch datasets.") + ->check(CLI::ExistingFile); + app.add_option("-o,--output", options.output_file, "Output file path") + ->required() + ->check(existing_parent_dir_validator); + app.add_option("-f,--system-frequency", options.system_frequency, "System frequency in Hz, default is 50.0 Hz."); + app.add_option("-c,--calculation-type", options.calculation_type, "Calculation type") + ->transform(CLI::CheckedTransformer( + EnumMap{ + {"power_flow", PGM_power_flow}, + {"short_circuit", PGM_short_circuit}, + {"state_estimation", PGM_state_estimation}, + }, + CLI::ignore_case)); + app.add_option("-m,--calculation-method", options.calculation_method, "Calculation method") + ->transform(CLI::CheckedTransformer( + EnumMap{ + {"default", PGM_default_method}, + {"newton_raphson", PGM_newton_raphson}, + {"iterative_linear", PGM_iterative_linear}, + {"iterative_current", PGM_iterative_current}, + {"linear_current", PGM_linear_current}, + {"iec60909", PGM_iec60909}, + }, + CLI::ignore_case)); + app.add_flag("-s,--symmetric-calculation,!-a,!--asymmetric-calculation", options.symmetric_calculation, + "Use symmetric calculation (1) or not (0)"); + app.add_option("-e,--error-tolerance", options.error_tolerance, "Error tolerance for iterative calculations"); + app.add_option("-x,--max-iterations", options.max_iterations, + "Maximum number of iterations for iterative calculations"); + app.add_option("-t,--threading", options.threading, "Number of threads to use (-1 for automatic selection)"); + app.add_option("--short-circuit-voltage-scaling", options.short_circuit_voltage_scaling, + "Short circuit voltage scaling") + ->transform(CLI::CheckedTransformer( + EnumMap{ + {"minimum", PGM_short_circuit_voltage_scaling_minimum}, + {"maximum", PGM_short_circuit_voltage_scaling_maximum}, + }, + CLI::ignore_case)); + app.add_option("--tap-changing-strategy", options.tap_changing_strategy, "Tap changing strategy") + ->transform(CLI::CheckedTransformer( + EnumMap{ + {"disabled", PGM_tap_changing_strategy_disabled}, + {"any", PGM_tap_changing_strategy_any_valid_tap}, + {"min_voltage", PGM_tap_changing_strategy_min_voltage_tap}, + {"max_voltage", PGM_tap_changing_strategy_max_voltage_tap}, + {"fast_any", PGM_tap_changing_strategy_fast_any_tap}, + }, + CLI::ignore_case)); + auto& msgpack_flag = + *app.add_flag("--msgpack,--use-msgpack-output-serialization,!--json,!--use-json-output-serialization", + options.use_msgpack_output_serialization, "Use MessagePack output serialization"); + app.add_option("--indent,--output-json-indent", options.output_json_indent, + "Number of spaces to indent JSON output"); + auto& compact_flag = + *app.add_flag("--compact,--use-compact-serialization,!--no-compact,!--no-compact-serialization", + options.use_compact_serialization, "Use compact serialization (no extra whitespace)"); + std::vector output_components; + app.add_option("--oc,--output-component", output_components, + "Filter output to only include specified components (can be specified multiple times)"); + std::vector output_attributes; + app.add_option("--oa,--output-attribute", output_attributes, + "Filter output to only include specified attributes, in the format `component.attribute` (can be " + "specified multiple times)"); + app.add_flag("--verbose", options.verbose, "Enable verbose output"); + + app.callback(CLIPostCallback{.options = options, + .msgpack_flag = msgpack_flag, + .compact_flag = compact_flag, + .output_components = output_components, + .output_attributes = output_attributes}); + + try { + app.parse(argc, argv); + } catch (CLI::ParseError const& e) { + return {.exit_code = app.exit(e), .should_exit = true}; + } + + return {.exit_code = 0, .should_exit = false}; +} + +std::ostream& operator<<(std::ostream& os, ClIOptions const& options) { + os << "Run PGM with following CLI Options:\n"; + os << "Input file: " << options.input_file << "\n"; + os << "Batch update file: \n"; + for (auto const& file : options.batch_update_file) { + os << '\t' << file << "\n"; + } + os << "Output file: " << options.output_file << "\n"; + + os << "Calculation type: " << options.calculation_type << "\n"; + os << "Calculation method: " << options.calculation_method << "\n"; + os << "Symmetric calculation: " << options.symmetric_calculation << "\n"; + os << "Error tolerance: " << options.error_tolerance << "\n"; + os << "Max iterations: " << options.max_iterations << "\n"; + os << "Threading: " << options.threading << "\n"; + os << "Short circuit voltage scaling: " << options.short_circuit_voltage_scaling << "\n"; + os << "Tap changing strategy: " << options.tap_changing_strategy << "\n"; + + os << "Use msgpack output serialization: " << options.use_msgpack_output_serialization << "\n"; + os << "Output JSON indent: " << options.output_json_indent << "\n"; + os << "Use compact serialization: " << options.use_compact_serialization << "\n"; + os << "Verbose: " << options.verbose << "\n"; + return os; +} + +} // namespace power_grid_model_cpp diff --git a/power_grid_model_c/power_grid_model_cli/main.cpp b/power_grid_model_c/power_grid_model_cli/main.cpp new file mode 100644 index 0000000000..bbf0032e25 --- /dev/null +++ b/power_grid_model_c/power_grid_model_cli/main.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include "cli_functions.hpp" + +#include + +#include + +using namespace power_grid_model_cpp; + +int main(int argc, char** argv) { + ClIOptions cli_options; + if (auto const parse_result = parse_cli_options(argc, argv, cli_options); parse_result) { + return parse_result.exit_code; + } + + if (cli_options.verbose) { + std::cout << cli_options << '\n'; + } + + try { + pgm_calculation(cli_options); + } catch (PowerGridError const& e) { + std::cerr << "PowerGridError: " << e.what() << '\n'; + return static_cast(e.error_code()); + } catch (std::exception const& e) { + std::cerr << "Exception: " << e.what() << '\n'; + return 1; + } catch (...) { + std::cerr << "Unknown exception caught." << '\n'; + return 1; + } + + return 0; +} diff --git a/power_grid_model_c/power_grid_model_cli/pgm_calculation.cpp b/power_grid_model_c/power_grid_model_cli/pgm_calculation.cpp new file mode 100644 index 0000000000..2018ba6a6c --- /dev/null +++ b/power_grid_model_c/power_grid_model_cli/pgm_calculation.cpp @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include "cli_functions.hpp" + +#include +#include +#include +#include +#include + +namespace power_grid_model_cpp { + +struct BatchDatasets { + explicit BatchDatasets(ClIOptions const& cli_options) { + if (!cli_options.is_batch) { + return; + } + for (auto const& [batch_file, format] : + std::views::zip(cli_options.batch_update_file, cli_options.batch_update_serialization_format)) { + datasets.emplace_back(load_dataset(batch_file, format)); + dataset_consts.emplace_back(datasets.back().dataset); + } + assert(!datasets.empty()); + for (auto it = dataset_consts.begin(); it != dataset_consts.end() - 1; ++it) { + it->set_next_cartesian_product_dimension(*(it + 1)); + } + batch_size = std::transform_reduce(datasets.begin(), datasets.end(), Idx{1}, std::multiplies{}, + [](OwningDataset const& ds) { return ds.dataset.get_info().batch_size(); }); + } + + DatasetConst const& head() const { + assert(!dataset_consts.empty()); + return dataset_consts.front(); + } + + Idx batch_size{1}; + std::vector datasets; + std::vector dataset_consts; +}; + +void pgm_calculation(ClIOptions const& cli_options) { + // Load input dataset + OwningDataset const input_dataset = load_dataset(cli_options.input_file, cli_options.input_serialization_format); + + // Apply batch updates if provided + BatchDatasets const batch_datasets{cli_options}; + + // create result dataset + // NOLINTNEXTLINE(misc-const-correctness) + OwningDataset result_dataset{input_dataset, cli_options.output_dataset_name, cli_options.is_batch, + batch_datasets.batch_size, cli_options.output_component_attribute_filters}; + // create model + Model model{cli_options.system_frequency, input_dataset.dataset}; + // create calculation options + Options calc_options{}; + calc_options.set_calculation_type(cli_options.calculation_type); + calc_options.set_calculation_method(cli_options.calculation_method); + calc_options.set_symmetric(static_cast(cli_options.symmetric_calculation)); + calc_options.set_err_tol(cli_options.error_tolerance); + calc_options.set_max_iter(cli_options.max_iterations); + calc_options.set_threading(cli_options.threading); + calc_options.set_short_circuit_voltage_scaling(cli_options.short_circuit_voltage_scaling); + calc_options.set_tap_changing_strategy(cli_options.tap_changing_strategy); + + // perform calculation + if (cli_options.is_batch) { + model.calculate(calc_options, result_dataset.dataset, batch_datasets.head()); + } else { + model.calculate(calc_options, result_dataset.dataset); + } + + // Save output dataset + save_dataset(cli_options.output_file, result_dataset.dataset, + cli_options.use_msgpack_output_serialization ? PGM_msgpack : PGM_json, + cli_options.use_compact_serialization ? 1 : 0, cli_options.output_json_indent); +} + +} // namespace power_grid_model_cpp diff --git a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/dataset.hpp b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/dataset.hpp index 7d383650eb..95e9f37b46 100644 --- a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/dataset.hpp +++ b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/dataset.hpp @@ -240,6 +240,10 @@ class AttributeBuffer { RawDataPtr get() { return pgm_type_func_selector(MetaData::attribute_ctype(attribute_), PtrGetter{*this}); } + MetaAttribute const* get_attribute() const { return attribute_; } + + template std::vector const& get_data_vector() const { return std::get>(buffer_); } + private: MetaAttribute const* attribute_{nullptr}; VariantType buffer_; diff --git a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/serialization.hpp b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/serialization.hpp index aa3ac5918b..57298875fc 100644 --- a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/serialization.hpp +++ b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/serialization.hpp @@ -111,7 +111,8 @@ inline OwningDataset load_dataset(std::filesystem::path const& path, PGM_Seriali return buffer; }; - Deserializer deserializer{read_file(path), serialization_format}; + auto const file_content = read_file(path); + Deserializer deserializer{file_content, serialization_format}; auto& writable_dataset = deserializer.get_dataset(); OwningDataset dataset{writable_dataset, enable_columnar_buffers}; deserializer.parse_to_buffer(); diff --git a/pyproject.toml b/pyproject.toml index 2ee3cafba6..1d7b20b910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,9 @@ Discussion = "https://github.com/orgs/PowerGridModel/discussions" [project.entry-points."cmake.root"] power_grid_model = "power_grid_model._core.power_grid_model_c" +[project.scripts] +power-grid-model = "power_grid_model._core.power_grid_model_c.run_pgm_cli:main" + [tool.scikit-build] logging.level = "INFO" diff --git a/src/power_grid_model/__init__.py b/src/power_grid_model/__init__.py index e4dc65f431..83c2fb38d9 100644 --- a/src/power_grid_model/__init__.py +++ b/src/power_grid_model/__init__.py @@ -5,6 +5,7 @@ """Power Grid Model""" from power_grid_model._core.dataset_definitions import ComponentType, DatasetType +from power_grid_model._core.power_grid_core import pgm_version from power_grid_model._core.power_grid_meta import ( attribute_dtype, attribute_empty_value, @@ -29,6 +30,8 @@ ) from power_grid_model.typing import ComponentAttributeMapping +__version__ = pgm_version + __all__ = [ "AngleMeasurementType", "Branch3Side", @@ -47,6 +50,7 @@ "ShortCircuitVoltageScaling", "TapChangingStrategy", "WindingType", + "__version__", "attribute_dtype", "attribute_empty_value", "initialize_array", diff --git a/src/power_grid_model/_core/power_grid_core.py b/src/power_grid_model/_core/power_grid_core.py index 3ad3170829..260ff057c6 100644 --- a/src/power_grid_model/_core/power_grid_core.py +++ b/src/power_grid_model/_core/power_grid_core.py @@ -167,8 +167,8 @@ def make_c_binding(func: Callable): c_restype = c_size_t # set argument in dll # mostly with handle pointer, except destroy function - is_destroy_func = "destroy" in name - if is_destroy_func: + is_func_without_handle = ("destroy" in name) or ("version" in name) + if is_func_without_handle: getattr(_CDLL, f"PGM_{name}").argtypes = c_argtypes else: getattr(_CDLL, f"PGM_{name}").argtypes = [HandlePtr, *c_argtypes] @@ -176,7 +176,7 @@ def make_c_binding(func: Callable): # binding function def cbind_func(self, *args, **kwargs): - c_inputs = [] if "destroy" in name else [self._handle] + c_inputs = [] if is_func_without_handle else [self._handle] args = chain(args, (kwargs[key] for key in py_argnames[len(args) :])) for arg in args: if isinstance(arg, str): @@ -241,6 +241,10 @@ def batch_errors(self) -> CStrPtr: # type: ignore[empty-body, valid-type] # ty def clear_error(self) -> None: # type: ignore[empty-body] pass # pragma: no cover + @make_c_binding + def version(self) -> str: # type: ignore[empty-body] + pass # pragma: no cover + @make_c_binding def meta_n_datasets(self) -> int: # type: ignore[empty-body] pass # pragma: no cover @@ -575,3 +579,6 @@ def get_power_grid_core() -> PowerGridCore: except AttributeError: _thread_local_data.power_grid_core = PowerGridCore() return _thread_local_data.power_grid_core + + +pgm_version = get_power_grid_core().version() diff --git a/src/power_grid_model/_core/power_grid_model_c/run_pgm_cli.py b/src/power_grid_model/_core/power_grid_model_c/run_pgm_cli.py new file mode 100644 index 0000000000..45e8e3c8dd --- /dev/null +++ b/src/power_grid_model/_core/power_grid_model_c/run_pgm_cli.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +import os +import platform +import sys +from importlib.resources import files +from pathlib import Path + + +def get_pgm_cli_path() -> Path: + """ + Returns the path to PGM CLI executable. + """ + package_dir = Path(str(files(__package__))) + bin_dir = package_dir / "bin" + platform_name = platform.uname().system + + # determine executable file name + if platform_name == "Windows": + exe_file = Path("power-grid-model.exe") + elif platform_name == "Darwin" or platform.system() == "Linux": + exe_file = Path("power-grid-model") + else: + raise NotImplementedError(f"Unsupported platform: {platform_name}") + bin_path = bin_dir / exe_file + + # determine editable path to the executable + # __file__ + # -> power_grid_model_c (..) + # -> _core (..) + # -> power_grid_model (..) + # -> src (..) + # -> repo_root (..) + # -> build + # -> bin + editable_dir = Path(__file__).resolve().parent.parent.parent.parent.parent / "build" / "bin" + editable_bin_path = editable_dir / exe_file + + # first try to load from bin_path + # then editable_bin_path + # then if it is inside conda, this Python shim should never be called, instead user calls the exe directly + # then for anything else, raise an error + if bin_path.exists(): + final_bin_path = bin_path + elif editable_bin_path.exists(): + final_bin_path = editable_bin_path + elif os.environ.get("CONDA_PREFIX"): + raise ImportError( + "PGM CLI Python shim should not be called inside conda environment. Please call the executable directly." + ) + else: + raise ImportError(f"Could not find executable: {exe_file}. Your PGM installation may be broken.") + + return final_bin_path + + +def main(): + exe_path = get_pgm_cli_path() + os.execv(str(exe_path), [str(exe_path), *sys.argv[1:]]) # noqa: S606 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6e983b5a03..98a6d8722c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,3 +10,4 @@ add_subdirectory("native_api_tests") add_subdirectory("cpp_unit_tests") add_subdirectory("cpp_validation_tests") add_subdirectory("benchmark_cpp") +add_subdirectory("cpp_cli_tests") diff --git a/tests/cpp_cli_tests/CMakeLists.txt b/tests/cpp_cli_tests/CMakeLists.txt new file mode 100644 index 0000000000..e8474339c5 --- /dev/null +++ b/tests/cpp_cli_tests/CMakeLists.txt @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +set(PROJECT_SOURCES "test_entry_point.cpp" "test_cli.cpp") + +add_executable(power_grid_model_cli_tests ${PROJECT_SOURCES}) + +target_link_libraries( + power_grid_model_cli_tests + PRIVATE + power_grid_model_cpp + doctest::doctest + nlohmann_json + nlohmann_json::nlohmann_json +) +target_compile_definitions( + power_grid_model_cli_tests + PRIVATE + POWER_GRID_MODEL_CLI_EXECUTABLE="$" +) +add_dependencies(power_grid_model_cli_tests power_grid_model_cli) + +doctest_discover_tests(power_grid_model_cli_tests) diff --git a/tests/cpp_cli_tests/test_cli.cpp b/tests/cpp_cli_tests/test_cli.cpp new file mode 100644 index 0000000000..0374cd864c --- /dev/null +++ b/tests/cpp_cli_tests/test_cli.cpp @@ -0,0 +1,491 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#define PGM_ENABLE_EXPERIMENTAL + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace power_grid_model_cpp { + +namespace { +namespace fs = std::filesystem; +using std::numbers::sqrt3; + +// namespace for hardcode json +namespace { + +constexpr std::string_view input_json = R"json({ + "version": "1.0", + "type": "input", + "is_batch": false, + "attributes": {}, + "data": { + "sym_load": [ + {"id": 2, "node": 0, "status": 1, "type": 0, "p_specified": 0, "q_specified": 0} + ], + "source": [ + {"id": 1, "node": 0, "status": 1, "u_ref": 1, "sk": 1e20} + ], + "node": [ + {"id": 0, "u_rated": 10e3} + ] + } +})json"; + +constexpr std::string_view batch_u_ref_json = R"json({ + "version": "1.0", + "type": "update", + "is_batch": true, + "attributes": {}, + "data": [ + { + "source": [ + {"u_ref": 0.9} + ] + }, + { + "source": [ + {"u_ref": 1.0} + ] + }, + { + "source": [ + {"u_ref": 1.1} + ] + } + ] +})json"; + +constexpr std::string_view batch_p_json = R"json({ + "version": "1.0", + "type": "update", + "is_batch": true, + "attributes": { "sym_load": ["p_specified"] }, + "data": [ + { + "sym_load": [ + [1e6] + ] + }, + { + "sym_load": [ + [2e6] + ] + }, + { + "sym_load": [ + [3e6] + ] + }, + { + "sym_load": [ + [4e6] + ] + } + ] +})json"; + +constexpr std::string_view batch_q_json = R"json({ + "version": "1.0", + "type": "update", + "is_batch": true, + "attributes": {}, + "data": [ + { + "sym_load": [ + {"q_specified": 0.1e6} + ] + }, + { + "sym_load": [ + {"q_specified": 0.2e6} + ] + }, + { + "sym_load": [ + {"q_specified": 0.3e6} + ] + }, + { + "sym_load": [ + {"q_specified": 0.4e6} + ] + }, + { + "sym_load": [ + {"q_specified": 0.5e6} + ] + } + ] +})json"; + +constexpr std::string_view cli_executable = POWER_GRID_MODEL_CLI_EXECUTABLE; + +} // namespace + +fs::path tmp_path() { + // Get the system temp directory + fs::path const tmpdir = fs::temp_directory_path(); + // Return the path + return tmpdir / "pgm_cli_test"; +} + +fs::path input_path() { return tmp_path() / "input.json"; } +fs::path batch_u_ref_path() { return tmp_path() / "batch_u_ref.json"; } +fs::path batch_p_path() { return tmp_path() / "batch_p.json"; } +fs::path batch_q_path() { return tmp_path() / "batch_q.json"; } +fs::path batch_p_path_msgpack() { return tmp_path() / "batch_p.pgmb"; } +fs::path output_path(PGM_SerializationFormat format) { + return format == PGM_json ? tmp_path() / "output.json" : tmp_path() / "output.pgmb"; +} +fs::path stdout_path() { return tmp_path() / "stdout.txt"; } + +void clear_and_create_tmp_path() { + fs::path const cli_test_dir = tmp_path(); + + // Remove the dir if it exists (including contents) + if (fs::exists(cli_test_dir)) { + fs::remove_all(cli_test_dir); + } + + // Create the empty directory + if (!fs::create_directory(cli_test_dir)) { + throw std::runtime_error("Failed to create cli_test temp directory"); + } +} + +void save_data(std::string_view json_data, fs::path const& path, PGM_SerializationFormat format) { + if (std::ofstream ofs(path, std::ios::binary); ofs) { + if (format == PGM_json) { + ofs << json_data; + } else { + nlohmann::json const j = nlohmann::json::parse(json_data); + std::string msgpack_buffer; + nlohmann::json::to_msgpack(j, msgpack_buffer); + ofs << msgpack_buffer; + } + } else { + throw std::runtime_error("Failed to open file for writing: " + path.string()); + } + // try to read the file, discard results + load_dataset(path, format, true); +} + +void prepare_data() { + clear_and_create_tmp_path(); + save_data(input_json, input_path(), PGM_json); + save_data(batch_u_ref_json, batch_u_ref_path(), PGM_json); + save_data(batch_p_json, batch_p_path(), PGM_json); + save_data(batch_p_json, batch_p_path_msgpack(), PGM_msgpack); + save_data(batch_q_json, batch_q_path(), PGM_json); +} + +std::string read_stdout_content() { + fs::path const file_name = stdout_path(); + std::ifstream version_ifs(file_name); + REQUIRE(version_ifs); + + // Get file size + version_ifs.seekg(0, std::ios::end); + std::streamsize const size = version_ifs.tellg(); + version_ifs.seekg(0, std::ios::beg); + + // Read the entire file + std::string file_content(size, '\0'); + version_ifs.read(file_content.data(), size); + return file_content; +} + +std::vector get_i_source_ref(bool is_batch) { + if (!is_batch) { + return {0.0}; + } + // 3-D batch update + double const u_rated = 10e3; + std::vector const u_ref{0.9, 1.0, 1.1}; + std::vector const p_specified{1e6, 2e6, 3e6, 4e6}; + std::vector const q_specified{0.1e6, 0.2e6, 0.3e6, 0.4e6, 0.5e6}; + Idx const size_u_ref = std::ssize(u_ref); + Idx const size_p_specified = std::ssize(p_specified); + Idx const size_q_specified = std::ssize(q_specified); + Idx const total_batch_size = size_u_ref * size_p_specified * size_q_specified; + + // calculate source current manually + std::vector i_source_ref(total_batch_size); + for (Idx i = 0; i < size_u_ref; ++i) { + for (Idx j = 0; j < size_p_specified; ++j) { + for (Idx k = 0; k < size_q_specified; ++k) { + Idx const index = i * size_p_specified * size_q_specified + j * size_q_specified + k; + double const s = std::abs(std::complex{p_specified[j], q_specified[k]}); + i_source_ref[index] = s / (sqrt3 * u_rated * u_ref[i]); + } + } + } + return i_source_ref; +} + +struct BufferRef { + PGM_SymmetryType symmetric{PGM_symmetric}; + bool use_attribute_buffer{false}; + Buffer const* row_buffer; + AttributeBuffer const* attribute_buffer; + + void check_i_source(std::vector const& i_source_ref) const { + Idx const batch_size = static_cast(i_source_ref.size()); + for (Idx idx = 0; idx < batch_size; ++idx) { + double const i_calculated = [this, idx]() { + if (use_attribute_buffer) { + if (symmetric == PGM_symmetric) { + auto const& data_vector = attribute_buffer->get_data_vector(); + return data_vector.at(idx); + } + // else: use attribute buffer with asymmetric data + auto const& data_vector = attribute_buffer->get_data_vector>(); + auto const& val_array = data_vector.at(idx); + CHECK(val_array[0] == doctest::Approx(val_array[1])); + CHECK(val_array[0] == doctest::Approx(val_array[2])); + return val_array[0]; + } + // else: use row buffer + if (symmetric == PGM_symmetric) { + double value{}; + row_buffer->get_value(PGM_def_sym_output_source_i, &value, idx, 0); + return value; + } + // else: use row buffer with asymmetric data + std::array val_array{}; + row_buffer->get_value(PGM_def_asym_output_source_i, val_array.data(), idx, 0); + CHECK(val_array[0] == doctest::Approx(val_array[1])); + CHECK(val_array[0] == doctest::Approx(val_array[2])); + return val_array[0]; + }(); + CHECK(i_calculated == doctest::Approx(i_source_ref.at(idx))); + } + } +}; + +struct CLITestCase { + bool is_batch{false}; + bool batch_p_msgpack{false}; + bool has_frequency{false}; + bool has_calculation_type{false}; + bool has_calculation_method{false}; + std::optional symmetry{}; + bool has_error_tolerance{false}; + bool has_max_iterations{false}; + bool has_threading{false}; + std::optional output_serialization{}; + std::optional output_json_indent{}; + std::optional output_compact_serialization{}; + bool component_filter{false}; + bool attribute_filter{false}; + + PGM_SerializationFormat get_output_format() const { + if (output_serialization.has_value()) { + return output_serialization.value(); + } + if (is_batch && batch_p_msgpack) { + return PGM_msgpack; + } + return PGM_json; + } + bool has_output_filter() const { return component_filter || attribute_filter; } + PGM_SymmetryType get_symmetry() const { + if (symmetry.has_value()) { + return symmetry.value(); + } + return PGM_symmetric; + } + bool output_columnar() const { + if (output_compact_serialization.has_value()) { + return output_compact_serialization.value(); + } + return get_output_format() == PGM_msgpack; + } + + std::string build_command() const { + std::stringstream command; + command << cli_executable; + command << " -i " << input_path(); + if (is_batch) { + command << " -b " << batch_u_ref_path(); + command << " -b " << (batch_p_msgpack ? batch_p_path_msgpack() : batch_p_path()); + command << " -b " << batch_q_path(); + } + command << " -o " << output_path(get_output_format()); + if (has_frequency) { + command << " --system-frequency 50.0"; + } + if (has_calculation_type) { + command << " --calculation-type " << "power_flow"; + } + if (has_calculation_method) { + command << " --calculation-method " << "newton_raphson"; + } + if (symmetry.has_value()) { + command << (symmetry.value() == PGM_symmetric ? " -s" : " -a"); + } + if (has_error_tolerance) { + command << " --error-tolerance 1e-8"; + } + if (has_max_iterations) { + command << " --max-iterations 20"; + } + if (has_threading) { + command << " --threading -1"; + } + if (output_serialization.has_value()) { + if (output_serialization.value() == PGM_msgpack) { + command << " --msgpack"; + } else { + command << " --json"; + } + } + if (output_json_indent.has_value()) { + command << " --indent " << output_json_indent.value(); + } + if (output_compact_serialization.has_value()) { + if (output_compact_serialization.value()) { + command << " --compact"; + } else { + command << " --no-compact"; + } + } + if (component_filter) { + command << " --oc source"; + } + if (attribute_filter) { + command << " --oa source.i"; + } + command << " --verbose"; + command << " > " << stdout_path() << " 2>&1"; + return command.str(); + } + + BufferRef get_source_buffer(OwningDataset const& dataset) const { + auto const& owning_memory = dataset.storage; + auto const& info = dataset.dataset.get_info(); + Idx const source_idx = info.component_idx("source"); + auto const* const row_buffer = [this, &owning_memory, &info, source_idx]() { + if (has_output_filter()) { + REQUIRE(info.n_components() == 1); + REQUIRE(source_idx == 0); + } + return &owning_memory.buffers[source_idx]; + }(); + auto const* const attribute_buffer = [this, &owning_memory, row_buffer, + source_idx]() -> AttributeBuffer const* { + if (output_columnar()) { + REQUIRE(row_buffer->get() == nullptr); + if (attribute_filter) { + REQUIRE(owning_memory.attribute_buffers[source_idx].size() == 1); + return owning_memory.attribute_buffers[source_idx].data(); + } + // else: search for 'i' attribute buffer + for (auto const& attr_buf : owning_memory.attribute_buffers[source_idx]) { + if (MetaData::attribute_name(attr_buf.get_attribute()) == "i") { + return &attr_buf; + } + } + DOCTEST_FAIL("Attribute 'i' buffer not found"); + } + // when no filter, buffer should not be nullptr, and return nullptr for attribute buffer + REQUIRE(row_buffer->get() != nullptr); + return nullptr; + }(); + return BufferRef{.symmetric = get_symmetry(), + .use_attribute_buffer = output_columnar(), + .row_buffer = row_buffer, + .attribute_buffer = attribute_buffer}; + } + + void check_results() const { + fs::path const out_path = output_path(get_output_format()); + OwningDataset const output_owning_dataset = load_dataset(out_path, get_output_format(), true); + auto const i_source_ref = get_i_source_ref(is_batch); + Idx const batch_size = output_owning_dataset.dataset.get_info().batch_size(); + REQUIRE(batch_size == std::ssize(i_source_ref)); + REQUIRE(is_batch == output_owning_dataset.dataset.get_info().is_batch()); + auto const buffer_ref = get_source_buffer(output_owning_dataset); + buffer_ref.check_i_source(i_source_ref); + } + + void run_command_and_check() const { + prepare_data(); + std::string const command = build_command(); + INFO("CLI command: ", command); + // NOLINTNEXTLINE(cert-env33-c,concurrency-mt-unsafe) + int const ret = std::system(command.c_str()); + std::string const stdout_content = read_stdout_content(); + INFO("CLI stdout content: ", stdout_content); + REQUIRE(ret == 0); + check_results(); + } +}; + +} // namespace + +TEST_CASE("Test CLI version") { + prepare_data(); + std::string const command = std::string{cli_executable} + " --version" + " > " + stdout_path().string(); + // NOLINTNEXTLINE(cert-env33-c,concurrency-mt-unsafe) + int const ret = std::system(command.c_str()); + std::string const file_content = read_stdout_content(); + INFO("CLI stdout content: ", file_content); + REQUIRE(ret == 0); + // Extract the first line + std::string const first_line = file_content.substr(0, file_content.find('\n')); + CHECK(first_line == PGM_version()); +} + +TEST_CASE("Test run CLI") { + std::vector const test_cases = { + // basic non-batch, symmetric, json + CLITestCase{}, + // basic batch, symmetric, json + CLITestCase{.is_batch = true}, + // batch, asymmetric, msgpack + CLITestCase{.is_batch = true, .symmetry = PGM_asymmetric, .output_serialization = PGM_msgpack}, + // batch, symmetric, json, with all options set + CLITestCase{.is_batch = true, + .batch_p_msgpack = true, + .has_frequency = true, + .has_calculation_type = true, + .has_calculation_method = true, + .symmetry = PGM_symmetric, + .has_error_tolerance = true, + .has_max_iterations = true, + .has_threading = true, + .output_serialization = PGM_json, + .output_json_indent = 4, + .output_compact_serialization = true, + .component_filter = true, + .attribute_filter = true}, + // batch, asymmetric, msgpack, with component and attribute filter + CLITestCase{.is_batch = true, + .symmetry = PGM_asymmetric, + .output_serialization = PGM_msgpack, + .component_filter = true, + .attribute_filter = true}, + }; + for (auto const& test_case : test_cases) { + SUBCASE(test_case.build_command().c_str()) { test_case.run_command_and_check(); } + } +} + +} // namespace power_grid_model_cpp diff --git a/tests/cpp_cli_tests/test_entry_point.cpp b/tests/cpp_cli_tests/test_entry_point.cpp new file mode 100644 index 0000000000..020ee88a48 --- /dev/null +++ b/tests/cpp_cli_tests/test_entry_point.cpp @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +// main cpp file + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#include diff --git a/tests/native_api_tests/CMakeLists.txt b/tests/native_api_tests/CMakeLists.txt index 0306b86117..7f23f54efa 100644 --- a/tests/native_api_tests/CMakeLists.txt +++ b/tests/native_api_tests/CMakeLists.txt @@ -15,6 +15,11 @@ set(PROJECT_SOURCES add_executable(power_grid_model_api_tests ${PROJECT_SOURCES}) +target_compile_definitions( + power_grid_model_api_tests + PRIVATE PGM_VERSION="${PGM_VERSION}" +) + target_link_libraries( power_grid_model_api_tests PRIVATE diff --git a/tests/native_api_tests/test_api_utils.cpp b/tests/native_api_tests/test_api_utils.cpp index 2818b22b93..8b200f58a8 100644 --- a/tests/native_api_tests/test_api_utils.cpp +++ b/tests/native_api_tests/test_api_utils.cpp @@ -25,4 +25,6 @@ TEST_CASE("API Utils") { CHECK(is_nan(nan_value())); } } + +TEST_CASE("Check version") { CHECK(std::string{PGM_VERSION} == std::string{PGM_version()}); } } // namespace power_grid_model_cpp diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000000..9d3c3eb269 --- /dev/null +++ b/tests/unit/test_cli.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + + +import subprocess + +from power_grid_model import __version__ + + +def test_cli_version(): + result = subprocess.run(["power-grid-model", "--version"], capture_output=True, text=True, check=True) + assert __version__ in result.stdout