From 1783520dd6479bafa85bb8c2477183eb0f14851d Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Fri, 9 Oct 2026 17:32:44 +0200 Subject: [PATCH 01/25] state querries in core and ready for python interface Signed-off-by: Jerry Jinfeng Guo --- .../calculation_parameters.hpp | 4 + .../power_grid_model/calculation_state.hpp | 65 +++++++ .../include/power_grid_model/job_adapter.hpp | 29 ++- .../include/power_grid_model/main_model.hpp | 12 ++ .../power_grid_model/main_model_impl.hpp | 167 ++++++++++++++--- .../math_solver/math_solver.hpp | 10 ++ .../math_solver/math_solver_dispatch.hpp | 3 + .../math_solver/newton_raphson_pf_solver.hpp | 27 ++- .../include/power_grid_model_c/basics.h | 3 + .../include/power_grid_model_c/model.h | 87 +++++++++ .../power_grid_model_c/src/model.cpp | 161 ++++++++++++++++- .../include/power_grid_model_cpp/basics.hpp | 1 + .../include/power_grid_model_cpp/model.hpp | 58 ++++++ src/power_grid_model/__init__.py | 16 ++ src/power_grid_model/_core/model_state.py | 169 ++++++++++++++++++ src/power_grid_model/_core/power_grid_core.py | 101 ++++++++++- .../_core/power_grid_model.py | 75 ++++++-- tests/unit/test_multi_dimensional_batch.py | 19 +- tests/unit/test_power_grid_model.py | 59 ++++++ 19 files changed, 1014 insertions(+), 52 deletions(-) create mode 100644 power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp create mode 100644 src/power_grid_model/_core/model_state.py diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp index 4307470fab..72ee9cd765 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp @@ -11,9 +11,11 @@ #include "common/small_vector.hpp" #include "common/statistics.hpp" #include "common/three_phase_tensor.hpp" +#include "calculation_state.hpp" #include #include +#include #include #include @@ -353,6 +355,7 @@ template struct SolverOutput { std::vector> shunt; std::vector> load_gen; std::vector voltage_regulator; + std::shared_ptr model_state_group; }; template struct ShortCircuitSolverOutput { @@ -437,6 +440,7 @@ template struct MathOutput { SolverOutputType solver_output; OptimizerOutput optimizer_output; std::vector> supernode_output; + std::optional model_state; }; // component indices at physical model side diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp new file mode 100644 index 0000000000..37030ee170 --- /dev/null +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#pragma once + +#include "common/common.hpp" + +#include +#include +#include + +namespace power_grid_model { + +struct ModelStateRequest { + bool y_bus{}; + bool jacobian{}; +}; + +enum class ModelStateBusKind : IntS { input_node, synthetic_branch3 }; + +struct ModelStateGroupMapping { + Idx group{}; + Idx n_bus{}; + bool is_symmetric{}; + + // Reverse ragged mapping: bus i owns user nodes in [bus_user_indptr[i], bus_user_indptr[i + 1]). + // Synthetic buses have no user-node entries and are identified by bus_kind and origin_branch3_id. + IdxVector bus_user_indptr; + IdxVector bus_user_sequence; + std::vector bus_user_id; + std::vector bus_kind; + std::vector origin_branch3_id; +}; + +struct ModelStateYBus { + IdxVector row_indptr; + IdxVector col_indices; + std::vector admittance_real; + std::vector admittance_imag; +}; + +struct ModelStateJacobian { + IdxVector row_indptr_lu; + IdxVector col_indices_lu; + std::array, 4> blocks; // H, N, M, L; flattened row-major per sparse entry. +}; + +struct ModelStateGroup { + ModelStateGroupMapping mapping; + std::optional y_bus; + std::optional jacobian; +}; + +struct ModelStateOutput { + bool y_bus_requested{}; + bool jacobian_requested{}; + // Forward mapping indexed by input node sequence. Disconnected nodes use {-1, -1}. + IdxVector input_node_group; + IdxVector input_node_bus; + std::vector input_node_id; + std::vector groups; +}; + +} // namespace power_grid_model \ No newline at end of file diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/job_adapter.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/job_adapter.hpp index 72acd5e8cb..8555e9bda0 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/job_adapter.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/job_adapter.hpp @@ -29,8 +29,11 @@ template class JobAdapter : public JobInterface { using ModelType = MainModel::ImplType; JobAdapter(std::reference_wrapper model_reference, - std::reference_wrapper options) - : model_reference_{model_reference}, options_{options} {} + std::reference_wrapper options, + std::span state_requests = {}, + std::span> state_outputs = {}) + : model_reference_{model_reference}, options_{options}, state_requests_{state_requests}, + state_outputs_{state_outputs} {} JobAdapter(JobAdapter const& other) : model_copy_{std::make_unique(other.model_reference_.get())}, model_reference_{std::ref(*model_copy_)}, @@ -38,7 +41,9 @@ template class JobAdapter : public JobInterface { components_to_update_{other.components_to_update_}, update_independence_{other.update_independence_}, independence_flags_{other.independence_flags_}, - all_scenarios_sequence_{other.all_scenarios_sequence_} {} + all_scenarios_sequence_{other.all_scenarios_sequence_}, + state_requests_{other.state_requests_}, + state_outputs_{other.state_outputs_} {} JobAdapter& operator=(JobAdapter const& other) { if (this != &other) { model_copy_ = std::make_unique(other.model_reference_.get()); @@ -48,6 +53,8 @@ template class JobAdapter : public JobInterface { update_independence_ = other.update_independence_; independence_flags_ = other.independence_flags_; all_scenarios_sequence_ = other.all_scenarios_sequence_; + state_requests_ = other.state_requests_; + state_outputs_ = other.state_outputs_; } return *this; } @@ -58,7 +65,9 @@ template class JobAdapter : public JobInterface { components_to_update_{std::move(other.components_to_update_)}, update_independence_{std::move(other.update_independence_)}, independence_flags_{std::move(other.independence_flags_)}, - all_scenarios_sequence_{std::move(other.all_scenarios_sequence_)} {} + all_scenarios_sequence_{std::move(other.all_scenarios_sequence_)}, + state_requests_{other.state_requests_}, + state_outputs_{other.state_outputs_} {} JobAdapter& operator=(JobAdapter&& other) noexcept { if (this != &other) { model_copy_ = std::move(other.model_copy_); @@ -68,6 +77,8 @@ template class JobAdapter : public JobInterface { update_independence_ = std::move(other.update_independence_); independence_flags_ = std::move(other.independence_flags_); all_scenarios_sequence_ = std::move(other.all_scenarios_sequence_); + state_requests_ = other.state_requests_; + state_outputs_ = other.state_outputs_; } return *this; } @@ -84,12 +95,20 @@ template class JobAdapter : public JobInterface { ModelType::UpdateIndependence update_independence_{}; ModelType::ComponentFlags independence_flags_{}; std::shared_ptr all_scenarios_sequence_; + std::span state_requests_; + std::span> state_outputs_; // current_scenario_sequence_cache_ is calculated per scenario, so it is excluded from the constructors. ModelType::SequenceIdx current_scenario_sequence_cache_{}; void calculate_impl(MutableDataset const& result_data, Idx scenario_idx, Logger& logger) const { + ModelStateRequest const* state_request = state_requests_.empty() ? nullptr : &state_requests_[scenario_idx]; + ModelStateOutput state_output; MainModel::calculator(options_.get(), model_reference_.get(), result_data.get_individual_scenario(scenario_idx), - false, logger); + false, logger, state_request, + state_requests_.empty() ? nullptr : &state_output); + if (!state_requests_.empty() && (state_request->y_bus || state_request->jacobian)) { + state_outputs_[scenario_idx] = std::move(state_output); + } } void cache_calculate_impl(Logger& logger) const { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model.hpp index 41bde0a991..d001ff3012 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model.hpp @@ -114,6 +114,18 @@ class MainModel { return JobDispatch::batch_calculation(adapter, result_data, update_data, options.threading, logger_.get()); } + BatchParameter calculate_with_state(Options const& options, MutableDataset const& result_data, + ConstDataset const& update_data, + std::span state_requests, + std::span> state_outputs) { + Idx const n_scenarios = update_data.empty() ? 1 : update_data.batch_size(); + if (std::ssize(state_requests) != n_scenarios || std::ssize(state_outputs) != n_scenarios) { + throw DatasetError{"Model state request count must match the calculation scenario count.\n"}; + } + JobAdapter adapter{std::ref(impl()), std::ref(options), state_requests, state_outputs}; + return JobDispatch::batch_calculation(adapter, result_data, update_data, options.threading, logger_.get()); + } + void check_no_experimental_features_used(Options const& options, ConstDataset const* batch_dataset) const { impl().check_no_experimental_features_used(options, batch_dataset); } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp index 26b841aa13..d3a1134539 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp @@ -292,15 +292,15 @@ class MainModelImpl { solver_output_type::const_reference>> - - auto calculate_(PrepareInputFn prepare_input, SolveFn solve, Logger& logger) { + auto calculate_(PrepareInputFn&& prepare_input, SolveFn&& solve, Logger& logger, + ModelStateRequest const& state_request) { using InputType = std::invoke_result_t::const_reference; using SolverOutputType = std::invoke_result_t; using sym = decode_symmetry_v; assert(construction_complete_); // prepare - auto const& input = [this, &logger, prepare_input_ = prepare_input] { + auto const& input = [this, &logger, prepare_input_ = std::forward(prepare_input)] { Timer const timer{logger, LogEvent::prepare}; assert(construction_complete_); prepare_solvers(state_, solver_preparation_context_, solvers_cache_status_); @@ -309,36 +309,120 @@ class MainModelImpl { return prepare_input_(get_n_math_solvers(state_)); }(); // calculate - return [this, &logger, &input, &solve_ = solve] { + return [this, &logger, &input, &state_request, solve_ = std::forward(solve)] { + (void)state_request; Timer const timer{logger, LogEvent::math_calculation}; auto& solvers = main_core::get_solvers(solver_preparation_context_.math_state); auto& y_bus_vec = main_core::get_y_bus(solver_preparation_context_.math_state); std::vector solver_output; solver_output.reserve(get_n_math_solvers(state_)); for (Idx i = 0; i != get_n_math_solvers(state_); ++i) { - solver_output.emplace_back(solve_(solvers[i], y_bus_vec[i], input[i])); + SolverOutputType result = solve_(solvers[i], y_bus_vec[i], input[i]); + if constexpr (steady_state_solver_output_type) { + if (state_request.y_bus || state_request.jacobian) { + auto group_state = std::make_shared(); + group_state->mapping = create_model_state_mapping(i, y_bus_vec[i].size()); + if (state_request.y_bus) { + group_state->y_bus = create_y_bus_state(y_bus_vec[i]); + } + if (state_request.jacobian) { + group_state->jacobian = solvers[i].get().get_last_jacobian_state(y_bus_vec[i]); + } + result.model_state_group = std::move(group_state); + } + } + solver_output.emplace_back(std::move(result)); } return solver_output; }(); } + template ModelStateGroupMapping create_model_state_mapping(Idx group, Idx n_bus) const { + ModelStateGroupMapping mapping{ + .group = group, + .n_bus = n_bus, + .is_symmetric = is_symmetric_v, + .bus_user_indptr = IdxVector(static_cast(n_bus + 1), 0), + .bus_kind = std::vector(static_cast(n_bus), + ModelStateBusKind::synthetic_branch3), + .origin_branch3_id = std::vector(static_cast(n_bus), static_cast(-1)), + }; + + auto const& node_coupling = state_.topo_comp_coup->node; + auto const nodes = state_.components.template citer(); + std::vector>> user_nodes_per_bus(static_cast(n_bus)); + for (Idx user_sequence = 0; user_sequence < static_cast(std::ranges::size(nodes)); ++user_sequence) { + Idx2D const math_idx = node_coupling[user_sequence]; + if (math_idx.group == group) { + user_nodes_per_bus[math_idx.pos].emplace_back(user_sequence, nodes[user_sequence].id()); + mapping.bus_kind[math_idx.pos] = ModelStateBusKind::input_node; + } + } + + auto const branch3s = state_.components.template citer(); + Idx const n_user_nodes = static_cast(std::ranges::size(nodes)); + for (Idx branch3_sequence = 0; branch3_sequence < static_cast(std::ranges::size(branch3s)); + ++branch3_sequence) { + Idx2D const math_idx = node_coupling[n_user_nodes + branch3_sequence]; + if (math_idx.group == group && user_nodes_per_bus[math_idx.pos].empty()) { + mapping.origin_branch3_id[math_idx.pos] = branch3s[branch3_sequence].id(); + } + } + + for (Idx bus = 0; bus < n_bus; ++bus) { + auto const& user_nodes = user_nodes_per_bus[bus]; + for (auto const& [sequence, id] : user_nodes) { + mapping.bus_user_sequence.push_back(sequence); + mapping.bus_user_id.push_back(id); + } + mapping.bus_user_indptr[bus + 1] = static_cast(mapping.bus_user_sequence.size()); + } + return mapping; + } + + template static ModelStateYBus create_y_bus_state(YBus const& y_bus) { + ModelStateYBus state{ + .row_indptr = y_bus.row_indptr(), + .col_indices = y_bus.col_indices(), + }; + auto const& admittance = y_bus.admittance(); + constexpr Idx block_size = is_symmetric_v ? 1 : 9; + state.admittance_real.reserve(static_cast(y_bus.nnz() * block_size)); + state.admittance_imag.reserve(static_cast(y_bus.nnz() * block_size)); + for (Idx entry = 0; entry < y_bus.nnz(); ++entry) { + if constexpr (is_symmetric_v) { + state.admittance_real.push_back(admittance[entry].real()); + state.admittance_imag.push_back(admittance[entry].imag()); + } else { + for (Idx row = 0; row < 3; ++row) { + for (Idx col = 0; col < 3; ++col) { + state.admittance_real.push_back(admittance[entry](row, col).real()); + state.admittance_imag.push_back(admittance[entry](row, col).imag()); + } + } + } + } + return state; + } + // Calculate with optimization, e.g., automatic tap changer template - auto calculate_with_optimizer(Options const& options, bool cache_run, Logger& logger) { - auto const get_calculator = [this, &options, cache_run, &logger] { + auto calculate_with_optimizer(Options const& options, bool cache_run, Logger& logger, + ModelStateRequest const& state_request) { + auto const get_calculator = [this, &options, cache_run, &logger, &state_request] { using Calc = Calculator; assert(options.optimizer_type == OptimizerType::no_optimization || (std::derived_from)); - return [this, &mutable_comp_coup = state_.comp_coup, &options, cache_run, - &logger](MainModelState const& state, CalculationMethod calculation_method) { + return [this, &mutable_comp_coup = state_.comp_coup, &options, cache_run, &logger, + &state_request](MainModelState const& state, CalculationMethod calculation_method) { (void)state; // to avoid unused-lambda-capture when in Release build assert(&state == &state_); return calculate_, YBus>( Calc::preparer(state, mutable_comp_coup, options), - Calc::solver(calculation_method, options, cache_run, logger), logger); + Calc::solver(calculation_method, options, cache_run, logger), logger, state_request); }; }; @@ -346,18 +430,46 @@ class MainModelImpl { ? SearchMethod::linear_search : SearchMethod::binary_search; - return optimizer::get_optimizer( - options.optimizer_type, options.optimizer_strategy, get_calculator(), - [this](ConstDataset const& update_data) { - this->update_components(update_data); - }, - *meta_data_, search_method) - ->optimize(state_, options.calculation_method); + auto result = optimizer::get_optimizer( + options.optimizer_type, options.optimizer_strategy, get_calculator(), + [this](ConstDataset const& update_data) { + this->update_components(update_data); + }, + *meta_data_, search_method) + ->optimize(state_, options.calculation_method); + if constexpr (std::derived_from) { + if (state_request.y_bus || state_request.jacobian) { + ModelStateOutput model_state; + model_state.y_bus_requested = state_request.y_bus; + model_state.jacobian_requested = state_request.jacobian; + auto const nodes = state_.components.template citer(); + auto const& node_coupling = state_.topo_comp_coup->node; + model_state.input_node_group.reserve(std::ranges::size(nodes)); + model_state.input_node_bus.reserve(std::ranges::size(nodes)); + model_state.input_node_id.reserve(std::ranges::size(nodes)); + for (Idx sequence = 0; sequence < static_cast(std::ranges::size(nodes)); ++sequence) { + model_state.input_node_group.push_back(node_coupling[sequence].group); + model_state.input_node_bus.push_back(node_coupling[sequence].pos); + model_state.input_node_id.push_back(nodes[sequence].id()); + } + for (auto& solver_output : result.solver_output) { + if (solver_output.model_state_group) { + model_state.groups.push_back(std::move(*solver_output.model_state_group)); + solver_output.model_state_group.reset(); + } + } + result.model_state = std::move(model_state); + } + } + return result; } // Single calculation, propagating the results to result_data - void calculate(Options options, bool cache_run, MutableDataset const& result_data, Logger& logger) { + void calculate(Options options, bool cache_run, MutableDataset const& result_data, Logger& logger, + ModelStateRequest const* state_request = nullptr, ModelStateOutput* state_output = nullptr) { assert(construction_complete_); + ModelStateRequest const no_state_request{}; + ModelStateRequest const& request = state_request != nullptr ? *state_request : no_state_request; if (options.calculation_type == CalculationType::short_circuit) { auto const faults = state_.components.template citer(); @@ -377,21 +489,26 @@ class MainModelImpl { options.calculation_type, options.calculation_symmetry, [cache_run]( MainModelImpl& main_model_, Options const& options_, MutableDataset const& result_data_, - Logger& logger) { - main_model_.output_result( - main_model_.calculate_with_optimizer(options_, cache_run, logger), - result_data_, logger); + Logger& logger, ModelStateRequest const& state_request_, ModelStateOutput* state_output_) { + auto math_output = main_model_.calculate_with_optimizer(options_, cache_run, + logger, + state_request_); + main_model_.output_result(math_output, result_data_, logger); + if (state_output_ != nullptr && math_output.model_state) { + *state_output_ = std::move(*math_output.model_state); + } }, - *this, options, result_data, logger); + *this, options, result_data, logger, request, state_output); } public: static auto calculator(Options const& options, MainModelImpl& model, MutableDataset const& target_data, - bool cache_run, Logger& logger) { + bool cache_run, Logger& logger, ModelStateRequest const* state_request = nullptr, + ModelStateOutput* state_output = nullptr) { auto sub_opt = options; // copy sub_opt.err_tol = cache_run ? std::numeric_limits::max() : options.err_tol; sub_opt.max_iter = cache_run ? 1 : options.max_iter; - model.calculate(sub_opt, cache_run, target_data, logger); + model.calculate(sub_opt, cache_run, target_data, logger, state_request, state_output); } auto const& state() const { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp index f31f186e9d..61a43a59bc 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp @@ -44,6 +44,7 @@ template class MathSolver : public MathSolverBase { Logger& log, CalculationMethod calculation_method, YBus const& y_bus) final { using enum CalculationMethod; + last_power_flow_used_newton_raphson_ = false; // set method to always linear if all load_gens have const_y calculation_method = all_const_y_ ? linear : calculation_method; @@ -51,6 +52,7 @@ template class MathSolver : public MathSolverBase { case default_method: [[fallthrough]]; // use Newton-Raphson by default case newton_raphson: + last_power_flow_used_newton_raphson_ = true; return run_power_flow_newton_raphson(input, err_tol, max_iter, cache_run, log, y_bus); case linear: return run_power_flow_linear(input, err_tol, max_iter, log, y_bus); @@ -111,6 +113,13 @@ template class MathSolver : public MathSolverBase { } } + std::optional get_last_jacobian_state(YBus const& y_bus) const final { + if (!last_power_flow_used_newton_raphson_ || !newton_raphson_pf_solver_) { + return std::nullopt; + } + return newton_raphson_pf_solver_->get_last_jacobian_state(y_bus); + } + private: std::shared_ptr topo_ptr_; bool all_const_y_; // if all the load_gen is const element_admittance (impedance) type @@ -120,6 +129,7 @@ template class MathSolver : public MathSolverBase { std::optional> iterative_linear_se_solver_; std::optional> newton_raphson_se_solver_; std::optional> iec60909_sc_solver_; + bool last_power_flow_used_newton_raphson_{false}; SolverOutput run_power_flow_newton_raphson(PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, Logger& log, YBus const& y_bus) { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp index 06fa0d1c74..b0b3f15f39 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp @@ -35,6 +35,9 @@ template class MathSolverBase { virtual SolverOutput run_state_estimation(StateEstimationInput const& input, double err_tol, Idx max_iter, Logger& log, CalculationMethod calculation_method, YBus const& y_bus) = 0; + virtual std::optional get_last_jacobian_state(YBus const& /* y_bus */) const { + return std::nullopt; + } virtual ShortCircuitSolverOutput run_short_circuit(ShortCircuitInput const& input, Logger& log, CalculationMethod calculation_method, YBus const& y_bus) = 0; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp index 11bb556094..52b5e83d9c 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp @@ -390,11 +390,36 @@ class NewtonRaphsonPFSolver : public IterativePFSolvern_bus_; ++i) { auto& bus_output = output.bus[i]; auto const& q_limit = bus_control_[i].q_limit; - bus_output.q_limit_violated = q_limit.limit_violated; } } + ModelStateJacobian get_last_jacobian_state(YBus const& y_bus) const { + ModelStateJacobian result{ + .row_indptr_lu = y_bus.row_indptr_lu(), + .col_indices_lu = y_bus.col_indices_lu(), + }; + constexpr Idx block_size = is_symmetric_v ? 1 : 9; + for (auto& block : result.blocks) { + block.reserve(static_cast(std::ssize(data_jac_) * block_size)); + } + + constexpr Idx sub_size = is_symmetric_v ? 1 : 3; + for (Idx entry = 0; entry < static_cast(std::ssize(data_jac_)); ++entry) { + for (Idx block = 0; block < 4; ++block) { + Idx const block_row = block / 2; + Idx const block_col = block % 2; + for (Idx row = 0; row < sub_size; ++row) { + for (Idx col = 0; col < sub_size; ++col) { + result.blocks[block].push_back(data_jac_[entry](block_row * sub_size + row, + block_col * sub_size + col)); + } + } + } + } + return result; + } + private: // data for jacobian std::vector> data_jac_; diff --git a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/basics.h b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/basics.h index 6df4d1274b..5e14a20282 100644 --- a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/basics.h +++ b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/basics.h @@ -85,6 +85,9 @@ typedef struct PGM_Handle PGM_Handle; */ typedef struct PGM_Options PGM_Options; +/** @brief Opaque owner of native calculation-state output buffers. */ +typedef struct PGM_StateOutput PGM_StateOutput; + /** * @brief Opaque struct for the attribute meta class. * diff --git a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h index aa54e31168..152faf7ebe 100644 --- a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h +++ b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h @@ -117,6 +117,93 @@ PGM_API void PGM_calculate(PGM_Handle* handle, PGM_PowerGridModel* model, PGM_Op PGM_MutableDataset const* output_dataset, PGM_ConstDataset const* batch_dataset) PGM_NOEXCEPT; +/** + * @brief Per-scenario request for optional native PF state output. + */ +typedef struct PGM_StateOutputRequest { + PGM_Idx y_bus; + PGM_Idx jacobian; +} PGM_StateOutputRequest; + +/** + * @brief Borrowed view of one scenario's state output. + * + * All pointers are valid until the owning PGM_StateOutput is destroyed. A zero + * availability flag means the corresponding pointer fields are NULL. + */ +typedef struct PGM_StateScenarioView { + PGM_Idx has_state; + PGM_Idx y_bus_requested; + PGM_Idx jacobian_requested; + PGM_Idx n_groups; + PGM_Idx n_input_nodes; + PGM_Idx const* input_node_group; + PGM_Idx const* input_node_bus; + PGM_ID const* input_node_id; +} PGM_StateScenarioView; + +/** + * @brief Borrowed view of one group's sparse matrices and bus-to-input mapping. + * + * CSR/value arrays are NULL when the corresponding matrix is absent. Mapping + * pointers are valid until the owning PGM_StateOutput is destroyed. + */ +typedef struct PGM_StateGroupView { + PGM_Idx group; + PGM_Idx n_bus; + PGM_Idx is_symmetric; + PGM_Idx n_user_node_refs; + PGM_Idx const* bus_user_indptr; + PGM_Idx const* bus_user_sequence; + PGM_ID const* bus_user_id; + int8_t const* bus_kind; + PGM_ID const* origin_branch3_id; + PGM_Idx has_y_bus; + PGM_Idx y_bus_nnz; + PGM_Idx const* y_bus_row_indptr; + PGM_Idx const* y_bus_col_indices; + double const* admittance_real; + double const* admittance_imag; + PGM_Idx n_admittance_values; + PGM_Idx has_jacobian; + PGM_Idx jacobian_nnz; + PGM_Idx const* jacobian_row_indptr; + PGM_Idx const* jacobian_col_indices; + double const* jacobian_h; + double const* jacobian_n; + double const* jacobian_m; + double const* jacobian_l; + PGM_Idx n_jacobian_values; +} PGM_StateGroupView; + +/** + * @brief Calculate normally and return an owned native state result. + * + * request_count must be one for a single calculation or equal the effective + * flattened batch scenario count. Requests are in output scenario order. + * The caller owns *state_output and must release it with + * PGM_destroy_state_output(). The component output dataset remains caller-owned. + */ +PGM_API void PGM_calculate_with_state(PGM_Handle* handle, PGM_PowerGridModel* model, PGM_Options const* opt, + PGM_MutableDataset const* output_dataset, + PGM_ConstDataset const* batch_dataset, + PGM_StateOutputRequest const* requests, PGM_Idx request_count, + PGM_StateOutput** state_output); + +/** @brief Get the number of scenarios represented by a state result. */ +PGM_API PGM_Idx PGM_state_output_scenario_count(PGM_Handle* handle, PGM_StateOutput const* state_output); + +/** @brief Get a borrowed scenario view. */ +PGM_API void PGM_state_output_get_scenario(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_StateScenarioView* view); + +/** @brief Get a borrowed group view for one scenario. */ +PGM_API void PGM_state_output_get_group(PGM_Handle* handle, PGM_StateOutput const* state_output, PGM_Idx scenario_idx, + PGM_Idx group_idx, PGM_StateGroupView* view); + +/** @brief Destroy an owned state result and invalidate all its borrowed views. */ +PGM_API void PGM_destroy_state_output(PGM_StateOutput* state_output); + /** * @brief Destroy the model returned by PGM_create_model() or PGM_copy_model(). * diff --git a/power_grid_model_c/power_grid_model_c/src/model.cpp b/power_grid_model_c/power_grid_model_c/src/model.cpp index deee1374e0..add13d1db4 100644 --- a/power_grid_model_c/power_grid_model_c/src/model.cpp +++ b/power_grid_model_c/power_grid_model_c/src/model.cpp @@ -32,10 +32,15 @@ #include #include #include +#include #include #include #include +struct PGM_StateOutput { + std::vector> scenarios; +}; + namespace { using namespace power_grid_model; @@ -214,7 +219,9 @@ class BadCalculationRequest : public PowerGridError { }; void calculate_single_batch_dimension_impl(MainModel& model, MainModel::Options const& options, - MutableDataset const& output_dataset, ConstDataset const* batch_dataset) { + MutableDataset const& output_dataset, ConstDataset const* batch_dataset, + std::span state_requests = {}, + std::span> state_outputs = {}) { // check dataset integrity if ((batch_dataset != nullptr) && (!batch_dataset->is_batch() || !output_dataset.is_batch())) { throw BadCalculationRequest{ @@ -225,7 +232,11 @@ void calculate_single_batch_dimension_impl(MainModel& model, MainModel::Options ? safe_ptr_get(batch_dataset) : ConstDataset{false, 1, "update", output_dataset.meta_data()}; - model.calculate(options, output_dataset, exported_update_dataset); + if (!state_requests.empty() || !state_outputs.empty()) { + model.calculate_with_state(options, output_dataset, exported_update_dataset, state_requests, state_outputs); + } else { + model.calculate(options, output_dataset, exported_update_dataset); + } } struct BatchExceptionHandler : public power_grid_model_c::DefaultExceptionHandler { @@ -314,10 +325,13 @@ Idx get_stride_size(ConstDataset const* batch_dataset) { // run calculation void calculate_multi_dimensional_impl(MainModel& model, MainModel::Options const& options, - MutableDataset const& output_dataset, ConstDataset const* batch_dataset) { + MutableDataset const& output_dataset, ConstDataset const* batch_dataset, + std::span state_requests = {}, + std::span> state_outputs = {}) { // for dimension < 2 (one-time or 1D batch), call implementation directly if (auto const batch_dimension = get_batch_dimension(batch_dataset); batch_dimension < 2) { - calculate_single_batch_dimension_impl(model, options, output_dataset, batch_dataset); + calculate_single_batch_dimension_impl(model, options, output_dataset, batch_dataset, state_requests, + state_outputs); return; } @@ -334,11 +348,15 @@ void calculate_multi_dimensional_impl(MainModel& model, MainModel::Options const // a new handle call_with_catch( &local_handle, - [&model, &options, &output_dataset, &safe_batch_dataset, i, stride_size] { + [&model, &options, &output_dataset, &safe_batch_dataset, &state_requests, state_outputs, i, stride_size] { // create sliced datasets for the rest of dimensions ConstDataset const single_update_dataset = safe_batch_dataset.get_individual_scenario(i); MutableDataset const sliced_output_dataset = output_dataset.get_slice_scenario(i * stride_size, (i + 1) * stride_size); + auto const sliced_state_requests = state_requests.subspan(static_cast(i * stride_size), + static_cast(stride_size)); + auto const sliced_state_outputs = state_outputs.subspan(static_cast(i * stride_size), + static_cast(stride_size)); // create a model copy MainModel local_model{model}; @@ -348,7 +366,8 @@ void calculate_multi_dimensional_impl(MainModel& model, MainModel::Options const // recursive call calculate_multi_dimensional_impl(local_model, options, sliced_output_dataset, - safe_batch_dataset.get_next_cartesian_product_dimension()); + safe_batch_dataset.get_next_cartesian_product_dimension(), + sliced_state_requests, sliced_state_outputs); }, MDBatchExceptionHandler{i * stride_size, stride_size}); } @@ -360,13 +379,29 @@ void calculate_multi_dimensional_impl(MainModel& model, MainModel::Options const } void calculate_impl(MainModel& model, PGM_Options const& options, MutableDataset const& output_dataset, - ConstDataset const* batch_dataset) { + ConstDataset const* batch_dataset, std::span state_requests = {}, + std::span> state_outputs = {}) { check_calculate_valid_options(options); auto const extracted_options = extract_calculation_options(options); check_experimental_support(options.experimental_features, model, extracted_options, batch_dataset); - calculate_multi_dimensional_impl(model, extracted_options, output_dataset, batch_dataset); + calculate_multi_dimensional_impl(model, extracted_options, output_dataset, batch_dataset, state_requests, + state_outputs); +} + +Idx get_total_scenarios(ConstDataset const* batch_dataset) { + if (batch_dataset == nullptr) { + return 1; + } + Idx total = 1; + auto current = batch_dataset; + while (current != nullptr) { + auto const& dataset = safe_ptr_get(current); + total *= dataset.batch_size(); + current = safe_ptr_maybe_nullptr(dataset.get_next_cartesian_product_dimension()); + } + return total; } } // namespace @@ -388,5 +423,115 @@ void PGM_calculate(PGM_Handle* handle, PGM_PowerGridModel* model, PGM_Options co batch_exception_handler); } +void PGM_calculate_with_state(PGM_Handle* handle, PGM_PowerGridModel* model, PGM_Options const* opt, + PGM_MutableDataset const* output_dataset, PGM_ConstDataset const* batch_dataset, + PGM_StateOutputRequest const* requests, PGM_Idx request_count, + PGM_StateOutput** state_output) { + call_with_catch( + handle, + [handle, model, opt, output_dataset, batch_dataset, requests, request_count, state_output] { + auto const* cpp_batch_dataset = safe_ptr_maybe_nullptr(cast_to_cpp(batch_dataset)); + Idx const n_scenarios = get_total_scenarios(cpp_batch_dataset); + if (request_count != n_scenarios) { + throw DatasetError{"Model state request count must match the calculation scenario count.\n"}; + } + if (request_count > 0 && requests == nullptr) { + throw DatasetError{"Model state requests cannot be null when request_count is positive.\n"}; + } + + std::vector converted_requests; + converted_requests.reserve(static_cast(request_count)); + for (Idx idx = 0; idx < request_count; ++idx) { + converted_requests.push_back({.y_bus = requests[idx].y_bus != 0, + .jacobian = requests[idx].jacobian != 0}); + } + auto result = std::make_unique(); + result->scenarios.resize(static_cast(n_scenarios)); + + auto& cpp_model = safe_ptr_get(cast_to_cpp(model)); + ScopedModuleLogger const logger_guard{cpp_model, get_logger(safe_ptr_get(handle).logger)}; + calculate_impl(cpp_model, safe_ptr_get(opt), safe_ptr_get(cast_to_cpp(output_dataset)), + cpp_batch_dataset, converted_requests, result->scenarios); + safe_ptr_get(state_output) = result.release(); + }, + batch_exception_handler); +} + +PGM_Idx PGM_state_output_scenario_count(PGM_Handle* handle, PGM_StateOutput const* state_output) { + return call_with_catch(handle, [state_output] { + return static_cast(safe_ptr_get(state_output).scenarios.size()); + }); +} + +void PGM_state_output_get_scenario(PGM_Handle* handle, PGM_StateOutput const* state_output, PGM_Idx scenario_idx, + PGM_StateScenarioView* view) { + call_with_catch(handle, [state_output, scenario_idx, view] { + auto const& state_result = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); + auto& output = safe_ptr_get(view); + output = {}; + if (!state_result.has_value()) { + return; + } + auto const& state = *state_result; + output.has_state = 1; + output.y_bus_requested = state.y_bus_requested; + output.jacobian_requested = state.jacobian_requested; + output.n_groups = static_cast(state.groups.size()); + output.n_input_nodes = static_cast(state.input_node_id.size()); + output.input_node_group = state.input_node_group.data(); + output.input_node_bus = state.input_node_bus.data(); + output.input_node_id = state.input_node_id.data(); + }); +} + +void PGM_state_output_get_group(PGM_Handle* handle, PGM_StateOutput const* state_output, PGM_Idx scenario_idx, + PGM_Idx group_idx, PGM_StateGroupView* view) { + call_with_catch(handle, [state_output, scenario_idx, group_idx, view] { + auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); + if (!scenario.has_value()) { + throw DatasetError{"No model state was requested for this scenario.\n"}; + } + auto const& group = scenario->groups.at(static_cast(group_idx)); + auto& output = safe_ptr_get(view); + output = {}; + auto const& mapping = group.mapping; + output.group = mapping.group; + output.n_bus = mapping.n_bus; + output.is_symmetric = mapping.is_symmetric; + output.n_user_node_refs = static_cast(mapping.bus_user_sequence.size()); + output.bus_user_indptr = mapping.bus_user_indptr.data(); + output.bus_user_sequence = mapping.bus_user_sequence.data(); + output.bus_user_id = mapping.bus_user_id.data(); + output.bus_kind = reinterpret_cast(mapping.bus_kind.data()); + output.origin_branch3_id = mapping.origin_branch3_id.data(); + if (group.y_bus.has_value()) { + auto const& y_bus = *group.y_bus; + output.has_y_bus = 1; + output.y_bus_nnz = static_cast(y_bus.col_indices.size()); + output.y_bus_row_indptr = y_bus.row_indptr.data(); + output.y_bus_col_indices = y_bus.col_indices.data(); + output.admittance_real = y_bus.admittance_real.data(); + output.admittance_imag = y_bus.admittance_imag.data(); + output.n_admittance_values = static_cast(y_bus.admittance_real.size()); + } + if (group.jacobian.has_value()) { + auto const& jacobian = *group.jacobian; + output.has_jacobian = 1; + output.jacobian_nnz = static_cast(jacobian.col_indices_lu.size()); + output.jacobian_row_indptr = jacobian.row_indptr_lu.data(); + output.jacobian_col_indices = jacobian.col_indices_lu.data(); + output.jacobian_h = jacobian.blocks[0].data(); + output.jacobian_n = jacobian.blocks[1].data(); + output.jacobian_m = jacobian.blocks[2].data(); + output.jacobian_l = jacobian.blocks[3].data(); + output.n_jacobian_values = static_cast(jacobian.blocks[0].size()); + } + }); +} + +void PGM_destroy_state_output(PGM_StateOutput* state_output) { + delete state_output; +} + // destroy model void PGM_destroy_model(PGM_PowerGridModel* model) noexcept { destroy(cast_to_cpp(model)); } diff --git a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/basics.hpp b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/basics.hpp index 3f7eb9b251..60337a4457 100644 --- a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/basics.hpp +++ b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/basics.hpp @@ -41,6 +41,7 @@ using RawDatasetInfo = PGM_DatasetInfo; using RawOptions = PGM_Options; using RawDeserializer = PGM_Deserializer; using RawSerializer = PGM_Serializer; +using RawStateOutput = PGM_StateOutput; namespace detail { // custom deleter diff --git a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp index 4a9e203501..a366ad26c7 100644 --- a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp +++ b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp @@ -16,8 +16,41 @@ #include #include +#include namespace power_grid_model_cpp { +struct StateOutputRequest { + bool y_bus{}; + bool jacobian{}; +}; + +class StateOutput { + public: + StateOutput(StateOutput&&) noexcept = default; + StateOutput& operator=(StateOutput&&) noexcept = default; + StateOutput(StateOutput const&) = delete; + StateOutput& operator=(StateOutput const&) = delete; + + Idx scenario_count() const { return handle_.call_with(PGM_state_output_scenario_count, output_.get()); } + PGM_StateScenarioView scenario(Idx scenario_idx) const { + PGM_StateScenarioView view{}; + handle_.call_with(PGM_state_output_get_scenario, output_.get(), scenario_idx, &view); + return view; + } + PGM_StateGroupView group(Idx scenario_idx, Idx group_idx) const { + PGM_StateGroupView view{}; + handle_.call_with(PGM_state_output_get_group, output_.get(), scenario_idx, group_idx, &view); + return view; + } + + private: + friend class Model; + explicit StateOutput(RawStateOutput* output) : output_{output} {} + + Handle handle_{}; + detail::UniquePtr output_; +}; + class Model { public: Model(double system_frequency, DatasetConst const& input_dataset) @@ -62,6 +95,17 @@ class Model { handle_.call_with(PGM_calculate, get(), opt.get(), output_dataset.get(), nullptr); } + StateOutput calculate_with_state(Options const& opt, DatasetMutable const& output_dataset, + std::vector const& requests) { + return calculate_with_state_impl(opt, output_dataset, nullptr, requests); + } + + StateOutput calculate_with_state(Options const& opt, DatasetMutable const& output_dataset, + DatasetConst const& batch_dataset, + std::vector const& requests) { + return calculate_with_state_impl(opt, output_dataset, batch_dataset.get(), requests); + } + // Attach a logger so it receives output from calculations performed on this model. // Attaching the same logger twice is a no-op. See logger.hpp for lifetime notes: the // logger may safely be destroyed while still registered, but it can then no longer be @@ -75,6 +119,20 @@ class Model { void detach_all_loggers() const { handle_.unregister_all_loggers(); } private: + StateOutput calculate_with_state_impl(Options const& opt, DatasetMutable const& output_dataset, + RawConstDataset const* batch_dataset, + std::vector const& requests) { + std::vector raw_requests; + raw_requests.reserve(requests.size()); + for (auto const& request : requests) { + raw_requests.push_back({static_cast(request.y_bus), static_cast(request.jacobian)}); + } + RawStateOutput* state_output{}; + handle_.call_with(PGM_calculate_with_state, get(), opt.get(), output_dataset.get(), batch_dataset, + raw_requests.data(), static_cast(raw_requests.size()), &state_output); + return StateOutput{state_output}; + } + Handle handle_{}; detail::UniquePtr model_; }; diff --git a/src/power_grid_model/__init__.py b/src/power_grid_model/__init__.py index 48ae1142be..f895257c5f 100644 --- a/src/power_grid_model/__init__.py +++ b/src/power_grid_model/__init__.py @@ -6,6 +6,15 @@ from power_grid_model._core.dataset_definitions import AttributeType, ComponentType, DatasetType from power_grid_model._core.logger import Logger +from power_grid_model._core.model_state import ( + ModelState, + ModelStateBusKind, + ModelStateGroup, + ModelStateGroupMapping, + ModelStateJacobian, + ModelStateYBus, + StateOutputRequest, +) from power_grid_model._core.power_grid_core import pgm_version from power_grid_model._core.power_grid_meta import ( attribute_dtype, @@ -53,8 +62,15 @@ "Logger", "LoggerType", "MeasuredTerminalType", + "ModelState", + "ModelStateBusKind", + "ModelStateGroup", + "ModelStateGroupMapping", + "ModelStateJacobian", + "ModelStateYBus", "PowerGridModel", "ShortCircuitVoltageScaling", + "StateOutputRequest", "TapChangingStrategy", "WindingType", "__version__", diff --git a/src/power_grid_model/_core/model_state.py b/src/power_grid_model/_core/model_state.py new file mode 100644 index 0000000000..ff59599d85 --- /dev/null +++ b/src/power_grid_model/_core/model_state.py @@ -0,0 +1,169 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +"""Typed Python views of native power-flow calculation state.""" + +from ctypes import pointer +from dataclasses import dataclass +from enum import IntEnum +from typing import TYPE_CHECKING + +import numpy as np +from numpy.typing import NDArray + +from power_grid_model._core.index_integer import IdNp, IdxNp +from power_grid_model._core.power_grid_core import ( + StateGroupViewC, + StateOutputPtr, + StateScenarioViewC, + get_power_grid_core, +) + +if TYPE_CHECKING: + from numpy.typing import DTypeLike + + +class ModelStateBusKind(IntEnum): + """Bus mapping origin in a solver group.""" + + input_node = 0 + synthetic_branch3 = 1 + + +@dataclass(frozen=True) +class StateOutputRequest: + y_bus: bool = False + jacobian: bool = False + + +class _OwnedArray(np.ndarray): + _state_owner: "_NativeStateOutput | None" + + def __array_finalize__(self, source): + self._state_owner = getattr(source, "_state_owner", None) + + +class _NativeStateOutput: + def __init__(self, pointer: StateOutputPtr): + self.pointer = pointer + + def __del__(self): + if self.pointer: + get_power_grid_core().destroy_state_output(self.pointer) + self.pointer = StateOutputPtr() + + +def _array(pointer, size: int, dtype: "DTypeLike", owner: _NativeStateOutput) -> NDArray: + result = np.empty(0, dtype=dtype) if size == 0 else np.ctypeslib.as_array(pointer, shape=(size,)) + result = result.view(_OwnedArray) + result._state_owner = owner + return result + + +@dataclass(frozen=True) +class ModelStateYBus: + row_indptr: NDArray[np.int64] + col_indices: NDArray[np.int64] + admittance_real: NDArray[np.float64] + admittance_imag: NDArray[np.float64] + + +@dataclass(frozen=True) +class ModelStateJacobian: + row_indptr_lu: NDArray[np.int64] + col_indices_lu: NDArray[np.int64] + h: NDArray[np.float64] + n: NDArray[np.float64] + m: NDArray[np.float64] + l_block: NDArray[np.float64] + + +@dataclass(frozen=True) +class ModelStateGroupMapping: + group: int + n_bus: int + is_symmetric: bool + bus_user_indptr: NDArray[np.int64] + bus_user_sequence: NDArray[np.int64] + bus_user_id: NDArray[np.int32] + bus_kind: NDArray[np.int8] + origin_branch3_id: NDArray[np.int32] + + +@dataclass(frozen=True) +class ModelStateGroup: + mapping: ModelStateGroupMapping + y_bus: ModelStateYBus | None + jacobian: ModelStateJacobian | None + + +@dataclass(frozen=True) +class ModelState: + y_bus_requested: bool + jacobian_requested: bool + input_node_group: NDArray[np.int64] + input_node_bus: NDArray[np.int64] + input_node_id: NDArray[np.int32] + groups: tuple[ModelStateGroup, ...] + + +def _build_scenario(owner: _NativeStateOutput, scenario_idx: int) -> ModelState | None: + pgc = get_power_grid_core() + scenario_view = StateScenarioViewC() + pgc.state_output_get_scenario(owner.pointer, scenario_idx, pointer(scenario_view)) + if not scenario_view.has_state: + return None + + input_node_group = _array(scenario_view.input_node_group, scenario_view.n_input_nodes, IdxNp, owner) + input_node_bus = _array(scenario_view.input_node_bus, scenario_view.n_input_nodes, IdxNp, owner) + input_node_id = _array(scenario_view.input_node_id, scenario_view.n_input_nodes, IdNp, owner) + groups = [] + for group_idx in range(scenario_view.n_groups): + view = StateGroupViewC() + pgc.state_output_get_group(owner.pointer, scenario_idx, group_idx, pointer(view)) + mapping = ModelStateGroupMapping( + group=view.group, + n_bus=view.n_bus, + is_symmetric=bool(view.is_symmetric), + bus_user_indptr=_array(view.bus_user_indptr, view.n_bus + 1, IdxNp, owner), + bus_user_sequence=_array(view.bus_user_sequence, view.n_user_node_refs, IdxNp, owner), + bus_user_id=_array(view.bus_user_id, view.n_user_node_refs, IdNp, owner), + bus_kind=_array(view.bus_kind, view.n_bus, np.int8, owner), + origin_branch3_id=_array(view.origin_branch3_id, view.n_bus, IdNp, owner), + ) + y_bus = None + if view.has_y_bus: + y_bus = ModelStateYBus( + row_indptr=_array(view.y_bus_row_indptr, view.n_bus + 1, IdxNp, owner), + col_indices=_array(view.y_bus_col_indices, view.y_bus_nnz, IdxNp, owner), + admittance_real=_array(view.admittance_real, view.n_admittance_values, np.float64, owner), + admittance_imag=_array(view.admittance_imag, view.n_admittance_values, np.float64, owner), + ) + jacobian = None + if view.has_jacobian: + jacobian = ModelStateJacobian( + row_indptr_lu=_array(view.jacobian_row_indptr, view.n_bus + 1, IdxNp, owner), + col_indices_lu=_array(view.jacobian_col_indices, view.jacobian_nnz, IdxNp, owner), + h=_array(view.jacobian_h, view.n_jacobian_values, np.float64, owner), + n=_array(view.jacobian_n, view.n_jacobian_values, np.float64, owner), + m=_array(view.jacobian_m, view.n_jacobian_values, np.float64, owner), + l_block=_array(view.jacobian_l, view.n_jacobian_values, np.float64, owner), + ) + groups.append(ModelStateGroup(mapping=mapping, y_bus=y_bus, jacobian=jacobian)) + + return ModelState( + y_bus_requested=bool(scenario_view.y_bus_requested), + jacobian_requested=bool(scenario_view.jacobian_requested), + input_node_group=input_node_group, + input_node_bus=input_node_bus, + input_node_id=input_node_id, + groups=tuple(groups), + ) + + +def build_model_states(pointer: StateOutputPtr) -> list[ModelState | None]: + owner = _NativeStateOutput(pointer) + pgc = get_power_grid_core() + n_scenarios = pgc.state_output_scenario_count(owner.pointer) + return [_build_scenario(owner, scenario_idx) for scenario_idx in range(n_scenarios)] diff --git a/src/power_grid_model/_core/power_grid_core.py b/src/power_grid_model/_core/power_grid_core.py index 35797d6445..f04a9073d3 100644 --- a/src/power_grid_model/_core/power_grid_core.py +++ b/src/power_grid_model/_core/power_grid_core.py @@ -8,7 +8,7 @@ import threading from collections.abc import Callable -from ctypes import CDLL, CFUNCTYPE, POINTER, c_char, c_char_p, c_double, c_size_t, c_void_p +from ctypes import CDLL, CFUNCTYPE, POINTER, Structure, c_char, c_char_p, c_double, c_int8, c_size_t, c_void_p from inspect import signature from itertools import chain from typing import Any @@ -133,6 +133,61 @@ class LoggerPtr(c_void_p): """ +class StateOutputPtr(c_void_p): + """Pointer to owned native state output.""" + + +class StateOutputRequestC(Structure): + _fields_ = [("y_bus", IdxC), ("jacobian", IdxC)] + + +class StateScenarioViewC(Structure): + _fields_ = [ + ("has_state", IdxC), + ("y_bus_requested", IdxC), + ("jacobian_requested", IdxC), + ("n_groups", IdxC), + ("n_input_nodes", IdxC), + ("input_node_group", IdxPtr), + ("input_node_bus", IdxPtr), + ("input_node_id", IDPtr), + ] + + +DoublePtr = POINTER(c_double) +Int8Ptr = POINTER(c_int8) + + +class StateGroupViewC(Structure): + _fields_ = [ + ("group", IdxC), + ("n_bus", IdxC), + ("is_symmetric", IdxC), + ("n_user_node_refs", IdxC), + ("bus_user_indptr", IdxPtr), + ("bus_user_sequence", IdxPtr), + ("bus_user_id", IDPtr), + ("bus_kind", Int8Ptr), + ("origin_branch3_id", IDPtr), + ("has_y_bus", IdxC), + ("y_bus_nnz", IdxC), + ("y_bus_row_indptr", IdxPtr), + ("y_bus_col_indices", IdxPtr), + ("admittance_real", DoublePtr), + ("admittance_imag", DoublePtr), + ("n_admittance_values", IdxC), + ("has_jacobian", IdxC), + ("jacobian_nnz", IdxC), + ("jacobian_row_indptr", IdxPtr), + ("jacobian_col_indices", IdxPtr), + ("jacobian_h", DoublePtr), + ("jacobian_n", DoublePtr), + ("jacobian_m", DoublePtr), + ("jacobian_l", DoublePtr), + ("n_jacobian_values", IdxC), + ] + + def _load_core() -> CDLL: """ @@ -156,6 +211,17 @@ def _load_core() -> CDLL: # manual argtypes for functions that use callback parameters (not handled by make_c_binding) _CDLL.PGM_logger_get_output.argtypes = [HandlePtr, LoggerPtr, _LogOutputCallbackType, c_void_p] _CDLL.PGM_logger_get_output.restype = None +_CDLL.PGM_calculate_with_state.argtypes = [ + HandlePtr, + ModelPtr, + OptionsPtr, + MutableDatasetPtr, + ConstDatasetPtr, + POINTER(StateOutputRequestC), + IdxC, + POINTER(StateOutputPtr), +] +_CDLL.PGM_calculate_with_state.restype = None def make_c_binding(func: Callable): @@ -409,6 +475,39 @@ def calculate( # type: ignore[empty-body] ) -> None: pass # pragma: no cover + @make_c_binding + def calculate_with_state( # noqa: PLR0913 # type: ignore[empty-body] + self, + model: ModelPtr, + opt: OptionsPtr, + output_data: MutableDatasetPtr, # type: ignore[valid-type] + update_data: ConstDatasetPtr, # type: ignore[valid-type] + requests: POINTER(StateOutputRequestC), + request_count: int, + state_output: POINTER(StateOutputPtr), + ) -> None: + pass # pragma: no cover + + @make_c_binding + def state_output_scenario_count(self, state_output: StateOutputPtr) -> int: # type: ignore[empty-body] + pass # pragma: no cover + + @make_c_binding + def state_output_get_scenario( + self, state_output: StateOutputPtr, scenario_idx: int, view: POINTER(StateScenarioViewC) + ) -> None: # type: ignore[empty-body] + pass # pragma: no cover + + @make_c_binding + def state_output_get_group( + self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int, view: POINTER(StateGroupViewC) + ) -> None: # type: ignore[empty-body] + pass # pragma: no cover + + @make_c_binding + def destroy_state_output(self, state_output: StateOutputPtr) -> None: # type: ignore[empty-body] + pass # pragma: no cover + @make_c_binding def dataset_info_name(self, info: DatasetInfoPtr) -> str: # type: ignore[empty-body] pass # pragma: no cover diff --git a/src/power_grid_model/_core/power_grid_model.py b/src/power_grid_model/_core/power_grid_model.py index 73ff0ce078..7d13a0971a 100644 --- a/src/power_grid_model/_core/power_grid_model.py +++ b/src/power_grid_model/_core/power_grid_model.py @@ -7,6 +7,8 @@ """ import itertools +from collections.abc import Sequence +from ctypes import pointer from enum import IntEnum from math import prod from typing import Any, Literal, overload @@ -50,17 +52,35 @@ ) from power_grid_model._core.error_handling import PowerGridBatchError, assert_no_error, handle_errors from power_grid_model._core.index_integer import IdNp, IdxNp +from power_grid_model._core.model_state import ModelState, StateOutputRequest, build_model_states from power_grid_model._core.options import Options from power_grid_model._core.power_grid_core import ( ConstDatasetPtr, IDPtr, IdxPtr, ModelPtr, + StateOutputPtr, + StateOutputRequestC, get_power_grid_core as get_pgc, ) from power_grid_model._core.typing import ComponentAttributeMapping, ComponentAttributeMappingDict +def _prepare_state_requests( + request: bool | Sequence[StateOutputRequest], batch_size: int +) -> list[StateOutputRequest] | None: + if request is False: + return None + if request is True: + return [StateOutputRequest(y_bus=True, jacobian=True) for _ in range(batch_size)] + requests = list(request) + if len(requests) != batch_size: + raise ValueError(f"Expected {batch_size} model state requests, got {len(requests)}.") + if any(not isinstance(item, StateOutputRequest) for item in requests): + raise TypeError("Each model state request must be a StateOutputRequest instance.") + return requests + + class PowerGridModel: """ Main class for Power Grid Model @@ -308,7 +328,8 @@ def _calculate_impl( # noqa: PLR0913 continue_on_batch_error: bool, decode_error: bool, experimental_features: _ExperimentalFeatures | str, # NOSONAR # noqa: ARG002 - ) -> Dataset: + get_model_state: bool | Sequence[StateOutputRequest] = False, + ) -> Dataset | tuple[Dataset, ModelState | list[ModelState | None] | None]: """ Core calculation routine @@ -338,6 +359,8 @@ def _calculate_impl( # noqa: PLR0913 update_ptr: ConstDatasetPtr = prepared_update[0].get_dataset_ptr() if prepared_update else ConstDatasetPtr() batch_size = prod(x.get_info().batch_size() for x in prepared_update) + state_requests = _prepare_state_requests(get_model_state, batch_size) + output_data = self._construct_output( output_component_types=output_component_types, calculation_type=calculation_type, @@ -351,13 +374,27 @@ def _calculate_impl( # noqa: PLR0913 ) # run calculation - get_pgc().calculate( - # model and options - self._model, - options.opt, - output_data=prepared_result.get_dataset_ptr(), - update_data=update_ptr, - ) + state_output = StateOutputPtr() + if state_requests is None: + get_pgc().calculate( + self._model, + options.opt, + output_data=prepared_result.get_dataset_ptr(), + update_data=update_ptr, + ) + else: + raw_requests = (StateOutputRequestC * batch_size)( + *(StateOutputRequestC(request.y_bus, request.jacobian) for request in state_requests) + ) + get_pgc().calculate_with_state( + self._model, + options.opt, + prepared_result.get_dataset_ptr(), + update_ptr, + raw_requests, + batch_size, + pointer(state_output), + ) self._handle_errors( continue_on_batch_error=continue_on_batch_error, @@ -365,7 +402,11 @@ def _calculate_impl( # noqa: PLR0913 decode_error=decode_error, ) - return output_data + if state_requests is None: + return output_data + states = build_model_states(state_output) if state_output else [None] * batch_size + model_state = states if is_batch else states[0] + return output_data, model_state def _calculate_power_flow( # noqa: PLR0913 self, @@ -381,8 +422,9 @@ def _calculate_power_flow( # noqa: PLR0913 decode_error: bool = True, tap_changing_strategy: TapChangingStrategy | str = TapChangingStrategy.disabled, calculation_initialization: CalculationInitialization | str = CalculationInitialization.default, + get_model_state: bool | Sequence[StateOutputRequest] = False, experimental_features: _ExperimentalFeatures | str = _ExperimentalFeatures.disabled, - ) -> Dataset: + ) -> Dataset | tuple[Dataset, ModelState | list[ModelState | None] | None]: calculation_type = CalculationType.power_flow options = self._options( calculation_type=calculation_type, @@ -404,6 +446,7 @@ def _calculate_power_flow( # noqa: PLR0913 continue_on_batch_error=continue_on_batch_error, decode_error=decode_error, experimental_features=experimental_features, + get_model_state=get_model_state, ) def _calculate_state_estimation( # noqa: PLR0913 @@ -571,6 +614,14 @@ def calculate_power_flow( tap_changing_strategy: TapChangingStrategy | str = ..., calculation_initialization: CalculationInitialization | str = ..., ) -> DenseBatchOutputDataset: ... + @overload + def calculate_power_flow( + self, + *, + get_model_state: bool | Sequence[StateOutputRequest], + **kwargs: Any, + ) -> Dataset | tuple[Dataset, ModelState | list[ModelState | None] | None]: ... + def calculate_power_flow( # noqa: PLR0913 self, *, @@ -585,7 +636,8 @@ def calculate_power_flow( # noqa: PLR0913 decode_error: bool = True, tap_changing_strategy: TapChangingStrategy | str = TapChangingStrategy.disabled, calculation_initialization: CalculationInitialization | str = CalculationInitialization.default, - ) -> Dataset: + get_model_state: bool | Sequence[StateOutputRequest] = False, + ) -> Dataset | tuple[Dataset, ModelState | list[ModelState | None] | None]: """ Calculate power flow once with the current model attributes. Or calculate in batch with the given update dataset in batch. @@ -687,6 +739,7 @@ def calculate_power_flow( # noqa: PLR0913 decode_error=decode_error, tap_changing_strategy=tap_changing_strategy, calculation_initialization=calculation_initialization, + get_model_state=get_model_state, ) @overload diff --git a/tests/unit/test_multi_dimensional_batch.py b/tests/unit/test_multi_dimensional_batch.py index d36782d689..0200e6ad32 100644 --- a/tests/unit/test_multi_dimensional_batch.py +++ b/tests/unit/test_multi_dimensional_batch.py @@ -6,7 +6,13 @@ import numpy as np -from power_grid_model import AttributeType as AT, ComponentType as CT, DatasetType, PowerGridModel +from power_grid_model import ( + AttributeType as AT, + ComponentType as CT, + DatasetType, + PowerGridModel, + StateOutputRequest, +) from power_grid_model.utils import json_deserialize input_data = { @@ -64,3 +70,14 @@ def test_multi_dimensional_batch(): ) assert np.allclose(result[CT.source][AT.i].ravel(), i_source_ref) + + state_requests = [StateOutputRequest(y_bus=scenario_idx % 2 == 0) for scenario_idx in range(60)] + _, model_states = pgm.calculate_power_flow( + update_data=[u_ref_batch, p_specified_batch, q_specified_batch], + output_component_types={CT.source: [AT.i]}, + get_model_state=state_requests, + ) + + assert isinstance(model_states, list) + assert len(model_states) == len(state_requests) + assert [state is not None for state in model_states] == [request.y_bus for request in state_requests] diff --git a/tests/unit/test_power_grid_model.py b/tests/unit/test_power_grid_model.py index 720e5a9751..b3a8a9094a 100644 --- a/tests/unit/test_power_grid_model.py +++ b/tests/unit/test_power_grid_model.py @@ -12,12 +12,14 @@ AttributeType as AT, BranchSide, CalculationInitialization, + CalculationMethod, ComponentAttributeFilterOptions, ComponentType as CT, DatasetType as DT, LoadGenType, MeasuredTerminalType, PowerGridModel, + StateOutputRequest, initialize_array, ) from power_grid_model._core.utils import compatibility_convert_row_columnar_dataset @@ -167,6 +169,63 @@ def test_simple_calculation_initialization(model: PowerGridModel, sym_output, ca compare_result(result, sym_output, rtol=0.0, atol=1e-8) +def test_power_flow_model_state(model: PowerGridModel): + output_data, model_state = model.calculate_power_flow(get_model_state=True) + + assert CT.node in output_data + assert model_state is not None + assert model_state.y_bus_requested + assert model_state.jacobian_requested + assert model_state.input_node_id.tolist() == [0] + assert len(model_state.groups) == 1 + group = model_state.groups[0] + assert group.y_bus is not None + assert group.jacobian is not None + assert group.mapping.bus_user_id.tolist() == [0] + assert group.y_bus.row_indptr.shape == (group.mapping.n_bus + 1,) + assert group.y_bus.admittance_real.size > 0 + assert group.jacobian.h.size == group.jacobian.n.size + + admittance_real = group.y_bus.admittance_real + del model_state + assert np.isfinite(admittance_real).all() + + +def test_linear_power_flow_model_state_has_no_jacobian(model: PowerGridModel): + _, model_state = model.calculate_power_flow( + calculation_method=CalculationMethod.linear, + get_model_state=[StateOutputRequest(y_bus=True, jacobian=True)], + ) + + assert model_state is not None + assert model_state.groups[0].y_bus is not None + assert model_state.groups[0].jacobian is None + + +def test_power_flow_model_state_batch_requests(model: PowerGridModel, update_batch): + _, states = model.calculate_power_flow( + update_data=update_batch, + get_model_state=[ + StateOutputRequest(y_bus=True), + StateOutputRequest(jacobian=True), + ], + ) + + assert isinstance(states, list) + assert len(states) == 2 + assert states[0] is not None and states[0].y_bus_requested + assert states[0].groups[0].y_bus is not None + assert states[0].groups[0].jacobian is None + assert states[1] is not None and states[1].jacobian_requested + assert states[1].groups[0].y_bus is None + assert states[1].groups[0].jacobian is not None + + +def test_power_flow_model_state_request_count_must_match_batch(model: PowerGridModel, update_batch): + with pytest.raises(ValueError, match="Expected 2 model state requests"): + model.calculate_power_flow(update_data=update_batch, get_model_state=[StateOutputRequest(y_bus=True)]) + + def test_simple_permanent_update(model: PowerGridModel, update_batch, sym_output_batch): model.update(update_data=get_dataset_scenario(update_batch, 0)) # single permanent model update result = model.calculate_power_flow() From e298d9db1da490b1580bd6e8844282a8d1152d3d Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Fri, 9 Oct 2026 20:53:30 +0200 Subject: [PATCH 02/25] python side Signed-off-by: Jerry Jinfeng Guo --- .../power_grid_model_c/src/model.cpp | 15 +++-- src/power_grid_model/_core/model_state.py | 4 +- src/power_grid_model/_core/power_grid_core.py | 18 ++++-- .../_core/power_grid_model.py | 63 ++++++++++++------- tests/native_api_tests/test_api_model.cpp | 16 +++++ tests/unit/test_power_grid_model.py | 8 ++- 6 files changed, 87 insertions(+), 37 deletions(-) diff --git a/power_grid_model_c/power_grid_model_c/src/model.cpp b/power_grid_model_c/power_grid_model_c/src/model.cpp index add13d1db4..0abdc4a235 100644 --- a/power_grid_model_c/power_grid_model_c/src/model.cpp +++ b/power_grid_model_c/power_grid_model_c/src/model.cpp @@ -353,10 +353,14 @@ void calculate_multi_dimensional_impl(MainModel& model, MainModel::Options const ConstDataset const single_update_dataset = safe_batch_dataset.get_individual_scenario(i); MutableDataset const sliced_output_dataset = output_dataset.get_slice_scenario(i * stride_size, (i + 1) * stride_size); - auto const sliced_state_requests = state_requests.subspan(static_cast(i * stride_size), - static_cast(stride_size)); - auto const sliced_state_outputs = state_outputs.subspan(static_cast(i * stride_size), - static_cast(stride_size)); + auto const sliced_state_requests = state_requests.empty() + ? state_requests + : state_requests.subspan(static_cast(i * stride_size), + static_cast(stride_size)); + auto const sliced_state_outputs = state_outputs.empty() + ? state_outputs + : state_outputs.subspan(static_cast(i * stride_size), + static_cast(stride_size)); // create a model copy MainModel local_model{model}; @@ -430,6 +434,9 @@ void PGM_calculate_with_state(PGM_Handle* handle, PGM_PowerGridModel* model, PGM call_with_catch( handle, [handle, model, opt, output_dataset, batch_dataset, requests, request_count, state_output] { + if (get_calculation_type(safe_ptr_get(opt)) != CalculationType::power_flow) { + throw BadCalculationRequest{"Model state output is only available for power-flow calculations.\n"}; + } auto const* cpp_batch_dataset = safe_ptr_maybe_nullptr(cast_to_cpp(batch_dataset)); Idx const n_scenarios = get_total_scenarios(cpp_batch_dataset); if (request_count != n_scenarios) { diff --git a/src/power_grid_model/_core/model_state.py b/src/power_grid_model/_core/model_state.py index ff59599d85..1758cd7d93 100644 --- a/src/power_grid_model/_core/model_state.py +++ b/src/power_grid_model/_core/model_state.py @@ -7,7 +7,7 @@ from ctypes import pointer from dataclasses import dataclass from enum import IntEnum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import numpy as np from numpy.typing import NDArray @@ -56,7 +56,7 @@ def __del__(self): def _array(pointer, size: int, dtype: "DTypeLike", owner: _NativeStateOutput) -> NDArray: result = np.empty(0, dtype=dtype) if size == 0 else np.ctypeslib.as_array(pointer, shape=(size,)) - result = result.view(_OwnedArray) + result = cast(_OwnedArray, result.view(_OwnedArray)) result._state_owner = owner return result diff --git a/src/power_grid_model/_core/power_grid_core.py b/src/power_grid_model/_core/power_grid_core.py index f04a9073d3..c5b5ecd889 100644 --- a/src/power_grid_model/_core/power_grid_core.py +++ b/src/power_grid_model/_core/power_grid_core.py @@ -188,6 +188,12 @@ class StateGroupViewC(Structure): ] +StateOutputRequestPtr = POINTER(StateOutputRequestC) +StateScenarioViewPtr = POINTER(StateScenarioViewC) +StateGroupViewPtr = POINTER(StateGroupViewC) +StateOutputPtrPtr = POINTER(StateOutputPtr) + + def _load_core() -> CDLL: """ @@ -217,9 +223,9 @@ def _load_core() -> CDLL: OptionsPtr, MutableDatasetPtr, ConstDatasetPtr, - POINTER(StateOutputRequestC), + StateOutputRequestPtr, IdxC, - POINTER(StateOutputPtr), + StateOutputPtrPtr, ] _CDLL.PGM_calculate_with_state.restype = None @@ -482,9 +488,9 @@ def calculate_with_state( # noqa: PLR0913 # type: ignore[empty-body] opt: OptionsPtr, output_data: MutableDatasetPtr, # type: ignore[valid-type] update_data: ConstDatasetPtr, # type: ignore[valid-type] - requests: POINTER(StateOutputRequestC), + requests: StateOutputRequestPtr, # type: ignore[valid-type] request_count: int, - state_output: POINTER(StateOutputPtr), + state_output: StateOutputPtrPtr, # type: ignore[valid-type] ) -> None: pass # pragma: no cover @@ -494,13 +500,13 @@ def state_output_scenario_count(self, state_output: StateOutputPtr) -> int: # t @make_c_binding def state_output_get_scenario( - self, state_output: StateOutputPtr, scenario_idx: int, view: POINTER(StateScenarioViewC) + self, state_output: StateOutputPtr, scenario_idx: int, view: StateScenarioViewPtr # type: ignore[valid-type] ) -> None: # type: ignore[empty-body] pass # pragma: no cover @make_c_binding def state_output_get_group( - self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int, view: POINTER(StateGroupViewC) + self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int, view: StateGroupViewPtr # type: ignore[valid-type] ) -> None: # type: ignore[empty-body] pass # pragma: no cover diff --git a/src/power_grid_model/_core/power_grid_model.py b/src/power_grid_model/_core/power_grid_model.py index 7d13a0971a..a299a921a6 100644 --- a/src/power_grid_model/_core/power_grid_model.py +++ b/src/power_grid_model/_core/power_grid_model.py @@ -11,7 +11,7 @@ from ctypes import pointer from enum import IntEnum from math import prod -from typing import Any, Literal, overload +from typing import Any, Literal, cast, overload import numpy as np @@ -404,7 +404,7 @@ def _calculate_impl( # noqa: PLR0913 if state_requests is None: return output_data - states = build_model_states(state_output) if state_output else [None] * batch_size + states: list[ModelState | None] = build_model_states(state_output) if state_output else [None] * batch_size model_state = states if is_batch else states[0] return output_data, model_state @@ -473,15 +473,18 @@ def _calculate_state_estimation( # noqa: PLR0913 threading=threading, experimental_features=experimental_features, ) - return self._calculate_impl( - calculation_type=calculation_type, - symmetric=symmetric, - update_data=update_data, - output_component_types=output_component_types, - options=options, - continue_on_batch_error=continue_on_batch_error, - decode_error=decode_error, - experimental_features=experimental_features, + return cast( + Dataset, + self._calculate_impl( + calculation_type=calculation_type, + symmetric=symmetric, + update_data=update_data, + output_component_types=output_component_types, + options=options, + continue_on_batch_error=continue_on_batch_error, + decode_error=decode_error, + experimental_features=experimental_features, + ), ) def _calculate_short_circuit( # noqa: PLR0913 @@ -507,15 +510,18 @@ def _calculate_short_circuit( # noqa: PLR0913 short_circuit_voltage_scaling=short_circuit_voltage_scaling, experimental_features=experimental_features, ) - return self._calculate_impl( - calculation_type=calculation_type, - symmetric=symmetric, - update_data=update_data, - output_component_types=output_component_types, - options=options, - continue_on_batch_error=continue_on_batch_error, - decode_error=decode_error, - experimental_features=experimental_features, + return cast( + Dataset, + self._calculate_impl( + calculation_type=calculation_type, + symmetric=symmetric, + update_data=update_data, + output_component_types=output_component_types, + options=options, + continue_on_batch_error=continue_on_batch_error, + decode_error=decode_error, + experimental_features=experimental_features, + ), ) @overload @@ -619,7 +625,16 @@ def calculate_power_flow( self, *, get_model_state: bool | Sequence[StateOutputRequest], - **kwargs: Any, + symmetric: bool = True, + error_tolerance: float = 1e-8, + max_iterations: int = 20, + calculation_method: CalculationMethod | str = CalculationMethod.newton_raphson, + update_data: BatchDataset | list[BatchDataset] | None = None, + threading: int = -1, + output_component_types: ComponentAttributeMapping = None, + continue_on_batch_error: bool = False, + decode_error: bool = True, + tap_changing_strategy: TapChangingStrategy | str = TapChangingStrategy.disabled, ) -> Dataset | tuple[Dataset, ModelState | list[ModelState | None] | None]: ... def calculate_power_flow( # noqa: PLR0913 @@ -711,9 +726,13 @@ def calculate_power_flow( # noqa: PLR0913 their reference voltage and voltage regulated nodes at their reference magnitude. - average_source: Every node at the average reference voltage of all sources with its phase shift, as in the iterative current power flow. + get_model_state (bool or sequence of StateOutputRequest, optional): + Request typed native solver state. True requests Y-bus and Jacobian for every scenario; a sequence + selects requested fields per flattened batch scenario. The sequence length must match the total + scenario count. False preserves the output-only return. Returns: - Dictionary of results of all components. + Dictionary of results of all components, or a tuple of (output, model state) when requested. - key: Component type name to be updated in batch. - value: diff --git a/tests/native_api_tests/test_api_model.cpp b/tests/native_api_tests/test_api_model.cpp index 14d503a3e2..064e4dc955 100644 --- a/tests/native_api_tests/test_api_model.cpp +++ b/tests/native_api_tests/test_api_model.cpp @@ -278,6 +278,22 @@ TEST_CASE("API Model") { CHECK(node_result_u_pu[0] == doctest::Approx(0.5)); check_common_node_results(); } + + SUBCASE("Native model state") { + auto state_output = model.calculate_with_state(options, single_output_dataset, {{true, true}}); + REQUIRE(state_output.scenario_count() == 1); + auto const scenario = state_output.scenario(0); + CHECK(scenario.has_state == 1); + CHECK(scenario.y_bus_requested == 1); + CHECK(scenario.jacobian_requested == 1); + REQUIRE(scenario.n_groups > 0); + + auto const group = state_output.group(0, 0); + CHECK(group.has_y_bus == 1); + CHECK(group.has_jacobian == 1); + CHECK(group.y_bus_nnz > 0); + CHECK(group.jacobian_nnz > 0); + } } SUBCASE("Test get indexer") { diff --git a/tests/unit/test_power_grid_model.py b/tests/unit/test_power_grid_model.py index b3a8a9094a..3a536258dc 100644 --- a/tests/unit/test_power_grid_model.py +++ b/tests/unit/test_power_grid_model.py @@ -212,11 +212,13 @@ def test_power_flow_model_state_batch_requests(model: PowerGridModel, update_bat ) assert isinstance(states, list) - assert len(states) == 2 - assert states[0] is not None and states[0].y_bus_requested + assert len(states) == 2 # noqa: PLR2004 + assert states[0] is not None + assert states[0].y_bus_requested assert states[0].groups[0].y_bus is not None assert states[0].groups[0].jacobian is None - assert states[1] is not None and states[1].jacobian_requested + assert states[1] is not None + assert states[1].jacobian_requested assert states[1].groups[0].y_bus is None assert states[1].groups[0].jacobian is not None From 204d34d865caf7ef8f7b1037b47bfbbb6983377d Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Fri, 9 Oct 2026 20:59:51 +0200 Subject: [PATCH 03/25] fix link solver Signed-off-by: Jerry Jinfeng Guo --- .../include/power_grid_model/link_solver.hpp | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/link_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/link_solver.hpp index 02e87abe42..c07da210ef 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/link_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/link_solver.hpp @@ -211,14 +211,10 @@ inline void backward_substitution(ReducedEchelonForm& elimination_result) { // only iterate over free columns to the right of the pivot column // as these are the only ones that can be affected by the backward substitution for (auto const backward_col_idx : backward_substitution_free_right_cols(free_col_indices, pivot_col_idx)) { - std::ignore = - elimination_result.matrix.get_value(pivot_row_idx, backward_col_idx) - .transform([&elimination_result, multiplier_value, row_idx, - backward_col_idx](IntS pivot_value) { - elimination_result.matrix.add_to_value(static_cast(-multiplier_value * pivot_value), - row_idx, backward_col_idx); - return pivot_value; - }); + if (auto const pivot_value = elimination_result.matrix.get_value(pivot_row_idx, backward_col_idx)) { + elimination_result.matrix.add_to_value( + static_cast(-multiplier_value * *pivot_value), row_idx, backward_col_idx); + } } elimination_result.rhs[row_idx] -= static_cast(multiplier_value) * elimination_result.rhs[pivot_row_idx]; @@ -270,11 +266,9 @@ inline SolutionSet set_solution_system(ReducedEchelonForm& result) { auto const pivot_edge_idx = result.pivot_edge_indices[matrix_row]; for (auto dfs_matrix_col : std::views::iota(Idx{}, free_indices_size)) { Idx const free_edge_idx = result.free_edge_indices[dfs_matrix_col]; - std::ignore = result.matrix.get_value(matrix_row, free_edge_idx) - .transform([&dfs_matrix, pivot_edge_idx, dfs_matrix_col](IntS matrix_element) { - dfs_matrix.set_value(matrix_element, pivot_edge_idx, dfs_matrix_col); - return matrix_element; - }); + if (auto const matrix_element = result.matrix.get_value(matrix_row, free_edge_idx)) { + dfs_matrix.set_value(*matrix_element, pivot_edge_idx, dfs_matrix_col); + } } extended_rhs[pivot_edge_idx] = result.rhs[matrix_row]; } @@ -295,13 +289,9 @@ inline std::vector set_projection_system(Idx free_indices_number, for (Idx dfs_matrix_col = 0; dfs_matrix_col < free_indices_number; dfs_matrix_col++) { auto dot_product_rhs = DoubleComplex{}; for (Idx dfs_matrix_row = 0; dfs_matrix_row < total_indices_number; dfs_matrix_row++) { - std::ignore = solution_set.dfs_matrix.get_value(dfs_matrix_row, dfs_matrix_col) - .transform([&dot_product_rhs, &extended_rhs_ = solution_set.extended_rhs, - &dfs_matrix_row](IntS first_value) { - dot_product_rhs += - static_cast(first_value) * extended_rhs_[dfs_matrix_row]; - return first_value; - }); + if (auto const first_value = solution_set.dfs_matrix.get_value(dfs_matrix_row, dfs_matrix_col)) { + dot_product_rhs += static_cast(*first_value) * solution_set.extended_rhs[dfs_matrix_row]; + } } auto& projection_system_row = projection_system[dfs_matrix_col]; projection_system_row[free_indices_number] = dot_product_rhs; @@ -366,11 +356,9 @@ inline ComplexVector compute_internal_loads(SolutionSet const& solution_set, std internal_loads[row] = solution_set.extended_rhs[row]; auto sum_value = DoubleComplex{}; for (auto const column : IdxRange{number_of_columns}) { - std::ignore = - solution_set.dfs_matrix.get_value(row, column).transform([&sum_value, &system, column](IntS value) { - sum_value += static_cast(value) * system[column].back(); - return value; - }); + if (auto const value = solution_set.dfs_matrix.get_value(row, column)) { + sum_value += static_cast(*value) * system[column].back(); + } } internal_loads[row] -= sum_value; } From 8c643e0689da4f455dcdef53dd23cd7766ee4fce Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Fri, 9 Oct 2026 21:13:16 +0200 Subject: [PATCH 04/25] basic unit test Signed-off-by: Jerry Jinfeng Guo --- tests/cpp_unit_tests/CMakeLists.txt | 1 + .../state_output/CMakeLists.txt | 16 ++ .../state_output/test_state_output.cpp | 137 ++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 tests/cpp_unit_tests/state_output/CMakeLists.txt create mode 100644 tests/cpp_unit_tests/state_output/test_state_output.cpp diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 89ad7acc77..40b74691d1 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -9,6 +9,7 @@ add_subdirectory(main_core) add_subdirectory(math_solver) add_subdirectory(optimizer) add_subdirectory(logging) +add_subdirectory(state_output) add_executable( power_grid_model_unit_tests diff --git a/tests/cpp_unit_tests/state_output/CMakeLists.txt b/tests/cpp_unit_tests/state_output/CMakeLists.txt new file mode 100644 index 0000000000..eb5a172235 --- /dev/null +++ b/tests/cpp_unit_tests/state_output/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +add_executable( + power_grid_model_unit_tests_state_output + "../test_entry_point.cpp" + "test_state_output.cpp" +) + +target_link_libraries( + power_grid_model_unit_tests_state_output + PRIVATE power_grid_model doctest::doctest +) + +doctest_discover_tests(power_grid_model_unit_tests_state_output) \ No newline at end of file diff --git a/tests/cpp_unit_tests/state_output/test_state_output.cpp b/tests/cpp_unit_tests/state_output/test_state_output.cpp new file mode 100644 index 0000000000..88d491d083 --- /dev/null +++ b/tests/cpp_unit_tests/state_output/test_state_output.cpp @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace power_grid_model { +namespace { +MainModel make_state_test_model(MathSolverDispatcher const& dispatcher) { + std::vector const node_id{0}; + std::vector const node_u_rated{100.0}; + + std::vector const source_id{1}; + std::vector const source_node{0}; + std::vector const source_status{1}; + std::vector const source_u_ref{1.0}; + std::vector const source_sk{1000.0}; + std::vector const source_rx_ratio{0.0}; + + std::vector const load_id{2}; + std::vector const load_node{0}; + std::vector const load_status{1}; + std::vector const load_type{2}; + std::vector const load_p{0.0}; + std::vector const load_q{500.0}; + + auto const& metadata = meta_data::meta_data_gen::meta_data; + ConstDataset input{false, 1, "input", metadata}; + input.add_buffer("node", 1, 1, nullptr, nullptr); + input.add_attribute_buffer("node", "id", node_id.data()); + input.add_attribute_buffer("node", "u_rated", node_u_rated.data()); + input.add_buffer("source", 1, 1, nullptr, nullptr); + input.add_attribute_buffer("source", "id", source_id.data()); + input.add_attribute_buffer("source", "node", source_node.data()); + input.add_attribute_buffer("source", "status", source_status.data()); + input.add_attribute_buffer("source", "u_ref", source_u_ref.data()); + input.add_attribute_buffer("source", "sk", source_sk.data()); + input.add_attribute_buffer("source", "rx_ratio", source_rx_ratio.data()); + input.add_buffer("sym_load", 1, 1, nullptr, nullptr); + input.add_attribute_buffer("sym_load", "id", load_id.data()); + input.add_attribute_buffer("sym_load", "node", load_node.data()); + input.add_attribute_buffer("sym_load", "status", load_status.data()); + input.add_attribute_buffer("sym_load", "type", load_type.data()); + input.add_attribute_buffer("sym_load", "p_specified", load_p.data()); + input.add_attribute_buffer("sym_load", "q_specified", load_q.data()); + + return MainModel{50.0, input, dispatcher}; +} + +std::optional calculate_state(MainModel& model, MainModelOptions const& options, + ModelStateRequest request) { + auto const& metadata = meta_data::meta_data_gen::meta_data; + ConstDataset const update{false, 1, "update", metadata}; + MutableDataset output{false, 1, "sym_output", metadata}; + std::array const requests{request}; + std::vector> state_outputs(1); + model.calculate_with_state(options, output, update, requests, state_outputs); + return std::move(state_outputs[0]); +} +} // namespace + +TEST_CASE("Test State output - Capture Y-bus and input-node mapping") { + MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; + auto model = make_state_test_model(dispatcher); + auto state_output = calculate_state(model, MainModelOptions{}, {.y_bus = true}); + + REQUIRE(state_output.has_value()); + auto const& state = *state_output; + REQUIRE(state.groups.size() == 1); + CHECK(state.input_node_id == std::vector{0}); + CHECK(state.input_node_group == IdxVector{0}); + CHECK(state.input_node_bus == IdxVector{0}); + + auto const& group = state.groups[0]; + CHECK(group.mapping.n_bus == 1); + CHECK(group.mapping.bus_user_indptr == IdxVector{0, 1}); + CHECK(group.mapping.bus_user_sequence == IdxVector{0}); + CHECK(group.mapping.bus_user_id == std::vector{0}); + REQUIRE(group.y_bus.has_value()); + CHECK(group.y_bus->row_indptr == IdxVector{0, 1}); + CHECK(group.y_bus->col_indices == IdxVector{0}); + REQUIRE(group.y_bus->admittance_real.size() == 1); + CHECK(std::isfinite(group.y_bus->admittance_real[0])); + CHECK(std::isfinite(group.y_bus->admittance_imag[0])); + CHECK_FALSE(group.jacobian.has_value()); +} + +TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { + MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; + auto model = make_state_test_model(dispatcher); + auto state_output = calculate_state(model, MainModelOptions{}, {.jacobian = true}); + + REQUIRE(state_output.has_value()); + REQUIRE(state_output->groups.size() == 1); + auto const& group = state_output->groups[0]; + CHECK_FALSE(group.y_bus.has_value()); + REQUIRE(group.jacobian.has_value()); + CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[1].size()); + CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[2].size()); + CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[3].size()); +} + +TEST_CASE("Test State output - Linear method has no Jacobian") { + MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; + auto model = make_state_test_model(dispatcher); + auto options = MainModelOptions{}; + options.calculation_method = CalculationMethod::linear; + auto state_output = calculate_state(model, options, {.y_bus = true, .jacobian = true}); + + REQUIRE(state_output.has_value()); + REQUIRE(state_output->groups.size() == 1); + CHECK(state_output->groups[0].y_bus.has_value()); + CHECK_FALSE(state_output->groups[0].jacobian.has_value()); +} + +TEST_CASE("Test State output - Unrequested model state is absent") { + MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; + auto model = make_state_test_model(dispatcher); + auto state_output = calculate_state(model, MainModelOptions{}, {}); + + CHECK_FALSE(state_output.has_value()); +} +} // namespace power_grid_model From de52da248653e0e2b563a9508814c4b219e0bd8d Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Fri, 9 Oct 2026 21:20:42 +0200 Subject: [PATCH 05/25] add a python example usage Signed-off-by: Jerry Jinfeng Guo --- docs/examples/Power Flow Example.ipynb | 69 ++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/docs/examples/Power Flow Example.ipynb b/docs/examples/Power Flow Example.ipynb index 6e1f95059a..1f65abac1c 100644 --- a/docs/examples/Power Flow Example.ipynb +++ b/docs/examples/Power Flow Example.ipynb @@ -253,7 +253,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "7ef134e9", "metadata": {}, "outputs": [], @@ -346,6 +346,67 @@ "print(pd.DataFrame(output_data[ComponentType.line]))" ] }, + { + "cell_type": "markdown", + "id": "9a0b78bc", + "metadata": {}, + "source": [ + "### Model state output\n", + "\n", + "`calculate_power_flow` can also return the solver's native model state. With `get_model_state=True`, the return value is `(output_data, model_state)`. The state includes a sparse Y-bus for each solver group and, for Newton-Raphson calculations, the final Jacobian assembled for the solve. Each group's mapping relates matrix bus positions to the input node IDs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca42da43", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input node IDs: [1 2 6]\n", + "Node groups: [0 0 0]\n", + "Node bus positions: [2 1 0]\n", + "Y-bus row pointers: [0 3 6 9]\n", + "Y-bus column indices: [0 1 2 0 1 2 0 1 2]\n", + "Y-bus values: [ 537.80487805-429.89754185j -268.90243902+215.12195122j\n", + " -268.90243902+215.12195122j -268.90243902+215.12195122j\n", + " 537.80487805-429.89754185j -268.90243902+215.12195122j\n", + " -268.90243902+215.12195122j -268.90243902+215.12195122j\n", + " 537.80487805-429.89754185j]\n", + "Jacobian row pointers: [0 3 6 9]\n", + "Jacobian column indices: [0 1 2 0 1 2 0 1 2]\n", + "Jacobian H, N, M, and L blocks:\\n[0. 0. 0. 0. 0. 0. 0. 0. 0.]\\n[0. 0. 0. 0. 0. 0. 0. 0. 0.]\\n[0. 0. 0. 0. 0. 0. 0. 0. 0.]\\n[0. 0. 0. 0. 0. 0. 0. 0. 0.]\n" + ] + } + ], + "source": [ + "output_data, model_state = model.calculate_power_flow(get_model_state=True)\n", + "\n", + "# Map each input node to its solver group and bus position.\n", + "print(\"Input node IDs:\", model_state.input_node_id)\n", + "print(\"Node groups:\", model_state.input_node_group)\n", + "print(\"Node bus positions:\", model_state.input_node_bus)\n", + "\n", + "# Matrices are stored in CSR form, with one state entry per solver group.\n", + "group_state = model_state.groups[0]\n", + "y_bus = group_state.y_bus\n", + "print(\"Y-bus row pointers:\", y_bus.row_indptr)\n", + "print(\"Y-bus column indices:\", y_bus.col_indices)\n", + "print(\"Y-bus values:\", y_bus.admittance_real + 1j * y_bus.admittance_imag)\n", + "\n", + "if group_state.jacobian is not None:\n", + " jacobian = group_state.jacobian\n", + " print(\"Jacobian row pointers:\", jacobian.row_indptr_lu)\n", + " print(\"Jacobian column indices:\", jacobian.col_indices_lu)\n", + " print(\"Jacobian H block:\", jacobian.h)\n", + " print(\"Jacobian N block:\", jacobian.n)\n", + " print(\"Jacobian M block:\", jacobian.m)\n", + " print(\"Jacobian L block:\", jacobian.l_block)" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -1340,7 +1401,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "1a221507", "metadata": {}, "outputs": [ @@ -1537,7 +1598,7 @@ ], "metadata": { "kernelspec": { - "display_name": "power-grid-model", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -1551,7 +1612,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.14.3" + "version": "3.14.8" } }, "nbformat": 4, From f8c70a29dbe90840332f92bf8055944963258f96 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sat, 10 Oct 2026 12:15:52 +0200 Subject: [PATCH 06/25] jacobian void capture fixed; add full histroy Signed-off-by: Jerry Jinfeng Guo --- docs/examples/Power Flow Example.ipynb | 45 +++++++++----- .../include/power_grid_model/calculation.hpp | 11 ++-- .../power_grid_model/calculation_state.hpp | 2 + .../power_grid_model/main_model_impl.hpp | 18 +++--- .../math_solver/iterative_pf_solver.hpp | 8 ++- .../math_solver/math_solver.hpp | 19 ++++-- .../math_solver/math_solver_dispatch.hpp | 3 +- .../math_solver/newton_raphson_pf_solver.hpp | 48 +++++++++++--- .../include/power_grid_model_c/model.h | 28 +++++++++ .../power_grid_model_c/src/model.cpp | 36 +++++++++++ .../include/power_grid_model_cpp/model.hpp | 10 +++ src/power_grid_model/_core/model_state.py | 40 +++++++++--- src/power_grid_model/_core/power_grid_core.py | 44 ++++++++++++- .../state_output/test_state_output.cpp | 62 ++++++++++++++++++- tests/native_api_tests/test_api_model.cpp | 7 +++ tests/unit/test_power_grid_model.py | 8 +++ 16 files changed, 338 insertions(+), 51 deletions(-) diff --git a/docs/examples/Power Flow Example.ipynb b/docs/examples/Power Flow Example.ipynb index 1f65abac1c..a57d7e0fee 100644 --- a/docs/examples/Power Flow Example.ipynb +++ b/docs/examples/Power Flow Example.ipynb @@ -141,7 +141,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 9, "id": "1034f6f3", "metadata": {}, "outputs": [], @@ -179,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "id": "f0c8c3e8", "metadata": {}, "outputs": [ @@ -220,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "id": "40509eaf", "metadata": {}, "outputs": [], @@ -290,7 +290,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 13, "id": "44c2de63", "metadata": {}, "outputs": [], @@ -313,7 +313,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 14, "id": "a581a36e", "metadata": {}, "outputs": [ @@ -353,12 +353,12 @@ "source": [ "### Model state output\n", "\n", - "`calculate_power_flow` can also return the solver's native model state. With `get_model_state=True`, the return value is `(output_data, model_state)`. The state includes a sparse Y-bus for each solver group and, for Newton-Raphson calculations, the final Jacobian assembled for the solve. Each group's mapping relates matrix bus positions to the input node IDs." + "`calculate_power_flow` can also return the solver's native model state. With `get_model_state=True`, the return value is `(output_data, model_state)`. The state includes a sparse Y-bus for each solver group and, for Newton-Raphson calculations, an ordered `jacobian_history` with one assembled Jacobian per iteration. The existing `jacobian` property remains the final entry in that history. Each group's mapping relates matrix bus positions to the input node IDs." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "ca42da43", "metadata": {}, "outputs": [ @@ -376,9 +376,20 @@ " 537.80487805-429.89754185j -268.90243902+215.12195122j\n", " -268.90243902+215.12195122j -268.90243902+215.12195122j\n", " 537.80487805-429.89754185j]\n", - "Jacobian row pointers: [0 3 6 9]\n", - "Jacobian column indices: [0 1 2 0 1 2 0 1 2]\n", - "Jacobian H, N, M, and L blocks:\\n[0. 0. 0. 0. 0. 0. 0. 0. 0.]\\n[0. 0. 0. 0. 0. 0. 0. 0. 0.]\\n[0. 0. 0. 0. 0. 0. 0. 0. 0.]\\n[0. 0. 0. 0. 0. 0. 0. 0. 0.]\n" + "Captured Jacobian iterations: [1, 2, 3]\n", + "Final Jacobian row pointers: [0 3 6 9]\n", + "Final Jacobian column indices: [0 1 2 0 1 2 0 1 2]\n", + "Final Jacobian H block: [ 399.92590067 -198.07684237 -201.84905831 -196.03923548\n", + " 394.72117311 -198.68193762 -211.59471597 -210.44188084\n", + " 10359.27247084]\n", + "Final Jacobian N block: [ 487.80812795 -245.5075059 -262.30062199 -247.13759141 467.54395568\n", + " -260.40636383 -254.50409586 -250.99840925 1529.74147013]\n", + "Final Jacobian M block: [ -507.8081279 245.5075059 262.30062199 247.13759141\n", + " -507.54395524 260.40636383 254.50409586 250.99840925\n", + " -1529.74147022]\n", + "Final Jacobian L block: [ 395.92590076 -198.07684237 -201.84905831 -196.03923548\n", + " 384.72117318 -198.68193762 -211.59471597 -210.44188084\n", + " 10359.27247102]\n" ] } ], @@ -397,14 +408,16 @@ "print(\"Y-bus column indices:\", y_bus.col_indices)\n", "print(\"Y-bus values:\", y_bus.admittance_real + 1j * y_bus.admittance_imag)\n", "\n", + "jacobian_history = group_state.jacobian_history\n", + "print(\"Captured Jacobian iterations:\", [jac.iteration for jac in jacobian_history])\n", "if group_state.jacobian is not None:\n", " jacobian = group_state.jacobian\n", - " print(\"Jacobian row pointers:\", jacobian.row_indptr_lu)\n", - " print(\"Jacobian column indices:\", jacobian.col_indices_lu)\n", - " print(\"Jacobian H block:\", jacobian.h)\n", - " print(\"Jacobian N block:\", jacobian.n)\n", - " print(\"Jacobian M block:\", jacobian.m)\n", - " print(\"Jacobian L block:\", jacobian.l_block)" + " print(\"Final Jacobian row pointers:\", jacobian.row_indptr_lu)\n", + " print(\"Final Jacobian column indices:\", jacobian.col_indices_lu)\n", + " print(\"Final Jacobian H block:\", jacobian.h)\n", + " print(\"Final Jacobian N block:\", jacobian.n)\n", + " print(\"Final Jacobian M block:\", jacobian.m)\n", + " print(\"Final Jacobian L block:\", jacobian.l_block)" ] }, { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp index f6bdbb6f8e..a282057788 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp @@ -92,8 +92,10 @@ template struct Calculator { static auto solver(CalculationMethod calculation_method, MainModelOptions const& options, bool cache_run, Logger& logger) { return [calculation_method, err_tol = options.err_tol, max_iter = options.max_iter, &logger, - cache_run](MathSolverProxy& solver, YBus const& y_bus, PowerFlowInput const& input) { - return solver.get().run_power_flow(input, err_tol, max_iter, cache_run, logger, calculation_method, y_bus); + cache_run](MathSolverProxy& solver, YBus const& y_bus, PowerFlowInput const& input, + bool capture_jacobian_state) { + return solver.get().run_power_flow(input, err_tol, max_iter, cache_run, logger, calculation_method, y_bus, + capture_jacobian_state); }; } }; @@ -108,7 +110,8 @@ template struct Calculator { static auto solver(CalculationMethod calculation_method, MainModelOptions const& options, bool /*cache_run*/, Logger& logger) { return [calculation_method, err_tol = options.err_tol, max_iter = options.max_iter, - &logger](MathSolverProxy& solver, YBus const& y_bus, StateEstimationInput const& input) { + &logger](MathSolverProxy& solver, YBus const& y_bus, + StateEstimationInput const& input, bool /*capture_jacobian_state*/) { return solver.get().run_state_estimation(input, err_tol, max_iter, logger, calculation_method, y_bus); }; } @@ -124,7 +127,7 @@ template struct Calculator { static auto solver(CalculationMethod calculation_method, MainModelOptions const& /*options*/, bool /*cache_run*/, Logger& logger) { return [calculation_method, &logger](MathSolverProxy& solver, YBus const& y_bus, - ShortCircuitInput const& input) { + ShortCircuitInput const& input, bool /*capture_jacobian_state*/) { return solver.get().run_short_circuit(input, logger, calculation_method, y_bus); }; } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp index 37030ee170..92c2f38d08 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp @@ -44,12 +44,14 @@ struct ModelStateJacobian { IdxVector row_indptr_lu; IdxVector col_indices_lu; std::array, 4> blocks; // H, N, M, L; flattened row-major per sparse entry. + Idx iteration{}; }; struct ModelStateGroup { ModelStateGroupMapping mapping; std::optional y_bus; std::optional jacobian; + std::vector jacobian_history; }; struct ModelStateOutput { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp index d3a1134539..5a2b49f6db 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp @@ -288,14 +288,16 @@ class MainModelImpl { std::ranges::range> && std::invocable< std::remove_cvref_t, MathSolverType&, YBus const&, - typename std::invoke_result_t::const_reference> && + typename std::invoke_result_t::const_reference, + bool> && solver_output_type::const_reference>> + typename std::invoke_result_t::const_reference, bool>> auto calculate_(PrepareInputFn&& prepare_input, SolveFn&& solve, Logger& logger, ModelStateRequest const& state_request) { - using InputType = std::invoke_result_t::const_reference; - using SolverOutputType = std::invoke_result_t; + using InputType = typename std::invoke_result_t::const_reference; + using SolverOutputType = + std::invoke_result_t; using sym = decode_symmetry_v; assert(construction_complete_); @@ -310,14 +312,13 @@ class MainModelImpl { }(); // calculate return [this, &logger, &input, &state_request, solve_ = std::forward(solve)] { - (void)state_request; Timer const timer{logger, LogEvent::math_calculation}; auto& solvers = main_core::get_solvers(solver_preparation_context_.math_state); auto& y_bus_vec = main_core::get_y_bus(solver_preparation_context_.math_state); std::vector solver_output; solver_output.reserve(get_n_math_solvers(state_)); for (Idx i = 0; i != get_n_math_solvers(state_); ++i) { - SolverOutputType result = solve_(solvers[i], y_bus_vec[i], input[i]); + SolverOutputType result = solve_(solvers[i], y_bus_vec[i], input[i], state_request.jacobian); if constexpr (steady_state_solver_output_type) { if (state_request.y_bus || state_request.jacobian) { auto group_state = std::make_shared(); @@ -326,7 +327,10 @@ class MainModelImpl { group_state->y_bus = create_y_bus_state(y_bus_vec[i]); } if (state_request.jacobian) { - group_state->jacobian = solvers[i].get().get_last_jacobian_state(y_bus_vec[i]); + group_state->jacobian_history = solvers[i].get().get_jacobian_history(y_bus_vec[i]); + if (!group_state->jacobian_history.empty()) { + group_state->jacobian = group_state->jacobian_history.back(); + } } result.model_state_group = std::move(group_state); } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_pf_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_pf_solver.hpp index 81fff0fd51..e4801400a4 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_pf_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_pf_solver.hpp @@ -30,9 +30,13 @@ namespace power_grid_model::math_solver { template class IterativePFSolver { public: friend DerivedSolver; + void log_matrix(Logger& /*log*/, YBus const& /*y_bus*/, Idx /*iter*/) {} + void copy_iteration_state_from(DerivedSolver const& /*solver*/) {} + SolverOutput run_power_flow(YBus const& y_bus, PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, Logger& log) { - auto& derived_solver = static_cast(*this); + // get derived solver class + auto derived_solver = static_cast(*this); // prepare SolverOutput output; @@ -60,6 +64,7 @@ template class IterativePFSolver { Timer const sub_timer{log, LogEvent::prepare_matrices}; derived_solver.prepare_matrix_and_rhs(y_bus, input, output.u); } + derived_solver.log_matrix(log, y_bus, num_iter); { // Solve the linear equations Timer const sub_timer{log, LogEvent::solve_sparse_linear_equation}; @@ -80,6 +85,7 @@ template class IterativePFSolver { } calculate_result(y_bus, input, output); } + static_cast(*this).copy_iteration_state_from(derived_solver); // Manually stop timers to avoid "Max number of iterations" to be included in the timing. main_timer.stop(); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp index 61a43a59bc..1a8ca34c6c 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp @@ -41,7 +41,8 @@ template class MathSolver : public MathSolverBase { } SolverOutput run_power_flow(PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, - Logger& log, CalculationMethod calculation_method, YBus const& y_bus) final { + Logger& log, CalculationMethod calculation_method, YBus const& y_bus, + bool capture_jacobian_state) final { using enum CalculationMethod; last_power_flow_used_newton_raphson_ = false; @@ -53,7 +54,8 @@ template class MathSolver : public MathSolverBase { [[fallthrough]]; // use Newton-Raphson by default case newton_raphson: last_power_flow_used_newton_raphson_ = true; - return run_power_flow_newton_raphson(input, err_tol, max_iter, cache_run, log, y_bus); + return run_power_flow_newton_raphson(input, err_tol, max_iter, cache_run, log, y_bus, + capture_jacobian_state); case linear: return run_power_flow_linear(input, err_tol, max_iter, log, y_bus); case linear_current: @@ -120,6 +122,13 @@ template class MathSolver : public MathSolverBase { return newton_raphson_pf_solver_->get_last_jacobian_state(y_bus); } + std::vector get_jacobian_history(YBus const& y_bus) const final { + if (!last_power_flow_used_newton_raphson_ || !newton_raphson_pf_solver_) { + return {}; + } + return newton_raphson_pf_solver_->get_jacobian_history(y_bus); + } + private: std::shared_ptr topo_ptr_; bool all_const_y_; // if all the load_gen is const element_admittance (impedance) type @@ -132,12 +141,14 @@ template class MathSolver : public MathSolverBase { bool last_power_flow_used_newton_raphson_{false}; SolverOutput run_power_flow_newton_raphson(PowerFlowInput const& input, double err_tol, Idx max_iter, - bool cache_run, Logger& log, YBus const& y_bus) { + bool cache_run, Logger& log, YBus const& y_bus, + bool capture_jacobian_state) { if (!newton_raphson_pf_solver_.has_value()) { Timer const timer{log, LogEvent::create_math_solver}; newton_raphson_pf_solver_.emplace(y_bus, *topo_ptr_); } - return newton_raphson_pf_solver_.value().run_power_flow(y_bus, input, err_tol, max_iter, cache_run, log); + newton_raphson_pf_solver_->set_jacobian_state_capture(capture_jacobian_state); + return newton_raphson_pf_solver_->run_power_flow(y_bus, input, err_tol, max_iter, cache_run, log); } SolverOutput run_power_flow_linear(PowerFlowInput const& input, double /* err_tol */, Idx /* max_iter */, diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp index b0b3f15f39..38e3b221ca 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp @@ -31,13 +31,14 @@ template class MathSolverBase { virtual SolverOutput run_power_flow(PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, Logger& log, CalculationMethod calculation_method, - YBus const& y_bus) = 0; + YBus const& y_bus, bool capture_jacobian_state) = 0; virtual SolverOutput run_state_estimation(StateEstimationInput const& input, double err_tol, Idx max_iter, Logger& log, CalculationMethod calculation_method, YBus const& y_bus) = 0; virtual std::optional get_last_jacobian_state(YBus const& /* y_bus */) const { return std::nullopt; } + virtual std::vector get_jacobian_history(YBus const& /* y_bus */) const { return {}; } virtual ShortCircuitSolverOutput run_short_circuit(ShortCircuitInput const& input, Logger& log, CalculationMethod calculation_method, YBus const& y_bus) = 0; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp index 52b5e83d9c..10fb15b4d0 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp @@ -394,13 +394,46 @@ class NewtonRaphsonPFSolver : public IterativePFSolver const& y_bus) const { - ModelStateJacobian result{ - .row_indptr_lu = y_bus.row_indptr_lu(), - .col_indices_lu = y_bus.col_indices_lu(), - }; + void set_jacobian_state_capture(bool capture) { + capture_jacobian_state_ = capture; + jacobian_history_.clear(); + } + + void copy_iteration_state_from(NewtonRaphsonPFSolver const& solver) { + jacobian_history_ = solver.jacobian_history_; + } + + std::optional get_last_jacobian_state(YBus const& y_bus) const { + if (jacobian_history_.empty()) { + return std::nullopt; + } + auto result = jacobian_history_.back(); + result.row_indptr_lu = y_bus.row_indptr_lu(); + result.col_indices_lu = y_bus.col_indices_lu(); + return result; + } + + std::vector get_jacobian_history(YBus const& y_bus) const { + auto result = jacobian_history_; + for (auto& jacobian : result) { + jacobian.row_indptr_lu = y_bus.row_indptr_lu(); + jacobian.col_indices_lu = y_bus.col_indices_lu(); + } + return result; + } + + void log_matrix(Logger& /*log*/, YBus const& /*y_bus*/, Idx iter) { + if (capture_jacobian_state_) { + capture_jacobian_state(iter); + } + } + + void capture_jacobian_state(Idx iteration) { + ModelStateJacobian result; + result.iteration = iteration; constexpr Idx block_size = is_symmetric_v ? 1 : 9; for (auto& block : result.blocks) { + block.clear(); block.reserve(static_cast(std::ssize(data_jac_) * block_size)); } @@ -417,12 +450,13 @@ class NewtonRaphsonPFSolver : public IterativePFSolver> data_jac_; + bool capture_jacobian_state_{}; + std::vector jacobian_history_; // calculation data std::vector> x_; // unknown // this stores in different steps diff --git a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h index 152faf7ebe..98a69aabec 100644 --- a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h +++ b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h @@ -176,6 +176,25 @@ typedef struct PGM_StateGroupView { PGM_Idx n_jacobian_values; } PGM_StateGroupView; +/** + * @brief Borrowed view of one Jacobian assembled during a Newton-Raphson run. + * + * Iterations are numbered from one. All pointers are valid until the owning + * PGM_StateOutput is destroyed. + */ +typedef struct PGM_StateJacobianView { + PGM_Idx iteration; + PGM_Idx n_bus; + PGM_Idx jacobian_nnz; + PGM_Idx const* jacobian_row_indptr; + PGM_Idx const* jacobian_col_indices; + double const* jacobian_h; + double const* jacobian_n; + double const* jacobian_m; + double const* jacobian_l; + PGM_Idx n_jacobian_values; +} PGM_StateJacobianView; + /** * @brief Calculate normally and return an owned native state result. * @@ -201,6 +220,15 @@ PGM_API void PGM_state_output_get_scenario(PGM_Handle* handle, PGM_StateOutput c PGM_API void PGM_state_output_get_group(PGM_Handle* handle, PGM_StateOutput const* state_output, PGM_Idx scenario_idx, PGM_Idx group_idx, PGM_StateGroupView* view); +/** @brief Get the number of captured Newton-Raphson Jacobians for one group. */ +PGM_API PGM_Idx PGM_state_output_jacobian_history_count(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx); + +/** @brief Get a borrowed view of one captured Newton-Raphson Jacobian. */ +PGM_API void PGM_state_output_get_jacobian_history(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx, PGM_Idx iteration_idx, + PGM_StateJacobianView* view); + /** @brief Destroy an owned state result and invalidate all its borrowed views. */ PGM_API void PGM_destroy_state_output(PGM_StateOutput* state_output); diff --git a/power_grid_model_c/power_grid_model_c/src/model.cpp b/power_grid_model_c/power_grid_model_c/src/model.cpp index 0abdc4a235..e368b5b279 100644 --- a/power_grid_model_c/power_grid_model_c/src/model.cpp +++ b/power_grid_model_c/power_grid_model_c/src/model.cpp @@ -536,6 +536,42 @@ void PGM_state_output_get_group(PGM_Handle* handle, PGM_StateOutput const* state }); } +PGM_Idx PGM_state_output_jacobian_history_count(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx) { + return call_with_catch(handle, [state_output, scenario_idx, group_idx] { + auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); + if (!scenario.has_value()) { + throw DatasetError{"No model state was requested for this scenario.\n"}; + } + return static_cast(scenario->groups.at(static_cast(group_idx)).jacobian_history.size()); + }); +} + +void PGM_state_output_get_jacobian_history(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx, PGM_Idx iteration_idx, + PGM_StateJacobianView* view) { + call_with_catch(handle, [state_output, scenario_idx, group_idx, iteration_idx, view] { + auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); + if (!scenario.has_value()) { + throw DatasetError{"No model state was requested for this scenario.\n"}; + } + auto const& group = scenario->groups.at(static_cast(group_idx)); + auto const& jacobian = group.jacobian_history.at(static_cast(iteration_idx)); + auto& output = safe_ptr_get(view); + output = {}; + output.iteration = jacobian.iteration; + output.n_bus = group.mapping.n_bus; + output.jacobian_nnz = static_cast(jacobian.col_indices_lu.size()); + output.jacobian_row_indptr = jacobian.row_indptr_lu.data(); + output.jacobian_col_indices = jacobian.col_indices_lu.data(); + output.jacobian_h = jacobian.blocks[0].data(); + output.jacobian_n = jacobian.blocks[1].data(); + output.jacobian_m = jacobian.blocks[2].data(); + output.jacobian_l = jacobian.blocks[3].data(); + output.n_jacobian_values = static_cast(jacobian.blocks[0].size()); + }); +} + void PGM_destroy_state_output(PGM_StateOutput* state_output) { delete state_output; } diff --git a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp index a366ad26c7..e0d43c4275 100644 --- a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp +++ b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp @@ -42,6 +42,16 @@ class StateOutput { handle_.call_with(PGM_state_output_get_group, output_.get(), scenario_idx, group_idx, &view); return view; } + Idx jacobian_history_count(Idx scenario_idx, Idx group_idx) const { + return handle_.call_with(PGM_state_output_jacobian_history_count, output_.get(), scenario_idx, + group_idx); + } + PGM_StateJacobianView jacobian_at_iteration(Idx scenario_idx, Idx group_idx, Idx iteration_idx) const { + PGM_StateJacobianView view{}; + handle_.call_with(PGM_state_output_get_jacobian_history, output_.get(), scenario_idx, group_idx, + iteration_idx, &view); + return view; + } private: friend class Model; diff --git a/src/power_grid_model/_core/model_state.py b/src/power_grid_model/_core/model_state.py index 1758cd7d93..ab617ce114 100644 --- a/src/power_grid_model/_core/model_state.py +++ b/src/power_grid_model/_core/model_state.py @@ -15,6 +15,7 @@ from power_grid_model._core.index_integer import IdNp, IdxNp from power_grid_model._core.power_grid_core import ( StateGroupViewC, + StateJacobianViewC, StateOutputPtr, StateScenarioViewC, get_power_grid_core, @@ -77,6 +78,19 @@ class ModelStateJacobian: n: NDArray[np.float64] m: NDArray[np.float64] l_block: NDArray[np.float64] + iteration: int = 0 + + +def _build_jacobian(view: StateGroupViewC | StateJacobianViewC, owner: _NativeStateOutput) -> ModelStateJacobian: + return ModelStateJacobian( + row_indptr_lu=_array(view.jacobian_row_indptr, view.n_bus + 1, IdxNp, owner), + col_indices_lu=_array(view.jacobian_col_indices, view.jacobian_nnz, IdxNp, owner), + h=_array(view.jacobian_h, view.n_jacobian_values, np.float64, owner), + n=_array(view.jacobian_n, view.n_jacobian_values, np.float64, owner), + m=_array(view.jacobian_m, view.n_jacobian_values, np.float64, owner), + l_block=_array(view.jacobian_l, view.n_jacobian_values, np.float64, owner), + iteration=getattr(view, "iteration", 0), + ) @dataclass(frozen=True) @@ -96,6 +110,7 @@ class ModelStateGroup: mapping: ModelStateGroupMapping y_bus: ModelStateYBus | None jacobian: ModelStateJacobian | None + jacobian_history: tuple[ModelStateJacobian, ...] = () @dataclass(frozen=True) @@ -140,17 +155,26 @@ def _build_scenario(owner: _NativeStateOutput, scenario_idx: int) -> ModelState admittance_real=_array(view.admittance_real, view.n_admittance_values, np.float64, owner), admittance_imag=_array(view.admittance_imag, view.n_admittance_values, np.float64, owner), ) + jacobian_history_items = [] + history_count = pgc.state_output_jacobian_history_count(owner.pointer, scenario_idx, group_idx) + for iteration_idx in range(history_count): + jacobian_view = StateJacobianViewC() + pgc.state_output_get_jacobian_history( + owner.pointer, scenario_idx, group_idx, iteration_idx, pointer(jacobian_view) + ) + jacobian_history_items.append(_build_jacobian(jacobian_view, owner)) + jacobian_history = tuple(jacobian_history_items) jacobian = None if view.has_jacobian: - jacobian = ModelStateJacobian( - row_indptr_lu=_array(view.jacobian_row_indptr, view.n_bus + 1, IdxNp, owner), - col_indices_lu=_array(view.jacobian_col_indices, view.jacobian_nnz, IdxNp, owner), - h=_array(view.jacobian_h, view.n_jacobian_values, np.float64, owner), - n=_array(view.jacobian_n, view.n_jacobian_values, np.float64, owner), - m=_array(view.jacobian_m, view.n_jacobian_values, np.float64, owner), - l_block=_array(view.jacobian_l, view.n_jacobian_values, np.float64, owner), + jacobian = jacobian_history[-1] if jacobian_history else _build_jacobian(view, owner) + groups.append( + ModelStateGroup( + mapping=mapping, + y_bus=y_bus, + jacobian=jacobian, + jacobian_history=jacobian_history, ) - groups.append(ModelStateGroup(mapping=mapping, y_bus=y_bus, jacobian=jacobian)) + ) return ModelState( y_bus_requested=bool(scenario_view.y_bus_requested), diff --git a/src/power_grid_model/_core/power_grid_core.py b/src/power_grid_model/_core/power_grid_core.py index c5b5ecd889..2b1e92fe23 100644 --- a/src/power_grid_model/_core/power_grid_core.py +++ b/src/power_grid_model/_core/power_grid_core.py @@ -188,9 +188,25 @@ class StateGroupViewC(Structure): ] +class StateJacobianViewC(Structure): + _fields_ = [ + ("iteration", IdxC), + ("n_bus", IdxC), + ("jacobian_nnz", IdxC), + ("jacobian_row_indptr", IdxPtr), + ("jacobian_col_indices", IdxPtr), + ("jacobian_h", DoublePtr), + ("jacobian_n", DoublePtr), + ("jacobian_m", DoublePtr), + ("jacobian_l", DoublePtr), + ("n_jacobian_values", IdxC), + ] + + StateOutputRequestPtr = POINTER(StateOutputRequestC) StateScenarioViewPtr = POINTER(StateScenarioViewC) StateGroupViewPtr = POINTER(StateGroupViewC) +StateJacobianViewPtr = POINTER(StateJacobianViewC) StateOutputPtrPtr = POINTER(StateOutputPtr) @@ -500,13 +516,37 @@ def state_output_scenario_count(self, state_output: StateOutputPtr) -> int: # t @make_c_binding def state_output_get_scenario( - self, state_output: StateOutputPtr, scenario_idx: int, view: StateScenarioViewPtr # type: ignore[valid-type] + self, + state_output: StateOutputPtr, + scenario_idx: int, + view: StateScenarioViewPtr, # type: ignore[valid-type] ) -> None: # type: ignore[empty-body] pass # pragma: no cover @make_c_binding def state_output_get_group( - self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int, view: StateGroupViewPtr # type: ignore[valid-type] + self, + state_output: StateOutputPtr, + scenario_idx: int, + group_idx: int, + view: StateGroupViewPtr, # type: ignore[valid-type] + ) -> None: # type: ignore[empty-body] + pass # pragma: no cover + + @make_c_binding + def state_output_jacobian_history_count( + self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int + ) -> int: # type: ignore[empty-body] + pass # pragma: no cover + + @make_c_binding + def state_output_get_jacobian_history( + self, + state_output: StateOutputPtr, + scenario_idx: int, + group_idx: int, + iteration_idx: int, + view: StateJacobianViewPtr, # type: ignore[valid-type] ) -> None: # type: ignore[empty-body] pass # pragma: no cover diff --git a/tests/cpp_unit_tests/state_output/test_state_output.cpp b/tests/cpp_unit_tests/state_output/test_state_output.cpp index 88d491d083..2120f3df6e 100644 --- a/tests/cpp_unit_tests/state_output/test_state_output.cpp +++ b/tests/cpp_unit_tests/state_output/test_state_output.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,8 @@ #include #include #include +#include +#include #include namespace power_grid_model { @@ -97,12 +100,17 @@ TEST_CASE("Test State output - Capture Y-bus and input-node mapping") { CHECK(std::isfinite(group.y_bus->admittance_real[0])); CHECK(std::isfinite(group.y_bus->admittance_imag[0])); CHECK_FALSE(group.jacobian.has_value()); + CHECK(group.jacobian_history.empty()); } TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; auto model = make_state_test_model(dispatcher); - auto state_output = calculate_state(model, MainModelOptions{}, {.jacobian = true}); + MultiThreadedTextLogger logger; + model.set_logger(logger); + auto options = MainModelOptions{}; + options.calculation_method = CalculationMethod::newton_raphson; + auto state_output = calculate_state(model, options, {.jacobian = true}); REQUIRE(state_output.has_value()); REQUIRE(state_output->groups.size() == 1); @@ -112,6 +120,57 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[1].size()); CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[2].size()); CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[3].size()); + CHECK(std::ranges::any_of(group.jacobian->blocks[0], [](double value) { return value != 0.0; })); + CHECK(std::ranges::all_of(group.jacobian->blocks[0], [](double value) { return std::isfinite(value); })); + + auto const report = logger.report(); + constexpr std::string_view jacobian_tag{"Tag:4001: "}; + std::vector jacobian_logs; + auto record_start = report.find(jacobian_tag); + while (record_start != std::string::npos) { + auto const record_end = report.find("\n[", record_start + jacobian_tag.size()); + jacobian_logs.push_back(std::string_view{report}.substr( + record_start + jacobian_tag.size(), record_end == std::string::npos ? std::string::npos + : record_end - record_start - jacobian_tag.size())); + record_start = report.find(jacobian_tag, record_start + jacobian_tag.size()); + } + REQUIRE_FALSE(jacobian_logs.empty()); + CHECK(jacobian_logs.size() > 1); + REQUIRE(group.jacobian_history.size() == jacobian_logs.size()); + auto parse_values = [](std::string_view jacobian_log, std::string_view field) { + auto const field_start = jacobian_log.find(std::string{field} + "="); + REQUIRE(field_start != std::string_view::npos); + auto const value_start = field_start + field.size() + 1; + auto const line_end = jacobian_log.find('\n', value_start); + std::istringstream stream{std::string{jacobian_log.substr(value_start, line_end - value_start)}}; + std::vector values; + double value{}; + while (stream >> value) { + values.push_back(value); + } + return values; + }; + auto check_logged_values = [&parse_values](std::string_view jacobian_log, std::string_view field, + std::vector const& expected) { + auto const logged = parse_values(jacobian_log, field); + REQUIRE(logged.size() == expected.size()); + for (size_t i = 0; i < expected.size(); ++i) { + CHECK(logged[i] == doctest::Approx(expected[i]).epsilon(1e-5)); + } + }; + for (size_t iteration_idx = 0; iteration_idx < jacobian_logs.size(); ++iteration_idx) { + auto const& jacobian = group.jacobian_history[iteration_idx]; + auto const jacobian_log = jacobian_logs[iteration_idx]; + CHECK(jacobian.iteration == static_cast(iteration_idx + 1)); + check_logged_values(jacobian_log, "row_indptr_lu", + std::vector(jacobian.row_indptr_lu.begin(), jacobian.row_indptr_lu.end())); + check_logged_values(jacobian_log, "col_indices_lu", + std::vector(jacobian.col_indices_lu.begin(), jacobian.col_indices_lu.end())); + check_logged_values(jacobian_log, "jac_h", jacobian.blocks[0]); + check_logged_values(jacobian_log, "jac_n", jacobian.blocks[1]); + check_logged_values(jacobian_log, "jac_m", jacobian.blocks[2]); + check_logged_values(jacobian_log, "jac_l", jacobian.blocks[3]); + } } TEST_CASE("Test State output - Linear method has no Jacobian") { @@ -125,6 +184,7 @@ TEST_CASE("Test State output - Linear method has no Jacobian") { REQUIRE(state_output->groups.size() == 1); CHECK(state_output->groups[0].y_bus.has_value()); CHECK_FALSE(state_output->groups[0].jacobian.has_value()); + CHECK(state_output->groups[0].jacobian_history.empty()); } TEST_CASE("Test State output - Unrequested model state is absent") { diff --git a/tests/native_api_tests/test_api_model.cpp b/tests/native_api_tests/test_api_model.cpp index 064e4dc955..615a9a9bd5 100644 --- a/tests/native_api_tests/test_api_model.cpp +++ b/tests/native_api_tests/test_api_model.cpp @@ -293,6 +293,13 @@ TEST_CASE("API Model") { CHECK(group.has_jacobian == 1); CHECK(group.y_bus_nnz > 0); CHECK(group.jacobian_nnz > 0); + auto const history_count = state_output.jacobian_history_count(0, 0); + REQUIRE(history_count > 0); + auto const first_jacobian = state_output.jacobian_at_iteration(0, 0, 0); + auto const last_jacobian = state_output.jacobian_at_iteration(0, 0, history_count - 1); + CHECK(first_jacobian.iteration == 1); + CHECK(last_jacobian.iteration == history_count); + CHECK(last_jacobian.jacobian_nnz == group.jacobian_nnz); } } diff --git a/tests/unit/test_power_grid_model.py b/tests/unit/test_power_grid_model.py index 3a536258dc..dc50ac614d 100644 --- a/tests/unit/test_power_grid_model.py +++ b/tests/unit/test_power_grid_model.py @@ -181,6 +181,11 @@ def test_power_flow_model_state(model: PowerGridModel): group = model_state.groups[0] assert group.y_bus is not None assert group.jacobian is not None + assert group.jacobian_history + assert [jacobian.iteration for jacobian in group.jacobian_history] == list( + range(1, len(group.jacobian_history) + 1) + ) + assert group.jacobian_history[-1] is group.jacobian assert group.mapping.bus_user_id.tolist() == [0] assert group.y_bus.row_indptr.shape == (group.mapping.n_bus + 1,) assert group.y_bus.admittance_real.size > 0 @@ -200,6 +205,7 @@ def test_linear_power_flow_model_state_has_no_jacobian(model: PowerGridModel): assert model_state is not None assert model_state.groups[0].y_bus is not None assert model_state.groups[0].jacobian is None + assert model_state.groups[0].jacobian_history == () def test_power_flow_model_state_batch_requests(model: PowerGridModel, update_batch): @@ -217,10 +223,12 @@ def test_power_flow_model_state_batch_requests(model: PowerGridModel, update_bat assert states[0].y_bus_requested assert states[0].groups[0].y_bus is not None assert states[0].groups[0].jacobian is None + assert states[0].groups[0].jacobian_history == () assert states[1] is not None assert states[1].jacobian_requested assert states[1].groups[0].y_bus is None assert states[1].groups[0].jacobian is not None + assert states[1].groups[0].jacobian_history def test_power_flow_model_state_request_count_must_match_batch(model: PowerGridModel, update_batch): From 4d059c6b253fdc8f7f9a832c824488f10694d480 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sat, 10 Oct 2026 15:04:58 +0200 Subject: [PATCH 07/25] restructured model state Signed-off-by: Jerry Jinfeng Guo --- docs/examples/Power Flow Example.ipynb | 52 +++++++++--- .../power_grid_model/calculation_state.hpp | 20 ++++- .../power_grid_model/main_model_impl.hpp | 10 ++- .../math_solver/math_solver.hpp | 10 +-- .../math_solver/math_solver_dispatch.hpp | 6 +- .../math_solver/newton_raphson_pf_solver.hpp | 48 ++++++----- .../include/power_grid_model_c/model.h | 52 ++++++++---- .../power_grid_model_c/src/model.cpp | 67 +++++++++------ .../include/power_grid_model_cpp/model.hpp | 19 +++-- src/power_grid_model/__init__.py | 4 + src/power_grid_model/_core/model_state.py | 81 ++++++++++++++----- src/power_grid_model/_core/power_grid_core.py | 46 +++++++---- .../state_output/test_state_output.cpp | 48 +++++++---- tests/native_api_tests/test_api_model.cpp | 21 +++-- tests/unit/test_power_grid_model.py | 31 ++++--- 15 files changed, 350 insertions(+), 165 deletions(-) diff --git a/docs/examples/Power Flow Example.ipynb b/docs/examples/Power Flow Example.ipynb index a57d7e0fee..046e88e4f8 100644 --- a/docs/examples/Power Flow Example.ipynb +++ b/docs/examples/Power Flow Example.ipynb @@ -353,12 +353,12 @@ "source": [ "### Model state output\n", "\n", - "`calculate_power_flow` can also return the solver's native model state. With `get_model_state=True`, the return value is `(output_data, model_state)`. The state includes a sparse Y-bus for each solver group and, for Newton-Raphson calculations, an ordered `jacobian_history` with one assembled Jacobian per iteration. The existing `jacobian` property remains the final entry in that history. Each group's mapping relates matrix bus positions to the input node IDs." + "`calculate_power_flow` can also return the solver's native model state. With `get_model_state=True`, the return value is `(output_data, model_state)`. The state includes a sparse Y-bus for each solver group and, for Newton-Raphson calculations, ordered `jacobians` and `nodal_states` sequences, aligned by iteration. The Jacobian sparsity pattern is stored once on the group and shared by every frame. Both matrix indices and nodal values use the solver group's internal bus ordering; use the group mapping to resolve input node IDs or synthetic branch3 buses. Asymmetric nodal states store three phase values per bus in phase order." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "id": "ca42da43", "metadata": {}, "outputs": [ @@ -377,8 +377,9 @@ " -268.90243902+215.12195122j -268.90243902+215.12195122j\n", " 537.80487805-429.89754185j]\n", "Captured Jacobian iterations: [1, 2, 3]\n", - "Final Jacobian row pointers: [0 3 6 9]\n", - "Final Jacobian column indices: [0 1 2 0 1 2 0 1 2]\n", + "Captured nodal-state iterations: [1, 2, 3]\n", + "Shared Jacobian row pointers: [0 3 6 9]\n", + "Shared Jacobian column indices: [0 1 2 0 1 2 0 1 2]\n", "Final Jacobian H block: [ 399.92590067 -198.07684237 -201.84905831 -196.03923548\n", " 394.72117311 -198.68193762 -211.59471597 -210.44188084\n", " 10359.27247084]\n", @@ -389,7 +390,13 @@ " -1529.74147022]\n", "Final Jacobian L block: [ 395.92590076 -198.07684237 -201.84905831 -196.03923548\n", " 384.72117318 -198.68193762 -211.59471597 -210.44188084\n", - " 10359.27247102]\n" + " 10359.27247102]\n", + "Final nodal voltage magnitudes: [0.96209647 0.95212621 0.9989881 ]\n", + "Final nodal voltage angles: [-0.02189476 -0.02603079 -0.00303947]\n", + "Final voltages by input node:\n", + " node 1: magnitude=[0.9989881], angle=[-0.00303947]\n", + " node 2: magnitude=[0.95212621], angle=[-0.02603079]\n", + " node 6: magnitude=[0.96209647], angle=[-0.02189476]\n" ] } ], @@ -408,16 +415,37 @@ "print(\"Y-bus column indices:\", y_bus.col_indices)\n", "print(\"Y-bus values:\", y_bus.admittance_real + 1j * y_bus.admittance_imag)\n", "\n", - "jacobian_history = group_state.jacobian_history\n", - "print(\"Captured Jacobian iterations:\", [jac.iteration for jac in jacobian_history])\n", - "if group_state.jacobian is not None:\n", - " jacobian = group_state.jacobian\n", - " print(\"Final Jacobian row pointers:\", jacobian.row_indptr_lu)\n", - " print(\"Final Jacobian column indices:\", jacobian.col_indices_lu)\n", + "jacobians = group_state.jacobians\n", + "nodal_states = group_state.nodal_states\n", + "print(\"Captured Jacobian iterations:\", [jac.iteration for jac in jacobians])\n", + "print(\"Captured nodal-state iterations:\", [state.iteration for state in nodal_states])\n", + "if jacobians:\n", + " jacobian = jacobians[-1]\n", + " nodal_state = nodal_states[-1]\n", + " print(\"Shared Jacobian row pointers:\", group_state.jacobian_structure.row_indptr_lu)\n", + " print(\"Shared Jacobian column indices:\", group_state.jacobian_structure.col_indices_lu)\n", " print(\"Final Jacobian H block:\", jacobian.h)\n", " print(\"Final Jacobian N block:\", jacobian.n)\n", " print(\"Final Jacobian M block:\", jacobian.m)\n", - " print(\"Final Jacobian L block:\", jacobian.l_block)" + " print(\"Final Jacobian L block:\", jacobian.l_block)\n", + " print(\"Final nodal voltage magnitudes:\", nodal_state.voltage_magnitude)\n", + " print(\"Final nodal voltage angles:\", nodal_state.voltage_angle)\n", + " print(\"Final voltages by input node:\")\n", + " for input_idx, node_id in enumerate(model_state.input_node_id):\n", + " group_idx = int(model_state.input_node_group[input_idx])\n", + " bus_idx = int(model_state.input_node_bus[input_idx])\n", + " if bus_idx < 0:\n", + " print(f\" node {node_id}: disconnected\")\n", + " continue\n", + " node_group = model_state.groups[group_idx]\n", + " node_state = node_group.nodal_states[-1]\n", + " phases_per_bus = 1 if node_group.mapping.is_symmetric else 3\n", + " phase_start = bus_idx * phases_per_bus\n", + " phase_stop = phase_start + phases_per_bus\n", + " print(\n", + " f\" node {node_id}: magnitude={node_state.voltage_magnitude[phase_start:phase_stop]}, \"\n", + " f\"angle={node_state.voltage_angle[phase_start:phase_stop]}\"\n", + " )" ] }, { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp index 92c2f38d08..4901d19d6a 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp @@ -41,17 +41,29 @@ struct ModelStateYBus { }; struct ModelStateJacobian { - IdxVector row_indptr_lu; - IdxVector col_indices_lu; std::array, 4> blocks; // H, N, M, L; flattened row-major per sparse entry. Idx iteration{}; }; +struct ModelStateNodalState { + std::vector voltage_magnitude; + std::vector voltage_angle; + Idx iteration{}; +}; + +struct ModelStateJacobianStructure { + // Indices use the solver group's internal bus ordering, independently of user node IDs. + IdxVector row_indptr_lu; + IdxVector col_indices_lu; +}; + struct ModelStateGroup { + // mapping translates the internal bus ordering to input node IDs and synthetic branch3 origins. ModelStateGroupMapping mapping; std::optional y_bus; - std::optional jacobian; - std::vector jacobian_history; + std::optional jacobian_structure; + std::vector jacobians; + std::vector nodal_states; }; struct ModelStateOutput { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp index 5a2b49f6db..4478d06cee 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp @@ -327,9 +327,13 @@ class MainModelImpl { group_state->y_bus = create_y_bus_state(y_bus_vec[i]); } if (state_request.jacobian) { - group_state->jacobian_history = solvers[i].get().get_jacobian_history(y_bus_vec[i]); - if (!group_state->jacobian_history.empty()) { - group_state->jacobian = group_state->jacobian_history.back(); + group_state->jacobians = solvers[i].get().get_jacobians(); + group_state->nodal_states = solvers[i].get().get_nodal_states(); + if (!group_state->jacobians.empty()) { + group_state->jacobian_structure = ModelStateJacobianStructure{ + .row_indptr_lu = y_bus_vec[i].row_indptr_lu(), + .col_indices_lu = y_bus_vec[i].col_indices_lu(), + }; } } result.model_state_group = std::move(group_state); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp index 1a8ca34c6c..08539044d4 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp @@ -115,18 +115,18 @@ template class MathSolver : public MathSolverBase { } } - std::optional get_last_jacobian_state(YBus const& y_bus) const final { + std::vector get_jacobians() const final { if (!last_power_flow_used_newton_raphson_ || !newton_raphson_pf_solver_) { - return std::nullopt; + return {}; } - return newton_raphson_pf_solver_->get_last_jacobian_state(y_bus); + return newton_raphson_pf_solver_->get_jacobians(); } - std::vector get_jacobian_history(YBus const& y_bus) const final { + std::vector get_nodal_states() const final { if (!last_power_flow_used_newton_raphson_ || !newton_raphson_pf_solver_) { return {}; } - return newton_raphson_pf_solver_->get_jacobian_history(y_bus); + return newton_raphson_pf_solver_->get_nodal_states(); } private: diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp index 38e3b221ca..eb71824344 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp @@ -35,10 +35,8 @@ template class MathSolverBase { virtual SolverOutput run_state_estimation(StateEstimationInput const& input, double err_tol, Idx max_iter, Logger& log, CalculationMethod calculation_method, YBus const& y_bus) = 0; - virtual std::optional get_last_jacobian_state(YBus const& /* y_bus */) const { - return std::nullopt; - } - virtual std::vector get_jacobian_history(YBus const& /* y_bus */) const { return {}; } + virtual std::vector get_jacobians() const { return {}; } + virtual std::vector get_nodal_states() const { return {}; } virtual ShortCircuitSolverOutput run_short_circuit(ShortCircuitInput const& input, Logger& log, CalculationMethod calculation_method, YBus const& y_bus) = 0; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp index 10fb15b4d0..167f6e17a5 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp @@ -396,31 +396,17 @@ class NewtonRaphsonPFSolver : public IterativePFSolver get_last_jacobian_state(YBus const& y_bus) const { - if (jacobian_history_.empty()) { - return std::nullopt; - } - auto result = jacobian_history_.back(); - result.row_indptr_lu = y_bus.row_indptr_lu(); - result.col_indices_lu = y_bus.col_indices_lu(); - return result; - } - - std::vector get_jacobian_history(YBus const& y_bus) const { - auto result = jacobian_history_; - for (auto& jacobian : result) { - jacobian.row_indptr_lu = y_bus.row_indptr_lu(); - jacobian.col_indices_lu = y_bus.col_indices_lu(); - } - return result; - } + std::vector get_jacobians() const { return jacobians_; } + std::vector get_nodal_states() const { return nodal_states_; } void log_matrix(Logger& /*log*/, YBus const& /*y_bus*/, Idx iter) { if (capture_jacobian_state_) { @@ -431,6 +417,22 @@ class NewtonRaphsonPFSolver : public IterativePFSolver ? 1 : 3; + nodal_state.voltage_magnitude.reserve(x_.size() * phases_per_bus); + nodal_state.voltage_angle.reserve(x_.size() * phases_per_bus); + for (auto& voltage : x_) { + if constexpr (is_symmetric_v) { + nodal_state.voltage_magnitude.push_back(voltage.v()); + nodal_state.voltage_angle.push_back(voltage.theta()); + } else { + for (Idx phase = 0; phase < 3; ++phase) { + nodal_state.voltage_magnitude.push_back(voltage.v().coeff(phase)); + nodal_state.voltage_angle.push_back(voltage.theta().coeff(phase)); + } + } + } constexpr Idx block_size = is_symmetric_v ? 1 : 9; for (auto& block : result.blocks) { block.clear(); @@ -450,13 +452,15 @@ class NewtonRaphsonPFSolver : public IterativePFSolver> data_jac_; bool capture_jacobian_state_{}; - std::vector jacobian_history_; + std::vector jacobians_; + std::vector nodal_states_; // calculation data std::vector> x_; // unknown // this stores in different steps diff --git a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h index 98a69aabec..cdc166c9f6 100644 --- a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h +++ b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h @@ -146,7 +146,11 @@ typedef struct PGM_StateScenarioView { * @brief Borrowed view of one group's sparse matrices and bus-to-input mapping. * * CSR/value arrays are NULL when the corresponding matrix is absent. Mapping - * pointers are valid until the owning PGM_StateOutput is destroyed. + * pointers are valid until the owning PGM_StateOutput is destroyed. Jacobian + * CSR indices and nodal-state values use the group's internal bus ordering. + * Use the scenario input_node_id/group/bus arrays to map input node order to + * these values. The group mapping also lists user nodes per bus and identifies + * synthetic buses with their originating branch3 IDs. */ typedef struct PGM_StateGroupView { PGM_Idx group; @@ -165,15 +169,12 @@ typedef struct PGM_StateGroupView { double const* admittance_real; double const* admittance_imag; PGM_Idx n_admittance_values; - PGM_Idx has_jacobian; + PGM_Idx has_jacobians; PGM_Idx jacobian_nnz; PGM_Idx const* jacobian_row_indptr; PGM_Idx const* jacobian_col_indices; - double const* jacobian_h; - double const* jacobian_n; - double const* jacobian_m; - double const* jacobian_l; - PGM_Idx n_jacobian_values; + PGM_Idx n_jacobians; + PGM_Idx n_nodal_states; } PGM_StateGroupView; /** @@ -184,10 +185,6 @@ typedef struct PGM_StateGroupView { */ typedef struct PGM_StateJacobianView { PGM_Idx iteration; - PGM_Idx n_bus; - PGM_Idx jacobian_nnz; - PGM_Idx const* jacobian_row_indptr; - PGM_Idx const* jacobian_col_indices; double const* jacobian_h; double const* jacobian_n; double const* jacobian_m; @@ -195,6 +192,20 @@ typedef struct PGM_StateJacobianView { PGM_Idx n_jacobian_values; } PGM_StateJacobianView; +/** + * @brief Borrowed view of nodal voltages at one Newton-Raphson iteration. + * + * Iterations are numbered from one and align with the group's Jacobians. + * Values are bus-major; asymmetric groups store phases 1, 2, 3 for each bus. + * All pointers are valid until the owning PGM_StateOutput is destroyed. + */ +typedef struct PGM_StateNodalStateView { + PGM_Idx iteration; + double const* voltage_magnitude; + double const* voltage_angle; + PGM_Idx n_voltage_values; +} PGM_StateNodalStateView; + /** * @brief Calculate normally and return an owned native state result. * @@ -221,13 +232,22 @@ PGM_API void PGM_state_output_get_group(PGM_Handle* handle, PGM_StateOutput cons PGM_Idx group_idx, PGM_StateGroupView* view); /** @brief Get the number of captured Newton-Raphson Jacobians for one group. */ -PGM_API PGM_Idx PGM_state_output_jacobian_history_count(PGM_Handle* handle, PGM_StateOutput const* state_output, - PGM_Idx scenario_idx, PGM_Idx group_idx); +PGM_API PGM_Idx PGM_state_output_jacobians_count(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx); /** @brief Get a borrowed view of one captured Newton-Raphson Jacobian. */ -PGM_API void PGM_state_output_get_jacobian_history(PGM_Handle* handle, PGM_StateOutput const* state_output, - PGM_Idx scenario_idx, PGM_Idx group_idx, PGM_Idx iteration_idx, - PGM_StateJacobianView* view); +PGM_API void PGM_state_output_get_jacobian(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx, PGM_Idx jacobian_idx, + PGM_StateJacobianView* view); + +/** @brief Get the number of nodal state snapshots for one group. */ +PGM_API PGM_Idx PGM_state_output_nodal_states_count(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx); + +/** @brief Get a borrowed view of one iteration's nodal voltages. */ +PGM_API void PGM_state_output_get_nodal_state(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx, PGM_Idx nodal_state_idx, + PGM_StateNodalStateView* view); /** @brief Destroy an owned state result and invalidate all its borrowed views. */ PGM_API void PGM_destroy_state_output(PGM_StateOutput* state_output); diff --git a/power_grid_model_c/power_grid_model_c/src/model.cpp b/power_grid_model_c/power_grid_model_c/src/model.cpp index e368b5b279..0618c18a6c 100644 --- a/power_grid_model_c/power_grid_model_c/src/model.cpp +++ b/power_grid_model_c/power_grid_model_c/src/model.cpp @@ -521,49 +521,41 @@ void PGM_state_output_get_group(PGM_Handle* handle, PGM_StateOutput const* state output.admittance_imag = y_bus.admittance_imag.data(); output.n_admittance_values = static_cast(y_bus.admittance_real.size()); } - if (group.jacobian.has_value()) { - auto const& jacobian = *group.jacobian; - output.has_jacobian = 1; - output.jacobian_nnz = static_cast(jacobian.col_indices_lu.size()); - output.jacobian_row_indptr = jacobian.row_indptr_lu.data(); - output.jacobian_col_indices = jacobian.col_indices_lu.data(); - output.jacobian_h = jacobian.blocks[0].data(); - output.jacobian_n = jacobian.blocks[1].data(); - output.jacobian_m = jacobian.blocks[2].data(); - output.jacobian_l = jacobian.blocks[3].data(); - output.n_jacobian_values = static_cast(jacobian.blocks[0].size()); + if (!group.jacobians.empty()) { + auto const& structure = *group.jacobian_structure; + output.has_jacobians = 1; + output.jacobian_nnz = static_cast(structure.col_indices_lu.size()); + output.jacobian_row_indptr = structure.row_indptr_lu.data(); + output.jacobian_col_indices = structure.col_indices_lu.data(); + output.n_jacobians = static_cast(group.jacobians.size()); + output.n_nodal_states = static_cast(group.nodal_states.size()); } }); } -PGM_Idx PGM_state_output_jacobian_history_count(PGM_Handle* handle, PGM_StateOutput const* state_output, - PGM_Idx scenario_idx, PGM_Idx group_idx) { +PGM_Idx PGM_state_output_jacobians_count(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx) { return call_with_catch(handle, [state_output, scenario_idx, group_idx] { auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); if (!scenario.has_value()) { throw DatasetError{"No model state was requested for this scenario.\n"}; } - return static_cast(scenario->groups.at(static_cast(group_idx)).jacobian_history.size()); + return static_cast(scenario->groups.at(static_cast(group_idx)).jacobians.size()); }); } -void PGM_state_output_get_jacobian_history(PGM_Handle* handle, PGM_StateOutput const* state_output, - PGM_Idx scenario_idx, PGM_Idx group_idx, PGM_Idx iteration_idx, - PGM_StateJacobianView* view) { - call_with_catch(handle, [state_output, scenario_idx, group_idx, iteration_idx, view] { +void PGM_state_output_get_jacobian(PGM_Handle* handle, PGM_StateOutput const* state_output, PGM_Idx scenario_idx, + PGM_Idx group_idx, PGM_Idx jacobian_idx, PGM_StateJacobianView* view) { + call_with_catch(handle, [state_output, scenario_idx, group_idx, jacobian_idx, view] { auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); if (!scenario.has_value()) { throw DatasetError{"No model state was requested for this scenario.\n"}; } auto const& group = scenario->groups.at(static_cast(group_idx)); - auto const& jacobian = group.jacobian_history.at(static_cast(iteration_idx)); + auto const& jacobian = group.jacobians.at(static_cast(jacobian_idx)); auto& output = safe_ptr_get(view); output = {}; output.iteration = jacobian.iteration; - output.n_bus = group.mapping.n_bus; - output.jacobian_nnz = static_cast(jacobian.col_indices_lu.size()); - output.jacobian_row_indptr = jacobian.row_indptr_lu.data(); - output.jacobian_col_indices = jacobian.col_indices_lu.data(); output.jacobian_h = jacobian.blocks[0].data(); output.jacobian_n = jacobian.blocks[1].data(); output.jacobian_m = jacobian.blocks[2].data(); @@ -572,6 +564,35 @@ void PGM_state_output_get_jacobian_history(PGM_Handle* handle, PGM_StateOutput c }); } +PGM_Idx PGM_state_output_nodal_states_count(PGM_Handle* handle, PGM_StateOutput const* state_output, + PGM_Idx scenario_idx, PGM_Idx group_idx) { + return call_with_catch(handle, [state_output, scenario_idx, group_idx] { + auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); + if (!scenario.has_value()) { + throw DatasetError{"No model state was requested for this scenario.\n"}; + } + return static_cast(scenario->groups.at(static_cast(group_idx)).nodal_states.size()); + }); +} + +void PGM_state_output_get_nodal_state(PGM_Handle* handle, PGM_StateOutput const* state_output, PGM_Idx scenario_idx, + PGM_Idx group_idx, PGM_Idx nodal_state_idx, PGM_StateNodalStateView* view) { + call_with_catch(handle, [state_output, scenario_idx, group_idx, nodal_state_idx, view] { + auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); + if (!scenario.has_value()) { + throw DatasetError{"No model state was requested for this scenario.\n"}; + } + auto const& nodal_state = scenario->groups.at(static_cast(group_idx)) + .nodal_states.at(static_cast(nodal_state_idx)); + auto& output = safe_ptr_get(view); + output = {}; + output.iteration = nodal_state.iteration; + output.voltage_magnitude = nodal_state.voltage_magnitude.data(); + output.voltage_angle = nodal_state.voltage_angle.data(); + output.n_voltage_values = static_cast(nodal_state.voltage_magnitude.size()); + }); +} + void PGM_destroy_state_output(PGM_StateOutput* state_output) { delete state_output; } diff --git a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp index e0d43c4275..056cb466c7 100644 --- a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp +++ b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp @@ -42,14 +42,23 @@ class StateOutput { handle_.call_with(PGM_state_output_get_group, output_.get(), scenario_idx, group_idx, &view); return view; } - Idx jacobian_history_count(Idx scenario_idx, Idx group_idx) const { - return handle_.call_with(PGM_state_output_jacobian_history_count, output_.get(), scenario_idx, + Idx jacobians_count(Idx scenario_idx, Idx group_idx) const { + return handle_.call_with(PGM_state_output_jacobians_count, output_.get(), scenario_idx, group_idx); } - PGM_StateJacobianView jacobian_at_iteration(Idx scenario_idx, Idx group_idx, Idx iteration_idx) const { + PGM_StateJacobianView jacobian(Idx scenario_idx, Idx group_idx, Idx jacobian_idx) const { PGM_StateJacobianView view{}; - handle_.call_with(PGM_state_output_get_jacobian_history, output_.get(), scenario_idx, group_idx, - iteration_idx, &view); + handle_.call_with(PGM_state_output_get_jacobian, output_.get(), scenario_idx, group_idx, jacobian_idx, + &view); + return view; + } + Idx nodal_states_count(Idx scenario_idx, Idx group_idx) const { + return handle_.call_with(PGM_state_output_nodal_states_count, output_.get(), scenario_idx, group_idx); + } + PGM_StateNodalStateView nodal_state(Idx scenario_idx, Idx group_idx, Idx nodal_state_idx) const { + PGM_StateNodalStateView view{}; + handle_.call_with(PGM_state_output_get_nodal_state, output_.get(), scenario_idx, group_idx, + nodal_state_idx, &view); return view; } diff --git a/src/power_grid_model/__init__.py b/src/power_grid_model/__init__.py index f895257c5f..158bed5c7d 100644 --- a/src/power_grid_model/__init__.py +++ b/src/power_grid_model/__init__.py @@ -12,6 +12,8 @@ ModelStateGroup, ModelStateGroupMapping, ModelStateJacobian, + ModelStateJacobianStructure, + ModelStateNodalState, ModelStateYBus, StateOutputRequest, ) @@ -67,6 +69,8 @@ "ModelStateGroup", "ModelStateGroupMapping", "ModelStateJacobian", + "ModelStateJacobianStructure", + "ModelStateNodalState", "ModelStateYBus", "PowerGridModel", "ShortCircuitVoltageScaling", diff --git a/src/power_grid_model/_core/model_state.py b/src/power_grid_model/_core/model_state.py index ab617ce114..24b9383bb7 100644 --- a/src/power_grid_model/_core/model_state.py +++ b/src/power_grid_model/_core/model_state.py @@ -16,6 +16,7 @@ from power_grid_model._core.power_grid_core import ( StateGroupViewC, StateJacobianViewC, + StateNodalStateViewC, StateOutputPtr, StateScenarioViewC, get_power_grid_core, @@ -71,9 +72,15 @@ class ModelStateYBus: @dataclass(frozen=True) -class ModelStateJacobian: +class ModelStateJacobianStructure: + """Shared Jacobian sparsity pattern in the solver group's bus ordering.""" + row_indptr_lu: NDArray[np.int64] col_indices_lu: NDArray[np.int64] + + +@dataclass(frozen=True) +class ModelStateJacobian: h: NDArray[np.float64] n: NDArray[np.float64] m: NDArray[np.float64] @@ -81,15 +88,33 @@ class ModelStateJacobian: iteration: int = 0 -def _build_jacobian(view: StateGroupViewC | StateJacobianViewC, owner: _NativeStateOutput) -> ModelStateJacobian: +@dataclass(frozen=True) +class ModelStateNodalState: + """Per-iteration bus-major voltages; asymmetric groups contain three phases per bus.""" + + voltage_magnitude: NDArray[np.float64] + voltage_angle: NDArray[np.float64] + iteration: int + + +def _build_jacobian( + view: StateJacobianViewC, + owner: _NativeStateOutput, +) -> ModelStateJacobian: return ModelStateJacobian( - row_indptr_lu=_array(view.jacobian_row_indptr, view.n_bus + 1, IdxNp, owner), - col_indices_lu=_array(view.jacobian_col_indices, view.jacobian_nnz, IdxNp, owner), h=_array(view.jacobian_h, view.n_jacobian_values, np.float64, owner), n=_array(view.jacobian_n, view.n_jacobian_values, np.float64, owner), m=_array(view.jacobian_m, view.n_jacobian_values, np.float64, owner), l_block=_array(view.jacobian_l, view.n_jacobian_values, np.float64, owner), - iteration=getattr(view, "iteration", 0), + iteration=view.iteration, + ) + + +def _build_nodal_state(view: StateNodalStateViewC, owner: _NativeStateOutput) -> ModelStateNodalState: + return ModelStateNodalState( + voltage_magnitude=_array(view.voltage_magnitude, view.n_voltage_values, np.float64, owner), + voltage_angle=_array(view.voltage_angle, view.n_voltage_values, np.float64, owner), + iteration=view.iteration, ) @@ -109,8 +134,9 @@ class ModelStateGroupMapping: class ModelStateGroup: mapping: ModelStateGroupMapping y_bus: ModelStateYBus | None - jacobian: ModelStateJacobian | None - jacobian_history: tuple[ModelStateJacobian, ...] = () + jacobian_structure: ModelStateJacobianStructure | None + jacobians: tuple[ModelStateJacobian, ...] = () + nodal_states: tuple[ModelStateNodalState, ...] = () @dataclass(frozen=True) @@ -155,24 +181,39 @@ def _build_scenario(owner: _NativeStateOutput, scenario_idx: int) -> ModelState admittance_real=_array(view.admittance_real, view.n_admittance_values, np.float64, owner), admittance_imag=_array(view.admittance_imag, view.n_admittance_values, np.float64, owner), ) - jacobian_history_items = [] - history_count = pgc.state_output_jacobian_history_count(owner.pointer, scenario_idx, group_idx) - for iteration_idx in range(history_count): - jacobian_view = StateJacobianViewC() - pgc.state_output_get_jacobian_history( - owner.pointer, scenario_idx, group_idx, iteration_idx, pointer(jacobian_view) + jacobians: tuple[ModelStateJacobian, ...] = () + nodal_states: tuple[ModelStateNodalState, ...] = () + jacobian_structure = None + if view.has_jacobians: + jacobian_row_indptr = _array(view.jacobian_row_indptr, view.n_bus + 1, IdxNp, owner) + jacobian_col_indices = _array(view.jacobian_col_indices, view.jacobian_nnz, IdxNp, owner) + jacobian_structure = ModelStateJacobianStructure( + row_indptr_lu=jacobian_row_indptr, + col_indices_lu=jacobian_col_indices, + ) + jacobians_items = [] + for jacobian_idx in range(view.n_jacobians): + jacobian_view = StateJacobianViewC() + pgc.state_output_get_jacobian( + owner.pointer, scenario_idx, group_idx, jacobian_idx, pointer(jacobian_view) + ) + jacobians_items.append(_build_jacobian(jacobian_view, owner)) + jacobians = tuple(jacobians_items) + nodal_states_items = [] + for nodal_state_idx in range(view.n_nodal_states): + nodal_state_view = StateNodalStateViewC() + pgc.state_output_get_nodal_state( + owner.pointer, scenario_idx, group_idx, nodal_state_idx, pointer(nodal_state_view) ) - jacobian_history_items.append(_build_jacobian(jacobian_view, owner)) - jacobian_history = tuple(jacobian_history_items) - jacobian = None - if view.has_jacobian: - jacobian = jacobian_history[-1] if jacobian_history else _build_jacobian(view, owner) + nodal_states_items.append(_build_nodal_state(nodal_state_view, owner)) + nodal_states = tuple(nodal_states_items) groups.append( ModelStateGroup( mapping=mapping, y_bus=y_bus, - jacobian=jacobian, - jacobian_history=jacobian_history, + jacobian_structure=jacobian_structure, + jacobians=jacobians, + nodal_states=nodal_states, ) ) diff --git a/src/power_grid_model/_core/power_grid_core.py b/src/power_grid_model/_core/power_grid_core.py index 2b1e92fe23..e3651161cc 100644 --- a/src/power_grid_model/_core/power_grid_core.py +++ b/src/power_grid_model/_core/power_grid_core.py @@ -176,25 +176,18 @@ class StateGroupViewC(Structure): ("admittance_real", DoublePtr), ("admittance_imag", DoublePtr), ("n_admittance_values", IdxC), - ("has_jacobian", IdxC), + ("has_jacobians", IdxC), ("jacobian_nnz", IdxC), ("jacobian_row_indptr", IdxPtr), ("jacobian_col_indices", IdxPtr), - ("jacobian_h", DoublePtr), - ("jacobian_n", DoublePtr), - ("jacobian_m", DoublePtr), - ("jacobian_l", DoublePtr), - ("n_jacobian_values", IdxC), + ("n_jacobians", IdxC), + ("n_nodal_states", IdxC), ] class StateJacobianViewC(Structure): _fields_ = [ ("iteration", IdxC), - ("n_bus", IdxC), - ("jacobian_nnz", IdxC), - ("jacobian_row_indptr", IdxPtr), - ("jacobian_col_indices", IdxPtr), ("jacobian_h", DoublePtr), ("jacobian_n", DoublePtr), ("jacobian_m", DoublePtr), @@ -203,10 +196,20 @@ class StateJacobianViewC(Structure): ] +class StateNodalStateViewC(Structure): + _fields_ = [ + ("iteration", IdxC), + ("voltage_magnitude", DoublePtr), + ("voltage_angle", DoublePtr), + ("n_voltage_values", IdxC), + ] + + StateOutputRequestPtr = POINTER(StateOutputRequestC) StateScenarioViewPtr = POINTER(StateScenarioViewC) StateGroupViewPtr = POINTER(StateGroupViewC) StateJacobianViewPtr = POINTER(StateJacobianViewC) +StateNodalStateViewPtr = POINTER(StateNodalStateViewC) StateOutputPtrPtr = POINTER(StateOutputPtr) @@ -534,22 +537,35 @@ def state_output_get_group( pass # pragma: no cover @make_c_binding - def state_output_jacobian_history_count( - self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int - ) -> int: # type: ignore[empty-body] + def state_output_jacobians_count(self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int) -> int: # type: ignore[empty-body] pass # pragma: no cover @make_c_binding - def state_output_get_jacobian_history( + def state_output_get_jacobian( self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int, - iteration_idx: int, + jacobian_idx: int, view: StateJacobianViewPtr, # type: ignore[valid-type] ) -> None: # type: ignore[empty-body] pass # pragma: no cover + @make_c_binding + def state_output_nodal_states_count(self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int) -> int: + pass # pragma: no cover + + @make_c_binding + def state_output_get_nodal_state( + self, + state_output: StateOutputPtr, + scenario_idx: int, + group_idx: int, + nodal_state_idx: int, + view: StateNodalStateViewPtr, # type: ignore[valid-type] + ) -> None: # type: ignore[empty-body] + pass # pragma: no cover + @make_c_binding def destroy_state_output(self, state_output: StateOutputPtr) -> None: # type: ignore[empty-body] pass # pragma: no cover diff --git a/tests/cpp_unit_tests/state_output/test_state_output.cpp b/tests/cpp_unit_tests/state_output/test_state_output.cpp index 2120f3df6e..45e7375060 100644 --- a/tests/cpp_unit_tests/state_output/test_state_output.cpp +++ b/tests/cpp_unit_tests/state_output/test_state_output.cpp @@ -99,8 +99,7 @@ TEST_CASE("Test State output - Capture Y-bus and input-node mapping") { REQUIRE(group.y_bus->admittance_real.size() == 1); CHECK(std::isfinite(group.y_bus->admittance_real[0])); CHECK(std::isfinite(group.y_bus->admittance_imag[0])); - CHECK_FALSE(group.jacobian.has_value()); - CHECK(group.jacobian_history.empty()); + CHECK(group.jacobians.empty()); } TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { @@ -116,12 +115,10 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { REQUIRE(state_output->groups.size() == 1); auto const& group = state_output->groups[0]; CHECK_FALSE(group.y_bus.has_value()); - REQUIRE(group.jacobian.has_value()); - CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[1].size()); - CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[2].size()); - CHECK(group.jacobian->blocks[0].size() == group.jacobian->blocks[3].size()); - CHECK(std::ranges::any_of(group.jacobian->blocks[0], [](double value) { return value != 0.0; })); - CHECK(std::ranges::all_of(group.jacobian->blocks[0], [](double value) { return std::isfinite(value); })); + REQUIRE_FALSE(group.jacobians.empty()); + REQUIRE(group.jacobian_structure.has_value()); + CHECK(group.jacobian_structure->row_indptr_lu.size() == static_cast(group.mapping.n_bus + 1)); + CHECK_FALSE(group.jacobian_structure->col_indices_lu.empty()); auto const report = logger.report(); constexpr std::string_view jacobian_tag{"Tag:4001: "}; @@ -136,7 +133,8 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { } REQUIRE_FALSE(jacobian_logs.empty()); CHECK(jacobian_logs.size() > 1); - REQUIRE(group.jacobian_history.size() == jacobian_logs.size()); + REQUIRE(group.jacobians.size() == jacobian_logs.size()); + REQUIRE(group.nodal_states.size() == group.jacobians.size()); auto parse_values = [](std::string_view jacobian_log, std::string_view field) { auto const field_start = jacobian_log.find(std::string{field} + "="); REQUIRE(field_start != std::string_view::npos); @@ -159,13 +157,30 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { } }; for (size_t iteration_idx = 0; iteration_idx < jacobian_logs.size(); ++iteration_idx) { - auto const& jacobian = group.jacobian_history[iteration_idx]; + auto const& jacobian = group.jacobians[iteration_idx]; + auto const& nodal_state = group.nodal_states[iteration_idx]; auto const jacobian_log = jacobian_logs[iteration_idx]; CHECK(jacobian.iteration == static_cast(iteration_idx + 1)); - check_logged_values(jacobian_log, "row_indptr_lu", - std::vector(jacobian.row_indptr_lu.begin(), jacobian.row_indptr_lu.end())); - check_logged_values(jacobian_log, "col_indices_lu", - std::vector(jacobian.col_indices_lu.begin(), jacobian.col_indices_lu.end())); + CHECK(nodal_state.iteration == jacobian.iteration); + auto const phases_per_bus = group.mapping.is_symmetric ? 1 : 3; + auto const n_voltage_values = static_cast(group.mapping.n_bus * phases_per_bus); + CHECK(nodal_state.voltage_magnitude.size() == n_voltage_values); + CHECK(nodal_state.voltage_angle.size() == n_voltage_values); + CHECK(std::ranges::all_of(nodal_state.voltage_magnitude, [](double value) { return std::isfinite(value); })); + CHECK(std::ranges::all_of(nodal_state.voltage_angle, [](double value) { return std::isfinite(value); })); + CHECK(jacobian.blocks[0].size() == jacobian.blocks[1].size()); + CHECK(jacobian.blocks[0].size() == jacobian.blocks[2].size()); + CHECK(jacobian.blocks[0].size() == jacobian.blocks[3].size()); + CHECK(std::ranges::any_of(jacobian.blocks[0], [](double value) { return value != 0.0; })); + CHECK(std::ranges::all_of(jacobian.blocks[0], [](double value) { return std::isfinite(value); })); + if (iteration_idx == 0) { + check_logged_values(jacobian_log, "row_indptr_lu", + std::vector(group.jacobian_structure->row_indptr_lu.begin(), + group.jacobian_structure->row_indptr_lu.end())); + check_logged_values(jacobian_log, "col_indices_lu", + std::vector(group.jacobian_structure->col_indices_lu.begin(), + group.jacobian_structure->col_indices_lu.end())); + } check_logged_values(jacobian_log, "jac_h", jacobian.blocks[0]); check_logged_values(jacobian_log, "jac_n", jacobian.blocks[1]); check_logged_values(jacobian_log, "jac_m", jacobian.blocks[2]); @@ -183,8 +198,9 @@ TEST_CASE("Test State output - Linear method has no Jacobian") { REQUIRE(state_output.has_value()); REQUIRE(state_output->groups.size() == 1); CHECK(state_output->groups[0].y_bus.has_value()); - CHECK_FALSE(state_output->groups[0].jacobian.has_value()); - CHECK(state_output->groups[0].jacobian_history.empty()); + CHECK(state_output->groups[0].jacobians.empty()); + CHECK(state_output->groups[0].nodal_states.empty()); + CHECK_FALSE(state_output->groups[0].jacobian_structure.has_value()); } TEST_CASE("Test State output - Unrequested model state is absent") { diff --git a/tests/native_api_tests/test_api_model.cpp b/tests/native_api_tests/test_api_model.cpp index 615a9a9bd5..010972e2d1 100644 --- a/tests/native_api_tests/test_api_model.cpp +++ b/tests/native_api_tests/test_api_model.cpp @@ -290,16 +290,23 @@ TEST_CASE("API Model") { auto const group = state_output.group(0, 0); CHECK(group.has_y_bus == 1); - CHECK(group.has_jacobian == 1); + CHECK(group.has_jacobians == 1); CHECK(group.y_bus_nnz > 0); CHECK(group.jacobian_nnz > 0); - auto const history_count = state_output.jacobian_history_count(0, 0); - REQUIRE(history_count > 0); - auto const first_jacobian = state_output.jacobian_at_iteration(0, 0, 0); - auto const last_jacobian = state_output.jacobian_at_iteration(0, 0, history_count - 1); + auto const jacobians_count = state_output.jacobians_count(0, 0); + REQUIRE(jacobians_count > 0); + CHECK(state_output.nodal_states_count(0, 0) == jacobians_count); + CHECK(group.n_nodal_states == jacobians_count); + auto const first_jacobian = state_output.jacobian(0, 0, 0); + auto const last_jacobian = state_output.jacobian(0, 0, jacobians_count - 1); + auto const last_nodal_state = state_output.nodal_state(0, 0, jacobians_count - 1); CHECK(first_jacobian.iteration == 1); - CHECK(last_jacobian.iteration == history_count); - CHECK(last_jacobian.jacobian_nnz == group.jacobian_nnz); + CHECK(last_jacobian.iteration == jacobians_count); + CHECK(last_jacobian.n_jacobian_values > 0); + CHECK(last_nodal_state.iteration == last_jacobian.iteration); + CHECK(last_nodal_state.n_voltage_values == group.n_bus * (group.is_symmetric ? 1 : 3)); + CHECK(group.jacobian_row_indptr[0] == 0); + CHECK(group.jacobian_col_indices != nullptr); } } diff --git a/tests/unit/test_power_grid_model.py b/tests/unit/test_power_grid_model.py index dc50ac614d..d47f2cec79 100644 --- a/tests/unit/test_power_grid_model.py +++ b/tests/unit/test_power_grid_model.py @@ -180,16 +180,19 @@ def test_power_flow_model_state(model: PowerGridModel): assert len(model_state.groups) == 1 group = model_state.groups[0] assert group.y_bus is not None - assert group.jacobian is not None - assert group.jacobian_history - assert [jacobian.iteration for jacobian in group.jacobian_history] == list( - range(1, len(group.jacobian_history) + 1) - ) - assert group.jacobian_history[-1] is group.jacobian + assert group.jacobians + assert [jacobian.iteration for jacobian in group.jacobians] == list(range(1, len(group.jacobians) + 1)) + assert group.jacobian_structure is not None + assert group.jacobian_structure.row_indptr_lu.shape == (group.mapping.n_bus + 1,) + assert len(group.nodal_states) == len(group.jacobians) + assert [state.iteration for state in group.nodal_states] == [jac.iteration for jac in group.jacobians] assert group.mapping.bus_user_id.tolist() == [0] assert group.y_bus.row_indptr.shape == (group.mapping.n_bus + 1,) assert group.y_bus.admittance_real.size > 0 - assert group.jacobian.h.size == group.jacobian.n.size + assert group.jacobians[-1].h.size == group.jacobians[-1].n.size + n_voltage_values = group.mapping.n_bus * (1 if group.mapping.is_symmetric else 3) + assert group.nodal_states[-1].voltage_magnitude.size == n_voltage_values + assert group.nodal_states[-1].voltage_angle.size == n_voltage_values admittance_real = group.y_bus.admittance_real del model_state @@ -204,8 +207,9 @@ def test_linear_power_flow_model_state_has_no_jacobian(model: PowerGridModel): assert model_state is not None assert model_state.groups[0].y_bus is not None - assert model_state.groups[0].jacobian is None - assert model_state.groups[0].jacobian_history == () + assert model_state.groups[0].jacobians == () + assert model_state.groups[0].nodal_states == () + assert model_state.groups[0].jacobian_structure is None def test_power_flow_model_state_batch_requests(model: PowerGridModel, update_batch): @@ -222,13 +226,14 @@ def test_power_flow_model_state_batch_requests(model: PowerGridModel, update_bat assert states[0] is not None assert states[0].y_bus_requested assert states[0].groups[0].y_bus is not None - assert states[0].groups[0].jacobian is None - assert states[0].groups[0].jacobian_history == () + assert states[0].groups[0].jacobians == () + assert states[0].groups[0].nodal_states == () + assert states[0].groups[0].jacobian_structure is None assert states[1] is not None assert states[1].jacobian_requested assert states[1].groups[0].y_bus is None - assert states[1].groups[0].jacobian is not None - assert states[1].groups[0].jacobian_history + assert states[1].groups[0].jacobians + assert states[1].groups[0].nodal_states def test_power_flow_model_state_request_count_must_match_batch(model: PowerGridModel, update_batch): From 16b3a7493cf6258d039e5a2b6646208e9e5ab27a Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sat, 10 Oct 2026 15:59:57 +0200 Subject: [PATCH 08/25] nodal state request seperate Signed-off-by: Jerry Jinfeng Guo --- docs/examples/Power Flow Example.ipynb | 73 +++++++----- .../include/power_grid_model/calculation.hpp | 11 +- .../calculation_parameters.hpp | 2 +- .../power_grid_model/calculation_state.hpp | 4 +- .../include/power_grid_model/job_adapter.hpp | 18 +-- .../include/power_grid_model/link_solver.hpp | 4 +- .../power_grid_model/main_model_impl.hpp | 47 ++++---- .../math_solver/math_solver.hpp | 10 +- .../math_solver/math_solver_dispatch.hpp | 3 +- .../math_solver/newton_raphson_pf_solver.hpp | 48 ++++---- .../power_grid_model/math_solver/y_bus.hpp | 1 - .../include/power_grid_model_c/model.h | 5 +- .../power_grid_model_c/src/model.cpp | 35 +++--- .../include/power_grid_model_cpp/model.hpp | 110 +++++++++--------- src/power_grid_model/_core/model_state.py | 3 + src/power_grid_model/_core/power_grid_core.py | 3 +- .../_core/power_grid_model.py | 7 +- .../state_output/test_state_output.cpp | 37 ++++-- tests/native_api_tests/test_api_model.cpp | 7 +- tests/unit/test_power_grid_model.py | 18 ++- 20 files changed, 263 insertions(+), 183 deletions(-) diff --git a/docs/examples/Power Flow Example.ipynb b/docs/examples/Power Flow Example.ipynb index 046e88e4f8..5dbf531c03 100644 --- a/docs/examples/Power Flow Example.ipynb +++ b/docs/examples/Power Flow Example.ipynb @@ -79,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "6f008736", "metadata": {}, "outputs": [], @@ -141,7 +141,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 4, "id": "1034f6f3", "metadata": {}, "outputs": [], @@ -179,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 5, "id": "f0c8c3e8", "metadata": {}, "outputs": [ @@ -220,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "id": "40509eaf", "metadata": {}, "outputs": [], @@ -253,7 +253,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 7, "id": "7ef134e9", "metadata": {}, "outputs": [], @@ -290,7 +290,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 8, "id": "44c2de63", "metadata": {}, "outputs": [], @@ -313,7 +313,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 9, "id": "a581a36e", "metadata": {}, "outputs": [ @@ -358,7 +358,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "id": "ca42da43", "metadata": {}, "outputs": [ @@ -391,9 +391,22 @@ "Final Jacobian L block: [ 395.92590076 -198.07684237 -201.84905831 -196.03923548\n", " 384.72117318 -198.68193762 -211.59471597 -210.44188084\n", " 10359.27247102]\n", - "Final nodal voltage magnitudes: [0.96209647 0.95212621 0.9989881 ]\n", - "Final nodal voltage angles: [-0.02189476 -0.02603079 -0.00303947]\n", - "Final voltages by input node:\n", + "Nodal voltage evolution by iteration:\n", + "Group 0, iteration 1:\n", + " Nodal voltage magnitudes: [0.96523561 0.95632505 0.99908707]\n", + " Nodal voltage angles: [-0.02020439 -0.02387315 -0.00278854]\n", + " node 1: magnitude=[0.99908707], angle=[-0.00278854]\n", + " node 2: magnitude=[0.95632505], angle=[-0.02387315]\n", + " node 6: magnitude=[0.96523561], angle=[-0.02020439]\n", + "Group 0, iteration 2:\n", + " Nodal voltage magnitudes: [0.96211126 0.9521469 0.998989 ]\n", + " Nodal voltage angles: [-0.02188866 -0.02602094 -0.00303857]\n", + " node 1: magnitude=[0.998989], angle=[-0.00303857]\n", + " node 2: magnitude=[0.9521469], angle=[-0.02602094]\n", + " node 6: magnitude=[0.96211126], angle=[-0.02188866]\n", + "Group 0, iteration 3:\n", + " Nodal voltage magnitudes: [0.96209647 0.95212621 0.9989881 ]\n", + " Nodal voltage angles: [-0.02189476 -0.02603079 -0.00303947]\n", " node 1: magnitude=[0.9989881], angle=[-0.00303947]\n", " node 2: magnitude=[0.95212621], angle=[-0.02603079]\n", " node 6: magnitude=[0.96209647], angle=[-0.02189476]\n" @@ -421,31 +434,35 @@ "print(\"Captured nodal-state iterations:\", [state.iteration for state in nodal_states])\n", "if jacobians:\n", " jacobian = jacobians[-1]\n", - " nodal_state = nodal_states[-1]\n", " print(\"Shared Jacobian row pointers:\", group_state.jacobian_structure.row_indptr_lu)\n", " print(\"Shared Jacobian column indices:\", group_state.jacobian_structure.col_indices_lu)\n", " print(\"Final Jacobian H block:\", jacobian.h)\n", " print(\"Final Jacobian N block:\", jacobian.n)\n", " print(\"Final Jacobian M block:\", jacobian.m)\n", " print(\"Final Jacobian L block:\", jacobian.l_block)\n", - " print(\"Final nodal voltage magnitudes:\", nodal_state.voltage_magnitude)\n", - " print(\"Final nodal voltage angles:\", nodal_state.voltage_angle)\n", - " print(\"Final voltages by input node:\")\n", + "\n", + " print(\"Nodal voltage evolution by iteration:\")\n", " for input_idx, node_id in enumerate(model_state.input_node_id):\n", - " group_idx = int(model_state.input_node_group[input_idx])\n", - " bus_idx = int(model_state.input_node_bus[input_idx])\n", - " if bus_idx < 0:\n", + " if model_state.input_node_bus[input_idx] < 0:\n", " print(f\" node {node_id}: disconnected\")\n", - " continue\n", - " node_group = model_state.groups[group_idx]\n", - " node_state = node_group.nodal_states[-1]\n", - " phases_per_bus = 1 if node_group.mapping.is_symmetric else 3\n", - " phase_start = bus_idx * phases_per_bus\n", - " phase_stop = phase_start + phases_per_bus\n", - " print(\n", - " f\" node {node_id}: magnitude={node_state.voltage_magnitude[phase_start:phase_stop]}, \"\n", - " f\"angle={node_state.voltage_angle[phase_start:phase_stop]}\"\n", - " )" + " for group_idx, node_group in enumerate(model_state.groups):\n", + " for node_state in node_group.nodal_states:\n", + " print(f\"Group {group_idx}, iteration {node_state.iteration}:\")\n", + " print(\" Nodal voltage magnitudes:\", node_state.voltage_magnitude)\n", + " print(\" Nodal voltage angles:\", node_state.voltage_angle)\n", + " for input_idx, node_id in enumerate(model_state.input_node_id):\n", + " if model_state.input_node_group[input_idx] != group_idx:\n", + " continue\n", + " bus_idx = int(model_state.input_node_bus[input_idx])\n", + " if bus_idx < 0:\n", + " continue\n", + " phases_per_bus = 1 if node_group.mapping.is_symmetric else 3\n", + " phase_start = bus_idx * phases_per_bus\n", + " phase_stop = phase_start + phases_per_bus\n", + " print(\n", + " f\" node {node_id}: magnitude={node_state.voltage_magnitude[phase_start:phase_stop]}, \"\n", + " f\"angle={node_state.voltage_angle[phase_start:phase_stop]}\"\n", + " )" ] }, { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp index a282057788..9459456a60 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp @@ -93,9 +93,9 @@ template struct Calculator { Logger& logger) { return [calculation_method, err_tol = options.err_tol, max_iter = options.max_iter, &logger, cache_run](MathSolverProxy& solver, YBus const& y_bus, PowerFlowInput const& input, - bool capture_jacobian_state) { + bool capture_jacobian_state, bool capture_nodal_state) { return solver.get().run_power_flow(input, err_tol, max_iter, cache_run, logger, calculation_method, y_bus, - capture_jacobian_state); + capture_jacobian_state, capture_nodal_state); }; } }; @@ -110,8 +110,8 @@ template struct Calculator { static auto solver(CalculationMethod calculation_method, MainModelOptions const& options, bool /*cache_run*/, Logger& logger) { return [calculation_method, err_tol = options.err_tol, max_iter = options.max_iter, - &logger](MathSolverProxy& solver, YBus const& y_bus, - StateEstimationInput const& input, bool /*capture_jacobian_state*/) { + &logger](MathSolverProxy& solver, YBus const& y_bus, StateEstimationInput const& input, + bool /*capture_jacobian_state*/, bool /*capture_nodal_state*/) { return solver.get().run_state_estimation(input, err_tol, max_iter, logger, calculation_method, y_bus); }; } @@ -127,7 +127,8 @@ template struct Calculator { static auto solver(CalculationMethod calculation_method, MainModelOptions const& /*options*/, bool /*cache_run*/, Logger& logger) { return [calculation_method, &logger](MathSolverProxy& solver, YBus const& y_bus, - ShortCircuitInput const& input, bool /*capture_jacobian_state*/) { + ShortCircuitInput const& input, bool /*capture_jacobian_state*/, + bool /*capture_nodal_state*/) { return solver.get().run_short_circuit(input, logger, calculation_method, y_bus); }; } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp index 72ee9cd765..8323121840 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp @@ -4,6 +4,7 @@ #pragma once +#include "calculation_state.hpp" #include "common/common.hpp" #include "common/enum.hpp" #include "common/grouped_index_vector.hpp" @@ -11,7 +12,6 @@ #include "common/small_vector.hpp" #include "common/statistics.hpp" #include "common/three_phase_tensor.hpp" -#include "calculation_state.hpp" #include #include diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp index 4901d19d6a..406f2bc273 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_state.hpp @@ -15,6 +15,7 @@ namespace power_grid_model { struct ModelStateRequest { bool y_bus{}; bool jacobian{}; + bool nodal_state{}; }; enum class ModelStateBusKind : IntS { input_node, synthetic_branch3 }; @@ -69,6 +70,7 @@ struct ModelStateGroup { struct ModelStateOutput { bool y_bus_requested{}; bool jacobian_requested{}; + bool nodal_state_requested{}; // Forward mapping indexed by input node sequence. Disconnected nodes use {-1, -1}. IdxVector input_node_group; IdxVector input_node_bus; @@ -76,4 +78,4 @@ struct ModelStateOutput { std::vector groups; }; -} // namespace power_grid_model \ No newline at end of file +} // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/job_adapter.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/job_adapter.hpp index 8555e9bda0..393b89e195 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/job_adapter.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/job_adapter.hpp @@ -29,11 +29,13 @@ template class JobAdapter : public JobInterface { using ModelType = MainModel::ImplType; JobAdapter(std::reference_wrapper model_reference, - std::reference_wrapper options, - std::span state_requests = {}, - std::span> state_outputs = {}) - : model_reference_{model_reference}, options_{options}, state_requests_{state_requests}, - state_outputs_{state_outputs} {} + std::reference_wrapper options, + std::span state_requests = {}, + std::span> state_outputs = {}) + : model_reference_{model_reference}, + options_{options}, + state_requests_{state_requests}, + state_outputs_{state_outputs} {} JobAdapter(JobAdapter const& other) : model_copy_{std::make_unique(other.model_reference_.get())}, model_reference_{std::ref(*model_copy_)}, @@ -104,9 +106,9 @@ template class JobAdapter : public JobInterface { ModelStateRequest const* state_request = state_requests_.empty() ? nullptr : &state_requests_[scenario_idx]; ModelStateOutput state_output; MainModel::calculator(options_.get(), model_reference_.get(), result_data.get_individual_scenario(scenario_idx), - false, logger, state_request, - state_requests_.empty() ? nullptr : &state_output); - if (!state_requests_.empty() && (state_request->y_bus || state_request->jacobian)) { + false, logger, state_request, state_requests_.empty() ? nullptr : &state_output); + if (!state_requests_.empty() && + (state_request->y_bus || state_request->jacobian || state_request->nodal_state)) { state_outputs_[scenario_idx] = std::move(state_output); } } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/link_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/link_solver.hpp index c07da210ef..32242d93f5 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/link_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/link_solver.hpp @@ -212,8 +212,8 @@ inline void backward_substitution(ReducedEchelonForm& elimination_result) { // as these are the only ones that can be affected by the backward substitution for (auto const backward_col_idx : backward_substitution_free_right_cols(free_col_indices, pivot_col_idx)) { if (auto const pivot_value = elimination_result.matrix.get_value(pivot_row_idx, backward_col_idx)) { - elimination_result.matrix.add_to_value( - static_cast(-multiplier_value * *pivot_value), row_idx, backward_col_idx); + elimination_result.matrix.add_to_value(static_cast(-multiplier_value * *pivot_value), row_idx, + backward_col_idx); } } elimination_result.rhs[row_idx] -= diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp index 4478d06cee..9078f3c99a 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp @@ -286,19 +286,19 @@ class MainModelImpl { template requires std::invocable, Idx /*n_math_solvers*/> && std::ranges::range> && - std::invocable< - std::remove_cvref_t, MathSolverType&, YBus const&, - typename std::invoke_result_t::const_reference, - bool> && + std::invocable, MathSolverType&, YBus const&, + typename std::invoke_result_t::const_reference, + bool, bool> && solver_output_type::const_reference, bool>> + typename std::invoke_result_t::const_reference, bool, + bool>> auto calculate_(PrepareInputFn&& prepare_input, SolveFn&& solve, Logger& logger, ModelStateRequest const& state_request) { - using InputType = typename std::invoke_result_t::const_reference; - using SolverOutputType = - std::invoke_result_t; - using sym = decode_symmetry_v; + using InputType = typename std::invoke_result_t::const_reference; + using SolverOutputType = + typename std::invoke_result_t; + using sym = decode_symmetry_v; assert(construction_complete_); // prepare @@ -318,9 +318,10 @@ class MainModelImpl { std::vector solver_output; solver_output.reserve(get_n_math_solvers(state_)); for (Idx i = 0; i != get_n_math_solvers(state_); ++i) { - SolverOutputType result = solve_(solvers[i], y_bus_vec[i], input[i], state_request.jacobian); + SolverOutputType result = + solve_(solvers[i], y_bus_vec[i], input[i], state_request.jacobian, state_request.nodal_state); if constexpr (steady_state_solver_output_type) { - if (state_request.y_bus || state_request.jacobian) { + if (state_request.y_bus || state_request.jacobian || state_request.nodal_state) { auto group_state = std::make_shared(); group_state->mapping = create_model_state_mapping(i, y_bus_vec[i].size()); if (state_request.y_bus) { @@ -328,7 +329,6 @@ class MainModelImpl { } if (state_request.jacobian) { group_state->jacobians = solvers[i].get().get_jacobians(); - group_state->nodal_states = solvers[i].get().get_nodal_states(); if (!group_state->jacobians.empty()) { group_state->jacobian_structure = ModelStateJacobianStructure{ .row_indptr_lu = y_bus_vec[i].row_indptr_lu(), @@ -336,6 +336,9 @@ class MainModelImpl { }; } } + if (state_request.nodal_state) { + group_state->nodal_states = solvers[i].get().get_nodal_states(); + } result.model_state_group = std::move(group_state); } } @@ -351,8 +354,8 @@ class MainModelImpl { .n_bus = n_bus, .is_symmetric = is_symmetric_v, .bus_user_indptr = IdxVector(static_cast(n_bus + 1), 0), - .bus_kind = std::vector(static_cast(n_bus), - ModelStateBusKind::synthetic_branch3), + .bus_kind = + std::vector(static_cast(n_bus), ModelStateBusKind::synthetic_branch3), .origin_branch3_id = std::vector(static_cast(n_bus), static_cast(-1)), }; @@ -438,18 +441,18 @@ class MainModelImpl { ? SearchMethod::linear_search : SearchMethod::binary_search; - auto result = optimizer::get_optimizer( - options.optimizer_type, options.optimizer_strategy, get_calculator(), - [this](ConstDataset const& update_data) { - this->update_components(update_data); - }, - *meta_data_, search_method) - ->optimize(state_, options.calculation_method); + auto result = + optimizer::get_optimizer( + options.optimizer_type, options.optimizer_strategy, get_calculator(), + [this](ConstDataset const& update_data) { this->update_components(update_data); }, + *meta_data_, search_method) + ->optimize(state_, options.calculation_method); if constexpr (std::derived_from) { - if (state_request.y_bus || state_request.jacobian) { + if (state_request.y_bus || state_request.jacobian || state_request.nodal_state) { ModelStateOutput model_state; model_state.y_bus_requested = state_request.y_bus; model_state.jacobian_requested = state_request.jacobian; + model_state.nodal_state_requested = state_request.nodal_state; auto const nodes = state_.components.template citer(); auto const& node_coupling = state_.topo_comp_coup->node; model_state.input_node_group.reserve(std::ranges::size(nodes)); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp index 08539044d4..0e523a5b9f 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp @@ -42,7 +42,7 @@ template class MathSolver : public MathSolverBase { SolverOutput run_power_flow(PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, Logger& log, CalculationMethod calculation_method, YBus const& y_bus, - bool capture_jacobian_state) final { + bool capture_jacobian_state, bool capture_nodal_state) final { using enum CalculationMethod; last_power_flow_used_newton_raphson_ = false; @@ -55,7 +55,7 @@ template class MathSolver : public MathSolverBase { case newton_raphson: last_power_flow_used_newton_raphson_ = true; return run_power_flow_newton_raphson(input, err_tol, max_iter, cache_run, log, y_bus, - capture_jacobian_state); + capture_jacobian_state, capture_nodal_state); case linear: return run_power_flow_linear(input, err_tol, max_iter, log, y_bus); case linear_current: @@ -142,13 +142,13 @@ template class MathSolver : public MathSolverBase { SolverOutput run_power_flow_newton_raphson(PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, Logger& log, YBus const& y_bus, - bool capture_jacobian_state) { + bool capture_jacobian_state, bool capture_nodal_state) { if (!newton_raphson_pf_solver_.has_value()) { Timer const timer{log, LogEvent::create_math_solver}; newton_raphson_pf_solver_.emplace(y_bus, *topo_ptr_); } - newton_raphson_pf_solver_->set_jacobian_state_capture(capture_jacobian_state); - return newton_raphson_pf_solver_->run_power_flow(y_bus, input, err_tol, max_iter, cache_run, log); + newton_raphson_pf_solver_->set_iteration_state_capture(capture_jacobian_state, capture_nodal_state); + return newton_raphson_pf_solver_->run_power_flow(y_bus, input, err_tol, max_iter, cache_run, log); } SolverOutput run_power_flow_linear(PowerFlowInput const& input, double /* err_tol */, Idx /* max_iter */, diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp index eb71824344..e9cff07c2b 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver_dispatch.hpp @@ -31,7 +31,8 @@ template class MathSolverBase { virtual SolverOutput run_power_flow(PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, Logger& log, CalculationMethod calculation_method, - YBus const& y_bus, bool capture_jacobian_state) = 0; + YBus const& y_bus, bool capture_jacobian_state, + bool capture_nodal_state) = 0; virtual SolverOutput run_state_estimation(StateEstimationInput const& input, double err_tol, Idx max_iter, Logger& log, CalculationMethod calculation_method, YBus const& y_bus) = 0; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp index 167f6e17a5..4e651edc99 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp @@ -394,8 +394,9 @@ class NewtonRaphsonPFSolver : public IterativePFSolver ? 1 : 3; - nodal_state.voltage_magnitude.reserve(x_.size() * phases_per_bus); - nodal_state.voltage_angle.reserve(x_.size() * phases_per_bus); - for (auto& voltage : x_) { - if constexpr (is_symmetric_v) { - nodal_state.voltage_magnitude.push_back(voltage.v()); - nodal_state.voltage_angle.push_back(voltage.theta()); - } else { - for (Idx phase = 0; phase < 3; ++phase) { - nodal_state.voltage_magnitude.push_back(voltage.v().coeff(phase)); - nodal_state.voltage_angle.push_back(voltage.theta().coeff(phase)); - } - } - } constexpr Idx block_size = is_symmetric_v ? 1 : 9; for (auto& block : result.blocks) { block.clear(); @@ -446,19 +434,39 @@ class NewtonRaphsonPFSolver : public IterativePFSolver ? 1 : 3; + nodal_state.voltage_magnitude.reserve(x_.size() * phases_per_bus); + nodal_state.voltage_angle.reserve(x_.size() * phases_per_bus); + for (auto& voltage : x_) { + if constexpr (is_symmetric_v) { + nodal_state.voltage_magnitude.push_back(voltage.v()); + nodal_state.voltage_angle.push_back(voltage.theta()); + } else { + for (Idx phase = 0; phase < 3; ++phase) { + nodal_state.voltage_magnitude.push_back(voltage.v().coeff(phase)); + nodal_state.voltage_angle.push_back(voltage.theta().coeff(phase)); + } + } + } nodal_states_.push_back(std::move(nodal_state)); } private: // data for jacobian std::vector> data_jac_; bool capture_jacobian_state_{}; + bool capture_nodal_state_{}; std::vector jacobians_; std::vector nodal_states_; // calculation data diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/y_bus.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/y_bus.hpp index 65b8515004..c0ccd0b04f 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/y_bus.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/y_bus.hpp @@ -624,7 +624,6 @@ template class YBus { }; } // namespace math_solver - using math_solver::YBus; } // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h index cdc166c9f6..45631f55ba 100644 --- a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h +++ b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h @@ -123,6 +123,7 @@ PGM_API void PGM_calculate(PGM_Handle* handle, PGM_PowerGridModel* model, PGM_Op typedef struct PGM_StateOutputRequest { PGM_Idx y_bus; PGM_Idx jacobian; + PGM_Idx nodal_state; } PGM_StateOutputRequest; /** @@ -140,6 +141,7 @@ typedef struct PGM_StateScenarioView { PGM_Idx const* input_node_group; PGM_Idx const* input_node_bus; PGM_ID const* input_node_id; + PGM_Idx nodal_state_requested; } PGM_StateScenarioView; /** @@ -215,8 +217,7 @@ typedef struct PGM_StateNodalStateView { * PGM_destroy_state_output(). The component output dataset remains caller-owned. */ PGM_API void PGM_calculate_with_state(PGM_Handle* handle, PGM_PowerGridModel* model, PGM_Options const* opt, - PGM_MutableDataset const* output_dataset, - PGM_ConstDataset const* batch_dataset, + PGM_MutableDataset const* output_dataset, PGM_ConstDataset const* batch_dataset, PGM_StateOutputRequest const* requests, PGM_Idx request_count, PGM_StateOutput** state_output); diff --git a/power_grid_model_c/power_grid_model_c/src/model.cpp b/power_grid_model_c/power_grid_model_c/src/model.cpp index 0618c18a6c..eb0ad721d3 100644 --- a/power_grid_model_c/power_grid_model_c/src/model.cpp +++ b/power_grid_model_c/power_grid_model_c/src/model.cpp @@ -331,7 +331,7 @@ void calculate_multi_dimensional_impl(MainModel& model, MainModel::Options const // for dimension < 2 (one-time or 1D batch), call implementation directly if (auto const batch_dimension = get_batch_dimension(batch_dataset); batch_dimension < 2) { calculate_single_batch_dimension_impl(model, options, output_dataset, batch_dataset, state_requests, - state_outputs); + state_outputs); return; } @@ -357,10 +357,10 @@ void calculate_multi_dimensional_impl(MainModel& model, MainModel::Options const ? state_requests : state_requests.subspan(static_cast(i * stride_size), static_cast(stride_size)); - auto const sliced_state_outputs = state_outputs.empty() - ? state_outputs - : state_outputs.subspan(static_cast(i * stride_size), - static_cast(stride_size)); + auto const sliced_state_outputs = + state_outputs.empty() + ? state_outputs + : state_outputs.subspan(static_cast(i * stride_size), static_cast(stride_size)); // create a model copy MainModel local_model{model}; @@ -450,7 +450,8 @@ void PGM_calculate_with_state(PGM_Handle* handle, PGM_PowerGridModel* model, PGM converted_requests.reserve(static_cast(request_count)); for (Idx idx = 0; idx < request_count; ++idx) { converted_requests.push_back({.y_bus = requests[idx].y_bus != 0, - .jacobian = requests[idx].jacobian != 0}); + .jacobian = requests[idx].jacobian != 0, + .nodal_state = requests[idx].nodal_state != 0}); } auto result = std::make_unique(); result->scenarios.resize(static_cast(n_scenarios)); @@ -465,9 +466,8 @@ void PGM_calculate_with_state(PGM_Handle* handle, PGM_PowerGridModel* model, PGM } PGM_Idx PGM_state_output_scenario_count(PGM_Handle* handle, PGM_StateOutput const* state_output) { - return call_with_catch(handle, [state_output] { - return static_cast(safe_ptr_get(state_output).scenarios.size()); - }); + return call_with_catch( + handle, [state_output] { return static_cast(safe_ptr_get(state_output).scenarios.size()); }); } void PGM_state_output_get_scenario(PGM_Handle* handle, PGM_StateOutput const* state_output, PGM_Idx scenario_idx, @@ -483,6 +483,7 @@ void PGM_state_output_get_scenario(PGM_Handle* handle, PGM_StateOutput const* st output.has_state = 1; output.y_bus_requested = state.y_bus_requested; output.jacobian_requested = state.jacobian_requested; + output.nodal_state_requested = state.nodal_state_requested; output.n_groups = static_cast(state.groups.size()); output.n_input_nodes = static_cast(state.input_node_id.size()); output.input_node_group = state.input_node_group.data(); @@ -528,13 +529,13 @@ void PGM_state_output_get_group(PGM_Handle* handle, PGM_StateOutput const* state output.jacobian_row_indptr = structure.row_indptr_lu.data(); output.jacobian_col_indices = structure.col_indices_lu.data(); output.n_jacobians = static_cast(group.jacobians.size()); - output.n_nodal_states = static_cast(group.nodal_states.size()); } + output.n_nodal_states = static_cast(group.nodal_states.size()); }); } -PGM_Idx PGM_state_output_jacobians_count(PGM_Handle* handle, PGM_StateOutput const* state_output, - PGM_Idx scenario_idx, PGM_Idx group_idx) { +PGM_Idx PGM_state_output_jacobians_count(PGM_Handle* handle, PGM_StateOutput const* state_output, PGM_Idx scenario_idx, + PGM_Idx group_idx) { return call_with_catch(handle, [state_output, scenario_idx, group_idx] { auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); if (!scenario.has_value()) { @@ -565,7 +566,7 @@ void PGM_state_output_get_jacobian(PGM_Handle* handle, PGM_StateOutput const* st } PGM_Idx PGM_state_output_nodal_states_count(PGM_Handle* handle, PGM_StateOutput const* state_output, - PGM_Idx scenario_idx, PGM_Idx group_idx) { + PGM_Idx scenario_idx, PGM_Idx group_idx) { return call_with_catch(handle, [state_output, scenario_idx, group_idx] { auto const& scenario = safe_ptr_get(state_output).scenarios.at(static_cast(scenario_idx)); if (!scenario.has_value()) { @@ -582,8 +583,8 @@ void PGM_state_output_get_nodal_state(PGM_Handle* handle, PGM_StateOutput const* if (!scenario.has_value()) { throw DatasetError{"No model state was requested for this scenario.\n"}; } - auto const& nodal_state = scenario->groups.at(static_cast(group_idx)) - .nodal_states.at(static_cast(nodal_state_idx)); + auto const& nodal_state = + scenario->groups.at(static_cast(group_idx)).nodal_states.at(static_cast(nodal_state_idx)); auto& output = safe_ptr_get(view); output = {}; output.iteration = nodal_state.iteration; @@ -593,9 +594,7 @@ void PGM_state_output_get_nodal_state(PGM_Handle* handle, PGM_StateOutput const* }); } -void PGM_destroy_state_output(PGM_StateOutput* state_output) { - delete state_output; -} +void PGM_destroy_state_output(PGM_StateOutput* state_output) { delete state_output; } // destroy model void PGM_destroy_model(PGM_PowerGridModel* model) noexcept { destroy(cast_to_cpp(model)); } diff --git a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp index 056cb466c7..32618b4ca9 100644 --- a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp +++ b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp @@ -20,54 +20,53 @@ namespace power_grid_model_cpp { struct StateOutputRequest { - bool y_bus{}; - bool jacobian{}; + bool y_bus{}; + bool jacobian{}; + bool nodal_state{}; }; class StateOutput { - public: - StateOutput(StateOutput&&) noexcept = default; - StateOutput& operator=(StateOutput&&) noexcept = default; - StateOutput(StateOutput const&) = delete; - StateOutput& operator=(StateOutput const&) = delete; - - Idx scenario_count() const { return handle_.call_with(PGM_state_output_scenario_count, output_.get()); } - PGM_StateScenarioView scenario(Idx scenario_idx) const { - PGM_StateScenarioView view{}; - handle_.call_with(PGM_state_output_get_scenario, output_.get(), scenario_idx, &view); - return view; - } - PGM_StateGroupView group(Idx scenario_idx, Idx group_idx) const { - PGM_StateGroupView view{}; - handle_.call_with(PGM_state_output_get_group, output_.get(), scenario_idx, group_idx, &view); - return view; - } - Idx jacobians_count(Idx scenario_idx, Idx group_idx) const { - return handle_.call_with(PGM_state_output_jacobians_count, output_.get(), scenario_idx, - group_idx); - } - PGM_StateJacobianView jacobian(Idx scenario_idx, Idx group_idx, Idx jacobian_idx) const { - PGM_StateJacobianView view{}; - handle_.call_with(PGM_state_output_get_jacobian, output_.get(), scenario_idx, group_idx, jacobian_idx, - &view); - return view; - } - Idx nodal_states_count(Idx scenario_idx, Idx group_idx) const { - return handle_.call_with(PGM_state_output_nodal_states_count, output_.get(), scenario_idx, group_idx); - } - PGM_StateNodalStateView nodal_state(Idx scenario_idx, Idx group_idx, Idx nodal_state_idx) const { - PGM_StateNodalStateView view{}; - handle_.call_with(PGM_state_output_get_nodal_state, output_.get(), scenario_idx, group_idx, - nodal_state_idx, &view); - return view; - } + public: + StateOutput(StateOutput&&) noexcept = default; + StateOutput& operator=(StateOutput&&) noexcept = default; + StateOutput(StateOutput const&) = delete; + StateOutput& operator=(StateOutput const&) = delete; + + Idx scenario_count() const { return handle_.call_with(PGM_state_output_scenario_count, output_.get()); } + PGM_StateScenarioView scenario(Idx scenario_idx) const { + PGM_StateScenarioView view{}; + handle_.call_with(PGM_state_output_get_scenario, output_.get(), scenario_idx, &view); + return view; + } + PGM_StateGroupView group(Idx scenario_idx, Idx group_idx) const { + PGM_StateGroupView view{}; + handle_.call_with(PGM_state_output_get_group, output_.get(), scenario_idx, group_idx, &view); + return view; + } + Idx jacobians_count(Idx scenario_idx, Idx group_idx) const { + return handle_.call_with(PGM_state_output_jacobians_count, output_.get(), scenario_idx, group_idx); + } + PGM_StateJacobianView jacobian(Idx scenario_idx, Idx group_idx, Idx jacobian_idx) const { + PGM_StateJacobianView view{}; + handle_.call_with(PGM_state_output_get_jacobian, output_.get(), scenario_idx, group_idx, jacobian_idx, &view); + return view; + } + Idx nodal_states_count(Idx scenario_idx, Idx group_idx) const { + return handle_.call_with(PGM_state_output_nodal_states_count, output_.get(), scenario_idx, group_idx); + } + PGM_StateNodalStateView nodal_state(Idx scenario_idx, Idx group_idx, Idx nodal_state_idx) const { + PGM_StateNodalStateView view{}; + handle_.call_with(PGM_state_output_get_nodal_state, output_.get(), scenario_idx, group_idx, nodal_state_idx, + &view); + return view; + } - private: - friend class Model; - explicit StateOutput(RawStateOutput* output) : output_{output} {} + private: + friend class Model; + explicit StateOutput(RawStateOutput* output) : output_{output} {} - Handle handle_{}; - detail::UniquePtr output_; + Handle handle_{}; + detail::UniquePtr output_; }; class Model { @@ -138,19 +137,20 @@ class Model { void detach_all_loggers() const { handle_.unregister_all_loggers(); } private: - StateOutput calculate_with_state_impl(Options const& opt, DatasetMutable const& output_dataset, - RawConstDataset const* batch_dataset, - std::vector const& requests) { - std::vector raw_requests; - raw_requests.reserve(requests.size()); - for (auto const& request : requests) { - raw_requests.push_back({static_cast(request.y_bus), static_cast(request.jacobian)}); - } - RawStateOutput* state_output{}; - handle_.call_with(PGM_calculate_with_state, get(), opt.get(), output_dataset.get(), batch_dataset, - raw_requests.data(), static_cast(raw_requests.size()), &state_output); - return StateOutput{state_output}; + StateOutput calculate_with_state_impl(Options const& opt, DatasetMutable const& output_dataset, + RawConstDataset const* batch_dataset, + std::vector const& requests) { + std::vector raw_requests; + raw_requests.reserve(requests.size()); + for (auto const& request : requests) { + raw_requests.push_back({static_cast(request.y_bus), static_cast(request.jacobian), + static_cast(request.nodal_state)}); } + RawStateOutput* state_output{}; + handle_.call_with(PGM_calculate_with_state, get(), opt.get(), output_dataset.get(), batch_dataset, + raw_requests.data(), static_cast(raw_requests.size()), &state_output); + return StateOutput{state_output}; + } Handle handle_{}; detail::UniquePtr model_; diff --git a/src/power_grid_model/_core/model_state.py b/src/power_grid_model/_core/model_state.py index 24b9383bb7..cce65876ea 100644 --- a/src/power_grid_model/_core/model_state.py +++ b/src/power_grid_model/_core/model_state.py @@ -37,6 +37,7 @@ class ModelStateBusKind(IntEnum): class StateOutputRequest: y_bus: bool = False jacobian: bool = False + nodal_state: bool = False class _OwnedArray(np.ndarray): @@ -143,6 +144,7 @@ class ModelStateGroup: class ModelState: y_bus_requested: bool jacobian_requested: bool + nodal_state_requested: bool input_node_group: NDArray[np.int64] input_node_bus: NDArray[np.int64] input_node_id: NDArray[np.int32] @@ -220,6 +222,7 @@ def _build_scenario(owner: _NativeStateOutput, scenario_idx: int) -> ModelState return ModelState( y_bus_requested=bool(scenario_view.y_bus_requested), jacobian_requested=bool(scenario_view.jacobian_requested), + nodal_state_requested=bool(scenario_view.nodal_state_requested), input_node_group=input_node_group, input_node_bus=input_node_bus, input_node_id=input_node_id, diff --git a/src/power_grid_model/_core/power_grid_core.py b/src/power_grid_model/_core/power_grid_core.py index e3651161cc..06fc3b4fbe 100644 --- a/src/power_grid_model/_core/power_grid_core.py +++ b/src/power_grid_model/_core/power_grid_core.py @@ -138,7 +138,7 @@ class StateOutputPtr(c_void_p): class StateOutputRequestC(Structure): - _fields_ = [("y_bus", IdxC), ("jacobian", IdxC)] + _fields_ = [("y_bus", IdxC), ("jacobian", IdxC), ("nodal_state", IdxC)] class StateScenarioViewC(Structure): @@ -151,6 +151,7 @@ class StateScenarioViewC(Structure): ("input_node_group", IdxPtr), ("input_node_bus", IdxPtr), ("input_node_id", IDPtr), + ("nodal_state_requested", IdxC), ] diff --git a/src/power_grid_model/_core/power_grid_model.py b/src/power_grid_model/_core/power_grid_model.py index a299a921a6..3576d72759 100644 --- a/src/power_grid_model/_core/power_grid_model.py +++ b/src/power_grid_model/_core/power_grid_model.py @@ -72,7 +72,7 @@ def _prepare_state_requests( if request is False: return None if request is True: - return [StateOutputRequest(y_bus=True, jacobian=True) for _ in range(batch_size)] + return [StateOutputRequest(y_bus=True, jacobian=True, nodal_state=True) for _ in range(batch_size)] requests = list(request) if len(requests) != batch_size: raise ValueError(f"Expected {batch_size} model state requests, got {len(requests)}.") @@ -384,7 +384,10 @@ def _calculate_impl( # noqa: PLR0913 ) else: raw_requests = (StateOutputRequestC * batch_size)( - *(StateOutputRequestC(request.y_bus, request.jacobian) for request in state_requests) + *( + StateOutputRequestC(request.y_bus, request.jacobian, request.nodal_state) + for request in state_requests + ) ) get_pgc().calculate_with_state( self._model, diff --git a/tests/cpp_unit_tests/state_output/test_state_output.cpp b/tests/cpp_unit_tests/state_output/test_state_output.cpp index 45e7375060..591fcdb8a6 100644 --- a/tests/cpp_unit_tests/state_output/test_state_output.cpp +++ b/tests/cpp_unit_tests/state_output/test_state_output.cpp @@ -109,7 +109,7 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { model.set_logger(logger); auto options = MainModelOptions{}; options.calculation_method = CalculationMethod::newton_raphson; - auto state_output = calculate_state(model, options, {.jacobian = true}); + auto state_output = calculate_state(model, options, {.jacobian = true, .nodal_state = true}); REQUIRE(state_output.has_value()); REQUIRE(state_output->groups.size() == 1); @@ -127,8 +127,8 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { while (record_start != std::string::npos) { auto const record_end = report.find("\n[", record_start + jacobian_tag.size()); jacobian_logs.push_back(std::string_view{report}.substr( - record_start + jacobian_tag.size(), record_end == std::string::npos ? std::string::npos - : record_end - record_start - jacobian_tag.size())); + record_start + jacobian_tag.size(), + record_end == std::string::npos ? std::string::npos : record_end - record_start - jacobian_tag.size())); record_start = report.find(jacobian_tag, record_start + jacobian_tag.size()); } REQUIRE_FALSE(jacobian_logs.empty()); @@ -175,11 +175,11 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { CHECK(std::ranges::all_of(jacobian.blocks[0], [](double value) { return std::isfinite(value); })); if (iteration_idx == 0) { check_logged_values(jacobian_log, "row_indptr_lu", - std::vector(group.jacobian_structure->row_indptr_lu.begin(), - group.jacobian_structure->row_indptr_lu.end())); + std::vector(group.jacobian_structure->row_indptr_lu.begin(), + group.jacobian_structure->row_indptr_lu.end())); check_logged_values(jacobian_log, "col_indices_lu", - std::vector(group.jacobian_structure->col_indices_lu.begin(), - group.jacobian_structure->col_indices_lu.end())); + std::vector(group.jacobian_structure->col_indices_lu.begin(), + group.jacobian_structure->col_indices_lu.end())); } check_logged_values(jacobian_log, "jac_h", jacobian.blocks[0]); check_logged_values(jacobian_log, "jac_n", jacobian.blocks[1]); @@ -188,6 +188,29 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { } } +TEST_CASE("Test State output - Capture nodal states independently") { + MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; + auto model = make_state_test_model(dispatcher); + auto options = MainModelOptions{}; + options.calculation_method = CalculationMethod::newton_raphson; + auto state_output = calculate_state(model, options, {.nodal_state = true}); + + REQUIRE(state_output.has_value()); + CHECK_FALSE(state_output->jacobian_requested); + CHECK(state_output->nodal_state_requested); + REQUIRE(state_output->groups.size() == 1); + auto const& group = state_output->groups[0]; + CHECK(group.jacobians.empty()); + CHECK_FALSE(group.jacobian_structure.has_value()); + REQUIRE_FALSE(group.nodal_states.empty()); + for (size_t state_idx = 0; state_idx < group.nodal_states.size(); ++state_idx) { + auto const& nodal_state = group.nodal_states[state_idx]; + CHECK(nodal_state.iteration == static_cast(state_idx + 1)); + CHECK(nodal_state.voltage_magnitude.size() == static_cast(group.mapping.n_bus)); + CHECK(nodal_state.voltage_angle.size() == static_cast(group.mapping.n_bus)); + } +} + TEST_CASE("Test State output - Linear method has no Jacobian") { MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; auto model = make_state_test_model(dispatcher); diff --git a/tests/native_api_tests/test_api_model.cpp b/tests/native_api_tests/test_api_model.cpp index 010972e2d1..c2d34c7e5d 100644 --- a/tests/native_api_tests/test_api_model.cpp +++ b/tests/native_api_tests/test_api_model.cpp @@ -279,13 +279,14 @@ TEST_CASE("API Model") { check_common_node_results(); } - SUBCASE("Native model state") { - auto state_output = model.calculate_with_state(options, single_output_dataset, {{true, true}}); + SUBCASE("Native model state") { + auto state_output = model.calculate_with_state(options, single_output_dataset, {{true, true, true}}); REQUIRE(state_output.scenario_count() == 1); auto const scenario = state_output.scenario(0); CHECK(scenario.has_state == 1); CHECK(scenario.y_bus_requested == 1); CHECK(scenario.jacobian_requested == 1); + CHECK(scenario.nodal_state_requested == 1); REQUIRE(scenario.n_groups > 0); auto const group = state_output.group(0, 0); @@ -307,7 +308,7 @@ TEST_CASE("API Model") { CHECK(last_nodal_state.n_voltage_values == group.n_bus * (group.is_symmetric ? 1 : 3)); CHECK(group.jacobian_row_indptr[0] == 0); CHECK(group.jacobian_col_indices != nullptr); - } + } } SUBCASE("Test get indexer") { diff --git a/tests/unit/test_power_grid_model.py b/tests/unit/test_power_grid_model.py index d47f2cec79..811e1864d9 100644 --- a/tests/unit/test_power_grid_model.py +++ b/tests/unit/test_power_grid_model.py @@ -176,6 +176,7 @@ def test_power_flow_model_state(model: PowerGridModel): assert model_state is not None assert model_state.y_bus_requested assert model_state.jacobian_requested + assert model_state.nodal_state_requested assert model_state.input_node_id.tolist() == [0] assert len(model_state.groups) == 1 group = model_state.groups[0] @@ -212,6 +213,20 @@ def test_linear_power_flow_model_state_has_no_jacobian(model: PowerGridModel): assert model_state.groups[0].jacobian_structure is None +def test_power_flow_model_state_nodal_state_only(model: PowerGridModel): + _, model_state = model.calculate_power_flow(get_model_state=[StateOutputRequest(nodal_state=True)]) + + assert model_state is not None + assert not model_state.y_bus_requested + assert not model_state.jacobian_requested + assert model_state.nodal_state_requested + group = model_state.groups[0] + assert group.y_bus is None + assert group.jacobians == () + assert group.jacobian_structure is None + assert group.nodal_states + + def test_power_flow_model_state_batch_requests(model: PowerGridModel, update_batch): _, states = model.calculate_power_flow( update_data=update_batch, @@ -231,9 +246,10 @@ def test_power_flow_model_state_batch_requests(model: PowerGridModel, update_bat assert states[0].groups[0].jacobian_structure is None assert states[1] is not None assert states[1].jacobian_requested + assert not states[1].nodal_state_requested assert states[1].groups[0].y_bus is None assert states[1].groups[0].jacobians - assert states[1].groups[0].nodal_states + assert states[1].groups[0].nodal_states == () def test_power_flow_model_state_request_count_must_match_batch(model: PowerGridModel, update_batch): From 69c1ce43c26b7fed1ddc0c11d249163527e68538 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sat, 10 Oct 2026 21:33:06 +0200 Subject: [PATCH 09/25] iterartive current has no jacobian Signed-off-by: Jerry Jinfeng Guo --- .../math_solver/iterative_pf_solver.hpp | 42 ++++++++++++++++++- .../math_solver/math_solver.hpp | 29 +++++++++---- .../state_output/test_state_output.cpp | 29 ++++++++++++- 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_pf_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_pf_solver.hpp index e4801400a4..c303d2e27d 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_pf_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_pf_solver.hpp @@ -13,6 +13,7 @@ #include "y_bus.hpp" #include "../calculation_parameters.hpp" +#include "../calculation_state.hpp" #include "../common/common.hpp" #include "../common/enum.hpp" #include "../common/exception.hpp" @@ -30,8 +31,17 @@ namespace power_grid_model::math_solver { template class IterativePFSolver { public: friend DerivedSolver; - void log_matrix(Logger& /*log*/, YBus const& /*y_bus*/, Idx /*iter*/) {} - void copy_iteration_state_from(DerivedSolver const& /*solver*/) {} + void log_matrix(Logger& /*log*/, YBus const& /*y_bus*/, Idx /*iter*/) {} + void copy_iteration_state_from(DerivedSolver const& solver) { + nodal_states_ = static_cast(solver).nodal_states_; + } + + void set_nodal_state_capture(bool capture) { + capture_nodal_state_ = capture; + nodal_states_.clear(); + } + + std::vector get_nodal_states() const { return nodal_states_; } SolverOutput run_power_flow(YBus const& y_bus, PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, Logger& log) { @@ -75,6 +85,12 @@ template class IterativePFSolver { Timer const sub_timer{log, LogEvent::iterate_unknown}; max_dev = derived_solver.iterate_unknown(output.u, err_tol, cache_run); } + if (capture_nodal_state_) { + derived_solver.capture_iteration_nodal_state(output.u, num_iter); + } + log.log(LogEvent::iterate_unknown, [num_iter, max_dev] { + return std::format("Iteration {:3}: max voltage deviation = {:.6e} p.u.", num_iter, max_dev); + }); } // calculate math result @@ -123,11 +139,33 @@ template class IterativePFSolver { } private: + void capture_iteration_nodal_state(ComplexValueVector const& voltage, Idx iteration) { + ModelStateNodalState state; + state.iteration = iteration; + constexpr size_t phases_per_bus = is_symmetric_v ? 1 : 3; + state.voltage_magnitude.reserve(voltage.size() * phases_per_bus); + state.voltage_angle.reserve(voltage.size() * phases_per_bus); + for (auto const& bus_voltage : voltage) { + if constexpr (is_symmetric_v) { + state.voltage_magnitude.push_back(cabs(bus_voltage)); + state.voltage_angle.push_back(arg(bus_voltage)); + } else { + for (Idx phase = 0; phase < 3; ++phase) { + state.voltage_magnitude.push_back(cabs(bus_voltage.coeff(phase))); + state.voltage_angle.push_back(arg(bus_voltage.coeff(phase))); + } + } + } + nodal_states_.push_back(std::move(state)); + } + Idx n_bus_; std::reference_wrapper phase_shift_; std::reference_wrapper load_gens_per_bus_; std::reference_wrapper sources_per_bus_; std::reference_wrapper const> load_gen_type_; + bool capture_nodal_state_{}; + std::vector nodal_states_; IterativePFSolver(YBus const& y_bus, MathModelTopology const& topo) : n_bus_{y_bus.size()}, phase_shift_{std::cref(topo.phase_shift)}, diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp index 0e523a5b9f..6d88f1c806 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp @@ -46,6 +46,7 @@ template class MathSolver : public MathSolverBase { using enum CalculationMethod; last_power_flow_used_newton_raphson_ = false; + last_power_flow_used_iterative_current_ = false; // set method to always linear if all load_gens have const_y calculation_method = all_const_y_ ? linear : calculation_method; @@ -59,9 +60,13 @@ template class MathSolver : public MathSolverBase { case linear: return run_power_flow_linear(input, err_tol, max_iter, log, y_bus); case linear_current: - return run_power_flow_linear_current(input, err_tol, max_iter, cache_run, log, y_bus); + last_power_flow_used_iterative_current_ = true; + return run_power_flow_linear_current(input, err_tol, max_iter, cache_run, log, y_bus, + capture_nodal_state); case iterative_current: - return run_power_flow_iterative_current(input, err_tol, max_iter, cache_run, log, y_bus); + last_power_flow_used_iterative_current_ = true; + return run_power_flow_iterative_current(input, err_tol, max_iter, cache_run, log, y_bus, + capture_nodal_state); default: throw InvalidCalculationMethod{}; } @@ -123,10 +128,13 @@ template class MathSolver : public MathSolverBase { } std::vector get_nodal_states() const final { - if (!last_power_flow_used_newton_raphson_ || !newton_raphson_pf_solver_) { - return {}; + if (last_power_flow_used_newton_raphson_ && newton_raphson_pf_solver_) { + return newton_raphson_pf_solver_->get_nodal_states(); + } + if (last_power_flow_used_iterative_current_ && iterative_current_pf_solver_) { + return iterative_current_pf_solver_->get_nodal_states(); } - return newton_raphson_pf_solver_->get_nodal_states(); + return {}; } private: @@ -139,6 +147,7 @@ template class MathSolver : public MathSolverBase { std::optional> newton_raphson_se_solver_; std::optional> iec60909_sc_solver_; bool last_power_flow_used_newton_raphson_{false}; + bool last_power_flow_used_iterative_current_{false}; SolverOutput run_power_flow_newton_raphson(PowerFlowInput const& input, double err_tol, Idx max_iter, bool cache_run, Logger& log, YBus const& y_bus, @@ -161,19 +170,21 @@ template class MathSolver : public MathSolverBase { } SolverOutput run_power_flow_iterative_current(PowerFlowInput const& input, double err_tol, Idx max_iter, - bool cache_run, Logger& log, YBus const& y_bus) { + bool cache_run, Logger& log, YBus const& y_bus, + bool capture_nodal_state) { if (!iterative_current_pf_solver_.has_value()) { Timer const timer{log, LogEvent::create_math_solver}; iterative_current_pf_solver_.emplace(y_bus, *topo_ptr_); } - return iterative_current_pf_solver_.value().run_power_flow(y_bus, input, err_tol, max_iter, cache_run, log); + iterative_current_pf_solver_->set_nodal_state_capture(capture_nodal_state); + return iterative_current_pf_solver_->run_power_flow(y_bus, input, err_tol, max_iter, cache_run, log); } SolverOutput run_power_flow_linear_current(PowerFlowInput const& input, double /* err_tol */, Idx /* max_iter */, bool cache_run, Logger& log, - YBus const& y_bus) { + YBus const& y_bus, bool capture_nodal_state) { return run_power_flow_iterative_current(input, std::numeric_limits::infinity(), 1, cache_run, log, - y_bus); + y_bus, capture_nodal_state); } SolverOutput run_state_estimation_iterative_linear(StateEstimationInput const& input, double err_tol, diff --git a/tests/cpp_unit_tests/state_output/test_state_output.cpp b/tests/cpp_unit_tests/state_output/test_state_output.cpp index 591fcdb8a6..700f404f95 100644 --- a/tests/cpp_unit_tests/state_output/test_state_output.cpp +++ b/tests/cpp_unit_tests/state_output/test_state_output.cpp @@ -211,18 +211,43 @@ TEST_CASE("Test State output - Capture nodal states independently") { } } +TEST_CASE("Test State output - Iterative current captures nodal states without Jacobians") { + MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; + auto model = make_state_test_model(dispatcher); + auto options = MainModelOptions{}; + options.calculation_method = CalculationMethod::iterative_current; + auto state_output = calculate_state(model, options, {.nodal_state = true}); + + REQUIRE(state_output.has_value()); + CHECK_FALSE(state_output->jacobian_requested); + CHECK(state_output->nodal_state_requested); + REQUIRE(state_output->groups.size() == 1); + auto const& group = state_output->groups[0]; + CHECK(group.jacobians.empty()); + CHECK_FALSE(group.jacobian_structure.has_value()); + REQUIRE(group.nodal_states.size() > 1); + for (size_t state_idx = 0; state_idx < group.nodal_states.size(); ++state_idx) { + auto const& nodal_state = group.nodal_states[state_idx]; + CHECK(nodal_state.iteration == static_cast(state_idx + 1)); + REQUIRE(nodal_state.voltage_magnitude.size() == static_cast(group.mapping.n_bus)); + REQUIRE(nodal_state.voltage_angle.size() == static_cast(group.mapping.n_bus)); + CHECK(std::ranges::all_of(nodal_state.voltage_magnitude, [](double value) { return std::isfinite(value); })); + CHECK(std::ranges::all_of(nodal_state.voltage_angle, [](double value) { return std::isfinite(value); })); + } +} + TEST_CASE("Test State output - Linear method has no Jacobian") { MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; auto model = make_state_test_model(dispatcher); auto options = MainModelOptions{}; options.calculation_method = CalculationMethod::linear; - auto state_output = calculate_state(model, options, {.y_bus = true, .jacobian = true}); + auto state_output = calculate_state(model, options, {.y_bus = true, .jacobian = true, .nodal_state = true}); REQUIRE(state_output.has_value()); REQUIRE(state_output->groups.size() == 1); CHECK(state_output->groups[0].y_bus.has_value()); CHECK(state_output->groups[0].jacobians.empty()); - CHECK(state_output->groups[0].nodal_states.empty()); + CHECK(state_output->groups[0].nodal_states.empty()); // linear method has only one iteration CHECK_FALSE(state_output->groups[0].jacobian_structure.has_value()); } From c4f0b189f9e0c250c178d99ee408ac9bc5712f15 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 13:13:02 +0200 Subject: [PATCH 10/25] update ajcobian member namign and example notebook Signed-off-by: Jerry Jinfeng Guo --- docs/examples/Power Flow Example.ipynb | 465 ++++++++++++++++++---- src/power_grid_model/_core/model_state.py | 4 +- tests/unit/test_power_grid_model.py | 1 + 3 files changed, 387 insertions(+), 83 deletions(-) diff --git a/docs/examples/Power Flow Example.ipynb b/docs/examples/Power Flow Example.ipynb index 5dbf531c03..e988559b28 100644 --- a/docs/examples/Power Flow Example.ipynb +++ b/docs/examples/Power Flow Example.ipynb @@ -79,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "6f008736", "metadata": {}, "outputs": [], @@ -141,7 +141,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "1034f6f3", "metadata": {}, "outputs": [], @@ -179,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "f0c8c3e8", "metadata": {}, "outputs": [ @@ -220,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "40509eaf", "metadata": {}, "outputs": [], @@ -253,7 +253,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 3, "id": "7ef134e9", "metadata": {}, "outputs": [], @@ -290,7 +290,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "44c2de63", "metadata": {}, "outputs": [], @@ -313,7 +313,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "a581a36e", "metadata": {}, "outputs": [ @@ -358,7 +358,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, "id": "ca42da43", "metadata": {}, "outputs": [ @@ -368,49 +368,337 @@ "text": [ "Input node IDs: [1 2 6]\n", "Node groups: [0 0 0]\n", - "Node bus positions: [2 1 0]\n", - "Y-bus row pointers: [0 3 6 9]\n", - "Y-bus column indices: [0 1 2 0 1 2 0 1 2]\n", - "Y-bus values: [ 537.80487805-429.89754185j -268.90243902+215.12195122j\n", - " -268.90243902+215.12195122j -268.90243902+215.12195122j\n", - " 537.80487805-429.89754185j -268.90243902+215.12195122j\n", - " -268.90243902+215.12195122j -268.90243902+215.12195122j\n", - " 537.80487805-429.89754185j]\n", + "Node bus positions: [2 1 0]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
admittance
rowcolumn
00537.804878-429.897542j
1-268.902439+215.121951j
2-268.902439+215.121951j
10-268.902439+215.121951j
1537.804878-429.897542j
2-268.902439+215.121951j
20-268.902439+215.121951j
1-268.902439+215.121951j
2537.804878-429.897542j
\n", + "
" + ], + "text/plain": [ + " admittance\n", + "row column \n", + "0 0 537.804878-429.897542j\n", + " 1 -268.902439+215.121951j\n", + " 2 -268.902439+215.121951j\n", + "1 0 -268.902439+215.121951j\n", + " 1 537.804878-429.897542j\n", + " 2 -268.902439+215.121951j\n", + "2 0 -268.902439+215.121951j\n", + " 1 -268.902439+215.121951j\n", + " 2 537.804878-429.897542j" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "Captured Jacobian iterations: [1, 2, 3]\n", + "Jacobian structure (last iteration):\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
HNML
rowcolumn
00399.925901487.808128-507.808128395.925901
1-198.076842-245.507506245.507506-198.076842
2-201.849058-262.300622262.300622-201.849058
10-196.039235-247.137591247.137591-196.039235
1394.721173467.543956-507.543955384.721173
2-198.681938-260.406364260.406364-198.681938
20-211.594716-254.504096254.504096-211.594716
1-210.441881-250.998409250.998409-210.441881
210359.2724711529.741470-1529.74147010359.272471
\n", + "
" + ], + "text/plain": [ + " H N M L\n", + "row column \n", + "0 0 399.925901 487.808128 -507.808128 395.925901\n", + " 1 -198.076842 -245.507506 245.507506 -198.076842\n", + " 2 -201.849058 -262.300622 262.300622 -201.849058\n", + "1 0 -196.039235 -247.137591 247.137591 -196.039235\n", + " 1 394.721173 467.543956 -507.543955 384.721173\n", + " 2 -198.681938 -260.406364 260.406364 -198.681938\n", + "2 0 -211.594716 -254.504096 254.504096 -211.594716\n", + " 1 -210.441881 -250.998409 250.998409 -210.441881\n", + " 2 10359.272471 1529.741470 -1529.741470 10359.272471" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "Captured nodal-state iterations: [1, 2, 3]\n", - "Shared Jacobian row pointers: [0 3 6 9]\n", - "Shared Jacobian column indices: [0 1 2 0 1 2 0 1 2]\n", - "Final Jacobian H block: [ 399.92590067 -198.07684237 -201.84905831 -196.03923548\n", - " 394.72117311 -198.68193762 -211.59471597 -210.44188084\n", - " 10359.27247084]\n", - "Final Jacobian N block: [ 487.80812795 -245.5075059 -262.30062199 -247.13759141 467.54395568\n", - " -260.40636383 -254.50409586 -250.99840925 1529.74147013]\n", - "Final Jacobian M block: [ -507.8081279 245.5075059 262.30062199 247.13759141\n", - " -507.54395524 260.40636383 254.50409586 250.99840925\n", - " -1529.74147022]\n", - "Final Jacobian L block: [ 395.92590076 -198.07684237 -201.84905831 -196.03923548\n", - " 384.72117318 -198.68193762 -211.59471597 -210.44188084\n", - " 10359.27247102]\n", - "Nodal voltage evolution by iteration:\n", - "Group 0, iteration 1:\n", - " Nodal voltage magnitudes: [0.96523561 0.95632505 0.99908707]\n", - " Nodal voltage angles: [-0.02020439 -0.02387315 -0.00278854]\n", - " node 1: magnitude=[0.99908707], angle=[-0.00278854]\n", - " node 2: magnitude=[0.95632505], angle=[-0.02387315]\n", - " node 6: magnitude=[0.96523561], angle=[-0.02020439]\n", - "Group 0, iteration 2:\n", - " Nodal voltage magnitudes: [0.96211126 0.9521469 0.998989 ]\n", - " Nodal voltage angles: [-0.02188866 -0.02602094 -0.00303857]\n", - " node 1: magnitude=[0.998989], angle=[-0.00303857]\n", - " node 2: magnitude=[0.9521469], angle=[-0.02602094]\n", - " node 6: magnitude=[0.96211126], angle=[-0.02188866]\n", - "Group 0, iteration 3:\n", - " Nodal voltage magnitudes: [0.96209647 0.95212621 0.9989881 ]\n", - " Nodal voltage angles: [-0.02189476 -0.02603079 -0.00303947]\n", - " node 1: magnitude=[0.9989881], angle=[-0.00303947]\n", - " node 2: magnitude=[0.95212621], angle=[-0.02603079]\n", - " node 6: magnitude=[0.96209647], angle=[-0.02189476]\n" + "Voltage history, solver group 0:\n" ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
|V|, iteration 1 (p.u.)angle, iteration 1 (rad)|V|, iteration 2 (p.u.)angle, iteration 2 (rad)|V|, iteration 3 (p.u.)angle, iteration 3 (rad)
nodephase
1symmetric0.999087-0.0027890.998989-0.0030390.998988-0.003039
2symmetric0.956325-0.0238730.952147-0.0260210.952126-0.026031
6symmetric0.965236-0.0202040.962111-0.0218890.962096-0.021895
\n", + "
" + ], + "text/plain": [ + " |V|, iteration 1 (p.u.) angle, iteration 1 (rad) \\\n", + "node phase \n", + "1 symmetric 0.999087 -0.002789 \n", + "2 symmetric 0.956325 -0.023873 \n", + "6 symmetric 0.965236 -0.020204 \n", + "\n", + " |V|, iteration 2 (p.u.) angle, iteration 2 (rad) \\\n", + "node phase \n", + "1 symmetric 0.998989 -0.003039 \n", + "2 symmetric 0.952147 -0.026021 \n", + "6 symmetric 0.962111 -0.021889 \n", + "\n", + " |V|, iteration 3 (p.u.) angle, iteration 3 (rad) \n", + "node phase \n", + "1 symmetric 0.998988 -0.003039 \n", + "2 symmetric 0.952126 -0.026031 \n", + "6 symmetric 0.962096 -0.021895 " + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -421,48 +709,63 @@ "print(\"Node groups:\", model_state.input_node_group)\n", "print(\"Node bus positions:\", model_state.input_node_bus)\n", "\n", - "# Matrices are stored in CSR form, with one state entry per solver group.\n", + "# Show the sparse Y-bus as row/column/value entries.\n", "group_state = model_state.groups[0]\n", "y_bus = group_state.y_bus\n", - "print(\"Y-bus row pointers:\", y_bus.row_indptr)\n", - "print(\"Y-bus column indices:\", y_bus.col_indices)\n", - "print(\"Y-bus values:\", y_bus.admittance_real + 1j * y_bus.admittance_imag)\n", + "y_bus_rows = np.repeat(np.arange(y_bus.row_indptr.size - 1), np.diff(y_bus.row_indptr))\n", + "y_bus_df = pd.DataFrame(\n", + " {\n", + " \"row\": y_bus_rows,\n", + " \"column\": y_bus.col_indices,\n", + " \"admittance\": y_bus.admittance_real + 1j * y_bus.admittance_imag,\n", + " }\n", + ").set_index([\"row\", \"column\"])\n", + "display(y_bus_df)\n", "\n", "jacobians = group_state.jacobians\n", "nodal_states = group_state.nodal_states\n", "print(\"Captured Jacobian iterations:\", [jac.iteration for jac in jacobians])\n", - "print(\"Captured nodal-state iterations:\", [state.iteration for state in nodal_states])\n", "if jacobians:\n", " jacobian = jacobians[-1]\n", - " print(\"Shared Jacobian row pointers:\", group_state.jacobian_structure.row_indptr_lu)\n", - " print(\"Shared Jacobian column indices:\", group_state.jacobian_structure.col_indices_lu)\n", - " print(\"Final Jacobian H block:\", jacobian.h)\n", - " print(\"Final Jacobian N block:\", jacobian.n)\n", - " print(\"Final Jacobian M block:\", jacobian.m)\n", - " print(\"Final Jacobian L block:\", jacobian.l_block)\n", - "\n", - " print(\"Nodal voltage evolution by iteration:\")\n", - " for input_idx, node_id in enumerate(model_state.input_node_id):\n", - " if model_state.input_node_bus[input_idx] < 0:\n", - " print(f\" node {node_id}: disconnected\")\n", + " pattern = group_state.jacobian_structure\n", + " jacobian_rows = np.repeat(np.arange(pattern.row_indptr_lu.size - 1), np.diff(pattern.row_indptr_lu))\n", + " jacobian_df = pd.DataFrame(\n", + " {\n", + " \"row\": jacobian_rows,\n", + " \"column\": pattern.col_indices_lu,\n", + " \"H\": jacobian.h,\n", + " \"N\": jacobian.n,\n", + " \"M\": jacobian.m,\n", + " \"L\": jacobian.l,\n", + " }\n", + " ).set_index([\"row\", \"column\"])\n", + " print(\"Jacobian structure (last iteration):\")\n", + " display(jacobian_df)\n", + "\n", + "print(\"Captured nodal-state iterations:\", [state.iteration for state in nodal_states])\n", + "if nodal_states:\n", " for group_idx, node_group in enumerate(model_state.groups):\n", - " for node_state in node_group.nodal_states:\n", - " print(f\"Group {group_idx}, iteration {node_state.iteration}:\")\n", - " print(\" Nodal voltage magnitudes:\", node_state.voltage_magnitude)\n", - " print(\" Nodal voltage angles:\", node_state.voltage_angle)\n", - " for input_idx, node_id in enumerate(model_state.input_node_id):\n", - " if model_state.input_node_group[input_idx] != group_idx:\n", - " continue\n", - " bus_idx = int(model_state.input_node_bus[input_idx])\n", - " if bus_idx < 0:\n", - " continue\n", - " phases_per_bus = 1 if node_group.mapping.is_symmetric else 3\n", - " phase_start = bus_idx * phases_per_bus\n", - " phase_stop = phase_start + phases_per_bus\n", - " print(\n", - " f\" node {node_id}: magnitude={node_state.voltage_magnitude[phase_start:phase_stop]}, \"\n", - " f\"angle={node_state.voltage_angle[phase_start:phase_stop]}\"\n", - " )" + " phases_per_bus = 1 if node_group.mapping.is_symmetric else 3\n", + " phases = [\"symmetric\"] if phases_per_bus == 1 else [\"phase 1\", \"phase 2\", \"phase 3\"]\n", + " voltage_rows = []\n", + " for input_idx, node_id in enumerate(model_state.input_node_id):\n", + " if model_state.input_node_group[input_idx] != group_idx:\n", + " continue\n", + " bus_idx = int(model_state.input_node_bus[input_idx])\n", + " if bus_idx < 0:\n", + " print(f\"Node {node_id}: disconnected\")\n", + " continue\n", + " for phase_idx, phase in enumerate(phases):\n", + " value_idx = bus_idx * phases_per_bus + phase_idx\n", + " row = {\"node\": node_id, \"phase\": phase}\n", + " for node_state in node_group.nodal_states:\n", + " row[f\"|V|, iteration {node_state.iteration} (p.u.)\"] = node_state.voltage_magnitude[value_idx]\n", + " row[f\"angle, iteration {node_state.iteration} (rad)\"] = node_state.voltage_angle[value_idx]\n", + " voltage_rows.append(row)\n", + " if voltage_rows:\n", + " print(f\"Voltage history, solver group {group_idx}:\")\n", + " voltage_df = pd.DataFrame(voltage_rows).set_index([\"node\", \"phase\"])\n", + " display(voltage_df)" ] }, { diff --git a/src/power_grid_model/_core/model_state.py b/src/power_grid_model/_core/model_state.py index cce65876ea..57d7758e0b 100644 --- a/src/power_grid_model/_core/model_state.py +++ b/src/power_grid_model/_core/model_state.py @@ -85,7 +85,7 @@ class ModelStateJacobian: h: NDArray[np.float64] n: NDArray[np.float64] m: NDArray[np.float64] - l_block: NDArray[np.float64] + l: NDArray[np.float64] # noqa: E741 iteration: int = 0 @@ -106,7 +106,7 @@ def _build_jacobian( h=_array(view.jacobian_h, view.n_jacobian_values, np.float64, owner), n=_array(view.jacobian_n, view.n_jacobian_values, np.float64, owner), m=_array(view.jacobian_m, view.n_jacobian_values, np.float64, owner), - l_block=_array(view.jacobian_l, view.n_jacobian_values, np.float64, owner), + l=_array(view.jacobian_l, view.n_jacobian_values, np.float64, owner), iteration=view.iteration, ) diff --git a/tests/unit/test_power_grid_model.py b/tests/unit/test_power_grid_model.py index 811e1864d9..ca45a6686c 100644 --- a/tests/unit/test_power_grid_model.py +++ b/tests/unit/test_power_grid_model.py @@ -191,6 +191,7 @@ def test_power_flow_model_state(model: PowerGridModel): assert group.y_bus.row_indptr.shape == (group.mapping.n_bus + 1,) assert group.y_bus.admittance_real.size > 0 assert group.jacobians[-1].h.size == group.jacobians[-1].n.size + assert group.jacobians[-1].l.size == group.jacobians[-1].h.size n_voltage_values = group.mapping.n_bus * (1 if group.mapping.is_symmetric else 3) assert group.nodal_states[-1].voltage_magnitude.size == n_voltage_values assert group.nodal_states[-1].voltage_angle.size == n_voltage_values From 6fa88669b244ccdd6a9abadacb21fb07c18835dd Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 13:19:18 +0200 Subject: [PATCH 11/25] documentation Signed-off-by: Jerry Jinfeng Guo --- docs/algorithms/pf-algorithms.md | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/algorithms/pf-algorithms.md b/docs/algorithms/pf-algorithms.md index 3dda2cb8b0..ab54f99649 100644 --- a/docs/algorithms/pf-algorithms.md +++ b/docs/algorithms/pf-algorithms.md @@ -234,3 +234,39 @@ They are most accurate when the actual voltages are close to 1 p.u. and the erro level. When we approximate the load as impedance at 1 p.u., the voltage error has quadratic relation to the actual voltage. When it is approximated as a current at 1 p.u., the voltage error is only linearly dependent in comparison. + +## Retrieving calculation state + +`PowerGridModel.calculate_power_flow` can return solver state together with the usual result data. Set +`get_model_state=True` to request the Y-bus, Jacobians, and nodal states: + +```python +output_data, model_state = model.calculate_power_flow(get_model_state=True) + +group = model_state.groups[0] +y_bus = group.y_bus +jacobians = group.jacobians +nodal_states = group.nodal_states +``` + +State is organized by solver group. The group mapping relates input node IDs to the solver's internal bus order. The +Y-bus is stored in CSR form; its row pointers and column indices define the positions of its admittance values. +`jacobian_structure` similarly provides the shared sparse pattern for the Jacobian blocks. Jacobians and nodal states +are sequences indexed by iteration; nodal voltage arrays are bus-major, with three phase values per bus for asymmetric +calculations. + +To request only selected state, pass a `StateOutputRequest` for each scenario instead of `True`: + +```python +from power_grid_model import StateOutputRequest + +output_data, model_state = model.calculate_power_flow( + get_model_state=[StateOutputRequest(nodal_state=True)] +) +``` + +The request flags select which data to capture, but not every method produces every kind of state. Newton-Raphson +provides Jacobians and nodal states per iteration. Iterative Current provides nodal states but no Jacobians. The linear +method has no iteration history, so Jacobians and nodal states are empty even when requested. For batch calculations, +`model_state` contains one state (or `None` for a failed scenario) per flattened scenario, and the request sequence +must have the same length. From 429138d4d4a2915535f6bf785ac2d51e4a7d4397 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 13:34:51 +0200 Subject: [PATCH 12/25] finalize unit test values Signed-off-by: Jerry Jinfeng Guo --- .../state_output/test_state_output.cpp | 76 +++++-------------- 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/tests/cpp_unit_tests/state_output/test_state_output.cpp b/tests/cpp_unit_tests/state_output/test_state_output.cpp index 700f404f95..766363ec5d 100644 --- a/tests/cpp_unit_tests/state_output/test_state_output.cpp +++ b/tests/cpp_unit_tests/state_output/test_state_output.cpp @@ -7,8 +7,6 @@ #include #include #include -#include -#include #include #include @@ -17,8 +15,6 @@ #include #include #include -#include -#include #include namespace power_grid_model { @@ -105,8 +101,6 @@ TEST_CASE("Test State output - Capture Y-bus and input-node mapping") { TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { MathSolverDispatcher const dispatcher{math_solver::math_solver_tag{}}; auto model = make_state_test_model(dispatcher); - MultiThreadedTextLogger logger; - model.set_logger(logger); auto options = MainModelOptions{}; options.calculation_method = CalculationMethod::newton_raphson; auto state_output = calculate_state(model, options, {.jacobian = true, .nodal_state = true}); @@ -115,51 +109,22 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { REQUIRE(state_output->groups.size() == 1); auto const& group = state_output->groups[0]; CHECK_FALSE(group.y_bus.has_value()); - REQUIRE_FALSE(group.jacobians.empty()); + constexpr std::array, 5> expected_jacobian_blocks{{ + {0.00066666666666666664, 0.0, 0.0, 0.00055555555555555545}, + {0.00053333333333333336, 0.0, 0.0, 0.00030222222222222217}, + {0.00050196078431372548, 0.0, 0.0, 0.00025294886582083806}, + {0.00050000762951094848, 0.0, 0.0, 0.00025001144438284152}, + {0.00050000000011641535, 0.0, 0.0, 0.00025000000017462302}, + }}; + REQUIRE(group.mapping.n_bus == 1); + REQUIRE(group.jacobians.size() == expected_jacobian_blocks.size()); REQUIRE(group.jacobian_structure.has_value()); - CHECK(group.jacobian_structure->row_indptr_lu.size() == static_cast(group.mapping.n_bus + 1)); - CHECK_FALSE(group.jacobian_structure->col_indices_lu.empty()); - - auto const report = logger.report(); - constexpr std::string_view jacobian_tag{"Tag:4001: "}; - std::vector jacobian_logs; - auto record_start = report.find(jacobian_tag); - while (record_start != std::string::npos) { - auto const record_end = report.find("\n[", record_start + jacobian_tag.size()); - jacobian_logs.push_back(std::string_view{report}.substr( - record_start + jacobian_tag.size(), - record_end == std::string::npos ? std::string::npos : record_end - record_start - jacobian_tag.size())); - record_start = report.find(jacobian_tag, record_start + jacobian_tag.size()); - } - REQUIRE_FALSE(jacobian_logs.empty()); - CHECK(jacobian_logs.size() > 1); - REQUIRE(group.jacobians.size() == jacobian_logs.size()); + CHECK(group.jacobian_structure->row_indptr_lu == IdxVector{0, 1}); + CHECK(group.jacobian_structure->col_indices_lu == IdxVector{0}); REQUIRE(group.nodal_states.size() == group.jacobians.size()); - auto parse_values = [](std::string_view jacobian_log, std::string_view field) { - auto const field_start = jacobian_log.find(std::string{field} + "="); - REQUIRE(field_start != std::string_view::npos); - auto const value_start = field_start + field.size() + 1; - auto const line_end = jacobian_log.find('\n', value_start); - std::istringstream stream{std::string{jacobian_log.substr(value_start, line_end - value_start)}}; - std::vector values; - double value{}; - while (stream >> value) { - values.push_back(value); - } - return values; - }; - auto check_logged_values = [&parse_values](std::string_view jacobian_log, std::string_view field, - std::vector const& expected) { - auto const logged = parse_values(jacobian_log, field); - REQUIRE(logged.size() == expected.size()); - for (size_t i = 0; i < expected.size(); ++i) { - CHECK(logged[i] == doctest::Approx(expected[i]).epsilon(1e-5)); - } - }; - for (size_t iteration_idx = 0; iteration_idx < jacobian_logs.size(); ++iteration_idx) { + for (size_t iteration_idx = 0; iteration_idx < group.jacobians.size(); ++iteration_idx) { auto const& jacobian = group.jacobians[iteration_idx]; auto const& nodal_state = group.nodal_states[iteration_idx]; - auto const jacobian_log = jacobian_logs[iteration_idx]; CHECK(jacobian.iteration == static_cast(iteration_idx + 1)); CHECK(nodal_state.iteration == jacobian.iteration); auto const phases_per_bus = group.mapping.is_symmetric ? 1 : 3; @@ -172,19 +137,12 @@ TEST_CASE("Test State output - Capture Newton-Raphson Jacobian") { CHECK(jacobian.blocks[0].size() == jacobian.blocks[2].size()); CHECK(jacobian.blocks[0].size() == jacobian.blocks[3].size()); CHECK(std::ranges::any_of(jacobian.blocks[0], [](double value) { return value != 0.0; })); - CHECK(std::ranges::all_of(jacobian.blocks[0], [](double value) { return std::isfinite(value); })); - if (iteration_idx == 0) { - check_logged_values(jacobian_log, "row_indptr_lu", - std::vector(group.jacobian_structure->row_indptr_lu.begin(), - group.jacobian_structure->row_indptr_lu.end())); - check_logged_values(jacobian_log, "col_indices_lu", - std::vector(group.jacobian_structure->col_indices_lu.begin(), - group.jacobian_structure->col_indices_lu.end())); + for (size_t block_idx = 0; block_idx < jacobian.blocks.size(); ++block_idx) { + auto const& block = jacobian.blocks[block_idx]; + REQUIRE(block.size() == 1); + CHECK(std::isfinite(block[0])); + CHECK(block[0] == doctest::Approx(expected_jacobian_blocks[iteration_idx][block_idx]).epsilon(1e-5)); } - check_logged_values(jacobian_log, "jac_h", jacobian.blocks[0]); - check_logged_values(jacobian_log, "jac_n", jacobian.blocks[1]); - check_logged_values(jacobian_log, "jac_m", jacobian.blocks[2]); - check_logged_values(jacobian_log, "jac_l", jacobian.blocks[3]); } } From a9dcc05c8c685ea29f2db143bda80410f5ab19f5 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 14:13:39 +0200 Subject: [PATCH 13/25] linux werror initializers Signed-off-by: Jerry Jinfeng Guo --- .../include/power_grid_model/main_model_impl.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp index 9078f3c99a..ea79eef601 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp @@ -354,6 +354,8 @@ class MainModelImpl { .n_bus = n_bus, .is_symmetric = is_symmetric_v, .bus_user_indptr = IdxVector(static_cast(n_bus + 1), 0), + .bus_user_sequence = {}, + .bus_user_id = {}, .bus_kind = std::vector(static_cast(n_bus), ModelStateBusKind::synthetic_branch3), .origin_branch3_id = std::vector(static_cast(n_bus), static_cast(-1)), @@ -395,6 +397,8 @@ class MainModelImpl { ModelStateYBus state{ .row_indptr = y_bus.row_indptr(), .col_indices = y_bus.col_indices(), + .admittance_real = {}, + .admittance_imag = {}, }; auto const& admittance = y_bus.admittance(); constexpr Idx block_size = is_symmetric_v ? 1 : 9; From af4a716d444cfe7e81ad6dd00b36dae1c0eab40d Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 14:16:00 +0200 Subject: [PATCH 14/25] mypy complaints Signed-off-by: Jerry Jinfeng Guo --- src/power_grid_model/_core/power_grid_core.py | 2 +- src/power_grid_model/_core/power_grid_model.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/power_grid_model/_core/power_grid_core.py b/src/power_grid_model/_core/power_grid_core.py index 06fc3b4fbe..dfa02226fa 100644 --- a/src/power_grid_model/_core/power_grid_core.py +++ b/src/power_grid_model/_core/power_grid_core.py @@ -553,7 +553,7 @@ def state_output_get_jacobian( pass # pragma: no cover @make_c_binding - def state_output_nodal_states_count(self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int) -> int: + def state_output_nodal_states_count(self, state_output: StateOutputPtr, scenario_idx: int, group_idx: int) -> int: # type: ignore[empty-body] pass # pragma: no cover @make_c_binding diff --git a/src/power_grid_model/_core/power_grid_model.py b/src/power_grid_model/_core/power_grid_model.py index 3576d72759..171a828f12 100644 --- a/src/power_grid_model/_core/power_grid_model.py +++ b/src/power_grid_model/_core/power_grid_model.py @@ -624,6 +624,23 @@ def calculate_power_flow( calculation_initialization: CalculationInitialization | str = ..., ) -> DenseBatchOutputDataset: ... @overload + def calculate_power_flow( + self, + *, + get_model_state: Literal[True] | Sequence[StateOutputRequest], + symmetric: bool = True, + error_tolerance: float = 1e-8, + max_iterations: int = 20, + calculation_method: CalculationMethod | str = CalculationMethod.newton_raphson, + update_data: None = None, + threading: int = -1, + output_component_types: ComponentAttributeMapping = None, + continue_on_batch_error: bool = False, + decode_error: bool = True, + tap_changing_strategy: TapChangingStrategy | str = TapChangingStrategy.disabled, + calculation_initialization: CalculationInitialization | str = CalculationInitialization.default, + ) -> tuple[Dataset, ModelState | None]: ... + @overload def calculate_power_flow( self, *, From ed3b60e097b58b41372b074ad31e78b74d85e0ea Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 14:23:20 +0200 Subject: [PATCH 15/25] more initializers Signed-off-by: Jerry Jinfeng Guo --- .../include/power_grid_model/optimizer/base_optimizer.hpp | 3 ++- .../power_grid_model/optimizer/tap_position_optimizer.hpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp index b5083ea936..9b9122a125 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp @@ -84,7 +84,8 @@ class NoOptimizer : public detail::BaseOptimizer { NoOptimizer(Calculator func) : func_{std::move(func)} {} auto optimize(State const& state, CalculationMethod method) -> MathOutput final { - return {.solver_output = func_(state, method), .optimizer_output = {}, .supernode_output = {}}; + return { + .solver_output = func_(state, method), .optimizer_output = {}, .supernode_output = {}, .model_state = {}}; } private: diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp index ec0a58f859..294fc43195 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp @@ -1043,7 +1043,8 @@ class TapPositionOptimizerImpl, StateCalculator, return {.solver_output = {std::move(solver_output)}, .optimizer_output = {std::move(transformer_tap_positions)}, - .supernode_output = {}}; + .supernode_output = {}, + .model_state = {}}; } auto iterate_with_fallback(State const& state, From d57d6da0d7f4bb7444b6deefe571dcb095d8f595 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 14:26:03 +0200 Subject: [PATCH 16/25] ruff Signed-off-by: Jerry Jinfeng Guo --- src/power_grid_model/_core/power_grid_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/power_grid_model/_core/power_grid_core.py b/src/power_grid_model/_core/power_grid_core.py index dfa02226fa..c51cd63bb2 100644 --- a/src/power_grid_model/_core/power_grid_core.py +++ b/src/power_grid_model/_core/power_grid_core.py @@ -502,7 +502,7 @@ def calculate( # type: ignore[empty-body] pass # pragma: no cover @make_c_binding - def calculate_with_state( # noqa: PLR0913 # type: ignore[empty-body] + def calculate_with_state( # noqa: PLR0913, PLR0917 # type: ignore[empty-body] self, model: ModelPtr, opt: OptionsPtr, From bba8dcc552cb38b775539af7e936e284cc13519d Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 19:46:06 +0200 Subject: [PATCH 17/25] more initializers Signed-off-by: Jerry Jinfeng Guo --- .../main_core/test_main_core_output.cpp | 50 +++++++++++++++---- .../test_topological_node_output.cpp | 3 +- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/tests/cpp_unit_tests/main_core/test_main_core_output.cpp b/tests/cpp_unit_tests/main_core/test_main_core_output.cpp index af7d0653f9..44bc97dbc9 100644 --- a/tests/cpp_unit_tests/main_core/test_main_core_output.cpp +++ b/tests/cpp_unit_tests/main_core/test_main_core_output.cpp @@ -226,7 +226,8 @@ template void check_null_current_sensor_output(CurrentSensorO } template auto make_math_output(std::vector solver_output) { - return MathOutput{.solver_output = std::move(solver_output), .optimizer_output = {}, .supernode_output = {}}; + return MathOutput{ + .solver_output = std::move(solver_output), .optimizer_output = {}, .supernode_output = {}, .model_state = {}}; } } // namespace @@ -257,10 +258,16 @@ TEST_CASE("Test main core output") { MathOutput>> const math_output{ .solver_output = {}, .optimizer_output = {}, - .supernode_output = { - {.bus_injection = {}, - .link = {{.s_f = {1.0, 2.0}, .s_t = {-1.0, -1.5}, .i_f = {3.0, 4.0}, .i_t = {-3.0, -4.0}}, - {.s_f = {3.0, 4.0}, .s_t = {-3.0, -4.0}, .i_f = {5.0, 6.0}, .i_t = {-5.0, -5.5}}}}}}; + .supernode_output = {{ + .bus_injection = {}, + .link = + { + {.s_f = {1.0, 2.0}, .s_t = {-1.0, -1.5}, .i_f = {3.0, 4.0}, .i_t = {-3.0, -4.0}}, + {.s_f = {3.0, 4.0}, .s_t = {-3.0, -4.0}, .i_f = {5.0, 6.0}, .i_t = {-5.0, -5.5}}, + }, + }}, + .model_state = {}, + }; std::vector output(3); output_result(state, math_output, output); @@ -283,8 +290,15 @@ TEST_CASE("Test main core output") { MathOutput>> const math_output{ .solver_output = {}, .optimizer_output = {}, - .supernode_output = { - {.link = {{.i_f = {1.0, 0.0}, .i_t = {-2.0, 0.0}}, {.i_f = {3.0, 0.0}, .i_t = {-4.0, 0.0}}}}}}; + .supernode_output = {{ + .link = + { + {.i_f = {1.0, 0.0}, .i_t = {-2.0, 0.0}}, + {.i_f = {3.0, 0.0}, .i_t = {-4.0, 0.0}}, + }, + }}, + .model_state = {}, + }; std::vector output(3); output_result(state, math_output, output); @@ -340,13 +354,20 @@ TEST_CASE("Test main core output") { .transformer_tap_positions = {{.transformer_id = 3, .tap_position = 1}}}; SUBCASE("Symmetric") { output_result( - state, SymOutput{.solver_output = {}, .optimizer_output = optimizer_output, .supernode_output = {}}, + state, + SymOutput{.solver_output = {}, + .optimizer_output = optimizer_output, + .supernode_output = {}, + .model_state = {}}, output); } SUBCASE("Asymmetric") { output_result( state, - AsymOutput{.solver_output = {}, .optimizer_output = optimizer_output, .supernode_output = {}}, + AsymOutput{.solver_output = {}, + .optimizer_output = optimizer_output, + .supernode_output = {}, + .model_state = {}}, output); } CHECK(output[0].id == 0); @@ -363,13 +384,20 @@ TEST_CASE("Test main core output") { {.transformer_id = 2, .tap_position = 3}}}; SUBCASE("Symmetric") { output_result( - state, SymOutput{.solver_output = {}, .optimizer_output = optimizer_output, .supernode_output = {}}, + state, + SymOutput{.solver_output = {}, + .optimizer_output = optimizer_output, + .supernode_output = {}, + .model_state = {}}, output); } SUBCASE("Asymmetric") { output_result( state, - AsymOutput{.solver_output = {}, .optimizer_output = optimizer_output, .supernode_output = {}}, + AsymOutput{.solver_output = {}, + .optimizer_output = optimizer_output, + .supernode_output = {}, + .model_state = {}}, output); } CHECK(output[0].id == 0); diff --git a/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp b/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp index 21cfde1910..0ee20b096f 100644 --- a/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp +++ b/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp @@ -203,7 +203,8 @@ inline MathOutput>> make_steady_state_math .shunt = {{.s = dummy_complex_value_sym(), .i = dummy_complex_value_sym()}}, .load_gen = {{.s = dummy_complex_value_sym(), .i = dummy_complex_value_sym()}, {.s = dummy_complex_value_sym(), .i = dummy_complex_value_sym()}}, - .voltage_regulator = {}}); + .voltage_regulator = {}, + .model_state_group = {}}); return math_output; } From 9e6aed9f232e539a1c30dae742b1a718aeae8262 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 19:52:59 +0200 Subject: [PATCH 18/25] format Signed-off-by: Jerry Jinfeng Guo --- docs/algorithms/pf-algorithms.md | 4 +--- .../include/power_grid_model/calculation.hpp | 2 +- .../main_core/calculation_input_preparation.hpp | 6 +++--- .../include/power_grid_model/main_core/core_utils.hpp | 4 ++-- .../include/power_grid_model/main_model_impl.hpp | 11 +++++------ .../math_solver/common_solver_functions.hpp | 9 ++++----- .../power_grid_model/math_solver/math_solver.hpp | 11 +++++------ .../math_solver/newton_raphson_pf_solver.hpp | 1 + .../power_grid_model/optimizer/base_optimizer.hpp | 2 +- .../optimizer/tap_position_optimizer.hpp | 4 ++-- power_grid_model_c/power_grid_model_c/src/model.cpp | 4 ++-- .../include/power_grid_model_cpp/dataset.hpp | 8 ++++---- .../cpp_unit_tests/common/test_counting_iterator.cpp | 2 +- tests/cpp_unit_tests/common/test_iterator_facade.cpp | 4 ++-- 14 files changed, 34 insertions(+), 38 deletions(-) diff --git a/docs/algorithms/pf-algorithms.md b/docs/algorithms/pf-algorithms.md index ab54f99649..56a08ea5ab 100644 --- a/docs/algorithms/pf-algorithms.md +++ b/docs/algorithms/pf-algorithms.md @@ -260,9 +260,7 @@ To request only selected state, pass a `StateOutputRequest` for each scenario in ```python from power_grid_model import StateOutputRequest -output_data, model_state = model.calculate_power_flow( - get_model_state=[StateOutputRequest(nodal_state=True)] -) +output_data, model_state = model.calculate_power_flow(get_model_state=[StateOutputRequest(nodal_state=True)]) ``` The request flags select which data to capture, but not every method produces every kind of state. Newton-Raphson diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp index 9459456a60..e893471d8f 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation.hpp @@ -93,7 +93,7 @@ template struct Calculator { Logger& logger) { return [calculation_method, err_tol = options.err_tol, max_iter = options.max_iter, &logger, cache_run](MathSolverProxy& solver, YBus const& y_bus, PowerFlowInput const& input, - bool capture_jacobian_state, bool capture_nodal_state) { + bool capture_jacobian_state, bool capture_nodal_state) { return solver.get().run_power_flow(input, err_tol, max_iter, cache_run, logger, calculation_method, y_bus, capture_jacobian_state, capture_nodal_state); }; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp index 7a7594f947..19f50df3ee 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp @@ -107,7 +107,7 @@ inline auto calculate_param(auto const& c, auto const&... extra_args) * The default lambda `include_all` always returns `true`. */ template (CalcStructOut::* comp_vect), class ComponentIn, + std::vector(CalcStructOut::*comp_vect), class ComponentIn, std::invocable PredicateIn = IncludeAll> requires std::convertible_to, bool> inline void prepare_input(main_model_state_c auto const& state, std::vector const& components, @@ -126,7 +126,7 @@ inline void prepare_input(main_model_state_c auto const& state, std::vector(CalcStructOut::* comp_vect), class ComponentIn, + std::vector(CalcStructOut::*comp_vect), class ComponentIn, std::invocable PredicateIn = IncludeAll> requires std::convertible_to, bool> inline void prepare_input(main_model_state_c auto const& state, std::vector const& components, @@ -145,7 +145,7 @@ inline void prepare_input(main_model_state_c auto const& state, std::vector +template requires std::same_as> || std::same_as> inline void prepare_input_status(main_model_state_c auto const& state, std::vector const& objects, std::vector& input) { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/core_utils.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/core_utils.hpp index c5a1eadcac..797c9c4c9b 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/core_utils.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/core_utils.hpp @@ -29,9 +29,9 @@ template constexpr auto run_functor_with_tuple_index_return_array(functor_c auto functor, std::index_sequence /*unused*/) { if constexpr (sizeof...(Indices) == 1) { - return std::array{functor.template operator()>()...}; + return std::array { functor.template operator()>()... }; } else { - auto result = std::array{functor.template operator()>()...}; + auto result = std::array { functor.template operator()>()... }; return result; } } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp index ea79eef601..164102dfdc 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp @@ -298,7 +298,7 @@ class MainModelImpl { using InputType = typename std::invoke_result_t::const_reference; using SolverOutputType = typename std::invoke_result_t; - using sym = decode_symmetry_v; + using sym = decode_symmetry_v; assert(construction_complete_); // prepare @@ -503,11 +503,10 @@ class MainModelImpl { calculation_type_symmetry_func_selector( options.calculation_type, options.calculation_symmetry, [cache_run]( - MainModelImpl& main_model_, Options const& options_, MutableDataset const& result_data_, - Logger& logger, ModelStateRequest const& state_request_, ModelStateOutput* state_output_) { + MainModelImpl& main_model_, Options const& options_, MutableDataset const& result_data_, Logger& logger, + ModelStateRequest const& state_request_, ModelStateOutput* state_output_) { auto math_output = main_model_.calculate_with_optimizer(options_, cache_run, - logger, - state_request_); + logger, state_request_); main_model_.output_result(math_output, result_data_, logger); if (state_output_ != nullptr && math_output.model_state) { *state_output_ = std::move(*math_output.model_state); @@ -523,7 +522,7 @@ class MainModelImpl { auto sub_opt = options; // copy sub_opt.err_tol = cache_run ? std::numeric_limits::max() : options.err_tol; sub_opt.max_iter = cache_run ? 1 : options.max_iter; - model.calculate(sub_opt, cache_run, target_data, logger, state_request, state_output); + model.calculate(sub_opt, cache_run, target_data, logger, state_request, state_output); } auto const& state() const { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp index de10657a20..c221127f85 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp @@ -168,11 +168,10 @@ template struct RegulatorQState { }; template -inline auto initialize_regulator_output_and_distribution(PowerFlowInput const& input, SolverOutput& output, - IdxRange const& load_gens, - std::map const& loadgen_to_regulator, - std::vector>& regulator_state) - -> std::tuple> { +inline auto initialize_regulator_output_and_distribution( + PowerFlowInput const& input, SolverOutput& output, IdxRange const& load_gens, + std::map const& loadgen_to_regulator, + std::vector>& regulator_state) -> std::tuple> { int num_regulating_gens = 0; ComplexValue s_load_gen_bus = ComplexValue{0.0}; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp index 6d88f1c806..e78403fad2 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/math_solver.hpp @@ -61,12 +61,11 @@ template class MathSolver : public MathSolverBase { return run_power_flow_linear(input, err_tol, max_iter, log, y_bus); case linear_current: last_power_flow_used_iterative_current_ = true; - return run_power_flow_linear_current(input, err_tol, max_iter, cache_run, log, y_bus, - capture_nodal_state); + return run_power_flow_linear_current(input, err_tol, max_iter, cache_run, log, y_bus, capture_nodal_state); case iterative_current: last_power_flow_used_iterative_current_ = true; return run_power_flow_iterative_current(input, err_tol, max_iter, cache_run, log, y_bus, - capture_nodal_state); + capture_nodal_state); default: throw InvalidCalculationMethod{}; } @@ -176,15 +175,15 @@ template class MathSolver : public MathSolverBase { Timer const timer{log, LogEvent::create_math_solver}; iterative_current_pf_solver_.emplace(y_bus, *topo_ptr_); } - iterative_current_pf_solver_->set_nodal_state_capture(capture_nodal_state); - return iterative_current_pf_solver_->run_power_flow(y_bus, input, err_tol, max_iter, cache_run, log); + iterative_current_pf_solver_->set_nodal_state_capture(capture_nodal_state); + return iterative_current_pf_solver_->run_power_flow(y_bus, input, err_tol, max_iter, cache_run, log); } SolverOutput run_power_flow_linear_current(PowerFlowInput const& input, double /* err_tol */, Idx /* max_iter */, bool cache_run, Logger& log, YBus const& y_bus, bool capture_nodal_state) { return run_power_flow_iterative_current(input, std::numeric_limits::infinity(), 1, cache_run, log, - y_bus, capture_nodal_state); + y_bus, capture_nodal_state); } SolverOutput run_state_estimation_iterative_linear(StateEstimationInput const& input, double err_tol, diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp index 4e651edc99..3c607b3fda 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp @@ -462,6 +462,7 @@ class NewtonRaphsonPFSolver : public IterativePFSolver> data_jac_; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp index 9b9122a125..5d6fdb507e 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp @@ -70,7 +70,7 @@ concept optimizer_c = { optimizer.optimize(state, method) } -> std::same_as< - MathOutput>>; + MathOutput>>; }; template diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp index 294fc43195..f9df493bc9 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp @@ -1043,8 +1043,8 @@ class TapPositionOptimizerImpl, StateCalculator, return {.solver_output = {std::move(solver_output)}, .optimizer_output = {std::move(transformer_tap_positions)}, - .supernode_output = {}, - .model_state = {}}; + .supernode_output = {}, + .model_state = {}}; } auto iterate_with_fallback(State const& state, diff --git a/power_grid_model_c/power_grid_model_c/src/model.cpp b/power_grid_model_c/power_grid_model_c/src/model.cpp index eb0ad721d3..53e5b0e8d7 100644 --- a/power_grid_model_c/power_grid_model_c/src/model.cpp +++ b/power_grid_model_c/power_grid_model_c/src/model.cpp @@ -458,8 +458,8 @@ void PGM_calculate_with_state(PGM_Handle* handle, PGM_PowerGridModel* model, PGM auto& cpp_model = safe_ptr_get(cast_to_cpp(model)); ScopedModuleLogger const logger_guard{cpp_model, get_logger(safe_ptr_get(handle).logger)}; - calculate_impl(cpp_model, safe_ptr_get(opt), safe_ptr_get(cast_to_cpp(output_dataset)), - cpp_batch_dataset, converted_requests, result->scenarios); + calculate_impl(cpp_model, safe_ptr_get(opt), safe_ptr_get(cast_to_cpp(output_dataset)), cpp_batch_dataset, + converted_requests, result->scenarios); safe_ptr_get(state_output) = result.release(); }, batch_exception_handler); 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 a85221c65f..e7c45ed7cc 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 @@ -141,8 +141,8 @@ class DatasetWritable { class DatasetMutable { public: explicit DatasetMutable(std::string const& dataset, bool is_batch, Idx batch_size) - : dataset_{ - handle_.call_with(PGM_create_dataset_mutable, dataset.c_str(), (is_batch ? Idx{1} : Idx{0}), batch_size)}, + : dataset_{handle_.call_with(PGM_create_dataset_mutable, dataset.c_str(), (is_batch ? Idx{1} : Idx{0}), + batch_size)}, info_{handle_.call_with(PGM_dataset_mutable_get_info, get())} {} RawMutableDataset const* get() const { return dataset_.get(); } @@ -180,8 +180,8 @@ class DatasetMutable { class DatasetConst { public: explicit DatasetConst(std::string const& dataset, bool is_batch, Idx batch_size) - : dataset_{ - handle_.call_with(PGM_create_dataset_const, dataset.c_str(), (is_batch ? Idx{1} : Idx{0}), batch_size)}, + : dataset_{handle_.call_with(PGM_create_dataset_const, dataset.c_str(), (is_batch ? Idx{1} : Idx{0}), + batch_size)}, info_{handle_.call_with(PGM_dataset_const_get_info, get())} {} DatasetConst(DatasetWritable const& writable_dataset) diff --git a/tests/cpp_unit_tests/common/test_counting_iterator.cpp b/tests/cpp_unit_tests/common/test_counting_iterator.cpp index 65d9244bfb..4599a3ef96 100644 --- a/tests/cpp_unit_tests/common/test_counting_iterator.cpp +++ b/tests/cpp_unit_tests/common/test_counting_iterator.cpp @@ -27,7 +27,7 @@ TEST_CASE("Counting Iterator") { CHECK(*IdxCount{0} == 0); CHECK(*IdxCount{2} == 2); CHECK(*(++IdxCount{0}) == 1); - CHECK(*(IdxCount{0} ++) == 0); + CHECK(*(IdxCount { 0 } ++) == 0); CHECK(*(IdxCount{0} + 1) == 1); CHECK(IdxRange{IdxRange{1, 3}.begin(), IdxRange{1, 3}.end()}.size() == 2); CHECK(*IdxRange{IdxRange{1, 3}.begin(), IdxRange{1, 3}.end()}.begin() == 1); diff --git a/tests/cpp_unit_tests/common/test_iterator_facade.cpp b/tests/cpp_unit_tests/common/test_iterator_facade.cpp index 361c52a21b..95b0e09be1 100644 --- a/tests/cpp_unit_tests/common/test_iterator_facade.cpp +++ b/tests/cpp_unit_tests/common/test_iterator_facade.cpp @@ -67,8 +67,8 @@ template class BaseTest return *it_; } - friend constexpr auto operator<=>(BaseTestIterator const& first, BaseTestIterator const& second) - -> std::strong_ordering { + friend constexpr auto operator<=>(BaseTestIterator const& first, + BaseTestIterator const& second) -> std::strong_ordering { first.last_call_ = IteratorFacadeableCalls::distance_to; return *first.it_ <=> *second.it_; } From a750318b7c371a7a63b5d370573585a9bed7d54a Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 20:21:34 +0200 Subject: [PATCH 19/25] clang-tidy complaints Signed-off-by: Jerry Jinfeng Guo --- .../include/power_grid_model_c/model.h | 10 +++++----- .../include/power_grid_model_cpp/model.hpp | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h index 45631f55ba..d0ffd5440e 100644 --- a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h +++ b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/model.h @@ -120,7 +120,7 @@ PGM_API void PGM_calculate(PGM_Handle* handle, PGM_PowerGridModel* model, PGM_Op /** * @brief Per-scenario request for optional native PF state output. */ -typedef struct PGM_StateOutputRequest { +typedef struct PGM_StateOutputRequest { // NOLINT(modernize-use-using): part of the C API PGM_Idx y_bus; PGM_Idx jacobian; PGM_Idx nodal_state; @@ -132,7 +132,7 @@ typedef struct PGM_StateOutputRequest { * All pointers are valid until the owning PGM_StateOutput is destroyed. A zero * availability flag means the corresponding pointer fields are NULL. */ -typedef struct PGM_StateScenarioView { +typedef struct PGM_StateScenarioView { // NOLINT(modernize-use-using): part of the C API PGM_Idx has_state; PGM_Idx y_bus_requested; PGM_Idx jacobian_requested; @@ -154,7 +154,7 @@ typedef struct PGM_StateScenarioView { * these values. The group mapping also lists user nodes per bus and identifies * synthetic buses with their originating branch3 IDs. */ -typedef struct PGM_StateGroupView { +typedef struct PGM_StateGroupView { // NOLINT(modernize-use-using): part of the C API PGM_Idx group; PGM_Idx n_bus; PGM_Idx is_symmetric; @@ -185,7 +185,7 @@ typedef struct PGM_StateGroupView { * Iterations are numbered from one. All pointers are valid until the owning * PGM_StateOutput is destroyed. */ -typedef struct PGM_StateJacobianView { +typedef struct PGM_StateJacobianView { // NOLINT(modernize-use-using): part of the C API PGM_Idx iteration; double const* jacobian_h; double const* jacobian_n; @@ -201,7 +201,7 @@ typedef struct PGM_StateJacobianView { * Values are bus-major; asymmetric groups store phases 1, 2, 3 for each bus. * All pointers are valid until the owning PGM_StateOutput is destroyed. */ -typedef struct PGM_StateNodalStateView { +typedef struct PGM_StateNodalStateView { // NOLINT(modernize-use-using): part of the C API PGM_Idx iteration; double const* voltage_magnitude; double const* voltage_angle; diff --git a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp index 32618b4ca9..67ed056a40 100644 --- a/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp +++ b/power_grid_model_c/power_grid_model_cpp/include/power_grid_model_cpp/model.hpp @@ -31,6 +31,7 @@ class StateOutput { StateOutput& operator=(StateOutput&&) noexcept = default; StateOutput(StateOutput const&) = delete; StateOutput& operator=(StateOutput const&) = delete; + ~StateOutput() = default; Idx scenario_count() const { return handle_.call_with(PGM_state_output_scenario_count, output_.get()); } PGM_StateScenarioView scenario(Idx scenario_idx) const { From b6ec7386b965997ce169c0b8f8f85c1abb224e55 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Sun, 11 Oct 2026 20:23:36 +0200 Subject: [PATCH 20/25] format Signed-off-by: Jerry Jinfeng Guo --- .../main_core/calculation_input_preparation.hpp | 6 +++--- .../include/power_grid_model/main_core/core_utils.hpp | 4 ++-- .../math_solver/common_solver_functions.hpp | 9 +++++---- .../power_grid_model/optimizer/base_optimizer.hpp | 2 +- .../include/power_grid_model_cpp/dataset.hpp | 8 ++++---- tests/cpp_unit_tests/common/test_counting_iterator.cpp | 2 +- tests/cpp_unit_tests/common/test_iterator_facade.cpp | 4 ++-- tests/cpp_unit_tests/state_output/CMakeLists.txt | 2 +- 8 files changed, 19 insertions(+), 18 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp index 19f50df3ee..7a7594f947 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp @@ -107,7 +107,7 @@ inline auto calculate_param(auto const& c, auto const&... extra_args) * The default lambda `include_all` always returns `true`. */ template (CalcStructOut::*comp_vect), class ComponentIn, + std::vector(CalcStructOut::* comp_vect), class ComponentIn, std::invocable PredicateIn = IncludeAll> requires std::convertible_to, bool> inline void prepare_input(main_model_state_c auto const& state, std::vector const& components, @@ -126,7 +126,7 @@ inline void prepare_input(main_model_state_c auto const& state, std::vector(CalcStructOut::*comp_vect), class ComponentIn, + std::vector(CalcStructOut::* comp_vect), class ComponentIn, std::invocable PredicateIn = IncludeAll> requires std::convertible_to, bool> inline void prepare_input(main_model_state_c auto const& state, std::vector const& components, @@ -145,7 +145,7 @@ inline void prepare_input(main_model_state_c auto const& state, std::vector +template requires std::same_as> || std::same_as> inline void prepare_input_status(main_model_state_c auto const& state, std::vector const& objects, std::vector& input) { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/core_utils.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/core_utils.hpp index 797c9c4c9b..c5a1eadcac 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/core_utils.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/core_utils.hpp @@ -29,9 +29,9 @@ template constexpr auto run_functor_with_tuple_index_return_array(functor_c auto functor, std::index_sequence /*unused*/) { if constexpr (sizeof...(Indices) == 1) { - return std::array { functor.template operator()>()... }; + return std::array{functor.template operator()>()...}; } else { - auto result = std::array { functor.template operator()>()... }; + auto result = std::array{functor.template operator()>()...}; return result; } } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp index c221127f85..de10657a20 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp @@ -168,10 +168,11 @@ template struct RegulatorQState { }; template -inline auto initialize_regulator_output_and_distribution( - PowerFlowInput const& input, SolverOutput& output, IdxRange const& load_gens, - std::map const& loadgen_to_regulator, - std::vector>& regulator_state) -> std::tuple> { +inline auto initialize_regulator_output_and_distribution(PowerFlowInput const& input, SolverOutput& output, + IdxRange const& load_gens, + std::map const& loadgen_to_regulator, + std::vector>& regulator_state) + -> std::tuple> { int num_regulating_gens = 0; ComplexValue s_load_gen_bus = ComplexValue{0.0}; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp index 5d6fdb507e..9b9122a125 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/base_optimizer.hpp @@ -70,7 +70,7 @@ concept optimizer_c = { optimizer.optimize(state, method) } -> std::same_as< - MathOutput>>; + MathOutput>>; }; template 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 e7c45ed7cc..a85221c65f 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 @@ -141,8 +141,8 @@ class DatasetWritable { class DatasetMutable { public: explicit DatasetMutable(std::string const& dataset, bool is_batch, Idx batch_size) - : dataset_{handle_.call_with(PGM_create_dataset_mutable, dataset.c_str(), (is_batch ? Idx{1} : Idx{0}), - batch_size)}, + : dataset_{ + handle_.call_with(PGM_create_dataset_mutable, dataset.c_str(), (is_batch ? Idx{1} : Idx{0}), batch_size)}, info_{handle_.call_with(PGM_dataset_mutable_get_info, get())} {} RawMutableDataset const* get() const { return dataset_.get(); } @@ -180,8 +180,8 @@ class DatasetMutable { class DatasetConst { public: explicit DatasetConst(std::string const& dataset, bool is_batch, Idx batch_size) - : dataset_{handle_.call_with(PGM_create_dataset_const, dataset.c_str(), (is_batch ? Idx{1} : Idx{0}), - batch_size)}, + : dataset_{ + handle_.call_with(PGM_create_dataset_const, dataset.c_str(), (is_batch ? Idx{1} : Idx{0}), batch_size)}, info_{handle_.call_with(PGM_dataset_const_get_info, get())} {} DatasetConst(DatasetWritable const& writable_dataset) diff --git a/tests/cpp_unit_tests/common/test_counting_iterator.cpp b/tests/cpp_unit_tests/common/test_counting_iterator.cpp index 4599a3ef96..65d9244bfb 100644 --- a/tests/cpp_unit_tests/common/test_counting_iterator.cpp +++ b/tests/cpp_unit_tests/common/test_counting_iterator.cpp @@ -27,7 +27,7 @@ TEST_CASE("Counting Iterator") { CHECK(*IdxCount{0} == 0); CHECK(*IdxCount{2} == 2); CHECK(*(++IdxCount{0}) == 1); - CHECK(*(IdxCount { 0 } ++) == 0); + CHECK(*(IdxCount{0} ++) == 0); CHECK(*(IdxCount{0} + 1) == 1); CHECK(IdxRange{IdxRange{1, 3}.begin(), IdxRange{1, 3}.end()}.size() == 2); CHECK(*IdxRange{IdxRange{1, 3}.begin(), IdxRange{1, 3}.end()}.begin() == 1); diff --git a/tests/cpp_unit_tests/common/test_iterator_facade.cpp b/tests/cpp_unit_tests/common/test_iterator_facade.cpp index 95b0e09be1..361c52a21b 100644 --- a/tests/cpp_unit_tests/common/test_iterator_facade.cpp +++ b/tests/cpp_unit_tests/common/test_iterator_facade.cpp @@ -67,8 +67,8 @@ template class BaseTest return *it_; } - friend constexpr auto operator<=>(BaseTestIterator const& first, - BaseTestIterator const& second) -> std::strong_ordering { + friend constexpr auto operator<=>(BaseTestIterator const& first, BaseTestIterator const& second) + -> std::strong_ordering { first.last_call_ = IteratorFacadeableCalls::distance_to; return *first.it_ <=> *second.it_; } diff --git a/tests/cpp_unit_tests/state_output/CMakeLists.txt b/tests/cpp_unit_tests/state_output/CMakeLists.txt index eb5a172235..ea9fcc23f4 100644 --- a/tests/cpp_unit_tests/state_output/CMakeLists.txt +++ b/tests/cpp_unit_tests/state_output/CMakeLists.txt @@ -13,4 +13,4 @@ target_link_libraries( PRIVATE power_grid_model doctest::doctest ) -doctest_discover_tests(power_grid_model_unit_tests_state_output) \ No newline at end of file +doctest_discover_tests(power_grid_model_unit_tests_state_output) From 1ff97ce4f537915d559ba44dcc7e69b35804e548 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Mon, 12 Oct 2026 09:43:34 +0200 Subject: [PATCH 21/25] clang-tidy complaints Signed-off-by: Jerry Jinfeng Guo --- .../power_grid_model_c/src/model.cpp | 14 +++++++++----- .../state_output/test_state_output.cpp | 9 +++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/power_grid_model_c/power_grid_model_c/src/model.cpp b/power_grid_model_c/power_grid_model_c/src/model.cpp index 53e5b0e8d7..7abb5acff3 100644 --- a/power_grid_model_c/power_grid_model_c/src/model.cpp +++ b/power_grid_model_c/power_grid_model_c/src/model.cpp @@ -17,6 +17,7 @@ #include "power_grid_model_c/model.h" #include +#include #include #include #include @@ -29,8 +30,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -399,7 +403,7 @@ Idx get_total_scenarios(ConstDataset const* batch_dataset) { return 1; } Idx total = 1; - auto current = batch_dataset; + auto const* current = batch_dataset; while (current != nullptr) { auto const& dataset = safe_ptr_get(current); total *= dataset.batch_size(); @@ -481,9 +485,9 @@ void PGM_state_output_get_scenario(PGM_Handle* handle, PGM_StateOutput const* st } auto const& state = *state_result; output.has_state = 1; - output.y_bus_requested = state.y_bus_requested; - output.jacobian_requested = state.jacobian_requested; - output.nodal_state_requested = state.nodal_state_requested; + output.y_bus_requested = static_cast(state.y_bus_requested); + output.jacobian_requested = static_cast(state.jacobian_requested); + output.nodal_state_requested = static_cast(state.nodal_state_requested); output.n_groups = static_cast(state.groups.size()); output.n_input_nodes = static_cast(state.input_node_id.size()); output.input_node_group = state.input_node_group.data(); @@ -505,7 +509,7 @@ void PGM_state_output_get_group(PGM_Handle* handle, PGM_StateOutput const* state auto const& mapping = group.mapping; output.group = mapping.group; output.n_bus = mapping.n_bus; - output.is_symmetric = mapping.is_symmetric; + output.is_symmetric = static_cast(mapping.is_symmetric); output.n_user_node_refs = static_cast(mapping.bus_user_sequence.size()); output.bus_user_indptr = mapping.bus_user_indptr.data(); output.bus_user_sequence = mapping.bus_user_sequence.data(); diff --git a/tests/cpp_unit_tests/state_output/test_state_output.cpp b/tests/cpp_unit_tests/state_output/test_state_output.cpp index 766363ec5d..6621a91db0 100644 --- a/tests/cpp_unit_tests/state_output/test_state_output.cpp +++ b/tests/cpp_unit_tests/state_output/test_state_output.cpp @@ -3,18 +3,23 @@ // SPDX-License-Identifier: MPL-2.0 #include -#include #include #include +#include #include #include +#include #include +#include #include +#include #include #include +#include #include +#include #include namespace power_grid_model { @@ -64,7 +69,7 @@ std::optional calculate_state(MainModel& model, MainModelOptio ModelStateRequest request) { auto const& metadata = meta_data::meta_data_gen::meta_data; ConstDataset const update{false, 1, "update", metadata}; - MutableDataset output{false, 1, "sym_output", metadata}; + MutableDataset const output{false, 1, "sym_output", metadata}; std::array const requests{request}; std::vector> state_outputs(1); model.calculate_with_state(options, output, update, requests, state_outputs); From 52f2b0323c853ae3a4defb702e88a22c23927914 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Tue, 20 Oct 2026 15:58:56 +0200 Subject: [PATCH 22/25] updated the example for power flow with trap changing transformers Signed-off-by: Jerry Jinfeng Guo --- docs/examples/Transformer Examples.ipynb | 465 ++++++++++++++++++++++- 1 file changed, 464 insertions(+), 1 deletion(-) diff --git a/docs/examples/Transformer Examples.ipynb b/docs/examples/Transformer Examples.ipynb index d8cec81c9d..91b8bc9ef2 100644 --- a/docs/examples/Transformer Examples.ipynb +++ b/docs/examples/Transformer Examples.ipynb @@ -894,6 +894,469 @@ "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" ] }, + { + "cell_type": "markdown", + "id": "a85cba49", + "metadata": {}, + "source": [ + "### Model state from a tap-changing power flow\n", + "\n", + "Model-state output can also be requested when automatic tap changing is enabled. This example starts from a fresh model, then inspects the state returned with the selected tap-changing result. The iteration histories describe the power-flow solve for the selected tap, not every tap considered during optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7d15a1e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Selected tap position:\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Y-bus nonzero entries: 7\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
admittance
rowcolumn
000.016000+0.000000j
1-0.016000+0.000000j
10-0.016000+0.000000j
10.116000-0.994987j
20.578313-0.803648j
21-0.406823-0.902658j
20.098030-0.975382j
\n", + "
" + ], + "text/plain": [ + " admittance\n", + "row column \n", + "0 0 0.016000+0.000000j\n", + " 1 -0.016000+0.000000j\n", + "1 0 -0.016000+0.000000j\n", + " 1 0.116000-0.994987j\n", + " 2 0.578313-0.803648j\n", + "2 1 -0.406823-0.902658j\n", + " 2 0.098030-0.975382j" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Jacobian iterations: [1, 2, 3, 4, 5]\n", + "Final Jacobian (selected tap):\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
HNML
rowcolumn
000.0050000.010191-0.012191-0.005000
1-0.005000-0.0121910.012191-0.005000
100.005000-0.0121910.0121910.005000
10.9647830.112479-0.1124790.964783
2-0.969783-0.1002880.100288-0.969783
21-0.970344-0.0947020.094702-0.970344
29951.336650995.134156-995.1341569951.336650
\n", + "
" + ], + "text/plain": [ + " H N M L\n", + "row column \n", + "0 0 0.005000 0.010191 -0.012191 -0.005000\n", + " 1 -0.005000 -0.012191 0.012191 -0.005000\n", + "1 0 0.005000 -0.012191 0.012191 0.005000\n", + " 1 0.964783 0.112479 -0.112479 0.964783\n", + " 2 -0.969783 -0.100288 0.100288 -0.969783\n", + "2 1 -0.970344 -0.094702 0.094702 -0.970344\n", + " 2 9951.336650 995.134156 -995.134156 9951.336650" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nodal-state iterations: [1, 2, 3, 4, 5]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
|V|, iteration 1 (p.u.)angle, iteration 1 (rad)|V|, iteration 2 (p.u.)angle, iteration 2 (rad)|V|, iteration 3 (p.u.)angle, iteration 3 (rad)|V|, iteration 4 (p.u.)angle, iteration 4 (rad)|V|, iteration 5 (p.u.)angle, iteration 5 (rad)
node
21.000000-1.675680e-070.999999-2.576861e-070.999999-2.804433e-070.999999-2.809399e-070.999999-2.809402e-07
40.985887-2.619709e+000.984731-2.620633e+000.984705-2.620868e+000.984705-2.620873e+000.984705-2.620873e+00
60.890190-2.333657e+000.846186-2.242660e+000.836566-2.231922e+000.836324-2.231657e+000.836324-2.231657e+00
\n", + "
" + ], + "text/plain": [ + " |V|, iteration 1 (p.u.) angle, iteration 1 (rad) \\\n", + "node \n", + "2 1.000000 -1.675680e-07 \n", + "4 0.985887 -2.619709e+00 \n", + "6 0.890190 -2.333657e+00 \n", + "\n", + " |V|, iteration 2 (p.u.) angle, iteration 2 (rad) \\\n", + "node \n", + "2 0.999999 -2.576861e-07 \n", + "4 0.984731 -2.620633e+00 \n", + "6 0.846186 -2.242660e+00 \n", + "\n", + " |V|, iteration 3 (p.u.) angle, iteration 3 (rad) \\\n", + "node \n", + "2 0.999999 -2.804433e-07 \n", + "4 0.984705 -2.620868e+00 \n", + "6 0.836566 -2.231922e+00 \n", + "\n", + " |V|, iteration 4 (p.u.) angle, iteration 4 (rad) \\\n", + "node \n", + "2 0.999999 -2.809399e-07 \n", + "4 0.984705 -2.620873e+00 \n", + "6 0.836324 -2.231657e+00 \n", + "\n", + " |V|, iteration 5 (p.u.) angle, iteration 5 (rad) \n", + "node \n", + "2 0.999999 -2.809402e-07 \n", + "4 0.984705 -2.620873e+00 \n", + "6 0.836324 -2.231657e+00 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "model5_state = PowerGridModel(input_data5)\n", + "output_data_state, model_state = model5_state.calculate_power_flow(\n", + " tap_changing_strategy=TapChangingStrategy.any_valid_tap,\n", + " get_model_state=True,\n", + ")\n", + "\n", + "print(\"Selected tap position:\")\n", + "display(pd.DataFrame(output_data_state[ComponentType.transformer_tap_regulator]))\n", + "\n", + "group_state = model_state.groups[0]\n", + "y_bus = group_state.y_bus\n", + "y_bus_rows = np.repeat(np.arange(y_bus.row_indptr.size - 1), np.diff(y_bus.row_indptr))\n", + "print(f\"Y-bus nonzero entries: {len(y_bus.col_indices)}\")\n", + "display(\n", + " pd.DataFrame(\n", + " {\n", + " \"row\": y_bus_rows,\n", + " \"column\": y_bus.col_indices,\n", + " \"admittance\": y_bus.admittance_real + 1j * y_bus.admittance_imag,\n", + " }\n", + " ).set_index([\"row\", \"column\"])\n", + ")\n", + "\n", + "print(\"Jacobian iterations:\", [jac.iteration for jac in group_state.jacobians])\n", + "if group_state.jacobians:\n", + " jacobian = group_state.jacobians[-1]\n", + " pattern = group_state.jacobian_structure\n", + " jacobian_rows = np.repeat(\n", + " np.arange(pattern.row_indptr_lu.size - 1), np.diff(pattern.row_indptr_lu)\n", + " )\n", + " print(\"Final Jacobian (selected tap):\")\n", + " display(\n", + " pd.DataFrame(\n", + " {\n", + " \"row\": jacobian_rows,\n", + " \"column\": pattern.col_indices_lu,\n", + " \"H\": jacobian.h,\n", + " \"N\": jacobian.n,\n", + " \"M\": jacobian.m,\n", + " \"L\": jacobian.l,\n", + " }\n", + " ).set_index([\"row\", \"column\"])\n", + " )\n", + "print(\"Nodal-state iterations:\", [state.iteration for state in group_state.nodal_states])\n", + "voltage_rows = []\n", + "for input_idx, node_id in enumerate(model_state.input_node_id):\n", + " if model_state.input_node_group[input_idx] != group_state.mapping.group:\n", + " continue\n", + " bus_idx = int(model_state.input_node_bus[input_idx])\n", + " row = {\"node\": node_id}\n", + " for state in group_state.nodal_states:\n", + " row[f\"|V|, iteration {state.iteration} (p.u.)\"] = state.voltage_magnitude[bus_idx]\n", + " row[f\"angle, iteration {state.iteration} (rad)\"] = state.voltage_angle[bus_idx]\n", + " voltage_rows.append(row)\n", + "display(pd.DataFrame(voltage_rows).set_index(\"node\"))" + ] + }, { "cell_type": "markdown", "id": "9f1a6f30", @@ -1735,7 +2198,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.14.3" + "version": "3.14.8" } }, "nbformat": 4, From 572c82087131b8cee270ac3a3a2977ef811ce462 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Tue, 27 Oct 2026 10:36:02 +0100 Subject: [PATCH 23/25] format Signed-off-by: Jerry Jinfeng Guo --- power_grid_model_c/power_grid_model_c/src/model.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/power_grid_model_c/power_grid_model_c/src/model.cpp b/power_grid_model_c/power_grid_model_c/src/model.cpp index 833d76ba55..1995dcad74 100644 --- a/power_grid_model_c/power_grid_model_c/src/model.cpp +++ b/power_grid_model_c/power_grid_model_c/src/model.cpp @@ -354,10 +354,10 @@ void calculate_multi_dimensional_impl(MainModel& model, MainModel::Options const ConstDataset const single_update_dataset = safe_batch_dataset.get_individual_scenario(i); MutableDataset const sliced_output_dataset = output_dataset.get_slice_scenario(i * stride_size, (i + 1) * stride_size); - auto const sliced_state_requests = state_requests.empty() - ? state_requests - : state_requests.subspan(static_cast(i * stride_size), - static_cast(stride_size)); + auto const sliced_state_requests = + state_requests.empty() + ? state_requests + : state_requests.subspan(static_cast(i * stride_size), static_cast(stride_size)); auto const sliced_state_outputs = state_outputs.empty() ? state_outputs From ba58e6c3a69dbab9e0e376cd0ff92286e483cfaf Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Tue, 27 Oct 2026 10:44:53 +0100 Subject: [PATCH 24/25] format Signed-off-by: Jerry Jinfeng Guo --- .../Transformer Examples.ipynb | 4408 ++++++++--------- 1 file changed, 2204 insertions(+), 2204 deletions(-) diff --git a/docs/example_notebooks/Transformer Examples.ipynb b/docs/example_notebooks/Transformer Examples.ipynb index 91b8bc9ef2..f38a9f918c 100644 --- a/docs/example_notebooks/Transformer Examples.ipynb +++ b/docs/example_notebooks/Transformer Examples.ipynb @@ -1,2206 +1,2206 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "89cf2628", - "metadata": {}, - "source": [ - "# Transformer Example\n", - "\n", - "In this notebook we will present examples of transformers in `power-grid-model`. \n", - "\n", - "Both two-winding transformer and three-winding transformer are covered. We will do one-time power flow calculation and one-time state estimation for both types of tranformers.\n", - "\n", - "This notebook serves as an example of how to use the Python API. For detailed API documentation, refer to\n", - "[Python API reference](../api_reference/python-api-reference.md)\n", - "and [Native Data Interface](../advanced_documentation/native-data-interface.md).\n", - "\n", - "## Transformer (Two-winding Transformer)\n", - "\n", - "Transformer is described as a pi model in `power-grid-model`, and it belongs to the `branch` component type which connects two nodes with possibly different voltage levels.\n", - "\n", - "### Example Network\n", - "\n", - "We use a simple network with 2 nodes, 1 source, 1 load and 1 transformer. As shown below:\n", - "\n", - "```txt\n", - " source_1 --- node_2 --- transformer_3 --- node_4 --- load_5\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "ae11dc9a", - "metadata": {}, - "outputs": [], - "source": [ - "# some basic imports\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "from power_grid_model import (\n", - " AttributeType,\n", - " BranchSide,\n", - " CalculationMethod,\n", - " CalculationType,\n", - " ComponentType,\n", - " DatasetType,\n", - " LoadGenType,\n", - " MeasuredTerminalType,\n", - " PowerGridModel,\n", - " TapChangingStrategy,\n", - " initialize_array,\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f983cef7", - "metadata": {}, - "source": [ - "### Input Dataset\n", - "\n", - "We create an input dataset by using the helper function `initialize_array`. \n", - "\n", - "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "6f008736", - "metadata": {}, - "outputs": [], - "source": [ - "# node\n", - "node = initialize_array(DatasetType.input, ComponentType.node, 2)\n", - "node[AttributeType.id] = np.array([2, 4])\n", - "node[AttributeType.u_rated] = [1e4, 4e2]\n", - "\n", - "# load\n", - "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 1)\n", - "sym_load[AttributeType.id] = [5]\n", - "sym_load[AttributeType.node] = [4]\n", - "sym_load[AttributeType.status] = [1]\n", - "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", - "sym_load[AttributeType.p_specified] = [1e3]\n", - "sym_load[AttributeType.q_specified] = [5e3]\n", - "\n", - "# source\n", - "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", - "source[AttributeType.id] = [1]\n", - "source[AttributeType.node] = [2]\n", - "source[AttributeType.status] = [1]\n", - "source[AttributeType.u_ref] = [1.0]\n", - "\n", - "# transformer\n", - "transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)\n", - "transformer[AttributeType.id] = [3]\n", - "transformer[AttributeType.from_node] = [2]\n", - "transformer[AttributeType.to_node] = [4]\n", - "transformer[AttributeType.from_status] = [1]\n", - "transformer[AttributeType.to_status] = [1]\n", - "transformer[AttributeType.u1] = [1e4]\n", - "transformer[AttributeType.u2] = [4e2]\n", - "transformer[AttributeType.sn] = [1e5]\n", - "transformer[AttributeType.uk] = [0.1]\n", - "transformer[AttributeType.pk] = [1e3]\n", - "transformer[AttributeType.i0] = [1.0e-6]\n", - "transformer[AttributeType.p0] = [0.1]\n", - "transformer[AttributeType.winding_from] = [2]\n", - "transformer[AttributeType.winding_to] = [1]\n", - "transformer[AttributeType.clock] = [5]\n", - "transformer[AttributeType.tap_side] = [0]\n", - "transformer[AttributeType.tap_pos] = [3]\n", - "transformer[AttributeType.tap_min] = [-11]\n", - "transformer[AttributeType.tap_max] = [9]\n", - "transformer[AttributeType.tap_size] = [100]\n", - "\n", - "# all\n", - "input_data = {\n", - " ComponentType.node: node,\n", - " ComponentType.transformer: transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "d16f9dea", - "metadata": {}, - "source": [ - "**We can print the input dataset by converting the numpy array to dataframe.**" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "37749c7c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " id from_node to_node from_status to_status u1 u2 sn \\\n", - "0 3 2 4 1 1 10000.0 400.0 100000.0 \n", - "\n", - " uk pk ... tap_nom tap_size uk_min uk_max pk_min pk_max \\\n", - "0 0.1 1000.0 ... -128 100.0 NaN NaN NaN NaN \n", - "\n", - " r_grounding_from x_grounding_from r_grounding_to x_grounding_to \n", - "0 NaN NaN NaN NaN \n", - "\n", - "[1 rows x 31 columns]\n" - ] - } - ], - "source": [ - "print(pd.DataFrame(input_data[ComponentType.transformer]))" - ] - }, - { - "cell_type": "markdown", - "id": "47a9c257", - "metadata": {}, - "source": [ - "### One-time Power Flow Calculation\n", - "\n", - "You can call the method `calculate_power_flow` to do a one-time calculation based on the current network data in the model.\n", - "\n", - "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [Power Flow Example](./Power%20Flow%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb). " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "7bb0f998", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n", - " id energized u_pu u u_angle p \\\n", - "0 2 1 0.999999 9999.994897 -4.976260e-08 1002.882212 \n", - "1 4 1 0.965618 386.247005 -2.618522e+00 -1000.000000 \n", - "\n", - " q \n", - "0 5027.744841 \n", - "1 -5000.000000 \n" - ] - } - ], - "source": [ - "# validation (optional)\n", - "from power_grid_model.validation import assert_valid_input_data\n", - "\n", - "assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.power_flow)\n", - "\n", - "# construction\n", - "model = PowerGridModel(input_data)\n", - "\n", - "# one-time power flow calculation\n", - "output_data = model.calculate_power_flow(\n", - " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", - ")\n", - "\n", - "# result dataset\n", - "print(\"------node result------\")\n", - "print(pd.DataFrame(output_data[ComponentType.node]))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "682c1c48", - "metadata": {}, - "source": [ - "### One-time State Estimation\n", - "Below we present a simple example of state estimation for a network with a two-winding transformer. \n", - "\n", - "NOTE: In `power-grid-model`, two-winding transformers belong to `branch` component type, therefore the `measured_terminal_type` of power sensors should be assigned to `MeasuredTerminalType.branch_from/_to`.\n", - "\n", - "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [State Estimation Example](./State%20Estimation%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "f0c8c3e8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n", - " id energized u_pu u u_angle p \\\n", - "0 2 1 9.987392 99873.919714 0.000000 470658.914282 \n", - "1 4 1 9.209290 3683.716125 -2.617974 -446912.927660 \n", - "\n", - " q \n", - "0 4.700696e+06 \n", - "1 -4.464515e+06 \n" - ] - } - ], - "source": [ - "# voltage sensor\n", - "sym_voltage_sensor = initialize_array(DatasetType.input, ComponentType.sym_voltage_sensor, 2)\n", - "sym_voltage_sensor[AttributeType.id] = [6, 7]\n", - "sym_voltage_sensor[AttributeType.measured_object] = [2, 4]\n", - "sym_voltage_sensor[AttributeType.u_sigma] = [1.0, 1.0]\n", - "sym_voltage_sensor[AttributeType.u_measured] = [1e5, 4e2]\n", - "\n", - "# power sensor\n", - "sym_power_sensor = initialize_array(DatasetType.input, ComponentType.sym_power_sensor, 2)\n", - "sym_power_sensor[AttributeType.id] = [8, 9]\n", - "sym_power_sensor[AttributeType.measured_object] = [3, 3]\n", - "sym_power_sensor[AttributeType.measured_terminal_type] = [\n", - " MeasuredTerminalType.branch_from,\n", - " MeasuredTerminalType.branch_to,\n", - "]\n", - "sym_power_sensor[AttributeType.power_sigma] = [1.0, 1.0]\n", - "sym_power_sensor[AttributeType.p_measured] = [2e6, 1e5]\n", - "sym_power_sensor[AttributeType.q_measured] = [2e7, 1e6]\n", - "\n", - "# use components from former input dataset cell.\n", - "input_data2 = {\n", - " ComponentType.node: node,\n", - " ComponentType.transformer: transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - " ComponentType.sym_voltage_sensor: sym_voltage_sensor,\n", - " ComponentType.sym_power_sensor: sym_power_sensor,\n", - "}\n", - "\n", - "# validation (optional)\n", - "from power_grid_model.validation import assert_valid_input_data\n", - "\n", - "assert_valid_input_data(input_data=input_data2, calculation_type=CalculationType.state_estimation)\n", - "\n", - "# construction\n", - "model2 = PowerGridModel(input_data2)\n", - "\n", - "# one-time state estimation\n", - "output_data2 = model2.calculate_state_estimation(\n", - " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.iterative_linear\n", - ")\n", - "\n", - "# result dataset\n", - "print(\"------node result------\")\n", - "print(pd.DataFrame(output_data2[ComponentType.node]))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e3605c3e", - "metadata": {}, - "source": [ - "## Three-Winding Transformer\n", - "\n", - "Three-winding transformer is described as 3 transformers of pi model each connected together in star configuration, and it belongs to the `branch3` component type which connects three nodes with possibly different voltage levels.\n", - "\n", - "### Example Network\n", - "\n", - "We use a simple network with 3 nodes, 1 source, 1 load and 1 three-winding transformer. As shown below:\n", - "\n", - "```txt\n", - " source_1 --- node_2 --- three_winding_transformer_3 --- node_4 --- load_5\n", - " |\n", - " -------------------node_6 --- load_7\n", - " \n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "84a7f686", - "metadata": {}, - "source": [ - "### Input Dataset\n", - "\n", - "We use the helper function `initialize_array` to create an input dataset.\n", - "\n", - "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "44c2de63", - "metadata": {}, - "outputs": [], - "source": [ - "# node\n", - "node = initialize_array(DatasetType.input, ComponentType.node, 3)\n", - "node[AttributeType.id] = np.array([2, 4, 6])\n", - "node[AttributeType.u_rated] = [1e4, 1e2, 1e2]\n", - "\n", - "# load\n", - "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 2)\n", - "sym_load[AttributeType.id] = [5, 7]\n", - "sym_load[AttributeType.node] = [4, 6]\n", - "sym_load[AttributeType.status] = [1]\n", - "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", - "sym_load[AttributeType.p_specified] = [1e3, 1e3]\n", - "sym_load[AttributeType.q_specified] = [5e3, 5e3]\n", - "\n", - "# source\n", - "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", - "source[AttributeType.id] = [1]\n", - "source[AttributeType.node] = [2]\n", - "source[AttributeType.status] = [1]\n", - "source[AttributeType.u_ref] = [1.0]\n", - "\n", - "# three-winding transformer\n", - "three_winding_transformer = initialize_array(DatasetType.input, ComponentType.three_winding_transformer, 1)\n", - "three_winding_transformer[AttributeType.id] = [3]\n", - "three_winding_transformer[AttributeType.node_1] = [2]\n", - "three_winding_transformer[AttributeType.node_2] = [4]\n", - "three_winding_transformer[AttributeType.node_3] = [6]\n", - "three_winding_transformer[AttributeType.status_1] = [1]\n", - "three_winding_transformer[AttributeType.status_2] = [1]\n", - "three_winding_transformer[AttributeType.status_3] = [1]\n", - "three_winding_transformer[AttributeType.u1] = [1e4]\n", - "three_winding_transformer[AttributeType.u2] = [1e2]\n", - "three_winding_transformer[AttributeType.u3] = [1e2]\n", - "three_winding_transformer[AttributeType.sn_1] = [1e5]\n", - "three_winding_transformer[AttributeType.sn_2] = [1e5]\n", - "three_winding_transformer[AttributeType.sn_3] = [1e5]\n", - "three_winding_transformer[AttributeType.uk_12] = [0.09]\n", - "three_winding_transformer[AttributeType.uk_13] = [0.06]\n", - "three_winding_transformer[AttributeType.uk_23] = [0.06]\n", - "three_winding_transformer[AttributeType.pk_12] = [1e3]\n", - "three_winding_transformer[AttributeType.pk_13] = [1e3]\n", - "three_winding_transformer[AttributeType.pk_23] = [1e3]\n", - "three_winding_transformer[AttributeType.i0] = [0]\n", - "three_winding_transformer[AttributeType.p0] = [0]\n", - "three_winding_transformer[AttributeType.winding_1] = [2]\n", - "three_winding_transformer[AttributeType.winding_2] = [1]\n", - "three_winding_transformer[AttributeType.winding_3] = [1]\n", - "three_winding_transformer[AttributeType.clock_12] = [5]\n", - "three_winding_transformer[AttributeType.clock_13] = [-7] # supports periodic input\n", - "three_winding_transformer[AttributeType.tap_side] = [0]\n", - "three_winding_transformer[AttributeType.tap_pos] = [0]\n", - "three_winding_transformer[AttributeType.tap_min] = [-10]\n", - "three_winding_transformer[AttributeType.tap_max] = [10]\n", - "three_winding_transformer[AttributeType.tap_nom] = [0]\n", - "three_winding_transformer[AttributeType.tap_size] = [1380]\n", - "\n", - "# all\n", - "input_data3 = {\n", - " ComponentType.node: node,\n", - " ComponentType.three_winding_transformer: three_winding_transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "c25444ec", - "metadata": {}, - "source": [ - "### One-time power flow calculation" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "b4e63620", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n", - " id energized u_pu u u_angle p \\\n", - "0 2 1 0.999999 9999.989788 -9.966674e-08 2007.896574 \n", - "1 4 1 0.993097 99.309655 -2.618590e+00 -1000.000000 \n", - "2 6 1 0.994637 99.463733 -2.618281e+00 -1000.000000 \n", - "\n", - " q \n", - "0 10062.592544 \n", - "1 -5000.000000 \n", - "2 -5000.000000 \n" - ] - } - ], - "source": [ - "# validation (optional)\n", - "from power_grid_model.validation import assert_valid_input_data\n", - "\n", - "assert_valid_input_data(input_data=input_data3, calculation_type=CalculationType.power_flow)\n", - "\n", - "# construction\n", - "model3 = PowerGridModel(input_data3)\n", - "\n", - "# one-time power flow calculation\n", - "output_data3 = model3.calculate_power_flow(\n", - " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", - ")\n", - "\n", - "# result dataset\n", - "print(\"------node result------\")\n", - "print(pd.DataFrame(output_data3[ComponentType.node]))" - ] - }, - { - "cell_type": "markdown", - "id": "36f829f1", - "metadata": {}, - "source": [ - "### One-time State Estimation\n", - "\n", - "Below we present a simple example of state estimation for a network with a three-winding transformer. \n", - "\n", - "NOTE: In `power-grid-model`, three-winding transformers belong to `branch3` component type, therefore the `measured_terminal_type` of power sensors should be assigned to `MeasuredTerminalType.branch3_1/_2/_3`.\n", - "\n", - "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [State Estimation Example](./State%20Estimation%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "1ae6c043", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------three-winding transformer result------\n", - " id energized loading_1 loading_2 loading_3 loading p_1 \\\n", - "0 3 1 0.033993 0.016958 0.016967 0.033993 667.156598 \n", - "\n", - " q_1 i_1 s_1 p_2 q_2 i_2 \\\n", - "0 3333.23678 0.196261 3399.347785 -333.400686 -1662.691395 9.813068 \n", - "\n", - " s_2 p_3 q_3 i_3 s_3 \n", - "0 1695.788516 -332.889246 -1663.677083 9.813068 1696.654557 \n" - ] - } - ], - "source": [ - "# voltage sensor\n", - "sym_voltage_sensor = initialize_array(DatasetType.input, ComponentType.sym_voltage_sensor, 3)\n", - "sym_voltage_sensor[AttributeType.id] = [8, 9, 10]\n", - "sym_voltage_sensor[AttributeType.measured_object] = [2, 4, 6]\n", - "sym_voltage_sensor[AttributeType.u_sigma] = [1.0, 1.0, 1.0]\n", - "sym_voltage_sensor[AttributeType.u_measured] = [1e4, 1e2, 1e2]\n", - "\n", - "# power sensor\n", - "sym_power_sensor = initialize_array(DatasetType.input, ComponentType.sym_power_sensor, 3)\n", - "sym_power_sensor[AttributeType.id] = [11, 12, 13]\n", - "sym_power_sensor[AttributeType.measured_object] = [3, 3, 3]\n", - "sym_power_sensor[AttributeType.measured_terminal_type] = [\n", - " MeasuredTerminalType.branch3_1,\n", - " MeasuredTerminalType.branch3_2,\n", - " MeasuredTerminalType.branch3_3,\n", - "]\n", - "sym_power_sensor[AttributeType.power_sigma] = [1.0, 1.0, 1.0]\n", - "sym_power_sensor[AttributeType.p_measured] = [2e3, 1e3, 1e3]\n", - "sym_power_sensor[AttributeType.q_measured] = [1e4, 5e3, 5e3]\n", - "\n", - "# use components from the one-time power flow calculation with three-winding transformer\n", - "input_data4 = {\n", - " ComponentType.node: node,\n", - " ComponentType.three_winding_transformer: three_winding_transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - " ComponentType.sym_voltage_sensor: sym_voltage_sensor,\n", - " ComponentType.sym_power_sensor: sym_power_sensor,\n", - "}\n", - "\n", - "# validation (optional)\n", - "from power_grid_model.validation import assert_valid_input_data\n", - "\n", - "assert_valid_input_data(input_data=input_data4, calculation_type=CalculationType.state_estimation)\n", - "\n", - "# construction\n", - "model4 = PowerGridModel(input_data4)\n", - "\n", - "# one-time state estimation\n", - "output_data4 = model4.calculate_state_estimation(\n", - " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.iterative_linear\n", - ")\n", - "\n", - "# result dataset\n", - "print(\"------three-winding transformer result------\")\n", - "print(pd.DataFrame(output_data4[ComponentType.three_winding_transformer]))" - ] - }, - { - "cell_type": "markdown", - "id": "7fb27b941602401d91542211134fc71a", - "metadata": {}, - "source": [ - "## Power flow calculations with automatic tap changing\n", - "\n", - "### Example Network\n", - "\n", - "We use a simple network with 3 nodes, 1 source, 1 load, 1 line, 1 transformer and 1 transformer tap regulator. As shown below:\n", - "\n", - "```txt\n", - " (tap_side) (control side)\n", - "source_1 --- node_2 --- transformer_3 --- node_4 --- line_5 --- node_6 --- load_7\n", - " | |\n", - " transformer_tap_regulator_8 <--------/ (control voltage)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "5ab5adba", - "metadata": {}, - "source": [ - "### Input Dataset\n", - "\n", - "We create an input dataset by using the helper function `initialize_array`. \n", - "\n", - "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "0c89a335", - "metadata": {}, - "outputs": [], - "source": [ - "# node\n", - "node = initialize_array(DatasetType.input, ComponentType.node, 3)\n", - "node[AttributeType.id] = [2, 4, 6]\n", - "node[AttributeType.u_rated] = [1e4, 4e2, 4e2]\n", - "\n", - "# load\n", - "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 1)\n", - "sym_load[AttributeType.id] = [7]\n", - "sym_load[AttributeType.node] = [6]\n", - "sym_load[AttributeType.status] = [1]\n", - "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", - "sym_load[AttributeType.p_specified] = [1e3]\n", - "sym_load[AttributeType.q_specified] = [5e3]\n", - "\n", - "# source\n", - "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", - "source[AttributeType.id] = [1]\n", - "source[AttributeType.node] = [2]\n", - "source[AttributeType.status] = [1]\n", - "source[AttributeType.u_ref] = [1.0]\n", - "\n", - "# line\n", - "line = initialize_array(DatasetType.input, ComponentType.line, 1)\n", - "line[AttributeType.id] = [5]\n", - "line[AttributeType.from_node] = [4]\n", - "line[AttributeType.to_node] = [6]\n", - "line[AttributeType.from_status] = [1]\n", - "line[AttributeType.to_status] = [1]\n", - "line[AttributeType.r1] = [10.0]\n", - "line[AttributeType.x1] = [0.0]\n", - "line[AttributeType.c1] = [0.0]\n", - "line[AttributeType.tan1] = [0.0]\n", - "\n", - "# transformer\n", - "transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)\n", - "transformer[AttributeType.id] = [3]\n", - "transformer[AttributeType.from_node] = [2]\n", - "transformer[AttributeType.to_node] = [4]\n", - "transformer[AttributeType.from_status] = [1]\n", - "transformer[AttributeType.to_status] = [1]\n", - "transformer[AttributeType.u1] = [1e4]\n", - "transformer[AttributeType.u2] = [4e2]\n", - "transformer[AttributeType.sn] = [1e5]\n", - "transformer[AttributeType.uk] = [0.1]\n", - "transformer[AttributeType.pk] = [1e3]\n", - "transformer[AttributeType.i0] = [1.0e-6]\n", - "transformer[AttributeType.p0] = [0.1]\n", - "transformer[AttributeType.winding_from] = [2]\n", - "transformer[AttributeType.winding_to] = [1]\n", - "transformer[AttributeType.clock] = [5]\n", - "transformer[AttributeType.tap_side] = [0]\n", - "transformer[AttributeType.tap_pos] = [3]\n", - "transformer[AttributeType.tap_min] = [-11]\n", - "transformer[AttributeType.tap_max] = [9]\n", - "transformer[AttributeType.tap_size] = [100]\n", - "\n", - "# transformer tap regulator\n", - "transformer_tap_regulator = initialize_array(DatasetType.input, ComponentType.transformer_tap_regulator, 1)\n", - "transformer_tap_regulator[AttributeType.id] = [8]\n", - "transformer_tap_regulator[AttributeType.regulated_object] = [3]\n", - "transformer_tap_regulator[AttributeType.status] = [1]\n", - "transformer_tap_regulator[AttributeType.control_side] = [BranchSide.to_side]\n", - "transformer_tap_regulator[AttributeType.u_set] = [400.0]\n", - "transformer_tap_regulator[AttributeType.u_band] = [20.0]\n", - "transformer_tap_regulator[AttributeType.line_drop_compensation_r] = [0.0]\n", - "transformer_tap_regulator[AttributeType.line_drop_compensation_x] = [0.0]\n", - "\n", - "# all\n", - "input_data5 = {\n", - " ComponentType.node: node,\n", - " ComponentType.line: line,\n", - " ComponentType.transformer: transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - " ComponentType.transformer_tap_regulator: transformer_tap_regulator,\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "a28369ab", - "metadata": {}, - "source": [ - "### One-time Power Flow Calculation\n", - "\n", - "As before, you can call the method `calculate_power_flow` to do a one-time calculation based on the current network data in the model.\n", - "\n", - "For a detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [Power Flow Example](./Power%20Flow%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb). " - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "2ff01266", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "89cf2628", + "metadata": {}, + "source": [ + "# Transformer Example\n", + "\n", + "In this notebook we will present examples of transformers in `power-grid-model`. \n", + "\n", + "Both two-winding transformer and three-winding transformer are covered. We will do one-time power flow calculation and one-time state estimation for both types of tranformers.\n", + "\n", + "This notebook serves as an example of how to use the Python API. For detailed API documentation, refer to\n", + "[Python API reference](../api_reference/python-api-reference.md)\n", + "and [Native Data Interface](../advanced_documentation/native-data-interface.md).\n", + "\n", + "## Transformer (Two-winding Transformer)\n", + "\n", + "Transformer is described as a pi model in `power-grid-model`, and it belongs to the `branch` component type which connects two nodes with possibly different voltage levels.\n", + "\n", + "### Example Network\n", + "\n", + "We use a simple network with 2 nodes, 1 source, 1 load and 1 transformer. As shown below:\n", + "\n", + "```txt\n", + " source_1 --- node_2 --- transformer_3 --- node_4 --- load_5\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ae11dc9a", + "metadata": {}, + "outputs": [], + "source": [ + "# some basic imports\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from power_grid_model import (\n", + " AttributeType,\n", + " BranchSide,\n", + " CalculationMethod,\n", + " CalculationType,\n", + " ComponentType,\n", + " DatasetType,\n", + " LoadGenType,\n", + " MeasuredTerminalType,\n", + " PowerGridModel,\n", + " TapChangingStrategy,\n", + " initialize_array,\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f983cef7", + "metadata": {}, + "source": [ + "### Input Dataset\n", + "\n", + "We create an input dataset by using the helper function `initialize_array`. \n", + "\n", + "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6f008736", + "metadata": {}, + "outputs": [], + "source": [ + "# node\n", + "node = initialize_array(DatasetType.input, ComponentType.node, 2)\n", + "node[AttributeType.id] = np.array([2, 4])\n", + "node[AttributeType.u_rated] = [1e4, 4e2]\n", + "\n", + "# load\n", + "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 1)\n", + "sym_load[AttributeType.id] = [5]\n", + "sym_load[AttributeType.node] = [4]\n", + "sym_load[AttributeType.status] = [1]\n", + "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", + "sym_load[AttributeType.p_specified] = [1e3]\n", + "sym_load[AttributeType.q_specified] = [5e3]\n", + "\n", + "# source\n", + "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", + "source[AttributeType.id] = [1]\n", + "source[AttributeType.node] = [2]\n", + "source[AttributeType.status] = [1]\n", + "source[AttributeType.u_ref] = [1.0]\n", + "\n", + "# transformer\n", + "transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)\n", + "transformer[AttributeType.id] = [3]\n", + "transformer[AttributeType.from_node] = [2]\n", + "transformer[AttributeType.to_node] = [4]\n", + "transformer[AttributeType.from_status] = [1]\n", + "transformer[AttributeType.to_status] = [1]\n", + "transformer[AttributeType.u1] = [1e4]\n", + "transformer[AttributeType.u2] = [4e2]\n", + "transformer[AttributeType.sn] = [1e5]\n", + "transformer[AttributeType.uk] = [0.1]\n", + "transformer[AttributeType.pk] = [1e3]\n", + "transformer[AttributeType.i0] = [1.0e-6]\n", + "transformer[AttributeType.p0] = [0.1]\n", + "transformer[AttributeType.winding_from] = [2]\n", + "transformer[AttributeType.winding_to] = [1]\n", + "transformer[AttributeType.clock] = [5]\n", + "transformer[AttributeType.tap_side] = [0]\n", + "transformer[AttributeType.tap_pos] = [3]\n", + "transformer[AttributeType.tap_min] = [-11]\n", + "transformer[AttributeType.tap_max] = [9]\n", + "transformer[AttributeType.tap_size] = [100]\n", + "\n", + "# all\n", + "input_data = {\n", + " ComponentType.node: node,\n", + " ComponentType.transformer: transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "d16f9dea", + "metadata": {}, + "source": [ + "**We can print the input dataset by converting the numpy array to dataframe.**" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "37749c7c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " id from_node to_node from_status to_status u1 u2 sn \\\n", + "0 3 2 4 1 1 10000.0 400.0 100000.0 \n", + "\n", + " uk pk ... tap_nom tap_size uk_min uk_max pk_min pk_max \\\n", + "0 0.1 1000.0 ... -128 100.0 NaN NaN NaN NaN \n", + "\n", + " r_grounding_from x_grounding_from r_grounding_to x_grounding_to \n", + "0 NaN NaN NaN NaN \n", + "\n", + "[1 rows x 31 columns]\n" + ] + } + ], + "source": [ + "print(pd.DataFrame(input_data[ComponentType.transformer]))" + ] + }, + { + "cell_type": "markdown", + "id": "47a9c257", + "metadata": {}, + "source": [ + "### One-time Power Flow Calculation\n", + "\n", + "You can call the method `calculate_power_flow` to do a one-time calculation based on the current network data in the model.\n", + "\n", + "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [Power Flow Example](./Power%20Flow%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb). " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7bb0f998", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n", + " id energized u_pu u u_angle p \\\n", + "0 2 1 0.999999 9999.994897 -4.976260e-08 1002.882212 \n", + "1 4 1 0.965618 386.247005 -2.618522e+00 -1000.000000 \n", + "\n", + " q \n", + "0 5027.744841 \n", + "1 -5000.000000 \n" + ] + } + ], + "source": [ + "# validation (optional)\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.power_flow)\n", + "\n", + "# construction\n", + "model = PowerGridModel(input_data)\n", + "\n", + "# one-time power flow calculation\n", + "output_data = model.calculate_power_flow(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", + ")\n", + "\n", + "# result dataset\n", + "print(\"------node result------\")\n", + "print(pd.DataFrame(output_data[ComponentType.node]))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "682c1c48", + "metadata": {}, + "source": [ + "### One-time State Estimation\n", + "Below we present a simple example of state estimation for a network with a two-winding transformer. \n", + "\n", + "NOTE: In `power-grid-model`, two-winding transformers belong to `branch` component type, therefore the `measured_terminal_type` of power sensors should be assigned to `MeasuredTerminalType.branch_from/_to`.\n", + "\n", + "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [State Estimation Example](./State%20Estimation%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f0c8c3e8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n", + " id energized u_pu u u_angle p \\\n", + "0 2 1 9.987392 99873.919714 0.000000 470658.914282 \n", + "1 4 1 9.209290 3683.716125 -2.617974 -446912.927660 \n", + "\n", + " q \n", + "0 4.700696e+06 \n", + "1 -4.464515e+06 \n" + ] + } + ], + "source": [ + "# voltage sensor\n", + "sym_voltage_sensor = initialize_array(DatasetType.input, ComponentType.sym_voltage_sensor, 2)\n", + "sym_voltage_sensor[AttributeType.id] = [6, 7]\n", + "sym_voltage_sensor[AttributeType.measured_object] = [2, 4]\n", + "sym_voltage_sensor[AttributeType.u_sigma] = [1.0, 1.0]\n", + "sym_voltage_sensor[AttributeType.u_measured] = [1e5, 4e2]\n", + "\n", + "# power sensor\n", + "sym_power_sensor = initialize_array(DatasetType.input, ComponentType.sym_power_sensor, 2)\n", + "sym_power_sensor[AttributeType.id] = [8, 9]\n", + "sym_power_sensor[AttributeType.measured_object] = [3, 3]\n", + "sym_power_sensor[AttributeType.measured_terminal_type] = [\n", + " MeasuredTerminalType.branch_from,\n", + " MeasuredTerminalType.branch_to,\n", + "]\n", + "sym_power_sensor[AttributeType.power_sigma] = [1.0, 1.0]\n", + "sym_power_sensor[AttributeType.p_measured] = [2e6, 1e5]\n", + "sym_power_sensor[AttributeType.q_measured] = [2e7, 1e6]\n", + "\n", + "# use components from former input dataset cell.\n", + "input_data2 = {\n", + " ComponentType.node: node,\n", + " ComponentType.transformer: transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + " ComponentType.sym_voltage_sensor: sym_voltage_sensor,\n", + " ComponentType.sym_power_sensor: sym_power_sensor,\n", + "}\n", + "\n", + "# validation (optional)\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "assert_valid_input_data(input_data=input_data2, calculation_type=CalculationType.state_estimation)\n", + "\n", + "# construction\n", + "model2 = PowerGridModel(input_data2)\n", + "\n", + "# one-time state estimation\n", + "output_data2 = model2.calculate_state_estimation(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.iterative_linear\n", + ")\n", + "\n", + "# result dataset\n", + "print(\"------node result------\")\n", + "print(pd.DataFrame(output_data2[ComponentType.node]))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e3605c3e", + "metadata": {}, + "source": [ + "## Three-Winding Transformer\n", + "\n", + "Three-winding transformer is described as 3 transformers of pi model each connected together in star configuration, and it belongs to the `branch3` component type which connects three nodes with possibly different voltage levels.\n", + "\n", + "### Example Network\n", + "\n", + "We use a simple network with 3 nodes, 1 source, 1 load and 1 three-winding transformer. As shown below:\n", + "\n", + "```txt\n", + " source_1 --- node_2 --- three_winding_transformer_3 --- node_4 --- load_5\n", + " |\n", + " -------------------node_6 --- load_7\n", + " \n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "84a7f686", + "metadata": {}, + "source": [ + "### Input Dataset\n", + "\n", + "We use the helper function `initialize_array` to create an input dataset.\n", + "\n", + "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "44c2de63", + "metadata": {}, + "outputs": [], + "source": [ + "# node\n", + "node = initialize_array(DatasetType.input, ComponentType.node, 3)\n", + "node[AttributeType.id] = np.array([2, 4, 6])\n", + "node[AttributeType.u_rated] = [1e4, 1e2, 1e2]\n", + "\n", + "# load\n", + "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 2)\n", + "sym_load[AttributeType.id] = [5, 7]\n", + "sym_load[AttributeType.node] = [4, 6]\n", + "sym_load[AttributeType.status] = [1]\n", + "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", + "sym_load[AttributeType.p_specified] = [1e3, 1e3]\n", + "sym_load[AttributeType.q_specified] = [5e3, 5e3]\n", + "\n", + "# source\n", + "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", + "source[AttributeType.id] = [1]\n", + "source[AttributeType.node] = [2]\n", + "source[AttributeType.status] = [1]\n", + "source[AttributeType.u_ref] = [1.0]\n", + "\n", + "# three-winding transformer\n", + "three_winding_transformer = initialize_array(DatasetType.input, ComponentType.three_winding_transformer, 1)\n", + "three_winding_transformer[AttributeType.id] = [3]\n", + "three_winding_transformer[AttributeType.node_1] = [2]\n", + "three_winding_transformer[AttributeType.node_2] = [4]\n", + "three_winding_transformer[AttributeType.node_3] = [6]\n", + "three_winding_transformer[AttributeType.status_1] = [1]\n", + "three_winding_transformer[AttributeType.status_2] = [1]\n", + "three_winding_transformer[AttributeType.status_3] = [1]\n", + "three_winding_transformer[AttributeType.u1] = [1e4]\n", + "three_winding_transformer[AttributeType.u2] = [1e2]\n", + "three_winding_transformer[AttributeType.u3] = [1e2]\n", + "three_winding_transformer[AttributeType.sn_1] = [1e5]\n", + "three_winding_transformer[AttributeType.sn_2] = [1e5]\n", + "three_winding_transformer[AttributeType.sn_3] = [1e5]\n", + "three_winding_transformer[AttributeType.uk_12] = [0.09]\n", + "three_winding_transformer[AttributeType.uk_13] = [0.06]\n", + "three_winding_transformer[AttributeType.uk_23] = [0.06]\n", + "three_winding_transformer[AttributeType.pk_12] = [1e3]\n", + "three_winding_transformer[AttributeType.pk_13] = [1e3]\n", + "three_winding_transformer[AttributeType.pk_23] = [1e3]\n", + "three_winding_transformer[AttributeType.i0] = [0]\n", + "three_winding_transformer[AttributeType.p0] = [0]\n", + "three_winding_transformer[AttributeType.winding_1] = [2]\n", + "three_winding_transformer[AttributeType.winding_2] = [1]\n", + "three_winding_transformer[AttributeType.winding_3] = [1]\n", + "three_winding_transformer[AttributeType.clock_12] = [5]\n", + "three_winding_transformer[AttributeType.clock_13] = [-7] # supports periodic input\n", + "three_winding_transformer[AttributeType.tap_side] = [0]\n", + "three_winding_transformer[AttributeType.tap_pos] = [0]\n", + "three_winding_transformer[AttributeType.tap_min] = [-10]\n", + "three_winding_transformer[AttributeType.tap_max] = [10]\n", + "three_winding_transformer[AttributeType.tap_nom] = [0]\n", + "three_winding_transformer[AttributeType.tap_size] = [1380]\n", + "\n", + "# all\n", + "input_data3 = {\n", + " ComponentType.node: node,\n", + " ComponentType.three_winding_transformer: three_winding_transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "c25444ec", + "metadata": {}, + "source": [ + "### One-time power flow calculation" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b4e63620", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n", + " id energized u_pu u u_angle p \\\n", + "0 2 1 0.999999 9999.989788 -9.966674e-08 2007.896574 \n", + "1 4 1 0.993097 99.309655 -2.618590e+00 -1000.000000 \n", + "2 6 1 0.994637 99.463733 -2.618281e+00 -1000.000000 \n", + "\n", + " q \n", + "0 10062.592544 \n", + "1 -5000.000000 \n", + "2 -5000.000000 \n" + ] + } + ], + "source": [ + "# validation (optional)\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "assert_valid_input_data(input_data=input_data3, calculation_type=CalculationType.power_flow)\n", + "\n", + "# construction\n", + "model3 = PowerGridModel(input_data3)\n", + "\n", + "# one-time power flow calculation\n", + "output_data3 = model3.calculate_power_flow(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", + ")\n", + "\n", + "# result dataset\n", + "print(\"------node result------\")\n", + "print(pd.DataFrame(output_data3[ComponentType.node]))" + ] + }, + { + "cell_type": "markdown", + "id": "36f829f1", + "metadata": {}, + "source": [ + "### One-time State Estimation\n", + "\n", + "Below we present a simple example of state estimation for a network with a three-winding transformer. \n", + "\n", + "NOTE: In `power-grid-model`, three-winding transformers belong to `branch3` component type, therefore the `measured_terminal_type` of power sensors should be assigned to `MeasuredTerminalType.branch3_1/_2/_3`.\n", + "\n", + "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [State Estimation Example](./State%20Estimation%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1ae6c043", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------three-winding transformer result------\n", + " id energized loading_1 loading_2 loading_3 loading p_1 \\\n", + "0 3 1 0.033993 0.016958 0.016967 0.033993 667.156598 \n", + "\n", + " q_1 i_1 s_1 p_2 q_2 i_2 \\\n", + "0 3333.23678 0.196261 3399.347785 -333.400686 -1662.691395 9.813068 \n", + "\n", + " s_2 p_3 q_3 i_3 s_3 \n", + "0 1695.788516 -332.889246 -1663.677083 9.813068 1696.654557 \n" + ] + } + ], + "source": [ + "# voltage sensor\n", + "sym_voltage_sensor = initialize_array(DatasetType.input, ComponentType.sym_voltage_sensor, 3)\n", + "sym_voltage_sensor[AttributeType.id] = [8, 9, 10]\n", + "sym_voltage_sensor[AttributeType.measured_object] = [2, 4, 6]\n", + "sym_voltage_sensor[AttributeType.u_sigma] = [1.0, 1.0, 1.0]\n", + "sym_voltage_sensor[AttributeType.u_measured] = [1e4, 1e2, 1e2]\n", + "\n", + "# power sensor\n", + "sym_power_sensor = initialize_array(DatasetType.input, ComponentType.sym_power_sensor, 3)\n", + "sym_power_sensor[AttributeType.id] = [11, 12, 13]\n", + "sym_power_sensor[AttributeType.measured_object] = [3, 3, 3]\n", + "sym_power_sensor[AttributeType.measured_terminal_type] = [\n", + " MeasuredTerminalType.branch3_1,\n", + " MeasuredTerminalType.branch3_2,\n", + " MeasuredTerminalType.branch3_3,\n", + "]\n", + "sym_power_sensor[AttributeType.power_sigma] = [1.0, 1.0, 1.0]\n", + "sym_power_sensor[AttributeType.p_measured] = [2e3, 1e3, 1e3]\n", + "sym_power_sensor[AttributeType.q_measured] = [1e4, 5e3, 5e3]\n", + "\n", + "# use components from the one-time power flow calculation with three-winding transformer\n", + "input_data4 = {\n", + " ComponentType.node: node,\n", + " ComponentType.three_winding_transformer: three_winding_transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + " ComponentType.sym_voltage_sensor: sym_voltage_sensor,\n", + " ComponentType.sym_power_sensor: sym_power_sensor,\n", + "}\n", + "\n", + "# validation (optional)\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "assert_valid_input_data(input_data=input_data4, calculation_type=CalculationType.state_estimation)\n", + "\n", + "# construction\n", + "model4 = PowerGridModel(input_data4)\n", + "\n", + "# one-time state estimation\n", + "output_data4 = model4.calculate_state_estimation(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.iterative_linear\n", + ")\n", + "\n", + "# result dataset\n", + "print(\"------three-winding transformer result------\")\n", + "print(pd.DataFrame(output_data4[ComponentType.three_winding_transformer]))" + ] + }, + { + "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", + "metadata": {}, + "source": [ + "## Power flow calculations with automatic tap changing\n", + "\n", + "### Example Network\n", + "\n", + "We use a simple network with 3 nodes, 1 source, 1 load, 1 line, 1 transformer and 1 transformer tap regulator. As shown below:\n", + "\n", + "```txt\n", + " (tap_side) (control side)\n", + "source_1 --- node_2 --- transformer_3 --- node_4 --- line_5 --- node_6 --- load_7\n", + " | |\n", + " transformer_tap_regulator_8 <--------/ (control voltage)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "5ab5adba", + "metadata": {}, + "source": [ + "### Input Dataset\n", + "\n", + "We create an input dataset by using the helper function `initialize_array`. \n", + "\n", + "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0c89a335", + "metadata": {}, + "outputs": [], + "source": [ + "# node\n", + "node = initialize_array(DatasetType.input, ComponentType.node, 3)\n", + "node[AttributeType.id] = [2, 4, 6]\n", + "node[AttributeType.u_rated] = [1e4, 4e2, 4e2]\n", + "\n", + "# load\n", + "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 1)\n", + "sym_load[AttributeType.id] = [7]\n", + "sym_load[AttributeType.node] = [6]\n", + "sym_load[AttributeType.status] = [1]\n", + "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", + "sym_load[AttributeType.p_specified] = [1e3]\n", + "sym_load[AttributeType.q_specified] = [5e3]\n", + "\n", + "# source\n", + "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", + "source[AttributeType.id] = [1]\n", + "source[AttributeType.node] = [2]\n", + "source[AttributeType.status] = [1]\n", + "source[AttributeType.u_ref] = [1.0]\n", + "\n", + "# line\n", + "line = initialize_array(DatasetType.input, ComponentType.line, 1)\n", + "line[AttributeType.id] = [5]\n", + "line[AttributeType.from_node] = [4]\n", + "line[AttributeType.to_node] = [6]\n", + "line[AttributeType.from_status] = [1]\n", + "line[AttributeType.to_status] = [1]\n", + "line[AttributeType.r1] = [10.0]\n", + "line[AttributeType.x1] = [0.0]\n", + "line[AttributeType.c1] = [0.0]\n", + "line[AttributeType.tan1] = [0.0]\n", + "\n", + "# transformer\n", + "transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)\n", + "transformer[AttributeType.id] = [3]\n", + "transformer[AttributeType.from_node] = [2]\n", + "transformer[AttributeType.to_node] = [4]\n", + "transformer[AttributeType.from_status] = [1]\n", + "transformer[AttributeType.to_status] = [1]\n", + "transformer[AttributeType.u1] = [1e4]\n", + "transformer[AttributeType.u2] = [4e2]\n", + "transformer[AttributeType.sn] = [1e5]\n", + "transformer[AttributeType.uk] = [0.1]\n", + "transformer[AttributeType.pk] = [1e3]\n", + "transformer[AttributeType.i0] = [1.0e-6]\n", + "transformer[AttributeType.p0] = [0.1]\n", + "transformer[AttributeType.winding_from] = [2]\n", + "transformer[AttributeType.winding_to] = [1]\n", + "transformer[AttributeType.clock] = [5]\n", + "transformer[AttributeType.tap_side] = [0]\n", + "transformer[AttributeType.tap_pos] = [3]\n", + "transformer[AttributeType.tap_min] = [-11]\n", + "transformer[AttributeType.tap_max] = [9]\n", + "transformer[AttributeType.tap_size] = [100]\n", + "\n", + "# transformer tap regulator\n", + "transformer_tap_regulator = initialize_array(DatasetType.input, ComponentType.transformer_tap_regulator, 1)\n", + "transformer_tap_regulator[AttributeType.id] = [8]\n", + "transformer_tap_regulator[AttributeType.regulated_object] = [3]\n", + "transformer_tap_regulator[AttributeType.status] = [1]\n", + "transformer_tap_regulator[AttributeType.control_side] = [BranchSide.to_side]\n", + "transformer_tap_regulator[AttributeType.u_set] = [400.0]\n", + "transformer_tap_regulator[AttributeType.u_band] = [20.0]\n", + "transformer_tap_regulator[AttributeType.line_drop_compensation_r] = [0.0]\n", + "transformer_tap_regulator[AttributeType.line_drop_compensation_x] = [0.0]\n", + "\n", + "# all\n", + "input_data5 = {\n", + " ComponentType.node: node,\n", + " ComponentType.line: line,\n", + " ComponentType.transformer: transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + " ComponentType.transformer_tap_regulator: transformer_tap_regulator,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "a28369ab", + "metadata": {}, + "source": [ + "### One-time Power Flow Calculation\n", + "\n", + "As before, you can call the method `calculate_power_flow` to do a one-time calculation based on the current network data in the model.\n", + "\n", + "For a detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [Power Flow Example](./Power%20Flow%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb). " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2ff01266", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994637
14386.141058
26322.704292
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994637\n", + "1 4 386.141058\n", + "2 6 322.704292" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# construction\n", + "model5 = PowerGridModel(input_data5)\n", + "\n", + "# one-time power flow calculation without automatic tap changing\n", + "output_data5 = model5.calculate_power_flow()\n", + "\n", + "# Both load-side nodes node_4 and node_6 have a voltage below 400 V\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])" + ] + }, + { + "cell_type": "markdown", + "id": "07af8e93", + "metadata": {}, + "source": [ + "### Any voltage tap position strategy\n", + "\n", + "In the regular one-time power flow calculation in the example above, the voltage at the low-voltage side of the transformer is lower than 390 V.\n", + "\n", + "To get a voltage at the control side of the transformer in the acceptable voltage band as provided to the transformer tap regulator, run the power flow calculation with any the tap changing strategy set to any valid tap." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7ad86bf0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994657
14393.881868
26334.529465
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994657\n", + "1 4 393.881868\n", + "2 6 334.529465" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data6 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.any_valid_tap)\n", + "\n", + "# the node at the control side of the transformer now has a voltage within the specified voltage band\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data6[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" + ] + }, + { + "cell_type": "markdown", + "id": "a85cba49", + "metadata": {}, + "source": [ + "### Model state from a tap-changing power flow\n", + "\n", + "Model-state output can also be requested when automatic tap changing is enabled. This example starts from a fresh model, then inspects the state returned with the selected tap-changing result. The iteration histories describe the power-flow solve for the selected tap, not every tap considered during optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7d15a1e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Selected tap position:\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Y-bus nonzero entries: 7\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
admittance
rowcolumn
000.016000+0.000000j
1-0.016000+0.000000j
10-0.016000+0.000000j
10.116000-0.994987j
20.578313-0.803648j
21-0.406823-0.902658j
20.098030-0.975382j
\n", + "
" + ], + "text/plain": [ + " admittance\n", + "row column \n", + "0 0 0.016000+0.000000j\n", + " 1 -0.016000+0.000000j\n", + "1 0 -0.016000+0.000000j\n", + " 1 0.116000-0.994987j\n", + " 2 0.578313-0.803648j\n", + "2 1 -0.406823-0.902658j\n", + " 2 0.098030-0.975382j" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Jacobian iterations: [1, 2, 3, 4, 5]\n", + "Final Jacobian (selected tap):\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
HNML
rowcolumn
000.0050000.010191-0.012191-0.005000
1-0.005000-0.0121910.012191-0.005000
100.005000-0.0121910.0121910.005000
10.9647830.112479-0.1124790.964783
2-0.969783-0.1002880.100288-0.969783
21-0.970344-0.0947020.094702-0.970344
29951.336650995.134156-995.1341569951.336650
\n", + "
" + ], + "text/plain": [ + " H N M L\n", + "row column \n", + "0 0 0.005000 0.010191 -0.012191 -0.005000\n", + " 1 -0.005000 -0.012191 0.012191 -0.005000\n", + "1 0 0.005000 -0.012191 0.012191 0.005000\n", + " 1 0.964783 0.112479 -0.112479 0.964783\n", + " 2 -0.969783 -0.100288 0.100288 -0.969783\n", + "2 1 -0.970344 -0.094702 0.094702 -0.970344\n", + " 2 9951.336650 995.134156 -995.134156 9951.336650" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nodal-state iterations: [1, 2, 3, 4, 5]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
|V|, iteration 1 (p.u.)angle, iteration 1 (rad)|V|, iteration 2 (p.u.)angle, iteration 2 (rad)|V|, iteration 3 (p.u.)angle, iteration 3 (rad)|V|, iteration 4 (p.u.)angle, iteration 4 (rad)|V|, iteration 5 (p.u.)angle, iteration 5 (rad)
node
21.000000-1.675680e-070.999999-2.576861e-070.999999-2.804433e-070.999999-2.809399e-070.999999-2.809402e-07
40.985887-2.619709e+000.984731-2.620633e+000.984705-2.620868e+000.984705-2.620873e+000.984705-2.620873e+00
60.890190-2.333657e+000.846186-2.242660e+000.836566-2.231922e+000.836324-2.231657e+000.836324-2.231657e+00
\n", + "
" + ], + "text/plain": [ + " |V|, iteration 1 (p.u.) angle, iteration 1 (rad) \\\n", + "node \n", + "2 1.000000 -1.675680e-07 \n", + "4 0.985887 -2.619709e+00 \n", + "6 0.890190 -2.333657e+00 \n", + "\n", + " |V|, iteration 2 (p.u.) angle, iteration 2 (rad) \\\n", + "node \n", + "2 0.999999 -2.576861e-07 \n", + "4 0.984731 -2.620633e+00 \n", + "6 0.846186 -2.242660e+00 \n", + "\n", + " |V|, iteration 3 (p.u.) angle, iteration 3 (rad) \\\n", + "node \n", + "2 0.999999 -2.804433e-07 \n", + "4 0.984705 -2.620868e+00 \n", + "6 0.836566 -2.231922e+00 \n", + "\n", + " |V|, iteration 4 (p.u.) angle, iteration 4 (rad) \\\n", + "node \n", + "2 0.999999 -2.809399e-07 \n", + "4 0.984705 -2.620873e+00 \n", + "6 0.836324 -2.231657e+00 \n", + "\n", + " |V|, iteration 5 (p.u.) angle, iteration 5 (rad) \n", + "node \n", + "2 0.999999 -2.809402e-07 \n", + "4 0.984705 -2.620873e+00 \n", + "6 0.836324 -2.231657e+00 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "model5_state = PowerGridModel(input_data5)\n", + "output_data_state, model_state = model5_state.calculate_power_flow(\n", + " tap_changing_strategy=TapChangingStrategy.any_valid_tap,\n", + " get_model_state=True,\n", + ")\n", + "\n", + "print(\"Selected tap position:\")\n", + "display(pd.DataFrame(output_data_state[ComponentType.transformer_tap_regulator]))\n", + "\n", + "group_state = model_state.groups[0]\n", + "y_bus = group_state.y_bus\n", + "y_bus_rows = np.repeat(np.arange(y_bus.row_indptr.size - 1), np.diff(y_bus.row_indptr))\n", + "print(f\"Y-bus nonzero entries: {len(y_bus.col_indices)}\")\n", + "display(\n", + " pd.DataFrame(\n", + " {\n", + " \"row\": y_bus_rows,\n", + " \"column\": y_bus.col_indices,\n", + " \"admittance\": y_bus.admittance_real + 1j * y_bus.admittance_imag,\n", + " }\n", + " ).set_index([\"row\", \"column\"])\n", + ")\n", + "\n", + "print(\"Jacobian iterations:\", [jac.iteration for jac in group_state.jacobians])\n", + "if group_state.jacobians:\n", + " jacobian = group_state.jacobians[-1]\n", + " pattern = group_state.jacobian_structure\n", + " jacobian_rows = np.repeat(\n", + " np.arange(pattern.row_indptr_lu.size - 1), np.diff(pattern.row_indptr_lu)\n", + " )\n", + " print(\"Final Jacobian (selected tap):\")\n", + " display(\n", + " pd.DataFrame(\n", + " {\n", + " \"row\": jacobian_rows,\n", + " \"column\": pattern.col_indices_lu,\n", + " \"H\": jacobian.h,\n", + " \"N\": jacobian.n,\n", + " \"M\": jacobian.m,\n", + " \"L\": jacobian.l,\n", + " }\n", + " ).set_index([\"row\", \"column\"])\n", + " )\n", + "print(\"Nodal-state iterations:\", [state.iteration for state in group_state.nodal_states])\n", + "voltage_rows = []\n", + "for input_idx, node_id in enumerate(model_state.input_node_id):\n", + " if model_state.input_node_group[input_idx] != group_state.mapping.group:\n", + " continue\n", + " bus_idx = int(model_state.input_node_bus[input_idx])\n", + " row = {\"node\": node_id}\n", + " for state in group_state.nodal_states:\n", + " row[f\"|V|, iteration {state.iteration} (p.u.)\"] = state.voltage_magnitude[bus_idx]\n", + " row[f\"angle, iteration {state.iteration} (rad)\"] = state.voltage_angle[bus_idx]\n", + " voltage_rows.append(row)\n", + "display(pd.DataFrame(voltage_rows).set_index(\"node\"))" + ] + }, + { + "cell_type": "markdown", + "id": "9f1a6f30", + "metadata": {}, + "source": [ + "You could also opt for fast_any_tap that takes advantage of binary search instead of linear search internally." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1fa08adb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994675
14401.932237
26346.203709
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994675\n", + "1 4 401.932237\n", + "2 6 346.203709" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n", + "\n", + "----------fast_any_tap----------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
081-1
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 -1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data6f = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.fast_any_tap)\n", + "\n", + "# the node at the control side of the transformer now has a voltage within the specified voltage band\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data6f[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "print(\"\\n----------fast_any_tap----------\")\n", + "display(pd.DataFrame(output_data6f[ComponentType.transformer_tap_regulator]))" + ] + }, + { + "cell_type": "markdown", + "id": "7828a4e3", + "metadata": {}, + "source": [ + "**NOTE:** the tap positions obtained using the `any_valid_tap` and `fast_any_tap` strategy may depend on the initial tap position of the transformers." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "be276930", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------transformer batch update------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idfrom_statusto_statustap_pos
03-128-1280
13-128-1281
\n", + "
" + ], + "text/plain": [ + " id from_status to_status tap_pos\n", + "0 3 -128 -128 0\n", + "1 3 -128 -128 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node_4 batch result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
04397.867176
14393.881868
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 4 397.867176\n", + "1 4 393.881868" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator batch result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0810
1811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 0\n", + "1 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "transformer_update = initialize_array(DatasetType.update, ComponentType.transformer, (2, 1))\n", + "transformer_update[AttributeType.id] = 3\n", + "transformer_update[AttributeType.tap_pos] = [[0], [1]]\n", + "\n", + "update_data = {ComponentType.transformer: transformer_update}\n", + "\n", + "print(\"------transformer batch update------\")\n", + "display(pd.DataFrame(update_data[ComponentType.transformer][:, 0]))\n", + "\n", + "# power flow batch calculation with automatic tap changing\n", + "output_data = model5.calculate_power_flow(\n", + " update_data=update_data, tap_changing_strategy=TapChangingStrategy.fast_any_tap\n", + ")\n", + "\n", + "print(\"------node_4 batch result------\")\n", + "display(pd.DataFrame(output_data[ComponentType.node][:, 1])[[AttributeType.id, AttributeType.u]]) # only output node 1\n", + "\n", + "print(\"\\n------tap regulator batch result------\")\n", + "display(pd.DataFrame(output_data[ComponentType.transformer_tap_regulator][:, 0]))" + ] + }, + { + "cell_type": "markdown", + "id": "6f6cd15c", + "metadata": {}, + "source": [ + "### Maximum voltage tap position strategy\n", + "\n", + "To specifically optimize for the higher end of the voltage band, the `max_voltage_tap` tap changing strategy can be provided." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "21b01a77", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994683
14406.079525
26352.021534
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994683\n", + "1 4 406.079525\n", + "2 6 352.021534" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
081-2
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 -2" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data5 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.max_voltage_tap)\n", + "\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.transformer_tap_regulator]))" + ] + }, + { + "cell_type": "markdown", + "id": "7b8ab2a2", + "metadata": {}, + "source": [ + "### Minimum voltage tap position strategy\n", + "\n", + "To specifically optimize for the lower end of the voltage band, the `min_voltage_tap` tap changing strategy can be provided." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "c0314199", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994657
14393.881868
26334.529465
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994657\n", + "1 4 393.881868\n", + "2 6 334.529465" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data5 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.min_voltage_tap)\n", + "\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.transformer_tap_regulator]))" + ] + }, + { + "cell_type": "markdown", + "id": "01a473bd", + "metadata": {}, + "source": [ + "### Automatic tap changing with line drop compensation\n", + "\n", + "In the above examples, the voltage at the low-voltage side of the transformer is controlled. However, the load is still connected to a node with a voltage lower than 390 V.\n", + "\n", + "To regulate the node that the load is connected to, the voltage needs to be compensated for the voltage drop over the line.\n", + "\n", + "```txt\n", + " (tap_side) (control side)\n", + "source_1 --- node_2 --- transformer_3 --- node_4 --- line_5 --- node_6 --- load_7\n", + " | | :\n", + " regulator_8 <-----/ <......................./ (line drop compensation)\n", + " (control voltage) \n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ac091392", + "metadata": {}, + "outputs": [], + "source": [ + "# use the same input data\n", + "input_data6 = {component: component_data.copy() for component, component_data in input_data5.items()}\n", + "\n", + "# set the regulator's line drop compensation\n", + "input_data6[ComponentType.transformer_tap_regulator][AttributeType.line_drop_compensation_r] = [10.0]\n", + "\n", + "# construction\n", + "model6 = PowerGridModel(input_data=input_data6)" + ] + }, + { + "cell_type": "markdown", + "id": "bb89ad98", + "metadata": {}, + "source": [ + "The result of the power flow calculation with automatic tap changing contains a voltage at the node that the load is connected to that is within the specified voltage band." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "9b66d16e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994731
14437.642686
26393.354002
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994731\n", + "1 4 437.642686\n", + "2 6 393.354002" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
081-9
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 -9" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data6 = model6.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.any_valid_tap)\n", + "\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data6[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "power-grid-model", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.8" + } }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994637
14386.141058
26322.704292
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994637\n", - "1 4 386.141058\n", - "2 6 322.704292" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# construction\n", - "model5 = PowerGridModel(input_data5)\n", - "\n", - "# one-time power flow calculation without automatic tap changing\n", - "output_data5 = model5.calculate_power_flow()\n", - "\n", - "# Both load-side nodes node_4 and node_6 have a voltage below 400 V\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])" - ] - }, - { - "cell_type": "markdown", - "id": "07af8e93", - "metadata": {}, - "source": [ - "### Any voltage tap position strategy\n", - "\n", - "In the regular one-time power flow calculation in the example above, the voltage at the low-voltage side of the transformer is lower than 390 V.\n", - "\n", - "To get a voltage at the control side of the transformer in the acceptable voltage band as provided to the transformer tap regulator, run the power flow calculation with any the tap changing strategy set to any valid tap." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "7ad86bf0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994657
14393.881868
26334.529465
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994657\n", - "1 4 393.881868\n", - "2 6 334.529465" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
0811
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data6 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.any_valid_tap)\n", - "\n", - "# the node at the control side of the transformer now has a voltage within the specified voltage band\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data6[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" - ] - }, - { - "cell_type": "markdown", - "id": "a85cba49", - "metadata": {}, - "source": [ - "### Model state from a tap-changing power flow\n", - "\n", - "Model-state output can also be requested when automatic tap changing is enabled. This example starts from a fresh model, then inspects the state returned with the selected tap-changing result. The iteration histories describe the power-flow solve for the selected tap, not every tap considered during optimization." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "7d15a1e7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Selected tap position:\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
0811
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Y-bus nonzero entries: 7\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
admittance
rowcolumn
000.016000+0.000000j
1-0.016000+0.000000j
10-0.016000+0.000000j
10.116000-0.994987j
20.578313-0.803648j
21-0.406823-0.902658j
20.098030-0.975382j
\n", - "
" - ], - "text/plain": [ - " admittance\n", - "row column \n", - "0 0 0.016000+0.000000j\n", - " 1 -0.016000+0.000000j\n", - "1 0 -0.016000+0.000000j\n", - " 1 0.116000-0.994987j\n", - " 2 0.578313-0.803648j\n", - "2 1 -0.406823-0.902658j\n", - " 2 0.098030-0.975382j" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Jacobian iterations: [1, 2, 3, 4, 5]\n", - "Final Jacobian (selected tap):\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
HNML
rowcolumn
000.0050000.010191-0.012191-0.005000
1-0.005000-0.0121910.012191-0.005000
100.005000-0.0121910.0121910.005000
10.9647830.112479-0.1124790.964783
2-0.969783-0.1002880.100288-0.969783
21-0.970344-0.0947020.094702-0.970344
29951.336650995.134156-995.1341569951.336650
\n", - "
" - ], - "text/plain": [ - " H N M L\n", - "row column \n", - "0 0 0.005000 0.010191 -0.012191 -0.005000\n", - " 1 -0.005000 -0.012191 0.012191 -0.005000\n", - "1 0 0.005000 -0.012191 0.012191 0.005000\n", - " 1 0.964783 0.112479 -0.112479 0.964783\n", - " 2 -0.969783 -0.100288 0.100288 -0.969783\n", - "2 1 -0.970344 -0.094702 0.094702 -0.970344\n", - " 2 9951.336650 995.134156 -995.134156 9951.336650" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Nodal-state iterations: [1, 2, 3, 4, 5]\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
|V|, iteration 1 (p.u.)angle, iteration 1 (rad)|V|, iteration 2 (p.u.)angle, iteration 2 (rad)|V|, iteration 3 (p.u.)angle, iteration 3 (rad)|V|, iteration 4 (p.u.)angle, iteration 4 (rad)|V|, iteration 5 (p.u.)angle, iteration 5 (rad)
node
21.000000-1.675680e-070.999999-2.576861e-070.999999-2.804433e-070.999999-2.809399e-070.999999-2.809402e-07
40.985887-2.619709e+000.984731-2.620633e+000.984705-2.620868e+000.984705-2.620873e+000.984705-2.620873e+00
60.890190-2.333657e+000.846186-2.242660e+000.836566-2.231922e+000.836324-2.231657e+000.836324-2.231657e+00
\n", - "
" - ], - "text/plain": [ - " |V|, iteration 1 (p.u.) angle, iteration 1 (rad) \\\n", - "node \n", - "2 1.000000 -1.675680e-07 \n", - "4 0.985887 -2.619709e+00 \n", - "6 0.890190 -2.333657e+00 \n", - "\n", - " |V|, iteration 2 (p.u.) angle, iteration 2 (rad) \\\n", - "node \n", - "2 0.999999 -2.576861e-07 \n", - "4 0.984731 -2.620633e+00 \n", - "6 0.846186 -2.242660e+00 \n", - "\n", - " |V|, iteration 3 (p.u.) angle, iteration 3 (rad) \\\n", - "node \n", - "2 0.999999 -2.804433e-07 \n", - "4 0.984705 -2.620868e+00 \n", - "6 0.836566 -2.231922e+00 \n", - "\n", - " |V|, iteration 4 (p.u.) angle, iteration 4 (rad) \\\n", - "node \n", - "2 0.999999 -2.809399e-07 \n", - "4 0.984705 -2.620873e+00 \n", - "6 0.836324 -2.231657e+00 \n", - "\n", - " |V|, iteration 5 (p.u.) angle, iteration 5 (rad) \n", - "node \n", - "2 0.999999 -2.809402e-07 \n", - "4 0.984705 -2.620873e+00 \n", - "6 0.836324 -2.231657e+00 " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "model5_state = PowerGridModel(input_data5)\n", - "output_data_state, model_state = model5_state.calculate_power_flow(\n", - " tap_changing_strategy=TapChangingStrategy.any_valid_tap,\n", - " get_model_state=True,\n", - ")\n", - "\n", - "print(\"Selected tap position:\")\n", - "display(pd.DataFrame(output_data_state[ComponentType.transformer_tap_regulator]))\n", - "\n", - "group_state = model_state.groups[0]\n", - "y_bus = group_state.y_bus\n", - "y_bus_rows = np.repeat(np.arange(y_bus.row_indptr.size - 1), np.diff(y_bus.row_indptr))\n", - "print(f\"Y-bus nonzero entries: {len(y_bus.col_indices)}\")\n", - "display(\n", - " pd.DataFrame(\n", - " {\n", - " \"row\": y_bus_rows,\n", - " \"column\": y_bus.col_indices,\n", - " \"admittance\": y_bus.admittance_real + 1j * y_bus.admittance_imag,\n", - " }\n", - " ).set_index([\"row\", \"column\"])\n", - ")\n", - "\n", - "print(\"Jacobian iterations:\", [jac.iteration for jac in group_state.jacobians])\n", - "if group_state.jacobians:\n", - " jacobian = group_state.jacobians[-1]\n", - " pattern = group_state.jacobian_structure\n", - " jacobian_rows = np.repeat(\n", - " np.arange(pattern.row_indptr_lu.size - 1), np.diff(pattern.row_indptr_lu)\n", - " )\n", - " print(\"Final Jacobian (selected tap):\")\n", - " display(\n", - " pd.DataFrame(\n", - " {\n", - " \"row\": jacobian_rows,\n", - " \"column\": pattern.col_indices_lu,\n", - " \"H\": jacobian.h,\n", - " \"N\": jacobian.n,\n", - " \"M\": jacobian.m,\n", - " \"L\": jacobian.l,\n", - " }\n", - " ).set_index([\"row\", \"column\"])\n", - " )\n", - "print(\"Nodal-state iterations:\", [state.iteration for state in group_state.nodal_states])\n", - "voltage_rows = []\n", - "for input_idx, node_id in enumerate(model_state.input_node_id):\n", - " if model_state.input_node_group[input_idx] != group_state.mapping.group:\n", - " continue\n", - " bus_idx = int(model_state.input_node_bus[input_idx])\n", - " row = {\"node\": node_id}\n", - " for state in group_state.nodal_states:\n", - " row[f\"|V|, iteration {state.iteration} (p.u.)\"] = state.voltage_magnitude[bus_idx]\n", - " row[f\"angle, iteration {state.iteration} (rad)\"] = state.voltage_angle[bus_idx]\n", - " voltage_rows.append(row)\n", - "display(pd.DataFrame(voltage_rows).set_index(\"node\"))" - ] - }, - { - "cell_type": "markdown", - "id": "9f1a6f30", - "metadata": {}, - "source": [ - "You could also opt for fast_any_tap that takes advantage of binary search instead of linear search internally." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "1fa08adb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994675
14401.932237
26346.203709
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994675\n", - "1 4 401.932237\n", - "2 6 346.203709" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n", - "\n", - "----------fast_any_tap----------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
081-1
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 -1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data6f = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.fast_any_tap)\n", - "\n", - "# the node at the control side of the transformer now has a voltage within the specified voltage band\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data6f[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "print(\"\\n----------fast_any_tap----------\")\n", - "display(pd.DataFrame(output_data6f[ComponentType.transformer_tap_regulator]))" - ] - }, - { - "cell_type": "markdown", - "id": "7828a4e3", - "metadata": {}, - "source": [ - "**NOTE:** the tap positions obtained using the `any_valid_tap` and `fast_any_tap` strategy may depend on the initial tap position of the transformers." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "be276930", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------transformer batch update------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idfrom_statusto_statustap_pos
03-128-1280
13-128-1281
\n", - "
" - ], - "text/plain": [ - " id from_status to_status tap_pos\n", - "0 3 -128 -128 0\n", - "1 3 -128 -128 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node_4 batch result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
04397.867176
14393.881868
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 4 397.867176\n", - "1 4 393.881868" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator batch result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
0810
1811
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 0\n", - "1 8 1 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "transformer_update = initialize_array(DatasetType.update, ComponentType.transformer, (2, 1))\n", - "transformer_update[AttributeType.id] = 3\n", - "transformer_update[AttributeType.tap_pos] = [[0], [1]]\n", - "\n", - "update_data = {ComponentType.transformer: transformer_update}\n", - "\n", - "print(\"------transformer batch update------\")\n", - "display(pd.DataFrame(update_data[ComponentType.transformer][:, 0]))\n", - "\n", - "# power flow batch calculation with automatic tap changing\n", - "output_data = model5.calculate_power_flow(\n", - " update_data=update_data, tap_changing_strategy=TapChangingStrategy.fast_any_tap\n", - ")\n", - "\n", - "print(\"------node_4 batch result------\")\n", - "display(pd.DataFrame(output_data[ComponentType.node][:, 1])[[AttributeType.id, AttributeType.u]]) # only output node 1\n", - "\n", - "print(\"\\n------tap regulator batch result------\")\n", - "display(pd.DataFrame(output_data[ComponentType.transformer_tap_regulator][:, 0]))" - ] - }, - { - "cell_type": "markdown", - "id": "6f6cd15c", - "metadata": {}, - "source": [ - "### Maximum voltage tap position strategy\n", - "\n", - "To specifically optimize for the higher end of the voltage band, the `max_voltage_tap` tap changing strategy can be provided." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "21b01a77", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994683
14406.079525
26352.021534
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994683\n", - "1 4 406.079525\n", - "2 6 352.021534" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
081-2
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 -2" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data5 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.max_voltage_tap)\n", - "\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.transformer_tap_regulator]))" - ] - }, - { - "cell_type": "markdown", - "id": "7b8ab2a2", - "metadata": {}, - "source": [ - "### Minimum voltage tap position strategy\n", - "\n", - "To specifically optimize for the lower end of the voltage band, the `min_voltage_tap` tap changing strategy can be provided." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "c0314199", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994657
14393.881868
26334.529465
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994657\n", - "1 4 393.881868\n", - "2 6 334.529465" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
0811
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data5 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.min_voltage_tap)\n", - "\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.transformer_tap_regulator]))" - ] - }, - { - "cell_type": "markdown", - "id": "01a473bd", - "metadata": {}, - "source": [ - "### Automatic tap changing with line drop compensation\n", - "\n", - "In the above examples, the voltage at the low-voltage side of the transformer is controlled. However, the load is still connected to a node with a voltage lower than 390 V.\n", - "\n", - "To regulate the node that the load is connected to, the voltage needs to be compensated for the voltage drop over the line.\n", - "\n", - "```txt\n", - " (tap_side) (control side)\n", - "source_1 --- node_2 --- transformer_3 --- node_4 --- line_5 --- node_6 --- load_7\n", - " | | :\n", - " regulator_8 <-----/ <......................./ (line drop compensation)\n", - " (control voltage) \n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "ac091392", - "metadata": {}, - "outputs": [], - "source": [ - "# use the same input data\n", - "input_data6 = {component: component_data.copy() for component, component_data in input_data5.items()}\n", - "\n", - "# set the regulator's line drop compensation\n", - "input_data6[ComponentType.transformer_tap_regulator][AttributeType.line_drop_compensation_r] = [10.0]\n", - "\n", - "# construction\n", - "model6 = PowerGridModel(input_data=input_data6)" - ] - }, - { - "cell_type": "markdown", - "id": "bb89ad98", - "metadata": {}, - "source": [ - "The result of the power flow calculation with automatic tap changing contains a voltage at the node that the load is connected to that is within the specified voltage band." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "9b66d16e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994731
14437.642686
26393.354002
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994731\n", - "1 4 437.642686\n", - "2 6 393.354002" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
081-9
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 -9" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data6 = model6.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.any_valid_tap)\n", - "\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data6[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "power-grid-model", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.14.8" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file From 3151a99ff18413a9d91c7ef22d7b616cf7574a91 Mon Sep 17 00:00:00 2001 From: Jerry Jinfeng Guo Date: Tue, 27 Oct 2026 10:54:18 +0100 Subject: [PATCH 25/25] format Signed-off-by: Jerry Jinfeng Guo --- .../Transformer Examples.ipynb | 4404 ++++++++--------- 1 file changed, 2201 insertions(+), 2203 deletions(-) diff --git a/docs/example_notebooks/Transformer Examples.ipynb b/docs/example_notebooks/Transformer Examples.ipynb index f38a9f918c..a8b8fee0b5 100644 --- a/docs/example_notebooks/Transformer Examples.ipynb +++ b/docs/example_notebooks/Transformer Examples.ipynb @@ -1,2206 +1,2204 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "89cf2628", - "metadata": {}, - "source": [ - "# Transformer Example\n", - "\n", - "In this notebook we will present examples of transformers in `power-grid-model`. \n", - "\n", - "Both two-winding transformer and three-winding transformer are covered. We will do one-time power flow calculation and one-time state estimation for both types of tranformers.\n", - "\n", - "This notebook serves as an example of how to use the Python API. For detailed API documentation, refer to\n", - "[Python API reference](../api_reference/python-api-reference.md)\n", - "and [Native Data Interface](../advanced_documentation/native-data-interface.md).\n", - "\n", - "## Transformer (Two-winding Transformer)\n", - "\n", - "Transformer is described as a pi model in `power-grid-model`, and it belongs to the `branch` component type which connects two nodes with possibly different voltage levels.\n", - "\n", - "### Example Network\n", - "\n", - "We use a simple network with 2 nodes, 1 source, 1 load and 1 transformer. As shown below:\n", - "\n", - "```txt\n", - " source_1 --- node_2 --- transformer_3 --- node_4 --- load_5\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "ae11dc9a", - "metadata": {}, - "outputs": [], - "source": [ - "# some basic imports\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "from power_grid_model import (\n", - " AttributeType,\n", - " BranchSide,\n", - " CalculationMethod,\n", - " CalculationType,\n", - " ComponentType,\n", - " DatasetType,\n", - " LoadGenType,\n", - " MeasuredTerminalType,\n", - " PowerGridModel,\n", - " TapChangingStrategy,\n", - " initialize_array,\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f983cef7", - "metadata": {}, - "source": [ - "### Input Dataset\n", - "\n", - "We create an input dataset by using the helper function `initialize_array`. \n", - "\n", - "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "6f008736", - "metadata": {}, - "outputs": [], - "source": [ - "# node\n", - "node = initialize_array(DatasetType.input, ComponentType.node, 2)\n", - "node[AttributeType.id] = np.array([2, 4])\n", - "node[AttributeType.u_rated] = [1e4, 4e2]\n", - "\n", - "# load\n", - "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 1)\n", - "sym_load[AttributeType.id] = [5]\n", - "sym_load[AttributeType.node] = [4]\n", - "sym_load[AttributeType.status] = [1]\n", - "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", - "sym_load[AttributeType.p_specified] = [1e3]\n", - "sym_load[AttributeType.q_specified] = [5e3]\n", - "\n", - "# source\n", - "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", - "source[AttributeType.id] = [1]\n", - "source[AttributeType.node] = [2]\n", - "source[AttributeType.status] = [1]\n", - "source[AttributeType.u_ref] = [1.0]\n", - "\n", - "# transformer\n", - "transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)\n", - "transformer[AttributeType.id] = [3]\n", - "transformer[AttributeType.from_node] = [2]\n", - "transformer[AttributeType.to_node] = [4]\n", - "transformer[AttributeType.from_status] = [1]\n", - "transformer[AttributeType.to_status] = [1]\n", - "transformer[AttributeType.u1] = [1e4]\n", - "transformer[AttributeType.u2] = [4e2]\n", - "transformer[AttributeType.sn] = [1e5]\n", - "transformer[AttributeType.uk] = [0.1]\n", - "transformer[AttributeType.pk] = [1e3]\n", - "transformer[AttributeType.i0] = [1.0e-6]\n", - "transformer[AttributeType.p0] = [0.1]\n", - "transformer[AttributeType.winding_from] = [2]\n", - "transformer[AttributeType.winding_to] = [1]\n", - "transformer[AttributeType.clock] = [5]\n", - "transformer[AttributeType.tap_side] = [0]\n", - "transformer[AttributeType.tap_pos] = [3]\n", - "transformer[AttributeType.tap_min] = [-11]\n", - "transformer[AttributeType.tap_max] = [9]\n", - "transformer[AttributeType.tap_size] = [100]\n", - "\n", - "# all\n", - "input_data = {\n", - " ComponentType.node: node,\n", - " ComponentType.transformer: transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "d16f9dea", - "metadata": {}, - "source": [ - "**We can print the input dataset by converting the numpy array to dataframe.**" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "37749c7c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " id from_node to_node from_status to_status u1 u2 sn \\\n", - "0 3 2 4 1 1 10000.0 400.0 100000.0 \n", - "\n", - " uk pk ... tap_nom tap_size uk_min uk_max pk_min pk_max \\\n", - "0 0.1 1000.0 ... -128 100.0 NaN NaN NaN NaN \n", - "\n", - " r_grounding_from x_grounding_from r_grounding_to x_grounding_to \n", - "0 NaN NaN NaN NaN \n", - "\n", - "[1 rows x 31 columns]\n" - ] - } - ], - "source": [ - "print(pd.DataFrame(input_data[ComponentType.transformer]))" - ] - }, - { - "cell_type": "markdown", - "id": "47a9c257", - "metadata": {}, - "source": [ - "### One-time Power Flow Calculation\n", - "\n", - "You can call the method `calculate_power_flow` to do a one-time calculation based on the current network data in the model.\n", - "\n", - "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [Power Flow Example](./Power%20Flow%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb). " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "7bb0f998", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n", - " id energized u_pu u u_angle p \\\n", - "0 2 1 0.999999 9999.994897 -4.976260e-08 1002.882212 \n", - "1 4 1 0.965618 386.247005 -2.618522e+00 -1000.000000 \n", - "\n", - " q \n", - "0 5027.744841 \n", - "1 -5000.000000 \n" - ] - } - ], - "source": [ - "# validation (optional)\n", - "from power_grid_model.validation import assert_valid_input_data\n", - "\n", - "assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.power_flow)\n", - "\n", - "# construction\n", - "model = PowerGridModel(input_data)\n", - "\n", - "# one-time power flow calculation\n", - "output_data = model.calculate_power_flow(\n", - " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", - ")\n", - "\n", - "# result dataset\n", - "print(\"------node result------\")\n", - "print(pd.DataFrame(output_data[ComponentType.node]))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "682c1c48", - "metadata": {}, - "source": [ - "### One-time State Estimation\n", - "Below we present a simple example of state estimation for a network with a two-winding transformer. \n", - "\n", - "NOTE: In `power-grid-model`, two-winding transformers belong to `branch` component type, therefore the `measured_terminal_type` of power sensors should be assigned to `MeasuredTerminalType.branch_from/_to`.\n", - "\n", - "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [State Estimation Example](./State%20Estimation%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "f0c8c3e8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n", - " id energized u_pu u u_angle p \\\n", - "0 2 1 9.987392 99873.919714 0.000000 470658.914282 \n", - "1 4 1 9.209290 3683.716125 -2.617974 -446912.927660 \n", - "\n", - " q \n", - "0 4.700696e+06 \n", - "1 -4.464515e+06 \n" - ] - } - ], - "source": [ - "# voltage sensor\n", - "sym_voltage_sensor = initialize_array(DatasetType.input, ComponentType.sym_voltage_sensor, 2)\n", - "sym_voltage_sensor[AttributeType.id] = [6, 7]\n", - "sym_voltage_sensor[AttributeType.measured_object] = [2, 4]\n", - "sym_voltage_sensor[AttributeType.u_sigma] = [1.0, 1.0]\n", - "sym_voltage_sensor[AttributeType.u_measured] = [1e5, 4e2]\n", - "\n", - "# power sensor\n", - "sym_power_sensor = initialize_array(DatasetType.input, ComponentType.sym_power_sensor, 2)\n", - "sym_power_sensor[AttributeType.id] = [8, 9]\n", - "sym_power_sensor[AttributeType.measured_object] = [3, 3]\n", - "sym_power_sensor[AttributeType.measured_terminal_type] = [\n", - " MeasuredTerminalType.branch_from,\n", - " MeasuredTerminalType.branch_to,\n", - "]\n", - "sym_power_sensor[AttributeType.power_sigma] = [1.0, 1.0]\n", - "sym_power_sensor[AttributeType.p_measured] = [2e6, 1e5]\n", - "sym_power_sensor[AttributeType.q_measured] = [2e7, 1e6]\n", - "\n", - "# use components from former input dataset cell.\n", - "input_data2 = {\n", - " ComponentType.node: node,\n", - " ComponentType.transformer: transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - " ComponentType.sym_voltage_sensor: sym_voltage_sensor,\n", - " ComponentType.sym_power_sensor: sym_power_sensor,\n", - "}\n", - "\n", - "# validation (optional)\n", - "from power_grid_model.validation import assert_valid_input_data\n", - "\n", - "assert_valid_input_data(input_data=input_data2, calculation_type=CalculationType.state_estimation)\n", - "\n", - "# construction\n", - "model2 = PowerGridModel(input_data2)\n", - "\n", - "# one-time state estimation\n", - "output_data2 = model2.calculate_state_estimation(\n", - " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.iterative_linear\n", - ")\n", - "\n", - "# result dataset\n", - "print(\"------node result------\")\n", - "print(pd.DataFrame(output_data2[ComponentType.node]))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e3605c3e", - "metadata": {}, - "source": [ - "## Three-Winding Transformer\n", - "\n", - "Three-winding transformer is described as 3 transformers of pi model each connected together in star configuration, and it belongs to the `branch3` component type which connects three nodes with possibly different voltage levels.\n", - "\n", - "### Example Network\n", - "\n", - "We use a simple network with 3 nodes, 1 source, 1 load and 1 three-winding transformer. As shown below:\n", - "\n", - "```txt\n", - " source_1 --- node_2 --- three_winding_transformer_3 --- node_4 --- load_5\n", - " |\n", - " -------------------node_6 --- load_7\n", - " \n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "84a7f686", - "metadata": {}, - "source": [ - "### Input Dataset\n", - "\n", - "We use the helper function `initialize_array` to create an input dataset.\n", - "\n", - "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "44c2de63", - "metadata": {}, - "outputs": [], - "source": [ - "# node\n", - "node = initialize_array(DatasetType.input, ComponentType.node, 3)\n", - "node[AttributeType.id] = np.array([2, 4, 6])\n", - "node[AttributeType.u_rated] = [1e4, 1e2, 1e2]\n", - "\n", - "# load\n", - "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 2)\n", - "sym_load[AttributeType.id] = [5, 7]\n", - "sym_load[AttributeType.node] = [4, 6]\n", - "sym_load[AttributeType.status] = [1]\n", - "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", - "sym_load[AttributeType.p_specified] = [1e3, 1e3]\n", - "sym_load[AttributeType.q_specified] = [5e3, 5e3]\n", - "\n", - "# source\n", - "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", - "source[AttributeType.id] = [1]\n", - "source[AttributeType.node] = [2]\n", - "source[AttributeType.status] = [1]\n", - "source[AttributeType.u_ref] = [1.0]\n", - "\n", - "# three-winding transformer\n", - "three_winding_transformer = initialize_array(DatasetType.input, ComponentType.three_winding_transformer, 1)\n", - "three_winding_transformer[AttributeType.id] = [3]\n", - "three_winding_transformer[AttributeType.node_1] = [2]\n", - "three_winding_transformer[AttributeType.node_2] = [4]\n", - "three_winding_transformer[AttributeType.node_3] = [6]\n", - "three_winding_transformer[AttributeType.status_1] = [1]\n", - "three_winding_transformer[AttributeType.status_2] = [1]\n", - "three_winding_transformer[AttributeType.status_3] = [1]\n", - "three_winding_transformer[AttributeType.u1] = [1e4]\n", - "three_winding_transformer[AttributeType.u2] = [1e2]\n", - "three_winding_transformer[AttributeType.u3] = [1e2]\n", - "three_winding_transformer[AttributeType.sn_1] = [1e5]\n", - "three_winding_transformer[AttributeType.sn_2] = [1e5]\n", - "three_winding_transformer[AttributeType.sn_3] = [1e5]\n", - "three_winding_transformer[AttributeType.uk_12] = [0.09]\n", - "three_winding_transformer[AttributeType.uk_13] = [0.06]\n", - "three_winding_transformer[AttributeType.uk_23] = [0.06]\n", - "three_winding_transformer[AttributeType.pk_12] = [1e3]\n", - "three_winding_transformer[AttributeType.pk_13] = [1e3]\n", - "three_winding_transformer[AttributeType.pk_23] = [1e3]\n", - "three_winding_transformer[AttributeType.i0] = [0]\n", - "three_winding_transformer[AttributeType.p0] = [0]\n", - "three_winding_transformer[AttributeType.winding_1] = [2]\n", - "three_winding_transformer[AttributeType.winding_2] = [1]\n", - "three_winding_transformer[AttributeType.winding_3] = [1]\n", - "three_winding_transformer[AttributeType.clock_12] = [5]\n", - "three_winding_transformer[AttributeType.clock_13] = [-7] # supports periodic input\n", - "three_winding_transformer[AttributeType.tap_side] = [0]\n", - "three_winding_transformer[AttributeType.tap_pos] = [0]\n", - "three_winding_transformer[AttributeType.tap_min] = [-10]\n", - "three_winding_transformer[AttributeType.tap_max] = [10]\n", - "three_winding_transformer[AttributeType.tap_nom] = [0]\n", - "three_winding_transformer[AttributeType.tap_size] = [1380]\n", - "\n", - "# all\n", - "input_data3 = {\n", - " ComponentType.node: node,\n", - " ComponentType.three_winding_transformer: three_winding_transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "c25444ec", - "metadata": {}, - "source": [ - "### One-time power flow calculation" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "b4e63620", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n", - " id energized u_pu u u_angle p \\\n", - "0 2 1 0.999999 9999.989788 -9.966674e-08 2007.896574 \n", - "1 4 1 0.993097 99.309655 -2.618590e+00 -1000.000000 \n", - "2 6 1 0.994637 99.463733 -2.618281e+00 -1000.000000 \n", - "\n", - " q \n", - "0 10062.592544 \n", - "1 -5000.000000 \n", - "2 -5000.000000 \n" - ] - } - ], - "source": [ - "# validation (optional)\n", - "from power_grid_model.validation import assert_valid_input_data\n", - "\n", - "assert_valid_input_data(input_data=input_data3, calculation_type=CalculationType.power_flow)\n", - "\n", - "# construction\n", - "model3 = PowerGridModel(input_data3)\n", - "\n", - "# one-time power flow calculation\n", - "output_data3 = model3.calculate_power_flow(\n", - " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", - ")\n", - "\n", - "# result dataset\n", - "print(\"------node result------\")\n", - "print(pd.DataFrame(output_data3[ComponentType.node]))" - ] - }, - { - "cell_type": "markdown", - "id": "36f829f1", - "metadata": {}, - "source": [ - "### One-time State Estimation\n", - "\n", - "Below we present a simple example of state estimation for a network with a three-winding transformer. \n", - "\n", - "NOTE: In `power-grid-model`, three-winding transformers belong to `branch3` component type, therefore the `measured_terminal_type` of power sensors should be assigned to `MeasuredTerminalType.branch3_1/_2/_3`.\n", - "\n", - "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [State Estimation Example](./State%20Estimation%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "1ae6c043", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------three-winding transformer result------\n", - " id energized loading_1 loading_2 loading_3 loading p_1 \\\n", - "0 3 1 0.033993 0.016958 0.016967 0.033993 667.156598 \n", - "\n", - " q_1 i_1 s_1 p_2 q_2 i_2 \\\n", - "0 3333.23678 0.196261 3399.347785 -333.400686 -1662.691395 9.813068 \n", - "\n", - " s_2 p_3 q_3 i_3 s_3 \n", - "0 1695.788516 -332.889246 -1663.677083 9.813068 1696.654557 \n" - ] - } - ], - "source": [ - "# voltage sensor\n", - "sym_voltage_sensor = initialize_array(DatasetType.input, ComponentType.sym_voltage_sensor, 3)\n", - "sym_voltage_sensor[AttributeType.id] = [8, 9, 10]\n", - "sym_voltage_sensor[AttributeType.measured_object] = [2, 4, 6]\n", - "sym_voltage_sensor[AttributeType.u_sigma] = [1.0, 1.0, 1.0]\n", - "sym_voltage_sensor[AttributeType.u_measured] = [1e4, 1e2, 1e2]\n", - "\n", - "# power sensor\n", - "sym_power_sensor = initialize_array(DatasetType.input, ComponentType.sym_power_sensor, 3)\n", - "sym_power_sensor[AttributeType.id] = [11, 12, 13]\n", - "sym_power_sensor[AttributeType.measured_object] = [3, 3, 3]\n", - "sym_power_sensor[AttributeType.measured_terminal_type] = [\n", - " MeasuredTerminalType.branch3_1,\n", - " MeasuredTerminalType.branch3_2,\n", - " MeasuredTerminalType.branch3_3,\n", - "]\n", - "sym_power_sensor[AttributeType.power_sigma] = [1.0, 1.0, 1.0]\n", - "sym_power_sensor[AttributeType.p_measured] = [2e3, 1e3, 1e3]\n", - "sym_power_sensor[AttributeType.q_measured] = [1e4, 5e3, 5e3]\n", - "\n", - "# use components from the one-time power flow calculation with three-winding transformer\n", - "input_data4 = {\n", - " ComponentType.node: node,\n", - " ComponentType.three_winding_transformer: three_winding_transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - " ComponentType.sym_voltage_sensor: sym_voltage_sensor,\n", - " ComponentType.sym_power_sensor: sym_power_sensor,\n", - "}\n", - "\n", - "# validation (optional)\n", - "from power_grid_model.validation import assert_valid_input_data\n", - "\n", - "assert_valid_input_data(input_data=input_data4, calculation_type=CalculationType.state_estimation)\n", - "\n", - "# construction\n", - "model4 = PowerGridModel(input_data4)\n", - "\n", - "# one-time state estimation\n", - "output_data4 = model4.calculate_state_estimation(\n", - " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.iterative_linear\n", - ")\n", - "\n", - "# result dataset\n", - "print(\"------three-winding transformer result------\")\n", - "print(pd.DataFrame(output_data4[ComponentType.three_winding_transformer]))" - ] - }, - { - "cell_type": "markdown", - "id": "7fb27b941602401d91542211134fc71a", - "metadata": {}, - "source": [ - "## Power flow calculations with automatic tap changing\n", - "\n", - "### Example Network\n", - "\n", - "We use a simple network with 3 nodes, 1 source, 1 load, 1 line, 1 transformer and 1 transformer tap regulator. As shown below:\n", - "\n", - "```txt\n", - " (tap_side) (control side)\n", - "source_1 --- node_2 --- transformer_3 --- node_4 --- line_5 --- node_6 --- load_7\n", - " | |\n", - " transformer_tap_regulator_8 <--------/ (control voltage)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "5ab5adba", - "metadata": {}, - "source": [ - "### Input Dataset\n", - "\n", - "We create an input dataset by using the helper function `initialize_array`. \n", - "\n", - "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "0c89a335", - "metadata": {}, - "outputs": [], - "source": [ - "# node\n", - "node = initialize_array(DatasetType.input, ComponentType.node, 3)\n", - "node[AttributeType.id] = [2, 4, 6]\n", - "node[AttributeType.u_rated] = [1e4, 4e2, 4e2]\n", - "\n", - "# load\n", - "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 1)\n", - "sym_load[AttributeType.id] = [7]\n", - "sym_load[AttributeType.node] = [6]\n", - "sym_load[AttributeType.status] = [1]\n", - "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", - "sym_load[AttributeType.p_specified] = [1e3]\n", - "sym_load[AttributeType.q_specified] = [5e3]\n", - "\n", - "# source\n", - "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", - "source[AttributeType.id] = [1]\n", - "source[AttributeType.node] = [2]\n", - "source[AttributeType.status] = [1]\n", - "source[AttributeType.u_ref] = [1.0]\n", - "\n", - "# line\n", - "line = initialize_array(DatasetType.input, ComponentType.line, 1)\n", - "line[AttributeType.id] = [5]\n", - "line[AttributeType.from_node] = [4]\n", - "line[AttributeType.to_node] = [6]\n", - "line[AttributeType.from_status] = [1]\n", - "line[AttributeType.to_status] = [1]\n", - "line[AttributeType.r1] = [10.0]\n", - "line[AttributeType.x1] = [0.0]\n", - "line[AttributeType.c1] = [0.0]\n", - "line[AttributeType.tan1] = [0.0]\n", - "\n", - "# transformer\n", - "transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)\n", - "transformer[AttributeType.id] = [3]\n", - "transformer[AttributeType.from_node] = [2]\n", - "transformer[AttributeType.to_node] = [4]\n", - "transformer[AttributeType.from_status] = [1]\n", - "transformer[AttributeType.to_status] = [1]\n", - "transformer[AttributeType.u1] = [1e4]\n", - "transformer[AttributeType.u2] = [4e2]\n", - "transformer[AttributeType.sn] = [1e5]\n", - "transformer[AttributeType.uk] = [0.1]\n", - "transformer[AttributeType.pk] = [1e3]\n", - "transformer[AttributeType.i0] = [1.0e-6]\n", - "transformer[AttributeType.p0] = [0.1]\n", - "transformer[AttributeType.winding_from] = [2]\n", - "transformer[AttributeType.winding_to] = [1]\n", - "transformer[AttributeType.clock] = [5]\n", - "transformer[AttributeType.tap_side] = [0]\n", - "transformer[AttributeType.tap_pos] = [3]\n", - "transformer[AttributeType.tap_min] = [-11]\n", - "transformer[AttributeType.tap_max] = [9]\n", - "transformer[AttributeType.tap_size] = [100]\n", - "\n", - "# transformer tap regulator\n", - "transformer_tap_regulator = initialize_array(DatasetType.input, ComponentType.transformer_tap_regulator, 1)\n", - "transformer_tap_regulator[AttributeType.id] = [8]\n", - "transformer_tap_regulator[AttributeType.regulated_object] = [3]\n", - "transformer_tap_regulator[AttributeType.status] = [1]\n", - "transformer_tap_regulator[AttributeType.control_side] = [BranchSide.to_side]\n", - "transformer_tap_regulator[AttributeType.u_set] = [400.0]\n", - "transformer_tap_regulator[AttributeType.u_band] = [20.0]\n", - "transformer_tap_regulator[AttributeType.line_drop_compensation_r] = [0.0]\n", - "transformer_tap_regulator[AttributeType.line_drop_compensation_x] = [0.0]\n", - "\n", - "# all\n", - "input_data5 = {\n", - " ComponentType.node: node,\n", - " ComponentType.line: line,\n", - " ComponentType.transformer: transformer,\n", - " ComponentType.sym_load: sym_load,\n", - " ComponentType.source: source,\n", - " ComponentType.transformer_tap_regulator: transformer_tap_regulator,\n", - "}" - ] - }, - { - "cell_type": "markdown", - "id": "a28369ab", - "metadata": {}, - "source": [ - "### One-time Power Flow Calculation\n", - "\n", - "As before, you can call the method `calculate_power_flow` to do a one-time calculation based on the current network data in the model.\n", - "\n", - "For a detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [Power Flow Example](./Power%20Flow%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb). " - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "2ff01266", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994637
14386.141058
26322.704292
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994637\n", - "1 4 386.141058\n", - "2 6 322.704292" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# construction\n", - "model5 = PowerGridModel(input_data5)\n", - "\n", - "# one-time power flow calculation without automatic tap changing\n", - "output_data5 = model5.calculate_power_flow()\n", - "\n", - "# Both load-side nodes node_4 and node_6 have a voltage below 400 V\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])" - ] - }, - { - "cell_type": "markdown", - "id": "07af8e93", - "metadata": {}, - "source": [ - "### Any voltage tap position strategy\n", - "\n", - "In the regular one-time power flow calculation in the example above, the voltage at the low-voltage side of the transformer is lower than 390 V.\n", - "\n", - "To get a voltage at the control side of the transformer in the acceptable voltage band as provided to the transformer tap regulator, run the power flow calculation with any the tap changing strategy set to any valid tap." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "7ad86bf0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994657
14393.881868
26334.529465
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994657\n", - "1 4 393.881868\n", - "2 6 334.529465" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
0811
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data6 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.any_valid_tap)\n", - "\n", - "# the node at the control side of the transformer now has a voltage within the specified voltage band\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data6[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" - ] - }, - { - "cell_type": "markdown", - "id": "a85cba49", - "metadata": {}, - "source": [ - "### Model state from a tap-changing power flow\n", - "\n", - "Model-state output can also be requested when automatic tap changing is enabled. This example starts from a fresh model, then inspects the state returned with the selected tap-changing result. The iteration histories describe the power-flow solve for the selected tap, not every tap considered during optimization." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "7d15a1e7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Selected tap position:\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
0811
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Y-bus nonzero entries: 7\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
admittance
rowcolumn
000.016000+0.000000j
1-0.016000+0.000000j
10-0.016000+0.000000j
10.116000-0.994987j
20.578313-0.803648j
21-0.406823-0.902658j
20.098030-0.975382j
\n", - "
" - ], - "text/plain": [ - " admittance\n", - "row column \n", - "0 0 0.016000+0.000000j\n", - " 1 -0.016000+0.000000j\n", - "1 0 -0.016000+0.000000j\n", - " 1 0.116000-0.994987j\n", - " 2 0.578313-0.803648j\n", - "2 1 -0.406823-0.902658j\n", - " 2 0.098030-0.975382j" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Jacobian iterations: [1, 2, 3, 4, 5]\n", - "Final Jacobian (selected tap):\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
HNML
rowcolumn
000.0050000.010191-0.012191-0.005000
1-0.005000-0.0121910.012191-0.005000
100.005000-0.0121910.0121910.005000
10.9647830.112479-0.1124790.964783
2-0.969783-0.1002880.100288-0.969783
21-0.970344-0.0947020.094702-0.970344
29951.336650995.134156-995.1341569951.336650
\n", - "
" - ], - "text/plain": [ - " H N M L\n", - "row column \n", - "0 0 0.005000 0.010191 -0.012191 -0.005000\n", - " 1 -0.005000 -0.012191 0.012191 -0.005000\n", - "1 0 0.005000 -0.012191 0.012191 0.005000\n", - " 1 0.964783 0.112479 -0.112479 0.964783\n", - " 2 -0.969783 -0.100288 0.100288 -0.969783\n", - "2 1 -0.970344 -0.094702 0.094702 -0.970344\n", - " 2 9951.336650 995.134156 -995.134156 9951.336650" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Nodal-state iterations: [1, 2, 3, 4, 5]\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
|V|, iteration 1 (p.u.)angle, iteration 1 (rad)|V|, iteration 2 (p.u.)angle, iteration 2 (rad)|V|, iteration 3 (p.u.)angle, iteration 3 (rad)|V|, iteration 4 (p.u.)angle, iteration 4 (rad)|V|, iteration 5 (p.u.)angle, iteration 5 (rad)
node
21.000000-1.675680e-070.999999-2.576861e-070.999999-2.804433e-070.999999-2.809399e-070.999999-2.809402e-07
40.985887-2.619709e+000.984731-2.620633e+000.984705-2.620868e+000.984705-2.620873e+000.984705-2.620873e+00
60.890190-2.333657e+000.846186-2.242660e+000.836566-2.231922e+000.836324-2.231657e+000.836324-2.231657e+00
\n", - "
" - ], - "text/plain": [ - " |V|, iteration 1 (p.u.) angle, iteration 1 (rad) \\\n", - "node \n", - "2 1.000000 -1.675680e-07 \n", - "4 0.985887 -2.619709e+00 \n", - "6 0.890190 -2.333657e+00 \n", - "\n", - " |V|, iteration 2 (p.u.) angle, iteration 2 (rad) \\\n", - "node \n", - "2 0.999999 -2.576861e-07 \n", - "4 0.984731 -2.620633e+00 \n", - "6 0.846186 -2.242660e+00 \n", - "\n", - " |V|, iteration 3 (p.u.) angle, iteration 3 (rad) \\\n", - "node \n", - "2 0.999999 -2.804433e-07 \n", - "4 0.984705 -2.620868e+00 \n", - "6 0.836566 -2.231922e+00 \n", - "\n", - " |V|, iteration 4 (p.u.) angle, iteration 4 (rad) \\\n", - "node \n", - "2 0.999999 -2.809399e-07 \n", - "4 0.984705 -2.620873e+00 \n", - "6 0.836324 -2.231657e+00 \n", - "\n", - " |V|, iteration 5 (p.u.) angle, iteration 5 (rad) \n", - "node \n", - "2 0.999999 -2.809402e-07 \n", - "4 0.984705 -2.620873e+00 \n", - "6 0.836324 -2.231657e+00 " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "model5_state = PowerGridModel(input_data5)\n", - "output_data_state, model_state = model5_state.calculate_power_flow(\n", - " tap_changing_strategy=TapChangingStrategy.any_valid_tap,\n", - " get_model_state=True,\n", - ")\n", - "\n", - "print(\"Selected tap position:\")\n", - "display(pd.DataFrame(output_data_state[ComponentType.transformer_tap_regulator]))\n", - "\n", - "group_state = model_state.groups[0]\n", - "y_bus = group_state.y_bus\n", - "y_bus_rows = np.repeat(np.arange(y_bus.row_indptr.size - 1), np.diff(y_bus.row_indptr))\n", - "print(f\"Y-bus nonzero entries: {len(y_bus.col_indices)}\")\n", - "display(\n", - " pd.DataFrame(\n", - " {\n", - " \"row\": y_bus_rows,\n", - " \"column\": y_bus.col_indices,\n", - " \"admittance\": y_bus.admittance_real + 1j * y_bus.admittance_imag,\n", - " }\n", - " ).set_index([\"row\", \"column\"])\n", - ")\n", - "\n", - "print(\"Jacobian iterations:\", [jac.iteration for jac in group_state.jacobians])\n", - "if group_state.jacobians:\n", - " jacobian = group_state.jacobians[-1]\n", - " pattern = group_state.jacobian_structure\n", - " jacobian_rows = np.repeat(\n", - " np.arange(pattern.row_indptr_lu.size - 1), np.diff(pattern.row_indptr_lu)\n", - " )\n", - " print(\"Final Jacobian (selected tap):\")\n", - " display(\n", - " pd.DataFrame(\n", - " {\n", - " \"row\": jacobian_rows,\n", - " \"column\": pattern.col_indices_lu,\n", - " \"H\": jacobian.h,\n", - " \"N\": jacobian.n,\n", - " \"M\": jacobian.m,\n", - " \"L\": jacobian.l,\n", - " }\n", - " ).set_index([\"row\", \"column\"])\n", - " )\n", - "print(\"Nodal-state iterations:\", [state.iteration for state in group_state.nodal_states])\n", - "voltage_rows = []\n", - "for input_idx, node_id in enumerate(model_state.input_node_id):\n", - " if model_state.input_node_group[input_idx] != group_state.mapping.group:\n", - " continue\n", - " bus_idx = int(model_state.input_node_bus[input_idx])\n", - " row = {\"node\": node_id}\n", - " for state in group_state.nodal_states:\n", - " row[f\"|V|, iteration {state.iteration} (p.u.)\"] = state.voltage_magnitude[bus_idx]\n", - " row[f\"angle, iteration {state.iteration} (rad)\"] = state.voltage_angle[bus_idx]\n", - " voltage_rows.append(row)\n", - "display(pd.DataFrame(voltage_rows).set_index(\"node\"))" - ] - }, - { - "cell_type": "markdown", - "id": "9f1a6f30", - "metadata": {}, - "source": [ - "You could also opt for fast_any_tap that takes advantage of binary search instead of linear search internally." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "1fa08adb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994675
14401.932237
26346.203709
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994675\n", - "1 4 401.932237\n", - "2 6 346.203709" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n", - "\n", - "----------fast_any_tap----------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
081-1
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 -1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data6f = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.fast_any_tap)\n", - "\n", - "# the node at the control side of the transformer now has a voltage within the specified voltage band\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data6f[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "print(\"\\n----------fast_any_tap----------\")\n", - "display(pd.DataFrame(output_data6f[ComponentType.transformer_tap_regulator]))" - ] - }, - { - "cell_type": "markdown", - "id": "7828a4e3", - "metadata": {}, - "source": [ - "**NOTE:** the tap positions obtained using the `any_valid_tap` and `fast_any_tap` strategy may depend on the initial tap position of the transformers." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "be276930", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------transformer batch update------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idfrom_statusto_statustap_pos
03-128-1280
13-128-1281
\n", - "
" - ], - "text/plain": [ - " id from_status to_status tap_pos\n", - "0 3 -128 -128 0\n", - "1 3 -128 -128 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node_4 batch result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
04397.867176
14393.881868
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 4 397.867176\n", - "1 4 393.881868" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator batch result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
0810
1811
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 0\n", - "1 8 1 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "transformer_update = initialize_array(DatasetType.update, ComponentType.transformer, (2, 1))\n", - "transformer_update[AttributeType.id] = 3\n", - "transformer_update[AttributeType.tap_pos] = [[0], [1]]\n", - "\n", - "update_data = {ComponentType.transformer: transformer_update}\n", - "\n", - "print(\"------transformer batch update------\")\n", - "display(pd.DataFrame(update_data[ComponentType.transformer][:, 0]))\n", - "\n", - "# power flow batch calculation with automatic tap changing\n", - "output_data = model5.calculate_power_flow(\n", - " update_data=update_data, tap_changing_strategy=TapChangingStrategy.fast_any_tap\n", - ")\n", - "\n", - "print(\"------node_4 batch result------\")\n", - "display(pd.DataFrame(output_data[ComponentType.node][:, 1])[[AttributeType.id, AttributeType.u]]) # only output node 1\n", - "\n", - "print(\"\\n------tap regulator batch result------\")\n", - "display(pd.DataFrame(output_data[ComponentType.transformer_tap_regulator][:, 0]))" - ] - }, - { - "cell_type": "markdown", - "id": "6f6cd15c", - "metadata": {}, - "source": [ - "### Maximum voltage tap position strategy\n", - "\n", - "To specifically optimize for the higher end of the voltage band, the `max_voltage_tap` tap changing strategy can be provided." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "21b01a77", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994683
14406.079525
26352.021534
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994683\n", - "1 4 406.079525\n", - "2 6 352.021534" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
081-2
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 -2" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data5 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.max_voltage_tap)\n", - "\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.transformer_tap_regulator]))" - ] - }, - { - "cell_type": "markdown", - "id": "7b8ab2a2", - "metadata": {}, - "source": [ - "### Minimum voltage tap position strategy\n", - "\n", - "To specifically optimize for the lower end of the voltage band, the `min_voltage_tap` tap changing strategy can be provided." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "c0314199", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994657
14393.881868
26334.529465
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994657\n", - "1 4 393.881868\n", - "2 6 334.529465" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
0811
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data5 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.min_voltage_tap)\n", - "\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "display(pd.DataFrame(output_data5[ComponentType.transformer_tap_regulator]))" - ] - }, - { - "cell_type": "markdown", - "id": "01a473bd", - "metadata": {}, - "source": [ - "### Automatic tap changing with line drop compensation\n", - "\n", - "In the above examples, the voltage at the low-voltage side of the transformer is controlled. However, the load is still connected to a node with a voltage lower than 390 V.\n", - "\n", - "To regulate the node that the load is connected to, the voltage needs to be compensated for the voltage drop over the line.\n", - "\n", - "```txt\n", - " (tap_side) (control side)\n", - "source_1 --- node_2 --- transformer_3 --- node_4 --- line_5 --- node_6 --- load_7\n", - " | | :\n", - " regulator_8 <-----/ <......................./ (line drop compensation)\n", - " (control voltage) \n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "ac091392", - "metadata": {}, - "outputs": [], - "source": [ - "# use the same input data\n", - "input_data6 = {component: component_data.copy() for component, component_data in input_data5.items()}\n", - "\n", - "# set the regulator's line drop compensation\n", - "input_data6[ComponentType.transformer_tap_regulator][AttributeType.line_drop_compensation_r] = [10.0]\n", - "\n", - "# construction\n", - "model6 = PowerGridModel(input_data=input_data6)" - ] - }, - { - "cell_type": "markdown", - "id": "bb89ad98", - "metadata": {}, - "source": [ - "The result of the power flow calculation with automatic tap changing contains a voltage at the node that the load is connected to that is within the specified voltage band." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "9b66d16e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "------node result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idu
029999.994731
14437.642686
26393.354002
\n", - "
" - ], - "text/plain": [ - " id u\n", - "0 2 9999.994731\n", - "1 4 437.642686\n", - "2 6 393.354002" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------tap regulator result------\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idenergizedtap_pos
081-9
\n", - "
" - ], - "text/plain": [ - " id energized tap_pos\n", - "0 8 1 -9" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# one-time power flow calculation with automatic tap changing\n", - "output_data6 = model6.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.any_valid_tap)\n", - "\n", - "print(\"------node result------\")\n", - "display(pd.DataFrame(output_data6[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", - "\n", - "print(\"\\n------tap regulator result------\")\n", - "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "power-grid-model", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.14.8" - } + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "89cf2628", + "metadata": {}, + "source": [ + "# Transformer Example\n", + "\n", + "In this notebook we will present examples of transformers in `power-grid-model`. \n", + "\n", + "Both two-winding transformer and three-winding transformer are covered. We will do one-time power flow calculation and one-time state estimation for both types of tranformers.\n", + "\n", + "This notebook serves as an example of how to use the Python API. For detailed API documentation, refer to\n", + "[Python API reference](../api_reference/python-api-reference.md)\n", + "and [Native Data Interface](../advanced_documentation/native-data-interface.md).\n", + "\n", + "## Transformer (Two-winding Transformer)\n", + "\n", + "Transformer is described as a pi model in `power-grid-model`, and it belongs to the `branch` component type which connects two nodes with possibly different voltage levels.\n", + "\n", + "### Example Network\n", + "\n", + "We use a simple network with 2 nodes, 1 source, 1 load and 1 transformer. As shown below:\n", + "\n", + "```txt\n", + " source_1 --- node_2 --- transformer_3 --- node_4 --- load_5\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ae11dc9a", + "metadata": {}, + "outputs": [], + "source": [ + "# some basic imports\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from power_grid_model import (\n", + " AttributeType,\n", + " BranchSide,\n", + " CalculationMethod,\n", + " CalculationType,\n", + " ComponentType,\n", + " DatasetType,\n", + " LoadGenType,\n", + " MeasuredTerminalType,\n", + " PowerGridModel,\n", + " TapChangingStrategy,\n", + " initialize_array,\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f983cef7", + "metadata": {}, + "source": [ + "### Input Dataset\n", + "\n", + "We create an input dataset by using the helper function `initialize_array`. \n", + "\n", + "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6f008736", + "metadata": {}, + "outputs": [], + "source": [ + "# node\n", + "node = initialize_array(DatasetType.input, ComponentType.node, 2)\n", + "node[AttributeType.id] = np.array([2, 4])\n", + "node[AttributeType.u_rated] = [1e4, 4e2]\n", + "\n", + "# load\n", + "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 1)\n", + "sym_load[AttributeType.id] = [5]\n", + "sym_load[AttributeType.node] = [4]\n", + "sym_load[AttributeType.status] = [1]\n", + "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", + "sym_load[AttributeType.p_specified] = [1e3]\n", + "sym_load[AttributeType.q_specified] = [5e3]\n", + "\n", + "# source\n", + "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", + "source[AttributeType.id] = [1]\n", + "source[AttributeType.node] = [2]\n", + "source[AttributeType.status] = [1]\n", + "source[AttributeType.u_ref] = [1.0]\n", + "\n", + "# transformer\n", + "transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)\n", + "transformer[AttributeType.id] = [3]\n", + "transformer[AttributeType.from_node] = [2]\n", + "transformer[AttributeType.to_node] = [4]\n", + "transformer[AttributeType.from_status] = [1]\n", + "transformer[AttributeType.to_status] = [1]\n", + "transformer[AttributeType.u1] = [1e4]\n", + "transformer[AttributeType.u2] = [4e2]\n", + "transformer[AttributeType.sn] = [1e5]\n", + "transformer[AttributeType.uk] = [0.1]\n", + "transformer[AttributeType.pk] = [1e3]\n", + "transformer[AttributeType.i0] = [1.0e-6]\n", + "transformer[AttributeType.p0] = [0.1]\n", + "transformer[AttributeType.winding_from] = [2]\n", + "transformer[AttributeType.winding_to] = [1]\n", + "transformer[AttributeType.clock] = [5]\n", + "transformer[AttributeType.tap_side] = [0]\n", + "transformer[AttributeType.tap_pos] = [3]\n", + "transformer[AttributeType.tap_min] = [-11]\n", + "transformer[AttributeType.tap_max] = [9]\n", + "transformer[AttributeType.tap_size] = [100]\n", + "\n", + "# all\n", + "input_data = {\n", + " ComponentType.node: node,\n", + " ComponentType.transformer: transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "d16f9dea", + "metadata": {}, + "source": [ + "**We can print the input dataset by converting the numpy array to dataframe.**" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "37749c7c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " id from_node to_node from_status to_status u1 u2 sn \\\n", + "0 3 2 4 1 1 10000.0 400.0 100000.0 \n", + "\n", + " uk pk ... tap_nom tap_size uk_min uk_max pk_min pk_max \\\n", + "0 0.1 1000.0 ... -128 100.0 NaN NaN NaN NaN \n", + "\n", + " r_grounding_from x_grounding_from r_grounding_to x_grounding_to \n", + "0 NaN NaN NaN NaN \n", + "\n", + "[1 rows x 31 columns]\n" + ] + } + ], + "source": [ + "print(pd.DataFrame(input_data[ComponentType.transformer]))" + ] + }, + { + "cell_type": "markdown", + "id": "47a9c257", + "metadata": {}, + "source": [ + "### One-time Power Flow Calculation\n", + "\n", + "You can call the method `calculate_power_flow` to do a one-time calculation based on the current network data in the model.\n", + "\n", + "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [Power Flow Example](./Power%20Flow%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb). " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7bb0f998", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n", + " id energized u_pu u u_angle p \\\n", + "0 2 1 0.999999 9999.994897 -4.976260e-08 1002.882212 \n", + "1 4 1 0.965618 386.247005 -2.618522e+00 -1000.000000 \n", + "\n", + " q \n", + "0 5027.744841 \n", + "1 -5000.000000 \n" + ] + } + ], + "source": [ + "# validation (optional)\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.power_flow)\n", + "\n", + "# construction\n", + "model = PowerGridModel(input_data)\n", + "\n", + "# one-time power flow calculation\n", + "output_data = model.calculate_power_flow(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", + ")\n", + "\n", + "# result dataset\n", + "print(\"------node result------\")\n", + "print(pd.DataFrame(output_data[ComponentType.node]))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "682c1c48", + "metadata": {}, + "source": [ + "### One-time State Estimation\n", + "Below we present a simple example of state estimation for a network with a two-winding transformer. \n", + "\n", + "NOTE: In `power-grid-model`, two-winding transformers belong to `branch` component type, therefore the `measured_terminal_type` of power sensors should be assigned to `MeasuredTerminalType.branch_from/_to`.\n", + "\n", + "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [State Estimation Example](./State%20Estimation%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f0c8c3e8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n", + " id energized u_pu u u_angle p \\\n", + "0 2 1 9.987392 99873.919714 0.000000 470658.914282 \n", + "1 4 1 9.209290 3683.716125 -2.617974 -446912.927660 \n", + "\n", + " q \n", + "0 4.700696e+06 \n", + "1 -4.464515e+06 \n" + ] + } + ], + "source": [ + "# voltage sensor\n", + "sym_voltage_sensor = initialize_array(DatasetType.input, ComponentType.sym_voltage_sensor, 2)\n", + "sym_voltage_sensor[AttributeType.id] = [6, 7]\n", + "sym_voltage_sensor[AttributeType.measured_object] = [2, 4]\n", + "sym_voltage_sensor[AttributeType.u_sigma] = [1.0, 1.0]\n", + "sym_voltage_sensor[AttributeType.u_measured] = [1e5, 4e2]\n", + "\n", + "# power sensor\n", + "sym_power_sensor = initialize_array(DatasetType.input, ComponentType.sym_power_sensor, 2)\n", + "sym_power_sensor[AttributeType.id] = [8, 9]\n", + "sym_power_sensor[AttributeType.measured_object] = [3, 3]\n", + "sym_power_sensor[AttributeType.measured_terminal_type] = [\n", + " MeasuredTerminalType.branch_from,\n", + " MeasuredTerminalType.branch_to,\n", + "]\n", + "sym_power_sensor[AttributeType.power_sigma] = [1.0, 1.0]\n", + "sym_power_sensor[AttributeType.p_measured] = [2e6, 1e5]\n", + "sym_power_sensor[AttributeType.q_measured] = [2e7, 1e6]\n", + "\n", + "# use components from former input dataset cell.\n", + "input_data2 = {\n", + " ComponentType.node: node,\n", + " ComponentType.transformer: transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + " ComponentType.sym_voltage_sensor: sym_voltage_sensor,\n", + " ComponentType.sym_power_sensor: sym_power_sensor,\n", + "}\n", + "\n", + "# validation (optional)\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "assert_valid_input_data(input_data=input_data2, calculation_type=CalculationType.state_estimation)\n", + "\n", + "# construction\n", + "model2 = PowerGridModel(input_data2)\n", + "\n", + "# one-time state estimation\n", + "output_data2 = model2.calculate_state_estimation(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.iterative_linear\n", + ")\n", + "\n", + "# result dataset\n", + "print(\"------node result------\")\n", + "print(pd.DataFrame(output_data2[ComponentType.node]))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e3605c3e", + "metadata": {}, + "source": [ + "## Three-Winding Transformer\n", + "\n", + "Three-winding transformer is described as 3 transformers of pi model each connected together in star configuration, and it belongs to the `branch3` component type which connects three nodes with possibly different voltage levels.\n", + "\n", + "### Example Network\n", + "\n", + "We use a simple network with 3 nodes, 1 source, 1 load and 1 three-winding transformer. As shown below:\n", + "\n", + "```txt\n", + " source_1 --- node_2 --- three_winding_transformer_3 --- node_4 --- load_5\n", + " |\n", + " -------------------node_6 --- load_7\n", + " \n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "84a7f686", + "metadata": {}, + "source": [ + "### Input Dataset\n", + "\n", + "We use the helper function `initialize_array` to create an input dataset.\n", + "\n", + "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "44c2de63", + "metadata": {}, + "outputs": [], + "source": [ + "# node\n", + "node = initialize_array(DatasetType.input, ComponentType.node, 3)\n", + "node[AttributeType.id] = np.array([2, 4, 6])\n", + "node[AttributeType.u_rated] = [1e4, 1e2, 1e2]\n", + "\n", + "# load\n", + "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 2)\n", + "sym_load[AttributeType.id] = [5, 7]\n", + "sym_load[AttributeType.node] = [4, 6]\n", + "sym_load[AttributeType.status] = [1]\n", + "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", + "sym_load[AttributeType.p_specified] = [1e3, 1e3]\n", + "sym_load[AttributeType.q_specified] = [5e3, 5e3]\n", + "\n", + "# source\n", + "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", + "source[AttributeType.id] = [1]\n", + "source[AttributeType.node] = [2]\n", + "source[AttributeType.status] = [1]\n", + "source[AttributeType.u_ref] = [1.0]\n", + "\n", + "# three-winding transformer\n", + "three_winding_transformer = initialize_array(DatasetType.input, ComponentType.three_winding_transformer, 1)\n", + "three_winding_transformer[AttributeType.id] = [3]\n", + "three_winding_transformer[AttributeType.node_1] = [2]\n", + "three_winding_transformer[AttributeType.node_2] = [4]\n", + "three_winding_transformer[AttributeType.node_3] = [6]\n", + "three_winding_transformer[AttributeType.status_1] = [1]\n", + "three_winding_transformer[AttributeType.status_2] = [1]\n", + "three_winding_transformer[AttributeType.status_3] = [1]\n", + "three_winding_transformer[AttributeType.u1] = [1e4]\n", + "three_winding_transformer[AttributeType.u2] = [1e2]\n", + "three_winding_transformer[AttributeType.u3] = [1e2]\n", + "three_winding_transformer[AttributeType.sn_1] = [1e5]\n", + "three_winding_transformer[AttributeType.sn_2] = [1e5]\n", + "three_winding_transformer[AttributeType.sn_3] = [1e5]\n", + "three_winding_transformer[AttributeType.uk_12] = [0.09]\n", + "three_winding_transformer[AttributeType.uk_13] = [0.06]\n", + "three_winding_transformer[AttributeType.uk_23] = [0.06]\n", + "three_winding_transformer[AttributeType.pk_12] = [1e3]\n", + "three_winding_transformer[AttributeType.pk_13] = [1e3]\n", + "three_winding_transformer[AttributeType.pk_23] = [1e3]\n", + "three_winding_transformer[AttributeType.i0] = [0]\n", + "three_winding_transformer[AttributeType.p0] = [0]\n", + "three_winding_transformer[AttributeType.winding_1] = [2]\n", + "three_winding_transformer[AttributeType.winding_2] = [1]\n", + "three_winding_transformer[AttributeType.winding_3] = [1]\n", + "three_winding_transformer[AttributeType.clock_12] = [5]\n", + "three_winding_transformer[AttributeType.clock_13] = [-7] # supports periodic input\n", + "three_winding_transformer[AttributeType.tap_side] = [0]\n", + "three_winding_transformer[AttributeType.tap_pos] = [0]\n", + "three_winding_transformer[AttributeType.tap_min] = [-10]\n", + "three_winding_transformer[AttributeType.tap_max] = [10]\n", + "three_winding_transformer[AttributeType.tap_nom] = [0]\n", + "three_winding_transformer[AttributeType.tap_size] = [1380]\n", + "\n", + "# all\n", + "input_data3 = {\n", + " ComponentType.node: node,\n", + " ComponentType.three_winding_transformer: three_winding_transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "c25444ec", + "metadata": {}, + "source": [ + "### One-time power flow calculation" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b4e63620", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n", + " id energized u_pu u u_angle p \\\n", + "0 2 1 0.999999 9999.989788 -9.966674e-08 2007.896574 \n", + "1 4 1 0.993097 99.309655 -2.618590e+00 -1000.000000 \n", + "2 6 1 0.994637 99.463733 -2.618281e+00 -1000.000000 \n", + "\n", + " q \n", + "0 10062.592544 \n", + "1 -5000.000000 \n", + "2 -5000.000000 \n" + ] + } + ], + "source": [ + "# validation (optional)\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "assert_valid_input_data(input_data=input_data3, calculation_type=CalculationType.power_flow)\n", + "\n", + "# construction\n", + "model3 = PowerGridModel(input_data3)\n", + "\n", + "# one-time power flow calculation\n", + "output_data3 = model3.calculate_power_flow(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", + ")\n", + "\n", + "# result dataset\n", + "print(\"------node result------\")\n", + "print(pd.DataFrame(output_data3[ComponentType.node]))" + ] + }, + { + "cell_type": "markdown", + "id": "36f829f1", + "metadata": {}, + "source": [ + "### One-time State Estimation\n", + "\n", + "Below we present a simple example of state estimation for a network with a three-winding transformer. \n", + "\n", + "NOTE: In `power-grid-model`, three-winding transformers belong to `branch3` component type, therefore the `measured_terminal_type` of power sensors should be assigned to `MeasuredTerminalType.branch3_1/_2/_3`.\n", + "\n", + "For detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [State Estimation Example](./State%20Estimation%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1ae6c043", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------three-winding transformer result------\n", + " id energized loading_1 loading_2 loading_3 loading p_1 \\\n", + "0 3 1 0.033993 0.016958 0.016967 0.033993 667.156598 \n", + "\n", + " q_1 i_1 s_1 p_2 q_2 i_2 \\\n", + "0 3333.23678 0.196261 3399.347785 -333.400686 -1662.691395 9.813068 \n", + "\n", + " s_2 p_3 q_3 i_3 s_3 \n", + "0 1695.788516 -332.889246 -1663.677083 9.813068 1696.654557 \n" + ] + } + ], + "source": [ + "# voltage sensor\n", + "sym_voltage_sensor = initialize_array(DatasetType.input, ComponentType.sym_voltage_sensor, 3)\n", + "sym_voltage_sensor[AttributeType.id] = [8, 9, 10]\n", + "sym_voltage_sensor[AttributeType.measured_object] = [2, 4, 6]\n", + "sym_voltage_sensor[AttributeType.u_sigma] = [1.0, 1.0, 1.0]\n", + "sym_voltage_sensor[AttributeType.u_measured] = [1e4, 1e2, 1e2]\n", + "\n", + "# power sensor\n", + "sym_power_sensor = initialize_array(DatasetType.input, ComponentType.sym_power_sensor, 3)\n", + "sym_power_sensor[AttributeType.id] = [11, 12, 13]\n", + "sym_power_sensor[AttributeType.measured_object] = [3, 3, 3]\n", + "sym_power_sensor[AttributeType.measured_terminal_type] = [\n", + " MeasuredTerminalType.branch3_1,\n", + " MeasuredTerminalType.branch3_2,\n", + " MeasuredTerminalType.branch3_3,\n", + "]\n", + "sym_power_sensor[AttributeType.power_sigma] = [1.0, 1.0, 1.0]\n", + "sym_power_sensor[AttributeType.p_measured] = [2e3, 1e3, 1e3]\n", + "sym_power_sensor[AttributeType.q_measured] = [1e4, 5e3, 5e3]\n", + "\n", + "# use components from the one-time power flow calculation with three-winding transformer\n", + "input_data4 = {\n", + " ComponentType.node: node,\n", + " ComponentType.three_winding_transformer: three_winding_transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + " ComponentType.sym_voltage_sensor: sym_voltage_sensor,\n", + " ComponentType.sym_power_sensor: sym_power_sensor,\n", + "}\n", + "\n", + "# validation (optional)\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "assert_valid_input_data(input_data=input_data4, calculation_type=CalculationType.state_estimation)\n", + "\n", + "# construction\n", + "model4 = PowerGridModel(input_data4)\n", + "\n", + "# one-time state estimation\n", + "output_data4 = model4.calculate_state_estimation(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.iterative_linear\n", + ")\n", + "\n", + "# result dataset\n", + "print(\"------three-winding transformer result------\")\n", + "print(pd.DataFrame(output_data4[ComponentType.three_winding_transformer]))" + ] + }, + { + "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", + "metadata": {}, + "source": [ + "## Power flow calculations with automatic tap changing\n", + "\n", + "### Example Network\n", + "\n", + "We use a simple network with 3 nodes, 1 source, 1 load, 1 line, 1 transformer and 1 transformer tap regulator. As shown below:\n", + "\n", + "```txt\n", + " (tap_side) (control side)\n", + "source_1 --- node_2 --- transformer_3 --- node_4 --- line_5 --- node_6 --- load_7\n", + " | |\n", + " transformer_tap_regulator_8 <--------/ (control voltage)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "5ab5adba", + "metadata": {}, + "source": [ + "### Input Dataset\n", + "\n", + "We create an input dataset by using the helper function `initialize_array`. \n", + "\n", + "Please refer to [Components](../user_manual/components.md) for detailed explanation of all component types and their input/output attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0c89a335", + "metadata": {}, + "outputs": [], + "source": [ + "# node\n", + "node = initialize_array(DatasetType.input, ComponentType.node, 3)\n", + "node[AttributeType.id] = [2, 4, 6]\n", + "node[AttributeType.u_rated] = [1e4, 4e2, 4e2]\n", + "\n", + "# load\n", + "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 1)\n", + "sym_load[AttributeType.id] = [7]\n", + "sym_load[AttributeType.node] = [6]\n", + "sym_load[AttributeType.status] = [1]\n", + "sym_load[AttributeType.type] = [LoadGenType.const_power]\n", + "sym_load[AttributeType.p_specified] = [1e3]\n", + "sym_load[AttributeType.q_specified] = [5e3]\n", + "\n", + "# source\n", + "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", + "source[AttributeType.id] = [1]\n", + "source[AttributeType.node] = [2]\n", + "source[AttributeType.status] = [1]\n", + "source[AttributeType.u_ref] = [1.0]\n", + "\n", + "# line\n", + "line = initialize_array(DatasetType.input, ComponentType.line, 1)\n", + "line[AttributeType.id] = [5]\n", + "line[AttributeType.from_node] = [4]\n", + "line[AttributeType.to_node] = [6]\n", + "line[AttributeType.from_status] = [1]\n", + "line[AttributeType.to_status] = [1]\n", + "line[AttributeType.r1] = [10.0]\n", + "line[AttributeType.x1] = [0.0]\n", + "line[AttributeType.c1] = [0.0]\n", + "line[AttributeType.tan1] = [0.0]\n", + "\n", + "# transformer\n", + "transformer = initialize_array(DatasetType.input, ComponentType.transformer, 1)\n", + "transformer[AttributeType.id] = [3]\n", + "transformer[AttributeType.from_node] = [2]\n", + "transformer[AttributeType.to_node] = [4]\n", + "transformer[AttributeType.from_status] = [1]\n", + "transformer[AttributeType.to_status] = [1]\n", + "transformer[AttributeType.u1] = [1e4]\n", + "transformer[AttributeType.u2] = [4e2]\n", + "transformer[AttributeType.sn] = [1e5]\n", + "transformer[AttributeType.uk] = [0.1]\n", + "transformer[AttributeType.pk] = [1e3]\n", + "transformer[AttributeType.i0] = [1.0e-6]\n", + "transformer[AttributeType.p0] = [0.1]\n", + "transformer[AttributeType.winding_from] = [2]\n", + "transformer[AttributeType.winding_to] = [1]\n", + "transformer[AttributeType.clock] = [5]\n", + "transformer[AttributeType.tap_side] = [0]\n", + "transformer[AttributeType.tap_pos] = [3]\n", + "transformer[AttributeType.tap_min] = [-11]\n", + "transformer[AttributeType.tap_max] = [9]\n", + "transformer[AttributeType.tap_size] = [100]\n", + "\n", + "# transformer tap regulator\n", + "transformer_tap_regulator = initialize_array(DatasetType.input, ComponentType.transformer_tap_regulator, 1)\n", + "transformer_tap_regulator[AttributeType.id] = [8]\n", + "transformer_tap_regulator[AttributeType.regulated_object] = [3]\n", + "transformer_tap_regulator[AttributeType.status] = [1]\n", + "transformer_tap_regulator[AttributeType.control_side] = [BranchSide.to_side]\n", + "transformer_tap_regulator[AttributeType.u_set] = [400.0]\n", + "transformer_tap_regulator[AttributeType.u_band] = [20.0]\n", + "transformer_tap_regulator[AttributeType.line_drop_compensation_r] = [0.0]\n", + "transformer_tap_regulator[AttributeType.line_drop_compensation_x] = [0.0]\n", + "\n", + "# all\n", + "input_data5 = {\n", + " ComponentType.node: node,\n", + " ComponentType.line: line,\n", + " ComponentType.transformer: transformer,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + " ComponentType.transformer_tap_regulator: transformer_tap_regulator,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "a28369ab", + "metadata": {}, + "source": [ + "### One-time Power Flow Calculation\n", + "\n", + "As before, you can call the method `calculate_power_flow` to do a one-time calculation based on the current network data in the model.\n", + "\n", + "For a detailed explanation of the arguments, batch calculations and asymmetric calculations, we refer to the [Power Flow Example](./Power%20Flow%20Example.ipynb) and [Asymmetric Calculation Example](./Asymmetric%20Calculation%20Example.ipynb). " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2ff01266", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] }, - "nbformat": 4, - "nbformat_minor": 5 + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994637
14386.141058
26322.704292
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994637\n", + "1 4 386.141058\n", + "2 6 322.704292" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# construction\n", + "model5 = PowerGridModel(input_data5)\n", + "\n", + "# one-time power flow calculation without automatic tap changing\n", + "output_data5 = model5.calculate_power_flow()\n", + "\n", + "# Both load-side nodes node_4 and node_6 have a voltage below 400 V\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])" + ] + }, + { + "cell_type": "markdown", + "id": "07af8e93", + "metadata": {}, + "source": [ + "### Any voltage tap position strategy\n", + "\n", + "In the regular one-time power flow calculation in the example above, the voltage at the low-voltage side of the transformer is lower than 390 V.\n", + "\n", + "To get a voltage at the control side of the transformer in the acceptable voltage band as provided to the transformer tap regulator, run the power flow calculation with any the tap changing strategy set to any valid tap." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7ad86bf0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994657
14393.881868
26334.529465
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994657\n", + "1 4 393.881868\n", + "2 6 334.529465" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data6 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.any_valid_tap)\n", + "\n", + "# the node at the control side of the transformer now has a voltage within the specified voltage band\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data6[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" + ] + }, + { + "cell_type": "markdown", + "id": "a85cba49", + "metadata": {}, + "source": [ + "### Model state from a tap-changing power flow\n", + "\n", + "Model-state output can also be requested when automatic tap changing is enabled. This example starts from a fresh model, then inspects the state returned with the selected tap-changing result. The iteration histories describe the power-flow solve for the selected tap, not every tap considered during optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7d15a1e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Selected tap position:\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Y-bus nonzero entries: 7\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
admittance
rowcolumn
000.016000+0.000000j
1-0.016000+0.000000j
10-0.016000+0.000000j
10.116000-0.994987j
20.578313-0.803648j
21-0.406823-0.902658j
20.098030-0.975382j
\n", + "
" + ], + "text/plain": [ + " admittance\n", + "row column \n", + "0 0 0.016000+0.000000j\n", + " 1 -0.016000+0.000000j\n", + "1 0 -0.016000+0.000000j\n", + " 1 0.116000-0.994987j\n", + " 2 0.578313-0.803648j\n", + "2 1 -0.406823-0.902658j\n", + " 2 0.098030-0.975382j" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Jacobian iterations: [1, 2, 3, 4, 5]\n", + "Final Jacobian (selected tap):\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
HNML
rowcolumn
000.0050000.010191-0.012191-0.005000
1-0.005000-0.0121910.012191-0.005000
100.005000-0.0121910.0121910.005000
10.9647830.112479-0.1124790.964783
2-0.969783-0.1002880.100288-0.969783
21-0.970344-0.0947020.094702-0.970344
29951.336650995.134156-995.1341569951.336650
\n", + "
" + ], + "text/plain": [ + " H N M L\n", + "row column \n", + "0 0 0.005000 0.010191 -0.012191 -0.005000\n", + " 1 -0.005000 -0.012191 0.012191 -0.005000\n", + "1 0 0.005000 -0.012191 0.012191 0.005000\n", + " 1 0.964783 0.112479 -0.112479 0.964783\n", + " 2 -0.969783 -0.100288 0.100288 -0.969783\n", + "2 1 -0.970344 -0.094702 0.094702 -0.970344\n", + " 2 9951.336650 995.134156 -995.134156 9951.336650" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nodal-state iterations: [1, 2, 3, 4, 5]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
|V|, iteration 1 (p.u.)angle, iteration 1 (rad)|V|, iteration 2 (p.u.)angle, iteration 2 (rad)|V|, iteration 3 (p.u.)angle, iteration 3 (rad)|V|, iteration 4 (p.u.)angle, iteration 4 (rad)|V|, iteration 5 (p.u.)angle, iteration 5 (rad)
node
21.000000-1.675680e-070.999999-2.576861e-070.999999-2.804433e-070.999999-2.809399e-070.999999-2.809402e-07
40.985887-2.619709e+000.984731-2.620633e+000.984705-2.620868e+000.984705-2.620873e+000.984705-2.620873e+00
60.890190-2.333657e+000.846186-2.242660e+000.836566-2.231922e+000.836324-2.231657e+000.836324-2.231657e+00
\n", + "
" + ], + "text/plain": [ + " |V|, iteration 1 (p.u.) angle, iteration 1 (rad) \\\n", + "node \n", + "2 1.000000 -1.675680e-07 \n", + "4 0.985887 -2.619709e+00 \n", + "6 0.890190 -2.333657e+00 \n", + "\n", + " |V|, iteration 2 (p.u.) angle, iteration 2 (rad) \\\n", + "node \n", + "2 0.999999 -2.576861e-07 \n", + "4 0.984731 -2.620633e+00 \n", + "6 0.846186 -2.242660e+00 \n", + "\n", + " |V|, iteration 3 (p.u.) angle, iteration 3 (rad) \\\n", + "node \n", + "2 0.999999 -2.804433e-07 \n", + "4 0.984705 -2.620868e+00 \n", + "6 0.836566 -2.231922e+00 \n", + "\n", + " |V|, iteration 4 (p.u.) angle, iteration 4 (rad) \\\n", + "node \n", + "2 0.999999 -2.809399e-07 \n", + "4 0.984705 -2.620873e+00 \n", + "6 0.836324 -2.231657e+00 \n", + "\n", + " |V|, iteration 5 (p.u.) angle, iteration 5 (rad) \n", + "node \n", + "2 0.999999 -2.809402e-07 \n", + "4 0.984705 -2.620873e+00 \n", + "6 0.836324 -2.231657e+00 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "model5_state = PowerGridModel(input_data5)\n", + "output_data_state, model_state = model5_state.calculate_power_flow(\n", + " tap_changing_strategy=TapChangingStrategy.any_valid_tap,\n", + " get_model_state=True,\n", + ")\n", + "\n", + "print(\"Selected tap position:\")\n", + "display(pd.DataFrame(output_data_state[ComponentType.transformer_tap_regulator]))\n", + "\n", + "group_state = model_state.groups[0]\n", + "y_bus = group_state.y_bus\n", + "y_bus_rows = np.repeat(np.arange(y_bus.row_indptr.size - 1), np.diff(y_bus.row_indptr))\n", + "print(f\"Y-bus nonzero entries: {len(y_bus.col_indices)}\")\n", + "display(\n", + " pd.DataFrame(\n", + " {\n", + " \"row\": y_bus_rows,\n", + " \"column\": y_bus.col_indices,\n", + " \"admittance\": y_bus.admittance_real + 1j * y_bus.admittance_imag,\n", + " }\n", + " ).set_index([\"row\", \"column\"])\n", + ")\n", + "\n", + "print(\"Jacobian iterations:\", [jac.iteration for jac in group_state.jacobians])\n", + "if group_state.jacobians:\n", + " jacobian = group_state.jacobians[-1]\n", + " pattern = group_state.jacobian_structure\n", + " jacobian_rows = np.repeat(np.arange(pattern.row_indptr_lu.size - 1), np.diff(pattern.row_indptr_lu))\n", + " print(\"Final Jacobian (selected tap):\")\n", + " display(\n", + " pd.DataFrame(\n", + " {\n", + " \"row\": jacobian_rows,\n", + " \"column\": pattern.col_indices_lu,\n", + " \"H\": jacobian.h,\n", + " \"N\": jacobian.n,\n", + " \"M\": jacobian.m,\n", + " \"L\": jacobian.l,\n", + " }\n", + " ).set_index([\"row\", \"column\"])\n", + " )\n", + "print(\"Nodal-state iterations:\", [state.iteration for state in group_state.nodal_states])\n", + "voltage_rows = []\n", + "for input_idx, node_id in enumerate(model_state.input_node_id):\n", + " if model_state.input_node_group[input_idx] != group_state.mapping.group:\n", + " continue\n", + " bus_idx = int(model_state.input_node_bus[input_idx])\n", + " row = {\"node\": node_id}\n", + " for state in group_state.nodal_states:\n", + " row[f\"|V|, iteration {state.iteration} (p.u.)\"] = state.voltage_magnitude[bus_idx]\n", + " row[f\"angle, iteration {state.iteration} (rad)\"] = state.voltage_angle[bus_idx]\n", + " voltage_rows.append(row)\n", + "display(pd.DataFrame(voltage_rows).set_index(\"node\"))" + ] + }, + { + "cell_type": "markdown", + "id": "9f1a6f30", + "metadata": {}, + "source": [ + "You could also opt for fast_any_tap that takes advantage of binary search instead of linear search internally." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1fa08adb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994675
14401.932237
26346.203709
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994675\n", + "1 4 401.932237\n", + "2 6 346.203709" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n", + "\n", + "----------fast_any_tap----------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
081-1
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 -1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data6f = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.fast_any_tap)\n", + "\n", + "# the node at the control side of the transformer now has a voltage within the specified voltage band\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data6f[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "print(\"\\n----------fast_any_tap----------\")\n", + "display(pd.DataFrame(output_data6f[ComponentType.transformer_tap_regulator]))" + ] + }, + { + "cell_type": "markdown", + "id": "7828a4e3", + "metadata": {}, + "source": [ + "**NOTE:** the tap positions obtained using the `any_valid_tap` and `fast_any_tap` strategy may depend on the initial tap position of the transformers." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "be276930", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------transformer batch update------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idfrom_statusto_statustap_pos
03-128-1280
13-128-1281
\n", + "
" + ], + "text/plain": [ + " id from_status to_status tap_pos\n", + "0 3 -128 -128 0\n", + "1 3 -128 -128 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node_4 batch result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
04397.867176
14393.881868
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 4 397.867176\n", + "1 4 393.881868" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator batch result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0810
1811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 0\n", + "1 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "transformer_update = initialize_array(DatasetType.update, ComponentType.transformer, (2, 1))\n", + "transformer_update[AttributeType.id] = 3\n", + "transformer_update[AttributeType.tap_pos] = [[0], [1]]\n", + "\n", + "update_data = {ComponentType.transformer: transformer_update}\n", + "\n", + "print(\"------transformer batch update------\")\n", + "display(pd.DataFrame(update_data[ComponentType.transformer][:, 0]))\n", + "\n", + "# power flow batch calculation with automatic tap changing\n", + "output_data = model5.calculate_power_flow(\n", + " update_data=update_data, tap_changing_strategy=TapChangingStrategy.fast_any_tap\n", + ")\n", + "\n", + "print(\"------node_4 batch result------\")\n", + "display(pd.DataFrame(output_data[ComponentType.node][:, 1])[[AttributeType.id, AttributeType.u]]) # only output node 1\n", + "\n", + "print(\"\\n------tap regulator batch result------\")\n", + "display(pd.DataFrame(output_data[ComponentType.transformer_tap_regulator][:, 0]))" + ] + }, + { + "cell_type": "markdown", + "id": "6f6cd15c", + "metadata": {}, + "source": [ + "### Maximum voltage tap position strategy\n", + "\n", + "To specifically optimize for the higher end of the voltage band, the `max_voltage_tap` tap changing strategy can be provided." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "21b01a77", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994683
14406.079525
26352.021534
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994683\n", + "1 4 406.079525\n", + "2 6 352.021534" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
081-2
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 -2" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data5 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.max_voltage_tap)\n", + "\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.transformer_tap_regulator]))" + ] + }, + { + "cell_type": "markdown", + "id": "7b8ab2a2", + "metadata": {}, + "source": [ + "### Minimum voltage tap position strategy\n", + "\n", + "To specifically optimize for the lower end of the voltage band, the `min_voltage_tap` tap changing strategy can be provided." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "c0314199", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994657
14393.881868
26334.529465
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994657\n", + "1 4 393.881868\n", + "2 6 334.529465" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
0811
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data5 = model5.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.min_voltage_tap)\n", + "\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "display(pd.DataFrame(output_data5[ComponentType.transformer_tap_regulator]))" + ] + }, + { + "cell_type": "markdown", + "id": "01a473bd", + "metadata": {}, + "source": [ + "### Automatic tap changing with line drop compensation\n", + "\n", + "In the above examples, the voltage at the low-voltage side of the transformer is controlled. However, the load is still connected to a node with a voltage lower than 390 V.\n", + "\n", + "To regulate the node that the load is connected to, the voltage needs to be compensated for the voltage drop over the line.\n", + "\n", + "```txt\n", + " (tap_side) (control side)\n", + "source_1 --- node_2 --- transformer_3 --- node_4 --- line_5 --- node_6 --- load_7\n", + " | | :\n", + " regulator_8 <-----/ <......................./ (line drop compensation)\n", + " (control voltage) \n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ac091392", + "metadata": {}, + "outputs": [], + "source": [ + "# use the same input data\n", + "input_data6 = {component: component_data.copy() for component, component_data in input_data5.items()}\n", + "\n", + "# set the regulator's line drop compensation\n", + "input_data6[ComponentType.transformer_tap_regulator][AttributeType.line_drop_compensation_r] = [10.0]\n", + "\n", + "# construction\n", + "model6 = PowerGridModel(input_data=input_data6)" + ] + }, + { + "cell_type": "markdown", + "id": "bb89ad98", + "metadata": {}, + "source": [ + "The result of the power flow calculation with automatic tap changing contains a voltage at the node that the load is connected to that is within the specified voltage band." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "9b66d16e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------node result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idu
029999.994731
14437.642686
26393.354002
\n", + "
" + ], + "text/plain": [ + " id u\n", + "0 2 9999.994731\n", + "1 4 437.642686\n", + "2 6 393.354002" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------tap regulator result------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idenergizedtap_pos
081-9
\n", + "
" + ], + "text/plain": [ + " id energized tap_pos\n", + "0 8 1 -9" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# one-time power flow calculation with automatic tap changing\n", + "output_data6 = model6.calculate_power_flow(tap_changing_strategy=TapChangingStrategy.any_valid_tap)\n", + "\n", + "print(\"------node result------\")\n", + "display(pd.DataFrame(output_data6[ComponentType.node])[[AttributeType.id, AttributeType.u]])\n", + "\n", + "print(\"\\n------tap regulator result------\")\n", + "display(pd.DataFrame(output_data6[ComponentType.transformer_tap_regulator]))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "power-grid-model", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } \ No newline at end of file