diff --git a/.prospector.yaml b/.prospector.yaml new file mode 100644 index 000000000..287a9ad52 --- /dev/null +++ b/.prospector.yaml @@ -0,0 +1,22 @@ +pydocstyle: + disable: + - D213 + +pylint: + options: + # This codebase assembles Jacobian/PIT entries in single functions that naturally handle many + # physical quantities (pressures, densities, derivatives, ...) as separate local variables and + # parameters - pylint's stock thresholds (15 locals / 5 args) are far below what that pattern + # needs. Bundling them into dicts/dataclasses purely to satisfy this metric would obscure the + # actual math rather than clarify it, so the limits are raised to comfortably cover the + # largest legitimate cases already in the codebase (39 locals, 13 positional args) instead. + max-locals: 45 + max-args: 15 + max-positional-arguments: 15 + disable: + # `if not len(x):` shows up throughout as `x` is almost always a numpy array or pandas + # DataFrame, not a plain sequence - `if not x:` raises `ValueError: The truth value of an + # array/DataFrame with more than one element is ambiguous` for anything but a 0/1-length + # array. Pylint's suggested rewrite is only valid for plain Python sequences, so applying it + # here would introduce a real crash rather than fix a style nit. + - use-implicit-booleaness-not-len diff --git a/doc/source/pipeflow/internal_functions.rst b/doc/source/pipeflow/internal_functions.rst index 9691ccbca..d45230095 100644 --- a/doc/source/pipeflow/internal_functions.rst +++ b/doc/source/pipeflow/internal_functions.rst @@ -14,14 +14,17 @@ in the pipeflow setup and were not yet described: .. _create_internal_results: .. autofunction:: pandapipes.create_internal_results -.. _extract_all_results: +.. _create_lookups: .. autofunction:: pandapipes.create_lookups -.. _create_junction: +.. _extract_all_results: .. autofunction:: pandapipes.extract_all_results -.. _extract_results_active_pit: -.. autofunction:: pandapipes.extract_results_active_pit +.. _extract_results_active_pit_hydraulics: +.. autofunction:: pandapipes.pf.result_extraction.extract_results_active_pit_hydraulics + +.. _extract_results_active_pit_heat_transfer: +.. autofunction:: pandapipes.pf.result_extraction.extract_results_active_pit_heat_transfer .. _get_lookup: .. autofunction:: pandapipes.get_lookup diff --git a/doc/source/pipeflow/pipeflow_procedure.rst b/doc/source/pipeflow/pipeflow_procedure.rst index 1f4a49fc3..21358ccfd 100644 --- a/doc/source/pipeflow/pipeflow_procedure.rst +++ b/doc/source/pipeflow/pipeflow_procedure.rst @@ -71,7 +71,7 @@ the help of the `scipy csgraph functionalities connectivity check disconnected network areas can be set out of service automatically, reducing the error-proneness of the calculation process. -.. autofunction:: pandapipes.pf.pipeflow_setup.check_connectivity +.. autofunction:: pandapipes.pf.pipeflow_setup.identify_active_nodes_branches .. _internal_matrix: @@ -98,7 +98,9 @@ The functions used to create the internal pit and extract results back from it a .. autofunction:: pandapipes.pf.pipeflow_setup.reduce_pit -.. autofunction:: pandapipes.pf.result_extraction.extract_results_active_pit +Results are written back from the internal pit via +:func:`~pandapipes.pf.result_extraction.extract_results_active_pit_hydraulics` and +:func:`~pandapipes.pf.result_extraction.extract_results_active_pit_heat_transfer`. .. _jacobian: @@ -186,6 +188,6 @@ is the load vector. expressed as a sparse matrix. More information can also be found in :cite:`Ferziger2002`. -.. autofunction:: pandapipes.pf.build_system_matrix.build_system_matrix +.. autofunction:: pandapipes.pf.calculation.solve_hydraulics diff --git a/src/pandapipes/component_models/abstract_models/base_component.py b/src/pandapipes/component_models/abstract_models/base_component.py index 3a58bef77..89bf4e2de 100644 --- a/src/pandapipes/component_models/abstract_models/base_component.py +++ b/src/pandapipes/component_models/abstract_models/base_component.py @@ -18,99 +18,19 @@ class Component: def table_name(cls): raise NotImplementedError() - @classmethod - def init_results(cls, net): - """ - Function that intializes the result table for the component. - - :param net: The pandapipes network - :type net: pandapipesNet - :return: No Output. - """ - output, all_float = cls.get_result_table(net) - init_results_element(net, cls.table_name(), output, all_float) - res_table = net["res_" + cls.table_name()] - return res_table - - @classmethod - def extract_results(cls, net, options, branch_results, mode): - """ - Function that extracts certain results. - - :param net: The pandapipes network - :type net: pandapipesNet - :param options: - :type options: - :param branch_results: - :type branch_results: - :param mode: - :type mode: - :return: No Output. - """ - raise NotImplementedError - @classmethod def get_component_input(cls): - """ - - :return: - :rtype: - """ - raise NotImplementedError + """Get component input. - @classmethod - def get_result_table(cls, net): - """ - Get result table. - - :param net: a pandapipes net - :type net: pandapipes.pandapipesNet :return: :rtype: """ raise NotImplementedError - @classmethod - def adaption_before_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - pass - - @classmethod - def adaption_after_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - pass - - @classmethod - def adaption_before_derivatives_thermal(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - pass - - @classmethod - def adaption_after_derivatives_thermal(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - pass - - @classmethod - def rerun_hydraulics(cls, net, branch_pit, node_pit, idx_lookups, options): - return False - - @classmethod - def rerun_thermal(cls, net, branch_pit, node_pit, idx_lookups, options): - return False - @classmethod def create_node_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current_start, current_table, internals): - """ - Function which creates node lookups. + """Function which creates node lookups. :param net: The pandapipes network :type net: pandapipesNet @@ -134,8 +54,7 @@ def create_node_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current @classmethod def create_branch_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current_start, current_table, internals): - """ - Function which creates branch lookups. + """Function which creates branch lookups. :param net: The pandapipes network :type net: pandapipesNet @@ -156,11 +75,19 @@ def create_branch_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, curre """ return current_start, current_table + @classmethod + def register_pit_node_entries(cls, net, node_pit, registry) -> None: + pass + + @classmethod + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + pass + @classmethod def create_component_array(cls, net, component_pits): - """ - Function which creates an internal array of the component in analogy to the pit, but with - component specific entries, that are not needed in the pit. + """Create an internal array of the component in analogy to the pit. + + Holds component-specific entries that are not needed in the pit. :param net: The pandapipes network :type net: pandapipesNet @@ -172,28 +99,57 @@ def create_component_array(cls, net, component_pits): pass @classmethod - def create_pit_node_entries(cls, net, node_pit): - """ - Function which creates pit branch entries. + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + pass + + @classmethod + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + pass + + @classmethod + def rerun_hydraulics(cls, net, branch_pit, node_pit, idx_lookups, options): # pylint: disable=unused-argument + return False + + @classmethod + def rerun_thermal(cls, net, branch_pit, node_pit, idx_lookups, options): # pylint: disable=unused-argument + return False + + @classmethod + def init_results(cls, net): + """Function that intializes the result table for the component. :param net: The pandapipes network :type net: pandapipesNet - :param node_pit: - :type node_pit: :return: No Output. """ - pass + output, all_float = cls.get_result_table(net) + init_results_element(net, cls.table_name(), output, all_float) + res_table = net["res_" + cls.table_name()] + return res_table @classmethod - def create_pit_branch_entries(cls, net, branch_pit): + def get_result_table(cls, net): + """Get result table. + + :param net: a pandapipes net + :type net: pandapipes.pandapipesNet + :return: + :rtype: """ - Function which creates pit branch entries. + raise NotImplementedError + + @classmethod + def extract_results(cls, net, options, branch_results, mode): + """Function that extracts certain results. :param net: The pandapipes network :type net: pandapipesNet - :param branch_pit: - :type branch_pit: + :param options: + :type options: + :param branch_results: + :type branch_results: + :param mode: + :type mode: :return: No Output. """ - pass - + raise NotImplementedError diff --git a/src/pandapipes/component_models/abstract_models/branch_models.py b/src/pandapipes/component_models/abstract_models/branch_models.py index 71366d061..42845081f 100644 --- a/src/pandapipes/component_models/abstract_models/branch_models.py +++ b/src/pandapipes/component_models/abstract_models/branch_models.py @@ -5,14 +5,10 @@ import numpy as np from pandapipes.component_models.abstract_models.base_component import Component -from pandapipes.idx_branch import ( - MDOTINIT, - branch_cols, - TEXT, - FLOW_RETURN_CONNECT, -) -from pandapipes.pf.pipeflow_setup import get_net_option -from pandapipes.pf.pipeflow_setup import get_table_number, get_lookup +from pandapipes.idx_branch import IdxBranch +from pandapipes.component_models.component_toolbox import build_pit_entries +from pandapipes.pf.pipeflow_setup import get_net_option, get_table_number, get_lookup +from pandapipes.pf.system_index import PitEntries try: import pandaplan.core.pplog as logging @@ -29,11 +25,11 @@ def table_name(cls): raise NotImplementedError @classmethod - def get_component_input(cls): - raise NotImplementedError + def active_identifier(cls): + raise NotImplementedError() @classmethod - def get_result_table(cls, net): + def get_connected_node_type(cls): raise NotImplementedError @classmethod @@ -41,18 +37,13 @@ def from_to_node_cols(cls): raise NotImplementedError @classmethod - def active_identifier(cls): - raise NotImplementedError() - - @classmethod - def get_connected_node_type(cls): + def get_component_input(cls): raise NotImplementedError @classmethod def create_branch_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current_start, current_table, internals): - """ - Function which creates branch lookups. + """Function which creates branch lookups. :param net: The pandapipes network :type net: pandapipesNet @@ -74,30 +65,24 @@ def create_branch_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, curre raise NotImplementedError @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries. - - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - node_pit = net["_pit"]["node"] + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] - branch_table_nr = get_table_number(get_lookup(net, "branch", "table"), cls.table_name()) - branch_component_pit = branch_pit[f:t, :] if not len(net[cls.table_name()]): - return branch_component_pit, node_pit + return + + rows = np.arange(f, t, dtype=np.int32) if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - branch_component_pit[:, :] = np.array([branch_table_nr] + [0] * (branch_cols - 1)) - branch_component_pit[:, TEXT] = get_net_option(net, 'ambient_temperature') - branch_component_pit[:, FLOW_RETURN_CONNECT] = False + branch_table_nr = get_table_number(get_lookup(net, "branch", "table"), cls.table_name()) + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxBranch.TABLE_IDX], + [float(branch_table_nr)], + ))) - branch_component_pit[:, MDOTINIT] = 0.1 - return branch_component_pit, node_pit + @classmethod + def get_result_table(cls, net): + raise NotImplementedError @classmethod def extract_results(cls, net, options, branch_results, mode): diff --git a/src/pandapipes/component_models/abstract_models/branch_w_internals_models.py b/src/pandapipes/component_models/abstract_models/branch_w_internals_models.py index 578fe2f63..74e32c697 100644 --- a/src/pandapipes/component_models/abstract_models/branch_w_internals_models.py +++ b/src/pandapipes/component_models/abstract_models/branch_w_internals_models.py @@ -6,18 +6,11 @@ import pandas as pd from pandapipes.component_models.abstract_models.branch_models import BranchComponent -from pandapipes.component_models.component_toolbox import set_entry_check_repeat, get_internal_lookup_structure -from pandapipes.idx_branch import ( - ACTIVE, - ELEMENT_IDX, - D, - DO, - LOSS_COEFFICIENT as LC, - AREA, - QEXT, -) -from pandapipes.idx_node import L, node_cols +from pandapipes.component_models.component_toolbox import get_internal_lookup_structure, build_pit_entries +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode from pandapipes.pf.pipeflow_setup import add_table_lookup, get_lookup, get_table_number, get_net_option +from pandapipes.pf.system_index import PitEntries try: import pandaplan.core.pplog as logging @@ -28,28 +21,18 @@ class BranchWInternalsComponent(BranchComponent): - """ - - """ + """Abstract base class for branch components with internal nodes.""" @classmethod def table_name(cls): raise NotImplementedError - @classmethod - def get_component_input(cls): - raise NotImplementedError - - @classmethod - def get_result_table(cls, net): - raise NotImplementedError - @classmethod def active_identifier(cls): raise NotImplementedError @classmethod - def calculate_temperature_lift(cls, net, branch_component_pit, node_pit): + def get_connected_node_type(cls): raise NotImplementedError @classmethod @@ -58,17 +41,13 @@ def from_to_node_cols(cls): @classmethod def internal_node_name(cls): - """ + """Return the name of the internal nodes for this class. :return: internal_node_name - name of the internal nodes for this class :rtype: str """ raise NotImplementedError - @classmethod - def get_connected_node_type(cls): - raise NotImplementedError - @classmethod def get_internal_node_number(cls, net, return_internal_only=True): raise NotImplementedError @@ -77,10 +56,13 @@ def get_internal_node_number(cls, net, return_internal_only=True): def get_internal_branch_number(cls, net): return NotImplementedError + @classmethod + def get_component_input(cls): + raise NotImplementedError + @classmethod def create_node_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current_start, current_table, internals): - """ - Function which creates node lookups. + """Function which creates node lookups. :param net: The pandapipes network :type net: pandapipesNet @@ -110,8 +92,7 @@ def create_node_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current @classmethod def create_branch_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current_start, current_table, internals): - """ - Function which creates branch lookups. + """Function which creates branch lookups. :param net: The pandapipes network :type net: pandapipesNet @@ -147,71 +128,62 @@ def create_branch_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, curre return end, current_table + 1 @classmethod - def create_pit_node_entries(cls, net, node_pit): - """ - Function which creates pit node entries. - - :param net: The pandapipes network - :type net: pandapipesNet - :param node_pit: - :type node_pit: - :return: No Output. - """ + def register_pit_node_entries(cls, net, node_pit, registry) -> None: table_lookup = get_lookup(net, "node", "table") table_nr = get_table_number(table_lookup, cls.internal_node_name()) - if table_nr is not None: - ft_lookup = get_lookup(net, "node", "from_to") - f, t = ft_lookup[cls.internal_node_name()] - - int_node_pit = node_pit[f:t, :] - if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - int_node_pit[:, :] = np.array([table_nr, 0, L] + [0] * (node_cols - 3)) - return int_node_pit + if table_nr is None: + return + ft_lookup = get_lookup(net, "node", "from_to") + f, t = ft_lookup[cls.internal_node_name()] + if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: + rows = np.arange(f, t, dtype=np.int32) + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxNode.TABLE_IDX, IdxNode.NODE_TYPE], + [float(table_nr), float(IdxNode.L)], + ))) @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries. - - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - branch_w_internals_pit, node_pit = super().create_pit_branch_entries(net, branch_pit) + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) - if not len(branch_w_internals_pit): - return branch_w_internals_pit, node_pit + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + if not len(net[cls.table_name()]): + return tbl = cls.table_name() node_ft_lookups = get_lookup(net, "node", "from_to") has_internals = cls.internal_node_name() in node_ft_lookups internal_branch_number = cls.get_internal_branch_number(net) + rows = np.arange(f, t, dtype=np.int32) + if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - set_entry_check_repeat(branch_w_internals_pit, ELEMENT_IDX, net[tbl].index.values, internal_branch_number, - has_internals) - set_entry_check_repeat(branch_w_internals_pit, ACTIVE, net[tbl][cls.active_identifier()].values, - internal_branch_number, has_internals) - set_entry_check_repeat(branch_w_internals_pit, D, net[tbl].inner_diameter_mm.values / 1000., internal_branch_number, - has_internals) + def _rep(vals): + return np.repeat(vals, internal_branch_number) if has_internals else vals + + d_vals = _rep(net[tbl].inner_diameter_mm.values / 1000.) + lc_vals = _rep(net[tbl].loss_coefficient.values) + elem_idx_vals = _rep(net[tbl].index.values.astype(float)) + active_vals = _rep(net[tbl][cls.active_identifier()].values.astype(float)) + if "outer_diameter_mm" in net[tbl]: - outer = net[tbl].outer_diameter_mm.values + outer = net[tbl].outer_diameter_mm.values.copy() inner = net[tbl].inner_diameter_mm.values outer[pd.isnull(outer)] = inner[pd.isnull(outer)] - set_entry_check_repeat(branch_w_internals_pit, DO, outer / 1000., internal_branch_number, - has_internals) - branch_w_internals_pit[np.isnan(branch_w_internals_pit[:, DO]), DO] = ( - branch_w_internals_pit)[np.isnan(branch_w_internals_pit[:, DO]), D] + do_vals = _rep(outer / 1000.) + do_vals[np.isnan(do_vals)] = d_vals[np.isnan(do_vals)] else: - set_entry_check_repeat(branch_w_internals_pit, DO, net[tbl].inner_diameter_mm.values / 1000., internal_branch_number, - has_internals) - set_entry_check_repeat(branch_w_internals_pit, LC, net[tbl].loss_coefficient.values, internal_branch_number, - has_internals) + do_vals = d_vals.copy() + + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxBranch.ELEMENT_IDX, IdxBranch.ACTIVE, IdxBranch.D, IdxBranch.DO, IdxBranch.LOSS_COEFFICIENT], + [elem_idx_vals, active_vals, d_vals, do_vals, lc_vals], + ))) - branch_w_internals_pit[:, AREA] = branch_w_internals_pit[:, D] ** 2 * np.pi / 4 - branch_w_internals_pit[:, QEXT] = 0.0 - return branch_w_internals_pit, node_pit + @classmethod + def calculate_temperature_lift(cls, net, branch_component_pit, node_pit): + raise NotImplementedError @classmethod def extract_results(cls, net, options, branch_results, mode): @@ -219,7 +191,7 @@ def extract_results(cls, net, options, branch_results, mode): @classmethod def get_internal_results(cls, net, branch): - """ + """Get internal results for a branch. :param net: :type net: @@ -229,3 +201,7 @@ def get_internal_results(cls, net, branch): :rtype: """ raise NotImplementedError + + @classmethod + def get_result_table(cls, net): + raise NotImplementedError diff --git a/src/pandapipes/component_models/abstract_models/branch_wo_internals_models.py b/src/pandapipes/component_models/abstract_models/branch_wo_internals_models.py index ed9287885..c0ce0211b 100644 --- a/src/pandapipes/component_models/abstract_models/branch_wo_internals_models.py +++ b/src/pandapipes/component_models/abstract_models/branch_wo_internals_models.py @@ -5,10 +5,10 @@ import numpy as np from pandapipes.component_models.abstract_models.branch_models import BranchComponent -from pandapipes.idx_branch import (FROM_NODE, TO_NODE, TOUTINIT, ELEMENT_IDX, ACTIVE, LENGTH, K, TEXT, ALPHA, - D, DO, AREA) -from pandapipes.idx_node import TINIT as TINIT_NODE +from pandapipes.component_models.component_toolbox import build_pit_entries +from pandapipes.idx_branch import IdxBranch from pandapipes.pf.pipeflow_setup import add_table_lookup, get_net_option, get_lookup +from pandapipes.pf.system_index import PitEntries try: import pandaplan.core.pplog as logging @@ -25,15 +25,11 @@ def table_name(cls): raise NotImplementedError @classmethod - def get_component_input(cls): - raise NotImplementedError - - @classmethod - def get_result_table(cls, net): + def active_identifier(cls): raise NotImplementedError @classmethod - def active_identifier(cls): + def get_connected_node_type(cls): raise NotImplementedError @classmethod @@ -41,13 +37,12 @@ def from_to_node_cols(cls): raise NotImplementedError @classmethod - def get_connected_node_type(cls): + def get_component_input(cls): raise NotImplementedError @classmethod def create_branch_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current_start, current_table, internals): - """ - Function which creates branch lookups. + """Function which creates branch lookups. :param net: The pandapipes network :type net: pandapipesNet @@ -72,40 +67,40 @@ def create_branch_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, curre return end, current_table + 1 @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - branch_wo_internals_pit, node_pit = super().create_pit_branch_entries(net, branch_pit) - junction_idx_lookup = get_lookup(net, "node", "index")[ - cls.get_connected_node_type().table_name()] - fn_col, tn_col = cls.from_to_node_cols() - from_nodes = junction_idx_lookup[net[cls.table_name()][fn_col].values] - to_nodes = junction_idx_lookup[net[cls.table_name()][tn_col].values] + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return - if not len(branch_wo_internals_pit): - return branch_wo_internals_pit + rows = np.arange(f, t, dtype=np.int32) + junction_table_name = cls.get_connected_node_type().table_name() + junction_idx_lookup = get_lookup(net, "node", "index")[junction_table_name] + fn_col, tn_col = cls.from_to_node_cols() + from_junctions = tbl[fn_col].values + to_junctions = tbl[tn_col].values + from_nodes = junction_idx_lookup[from_junctions] + to_nodes = junction_idx_lookup[to_junctions] if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - branch_wo_internals_pit[:, FROM_NODE] = from_nodes - branch_wo_internals_pit[:, TO_NODE] = to_nodes - branch_wo_internals_pit[:, TOUTINIT] = node_pit[to_nodes, TINIT_NODE] - branch_wo_internals_pit[:, ELEMENT_IDX] = net[cls.table_name()].index.values - branch_wo_internals_pit[:, ACTIVE] = net[cls.table_name()][cls.active_identifier()].values - branch_wo_internals_pit[:, LENGTH] = 0 - branch_wo_internals_pit[:, K] = 1e-3 - branch_wo_internals_pit[:, TEXT] = get_net_option(net, 'ambient_temperature') - branch_wo_internals_pit[:, ALPHA] = 0 - branch_wo_internals_pit[:, D] = 0.1 - branch_wo_internals_pit[:, DO] = branch_wo_internals_pit[:, D] - branch_wo_internals_pit[:, AREA] = branch_wo_internals_pit[:, D] ** 2 * np.pi / 4 - return branch_wo_internals_pit + toutinit_vals = cls._toutinit_vals(net, to_junctions, junction_table_name) + ambient_t = get_net_option(net, 'ambient_temperature') + d_val = 0.1 + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxBranch.FROM_NODE, IdxBranch.TO_NODE, IdxBranch.TOUTINIT, IdxBranch.ELEMENT_IDX, + IdxBranch.ACTIVE, IdxBranch.LENGTH, IdxBranch.K, IdxBranch.TEXT, IdxBranch.ALPHA, + IdxBranch.D, IdxBranch.DO], + [from_nodes.astype(float), to_nodes.astype(float), toutinit_vals, + tbl.index.values.astype(float), tbl[cls.active_identifier()].values.astype(float), + 0., 1e-3, float(ambient_t), 0., d_val, d_val], + ))) + + @classmethod + def _toutinit_vals(cls, net, to_junctions, junction_table_name): + return net[junction_table_name].loc[to_junctions, "tfluid_k"].values @classmethod def calculate_temperature_lift(cls, net, branch_component_pit, node_pit): @@ -114,3 +109,7 @@ def calculate_temperature_lift(cls, net, branch_component_pit, node_pit): @classmethod def extract_results(cls, net, options, branch_results, mode): raise NotImplementedError + + @classmethod + def get_result_table(cls, net): + raise NotImplementedError diff --git a/src/pandapipes/component_models/abstract_models/circulation_pump.py b/src/pandapipes/component_models/abstract_models/circulation_pump.py index 83c014204..5e7cf0da0 100644 --- a/src/pandapipes/component_models/abstract_models/circulation_pump.py +++ b/src/pandapipes/component_models/abstract_models/circulation_pump.py @@ -5,12 +5,13 @@ import numpy as np from pandapipes.component_models.abstract_models.branch_wo_internals_models import BranchWOInternalsComponent -from pandapipes.component_models.component_toolbox import set_fixed_node_entries, standard_branch_wo_internals_result_lookup -from pandapipes.idx_branch import D, AREA, LOAD_VEC_BRANCHES_T, TO_NODE, TOUTINIT, JAC_DERIV_DT, JAC_DERIV_DTOUT, MDOTINIT -from pandapipes.idx_node import MDOTSLACKINIT, VAR_MASS_SLACK, JAC_DERIV_MSL, NODE_TYPE_T, GE, TINIT +from pandapipes.component_models.component_toolbox import build_pit_entries, standard_branch_wo_internals_result_lookup +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode from pandapipes.pf.pipeflow_setup import get_fluid, get_lookup from pandapipes.pf.internals_toolbox import get_from_nodes_corrected from pandapipes.pf.result_extraction import extract_branch_results_without_internals +from pandapipes.pf.system_index import PitEntries, PitWriteMode try: import pandaplan.core.pplog as logging @@ -26,32 +27,6 @@ class CirculationPump(BranchWOInternalsComponent): def table_name(cls): raise NotImplementedError - @classmethod - def get_component_input(cls): - raise NotImplementedError - - @classmethod - def get_result_table(cls, net): - """ - - Gets the result table. - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ - if get_fluid(net).is_gas: - output = ["p_from_bar", "p_to_bar", "t_from_k", - "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_norm_m3_per_s", - "normfactor_from", "normfactor_to"] - else: - output = ["p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", - "mdot_to_kg_per_s", "vdot_m3_per_s"] - output += ['deltat_k', 'qext_w'] - return output, True - @classmethod def active_identifier(cls): raise NotImplementedError @@ -66,102 +41,69 @@ def get_connected_node_type(cls): return Junction @classmethod - def create_pit_node_entries(cls, net, node_pit): - """ - Function which creates pit node entries. - - :param net: The pandapipes network - :type net: pandapipesNet - :param node_pit: - :type node_pit: - :return: No Output. - """ - circ_pump_tbl = net[cls.table_name()][net[cls.table_name()][cls.active_identifier()].values] - - junction = circ_pump_tbl[cls.from_to_node_cols()[1]].values + def get_component_input(cls): + raise NotImplementedError - # TODO: there should be a warning, if any p_bar value is not given or any of the types does - # not contain "p", as this should not be allowed for this component + @classmethod + def register_pit_node_entries(cls, net, node_pit, registry) -> None: + tbl = net[cls.table_name()] + active_mask = tbl[cls.active_identifier()].values + circ_pump_tbl = tbl[active_mask] + if not len(circ_pump_tbl): + return + + _, tn_col = cls.from_to_node_cols() + junction = circ_pump_tbl[tn_col].values types = circ_pump_tbl.type.values p_values = circ_pump_tbl.p_flow_bar.values - index_p = set_fixed_node_entries( - net, node_pit, junction, types, p_values, cls.get_connected_node_type(), 'p') - node_pit[index_p, JAC_DERIV_MSL] = -1. - node_pit[index_p, NODE_TYPE_T] = GE - return circ_pump_tbl, p_values - @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ + junction_lookup = get_lookup(net, "node", "index")[cls.get_connected_node_type().table_name()] + mask_p = np.isin(types, ["p", "pt"]) + index_p = junction_lookup[junction[mask_p]] - node_pit = net["_pit"]["node"] - junction_idx_lookups = get_lookup(net, "node", "index")[ - cls.get_connected_node_type().table_name()] + registry.add_override(PitEntries(*build_pit_entries( + index_p, + [IdxNode.PINIT, IdxNode.NODE_TYPE, IdxNode.NODE_TYPE_T], + [p_values[mask_p], float(IdxNode.P), float(IdxNode.GE)], + ), mode=PitWriteMode.UNIQUE)) - circ_pump_tbl = net[cls.table_name()][net[cls.table_name()][cls.active_identifier()].values] - circ_pump_pit = super().create_pit_branch_entries(net, branch_pit) - circ_pump_pit[:, D] = 0.1 - circ_pump_pit[:, AREA] = circ_pump_pit[:, D] ** 2 * np.pi / 4 - - types = circ_pump_tbl.type.values - mask_t = np.isin(types, ["pt", "t"]) - juncts = circ_pump_tbl[cls.from_to_node_cols()[1]].values - to_nodes = junction_idx_lookups[juncts] - circ_pump_pit[mask_t, TOUTINIT] = circ_pump_tbl.t_flow_k.values[mask_t] - circ_pump_pit[~mask_t, TOUTINIT] = node_pit[to_nodes[~mask_t], TINIT] - return circ_pump_pit + @classmethod + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) @classmethod - def adaption_after_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - """ - Function which creates pit branch entries with a specific table. - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - f, t = idx_lookups[cls.table_name()] - circ_pump_pit = branch_pit[f:t, :] - tn = circ_pump_pit[:, TO_NODE].astype(np.int32) - mask = node_pit[tn, VAR_MASS_SLACK].astype(bool) - node_pit[tn[~mask], MDOTSLACKINIT] = 0 - return circ_pump_pit + def _toutinit_vals(cls, net, to_junctions, junction_table_name): + tbl = net[cls.table_name()] + mask_t = np.isin(tbl.type.values, ["pt", "t"]) + return np.where( + mask_t, + tbl.t_flow_k.values, + net[junction_table_name].loc[to_junctions, "tfluid_k"].values, + ) @classmethod - def adaption_after_derivatives_thermal(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - """ - Function which creates pit branch entries with a specific table. + def get_result_table(cls, net): + """Gets the result table. + :param net: The pandapipes network :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. + :return: (columns, all_float) - the column names and whether they are all float type. Only + if False, returns columns as tuples also specifying the dtypes + :rtype: (list, bool) """ - f, t = idx_lookups[cls.table_name()] - circ_pump_pit = branch_pit[f:t, :] - circ_pump_pit[:, LOAD_VEC_BRANCHES_T] = 0 - circ_pump_pit[:, JAC_DERIV_DTOUT] = 1 - circ_pump_pit[:, JAC_DERIV_DT] = 0 - + if get_fluid(net).is_gas: + output = ["p_from_bar", "p_to_bar", "t_from_k", + "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_norm_m3_per_s", + "normfactor_from", "normfactor_to"] + else: + output = ["p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", + "mdot_to_kg_per_s", "vdot_m3_per_s"] + output += ['deltat_k', 'qext_w'] + return output, True @classmethod def extract_results(cls, net, options, branch_results, mode): - """ - Function that extracts certain results. + """Function that extracts certain results. :param mode: :type mode: @@ -178,7 +120,7 @@ def extract_results(cls, net, options, branch_results, mode): branch_lookups = get_lookup(net, "branch", "from_to") f, t = branch_lookups[cls.table_name()] - mask = (branch_pit[f:t, MDOTINIT] < 0) & ~np.isclose(branch_pit[f:t, MDOTINIT], 0) + mask = (branch_pit[f:t, IdxBranch.MDOTINIT] < 0) & ~np.isclose(branch_pit[f:t, IdxBranch.MDOTINIT], 0) if np.any(mask): raise UserWarning(r'Your grid is badly modelled and would lead to a direction change in circulation pump %s' % str(net[cls.table_name()].index[mask].tolist())) @@ -191,14 +133,15 @@ def extract_results(cls, net, options, branch_results, mode): res_table = net["res_" + cls.table_name()] from_nodes = get_from_nodes_corrected(branch_pit[f:t]) - t_from = node_pit[from_nodes, TINIT] - tout = branch_pit[f:t, TOUTINIT] + t_from = node_pit[from_nodes, IdxNode.TINIT] + tout = branch_pit[f:t, IdxBranch.TOUTINIT] res_table['deltat_k'].values[:] = t_from - tout fluid = get_fluid(net) cp_i = fluid.get_heat_capacity(t_from) cp_i1 = fluid.get_heat_capacity(tout) + cp = (cp_i + cp_i1) / 2 - mass = branch_pit[f:t, MDOTINIT] - res_table['qext_w'].values[:] = mass * (cp_i1 * tout - cp_i * t_from) \ No newline at end of file + mass = branch_pit[f:t, IdxBranch.MDOTINIT] + res_table['qext_w'].values[:] = mass * cp * (tout - t_from) diff --git a/src/pandapipes/component_models/abstract_models/const_flow_models.py b/src/pandapipes/component_models/abstract_models/const_flow_models.py index a91697609..fb61aa3b0 100644 --- a/src/pandapipes/component_models/abstract_models/const_flow_models.py +++ b/src/pandapipes/component_models/abstract_models/const_flow_models.py @@ -5,9 +5,11 @@ import numpy as np from numpy import dtype from pandapipes.component_models.abstract_models.node_element_models import NodeElementComponent -from pandapipes.idx_node import LOAD, ELEMENT_IDX +from pandapipes.idx_node import IdxNode from pandapipes.pf.internals_toolbox import _sum_by_group from pandapipes.pf.pipeflow_setup import get_lookup, get_net_option +from pandapipes.pf.system_index import PitEntries, PitWriteMode, ComponentEquations, HydVarEq +from pandapipes.pf.derivative_calculation import calculate_load_hydraulic class ConstFlow(NodeElementComponent): @@ -16,6 +18,10 @@ class ConstFlow(NodeElementComponent): def table_name(cls): raise NotImplementedError + @classmethod + def active_identifier(cls): + raise NotImplementedError + @classmethod def sign(cls): raise NotImplementedError() @@ -25,20 +31,21 @@ def get_connected_node_type(cls): raise NotImplementedError @classmethod - def active_identifier(cls): - raise NotImplementedError + def get_component_input(cls): + """Get component input. - @classmethod - def create_pit_node_entries(cls, net, node_pit): + :return: + :rtype: """ - Function which creates pit node entries. + return [("name", dtype(object)), + ("junction", "u4"), + ("mdot_kg_per_s", "f8"), + ("scaling", "f8"), + ("in_service", "bool"), + ("type", dtype(object))] - :param net: The pandapipes network - :type net: pandapipesNet - :param node_pit: - :type node_pit: - :return: No Output. - """ + @classmethod + def register_pit_node_entries(cls, net, node_pit, registry) -> None: loads = net[cls.table_name()] helper = loads.in_service.values * loads.scaling.values * cls.sign() mf = np.nan_to_num(loads.mdot_kg_per_s.values) @@ -47,13 +54,48 @@ def create_pit_node_entries(cls, net, node_pit): mass_flow_loads) junction_idx_lookups = get_lookup(net, "node", "index")[ cls.get_connected_node_type().table_name()] - index = junction_idx_lookups[juncts] - node_pit[index, LOAD] += loads_sum + index = junction_idx_lookups[juncts].astype(np.int32) + registry.add(PitEntries( + index, + np.full(len(index), IdxNode.LOAD, dtype=np.int32), + loads_sum.astype(np.float64), + mode=PitWriteMode.ADDITIVE, + )) @classmethod - def extract_results(cls, net, options, branch_results, mode): + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + index, loads_sum = calculate_load_hydraulic( + net, net[cls.table_name()], cls.sign(), cls.get_connected_node_type().table_name()) + if not len(index): + return + + # equation positions node + n_eq = sys_idx.idx(HydVarEq.NODE, index) + + # load vector node + registry.add(ComponentEquations( + rows=np.empty(0, dtype=np.int32), + cols=np.empty(0, dtype=np.int32), + data=np.empty(0, dtype=np.float64), + load_rows=n_eq.astype(np.int32), + load_data=loads_sum.astype(np.float64), + )) + + @classmethod + def get_result_table(cls, net): + """Get results. + + :param net: The pandapipes network + :type net: pandapipesNet + :return: (columns, all_float) - the column names and whether they are all float type. Only + if False, returns columns as tuples also specifying the dtypes + :rtype: (list, bool) """ - Function that extracts certain results. + return ["mdot_kg_per_s"], True + + @classmethod + def extract_results(cls, net, options, branch_results, mode): + """Function that extracts certain results. :param mode: :type mode: @@ -73,34 +115,8 @@ def extract_results(cls, net, options, branch_results, mode): fj, tj = get_lookup(net, "node", "from_to")[cls.get_connected_node_type().table_name()] junct_pit = net["_pit"]["node"][fj:tj, :] nodes_connected_hyd = get_lookup(net, "node", "active_hydraulics")[fj:tj] - is_juncts = np.isin(loads.junction.values, junct_pit[nodes_connected_hyd, ELEMENT_IDX]) + is_juncts = np.isin(loads.junction.values, junct_pit[nodes_connected_hyd, IdxNode.ELEMENT_IDX]) is_calc = is_loads & is_juncts res_table["mdot_kg_per_s"].values[is_calc] = loads.mdot_kg_per_s.values[is_calc] \ * loads.scaling.values[is_calc] - - @classmethod - def get_component_input(cls): - """ - - :return: - :rtype: - """ - return [("name", dtype(object)), - ("junction", "u4"), - ("mdot_kg_per_s", "f8"), - ("scaling", "f8"), - ("in_service", "bool"), - ("type", dtype(object))] - - @classmethod - def get_result_table(cls, net): - """Get results. - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ - return ["mdot_kg_per_s"], True diff --git a/src/pandapipes/component_models/abstract_models/node_element_models.py b/src/pandapipes/component_models/abstract_models/node_element_models.py index 9fb0ae959..cb9d1709b 100644 --- a/src/pandapipes/component_models/abstract_models/node_element_models.py +++ b/src/pandapipes/component_models/abstract_models/node_element_models.py @@ -13,41 +13,26 @@ class NodeElementComponent(Component): - """ - - """ + """Abstract base class for node element components.""" @classmethod def table_name(cls): raise NotImplementedError @classmethod - def get_connected_node_type(cls): - raise NotImplementedError - - @classmethod - def get_component_input(cls): + def active_identifier(cls): raise NotImplementedError @classmethod - def get_result_table(cls, net): + def get_connected_node_type(cls): raise NotImplementedError @classmethod - def active_identifier(cls): + def get_component_input(cls): raise NotImplementedError @classmethod - def create_pit_node_entries(cls, net, node_pit): - """ - Function that creates pit node entries. - - :param net: The pandapipes network - :type net: pandapipesNet - :param node_pit: - :type node_pit: - :return: No Output. - """ + def get_result_table(cls, net): raise NotImplementedError @classmethod diff --git a/src/pandapipes/component_models/abstract_models/node_models.py b/src/pandapipes/component_models/abstract_models/node_models.py index 3eb4c3f4e..cb90e42c7 100644 --- a/src/pandapipes/component_models/abstract_models/node_models.py +++ b/src/pandapipes/component_models/abstract_models/node_models.py @@ -13,19 +13,20 @@ class NodeComponent(Component): - """ - - """ + """Abstract base class for node components.""" @classmethod def table_name(cls): raise NotImplementedError + @classmethod + def get_component_input(cls): + raise NotImplementedError + @classmethod def create_node_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current_start, current_table, internals): - """ - Function which creates node lookups. + """Function which creates node lookups. :param net: The pandapipes network :type net: pandapipesNet @@ -45,22 +46,6 @@ def create_node_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current """ raise NotImplementedError - @classmethod - def create_pit_node_entries(cls, net, node_pit): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :param node_pit: - :type node_pit: - :return: No Output. - """ - raise NotImplementedError - - @classmethod - def get_component_input(cls): - raise NotImplementedError - @classmethod def get_result_table(cls, net): raise NotImplementedError diff --git a/src/pandapipes/component_models/circulation_pump_mass_component.py b/src/pandapipes/component_models/circulation_pump_mass_component.py index f773bbe27..e7e78f5f5 100644 --- a/src/pandapipes/component_models/circulation_pump_mass_component.py +++ b/src/pandapipes/component_models/circulation_pump_mass_component.py @@ -2,12 +2,20 @@ # and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. +import numpy as np from numpy import dtype from pandapipes.component_models.abstract_models.circulation_pump import CirculationPump +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, get_thermal_options, register_branch_node_thermal_balance, + register_circ_pump_node_continuity, register_circ_pump_slack_equations, +) from pandapipes.component_models.junction_component import Junction -from pandapipes.idx_branch import JAC_DERIV_DP, JAC_DERIV_DP1, JAC_DERIV_DM, MDOTINIT, \ - LOAD_VEC_BRANCHES +from pandapipes.idx_branch import IdxBranch +from pandapipes.pf.derivative_calculation import calculate_derivatives_branch_thermal +from pandapipes.pf.internals_toolbox import get_to_nodes_corrected +from pandapipes.pf.pipeflow_setup import get_lookup +from pandapipes.pf.system_index import ComponentEquations, EqWriteMode, PitEntries, HydVarEq, ThermVarEq try: import pandaplan.core.pplog as logging @@ -23,9 +31,17 @@ class CirculationPumpMass(CirculationPump): def table_name(cls): return "circ_pump_mass" + @classmethod + def active_identifier(cls): + return "in_service" + + @classmethod + def get_connected_node_type(cls): + return Junction + @classmethod def get_component_input(cls): - """ + """Get component input. :return: :rtype: @@ -40,31 +56,96 @@ def get_component_input(cls): ("type", dtype(object))] @classmethod - def get_connected_node_type(cls): - return Junction + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) - @classmethod - def active_identifier(cls): - return "in_service" + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return + + rows = np.arange(f, t, dtype=np.int32) + registry.add(PitEntries(*build_pit_entries( + rows, [IdxBranch.MDOTINIT], [tbl.mdot_flow_kg_per_s.values], + ))) @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - circ_pump_pit = super().create_pit_branch_entries(net, branch_pit) - circ_pump_pit[:, MDOTINIT] = net[cls.table_name()].mdot_flow_kg_per_s.values + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + register_circ_pump_node_continuity(net, branch_pit, sys_idx, registry, cls.table_name()) + _, tn_col = cls.from_to_node_cols() + register_circ_pump_slack_equations( + net, node_pit, sys_idx, registry, cls.table_name(), cls.active_identifier(), + tn_col, cls.get_connected_node_type().table_name(), + ) + + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + if f == t: + return + + branch_idx = np.arange(f, t, dtype=np.int32) + + # variables + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # system matrix branch: 1 * δm = 0 (mass flow is fixed, no change; override) + rows_branch = branch_eq.astype(np.int32) + cols_branch = mdot_col.astype(np.int32) + data_branch = np.ones(len(branch_idx), dtype=np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = np.zeros(len(branch_idx), dtype=np.float64) + + registry.add_override(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + mode=EqWriteMode.UNIQUE, + )) @classmethod - def adaption_after_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - # set all pressure derivatives to 0 and velocity to 1; load vector must be 0, as no change - # of velocity is allowed during the pipeflow iteration - circ_pump_pit = super().adaption_after_derivatives_hydraulic(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options) - circ_pump_pit[:, JAC_DERIV_DP] = 0 - circ_pump_pit[:, JAC_DERIV_DP1] = 0 - circ_pump_pit[:, JAC_DERIV_DM] = 1 - circ_pump_pit[:, LOAD_VEC_BRANCHES] = 0 + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + if f == t: + return + + branch_idx = np.arange(f, t, dtype=np.int32) + branch_pit_old = net["_active_old_pit"]["branch"] + fnt, dfnt_dt, dfnt_dtout, _, _, _ = calculate_derivatives_branch_thermal( + net, branch_pit[f:t], node_pit, branch_pit_old[f:t], get_thermal_options(net) + ) + + b_pit = branch_pit[f:t] + tn = get_to_nodes_corrected(b_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # system matrix branch: outlet temperature fixed at t_flow_k (override) + rows_branch = branch_eq.astype(np.int32) + cols_branch = branch_eq.astype(np.int32) + data_branch = np.ones(len(branch_idx), dtype=np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = np.zeros(len(branch_idx), dtype=np.float64) + + registry.add_override(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + mode=EqWriteMode.UNIQUE, + )) + # t_tn_col == the node equation's own row index here (ThermVarEq.NODE and ThermVarEq.TINIT + # share the same block in a square system, see HeatSystemIndex) - computed explicitly + # rather than reusing tn_eq, to match register_branch_node_thermal_balance's own signature + t_tn_col = sys_idx.idx(ThermVarEq.TINIT, tn) + register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) diff --git a/src/pandapipes/component_models/circulation_pump_pressure_component.py b/src/pandapipes/component_models/circulation_pump_pressure_component.py index a2fd6d36d..5dffebb21 100644 --- a/src/pandapipes/component_models/circulation_pump_pressure_component.py +++ b/src/pandapipes/component_models/circulation_pump_pressure_component.py @@ -2,11 +2,23 @@ # and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. +import numpy as np from numpy import dtype from pandapipes.component_models.abstract_models.circulation_pump import CirculationPump +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, get_thermal_options, register_branch_node_thermal_balance, + register_circ_pump_node_continuity, register_circ_pump_slack_equations, +) from pandapipes.component_models.junction_component import Junction -from pandapipes.idx_branch import JAC_DERIV_DP, JAC_DERIV_DP1, PL +from pandapipes.constants import GRAVITATION_CONSTANT, P_CONVERSION +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.derivative_calculation import calculate_derivatives_branch_thermal +from pandapipes.pf.internals_toolbox import get_to_nodes_corrected +from pandapipes.pf.pipeflow_setup import get_lookup, get_fluid +from pandapipes.properties.properties_toolbox import get_branch_real_density +from pandapipes.pf.system_index import ComponentEquations, EqWriteMode, PitEntries, HydVarEq, ThermVarEq try: import pandaplan.core.pplog as logging @@ -22,9 +34,17 @@ class CirculationPumpPressure(CirculationPump): def table_name(cls): return "circ_pump_pressure" + @classmethod + def active_identifier(cls): + return "in_service" + + @classmethod + def get_connected_node_type(cls): + return Junction + @classmethod def get_component_input(cls): - """ + """Get component input. :return: :rtype: @@ -33,37 +53,109 @@ def get_component_input(cls): ("t_flow_k", "f8"), ("plift_bar", "f8"), ("in_service", 'bool'), ("type", dtype(object))] @classmethod - def active_identifier(cls): - return "in_service" + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) - @classmethod - def get_connected_node_type(cls): - return Junction + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return + + rows = np.arange(f, t, dtype=np.int32) + registry.add(PitEntries(*build_pit_entries( + rows, [IdxBranch.PL], [tbl['plift_bar'].values], + ))) @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - circ_pump_pit = super().create_pit_branch_entries(net, branch_pit) - circ_pump_pit[:, PL] = net[cls.table_name()]['plift_bar'].values - return circ_pump_pit + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + register_circ_pump_node_continuity(net, branch_pit, sys_idx, registry, cls.table_name()) + _, tn_col = cls.from_to_node_cols() + register_circ_pump_slack_equations( + net, node_pit, sys_idx, registry, cls.table_name(), cls.active_identifier(), + tn_col, cls.get_connected_node_type().table_name(), + ) + + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + if f == t: + return + + branch_idx = np.arange(f, t, dtype=np.int32) + b_pit = branch_pit[f:t] + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + + # Pressure residual: p_from - p_to + PL + height_correction + p_from_abs = node_pit[fn, IdxNode.PINIT] + node_pit[fn, IdxNode.PAMB] + p_to_abs = node_pit[tn, IdxNode.PINIT] + node_pit[tn, IdxNode.PAMB] + fluid = get_fluid(net) + rho = get_branch_real_density(fluid, node_pit, b_pit) + height_diff = node_pit[fn, IdxNode.HEIGHT] - node_pit[tn, IdxNode.HEIGHT] + const_height = rho * GRAVITATION_CONSTANT * height_diff / P_CONVERSION + load = p_from_abs - p_to_abs + b_pit[:, IdxBranch.PL] + const_height + + # variables + p_from_col = sys_idx.idx(HydVarEq.PINIT, fn) + p_to_col = sys_idx.idx(HydVarEq.PINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # system matrix branch: 1 * δp_from - 1 * δp_to = load (override) + rows_branch = np.concatenate([branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([p_from_col, p_to_col]).astype(np.int32) + data_branch = np.concatenate([np.ones(len(branch_idx)), -np.ones(len(branch_idx))]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = load.astype(np.float64) + + registry.add_override(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + mode=EqWriteMode.UNIQUE, + )) @classmethod - def adaption_after_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - # set all pressure derivatives to 0 and velocity to 1; load vector must be 0, as no change - # of velocity is allowed during the pipeflow iteration - circ_pump_pit = super().adaption_after_derivatives_hydraulic(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options) - circ_pump_pit[:, JAC_DERIV_DP] = 1 - circ_pump_pit[:, JAC_DERIV_DP1] = -1 + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + if f == t: + return + + branch_idx = np.arange(f, t, dtype=np.int32) + branch_pit_old = net["_active_old_pit"]["branch"] + fnt, dfnt_dt, dfnt_dtout, _, _, _ = calculate_derivatives_branch_thermal( + net, branch_pit[f:t], node_pit, branch_pit_old[f:t], get_thermal_options(net) + ) + + b_pit = branch_pit[f:t] + tn = get_to_nodes_corrected(b_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # system matrix branch: outlet temperature fixed at t_flow_k (override) + rows_branch = branch_eq.astype(np.int32) + cols_branch = branch_eq.astype(np.int32) + data_branch = np.ones(len(branch_idx), dtype=np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = np.zeros(len(branch_idx), dtype=np.float64) + + registry.add_override(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + mode=EqWriteMode.UNIQUE, + )) + + # t_tn_col == the node equation's own row index here (ThermVarEq.NODE and ThermVarEq.TINIT + # share the same block in a square system, see HeatSystemIndex) - computed explicitly + # rather than reusing tn_eq, to match register_branch_node_thermal_balance's own signature + t_tn_col = sys_idx.idx(ThermVarEq.TINIT, tn) + register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) diff --git a/src/pandapipes/component_models/component_toolbox.py b/src/pandapipes/component_models/component_toolbox.py index 381f11de1..b0803e16d 100644 --- a/src/pandapipes/component_models/component_toolbox.py +++ b/src/pandapipes/component_models/component_toolbox.py @@ -9,14 +9,193 @@ from pandapipes import get_fluid from pandapipes.constants import NORMAL_PRESSURE, TEMP_GRADIENT_KPM, AVG_TEMPERATURE_K, \ HEIGHT_EXPONENT -from pandapipes.idx_branch import LOAD_VEC_NODES_FROM, LOAD_VEC_NODES_TO, FROM_NODE, TO_NODE -from pandapipes.idx_node import (EXT_GRID_OCCURENCE, EXT_GRID_OCCURENCE_T, - PINIT, NODE_TYPE, P, TINIT, NODE_TYPE_T, T, LOAD) -from pandapipes.pf.pipeflow_setup import get_net_option, get_lookup -from pandapipes.pf.internals_toolbox import _sum_by_group +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.pipeflow_setup import get_lookup, get_net_option +from pandapipes.pf.system_index import ComponentEquations, EqWriteMode, HydVarEq, ThermVarEq from pandas import Index +def get_hydraulic_options(net): + """``options`` dict expected by calculate_derivatives_hydraulic. + + Factored out because every branch component's own register_hydraulic_equations rebuilt + this same 2-key dict from net["_options"] independently. + """ + return {"use_numba": get_net_option(net, "use_numba"), + "friction_model": get_net_option(net, "friction_model")} + + +def get_thermal_options(net): + """``options`` dict expected by calculate_derivatives_branch_thermal/calculate_derivatives_node_thermal. + + Same rationale as get_hydraulic_options; the thermal derivatives only ever need + use_numba, not friction_model. + """ + return {"use_numba": get_net_option(net, "use_numba")} + + +def register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, df_dm_node, load_fn, load_tn): + """Register a branch's own mass flow (MDOTINIT) into both its from- and to-node balances. + + Feeds additively into both its from-node's and its to-node's mass-balance equation, + with opposite sign (mass leaving the from-node is mass entering the to-node) - this + exact block used to be duplicated near-verbatim in every branch component that calls + calculate_derivatives_hydraulic (pipe, valve, flow_control, pump, pressure_control, + heat_exchanger), plus heat_consumer's own differently-derived equivalent. + + df_dm_node is always plain ones (see calculate_derivatives_hydraulic's own df_dm_nodes, or + heat_consumer's np.ones_like(branch_idx)) - a unit of mdot change always changes a node's mass + balance by exactly that same unit - so the +1/-1 split is baked in here. load_fn/load_tn are + NOT re-signed here, unlike df_dm_node: callers must pass them already carrying whatever sign + their own upstream computation assigns (calculate_derivatives_hydraulic's callers pass + -load_fn/load_tn; heat_consumer, which derives load_fn = -MDOTINIT itself, passes load_fn/ + load_tn unchanged) - this function only assembles the (row, col, data) COO triples and + registers them, it never touches the derivative math itself. + """ + fn_eq = sys_idx.idx(HydVarEq.NODE, fn) + tn_eq = sys_idx.idx(HydVarEq.NODE, tn) + + rows_node = np.concatenate([fn_eq, tn_eq]).astype(np.int32) + cols_node = np.concatenate([mdot_col, mdot_col]).astype(np.int32) + data_node = np.concatenate([-df_dm_node, df_dm_node]).astype(np.float64) + load_rows_node = np.concatenate([fn_eq, tn_eq]).astype(np.int32) + load_node = np.concatenate([load_fn, load_tn]).astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_node, + cols=cols_node, + data=data_node, + load_rows=load_rows_node, + load_data=load_node, + )) + + +def register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, dfnt_dt, dfnt_dtout, fnt): + """Register a branch's own thermal continuity equation at its to-node. + + Every branch component's own thermal continuity equation (mixed-temperature energy + balance at its own to-node, from calculate_derivatives_branch_thermal's fnt/dfnt_dt/ + dfnt_dtout) was duplicated near-verbatim in every branch component that calls it (pipe, + valve, flow_control, pump, pressure_control, heat_exchanger, heat_consumer) - this function + only assembles the (row, col, data) COO triples and registers them, it never touches the + derivative math itself. t_tn_col/t_out_col are the to-node's TINIT column and the branch's + own TOUTINIT column respectively (callers derive them via sys_idx.idx(ThermVarEq.TINIT, tn)/ + sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx)). + """ + tn_eq = sys_idx.idx(ThermVarEq.NODE, tn) + + rows_node = np.concatenate([tn_eq, tn_eq]).astype(np.int32) + cols_node = np.concatenate([t_tn_col, t_out_col]).astype(np.int32) + data_node = np.concatenate([dfnt_dt, dfnt_dtout]).astype(np.float64) + load_rows_node = tn_eq.astype(np.int32) + load_node = fnt.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_node, + cols=cols_node, + data=data_node, + load_rows=load_rows_node, + load_data=load_node, + )) + + +def register_circ_pump_node_continuity(net, branch_pit, sys_idx, registry, table_name): + """Register a circulation pump's own branch flow into both its nodes' mass balances. + + A circulation pump's own branch (return_junction -> flow_junction) has no momentum + equation of its own - it prescribes flow rather than deriving a pressure drop from + friction (unlike calculate_derivatives_hydraulic's branch components) - so its + contribution to both nodes' mass balance is just its own MDOTINIT flowing straight + through: d(mdot)/d(mdot) == 1, load = the branch's own signed mass flow. This was + duplicated near-identically in CirculationPumpMass's and CirculationPumpPressure's own + register_hydraulic_equations before being factored out here; the actual (row, col, data) + assembly is register_branch_node_mass_balance's. + """ + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[table_name] + if f == t: + return + + branch_idx = np.arange(f, t, dtype=np.int32) + b_pit = branch_pit[f:t] + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + m = b_pit[:, IdxBranch.MDOTINIT] + dm_node = np.ones(len(branch_idx), dtype=np.float64) + + register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, dm_node, -m, m) + + +def register_circ_pump_slack_equations(net, node_pit, sys_idx, registry, table_name, + active_identifier, to_junction_col, connected_node_table): + """Register the pressure/slack-mass equations for a circulation pump's own flow junction. + + A circ pump's own flow junction gets ``NODE_TYPE = P`` purely to anchor an absolute + pressure reference (pressure is only ever defined up to a constant otherwise) - it has + no genuine external connection to freely supply/absorb mass, unlike a real ext_grid. + + So: the pressure-fix equation is always registered for its own flow junction (MEAN, + letting it coexist with ExtGrid's own pressure-fix there if a real ext_grid happens to + sit at the same node too - see ``ExtGrid.register_hydraulic_equations``). But + ``MDOTSLACKINIT`` is only forced to 0 there if there's no real ext_grid also present + (``COUNT_VAR_MASS_SLACK``, set by ``ExtGrid.register_pit_node_entries``) - where one is, + ExtGrid's own registration already lets ``MDOTSLACKINIT`` freely absorb residual mass + there, and this must not fight it for ownership of that row. + """ + tbl = net[table_name] + tbl = tbl[tbl[active_identifier].values] + if not len(tbl): + return + + p_pumps = tbl[np.isin(tbl.type.values, ["p", "pt"])] + if not len(p_pumps): + return + + # "index_active_hydraulics" (not the plain "index" lookup!) maps onto the ACTIVE/reduced + # pit this method operates on - see ExtGrid.register_hydraulic_equations for why the + # plain lookup is wrong here. -1 means disconnected - skip those. + junction_lookup = get_lookup(net, "node", "index_active_hydraulics")[connected_node_table] + # one entry per circ_pump ROW - not deduplicated, mirrors ExtGrid's own pressure-fix + pump_nodes = junction_lookup[p_pumps[to_junction_col].values].astype(np.int32) + pump_nodes = pump_nodes[pump_nodes != -1] + if not len(pump_nodes): + return + + p_col = sys_idx.idx(HydVarEq.PINIT, pump_nodes) + slack_eq = sys_idx.idx(HydVarEq.SLACK, pump_nodes) + + registry.add(ComponentEquations( + rows=slack_eq.astype(np.int32), + cols=p_col.astype(np.int32), + data=np.ones(len(slack_eq), dtype=np.float64), + load_rows=slack_eq.astype(np.int32), + load_data=np.zeros(len(slack_eq), dtype=np.float64), + mode=EqWriteMode.MEAN, + )) + + # Where a real ext_grid also sits (COUNT_VAR_MASS_SLACK != 0), ExtGrid's own registration + # already adds MDOTSLACKINIT to this node's balance - skip those nodes entirely here, + # or the coefficient would double. Deduplicated by node (unlike the pressure-fix above): + # there is exactly one shared MDOTSLACKINIT unknown per node to reset/contribute to, not + # one share per pump instance. + force_zero = np.unique(pump_nodes[node_pit[pump_nodes, IdxNode.COUNT_VAR_MASS_SLACK] == 0]) + node_pit[force_zero, IdxNode.MDOTSLACKINIT] = 0. + + n_eq = sys_idx.idx(HydVarEq.NODE, force_zero) + slack_col = sys_idx.idx(HydVarEq.MDOTSLACKINIT, force_zero) + + # plain add() (ADDITIVE, default) - joins the node's genuine balance (pipe/sink flows, + # contributed by other components), does not replace or strip it + registry.add(ComponentEquations( + rows=n_eq.astype(np.int32), + cols=slack_col.astype(np.int32), + data=np.ones(len(n_eq), dtype=np.float64), + load_rows=n_eq.astype(np.int32), + load_data=node_pit[force_zero, IdxNode.MDOTSLACKINIT].astype(np.float64), # == 0. now + )) + + def get_internal_lookup_structure(internals, table_name, internal_elements, start=0): internals[table_name] = np.empty((len(internal_elements), 2), dtype=np.int32) end = np.cumsum(internal_elements) - 1 + start @@ -25,7 +204,7 @@ def get_internal_lookup_structure(internals, table_name, internal_elements, star internals[table_name][:, 1] = end def p_correction_height_air(height): - """ + """Calculate the atmospheric pressure correction for a height using the barometric formula. :param height: :type height: @@ -37,7 +216,7 @@ def p_correction_height_air(height): def vinterp(min_vals, max_vals, lengths): - """ + """Compute linearly interpolated values between min_vals and max_vals for each range. :param min_vals: :type min_vals: @@ -55,8 +234,7 @@ def vinterp(min_vals, max_vals, lengths): def vrange(starts, lengths): - """ - Create concatenated ranges of integers for multiple start/length + """Create concatenated ranges of integers for multiple start/length. :param starts: starts for each range :type starts: numpy.array @@ -79,7 +257,7 @@ def vrange(starts, lengths): def init_results_element(net, element, output, all_float): - """ + """Initialize the results table for an element type. :param net: The pandapipes network :type net: pandapipesNet @@ -102,7 +280,7 @@ def init_results_element(net, element, output, all_float): def add_new_component(net, component, overwrite=False): - """ + """Add a new component to the net, creating its table if necessary. :param net: :type net: @@ -142,55 +320,15 @@ def set_entry_check_repeat(pit, column, entry, repeat_number, repeated=True): pit[:, column] = np.repeat(entry, repeat_number) if repeated else entry -def set_fixed_node_entries(net, node_pit, junctions, types, values, node_comp, mode): - if not len(junctions): - return [], [] - - junction_idx_lookups = get_lookup(net, "node", "index")[node_comp.table_name()] - use_numba = get_net_option(net, "use_numba") - - if mode == "p": - val_col, type_col, count_col, typ, valid_types, values = \ - PINIT, NODE_TYPE, EXT_GRID_OCCURENCE, P, ["p", "pt"], values - elif mode == "t": - val_col, type_col, count_col, typ, valid_types, values = \ - TINIT, NODE_TYPE_T, EXT_GRID_OCCURENCE_T, T, ["t", "pt"], values - else: - raise UserWarning(r'The mode %s is not supported. Choose either mode "p" or "t"' % mode) - - mask = np.isin(types, valid_types) - - juncts, val_sum, number = _sum_by_group(use_numba, junctions[mask], values[mask], - np.ones_like(values[mask], dtype=np.int32)) - - index = junction_idx_lookups[juncts] - - node_pit[index, val_col] = (node_pit[index, val_col] * node_pit[index, count_col] + val_sum) / \ - (number + node_pit[index, count_col]) - - node_pit[index, count_col] += number - node_pit[index, type_col] = typ - - return index - - -def get_mass_flow_at_nodes(net, node_pit, branch_pit, eg_nodes, comp): - node_uni, inverse_nodes, counts = np.unique(eg_nodes, return_counts=True, return_inverse=True) - eg_from_branches = np.isin(branch_pit[:, FROM_NODE], node_uni) - eg_to_branches = np.isin(branch_pit[:, TO_NODE], node_uni) - from_nodes = branch_pit[eg_from_branches, FROM_NODE] - to_nodes = branch_pit[eg_to_branches, TO_NODE] - mass_flow_from = branch_pit[eg_from_branches, LOAD_VEC_NODES_FROM] - mass_flow_to = branch_pit[eg_to_branches, LOAD_VEC_NODES_TO] - loads = node_pit[node_uni, LOAD] - all_index_nodes = np.concatenate([from_nodes, to_nodes, node_uni]) - all_mass_flows = np.concatenate([-mass_flow_from, mass_flow_to, -loads]) - nodes, sum_mass_flows = _sum_by_group(get_net_option(net, "use_numba"), all_index_nodes, - all_mass_flows) - if not np.all(nodes == node_uni): - raise UserWarning("In component %s: Something went wrong with the mass flow balance. " - "Please report this error at github." % comp.__name__) - return sum_mass_flows, inverse_nodes, counts +def build_pit_entries(rows: np.ndarray, cols: list, data: list) -> tuple: + n = len(rows) + all_rows = np.tile(rows, len(cols)) + all_cols = np.concatenate([np.full(n, c, dtype=np.int32) for c in cols]) + all_data = np.concatenate([ + np.full(n, d, dtype=np.float64) if np.isscalar(d) else np.asarray(d, dtype=np.float64) + for d in data + ]) + return all_rows, all_cols, all_data def standard_branch_wo_internals_result_lookup(net): @@ -212,8 +350,7 @@ def standard_branch_wo_internals_result_lookup(net): def get_component_array(net, component_name, component_type="branch", mode='hydraulics', only_active=True): - """ - Returns the internal array of a component. + """Returns the internal array of a component. :param net: The pandapipes network :type net: pandapipesNet @@ -229,7 +366,7 @@ def get_component_array(net, component_name, component_type="branch", mode='hydr if not only_active: return net["_pit"]["components"][component_name] f_all, t_all = get_lookup(net, component_type, "from_to")[component_name] - in_service_elm = get_lookup(net, component_type, "active_%s"%mode)[f_all:t_all] + in_service_elm = get_lookup(net, component_type, "active_" + mode)[f_all:t_all] return net["_pit"]["components"][component_name][in_service_elm] diff --git a/src/pandapipes/component_models/compressor_component.py b/src/pandapipes/component_models/compressor_component.py index 024c02fff..3193de2c8 100644 --- a/src/pandapipes/component_models/compressor_component.py +++ b/src/pandapipes/component_models/compressor_component.py @@ -8,14 +8,13 @@ from pandapipes.component_models.component_toolbox import get_component_array from pandapipes.component_models.junction_component import Junction from pandapipes.component_models.pump_component import Pump -from pandapipes.idx_branch import MDOTINIT, D, AREA, LOSS_COEFFICIENT as LC, FROM_NODE, PL -from pandapipes.idx_node import PINIT, PAMB +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode class Compressor(Pump): - """ + """Compressor component that lifts pressure by a fixed pressure ratio.""" - """ PRESSURE_RATIO = 0 internal_cols = 1 @@ -29,69 +28,31 @@ def get_connected_node_type(cls): return Junction @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. - - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - compressor_pit = super(Pump, cls).create_pit_branch_entries(net, branch_pit) - compressor_pit[:, LC] = 0 + def get_component_input(cls): + return [("name", dtype(object)), + ("from_junction", "u4"), + ("to_junction", "u4"), + ("pressure_ratio", "f8"), + ("in_service", 'bool')] @classmethod def create_component_array(cls, net, component_pits): - """ - Function which creates an internal array of the component in analogy to the pit, but with - component specific entries, that are not needed in the pit. - - :param net: The pandapipes network - :type net: pandapipesNet - :param component_pits: dictionary of component specific arrays - :type component_pits: dict - :return: - :rtype: - """ tbl = net[cls.table_name()] compr_array = np.zeros(shape=(len(tbl), cls.internal_cols), dtype=np.float64) compr_array[:, cls.PRESSURE_RATIO] = net[cls.table_name()].pressure_ratio.values component_pits[cls.table_name()] = compr_array @classmethod - def adaption_before_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - # calculation of pressure lift - f, t = idx_lookups[cls.table_name()] - compressor_branch_pit = branch_pit[f:t, :] - compressor_array = get_component_array(net, cls.table_name()) - - from_nodes = compressor_branch_pit[:, FROM_NODE].astype(np.int32) - p_from = node_pit[from_nodes, PAMB] + node_pit[from_nodes, PINIT] - - p_to_calc = p_from * compressor_array[:, cls.PRESSURE_RATIO] - pl_abs = p_to_calc - p_from - - m_mps = compressor_branch_pit[:, MDOTINIT] - pl_abs[m_mps < 0] = 0 # force pressure lift = 0 for reverse flow - - compressor_branch_pit[:, PL] = pl_abs - - @classmethod - def get_component_input(cls): - """ + def _compute_pl(cls, net, b_pit, node_pit): + """Compute pressure lift from pressure_ratio and write into b_pit[:, PL]. - Get component input. - - :return: - :rtype: + See Pump._compute_pl's docstring: compr_array is already row-aligned with b_pit through + get_component_array's own active_hydraulics filtering, so no separate index is needed. """ - return [("name", dtype(object)), - ("from_junction", "u4"), - ("to_junction", "u4"), - ("pressure_ratio", "f8"), - ("in_service", 'bool')] + compr_array = get_component_array(net, cls.table_name()) + pressure_ratio = compr_array[:, cls.PRESSURE_RATIO] + from_nodes = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + p_from = node_pit[from_nodes, IdxNode.PAMB] + node_pit[from_nodes, IdxNode.PINIT] + pl_abs = p_from * pressure_ratio - p_from + pl_abs[b_pit[:, IdxBranch.MDOTINIT] < 0] = 0.0 + b_pit[:, IdxBranch.PL] = pl_abs diff --git a/src/pandapipes/component_models/ext_grid_component.py b/src/pandapipes/component_models/ext_grid_component.py index cb081f6c7..04506679a 100644 --- a/src/pandapipes/component_models/ext_grid_component.py +++ b/src/pandapipes/component_models/ext_grid_component.py @@ -6,9 +6,10 @@ from numpy import dtype from pandapipes.component_models.abstract_models.node_element_models import NodeElementComponent -from pandapipes.component_models.component_toolbox import set_fixed_node_entries +from pandapipes.component_models.component_toolbox import build_pit_entries from pandapipes.pf.pipeflow_setup import get_lookup -from pandapipes.idx_node import MDOTSLACKINIT, VAR_MASS_SLACK, JAC_DERIV_MSL +from pandapipes.idx_node import IdxNode +from pandapipes.pf.system_index import ComponentEquations, EqWriteMode, PitEntries, PitWriteMode, HydVarEq, ThermVarEq try: import pandaplan.core.pplog as logging @@ -19,17 +20,19 @@ class ExtGrid(NodeElementComponent): - """ - - """ + """External grid component acting as the network's slack node for pressure and temperature.""" @classmethod def table_name(cls): return "ext_grid" + @classmethod + def active_identifier(cls): + return "in_service" + @classmethod def sign(cls): - return 1. + return -1. @classmethod def get_connected_node_type(cls): @@ -37,38 +40,209 @@ def get_connected_node_type(cls): return Junction @classmethod - def active_identifier(cls): - return "in_service" + def get_connected_junction(cls, net): + junction = net[cls.table_name()].junction + return junction @classmethod - def create_pit_node_entries(cls, net, node_pit): - """ - Function which creates pit node entries. + def get_node_col(cls): + return "junction" - :param net: The pandapipes network - :type net: pandapipesNet - :param node_pit: - :type node_pit: - :return: No Output. + @classmethod + def get_component_input(cls): + """Get component input. + + :return: + :rtype: """ + return [("name", dtype(object)), + ("junction", "u4"), + ("p_bar", "f8"), + ("t_k", "f8"), + ("in_service", "bool"), + ('type', dtype(object))] + + @classmethod + def register_pit_node_entries(cls, net, node_pit, registry) -> None: ext_grids = net[cls.table_name()] ext_grids = ext_grids[ext_grids[cls.active_identifier()].values] + if not len(ext_grids): + return junction = ext_grids[cls.get_node_col()].values types = ext_grids.type.values - p_values = ext_grids.p_bar.values - t_values = ext_grids.t_k.values - index_p = set_fixed_node_entries( - net, node_pit, junction, types, p_values, cls.get_connected_node_type(), 'p') - set_fixed_node_entries(net, node_pit, junction, types, t_values, cls.get_connected_node_type(), 't') - node_pit[index_p, JAC_DERIV_MSL] = -1. - node_pit[index_p, VAR_MASS_SLACK] = True - return ext_grids, p_values + junction_lookup = get_lookup(net, "node", "index")[cls.get_connected_node_type().table_name()] + mask_p = np.isin(types, ["p", "pt"]) + mask_t = np.isin(types, ["t", "pt"]) + index_p = junction_lookup[junction[mask_p]] + index_t = junction_lookup[junction[mask_t]] + + registry.add_override(PitEntries(*build_pit_entries( + index_p, + [IdxNode.PINIT, IdxNode.NODE_TYPE], + [ext_grids.p_bar.values[mask_p], float(IdxNode.P)], + ), mode=PitWriteMode.MEAN)) + registry.add_override(PitEntries(*build_pit_entries( + index_t, + [IdxNode.TINIT, IdxNode.NODE_TYPE_T], + [ext_grids.t_k.values[mask_t], float(IdxNode.T)], + ), mode=PitWriteMode.MEAN)) + # COUNT_VAR_MASS_SLACK is a "does a genuine mass slack exist at this node at all" marker, not an + # exclusively-owned value - UNIQUE would make it impossible for any other slack-capable + # component to ever ALSO mark the same node (a hard conflict error, even though both sides + # would agree on the same value). ADDITIVE lets any number of contributors coexist; the + # only reader (register_circ_pump_slack_equations) checks "== 0" / "!= 0", so an + # accumulated value like 2. from two co-located ext_grids is still read correctly as + # "yes, a real slack is here" - there's no need to clamp/OR it down to exactly 1. + registry.add(PitEntries(*build_pit_entries( + index_p, + [IdxNode.COUNT_VAR_MASS_SLACK], + [1.]), + mode=PitWriteMode.ADDITIVE)) @classmethod - def extract_results(cls, net, options, branch_results, mode): + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + # register only for nodes that actually have an active ext_grid row - NOT every P-type + # node in the system (a circ_pump also marks its own flow junction as NODE_TYPE=P purely + # to anchor a pressure reference; that node is none of ExtGrid's business - it's handled + # by register_circ_pump_slack_equations instead, using the COUNT_VAR_MASS_SLACK flag + # written below to know whether a real ext_grid also sits there). ext_grid ALWAYS + # provides genuine mass-slack capability - no COUNT_VAR_MASS_SLACK check needed on this side. + ext_grids = net[cls.table_name()] + ext_grids = ext_grids[ext_grids[cls.active_identifier()].values] + p_grids = ext_grids[np.isin(ext_grids.type.values, ["p", "pt"])] + if not len(p_grids): + return + + # "index_active_hydraulics" (not the plain "index" lookup!) maps onto the ACTIVE/reduced + # pit register_hydraulic_equations operates on here - the plain lookup is for the full + # pit, used by register_pit_node_entries before reduction; using it here would index into + # the wrong (larger) array and either crash or silently hit the wrong node. -1 means + # disconnected (dropped from the active pit) - skip those, same as register_thermal_equations. + junction_lookup = get_lookup(net, "node", "index_active_hydraulics")[ + cls.get_connected_node_type().table_name()] + # one entry per ext_grid ROW - deliberately NOT deduplicated by node (see below: multiple + # ext_grids at the same node each contribute their own additive share to MDOTSLACKINIT) + eg_nodes = junction_lookup[p_grids[cls.get_node_col()].values].astype(np.int32) + eg_nodes = eg_nodes[eg_nodes != -1] + if not len(eg_nodes): + return + + # variables - MDOTSLACKINIT/SLACK are indexed by raw node index too (like PINIT/NODE), + # no rank-within-slack_nodes translation needed (see HydraulicSystemIndex) + p_col = sys_idx.idx(HydVarEq.PINIT, eg_nodes) + slack_col = sys_idx.idx(HydVarEq.MDOTSLACKINIT, eg_nodes) + + # equation position slack + slack_eq = sys_idx.idx(HydVarEq.SLACK, eg_nodes) + + # system matrix slack: pressure fix — δPINIT = 0. Registered once per ext_grid ROW (not + # deduplicated by node) with MEAN: several ext_grids at the same junction all target the + # same row, and MEAN lets them coexist there peacefully (also with a circ_pump's own + # pressure fix, if co-located) instead of UNIQUE's exclusive-ownership conflict check. + rows_slack = slack_eq.astype(np.int32) + cols_slack = p_col.astype(np.int32) + data_slack = np.ones(len(slack_eq), dtype=np.float64) + load_rows_slack = slack_eq.astype(np.int32) + load_slack = np.zeros(len(slack_eq), dtype=np.float64) + + # equation position node + n_eq = sys_idx.idx(HydVarEq.NODE, eg_nodes) + + # system matrix node: MDOTSLACKINIT participates in mass balance - free to absorb + # whatever residual the rest of the network leaves over, exactly the point of a real + # ext_grid (unlike a circ_pump's own anchor node, see CirculationPump). Also registered + # once per ext_grid ROW (not deduplicated): N ext_grids at the same node each add their + # own +1 coefficient to that SAME row, so the row's total coefficient becomes N and + # Newton solves directly for MDOTSLACKINIT = (whatever the rest of the network leaves + # over) / N - each ext_grid's own share, with no separate averaging step needed in + # extract_results. + rows_node = n_eq.astype(np.int32) + cols_node = slack_col.astype(np.int32) + data_node = np.ones(len(n_eq), dtype=np.float64) + load_rows_node = n_eq.astype(np.int32) + load_node = node_pit[eg_nodes, IdxNode.MDOTSLACKINIT].astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_slack, + cols=cols_slack, + data=data_slack, + load_rows=load_rows_slack, + load_data=load_slack, + mode=EqWriteMode.MEAN, + )) + + registry.add(ComponentEquations( + rows=rows_node, + cols=cols_node, + data=data_node, + load_rows=load_rows_node, + load_data=load_node, + )) + + @classmethod + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + ext_grids = net[cls.table_name()] + ext_grids = ext_grids[ext_grids[cls.active_identifier()].values] + if not len(ext_grids): + return + + junction = ext_grids[cls.get_node_col()].values + types = ext_grids.type.values + mask_t = np.isin(types, ["t", "pt"]) + + junction_lookup = get_lookup(net, "node", "index_active_heat_transfer")[ + cls.get_connected_node_type().table_name() + ] + ext_nodes = junction_lookup[junction[mask_t].astype(np.int32)] + ext_nodes = ext_nodes[ext_nodes != -1] # drop disconnected, sort to match infeed_nodes order + + if not len(ext_nodes): + return + + infeed_mask = node_pit[:, IdxNode.INFEED].astype(bool) + infeed_nodes = np.where(infeed_mask)[0].astype(np.int32) + + if not len(infeed_nodes): + return + + # variables + t_col = sys_idx.idx(ThermVarEq.TINIT, ext_nodes) + + # equation position node + n_eq = sys_idx.idx(ThermVarEq.NODE, infeed_nodes) + + # system matrix node + rows_node = n_eq.astype(np.int32) + cols_node = t_col.astype(np.int32) + data_node = np.ones(len(n_eq), dtype=np.float64) + load_rows_node = n_eq.astype(np.int32) + load_node = np.zeros(len(n_eq), dtype=np.float64) + + registry.add_override(ComponentEquations( + rows=rows_node, + cols=cols_node, + data=data_node, + load_rows=load_rows_node, + load_data=load_node, + mode=EqWriteMode.MEAN, + )) + + @classmethod + def get_result_table(cls, net): + """Get the result table columns. + + :param net: The pandapipes network + :type net: pandapipesNet + :return: (columns, all_float) - the column names and whether they are all float type. Only + if False, returns columns as tuples also specifying the dtypes + :rtype: (list, bool) """ - Function that extracts certain results. + return ["mdot_kg_per_s"], True + + @classmethod + def extract_results(cls, net, options, branch_results, mode): + """Function that extracts certain results. :param branch_results: :type branch_results: @@ -87,7 +261,6 @@ def extract_results(cls, net, options, branch_results, mode): res_table = net["res_" + cls.table_name()] - branch_pit = net['_pit']['branch'] node_pit = net["_pit"]["node"] p_grids = np.isin(ext_grids.type.values, ["p", "pt"]) & ext_grids.in_service.values @@ -95,46 +268,10 @@ def extract_results(cls, net, options, branch_results, mode): # get indices in internal structure for junctions in ext_grid tables which are "active" eg_nodes = get_lookup(net, "node", "index")[cls.get_connected_node_type().table_name()][ junction[p_grids]] - node_uni, inverse_nodes, counts = np.unique(eg_nodes, return_counts=True, return_inverse=True) - sum_mass_flows = node_pit[node_uni, MDOTSLACKINIT] # positive results mean that the ext_grid feeds in, negative means that the ext grid - # extracts (like a load) - res_table["mdot_kg_per_s"].values[p_grids] = \ - cls.sign() * (sum_mass_flows / counts)[inverse_nodes] - return res_table, ext_grids, node_pit, branch_pit - - @classmethod - def get_connected_junction(cls, net): - junction = net[cls.table_name()].junction - return junction - - @classmethod - def get_node_col(cls): - return "junction" - - @classmethod - def get_component_input(cls): - """ - - :return: - :rtype: - """ - return [("name", dtype(object)), - ("junction", "u4"), - ("p_bar", "f8"), - ("t_k", "f8"), - ("in_service", "bool"), - ('type', dtype(object))] - - @classmethod - def get_result_table(cls, net): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ - return ["mdot_kg_per_s"], True + # extracts (like a load). MDOTSLACKINIT already IS this ext_grid's own share (see + # register_hydraulic_equations: N co-located ext_grids each add their own +1 coefficient + # to the same row, so Newton solves directly for the per-instance value) - no separate + # averaging needed here. + res_table["mdot_kg_per_s"].values[p_grids] = cls.sign() * node_pit[eg_nodes, IdxNode.MDOTSLACKINIT] diff --git a/src/pandapipes/component_models/flow_control_component.py b/src/pandapipes/component_models/flow_control_component.py index 2a345af06..86c70e226 100644 --- a/src/pandapipes/component_models/flow_control_component.py +++ b/src/pandapipes/component_models/flow_control_component.py @@ -6,22 +6,31 @@ from numpy import dtype from pandapipes.component_models.abstract_models import BranchWOInternalsComponent -from pandapipes.properties import get_fluid -from pandapipes.component_models.component_toolbox import \ - standard_branch_wo_internals_result_lookup, get_component_array +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, standard_branch_wo_internals_result_lookup, get_component_array, + get_hydraulic_options, get_thermal_options, register_branch_node_mass_balance, + register_branch_node_thermal_balance, +) from pandapipes.component_models.junction_component import Junction -from pandapipes.idx_branch import (JAC_DERIV_DP, JAC_DERIV_DP1, JAC_DERIV_DM, MDOTINIT, LOAD_VEC_BRANCHES, - FLOW_RETURN_CONNECT) +from pandapipes.idx_branch import IdxBranch +from pandapipes.pf.derivative_calculation import ( + calculate_derivatives_hydraulic, calculate_derivatives_branch_thermal, +) +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected +from pandapipes.pf.pipeflow_setup import get_fluid, get_lookup from pandapipes.pf.result_extraction import extract_branch_results_without_internals +from pandapipes.pf.system_index import ( + ComponentEquations, HydVarEq, PitEntries, ThermVarEq, +) class FlowControlComponent(BranchWOInternalsComponent): - """ + """Flow control component that prescribes a fixed mass flow through a branch.""" - """ CONTROL_ACTIVE = 0 + CONTROLLED_MDOT = 1 - internal_cols = 1 + internal_cols = 2 @classmethod def table_name(cls): @@ -31,62 +40,156 @@ def table_name(cls): def active_identifier(cls): return "in_service" + @classmethod + def get_connected_node_type(cls): + return Junction + @classmethod def from_to_node_cols(cls): return "from_junction", "to_junction" @classmethod - def get_connected_node_type(cls): - return Junction + def get_component_input(cls): + return [("name", dtype(object)), + ("from_junction", "u4"), + ("to_junction", "u4"), + ("controlled_mdot_kg_per_s", "f8"), + ("control_active", "bool"), + ("in_service", 'bool'), + ("type", dtype(object))] @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - fc_branch_pit = super().create_pit_branch_entries(net, branch_pit) - fc_branch_pit[:, MDOTINIT] = net[cls.table_name()].controlled_mdot_kg_per_s.values - fc_branch_pit[net[cls.table_name()].control_active, FLOW_RETURN_CONNECT] = True + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) + + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return + + rows = np.arange(f, t, dtype=np.int32) + + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxBranch.MDOTINIT], + [tbl.controlled_mdot_kg_per_s.values], + ))) + + ctrl_active = tbl.control_active.values.astype(bool) + if np.any(ctrl_active): + registry.add(PitEntries(*build_pit_entries( + rows[ctrl_active], + [IdxBranch.FLOW_RETURN_CONNECT], + [1.0], + ))) @classmethod def create_component_array(cls, net, component_pits): - """ - Function which creates an internal array of the component in analogy to the pit, but with - component specific entries, that are not needed in the pit. - - :param net: The pandapipes network - :type net: pandapipesNet - :param component_pits: dictionary of component specific arrays - :type component_pits: dict - :return: - :rtype: - """ tbl = net[cls.table_name()] fc_pit = np.zeros(shape=(len(tbl), cls.internal_cols), dtype=np.float64) fc_pit[:, cls.CONTROL_ACTIVE] = tbl.control_active.values + fc_pit[:, cls.CONTROLLED_MDOT] = tbl.controlled_mdot_kg_per_s.values component_pits[cls.table_name()] = fc_pit - @classmethod - def adaption_after_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - # set all pressure derivatives to 0 and velocity to 1; load vector must be 0, as no change - # of velocity is allowed during the pipeflow iteration - f, t = idx_lookups[cls.table_name()] - fc_branch_pit = branch_pit[f:t, :] + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + + # Compute derivatives for all active branches (writes RE, LAMBDA back to pit) + df_dm, df_dp, df_dp1, df_dm_node, load, load_fn, load_tn = ( + calculate_derivatives_hydraulic(net, branch_pit[f:t], node_pit, get_hydraulic_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + # fc_array is filtered by the same active_hydraulics mask, over the same table row + # range, as b_pit itself - so its rows are already aligned 1:1 with b_pit's rows (see + # Pump._compute_pl's docstring for the full reasoning). Indexing net[table_name] via + # IdxBranch.ELEMENT_IDX (a pandas index label) instead was a bug: .values is positional, + # so a label-based index breaks as soon as the table's index isn't a contiguous 0..n-1 + # range. fc_array = get_component_array(net, cls.table_name()) - active = fc_array[:, cls.CONTROL_ACTIVE].astype(np.bool_) - fc_branch_pit[active, JAC_DERIV_DP] = 0 - fc_branch_pit[active, JAC_DERIV_DP1] = 0 - fc_branch_pit[active, JAC_DERIV_DM] = 1 - fc_branch_pit[active, LOAD_VEC_BRANCHES] = 0 + ctrl_active = fc_array[:, cls.CONTROL_ACTIVE].astype(bool) + controlled_mdot = fc_array[:, cls.CONTROLLED_MDOT] + + # For control-active branches: prescribe mass flow (override branch equation) + df_dm[ctrl_active] = 1.0 + df_dp[ctrl_active] = 0.0 + df_dp1[ctrl_active] = 0.0 + load[ctrl_active] = b_pit[ctrl_active, IdxBranch.MDOTINIT] - controlled_mdot[ctrl_active] + # load_fn / load_tn keep their MDOTINIT values — correct for all node mass balances + + # variables + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + p_from_col = sys_idx.idx(HydVarEq.PINIT, fn) + p_to_col = sys_idx.idx(HydVarEq.PINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([mdot_col, p_from_col, p_to_col]).astype(np.int32) + data_branch = np.concatenate([df_dm, df_dp, df_dp1]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = load.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, df_dm_node, + -load_fn, load_tn) + + @classmethod + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + branch_pit_old = net["_active_old_pit"]["branch"] + fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout = ( + calculate_derivatives_branch_thermal(net, branch_pit[f:t], node_pit, branch_pit_old[f:t], + get_thermal_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = get_from_nodes_corrected(b_pit).astype(np.int32) + tn = get_to_nodes_corrected(b_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + t_from_col = sys_idx.idx(ThermVarEq.TINIT, fn) + t_tn_col = sys_idx.idx(ThermVarEq.TINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([t_from_col, t_out_col]).astype(np.int32) + data_branch = np.concatenate([dfb_dt, dfb_dtout]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = fb.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) @classmethod def extract_results(cls, net, options, branch_results, mode): @@ -95,35 +198,8 @@ def extract_results(cls, net, options, branch_results, mode): extract_branch_results_without_internals(net, branch_results, required_results_hyd, required_results_ht, cls.table_name(), mode) - @classmethod - def get_component_input(cls): - """ - - Get component input. - - :return: - :rtype: - """ - return [("name", dtype(object)), - ("from_junction", "u4"), - ("to_junction", "u4"), - ("controlled_mdot_kg_per_s", "f8"), - ("control_active", "bool"), - ("in_service", 'bool'), - ("type", dtype(object))] - @classmethod def get_result_table(cls, net): - """ - - Gets the result table. - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ if get_fluid(net).is_gas: output = ["p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", diff --git a/src/pandapipes/component_models/heat_consumer_component.py b/src/pandapipes/component_models/heat_consumer_component.py index 3c9ed5ce5..9afc3fdd9 100644 --- a/src/pandapipes/component_models/heat_consumer_component.py +++ b/src/pandapipes/component_models/heat_consumer_component.py @@ -7,14 +7,18 @@ from pandapipes.component_models import (get_fluid, BranchWOInternalsComponent, get_component_array, standard_branch_wo_internals_result_lookup) +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, get_thermal_options, register_branch_node_mass_balance, + register_branch_node_thermal_balance, +) from pandapipes.component_models.junction_component import Junction -from pandapipes.idx_branch import (MDOTINIT, QEXT, JAC_DERIV_DP1, JAC_DERIV_DM, - JAC_DERIV_DP, LOAD_VEC_BRANCHES, TOUTINIT, JAC_DERIV_DT, - JAC_DERIV_DTOUT, LOAD_VEC_BRANCHES_T, FLOW_RETURN_CONNECT) -from pandapipes.idx_node import TINIT -from pandapipes.pf.internals_toolbox import get_from_nodes_corrected +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected from pandapipes.pf.pipeflow_setup import get_lookup +from pandapipes.pf.derivative_calculation import calculate_derivatives_branch_thermal from pandapipes.pf.result_extraction import extract_branch_results_without_internals +from pandapipes.pf.system_index import ComponentEquations, EqWriteMode, HydVarEq, ThermVarEq, PitEntries from pandapipes.properties.properties_toolbox import get_branch_cp try: @@ -25,9 +29,8 @@ logger = logging.getLogger(__name__) class HeatConsumer(BranchWOInternalsComponent): - """ + """Heat consumer component that extracts heat via a prescribed qext, mass flow, temperature drop, or return temperature.""" - """ # columns for internal array MASS = 0 QEXT = 1 @@ -49,48 +52,67 @@ def table_name(cls): return "heat_consumer" @classmethod - def get_connected_node_type(cls): - return Junction + def active_identifier(cls): + return "in_service" @classmethod def from_to_node_cols(cls): return "from_junction", "to_junction" @classmethod - def active_identifier(cls): - return "in_service" + def get_connected_node_type(cls): + return Junction @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. + def get_component_input(cls): + """Get component input. + + :return: + :rtype: """ - hc_pit = super().create_pit_branch_entries(net, branch_pit) - qext = net[cls.table_name()].qext_w.values - hc_pit[~np.isnan(qext), QEXT] = qext[~np.isnan(qext)] - mdot = net[cls.table_name()].controlled_mdot_kg_per_s.values - hc_pit[~np.isnan(mdot), MDOTINIT] = mdot[~np.isnan(mdot)] - treturn = net[cls.table_name()].treturn_k.values + return [("name", dtype(object)), ("from_junction", "u4"), ("to_junction", "u4"), ("qext_w", "f8"), + ("controlled_mdot_kg_per_s", "f8"), ("deltat_k", "f8"), ("treturn_k", "f8"), + ("in_service", "bool"), ("type", dtype(object))] + + @classmethod + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) + + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return + + rows = np.arange(f, t, dtype=np.int32) + + qext = tbl.qext_w.values + mask_qext = ~np.isnan(qext) + if np.any(mask_qext): + registry.add_override(PitEntries(*build_pit_entries(rows[mask_qext], [IdxBranch.QEXT], [qext[mask_qext]]))) + + mdot = tbl.controlled_mdot_kg_per_s.values + mask_mdot = ~np.isnan(mdot) + if np.any(mask_mdot): + registry.add_override(PitEntries(*build_pit_entries(rows[mask_mdot], [IdxBranch.MDOTINIT], [mdot[mask_mdot]]))) + + treturn = tbl.treturn_k.values mask_tr = ~np.isnan(treturn) - hc_pit[mask_tr, TOUTINIT] = treturn[mask_tr] - hc_pit[:, FLOW_RETURN_CONNECT] = True - mask_q0 = qext == 0 & np.isnan(mdot) + if np.any(mask_tr): + registry.add_override(PitEntries(*build_pit_entries(rows[mask_tr], [IdxBranch.TOUTINIT], [treturn[mask_tr]]))) + + registry.add_override(PitEntries(*build_pit_entries(rows, [IdxBranch.FLOW_RETURN_CONNECT], [np.ones(len(rows))]))) + + mask_q0 = (qext == 0) & np.isnan(mdot) if np.any(mask_q0): logger.warning(r'qext_w is equals to zero for heat consumers with index %s. ' - r'Therefore, the defined temperature control cannot be maintained.' \ - %net[cls.table_name()].index[mask_q0]) - return hc_pit + r'Therefore, the defined temperature control cannot be maintained.', + tbl.index[mask_q0]) @classmethod def create_component_array(cls, net, component_pits): - """ - Function which creates an internal array of the component in analogy to the pit, but with - component specific entries, that are not needed in the pit. + """Create an internal array of the component in analogy to the pit. + + Holds component-specific entries that are not needed in the pit. :param net: The pandapipes network :type net: pandapipesNet @@ -121,126 +143,153 @@ def create_component_array(cls, net, component_pits): component_pits[cls.table_name()] = consumer_array @classmethod - def adaption_before_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - f, t = idx_lookups[cls.table_name()] - hc_pit = branch_pit[f:t, :] - consumer_array = get_component_array(net, cls.table_name()) - - mask = consumer_array[:, cls.MODE] == cls.QE_DT - if np.any(mask): - cp = get_branch_cp(get_fluid(net), node_pit, hc_pit[mask]) - deltat = consumer_array[mask, cls.DELTAT] - mass = hc_pit[mask, QEXT] / (cp * deltat) - hc_pit[mask, MDOTINIT] = mass - - @classmethod - def adaption_after_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - """ - Perform adaptions to the branch pit after the derivatives have been calculated globally. - - :param net: The pandapipes network containing all relevant info - :type net: pandapipesNet - :param branch_pit: The branch internal array - :type branch_pit: np.ndarray - :param node_pit: The node internal array - :type node_pit: np.ndarray - :param idx_lookups: Lookup for the relevant indices in the pit - :type idx_lookups: dict - :param options: Options for the pipeflow - :type options: dict - :return: No Output. - :rtype: None - """ - # set all pressure derivatives to 0 and velocity to 1; load vector must be 0, as no change - # of velocity is allowed during the pipeflow iteration - f, t = idx_lookups[cls.table_name()] - consumer_array = get_component_array(net, cls.table_name()) - - hc_pit = branch_pit[f:t, :] - hc_pit[:, JAC_DERIV_DP] = 0 - hc_pit[:, JAC_DERIV_DP1] = 0 - hc_pit[:, JAC_DERIV_DM] = 1 - hc_pit[:, LOAD_VEC_BRANCHES] = 0 - - mask = consumer_array[:, cls.MODE] == cls.QE_TR - if np.any(mask): - cp = get_branch_cp(get_fluid(net), node_pit, hc_pit) - from_nodes = get_from_nodes_corrected(hc_pit) - t_in = node_pit[from_nodes, TINIT] - t_out = hc_pit[:, TOUTINIT] - - df_dm = - cp * (t_out - t_in) + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + b_pit = branch_pit[f:t] + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + + consumer_array = get_component_array(net, cls.table_name(), mode='hydraulics') + + # variables + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # derivative and load vector branch + df_dm = np.ones_like(branch_idx, dtype=np.float64) + load = np.zeros_like(branch_idx, dtype=np.float64) + + mask_qe_dt = consumer_array[:, cls.MODE] == cls.QE_DT + if np.any(mask_qe_dt): + cp = get_branch_cp(get_fluid(net), node_pit, b_pit[mask_qe_dt]) + deltat = consumer_array[mask_qe_dt, cls.DELTAT] + mdot = b_pit[mask_qe_dt, IdxBranch.QEXT] / (cp * deltat) + load[mask_qe_dt] = - mdot + b_pit[mask_qe_dt, IdxBranch.MDOTINIT] + + mask_qe_tr = consumer_array[:, cls.MODE] == cls.QE_TR + if np.any(mask_qe_tr): + cp = get_branch_cp(get_fluid(net), node_pit, b_pit) + from_nodes = get_from_nodes_corrected(b_pit).astype(np.int32) + t_in = node_pit[from_nodes, IdxNode.TINIT] + t_out = b_pit[:, IdxBranch.TOUTINIT] + df_dm_qetr = -cp * (t_out - t_in) mask_equal = t_out >= t_in - mask_zero = hc_pit[:, QEXT] == 0 - mask_ign = mask_equal | mask_zero - hc_pit[mask & mask_ign, MDOTINIT] = 0 - hc_pit[mask & ~mask_ign, JAC_DERIV_DM] = df_dm[mask & ~mask_ign] - hc_pit[mask, LOAD_VEC_BRANCHES] = - hc_pit[mask, QEXT] + df_dm[mask] * hc_pit[mask, MDOTINIT] - - @classmethod - def adaption_before_derivatives_thermal(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - f, t = idx_lookups[cls.table_name()] - hc_pit = branch_pit[f:t, :] - consumer_array = get_component_array(net, cls.table_name(), mode='heat_transfer') - mask = consumer_array[:, cls.MODE] == cls.MF_DT - if np.any(mask): - cp = get_branch_cp(get_fluid(net), node_pit, hc_pit) - q_ext = cp[mask] * hc_pit[mask, MDOTINIT] * consumer_array[mask, cls.DELTAT] - hc_pit[mask, QEXT] = q_ext - - mask = consumer_array[:, cls.MODE] == cls.MF_TR - if np.any(mask): - cp = get_branch_cp(get_fluid(net), node_pit, hc_pit) - from_nodes = get_from_nodes_corrected(hc_pit[mask]) - t_in = node_pit[from_nodes, TINIT] - t_out = consumer_array[mask, cls.TRETURN] - q_ext = cp[mask] * hc_pit[mask, MDOTINIT] * (t_in - t_out) - hc_pit[mask, QEXT] = q_ext + mask_zero = b_pit[:, IdxBranch.QEXT] == 0 + mask_ign = mask_equal | mask_zero + + # A degenerate QE_TR consumer (t_out already >= t_in, or qext_w == 0) has no valid + # mdot = qext/(cp*(t_in-t_out)) to solve for - reset MDOTINIT to 0 in the pit itself + # (not just locally skip it) before it's read below, so a stale mass flow from a + # prior, non-degenerate iterate can't leak into this branch's own load (next line) or + # the node mass-balance load further down. + b_pit[mask_qe_tr & mask_ign, IdxBranch.MDOTINIT] = 0. + + df_dm[mask_qe_tr & ~mask_ign] = df_dm_qetr[mask_qe_tr & ~mask_ign] + load[mask_qe_tr] = (-b_pit[mask_qe_tr, IdxBranch.QEXT] + df_dm_qetr[mask_qe_tr] * b_pit[mask_qe_tr, IdxBranch.MDOTINIT]) + + # system matrix branch + rows_branch = branch_eq.astype(np.int32) + cols_branch = mdot_col.astype(np.int32) + data_branch = df_dm.astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = load.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + mode=EqWriteMode.UNIQUE, + )) + + # derivative and load vector node - no extra masking needed for degenerate QE_TR rows + # here, MDOTINIT was already reset to 0 in the pit above, so both loads read 0 there too + df_dm_node = np.ones_like(branch_idx) + load = b_pit[:, IdxBranch.MDOTINIT] + register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, df_dm_node, + -load, load) @classmethod - def adaption_after_derivatives_thermal(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - f, t = idx_lookups[cls.table_name()] - hc_pit = branch_pit[f:t, :] + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + branch_pit_old = net["_active_old_pit"]["branch"] + + b_pit = branch_pit[f:t] consumer_array = get_component_array(net, cls.table_name(), mode='heat_transfer') - mask= consumer_array[:, cls.MODE] == cls.QE_TR - if np.any(mask): - mask_ign = hc_pit[:, QEXT] == 0 - mask = mask & ~mask_ign - hc_pit[mask, LOAD_VEC_BRANCHES_T] = 0 - hc_pit[mask, JAC_DERIV_DTOUT] = 1 - hc_pit[mask, JAC_DERIV_DT] = 0 + # preparation + mask_mf_dt = consumer_array[:, cls.MODE] == cls.MF_DT + if np.any(mask_mf_dt): + cp = get_branch_cp(get_fluid(net), node_pit, b_pit) + b_pit[mask_mf_dt, IdxBranch.QEXT] = (cp[mask_mf_dt] * b_pit[mask_mf_dt, IdxBranch.MDOTINIT] + * consumer_array[mask_mf_dt, cls.DELTAT]) + + mask_mf_tr = consumer_array[:, cls.MODE] == cls.MF_TR + if np.any(mask_mf_tr): + cp = get_branch_cp(get_fluid(net), node_pit, b_pit) + fn_t = get_from_nodes_corrected(b_pit[mask_mf_tr]).astype(np.int32) + t_in = node_pit[fn_t, IdxNode.TINIT] + t_out = consumer_array[mask_mf_tr, cls.TRETURN] + b_pit[mask_mf_tr, IdxBranch.QEXT] = cp[mask_mf_tr] * b_pit[mask_mf_tr, IdxBranch.MDOTINIT] * (t_in - t_out) + + + fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout = calculate_derivatives_branch_thermal( + net, branch_pit[f:t], node_pit, branch_pit_old[f:t], get_thermal_options(net) + ) + + fn = get_from_nodes_corrected(b_pit).astype(np.int32) + tn = get_to_nodes_corrected(b_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + t_from_col = sys_idx.idx(ThermVarEq.TINIT, fn) + t_to_col = sys_idx.idx(ThermVarEq.TINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # derivative and load vector branches + mask_qe_tr = consumer_array[:, cls.MODE] == cls.QE_TR + if np.any(mask_qe_tr): + mask_ign = b_pit[:, IdxBranch.QEXT] == 0 + mask = mask_qe_tr & ~mask_ign + dfb_dt[mask] = 0 + dfb_dtout[mask] = 1 + fb[mask] = 0 + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([t_from_col, t_out_col]).astype(np.int32) + data_branch = np.concatenate([dfb_dt, dfb_dtout]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = fb.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + mode=EqWriteMode.UNIQUE, + )) + + register_branch_node_thermal_balance(sys_idx, registry, tn, t_to_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) - @classmethod - def get_component_input(cls): - """ - Get component input. - - :return: - :rtype: - """ - return [("name", dtype(object)), ("from_junction", "u4"), ("to_junction", "u4"), ("qext_w", "f8"), - ("controlled_mdot_kg_per_s", "f8"), ("deltat_k", "f8"), ("treturn_k", "f8"), - ("in_service", "bool"), ("type", dtype(object))] @classmethod def get_result_table(cls, net): - """ - - Gets the result table. + """Gets the result table. :param net: The pandapipes network :type net: pandapipesNet @@ -260,7 +309,7 @@ def get_result_table(cls, net): @classmethod def extract_results(cls, net, options, branch_results, mode): - """ + """Extract heat consumer results from the pipeflow internal structure. :param net: :type net: @@ -285,8 +334,8 @@ def extract_results(cls, net, options, branch_results, mode): res_table = net["res_" + cls.table_name()] - res_table['qext_w'].values[:] = branch_pit[f:t, QEXT] + res_table['qext_w'].values[:] = branch_pit[f:t, IdxBranch.QEXT] from_nodes = get_from_nodes_corrected(branch_pit[f:t]) - t_from = node_pit[from_nodes, TINIT] - tout = branch_pit[f:t, TOUTINIT] + t_from = node_pit[from_nodes, IdxNode.TINIT] + tout = branch_pit[f:t, IdxBranch.TOUTINIT] res_table['deltat_k'].values[:] = t_from - tout diff --git a/src/pandapipes/component_models/heat_exchanger_component.py b/src/pandapipes/component_models/heat_exchanger_component.py index 0908ccd77..2b541b959 100644 --- a/src/pandapipes/component_models/heat_exchanger_component.py +++ b/src/pandapipes/component_models/heat_exchanger_component.py @@ -1,5 +1,5 @@ # Copyright (c) 2020-2026 by Fraunhofer Institute for Energy Economics -# and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. +# and Energy System Technology (IEE), Kassel. All rights reserved. # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. import numpy as np @@ -8,10 +8,17 @@ from pandapipes.component_models import standard_branch_wo_internals_result_lookup from pandapipes.component_models.abstract_models.branch_wo_internals_models import \ BranchWOInternalsComponent +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, get_hydraulic_options, get_thermal_options, register_branch_node_mass_balance, + register_branch_node_thermal_balance, +) from pandapipes.component_models.junction_component import Junction -from pandapipes.idx_branch import QEXT, D, LOSS_COEFFICIENT as LC, DO -from pandapipes.pf.pipeflow_setup import get_fluid +from pandapipes.idx_branch import IdxBranch +from pandapipes.pf.derivative_calculation import calculate_derivatives_hydraulic, calculate_derivatives_branch_thermal +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected +from pandapipes.pf.pipeflow_setup import get_fluid, get_lookup from pandapipes.pf.result_extraction import extract_branch_results_without_internals +from pandapipes.pf.system_index import ComponentEquations, HydVarEq, PitEntries, PitWriteMode, ThermVarEq try: import pandaplan.core.pplog as logging @@ -24,10 +31,6 @@ class HeatExchanger(BranchWOInternalsComponent): - @classmethod - def from_to_node_cols(cls): - return "from_junction", "to_junction" - @classmethod def table_name(cls): return "heat_exchanger" @@ -36,32 +39,128 @@ def table_name(cls): def active_identifier(cls): return "in_service" + @classmethod + def from_to_node_cols(cls): + return "from_junction", "to_junction" + @classmethod def get_connected_node_type(cls): return Junction @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. + def get_component_input(cls): + return [("name", dtype(object)), + ("from_junction", "u4"), + ("to_junction", "u4"), + ("inner_diameter_mm", "f8"), + ("qext_w", 'f8'), + ("loss_coefficient", "f8"), + ("in_service", 'bool'), + ("type", dtype(object))] - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - heat_exchanger_pit = super().create_pit_branch_entries(net, branch_pit) - tbl = cls.table_name() - heat_exchanger_pit[:, LC] = net[tbl].loss_coefficient.values - heat_exchanger_pit[:, QEXT] = net[tbl].qext_w.values - heat_exchanger_pit[:, D] = net[tbl].inner_diameter_mm.values / 1000. - heat_exchanger_pit[:, DO] = heat_exchanger_pit[:, D] + @classmethod + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) + + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return + + rows = np.arange(f, t, dtype=np.int32) + d_vals = tbl.inner_diameter_mm.values / 1000. + + registry.add_override(PitEntries(*build_pit_entries( + rows, + [IdxBranch.QEXT, IdxBranch.D, IdxBranch.DO, IdxBranch.LOSS_COEFFICIENT], + [tbl.qext_w.values, d_vals, d_vals, tbl.loss_coefficient.values], + ), mode=PitWriteMode.UNIQUE)) + + @classmethod + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + df_dm, df_dp, df_dp1, df_dm_node, load, load_fn, load_tn = ( + calculate_derivatives_hydraulic(net, branch_pit[f:t], node_pit, get_hydraulic_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + + # variables + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + p_from_col = sys_idx.idx(HydVarEq.PINIT, fn) + p_to_col = sys_idx.idx(HydVarEq.PINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([mdot_col, p_from_col, p_to_col]).astype(np.int32) + data_branch = np.concatenate([df_dm, df_dp, df_dp1]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = load.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, df_dm_node, + -load_fn, load_tn) + + @classmethod + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + branch_pit_old = net["_active_old_pit"]["branch"] + fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout = ( + calculate_derivatives_branch_thermal(net, branch_pit[f:t], node_pit, branch_pit_old[f:t], + get_thermal_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = get_from_nodes_corrected(b_pit).astype(np.int32) + tn = get_to_nodes_corrected(b_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + t_from_col = sys_idx.idx(ThermVarEq.TINIT, fn) + t_tn_col = sys_idx.idx(ThermVarEq.TINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([t_from_col, t_out_col]).astype(np.int32) + data_branch = np.concatenate([dfb_dt, dfb_dtout]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = fb.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) @classmethod def extract_results(cls, net, options, branch_results, mode): - """ - Class method to extract pipeflow results from the internal structure into the results table. + """Class method to extract pipeflow results from the internal structure into the results table. :param net: The pandapipes network :type net: pandapipesNet @@ -79,32 +178,8 @@ def extract_results(cls, net, options, branch_results, mode): extract_branch_results_without_internals(net, branch_results, required_results_hyd, required_results_ht, cls.table_name(), mode) - @classmethod - def get_component_input(cls): - """ - - :return: - :rtype: - """ - return [("name", dtype(object)), - ("from_junction", "u4"), - ("to_junction", "u4"), - ("inner_diameter_mm", "f8"), - ("qext_w", 'f8'), - ("loss_coefficient", "f8"), - ("in_service", 'bool'), - ("type", dtype(object))] - @classmethod def get_result_table(cls, net): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ if get_fluid(net).is_gas: output = ["p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", diff --git a/src/pandapipes/component_models/junction_component.py b/src/pandapipes/component_models/junction_component.py index d864f4a39..1f65c21fc 100644 --- a/src/pandapipes/component_models/junction_component.py +++ b/src/pandapipes/component_models/junction_component.py @@ -9,28 +9,41 @@ from numpy import dtype from pandapipes.component_models.abstract_models.node_models import NodeComponent -from pandapipes.component_models.component_toolbox import p_correction_height_air -from pandapipes.idx_node import L, ELEMENT_IDX, PINIT, node_cols, HEIGHT, TINIT, PAMB, \ - ACTIVE as ACTIVE_ND, EXT_GRID_OCCURENCE, EXT_GRID_OCCURENCE_T, LOAD +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, p_correction_height_air, get_thermal_options, +) +from pandapipes.idx_node import IdxNode +from pandapipes.pf.derivative_calculation import calculate_derivatives_node_thermal +from pandapipes.pf.system_index import PitEntries, ComponentEquations, ThermVarEq from pandapipes.pf.pipeflow_setup import add_table_lookup, get_table_number, \ - get_lookup -from pandapipes.pf.pipeflow_setup import get_net_option + get_lookup, get_net_option class Junction(NodeComponent): - """ - - """ + """Junction node component.""" @classmethod def table_name(cls): return "junction" + @classmethod + def get_component_input(cls): + """Get the component input columns for this table. + + :return: + :rtype: + """ + return [('name', dtype(object)), + ('pn_bar', 'f8'), + ("tfluid_k", 'f8'), + ("height_m", 'f8'), + ('in_service', 'bool'), + ('type', dtype(object))] + @classmethod def create_node_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current_start, current_table, internals): - """ - Function which creates node lookups. + """Create node lookups. :param net: The pandapipes network :type net: pandapipesNet @@ -63,41 +76,87 @@ def create_node_lookups(cls, net, ft_lookups, table_lookup, idx_lookups, current return end, current_table + 1 @classmethod - def create_pit_node_entries(cls, net, node_pit): - """ - Function which creates pit node entries. - - :param net: The pandapipes network - :type net: pandapipesNet - :param node_pit: - :type node_pit: - :return: No Output. - """ + def register_pit_node_entries(cls, net, node_pit, registry) -> None: ft_lookup = get_lookup(net, "node", "from_to") table_nr = get_table_number(get_lookup(net, "node", "table"), cls.table_name()) f, t = ft_lookup[cls.table_name()] - junctions = net[cls.table_name()] - junction_pit = node_pit[f:t, :] + rows = np.arange(f, t, dtype=np.int32) if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - junction_pit[:, :] = np.array([table_nr, 0, L] + [0] * (node_cols - 3)) - junction_pit[:, ELEMENT_IDX] = junctions.index.values - junction_pit[:, HEIGHT] = junctions.height_m.values - junction_pit[:, PAMB] = p_correction_height_air(junction_pit[:, HEIGHT]) - junction_pit[:, ACTIVE_ND] = junctions.in_service.values + height_vals = junctions.height_m.values + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxNode.TABLE_IDX, IdxNode.ELEMENT_IDX, IdxNode.NODE_TYPE, IdxNode.HEIGHT, IdxNode.PAMB, IdxNode.ACTIVE, IdxNode.TINIT, IdxNode.PINIT], + [float(table_nr), junctions.index.values.astype(float), float(IdxNode.L), + height_vals, p_correction_height_air(height_vals), + junctions.in_service.values.astype(float), + junctions.tfluid_k.values, junctions.pn_bar.values], + ))) else: - junction_pit[:, EXT_GRID_OCCURENCE] = 0 - junction_pit[:, EXT_GRID_OCCURENCE_T] = 0 - junction_pit[:, LOAD] = 0 + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxNode.TINIT, IdxNode.PINIT], + [junctions.tfluid_k.values, junctions.pn_bar.values], + ))) - junction_pit[:, TINIT] = junctions.tfluid_k.values - junction_pit[:, PINIT] = junctions.pn_bar.values + @classmethod + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + node_pit_old = net["_active_old_pit"]["node"] + + fn_node, dfn_dt = calculate_derivatives_node_thermal( + net, branch_pit, node_pit, node_pit_old, get_thermal_options(net) + ) + + stagnant = np.where(dfn_dt != 0)[0].astype(np.int32) + if not len(stagnant): + return + + # variables + t_n_col = sys_idx.idx(ThermVarEq.TINIT, stagnant) + + # equation position node + n_eq = sys_idx.idx(ThermVarEq.NODE, stagnant) + + # system matrix node + rows_node = n_eq.astype(np.int32) + cols_node = t_n_col.astype(np.int32) + data_node = dfn_dt[stagnant].astype(np.float64) + load_rows_node = n_eq.astype(np.int32) + load_node = fn_node[stagnant].astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_node, + cols=cols_node, + data=data_node, + load_rows=load_rows_node, + load_data=load_node, + )) @classmethod - def extract_results(cls, net, options, branch_results, mode): + def geodata(cls): + """Get geodata columns. + + :return: + :rtype: """ - Function that extracts certain results. + return [("x", "f8"), ("y", "f8")] + + @classmethod + def get_result_table(cls, net): + """Get the result table columns. + + :param net: The pandapipes network + :type net: pandapipesNet + :return: (columns, all_float) - the column names and whether they are all float type. Only + if False, returns columns as tuples also specifying the dtypes + :rtype: (list, bool) + """ + return ["p_bar", "t_k"], True + + @classmethod + def extract_results(cls, net, options, branch_results, mode): + """Extract certain results. :param mode: :type mode: @@ -118,10 +177,10 @@ def extract_results(cls, net, options, branch_results, mode): # TODO: This must be made more precise in different components net["res_internal"] = pd.DataFrame( np.nan, columns=["t_k"], index=np.arange(len(net["_active_pit"]["node"][:, - TINIT])), + IdxNode.TINIT])), dtype=np.float64 ) - net["res_internal"]["t_k"] = net["_active_pit"]["node"][:, TINIT] + net["res_internal"]["t_k"] = net["_active_pit"]["node"][:, IdxNode.TINIT] f, t = get_lookup(net, "node", "from_to")[cls.table_name()] junction_pit = net["_pit"]["node"][f:t, :] @@ -129,52 +188,10 @@ def extract_results(cls, net, options, branch_results, mode): if mode in ["hydraulics", "sequential", "bidirectional"]: junctions_connected_hydraulic = get_lookup(net, "node", "active_hydraulics")[f:t] - if np.any(junction_pit[junctions_connected_hydraulic, PINIT] < 0): + if np.any(junction_pit[junctions_connected_hydraulic, IdxNode.PINIT] < 0): warn(UserWarning('Pipeflow converged, however, the results are physically incorrect ' 'as pressure is negative at nodes %s' - % junction_pit[junction_pit[:, PINIT] < 0, ELEMENT_IDX])) - - # res_table["p_bar"].values[junctions_connected_hydraulic] = junction_pit[:, PINIT] - # if mode == "hydraulics": - # res_table["t_k"].values[junctions_connected_hydraulic] = junction_pit[:, TINIT] - # - # if mode in ["heat", "sequential", "bidirectional]: - # junctions_connected_ht = get_lookup(net, "node", "active_heat_transfer")[f:t] - # res_table["t_k"].values[junctions_connected_ht] = junction_pit[:, TINIT] - res_table["p_bar"].values[:] = junction_pit[:, PINIT] - res_table["t_k"].values[:] = junction_pit[:, TINIT] - - @classmethod - def get_component_input(cls): - """ - - :return: - :rtype: - """ - return [('name', dtype(object)), - ('pn_bar', 'f8'), - ("tfluid_k", 'f8'), - ("height_m", 'f8'), - ('in_service', 'bool'), - ('type', dtype(object))] - - @classmethod - def geodata(cls): - """ - - :return: - :rtype: - """ - return [("x", "f8"), ("y", "f8")] + % junction_pit[junction_pit[:, IdxNode.PINIT] < 0, IdxNode.ELEMENT_IDX])) - @classmethod - def get_result_table(cls, net): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ - return ["p_bar", "t_k"], True + res_table["p_bar"].values[:] = junction_pit[:, IdxNode.PINIT] + res_table["t_k"].values[:] = junction_pit[:, IdxNode.TINIT] diff --git a/src/pandapipes/component_models/mass_storage_component.py b/src/pandapipes/component_models/mass_storage_component.py index a4566eb24..c2f53e7d1 100644 --- a/src/pandapipes/component_models/mass_storage_component.py +++ b/src/pandapipes/component_models/mass_storage_component.py @@ -7,9 +7,7 @@ class MassStorage(ConstFlow): - """ - - """ + """Mass storage component that stores or releases mass at a junction.""" @classmethod def table_name(cls): @@ -19,9 +17,13 @@ def table_name(cls): def sign(cls): return 1 + @classmethod + def get_connected_node_type(cls): + return Junction + @classmethod def get_component_input(cls): - """ + """Get the component input columns for this table. :return: :rtype: @@ -48,7 +50,3 @@ def get_result_table(cls, net): :rtype: (list, bool) """ return ["mdot_kg_per_s"], True - - @classmethod - def get_connected_node_type(cls): - return Junction diff --git a/src/pandapipes/component_models/pipe_component.py b/src/pandapipes/component_models/pipe_component.py index 77a88697d..49d1d61a5 100644 --- a/src/pandapipes/component_models/pipe_component.py +++ b/src/pandapipes/component_models/pipe_component.py @@ -7,14 +7,20 @@ from numpy import dtype from pandapipes.component_models.abstract_models import BranchWInternalsComponent -from pandapipes.component_models.component_toolbox import set_entry_check_repeat, vinterp, p_correction_height_air +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, vinterp, p_correction_height_air, get_hydraulic_options, get_thermal_options, + register_branch_node_mass_balance, register_branch_node_thermal_balance, +) from pandapipes.component_models.junction_component import Junction from pandapipes.constants import NORMAL_TEMPERATURE, NORMAL_PRESSURE -from pandapipes.idx_branch import FROM_NODE, TO_NODE, LENGTH, D, AREA, K, MDOTINIT, ALPHA, TEXT, TOUTINIT -from pandapipes.idx_node import TINIT as TINIT_NODE, HEIGHT, PINIT, PAMB, ACTIVE as ACTIVE_ND -from pandapipes.pf.pipeflow_setup import get_fluid, get_lookup, get_net_option +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.derivative_calculation import calculate_derivatives_hydraulic, calculate_derivatives_branch_thermal +from pandapipes.pf.internals_toolbox import branch_area, get_from_nodes_corrected, get_to_nodes_corrected +from pandapipes.pf.pipeflow_setup import get_fluid, get_lookup, get_net_option, get_table_number from pandapipes.pf.result_extraction import extract_branch_results_with_internals, \ extract_branch_results_without_internals +from pandapipes.pf.system_index import ComponentEquations, BaseSystemIndex, HydVarEq, PitEntries, ThermVarEq try: import pandaplan.core.pplog as logging @@ -26,22 +32,12 @@ class Pipe(BranchWInternalsComponent): - """ - - """ - - @classmethod - def from_to_node_cols(cls): - return "from_junction", "to_junction" + """Pipe branch component with internal sections.""" @classmethod def table_name(cls): return "pipe" - @classmethod - def internal_node_name(cls): - return "pipe_nodes" - @classmethod def active_identifier(cls): return "in_service" @@ -50,9 +46,17 @@ def active_identifier(cls): def get_connected_node_type(cls): return Junction + @classmethod + def from_to_node_cols(cls): + return "from_junction", "to_junction" + + @classmethod + def internal_node_name(cls): + return "pipe_nodes" + @classmethod def get_internal_node_number(cls, net, return_internal_only=True): - """ + """Get the number of internal nodes per pipe. :param net: The pandapipes network :type net: pandapipesNet @@ -63,7 +67,7 @@ def get_internal_node_number(cls, net, return_internal_only=True): @classmethod def get_internal_branch_number(cls, net): - """ + """Get the number of internal branches (sections) per pipe. :param net: The pandapipes network :type net: pandapipesNet @@ -73,53 +77,69 @@ def get_internal_branch_number(cls, net): return np.array(net[cls.table_name()].sections.values).astype(np.int32) @classmethod - def create_pit_node_entries(cls, net, node_pit): - int_node_pit = super().create_pit_node_entries(net, node_pit) - if int_node_pit is not None: - int_node_number = cls.get_internal_node_number(net) - ft_lookup = get_lookup(net, "node", "from_to") - junction_table_name = cls.get_connected_node_type().table_name() - fj_name, tj_name = cls.from_to_node_cols() - f_junction, t_junction = ft_lookup[junction_table_name] - junction_pit = node_pit[f_junction:t_junction, :] - from_junctions = net[cls.table_name()][fj_name].values.astype(np.int32) - to_junctions = net[cls.table_name()][tj_name].values.astype(np.int32) - junction_indices = get_lookup(net, "node", "index")[junction_table_name] - fj_nodes = junction_indices[from_junctions] - tj_nodes = junction_indices[to_junctions] - int_node_pit[:, TINIT_NODE] = vinterp(junction_pit[fj_nodes, TINIT_NODE], - junction_pit[tj_nodes, TINIT_NODE], int_node_number) - int_node_pit[:, PINIT] = vinterp(junction_pit[fj_nodes, PINIT], junction_pit[tj_nodes, PINIT], - int_node_number) - if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - int_node_pit[:, HEIGHT] = vinterp(junction_pit[fj_nodes, HEIGHT], junction_pit[tj_nodes, HEIGHT], - int_node_number) - int_node_pit[:, PAMB] = p_correction_height_air(int_node_pit[:, HEIGHT]) - int_node_pit[:, ACTIVE_ND] = np.repeat(net[cls.table_name()][cls.active_identifier()].values, - int_node_number) + def get_component_input(cls): + """Get the component input columns for this table. + :return: + :rtype: + """ + return [("name", dtype(object)), ("from_junction", "u4"), ("to_junction", "u4"), ("std_type", dtype(object)), + ("length_km", "f8"), ("inner_diameter_mm", "f8"), ("outer_diameter_mm", "f8"), + ("k_mm", "f8"), ("loss_coefficient", "f8"), + ("u_w_per_m2k", 'f8'), ("text_k", 'f8'), ("sections", "u4"), ("in_service", 'bool'), + ("type", dtype(object))] @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries. + def register_pit_node_entries(cls, net, node_pit, registry) -> None: + super().register_pit_node_entries(net, node_pit, registry) + + table_lookup = get_lookup(net, "node", "table") + if get_table_number(table_lookup, cls.internal_node_name()) is None: + return + + ft_lookup = get_lookup(net, "node", "from_to") + f, t = ft_lookup[cls.internal_node_name()] + int_node_number = cls.get_internal_node_number(net) + junction_table_name = cls.get_connected_node_type().table_name() + fj_name, tj_name = cls.from_to_node_cols() + from_junctions = net[cls.table_name()][fj_name].values.astype(np.int32) + to_junctions = net[cls.table_name()][tj_name].values.astype(np.int32) + junction_table = net[junction_table_name] + + rows = np.arange(f, t, dtype=np.int32) + tinit_vals = vinterp(junction_table.loc[from_junctions, "tfluid_k"].values, + junction_table.loc[to_junctions, "tfluid_k"].values, int_node_number) + pinit_vals = vinterp(junction_table.loc[from_junctions, "pn_bar"].values, + junction_table.loc[to_junctions, "pn_bar"].values, int_node_number) - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ + if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: + height_vals = vinterp(junction_table.loc[from_junctions, "height_m"].values, + junction_table.loc[to_junctions, "height_m"].values, int_node_number) + pamb_vals = p_correction_height_air(height_vals) + active_vals = np.repeat(net[cls.table_name()][cls.active_identifier()].values, int_node_number).astype(float) + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxNode.TINIT, IdxNode.PINIT, IdxNode.HEIGHT, IdxNode.PAMB, IdxNode.ACTIVE], + [tinit_vals, pinit_vals, height_vals, pamb_vals, active_vals], + ))) + else: + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxNode.TINIT, IdxNode.PINIT], + [tinit_vals, pinit_vals]))) - pipe_pit, node_pit = super().create_pit_branch_entries(net, branch_pit) - junction_idx_lookup = get_lookup(net, "node", "index")[ - cls.get_connected_node_type().table_name()] - fn_col, tn_col = cls.from_to_node_cols() + @classmethod + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + junction_idx_lookup = get_lookup(net, "node", "index")[cls.get_connected_node_type().table_name()] + fn_col, tn_col = cls.from_to_node_cols() from_nodes = junction_idx_lookup[net[cls.table_name()][fn_col].values] to_nodes = junction_idx_lookup[net[cls.table_name()][tn_col].values] internal_pipe_number = cls.get_internal_branch_number(net) has_internals = np.any(internal_pipe_number > 1) + if has_internals: internal_node_number = cls.get_internal_node_number(net) node_ft_lookups = get_lookup(net, "node", "from_to") @@ -128,22 +148,153 @@ def create_pit_branch_entries(cls, net, branch_pit): insert_places = np.repeat(np.arange(len(from_nodes)), internal_node_number) from_nodes = np.insert(from_nodes, insert_places + 1, pipe_nodes_idx) to_nodes = np.insert(to_nodes, insert_places, pipe_nodes_idx) + + rows = np.arange(f, t, dtype=np.int32) + tbl = cls.table_name() + junction_table_name = cls.get_connected_node_type().table_name() + + def _rep(vals): + return np.repeat(vals, internal_pipe_number) if has_internals else vals + + to_junctions_br = net[tbl][tn_col].values + toutinit_vals = _rep(net[junction_table_name].loc[to_junctions_br, "tfluid_k"].values) + d_vals = _rep(net[tbl].inner_diameter_mm.values / 1000.) + area_vals = d_vals ** 2 * np.pi / 4 + mdotinit_vals = 0.1 * area_vals * get_fluid(net).get_density(NORMAL_TEMPERATURE) + if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - pipe_pit[:, FROM_NODE] = from_nodes - pipe_pit[:, TO_NODE] = to_nodes - - tbl = cls.table_name() - set_entry_check_repeat(pipe_pit, LENGTH, net[tbl].length_km.values * 1000 / internal_pipe_number, - internal_pipe_number, has_internals) - set_entry_check_repeat(pipe_pit, K, net[tbl].k_mm.values / 1000, internal_pipe_number, has_internals) - set_entry_check_repeat(pipe_pit, ALPHA, net[tbl].u_w_per_m2k.values, internal_pipe_number, has_internals) - set_entry_check_repeat(pipe_pit, TEXT, net[tbl].text_k.values, internal_pipe_number, has_internals) - nan_mask = np.isnan(pipe_pit[:, TEXT]) - pipe_pit[nan_mask, TEXT] = get_net_option(net, 'ambient_temperature') - pipe_pit[:, AREA] = pipe_pit[:, D] ** 2 * np.pi / 4 - - pipe_pit[:, TOUTINIT] = node_pit[to_nodes, TINIT_NODE] - pipe_pit[:, MDOTINIT] *= pipe_pit[:, AREA] * get_fluid(net).get_density(NORMAL_TEMPERATURE) + length_vals = _rep(net[tbl].length_km.values * 1000 / internal_pipe_number) + k_vals = _rep(net[tbl].k_mm.values / 1000) + alpha_vals = _rep(net[tbl].u_w_per_m2k.values) + text_vals = _rep(net[tbl].text_k.values) + text_vals[np.isnan(text_vals)] = get_net_option(net, 'ambient_temperature') + + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxBranch.FROM_NODE, IdxBranch.TO_NODE, IdxBranch.LENGTH, IdxBranch.K, IdxBranch.ALPHA, IdxBranch.TEXT, IdxBranch.TOUTINIT], + [from_nodes.astype(float), to_nodes.astype(float), + length_vals, k_vals, alpha_vals, text_vals, toutinit_vals], + ))) + else: + registry.add(PitEntries(*build_pit_entries( + rows, [IdxBranch.TOUTINIT], [toutinit_vals], + ))) + + registry.add(PitEntries(*build_pit_entries( + rows, [IdxBranch.MDOTINIT], [mdotinit_vals], + ))) + + @classmethod + def register_hydraulic_equations(cls, net, branch_pit, node_pit, + sys_idx: BaseSystemIndex, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + df_dm, df_dp, df_dp1, df_dm_node, load, load_fn, load_tn = ( + calculate_derivatives_hydraulic(net, branch_pit[f:t], node_pit, get_hydraulic_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + + # variables + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + p_from_col = sys_idx.idx(HydVarEq.PINIT, fn) + p_to_col = sys_idx.idx(HydVarEq.PINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([mdot_col, p_from_col, p_to_col]).astype(np.int32) + data_branch = np.concatenate([df_dm, df_dp, df_dp1]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = load.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, df_dm_node, + -load_fn, load_tn) + + @classmethod + def register_thermal_equations(cls, net, branch_pit, node_pit, + sys_idx: BaseSystemIndex, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + branch_pit_old = net["_active_old_pit"]["branch"] + fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout = ( + calculate_derivatives_branch_thermal(net, branch_pit[f:t], node_pit, branch_pit_old[f:t], + get_thermal_options(net)) + ) + + pipe_pit = branch_pit[f:t] + fn = get_from_nodes_corrected(pipe_pit).astype(np.int32) + tn = get_to_nodes_corrected(pipe_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + t_from_col = sys_idx.idx(ThermVarEq.TINIT, fn) + t_tn_col = sys_idx.idx(ThermVarEq.TINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([t_from_col, t_out_col]).astype(np.int32) + data_branch = np.concatenate([dfb_dt, dfb_dtout]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = fb.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) + + @classmethod + def geodata(cls): + """Get geodata columns. + + :return: + :rtype: + """ + return [("coords", dtype(object))] + + @classmethod + def get_result_table(cls, net): + """Get the result table columns. + + :param net: The pandapipes network + :type net: pandapipesNet + :return: (columns, all_float) - the column names and whether they are all float type. Only + if False, returns columns as tuples also specifying the dtypes + :rtype: (list, bool) + """ + if get_fluid(net).is_gas: + output = ["v_from_m_per_s", "v_to_m_per_s", "v_mean_m_per_s", "p_from_bar", "p_to_bar", "t_from_k", + "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_norm_m3_per_s", + "reynolds", "lambda", "normfactor_from", "normfactor_to", "dp_friction_loss_bar"] + else: + output = ["v_mean_m_per_s", "p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", + "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_m3_per_s", "reynolds", "lambda", "dp_friction_loss_bar"] + return output, True @classmethod def extract_results(cls, net, options, branch_results, mode): @@ -173,9 +324,9 @@ def extract_results(cls, net, options, branch_results, mode): @classmethod def get_internal_results(cls, net, pipe): - """ - Retrieve velocity (at to/from node; mean), pressure and temperature of the internal sections - of pipes. The pipes have to have at least 2 internal sections. + """Retrieve velocity (at to/from node; mean), pressure and temperature of the internal sections of pipes. + + The pipes have to have at least 2 internal sections. :param net: The pandapipes network :type net: pandapipesNet @@ -210,26 +361,27 @@ def get_internal_results(cls, net, pipe): m_nodes = int_v_lookup[pipe_lookup_index] m_nodes = [np.arange(x, y + 1) for x,y in zip(m_nodes[:, 0], m_nodes[:, 1])] - v_pipe_data = pipe_pit[m_nodes, MDOTINIT] / fluid.get_density(NORMAL_TEMPERATURE) / pipe_pit[m_nodes, AREA] - p_node_data = node_pit[p_nodes, PINIT] - t_node_data = node_pit[p_nodes, TINIT_NODE] + v_pipe_data = pipe_pit[m_nodes, IdxBranch.MDOTINIT] / fluid.get_density(NORMAL_TEMPERATURE) / ( + branch_area(pipe_pit)[m_nodes]) + p_node_data = node_pit[p_nodes, IdxNode.PINIT] + t_node_data = node_pit[p_nodes, IdxNode.TINIT] gas_mode = fluid.is_gas if gas_mode: - from_nodes = pipe_pit[m_nodes, FROM_NODE].astype(np.int32) - to_nodes = pipe_pit[m_nodes, TO_NODE].astype(np.int32) - p_from = node_pit[from_nodes, PAMB] + node_pit[from_nodes, PINIT] - p_to = node_pit[to_nodes, PAMB] + node_pit[to_nodes, PINIT] + from_nodes = pipe_pit[m_nodes, IdxBranch.FROM_NODE].astype(np.int32) + to_nodes = pipe_pit[m_nodes, IdxBranch.TO_NODE].astype(np.int32) + p_from = node_pit[from_nodes, IdxNode.PAMB] + node_pit[from_nodes, IdxNode.PINIT] + p_to = node_pit[to_nodes, IdxNode.PAMB] + node_pit[to_nodes, IdxNode.PINIT] p_mean = np.where(p_from == p_to, p_from, 2 / 3 * (p_from ** 3 - p_to ** 3) / (p_from ** 2 - p_to ** 2)) - factor = NORMAL_PRESSURE * node_pit[m_nodes, TINIT_NODE] / NORMAL_TEMPERATURE + factor = NORMAL_PRESSURE * node_pit[m_nodes, IdxNode.TINIT] / NORMAL_TEMPERATURE args_from, args_to, args_mean = [p_from], [p_to], [p_mean] if (hasattr(fluid.all_properties["compressibility"], "allow_2d") and fluid.all_properties["compressibility"].allow_2d): # TODO: this is only allowed without temperature calculation (assumed for gases) - t_from = node_pit[from_nodes, TINIT_NODE] - t_to = node_pit[to_nodes, TINIT_NODE] + t_from = node_pit[from_nodes, IdxNode.TINIT] + t_to = node_pit[to_nodes, IdxNode.TINIT] args_from.append(t_from) args_to.append(t_to) args_mean.append((t_from + t_to) / 2) @@ -266,50 +418,9 @@ def get_internal_results(cls, net, pipe): return pipe_results - @classmethod - def get_component_input(cls): - """ - - :return: - :rtype: - """ - return [("name", dtype(object)), ("from_junction", "u4"), ("to_junction", "u4"), ("std_type", dtype(object)), - ("length_km", "f8"), ("inner_diameter_mm", "f8"), ("outer_diameter_mm", "f8"), - ("k_mm", "f8"), ("loss_coefficient", "f8"), - ("u_w_per_m2k", 'f8'), ("text_k", 'f8'), ("sections", "u4"), ("in_service", 'bool'), - ("type", dtype(object))] - - @classmethod - def geodata(cls): - """ - - :return: - :rtype: - """ - return [("coords", dtype(object))] - - @classmethod - def get_result_table(cls, net): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ - if get_fluid(net).is_gas: - output = ["v_from_m_per_s", "v_to_m_per_s", "v_mean_m_per_s", "p_from_bar", "p_to_bar", "t_from_k", - "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_norm_m3_per_s", - "reynolds", "lambda", "normfactor_from", "normfactor_to", "dp_friction_loss_bar"] - else: - output = ["v_mean_m_per_s", "p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", - "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_m3_per_s", "reynolds", "lambda", "dp_friction_loss_bar"] - return output, True - @classmethod def plot_pipe(cls, net, pipe, pipe_results): - """ + """Plot pressure, velocity and temperature profiles along a pipe. :param net: The pandapipes network :type net: pandapipesNet @@ -330,14 +441,14 @@ def plot_pipe(cls, net, pipe, pipe_results): from_junction_nodes = junction_idx_lookup[net[cls.table_name()]["from_junction"].values] to_junction_nodes = junction_idx_lookup[net[cls.table_name()]["to_junction"].values] p_values = np.zeros(len(pipe_p_data[0]) + 2) - p_values[0] = node_pit[from_junction_nodes[pipe], PINIT] + p_values[0] = node_pit[from_junction_nodes[pipe], IdxNode.PINIT] p_values[1:-1] = pipe_p_data[:] - p_values[-1] = node_pit[to_junction_nodes[pipe], PINIT] + p_values[-1] = node_pit[to_junction_nodes[pipe], IdxNode.PINIT] t_values = np.zeros(len(pipe_t_data[0]) + 2) - t_values[0] = node_pit[from_junction_nodes[pipe], TINIT_NODE] + t_values[0] = node_pit[from_junction_nodes[pipe], IdxNode.TINIT] t_values[1:-1] = pipe_t_data[:] - t_values[-1] = node_pit[to_junction_nodes[pipe], TINIT_NODE] + t_values[-1] = node_pit[to_junction_nodes[pipe], IdxNode.TINIT] v_values = pipe_v_data[0, :] diff --git a/src/pandapipes/component_models/pressure_control_component.py b/src/pandapipes/component_models/pressure_control_component.py index f0b4c7418..83c76b7d7 100644 --- a/src/pandapipes/component_models/pressure_control_component.py +++ b/src/pandapipes/component_models/pressure_control_component.py @@ -7,26 +7,35 @@ from pandapipes.component_models.abstract_models.branch_wo_internals_models import \ BranchWOInternalsComponent -from pandapipes.component_models.component_toolbox import get_component_array -from pandapipes.component_models import standard_branch_wo_internals_result_lookup +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, standard_branch_wo_internals_result_lookup, get_component_array, + get_hydraulic_options, get_thermal_options, register_branch_node_mass_balance, + register_branch_node_thermal_balance, +) from pandapipes.component_models.junction_component import Junction -from pandapipes.idx_branch import DIRECTED, \ - JAC_DERIV_DP, JAC_DERIV_DP1, JAC_DERIV_DM, BRANCH_TYPE, LOSS_COEFFICIENT as LC, PC as PC_BRANCH -from pandapipes.idx_node import PINIT, NODE_TYPE, PC as PC_NODE +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.derivative_calculation import ( + calculate_derivatives_hydraulic, calculate_derivatives_branch_thermal, +) +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected from pandapipes.pf.pipeflow_setup import get_lookup from pandapipes.pf.result_extraction import extract_branch_results_without_internals +from pandapipes.pf.system_index import ( + ComponentEquations, HydVarEq, PitEntries, PitWriteMode, ThermVarEq, +) from pandapipes.properties.fluids import get_fluid class PressureControlComponent(BranchWOInternalsComponent): - """ + """Pressure control component enforcing a target pressure at a controlled junction.""" - """ JUNCTS = 0 IN_SERVICE = 1 CONTROLLED = 2 + CONTROLLED_P = 3 - internal_cols = 3 + internal_cols = 4 @classmethod def table_name(cls): @@ -36,110 +45,198 @@ def table_name(cls): def active_identifier(cls): return "in_service" + @classmethod + def get_connected_node_type(cls): + return Junction + @classmethod def from_to_node_cols(cls): return "from_junction", "to_junction" @classmethod - def get_connected_node_type(cls): - return Junction + def get_component_input(cls): + return [("name", dtype(object)), + ("from_junction", "u4"), + ("to_junction", "u4"), + ("controlled_junction", "u4"), + ("controlled_p_bar", "f8"), + ("control_active", "bool"), + ("loss_coefficient", "f8"), + ("in_service", 'bool'), + ("type", dtype(object))] + + @classmethod + def register_pit_node_entries(cls, net, node_pit, registry) -> None: + pcs = net[cls.table_name()] + controlled = pcs.control_active.values & pcs.in_service.values + if not np.any(controlled): + return + juncts = pcs['controlled_junction'].values[controlled] + press = pcs['controlled_p_bar'].values[controlled] + junction_idx_lookup = get_lookup(net, "node", "index")[ + cls.get_connected_node_type().table_name() + ] + index_pc = junction_idx_lookup[juncts] + registry.add_override(PitEntries( + index_pc.astype(np.int32), + np.full(len(index_pc), IdxNode.PINIT, dtype=np.int32), + press.astype(np.float64), + mode=PitWriteMode.MEAN, + )) + + @classmethod + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) + + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return + + rows = np.arange(f, t, dtype=np.int32) + registry.add(PitEntries(*build_pit_entries( + rows, [IdxBranch.LOSS_COEFFICIENT, IdxBranch.DIRECTED], [tbl.loss_coefficient.values, True], + ))) @classmethod def create_component_array(cls, net, component_pits): - """ - Function which creates an internal array of the component in analogy to the pit, but with - component specific entries, that are not needed in the pit. - - :param net: The pandapipes network - :type net: pandapipesNet - :param component_pits: dictionary of component specific arrays - :type component_pits: dict - :return: - :rtype: - """ tbl = net[cls.table_name()] pc_array = np.zeros(shape=(len(tbl), cls.internal_cols), dtype=np.float64) pc_array[:, cls.JUNCTS] = tbl["controlled_junction"].values pc_array[:, cls.CONTROLLED] = tbl.control_active.values pc_array[:, cls.IN_SERVICE] = tbl.in_service.values + pc_array[:, cls.CONTROLLED_P] = tbl.controlled_p_bar.values component_pits[cls.table_name()] = pc_array @classmethod - def create_pit_node_entries(cls, net, node_pit): - pcs = net[cls.table_name()] - controlled = pcs.control_active.values & pcs.in_service.values - juncts = pcs['controlled_junction'].values[controlled] - press = pcs['controlled_p_bar'].values[controlled] - junction_idx_lookups = get_lookup(net, "node", "index")[ - cls.get_connected_node_type().table_name()] - index_pc = junction_idx_lookups[juncts] - node_pit[index_pc, PINIT] = press - - @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - pc_pit = super().create_pit_branch_entries(net, branch_pit) - pc_pit[net[cls.table_name()].control_active.values, BRANCH_TYPE] = PC_BRANCH - pc_pit[:, LC] = net[cls.table_name()].loss_coefficient.values - pc_pit[:, DIRECTED] = True - + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return - @classmethod - def adaption_before_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): + b_pit = branch_pit[f:t] + # pc_array is filtered by the same active_hydraulics mask, over the same table row + # range, as b_pit itself - so its rows are already aligned 1:1 with b_pit's rows (see + # Pump._compute_pl's docstring for the full reasoning). Indexing net[table_name] via + # IdxBranch.ELEMENT_IDX (a pandas index label) instead was a bug: .values is positional, + # so a label-based index breaks as soon as the table's index isn't a contiguous 0..n-1 + # range. pc_array = get_component_array(net, cls.table_name()) - junction_idx_lookups_active = get_lookup(net, "node", "index_active_hydraulics")[ + ctrl_active = pc_array[:, cls.CONTROLLED].astype(bool) + in_service_arr = pc_array[:, cls.IN_SERVICE].astype(bool) + ctrl_juncts = pc_array[:, cls.JUNCTS].astype(np.int32) + + junction_idx_active = get_lookup(net, "node", "index_active_hydraulics")[ cls.get_connected_node_type().table_name() ] - in_service = pc_array[:, cls.IN_SERVICE].astype(bool) - index_pc = junction_idx_lookups_active[pc_array[in_service, cls.JUNCTS].astype(np.int32)] - if np.any(index_pc == -1): + index_pc = junction_idx_active[ctrl_juncts] + + if np.any(index_pc[in_service_arr] == -1): raise UserWarning( - f"The following controlled junction(s) were identified as disconnected, although" - f" the controlling pressure controller is in service: " - f"{pc_array[in_service, cls.JUNCTS][index_pc == -1].astype(np.int32)}" + f"Controlled junction(s) are disconnected while the pressure controller is in " + f"service: {ctrl_juncts[in_service_arr][index_pc[in_service_arr] == -1]}" ) - controlled = pc_array[in_service, cls.CONTROLLED].astype(bool) - node_pit[index_pc[controlled], NODE_TYPE] = PC_NODE + + df_dm, df_dp, df_dp1, df_dm_node, load, load_fn, load_tn = ( + calculate_derivatives_hydraulic(net, b_pit, node_pit, get_hydraulic_options(net)) + ) + + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + + # Zero out branch equation contributions for ctrl_active branches (replaced by PC constraint) + df_dm[ctrl_active] = 0.0 + df_dp[ctrl_active] = 0.0 + df_dp1[ctrl_active] = 0.0 + load[ctrl_active] = 0.0 + + # variables + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + p_from_col = sys_idx.idx(HydVarEq.PINIT, fn) + p_to_col = sys_idx.idx(HydVarEq.PINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([mdot_col, p_from_col, p_to_col]).astype(np.int32) + data_branch = np.concatenate([df_dm, df_dp, df_dp1]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = load.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, df_dm_node, + -load_fn, load_tn) + + # Pressure constraint for ctrl_active branches: P_ctrl_node = P_target + if np.any(ctrl_active): + ca_branch_eq = branch_eq[ctrl_active] + ca_index_pc = index_pc[ctrl_active] + p_ctrl_col = sys_idx.idx(HydVarEq.PINIT, ca_index_pc) + p_target = pc_array[ctrl_active, cls.CONTROLLED_P] + p_ctrl_val = node_pit[ca_index_pc, IdxNode.PINIT] + + registry.add(ComponentEquations( + rows=ca_branch_eq.astype(np.int32), + cols=p_ctrl_col.astype(np.int32), + data=np.ones(len(ca_branch_eq), dtype=np.float64), + load_rows=ca_branch_eq.astype(np.int32), + load_data=(p_ctrl_val - p_target).astype(np.float64), + )) @classmethod - def adaption_after_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - # set all PC branches to derivatives to 0 - f, t = idx_lookups[cls.table_name()] - press_pit = branch_pit[f:t, :] - pc_branch = press_pit[:, BRANCH_TYPE] == PC_BRANCH - press_pit[pc_branch, JAC_DERIV_DP] = 0 - press_pit[pc_branch, JAC_DERIV_DP1] = 0 - press_pit[pc_branch, JAC_DERIV_DM] = 0 + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + branch_pit_old = net["_active_old_pit"]["branch"] + fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout = ( + calculate_derivatives_branch_thermal(net, branch_pit[f:t], node_pit, + branch_pit_old[f:t], get_thermal_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = get_from_nodes_corrected(b_pit).astype(np.int32) + tn = get_to_nodes_corrected(b_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + t_from_col = sys_idx.idx(ThermVarEq.TINIT, fn) + t_tn_col = sys_idx.idx(ThermVarEq.TINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([t_from_col, t_out_col]).astype(np.int32) + data_branch = np.concatenate([dfb_dt, dfb_dtout]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = fb.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) @classmethod def extract_results(cls, net, options, branch_results, mode): - """ - Function that extracts certain results. - - :param mode: - :type mode: - :param branch_results: - :type branch_results: - :param net: The pandapipes network - :type net: pandapipesNet - :param options: - :type options: - :return: No Output. - """ - required_results_hyd, required_results_ht = standard_branch_wo_internals_result_lookup(net) extract_branch_results_without_internals(net, branch_results, required_results_hyd, @@ -151,37 +248,8 @@ def extract_results(cls, net, options, branch_results, mode): p_from = branch_results["p_from"][f:t] res_table["deltap_bar"].values[:] = p_to - p_from - @classmethod - def get_component_input(cls): - """ - - Get component input. - - :return: - :rtype: - """ - return [("name", dtype(object)), - ("from_junction", "u4"), - ("to_junction", "u4"), - ("controlled_junction", "u4"), - ("controlled_p_bar", "f8"), - ("control_active", "bool"), - ("loss_coefficient", "f8"), - ("in_service", 'bool'), - ("type", dtype(object))] - @classmethod def get_result_table(cls, net): - """ - - Gets the result table. - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ if get_fluid(net).is_gas: output = ["p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", diff --git a/src/pandapipes/component_models/pump_component.py b/src/pandapipes/component_models/pump_component.py index ac11bfe42..9a9d3d8ce 100644 --- a/src/pandapipes/component_models/pump_component.py +++ b/src/pandapipes/component_models/pump_component.py @@ -11,15 +11,25 @@ from pandapipes.component_models.abstract_models.branch_wo_internals_models import \ BranchWOInternalsComponent from pandapipes.component_models.component_toolbox import ( + build_pit_entries, get_component_array, get_std_type_lookup, + get_hydraulic_options, + get_thermal_options, + register_branch_node_mass_balance, + register_branch_node_thermal_balance, ) from pandapipes.component_models.junction_component import Junction from pandapipes.constants import NORMAL_TEMPERATURE, NORMAL_PRESSURE, R_UNIVERSAL, P_CONVERSION -from pandapipes.idx_branch import MDOTINIT, AREA, LOSS_COEFFICIENT as LC, FROM_NODE, PL -from pandapipes.idx_node import PINIT, PAMB, TINIT as TINIT_NODE +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.derivative_calculation import ( + calculate_derivatives_hydraulic, calculate_derivatives_branch_thermal, +) +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected, branch_area from pandapipes.pf.pipeflow_setup import get_fluid, get_net_option, get_lookup from pandapipes.pf.result_extraction import extract_branch_results_without_internals +from pandapipes.pf.system_index import ComponentEquations, HydVarEq, PitEntries, ThermVarEq try: import pandaplan.core.pplog as logging @@ -30,17 +40,12 @@ class Pump(BranchWOInternalsComponent): - """ + """Pump component that lifts pressure according to a characteristic curve.""" - """ STD_TYPE = 0 internal_cols = 1 - @classmethod - def from_to_node_cols(cls): - return "from_junction", "to_junction" - @classmethod def table_name(cls): return "pump" @@ -54,32 +59,37 @@ def get_connected_node_type(cls): return Junction @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. + def from_to_node_cols(cls): + return "from_junction", "to_junction" - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - pump_pit = super().create_pit_branch_entries(net, branch_pit) - pump_pit[:, LC] = 0 + @classmethod + def get_component_input(cls): + return [("name", dtype(object)), + ("from_junction", "u4"), + ("to_junction", "u4"), + ("std_type", dtype(object)), + ("in_service", 'bool'), + ("type", dtype(object))] + + @classmethod + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) + + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return + + rows = np.arange(f, t, dtype=np.int32) + d_val = 0.1 + area_val = d_val ** 2 * np.pi / 4 + mdotinit = 0.1 * area_val * get_fluid(net).get_density(NORMAL_TEMPERATURE) + registry.add(PitEntries(*build_pit_entries( + rows, [IdxBranch.MDOTINIT], [mdotinit], + ))) @classmethod def create_component_array(cls, net, component_pits): - """ - Function which creates an internal array of the component in analogy to the pit, but with - component specific entries, that are not needed in the pit. - - :param net: The pandapipes network - :type net: pandapipesNet - :param component_pits: dictionary of component specific arrays - :type component_pits: dict - :return: - :rtype: - """ tbl = net[cls.table_name()] if len(tbl): pump_array = np.zeros(shape=(len(tbl), cls.internal_cols), dtype=np.float64) @@ -91,58 +101,146 @@ def create_component_array(cls, net, component_pits): component_pits[cls.table_name()] = pump_array @classmethod - def adaption_before_derivatives_hydraulic(cls, net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - idx_lookups, options): - # calculation of pressure lift - f, t = idx_lookups[cls.table_name()] - pump_branch_pit = branch_pit[f:t, :] - if len(pump_branch_pit): - area = pump_branch_pit[:, AREA] - - pump_array = get_component_array(net, cls.table_name()) - idx = pump_array[:, cls.STD_TYPE].astype(np.int32) - std_types = get_std_type_lookup(net, cls.table_name())[idx] - - from_nodes = pump_branch_pit[:, FROM_NODE].astype(np.int32) - # to_nodes = pump_branch_pit[:, TO_NODE].astype(np.int32) - fluid = get_fluid(net) - p_from = node_pit[from_nodes, PAMB] + node_pit[from_nodes, PINIT] - # p_to = node_pit[to_nodes, PAMB] + node_pit[to_nodes, PINIT] - t_from = node_pit[from_nodes, TINIT_NODE] - numerator_from = NORMAL_PRESSURE * t_from - v_mps = pump_branch_pit[:, MDOTINIT] / pump_branch_pit[:, AREA] / fluid.get_density(NORMAL_TEMPERATURE) - if fluid.is_gas: - # consider volume flow at inlet - normfactor_from = numerator_from * fluid.get_compressibility(p_from, t_from) \ - / (p_from * NORMAL_TEMPERATURE) - v_from = v_mps * normfactor_from - else: - v_from = v_mps - vol = v_from * area - if len(std_types): - fcts = itemgetter(*std_types)(net['std_types']['pump']) - fcts = [fcts] if not isinstance(fcts, tuple) else fcts - pl = np.array(list(map(lambda x, y: x.get_pressure(y), fcts, vol))) - pump_branch_pit[:, PL] = pl + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + + b_pit = branch_pit[f:t] + cls._compute_pl(net, b_pit, node_pit) + + df_dm, df_dp, df_dp1, df_dm_node, load, load_fn, load_tn = ( + calculate_derivatives_hydraulic(net, b_pit, node_pit, get_hydraulic_options(net)) + ) + + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + + # variables + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + p_from_col = sys_idx.idx(HydVarEq.PINIT, fn) + p_to_col = sys_idx.idx(HydVarEq.PINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([mdot_col, p_from_col, p_to_col]).astype(np.int32) + data_branch = np.concatenate([df_dm, df_dp, df_dp1]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = load.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, df_dm_node, + -load_fn, load_tn) @classmethod - def extract_results(cls, net, options, branch_results, mode): - """ - Function that extracts certain results. - - :param branch_results: - :type branch_results: - :param net: The pandapipes network - :type net: pandapipesNet - :param options: - :type options: - :param mode: - :type mode: - :return: No Output. + def _compute_pl(cls, net, b_pit, node_pit): + """Compute pressure lift from pump characteristic and write it into b_pit[:, PL]. + + get_component_array(net, cls.table_name()) is filtered by the same active_hydraulics + mask, over the same table row range, as b_pit itself (see register_hydraulic_equations + above and get_component_array's own only_active filtering) - so its rows are already + aligned 1:1 with b_pit's rows without needing any extra index. Indexing it via + IdxBranch.ELEMENT_IDX (a pandas index *label*) instead of positionally was a bug: that + array is built positionally (row i = i-th row of net[table_name()]), so a label-based + index silently breaks as soon as the table's index isn't a contiguous 0..n-1 range (e.g. + after dropping a row and adding a new one). """ + pump_array = get_component_array(net, cls.table_name()) + idx = pump_array[:, cls.STD_TYPE].astype(np.int32) + std_types = get_std_type_lookup(net, cls.table_name())[idx] + + from_nodes = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + fluid = get_fluid(net) + area = branch_area(b_pit) + v_mps = b_pit[:, IdxBranch.MDOTINIT] / area / fluid.get_density(NORMAL_TEMPERATURE) + if fluid.is_gas: + p_from = node_pit[from_nodes, IdxNode.PAMB] + node_pit[from_nodes, IdxNode.PINIT] + t_from = node_pit[from_nodes, IdxNode.TINIT] + normfactor = (NORMAL_PRESSURE * t_from + * fluid.get_compressibility(p_from, t_from) + / (p_from * NORMAL_TEMPERATURE)) + v_from = v_mps * normfactor + else: + v_from = v_mps + + vol = v_from * area + if len(std_types): + fcts = itemgetter(*std_types)(net['std_types']['pump']) + fcts = [fcts] if not isinstance(fcts, tuple) else fcts + b_pit[:, IdxBranch.PL] = np.array(list(map(lambda f, v: f.get_pressure(v), fcts, vol))) + + @classmethod + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + branch_pit_old = net["_active_old_pit"]["branch"] + fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout = ( + calculate_derivatives_branch_thermal(net, branch_pit[f:t], node_pit, + branch_pit_old[f:t], get_thermal_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = get_from_nodes_corrected(b_pit).astype(np.int32) + tn = get_to_nodes_corrected(b_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + t_from_col = sys_idx.idx(ThermVarEq.TINIT, fn) + t_tn_col = sys_idx.idx(ThermVarEq.TINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([t_from_col, t_out_col]).astype(np.int32) + data_branch = np.concatenate([dfb_dt, dfb_dtout]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = fb.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) + + @classmethod + def get_result_table(cls, net): + calc_compr_pow = get_net_option(net, 'calc_compression_power') + + if get_fluid(net).is_gas: + output = ["deltap_bar", + "p_from_bar", "p_to_bar", + "t_from_k", "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", + "vdot_norm_m3_per_s", "normfactor_from", "normfactor_to"] + else: + output = ["deltap_bar", "p_from_bar", "p_to_bar", "t_from_k", + "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_m3_per_s"] + if calc_compr_pow: + output += ["compr_power_mw"] + + return output, True + @classmethod + def extract_results(cls, net, options, branch_results, mode): required_results_hyd, required_results_ht = standard_branch_wo_internals_result_lookup(net) required_results_hyd.extend([("deltap_bar", "pl")]) @@ -158,21 +256,20 @@ def extract_results(cls, net, options, branch_results, mode): if net.fluid.is_gas: p_from = branch_results["p_abs_from"][f:t] p_to = branch_results["p_abs_to"][f:t] - t0 = net["_pit"]["node"][from_nodes, TINIT_NODE] + t0 = net["_pit"]["node"][from_nodes, IdxNode.TINIT] mf_sum_int = branch_results["mf_from"][f:t] - # calculate ideal compression power compr = get_fluid(net).get_compressibility(p_from, t0) try: - molar_mass = net.fluid.get_molar_mass() # [g/mol] + molar_mass = net.fluid.get_molar_mass() except UserWarning: logger.error('Molar mass is missing in your fluid. Before you are able to ' 'retrieve the compression power make sure that the molar mass is' ' defined') else: - r_spec = 1e3 * R_UNIVERSAL / molar_mass # [J/(kg * K)] + r_spec = 1e3 * R_UNIVERSAL / molar_mass cp = net.fluid.get_heat_capacity(t0) cv = cp - r_spec - k = cp/cv # 'kappa' heat capacity ratio + k = cp / cv w_real_isentr = (k / (k - 1)) * r_spec * compr * t0 * \ (np.divide(p_to, p_from) ** ((k - 1) / k) - 1) res_table['compr_power_mw'].values[:] = \ @@ -181,46 +278,3 @@ def extract_results(cls, net, options, branch_results, mode): vf_sum_int = branch_results["vf"][f:t] pl = branch_results["pl"][f:t] res_table['compr_power_mw'].values[:] = pl * P_CONVERSION * vf_sum_int / 1e6 - - @classmethod - def get_component_input(cls): - """ - - Get component input. - - :return: - :rtype: - """ - return [("name", dtype(object)), - ("from_junction", "u4"), - ("to_junction", "u4"), - ("std_type", dtype(object)), - ("in_service", 'bool'), - ("type", dtype(object))] - - @classmethod - def get_result_table(cls, net): - """ - - Gets the result table. - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ - calc_compr_pow = get_net_option(net, 'calc_compression_power') - - if get_fluid(net).is_gas: - output = ["deltap_bar", - "p_from_bar", "p_to_bar", - "t_from_k", "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", - "vdot_norm_m3_per_s", "normfactor_from", "normfactor_to"] - else: - output = ["deltap_bar", "p_from_bar", "p_to_bar", "t_from_k", - "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_m3_per_s"] - if calc_compr_pow: - output += ["compr_power_mw"] - - return output, True diff --git a/src/pandapipes/component_models/sink_component.py b/src/pandapipes/component_models/sink_component.py index be338482f..f43a491e9 100644 --- a/src/pandapipes/component_models/sink_component.py +++ b/src/pandapipes/component_models/sink_component.py @@ -6,14 +6,16 @@ class Sink(ConstFlow): - """ - - """ + """Sink component that removes mass from a junction.""" @classmethod def table_name(cls): return "sink" + @classmethod + def active_identifier(cls): + return "in_service" + @classmethod def sign(cls): return 1 @@ -21,7 +23,3 @@ def sign(cls): @classmethod def get_connected_node_type(cls): return Junction - - @classmethod - def active_identifier(cls): - return "in_service" diff --git a/src/pandapipes/component_models/source_component.py b/src/pandapipes/component_models/source_component.py index 0c670d7b1..556956fd1 100644 --- a/src/pandapipes/component_models/source_component.py +++ b/src/pandapipes/component_models/source_component.py @@ -6,14 +6,16 @@ class Source(ConstFlow): - """ - - """ + """Source component that injects mass into a junction.""" @classmethod def table_name(cls): return "source" + @classmethod + def active_identifier(cls): + return "in_service" + @classmethod def sign(cls): return -1 @@ -21,7 +23,3 @@ def sign(cls): @classmethod def get_connected_node_type(cls): return Junction - - @classmethod - def active_identifier(cls): - return "in_service" diff --git a/src/pandapipes/component_models/valve_component.py b/src/pandapipes/component_models/valve_component.py index 111711c50..72ffca001 100644 --- a/src/pandapipes/component_models/valve_component.py +++ b/src/pandapipes/component_models/valve_component.py @@ -6,24 +6,30 @@ from numpy import dtype from pandapipes.component_models.abstract_models.branch_w_internals_models import BranchWInternalsComponent -from pandapipes.component_models.component_toolbox import standard_branch_wo_internals_result_lookup +from pandapipes.component_models.component_toolbox import ( + build_pit_entries, p_correction_height_air, standard_branch_wo_internals_result_lookup, + get_hydraulic_options, get_thermal_options, register_branch_node_mass_balance, + register_branch_node_thermal_balance, +) from pandapipes.component_models.junction_component import Junction -from pandapipes.idx_branch import LENGTH, K, TEXT, ALPHA, FROM_NODE, TO_NODE, TOUTINIT, DO, D -from pandapipes.idx_node import TINIT as TINIT_NODE, HEIGHT, PINIT, ACTIVE as ACTIVE_ND, PAMB +from pandapipes.constants import NORMAL_TEMPERATURE +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.derivative_calculation import ( + calculate_derivatives_hydraulic, calculate_derivatives_branch_thermal, +) +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected from pandapipes.pf.pipeflow_setup import get_fluid, get_net_option, get_lookup from pandapipes.pf.result_extraction import extract_branch_results_without_internals +from pandapipes.pf.system_index import ComponentEquations, HydVarEq, PitEntries, ThermVarEq class Valve(BranchWInternalsComponent): - """ - Valves are branch elements that can separate two junctions. + """Valves are branch elements that can separate two junctions. + They have a length of 0, but can introduce a lumped pressure loss. """ - @classmethod - def from_to_node_cols(cls): - return "junction", "element" - @classmethod def table_name(cls): return "valve" @@ -36,20 +42,16 @@ def active_identifier(cls): def get_connected_node_type(cls): return Junction + @classmethod + def from_to_node_cols(cls): + return "junction", "element" + @classmethod def internal_node_name(cls): return "valve_nodes" @classmethod def get_internal_node_number(cls, net, return_internal_only=True): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :return: - :rtype: - """ - int_nodes = np.zeros(len(net[cls.table_name()]), dtype=np.int32) mask_p = np.flatnonzero(net[cls.table_name()]['et'].values == 'pi') val = net[cls.table_name()][list(cls.from_to_node_cols())].values[mask_p] @@ -62,109 +64,236 @@ def get_internal_node_number(cls, net, return_internal_only=True): @classmethod def get_internal_branch_number(cls, net): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :return: - :rtype: - """ - return np.ones(len(net[cls.table_name()]), dtype=np.int32) @classmethod - def create_pit_node_entries(cls, net, node_pit): - int_node_pit = super().create_pit_node_entries(net, node_pit) - if int_node_pit is not None: - int_node_number = cls.get_internal_node_number(net) - junction_table_name = cls.get_connected_node_type().table_name() - ft_lookup = get_lookup(net, "node", "from_to") - fj_name, _ = cls.from_to_node_cols() - f_junction, t_junction = ft_lookup[junction_table_name] - junction_pit = node_pit[f_junction:t_junction, :] - - from_junctions = net[cls.table_name()][fj_name].values.astype(np.int32) - junction_indices = get_lookup(net, "node", "index")[junction_table_name] - junct_pit_index = junction_indices[from_junctions] - fj_nodes = np.repeat(junct_pit_index, int_node_number) - int_node_pit[:, TINIT_NODE] = junction_pit[fj_nodes, TINIT_NODE] - int_node_pit[:, PINIT] = junction_pit[fj_nodes, PINIT] - if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - int_node_pit[:, HEIGHT] = junction_pit[fj_nodes, HEIGHT] - int_node_pit[:, PAMB] = junction_pit[fj_nodes, PAMB] - int_node_pit[:, ACTIVE_ND] = junction_pit[fj_nodes, ACTIVE_ND] + def get_component_input(cls): + return [ + ("name", dtype(object)), + ("junction", "i8"), + ("element", "i8"), + ("et", dtype(object)), + ("inner_diameter_mm", "f8"), + ("opened", "bool"), + ("loss_coefficient", "f8"), + ("type", dtype(object)) + ] @classmethod - def create_pit_branch_entries(cls, net, branch_pit): - """ - Function which creates pit branch entries with a specific table. - - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :return: No Output. - """ - valve_pit, node_pit = super().create_pit_branch_entries(net, branch_pit) + def register_pit_node_entries(cls, net, node_pit, registry) -> None: + super().register_pit_node_entries(net, node_pit, registry) + + node_ft_lookups = get_lookup(net, "node", "from_to") + if cls.internal_node_name() not in node_ft_lookups: + return + f, t = node_ft_lookups[cls.internal_node_name()] + if f == t: + return + + int_node_number = cls.get_internal_node_number(net) + junction_table_name = cls.get_connected_node_type().table_name() + fj_name, _ = cls.from_to_node_cols() + + from_junctions = net[cls.table_name()][fj_name].values.astype(np.int32) + junction_indices = get_lookup(net, "node", "index")[junction_table_name] + junct_pit_index = junction_indices[from_junctions] + fj_nodes = np.repeat(junct_pit_index, int_node_number) + + f_junc, _ = node_ft_lookups[junction_table_name] + junc_df = net[junction_table_name] + local_idx = fj_nodes - f_junc + + rows = np.arange(f, t, dtype=np.int32) + + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxNode.TINIT, IdxNode.PINIT], + [junc_df.tfluid_k.values[local_idx], junc_df.pn_bar.values[local_idx]], + ))) + if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: + height_vals = junc_df.height_m.values[local_idx] + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxNode.HEIGHT, IdxNode.PAMB, IdxNode.ACTIVE], + [height_vals, p_correction_height_air(height_vals), + junc_df.in_service.values[local_idx].astype(float)], + ))) + + @classmethod + def register_pit_branch_entries(cls, net, branch_pit, node_pit, registry) -> None: + super().register_pit_branch_entries(net, branch_pit, node_pit, registry) + + f, t = get_lookup(net, "branch", "from_to")[cls.table_name()] + tbl = net[cls.table_name()] + if not len(tbl): + return + internal_node_number, inverse_index, mask_p = cls.get_internal_node_number(net, False) fn_col, tn_col = cls.from_to_node_cols() - junction_idx_lookup = get_lookup(net, "node", "index")[Junction.table_name()] - from_nodes = junction_idx_lookup[net[cls.table_name()][fn_col].values] - to_nodes = np.zeros_like(from_nodes, dtype=int) - mask_j = net[cls.table_name()].et == 'ju' - to_elements = net[cls.table_name()][tn_col].values - to_nodes[mask_j] = junction_idx_lookup[to_elements[mask_j]] + junction_table_name = cls.get_connected_node_type().table_name() + junction_idx_lookup = get_lookup(net, "node", "index")[junction_table_name] + f_junc, _ = get_lookup(net, "node", "from_to")[junction_table_name] + junc_df = net[junction_table_name] + + from_junctions_raw = tbl[fn_col].values.astype(np.int32) + from_nodes = junction_idx_lookup[from_junctions_raw] + to_nodes = np.zeros(len(tbl), dtype=np.int64) + mask_j = tbl.et.values == 'ju' + to_elements = tbl[tn_col].values + to_nodes[mask_j] = junction_idx_lookup[to_elements[mask_j].astype(np.int32)] + + rows = np.arange(f, t, dtype=np.int32) if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: has_internals = np.any(internal_node_number > 0) if has_internals: - f, t = get_lookup(net, "branch", "from_to")['pipe'] - pipe_pit = branch_pit[f:t, :] pipe_idx_lookup = get_lookup(net, "branch", "index")['pipe'] mask_p_uni = internal_node_number.astype(bool) - pipes = pipe_idx_lookup[to_elements[mask_p_uni]] + pipes = pipe_idx_lookup[to_elements[mask_p_uni].astype(np.int32)] internal = net['_lookups']['internal_branches']['pipe'] - fn_pipe = pipe_pit[internal[pipes, 0], FROM_NODE] - fp = np.where(fn_pipe == from_nodes[mask_p_uni], True, False) - f, t = get_lookup(net, "node", "from_to")['valve_nodes'] - valve_nodes = np.arange(f, t) - pipe_pit[internal[pipes[fp], 0], FROM_NODE] = valve_nodes[fp] - pipe_pit[internal[pipes[~fp], 1], TO_NODE] = valve_nodes[~fp] + # Determine pipe direction from net.pipe (branch_pit not yet filled at registry phase) + pipe_from_junctions = net['pipe'].loc[ + to_elements[mask_p_uni].astype(np.int32), 'from_junction' + ].values + fn_pipe = junction_idx_lookup[pipe_from_junctions.astype(np.int32)] + fp = fn_pipe == from_nodes[mask_p_uni] + + vn_f, vn_t = get_lookup(net, "node", "from_to")['valve_nodes'] + valve_nodes = np.arange(vn_f, vn_t, dtype=np.int32) + + if np.any(fp): + registry.add_override(PitEntries(*build_pit_entries( + internal[pipes[fp], 0].astype(np.int32), + [IdxBranch.FROM_NODE], [valve_nodes[fp].astype(float)], + ))) + if np.any(~fp): + registry.add_override(PitEntries(*build_pit_entries( + internal[pipes[~fp], 1].astype(np.int32), + [IdxBranch.TO_NODE], [valve_nodes[~fp].astype(float)], + ))) to_nodes[mask_p] = valve_nodes[inverse_index] - tbl = cls.table_name() - valve_pit[:, FROM_NODE] = from_nodes - valve_pit[:, TO_NODE] = to_nodes - valve_pit[:, LENGTH] = 0 - valve_pit[:, K] = 1e-3 - valve_pit[:, TEXT] = get_net_option(net, 'ambient_temperature') - valve_pit[:, ALPHA] = 0 - valve_pit[:, D] = net[tbl].inner_diameter_mm.values / 1000. - valve_pit[:, DO] = valve_pit[:, D] + registry.add(PitEntries(*build_pit_entries( + rows, + [IdxBranch.FROM_NODE, IdxBranch.TO_NODE, IdxBranch.K, IdxBranch.TEXT], + [from_nodes.astype(float), to_nodes.astype(float), + np.full(len(rows), 1e-3), + np.full(len(rows), get_net_option(net, 'ambient_temperature'))], + ))) + + # TOUTINIT — always set (not conditional on transient) + toutinit = np.zeros(len(rows)) + if np.any(mask_j): + toutinit[mask_j] = junc_df.tfluid_k.values[ + junction_idx_lookup[to_elements[mask_j].astype(np.int32)] - f_junc + ] + if len(mask_p): + toutinit[mask_p] = junc_df.tfluid_k.values[ + junction_idx_lookup[from_junctions_raw[mask_p]] - f_junc + ] + registry.add(PitEntries(*build_pit_entries(rows, [IdxBranch.TOUTINIT], [toutinit]))) + + d_vals = tbl.inner_diameter_mm.values / 1000. + area_vals = d_vals ** 2 * np.pi / 4 + mdotinit_vals = 0.1 * area_vals * get_fluid(net).get_density(NORMAL_TEMPERATURE) + registry.add(PitEntries(*build_pit_entries(rows, [IdxBranch.MDOTINIT], [mdotinit_vals]))) - valve_pit[:, TOUTINIT] = node_pit[to_nodes, TINIT_NODE] + @classmethod + def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry) -> None: + f, t = get_lookup(net, "branch", "from_to_active_hydraulics")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + df_dm, df_dp, df_dp1, df_dm_node, load, load_fn, load_tn = ( + calculate_derivatives_hydraulic(net, branch_pit[f:t], node_pit, get_hydraulic_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tn = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + + # variables + mdot_col = sys_idx.idx(HydVarEq.MDOTINIT, branch_idx) + p_from_col = sys_idx.idx(HydVarEq.PINIT, fn) + p_to_col = sys_idx.idx(HydVarEq.PINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(HydVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([mdot_col, p_from_col, p_to_col]).astype(np.int32) + data_branch = np.concatenate([df_dm, df_dp, df_dp1]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = load.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_mass_balance(sys_idx, registry, fn, tn, mdot_col, df_dm_node, + -load_fn, load_tn) @classmethod - def get_component_input(cls): - """ + def register_thermal_equations(cls, net, branch_pit, node_pit, sys_idx, registry): + f, t = get_lookup(net, "branch", "from_to_active_heat_transfer")[cls.table_name()] + branch_idx = np.arange(f, t, dtype=np.int32) + if not len(branch_idx): + return + branch_pit_old = net["_active_old_pit"]["branch"] + fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout = ( + calculate_derivatives_branch_thermal(net, branch_pit[f:t], node_pit, branch_pit_old[f:t], + get_thermal_options(net)) + ) + + b_pit = branch_pit[f:t] + fn = get_from_nodes_corrected(b_pit).astype(np.int32) + tn = get_to_nodes_corrected(b_pit).astype(np.int32) + + # variables + t_out_col = sys_idx.idx(ThermVarEq.TOUTINIT, branch_idx) + t_from_col = sys_idx.idx(ThermVarEq.TINIT, fn) + t_tn_col = sys_idx.idx(ThermVarEq.TINIT, tn) + + # equation position branch + branch_eq = sys_idx.idx(ThermVarEq.BRANCH, branch_idx) + + # system matrix branch + rows_branch = np.concatenate([branch_eq, branch_eq]).astype(np.int32) + cols_branch = np.concatenate([t_from_col, t_out_col]).astype(np.int32) + data_branch = np.concatenate([dfb_dt, dfb_dtout]).astype(np.float64) + load_rows_branch = branch_eq.astype(np.int32) + load_branch = fb.astype(np.float64) + + registry.add(ComponentEquations( + rows=rows_branch, + cols=cols_branch, + data=data_branch, + load_rows=load_rows_branch, + load_data=load_branch, + )) + + register_branch_node_thermal_balance(sys_idx, registry, tn, t_tn_col, t_out_col, + dfnt_dt, dfnt_dtout, fnt) - :return: - :rtype: - """ - return [ - ("name", dtype(object)), - ("junction", "i8"), - ("element", "i8"), - ("et", dtype(object)), - ("inner_diameter_mm", "f8"), - ("opened", "bool"), - ("loss_coefficient", "f8"), - ("type", dtype(object)) - ] + @classmethod + def get_result_table(cls, net): + if get_fluid(net).is_gas: + output = ["v_from_m_per_s", "v_to_m_per_s", "v_mean_m_per_s", "p_from_bar", "p_to_bar", "t_from_k", + "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_norm_m3_per_s", + "reynolds", "lambda", "normfactor_from", "normfactor_to"] + else: + output = ["v_mean_m_per_s", "p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", + "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_m3_per_s", "reynolds", "lambda"] + return output, True @classmethod def extract_results(cls, net, options, branch_results, mode): @@ -177,22 +306,3 @@ def extract_results(cls, net, options, branch_results, mode): extract_branch_results_without_internals(net, branch_results, required_results_hyd, required_results_ht, cls.table_name(), mode) - - @classmethod - def get_result_table(cls, net): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :return: (columns, all_float) - the column names and whether they are all float type. Only - if False, returns columns as tuples also specifying the dtypes - :rtype: (list, bool) - """ - if get_fluid(net).is_gas: - output = ["v_from_m_per_s", "v_to_m_per_s", "v_mean_m_per_s", "p_from_bar", "p_to_bar", "t_from_k", - "t_to_k", "t_outlet_k", "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_norm_m3_per_s", - "reynolds", "lambda", "normfactor_from", "normfactor_to"] - else: - output = ["v_mean_m_per_s", "p_from_bar", "p_to_bar", "t_from_k", "t_to_k", "t_outlet_k", - "mdot_from_kg_per_s", "mdot_to_kg_per_s", "vdot_m3_per_s", "reynolds", "lambda"] - return output, True diff --git a/src/pandapipes/control/run_control.py b/src/pandapipes/control/run_control.py index 4a044fb1d..392375d13 100644 --- a/src/pandapipes/control/run_control.py +++ b/src/pandapipes/control/run_control.py @@ -5,12 +5,11 @@ from pandapower.control import run_control as run_control_pandapower, \ prepare_run_ctrl as prepare_run_control_pandapower import pandapipes as ppipe -from pandapipes.pipeflow import PipeflowNotConverged +from pandapipes.pf.pipeflow_setup import PipeflowNotConverged def run_control(net, ctrl_variables=None, max_iter=30, **kwargs): - """ - Function to run a control of the pandapipes network. + """Function to run a control of the pandapipes network. :param net: The pandapipes network :type net: pandapipesNet @@ -29,8 +28,7 @@ def run_control(net, ctrl_variables=None, max_iter=30, **kwargs): def prepare_run_ctrl(net, ctrl_variables, **kwargs): - """ - Function that defines default control variables. + """Function that defines default control variables. :param net: The pandapipes network :type net: pandapipesNet diff --git a/src/pandapipes/converter/stanet/preparing_steps.py b/src/pandapipes/converter/stanet/preparing_steps.py index f2fe69753..895cd124b 100644 --- a/src/pandapipes/converter/stanet/preparing_steps.py +++ b/src/pandapipes/converter/stanet/preparing_steps.py @@ -49,8 +49,8 @@ def get_stanet_raw_data(stanet_path, read_options=None, add_layers=True, return_line_info=False, keywords=None, decimal='.'): - """ - Extract raw data from STANET file. + """Extract raw data from STANET file. + :param stanet_path: Path to STANET .csv file :type stanet_path: string :param read_options: @@ -146,8 +146,7 @@ def get_stanet_raw_data(stanet_path, read_options=None, add_layers=True, return_ def get_key_from_value(val, used_dict): - """ - Reversed mapping operation. + """Reversed mapping operation. :param val: :type val: @@ -162,8 +161,8 @@ def get_key_from_value(val, used_dict): def get_net_params(net, stored_data): - """ - Returns pandapipesNet Parameters from STANET data. + """Returns pandapipesNet Parameters from STANET data. + :param net: Empty pandapipesNet :type net: pandapipesNet :param stored_data: dict of STANET tables @@ -290,8 +289,9 @@ def adapt_pipe_data(stored_data, pipe_data, coord_names, use_clients): def get_pipe_geo(stored_data, modus): - """ - Identify the geodata of all pipes. If inflexion points are given, they must be inserted for the + """Identify the geodata of all pipes. + + If inflexion points are given, they must be inserted for the correct pipes. STANET-like: pipe -> [(x_start, y_start), (x_end, y_end)]; inflexion_points -> [(x_infl, y_infl), pipe] @@ -357,8 +357,8 @@ def sum_coords(geo): def connection_pipe_section_table(stored_data, pipe_geodata, house_pipe_geodata, remove_unused_household_connections): - """ - Returns pipe and house connection lines + """Returns pipe and house connection lines. + :param stored_data: dict of STANET tables :type stored_data: dict :param pipe_geodata: geodata of all pipes diff --git a/src/pandapipes/converter/stanet/stanet2pandapipes.py b/src/pandapipes/converter/stanet/stanet2pandapipes.py index dd4cb3e3c..f357c593d 100644 --- a/src/pandapipes/converter/stanet/stanet2pandapipes.py +++ b/src/pandapipes/converter/stanet/stanet2pandapipes.py @@ -175,7 +175,7 @@ def stanet_to_pandapipes(stanet_path, name="net", remove_unused_household_connec def add_rated_p_values(net, **kwargs): - """ + """Set the rated pressure (pn_bar) of the net's junctions. :param net: :type net: diff --git a/src/pandapipes/converter/stanet/table_creation.py b/src/pandapipes/converter/stanet/table_creation.py index aebe70b6b..1741500d1 100644 --- a/src/pandapipes/converter/stanet/table_creation.py +++ b/src/pandapipes/converter/stanet/table_creation.py @@ -56,8 +56,8 @@ class ValveMode(StrEnum): def create_junctions_from_nodes(net, stored_data, net_params, index_mapping, add_layers): - """ - Creates pandapipes junctions from given STANET nodes. + """Creates pandapipes junctions from given STANET nodes. + :param net: pandapipes Net :type net: pandapipesNet :param stored_data: STANET data @@ -112,8 +112,8 @@ def create_junctions_from_nodes(net, stored_data, net_params, index_mapping, add def create_valve_and_pipe(net, stored_data, index_mapping, net_params, valve_mode, add_layers): - """ - Creates pandapipes valves and pipes from STANET data. + """Creates pandapipes valves and pipes from STANET data. + :param net: pipe network :type net: pandapipesNet :param stored_data: dictionary of STANET element tables @@ -272,8 +272,7 @@ def create_valve_and_pipe(net, stored_data, index_mapping, net_params, valve_mod def create_slider_valves(net, stored_data, index_mapping, add_layers, guess_opened_from_types=False): - """ - Creates pandapipes slider valves from STANET data. + """Creates pandapipes slider valves from STANET data. :param net: pandapipes net to which to add slider valves :type net: pandapipesNet @@ -356,8 +355,8 @@ def create_slider_valves(net, stored_data, index_mapping, add_layers, # noinspection PyTypeChecker def create_pumps(net, pump_table, index_mapping, add_layers): - """ - Creates pandapipes pumps from STANET data. + """Creates pandapipes pumps from STANET data. + :param net: :type net: :param pump_table: @@ -402,8 +401,8 @@ def create_pumps(net, pump_table, index_mapping, add_layers): def create_control_components(net, stored_data, index_mapping, net_params, add_layers, **kwargs): - """ - Creates pandapipes controller from STANET data. + """Creates pandapipes controller from STANET data. + :param net: :type net: :param stored_data: @@ -524,8 +523,7 @@ def create_control_components(net, stored_data, index_mapping, net_params, add_l def get_connection_types(connection_table): - """ - Returns the connection types contained in the STANET raw values. + """Returns the connection types contained in the STANET raw values. :param connection_table: table of connections on pipes :type connection_table: pd.DataFrame @@ -541,8 +539,8 @@ def get_connection_types(connection_table): def create_junctions_from_connections(net, connection_table, net_params, index_mapping, add_layers): - """ - Creates pandapipes junctions from STANET connections. + """Creates pandapipes junctions from STANET connections. + :param net: :type net: :param connection_table: @@ -594,7 +592,7 @@ def create_junctions_from_connections(net, connection_table, net_params, index_m def determine_junctions_from_connection_nodes(pipe_sections, index_mapping): - """ + """Determine the from/to junctions of pipe sections based on their connection nodes. :param pipe_sections: :type pipe_sections: @@ -626,8 +624,8 @@ def determine_junctions_from_connection_nodes(pipe_sections, index_mapping): def create_pipes_from_connections(net, stored_data, connection_table, index_mapping, pipe_geodata, add_layers): - """ - Creates pandapipes pipes from STANET connections. + """Creates pandapipes pipes from STANET connections. + :param net: :type net: :param stored_data: @@ -724,8 +722,8 @@ def create_geodata_sections(row): def create_heat_exchangers_stanet(net, stored_data, index_mapping, add_layers, add_flow=False): - """ - Creates pandapipes heat exchangers from STANET connections. + """Creates pandapipes heat exchangers from STANET connections. + :param net: :type net: :param stored_data: @@ -781,8 +779,8 @@ def create_heat_exchangers_stanet(net, stored_data, index_mapping, add_layers, a def create_pipes_from_remaining_pipe_table(net, stored_data, connection_table, index_mapping, pipe_geodata, add_layers): - """ - + """Create pipes for the entries of the pipe table not covered by the connection table. + :param net: :type net: :param stored_data: @@ -1045,7 +1043,7 @@ def get_tables_in_stanet_indices(stored_data, connection_table, house_table, met def create_nodes_house_connections(net, stored_data, connection_table, meter_table, house_table, index_mapping, net_params, add_layers): - """ + """Create junctions for houses and other house connection infrastructure. :param net: :type net: @@ -1177,7 +1175,7 @@ def create_geodata_sections(row): def create_sinks_meters(net, meter_table, index_mapping, net_params, add_layers): - """ + """Create sinks and sources for meters. :param net: :type net: @@ -1280,7 +1278,7 @@ def create_sinks_meters(net, meter_table, index_mapping, net_params, add_layers) def create_sinks_from_nodes(net, node_table, index_mapping, net_params, sinks_defined, control_flows, add_layers): - """ + """Create sinks for fixed feed-in or consumption nodes. :param net: :type net: diff --git a/src/pandapipes/converter/stanet/valve_pipe_component/create_valve_pipe.py b/src/pandapipes/converter/stanet/valve_pipe_component/create_valve_pipe.py index 1d373dff8..a6deaa67e 100644 --- a/src/pandapipes/converter/stanet/valve_pipe_component/create_valve_pipe.py +++ b/src/pandapipes/converter/stanet/valve_pipe_component/create_valve_pipe.py @@ -14,9 +14,9 @@ def create_valve_pipe(net, from_junction, to_junction, std_type, length_km, k_mm=0.15e-3, opened=True, loss_coefficient=0, sections=1, u_w_per_m2k=0., text_k=293, qext_w=0., name=None, index=None, geodata=None, in_service=True, type="valve_pipe", **kwargs): - """ - Creates a valve pipe element in net["valve_pipe"] from valve pipe parameters. In any case the - line parameters are defined through a single standard type. This component is + """Creates a valve pipe element in net["valve_pipe"] from valve pipe parameters. + + In any case the line parameters are defined through a single standard type. This component is an equivalent to STANET's valve element, as it represents a valve with a length, unlike the normal pandapipes valve which is assumed to be of length 0. This component is not supposed to be added to the standard pandapipes components, as it doesn't add value to the model itself, but @@ -67,13 +67,12 @@ def create_valve_pipe(net, from_junction, to_junction, std_type, length_km, k_mm :return: index - The unique ID of the created valve pipe :rtype: int - EXAMPLE: + Example + ------- create_valve_pipe(net, "valve_pipe1", from_junction=0, to_junction=1, std_type='315_PE_80_SDR_17', length_km=1) """ - # check if junction exist to attach the pipe to - add_new_component(net, ValvePipe) index = _get_index_with_check(net, "valve_pipe", index) @@ -99,8 +98,9 @@ def create_valve_pipe_from_parameters(net, from_junction, to_junction, length_km opened=True, loss_coefficient=0, sections=1, u_w_per_m2k=0., text_k=293, qext_w=0., name=None, index=None, geodata=None, in_service=True, type="valve_pipe", **kwargs): - """ - Creates a valve pipe element in net["valve_pipe"] from valve pipe parameters. This component is + """Creates a valve pipe element in net["valve_pipe"] from valve pipe parameters. + + This component is an equivalent to STANET's valve element, as it represents a valve with a length, unlike the normal pandapipes valve which is assumed to be of length 0. This component is not supposed to be added to the standard pandapipes components, as it doesn't add value to the model itself, but @@ -151,13 +151,12 @@ def create_valve_pipe_from_parameters(net, from_junction, to_junction, length_km :return: index - The unique ID of the created valve pipe :rtype: int - EXAMPLE: + Example + ------- create_valve_pipe_from_parameters(net, "valve_pipe1", from_junction=0, to_junction=1, length_km=1, d=4e-3) """ - # check if junction exist to attach the pipe to - add_new_component(net, ValvePipe) index = _get_index_with_check(net, "valve_pipe", index) diff --git a/src/pandapipes/converter/stanet/valve_pipe_component/valve_pipe_component.py b/src/pandapipes/converter/stanet/valve_pipe_component/valve_pipe_component.py index 3e3080769..6fbb80c11 100644 --- a/src/pandapipes/converter/stanet/valve_pipe_component/valve_pipe_component.py +++ b/src/pandapipes/converter/stanet/valve_pipe_component/valve_pipe_component.py @@ -7,7 +7,6 @@ from pandapipes.component_models.junction_component import Junction from pandapipes.component_models.pipe_component import Pipe -from pandapipes.idx_branch import LENGTH, K, D, AREA, LOSS_COEFFICIENT as LC from pandapipes.properties.fluids import get_fluid @@ -32,28 +31,6 @@ def active_identifier(cls): def get_connected_node_type(cls): return Junction - @classmethod - def create_pit_branch_entries_table_specific(cls, net, comp_pit, internal_pipe_number): - """ - - :param net: The pandapipes network - :type net: pandapipesNet - :param comp_pit: - :type comp_pit: - :param internal_pipe_number: - :type internal_pipe_number: - :return: - :rtype: - """ - comp_pit[:, LENGTH] = np.repeat(net[cls.table_name].length_km.values * 1000 / - internal_pipe_number, internal_pipe_number) - comp_pit[:, K] = np.repeat(net[cls.table_name].k_mm.values / 1000, - internal_pipe_number) - comp_pit[:, D] = np.repeat(net[cls.table_name].inner_diameter_mm.values / 1000., internal_pipe_number) - comp_pit[:, AREA] = comp_pit[:, D] ** 2 * np.pi / 4 - comp_pit[:, LC] = np.repeat(net[cls.table_name].loss_coefficient.values, - internal_pipe_number) - @classmethod def get_component_input(cls): return [("name", dtype(object)), @@ -76,10 +53,10 @@ def get_component_input(cls): @classmethod def geodata(cls): - """ + """Return the geodata columns for this component. - :return: - :rtype: + :return: column definitions + :rtype: list """ return [("coords", dtype(object))] diff --git a/src/pandapipes/converter/stanet/valve_pipe_component/valve_pipe_plotting.py b/src/pandapipes/converter/stanet/valve_pipe_component/valve_pipe_plotting.py index 86a6e4148..44a2a3265 100644 --- a/src/pandapipes/converter/stanet/valve_pipe_component/valve_pipe_plotting.py +++ b/src/pandapipes/converter/stanet/valve_pipe_component/valve_pipe_plotting.py @@ -24,8 +24,8 @@ def create_valve_pipe_collection(net, valve_pipes=None, valve_pipe_geodata=None, use_junction_geodata=False, infofunc=None, fill_closed=True, respect_valves=False, size=5., cmap=None, norm=None, picker=False, z=None, cbar_title="Pipe Loading [%]", clim=None, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes junction-junctiion valve_pipes. + """Creates a matplotlib patch collection of pandapipes junction-junctiion valve_pipes. + Valve_pipes are plotted in the center between two junctions with a "helper" line (dashed and thin) being drawn between the junctions as well. diff --git a/src/pandapipes/create.py b/src/pandapipes/create.py index a78eba036..6c9d0206c 100644 --- a/src/pandapipes/create.py +++ b/src/pandapipes/create.py @@ -112,8 +112,7 @@ def check_entry(val): def create_empty_network(name="", fluid=None, add_stdtypes=True, sector=Sector.ALL): - """ - This function initializes the pandapipes datastructure. + """Initializes the pandapipes datastructure. :param name: Name for the network :type name: string, default None @@ -158,9 +157,9 @@ def create_empty_network(name="", fluid=None, add_stdtypes=True, sector=Sector.A def create_junction(net, pn_bar, tfluid_k, height_m=0, name=None, index=None, in_service=True, type="junction", geodata=None, **kwargs): - """ - Adds one junction in table net["junction"]. Junctions are the nodes of the network that - all other elements connect to. + """Adds one junction in table net["junction"]. + + Junctions are the nodes of the network that all other elements connect to. :param net: The pandapipes network in which the element is created :type net: pandapipesNet @@ -210,8 +209,7 @@ def create_junction(net, pn_bar, tfluid_k, height_m=0, name=None, index=None, in def create_sink(net, junction, mdot_kg_per_s, scaling=1., name=None, index=None, in_service=True, type='sink', **kwargs): - """ - Adds one sink in table net["sink"]. + """Adds one sink in table net["sink"]. :param net: The net for which this sink should be created :type net: pandapipesNet @@ -253,8 +251,7 @@ def create_sink(net, junction, mdot_kg_per_s, scaling=1., name=None, index=None, def create_source(net, junction, mdot_kg_per_s, scaling=1., name=None, index=None, in_service=True, type='source', **kwargs): - """ - Adds one source in table net["source"]. + """Adds one source in table net["source"]. :param net: The net for which this source should be created :type net: pandapipesNet @@ -297,8 +294,7 @@ def create_source(net, junction, mdot_kg_per_s, scaling=1., name=None, index=Non def create_mass_storage(net, junction, mdot_kg_per_s, init_m_stored_kg=0, min_m_stored_kg=0., max_m_stored_kg=np.inf, scaling=1., name=None, index=None, in_service=True, type="mass_storage", **kwargs): - """ - Adds one storage entry in table net["mass_storage"]. Not suitable for thermal storage tanks. + """Adds one storage entry in table net["mass_storage"]. Not suitable for thermal storage tanks. :param net: The net for which this storage unit should be created :type net: pandapipesNet @@ -360,11 +356,11 @@ def create_mass_storage(net, junction, mdot_kg_per_s, init_m_stored_kg=0, min_m_ def create_ext_grid(net, junction, p_bar=None, t_k=None, type="auto", name=None, in_service=True, index=None, **kwargs): - """ - Creates an external grid and adds it to the table net["ext_grid"]. It transfers the junction - that it is connected to into a node with fixed value for either pressure, temperature or both - (depending on the type). Usually external grids represent connections to other grids feeding - the given pandapipesNet. + """Creates an external grid and adds it to the table net["ext_grid"]. + + It transfers the junction that it is connected to into a node with fixed value for either + pressure, temperature or both (depending on the type). Usually external grids represent + connections to other grids feeding the given pandapipesNet. :param net: The net that the external grid should be connected to :type net: pandapipesNet @@ -417,8 +413,7 @@ def create_ext_grid(net, junction, p_bar=None, t_k=None, type="auto", name=None, def create_heat_exchanger(net, from_junction, to_junction, qext_w, inner_diameter_mm, loss_coefficient=0, name=None, index=None, in_service=True, type="heat_exchanger", **kwargs): - """ - Creates a heat exchanger element in net["heat_exchanger"] from heat exchanger parameters. + """Creates a heat exchanger element in net["heat_exchanger"] from heat exchanger parameters. :param net: The net for which this heat exchanger should be created :type net: pandapipesNet @@ -473,8 +468,7 @@ def create_heat_exchanger(net, from_junction, to_junction, qext_w, inner_diamete def create_pipe(net, from_junction, to_junction, std_type, length_km, loss_coefficient=0, sections=1, text_k=0, name=None, index=None, geodata=None, in_service=True, type="pipe", **kwargs): - """ - Creates a pipe element in net["pipe"] from pipe parameters. + """Creates a pipe element in net["pipe"] from pipe parameters. :param net: The net for which this pipe should be created :type net: pandapipesNet @@ -554,8 +548,7 @@ def create_pipe(net, from_junction, to_junction, std_type, length_km, loss_coeff def create_pipe_from_parameters(net, from_junction, to_junction, length_km, inner_diameter_mm, outer_diameter_mm=None, k_mm=0.2, loss_coefficient=0, sections=1, u_w_per_m2k=0., text_k=None, name=None, index=None, geodata=None, in_service=True, type="pipe", **kwargs): - """ - Creates a pipe element in net["pipe"] from pipe parameters. + """Creates a pipe element in net["pipe"] from pipe parameters. :param net: The net for which this pipe should be created :type net: pandapipesNet @@ -644,8 +637,7 @@ def create_pipe_from_parameters(net, from_junction, to_junction, length_km, inne @deprecated_input(input_handler=input_handler_valve) def create_valve(net, junction, element, et, inner_diameter_mm, opened=True, loss_coefficient=0, name=None, index=None, type='valve', **kwargs): - """ - Creates a valve element in net["valve"] from valve parameters. + """Creates a valve element in net["valve"] from valve parameters. :param net: The net for which this valve should be created :type net: pandapipesNet @@ -704,8 +696,7 @@ def create_valve(net, junction, element, et, inner_diameter_mm, opened=True, los def create_pump(net, from_junction, to_junction, std_type, name=None, index=None, in_service=True, type="pump", **kwargs): - """ - Adds one pump in table net["pump"]. + r"""Adds one pump in table net["pump"]. :param net: The net for which this pump should be created :type net: pandapipesNet @@ -732,7 +723,8 @@ def create_pump(net, from_junction, to_junction, std_type, name=None, index=None :return: index - The unique ID of the created element :rtype: int - EXAMPLE: + Example + ------- >>> create_pump(net, 0, 1, std_type="P1") """ @@ -753,8 +745,7 @@ def create_pump_from_parameters(net, from_junction, to_junction, new_std_type_na pressure_list=None, flowrate_list=None, reg_polynomial_degree=None, poly_coefficents=None, name=None, index=None, in_service=True, type="pump", **kwargs): - """ - Adds one pump in table net["pump"]. + r"""Adds one pump in table net["pump"]. :param net: The net for which this pump should be created :type net: pandapipesNet @@ -765,44 +756,45 @@ def create_pump_from_parameters(net, from_junction, to_junction, new_std_type_na :param new_std_type_name: Set a name for your pump. You will find your definied pump under std_type in your net. The name will be given under std_type in net.pump. :type new_std_type_name: string - :param pressure_list: This list contains measured pressure supporting points required\ - to define and determine the dependencies of the pump between pressure and volume flow.\ - The pressure must be given in [bar]. Needs to be defined only if no pump of standard\ + :param pressure_list: This list contains measured pressure supporting points required + to define and determine the dependencies of the pump between pressure and volume flow. + The pressure must be given in [bar]. Needs to be defined only if no pump of standard type is selected. :type pressure_list: list, default None - :param flowrate_list: This list contains the corresponding flowrate values to the given\ - pressure values. Thus, the length must be equal to the pressure list. Needs to be\ - defined only if no pump of standard type is selected. ATTENTION: The flowrate values\ - are given in :math:`[\\frac{m^3}{h}]`. + :param flowrate_list: This list contains the corresponding flowrate values to the given + pressure values. Thus, the length must be equal to the pressure list. Needs to be + defined only if no pump of standard type is selected. ATTENTION: The flowrate values + are given in :math:`[\frac{m^3}{h}]`. :type flowrate_list: list, default None - :param reg_polynomial_degree: The degree of the polynomial fit must be defined if pressure\ - and flowrate list are given. The fit describes the behaviour of the pump (delta P /\ + :param reg_polynomial_degree: The degree of the polynomial fit must be defined if pressure + and flowrate list are given. The fit describes the behaviour of the pump (delta P / volumen flow curve). :type reg_polynomial_degree: int, default None :param poly_coefficents: Alternatviely to taking measurement values and degree of polynomial fit, previously calculated regression parameters can also be given directly. It - describes the dependency between pressure and flowrate.\ - ATTENTION: The determined parameteres must be retrieved by setting flowrate given\ - in :math:`[\\frac{m^3}{h}]` and pressure given in bar in context. The first entry in\ - the list (c[0]) is for the polynom of the highest degree (c[0]*x**n), the last one for\ + describes the dependency between pressure and flowrate. + ATTENTION: The determined parameteres must be retrieved by setting flowrate given + in :math:`[\frac{m^3}{h}]` and pressure given in bar in context. The first entry in + the list (c[0]) is for the polynom of the highest degree (c[0]*x**n), the last one for c*x**0. :type poly_coefficents: list, default None :param name: A name tag for this pump :type name: str, default None - :param index: Force a specified ID if it is available. If None, the index one higher than the\ + :param index: Force a specified ID if it is available. If None, the index one higher than the highest already existing index is selected. :type index: int, default None :param in_service: True if the pump is in service or False if it is out of service :type in_service: bool, default True :param type: type variable to classify the pump :type type: str, default "pump" - :param kwargs: Additional keyword arguments will be added as further columns to the\ + :param kwargs: Additional keyword arguments will be added as further columns to the net["pump"] table :type kwargs: dict :return: index - The unique ID of the created element :rtype: int - EXAMPLE: + Example + ------- >>> create_pump_from_parameters(net, 0, 1, 'pump1', pressure_list=[0,1,2,3], >>> flowrate_list=[0,1,2,3], reg_polynomial_degree=1) >>> create_pump_from_parameters(net, 0, 1, 'pump2', poly_coefficents=[1,0]) @@ -831,10 +823,11 @@ def create_pump_from_parameters(net, from_junction, to_junction, new_std_type_na def create_circ_pump_const_pressure(net, return_junction, flow_junction, p_flow_bar, plift_bar, t_flow_k=None, type="auto", name=None, index=None, in_service=True, **kwargs): - """ - Adds one circulation pump with a constant pressure lift in table net["circ_pump_pressure"]. \n + """Adds one circulation pump with a constant pressure lift in table net["circ_pump_pressure"]. + A circulation pump is a component that sets the pressure at its outlet (flow junction) and - asserts that the correct mass flow is extracted at its inlet (return junction). \n + asserts that the correct mass flow is extracted at its inlet (return junction). + In this particular case, the pressure lift is fixed, i.e. the pressure on both sides are set (with the pressure lift as difference). The mass flow through the component is just a result of the balance of the network. An equal representation is adding external grids at each of the @@ -899,10 +892,11 @@ def create_circ_pump_const_pressure(net, return_junction, flow_junction, p_flow_ def create_circ_pump_const_mass_flow(net, return_junction, flow_junction, p_flow_bar, mdot_flow_kg_per_s, t_flow_k=None, type="auto", name=None, index=None, in_service=True, **kwargs): - """ - Adds one circulation pump with a constant mass flow in table net["circ_pump_mass"].\n + """Adds one circulation pump with a constant mass flow in table net["circ_pump_mass"]. + A circulation pump is a component that sets the pressure at its outlet (flow junction) and - asserts that the correct mass flow is extracted at its inlet (return junction). \n + asserts that the correct mass flow is extracted at its inlet (return junction). + In this particular case, the mass flow and the pressure on the flow side are fixed, i.e. the pressure on the return side is just a result of the friction losses in the network. An equal representation is adding an external grid at the flow junction and a sink with the given mass @@ -993,7 +987,8 @@ def create_compressor(net, from_junction, to_junction, pressure_ratio, name=None :return: index - The unique ID of the created element :rtype: int - EXAMPLE: + Example + ------- >>> create_compressor(net, 0, 1, pressure_ratio=1.3) """ @@ -1108,8 +1103,7 @@ def create_pressure_control( def create_flow_control(net, from_junction, to_junction, controlled_mdot_kg_per_s, control_active=True, name=None, index=None, in_service=True, type="fc", **kwargs): - """ - Adds one flow control with a constant mass flow in table net["flow_control"]. + """Adds one flow control with a constant mass flow in table net["flow_control"]. :param net: The net for which this flow control should be created :type net: pandapipesNet @@ -1163,8 +1157,7 @@ def create_flow_control(net, from_junction, to_junction, controlled_mdot_kg_per_ def create_heat_consumer(net, from_junction, to_junction, qext_w=None, controlled_mdot_kg_per_s=None, deltat_k=None, treturn_k=None, name=None, index=None, in_service=True, type="heat_consumer", **kwargs): - """ - Creates a heat consumer element in net["heat_consumer"] from heat consumer parameters. + """Creates a heat consumer element in net["heat_consumer"] from heat consumer parameters. :param net: The net for which this heat consumer should be created :type net: @@ -1228,8 +1221,7 @@ def create_heat_consumer(net, from_junction, to_junction, qext_w=None, controlle def create_junctions(net, nr_junctions, pn_bar, tfluid_k, height_m=0, name=None, index=None, in_service=True, type="junction", geodata=None, **kwargs): - """ - Convenience function for creating many junctions at once. Parameter 'nr_junctions' specifies \ + """Convenience function for creating many junctions at once. Parameter 'nr_junctions' specifies \ the number of junctions created. Other parameters may be either arrays of length 'nr_junctions'\ or single values. @@ -1287,8 +1279,7 @@ def create_junctions(net, nr_junctions, pn_bar, tfluid_k, height_m=0, name=None, def create_sinks(net, junctions, mdot_kg_per_s, scaling=1., name=None, index=None, in_service=True, type='sink', **kwargs): - """ - Convenience function for creating many sinks at once. Parameter 'junctions' must be an array \ + """Convenience function for creating many sinks at once. Parameter 'junctions' must be an array \ of the desired length. Other parameters may be either arrays of the same length or single \ values. @@ -1332,8 +1323,7 @@ def create_sinks(net, junctions, mdot_kg_per_s, scaling=1., name=None, index=Non def create_sources(net, junctions, mdot_kg_per_s, scaling=1., name=None, index=None, in_service=True, type='source', **kwargs): - """ - Convenience function for creating many sources at once. Parameter 'junctions' must be an array \ + """Convenience function for creating many sources at once. Parameter 'junctions' must be an array \ of the desired length. Other parameters may be either arrays of the same length or single \ values. @@ -1376,12 +1366,13 @@ def create_sources(net, junctions, mdot_kg_per_s, scaling=1., name=None, index=N def create_ext_grids(net, junctions, p_bar, t_k, name=None, in_service=True, index=None, type="auto", **kwargs): - """ - Convenience function for creating many external grids at once. Parameter 'junctions' must be an\ - array of the desired length. Other parameters may be either arrays of the same length or single\ - values.\n - External grids transfer the junction that it is connected into a node with fixed value for \ - either pressure, temperature or both (depending on the type). Usually external grids represent \ + """Convenience function for creating many external grids at once. + + Parameter 'junctions' must be an array of the desired length. Other parameters may be either + arrays of the same length or single values. + + External grids transfer the junction that it is connected into a node with fixed value for + either pressure, temperature or both (depending on the type). Usually external grids represent connections to other grids feeding the given pandapipesNet. :param net: The net that the external grid should be connected to @@ -1396,19 +1387,20 @@ def create_ext_grids(net, junctions, p_bar, t_k, name=None, in_service=True, ind :type name: Iterable(str) or str, default None :param in_service: True for in service, False for out of service :type in_service: Iterable(bool) or bool, default True - :param index: Force specified IDs if they are available. If None, the index one higher than the\ + :param index: Force specified IDs if they are available. If None, the index one higher than the highest already existing index is selected and counted onwards. :type index: Iterable(int), default None - :param type: The external grid type denotes the values that are fixed at the respective node:\n - - "auto": Will automatically assign one of the following types based on the input for \ - p_bar and t_k \n - - "p": The pressure is fixed, the node acts as a slack node for the mass flow. \n - - "t": The temperature is fixed and will not be solved for, but is assumed as the \ - node's mix temperature. Please note that pandapipes cannot check for \ - inconsistencies in the formulation of heat transfer equations yet. \n + :param type: The external grid type denotes the values that are fixed at the respective node: + + - "auto": Will automatically assign one of the following types based on the input for + p_bar and t_k + - "p": The pressure is fixed, the node acts as a slack node for the mass flow. + - "t": The temperature is fixed and will not be solved for, but is assumed as the + node's mix temperature. Please note that pandapipes cannot check for + inconsistencies in the formulation of heat transfer equations yet. - "pt": The external grid shows both "p" and "t" behavior. :type type: Iterable(str) or str, default "auto" - :param kwargs: Additional keyword arguments will be added as further columns to the\ + :param kwargs: Additional keyword arguments will be added as further columns to the net["ext_grid"] table :return: index - The unique IDs of the created elements :rtype: Iterable(int) @@ -1433,8 +1425,7 @@ def create_ext_grids(net, junctions, p_bar, t_k, name=None, in_service=True, ind def create_pipes(net, from_junctions, to_junctions, std_type, length_km, loss_coefficient=0, sections=1, text_k=None, name=None, index=None, geodata=None, in_service=True, type="pipe", **kwargs): - """ - Convenience function for creating many pipes at once. Parameters 'from_junctions' and \ + """Convenience function for creating many pipes at once. Parameters 'from_junctions' and \ 'to_junctions' must be arrays of equal length. Other parameters may be either arrays of the \ same length or single values. In any case the line parameters are defined through a single \ standard type, so all pipes have the same standard type. @@ -1536,8 +1527,7 @@ def create_pipes_from_parameters(net, from_junctions, to_junctions, length_km, loss_coefficient=0, sections=1, u_w_per_m2k=0., text_k=None, name=None, index=None, geodata=None, in_service=True, type="pipe", **kwargs): - """ - Convenience function for creating many pipes at once. Parameters 'from_junctions' and \ + """Convenience function for creating many pipes at once. Parameters 'from_junctions' and \ 'to_junctions' must be arrays of equal length. Other parameters may be either arrays of the \ same length or single values. @@ -1633,8 +1623,7 @@ def create_pipes_from_parameters(net, from_junctions, to_junctions, length_km, @deprecated_input(input_handler=input_handler_valve, multiple=True) def create_valves(net, junctions, elements, et, inner_diameter_mm, opened=True, loss_coefficient=0, name=None, index=None, type='valve', **kwargs): - """ - Convenience function for creating many valves at once. Parameters 'junctions' and \ + """Convenience function for creating many valves at once. Parameters 'junctions' and \ 'elements' must be arrays of equal length. Other parameters may be either arrays of the \ same length or single values. @@ -1720,8 +1709,7 @@ def create_valves(net, junctions, elements, et, inner_diameter_mm, opened=True, def create_pressure_controls(net, from_junctions, to_junctions, controlled_junctions, controlled_p_bar, control_active=True, loss_coefficient=0., name=None, index=None, in_service=True, type="pressure_control", **kwargs): - """ - Convenience function for creating many pressure controls at once. Parameters 'from_junctions'\ + """Convenience function for creating many pressure controls at once. Parameters 'from_junctions'\ and 'to_junctions' must be arrays of equal length. Other parameters may be either arrays of the\ same length or single values. @@ -1799,8 +1787,7 @@ def create_pressure_controls(net, from_junctions, to_junctions, controlled_junct def create_flow_controls(net, from_junctions, to_junctions, controlled_mdot_kg_per_s, control_active=True, name=None, index=None, in_service=True, type="fc", **kwargs): - """ - Convenience function for creating many flow controls at once. Parameters 'from_junctions'\ + """Convenience function for creating many flow controls at once. Parameters 'from_junctions'\ and 'to_junctions' must be arrays of equal length. Other parameters may be either arrays of the\ same length or single values. @@ -1859,8 +1846,7 @@ def create_flow_controls(net, from_junctions, to_junctions, controlled_mdot_kg_p def create_heat_exchangers(net, from_junctions, to_junctions, qext_w, inner_diameter_mm, loss_coefficient=0, name=None, index=None, in_service=True, type="heat_exchanger", **kwargs): - """ - Convenience function for creating many heat exchangers at once. Parameters 'from_junctions'\ + """Convenience function for creating many heat exchangers at once. Parameters 'from_junctions'\ and 'to_junctions' must be arrays of equal length. Other parameters may be either arrays of the\ same length or single values. @@ -1920,8 +1906,7 @@ def create_heat_exchangers(net, from_junctions, to_junctions, qext_w, inner_diam def create_heat_consumers(net, from_junctions, to_junctions, qext_w=None, controlled_mdot_kg_per_s=None, deltat_k=None, treturn_k=None, name=None, index=None, in_service=True, type="heat_consumer", **kwargs): - """ - Creates several heat consumer elements in net["heat_consumer"] from heat consumer parameters. + """Creates several heat consumer elements in net["heat_consumer"] from heat consumer parameters. :param net: The net for which this heat consumer should be created :type net: @@ -1988,8 +1973,8 @@ def create_heat_consumers(net, from_junctions, to_junctions, qext_w=None, contro def create_fluid_from_lib(net, name, overwrite=True): - """ - Creates a fluid from library (if there is an entry) and sets net["fluid"] to this value. + """Creates a fluid from library (if there is an entry) and sets net["fluid"] to this value. + Currently, existing fluids in the library are: "hgas", "lgas", "hydrogen", "methane", "water","biomethane_pure", "biomethane_treated", "air". @@ -2055,8 +2040,8 @@ def _add_multiple_branch_geodata(net, table, geodata, index): def _auto_ext_grid_type(p_bar, t_k, typ, comp): - """ - Determine the type of node that an "ext_grid" would imply (fixed pressure and / or temperature). + """Determine the type of node that an "ext_grid" would imply (fixed pressure and / or temperature). + Also perform some validity checks. :param p_bar: fixed pressure @@ -2109,8 +2094,8 @@ def _auto_ext_grid_type(p_bar, t_k, typ, comp): def _auto_ext_grid_types(p_bar, t_k, typ, comp): - """ - Determine the type of node that an "ext_grid" would imply (fixed pressure and / or temperature). + """Determine the type of node that an "ext_grid" would imply (fixed pressure and / or temperature). + Also perform some validity checks. --> Same as `_auto_ext_grid_type`, but vectorized. .. note: diff --git a/src/pandapipes/idx.py b/src/pandapipes/idx.py new file mode 100644 index 000000000..aaf20f067 --- /dev/null +++ b/src/pandapipes/idx.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020-2026 by Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + + +class IndexMeta(type): + """Metaclass that collects a class's ``int``-valued attributes into an iterable dict. + + The resulting ``_indices`` dict is merged with any base classes' ``_indices`` (so + subclasses inherit and can extend their parent's index set). + """ + + def __new__(cls, name, bases, classdict): + """Create the class and collect its int-valued attributes into a merged ``_indices`` dict.""" + clsobj = super().__new__(cls, name, bases, classdict) + + clsobj._indices = { + k: v for k, v in classdict.items() + if not k.startswith("__") and isinstance(v, int) + } + + for base in bases: + if hasattr(base, "_indices"): + clsobj._indices = {**base._indices, **clsobj._indices} + + return clsobj + + def __iter__(cls): + """Iterate over ``(name, value)`` pairs of the class's collected index attributes.""" + return iter(cls._indices.items()) + + def keys(cls): + return cls._indices.keys() + + def values(cls): + return cls._indices.values() + + def items(cls): + return cls._indices.items() diff --git a/src/pandapipes/idx_branch.py b/src/pandapipes/idx_branch.py index d84d71441..c115e0500 100644 --- a/src/pandapipes/idx_branch.py +++ b/src/pandapipes/idx_branch.py @@ -2,52 +2,40 @@ # and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -# branch types -PC = 1 # Pressure controller branch - -# branch indices -TABLE_IDX = 0 # number of the table that this branch belongs to -ELEMENT_IDX = 1 # index of the element that this branch belongs to (within the given table) -BRANCH_TYPE = 2 # branch type relevant for the pressure controller -DIRECTED = 3 -FROM_NODE = 4 # f, from bus number -TO_NODE = 5 # t, to bus number -ACTIVE = 6 -LENGTH = 7 # Pipe length in [m] -D = 8 # Diameter in [m] -DO = 9 # Outer Diameter in [m] -AREA = 10 # Area in [m²] -K = 11 # Pipe roughness in [m] -RE = 12 # Reynolds number -LAMBDA = 13 # Lambda -LOSS_COEFFICIENT = 14 -ALPHA = 15 # Slot for heat transfer coefficient -QEXT = 16 # heat input into the branch [W] -TEXT = 17 # temperature of surrounding [K] -PL = 18 # Pressure lift [bar] -TL = 19 # Temperature lift [K] - -MDOTINIT = 20 # mass in [m/s] -MDOTINIT_T = 21 -FROM_NODE_T_SWITCHED = 22 # flag to indicate if the from and to node are switched in the thermal calculation -TOUTINIT = 23 # Internal slot for outlet pipe temperature -FLOW_RETURN_CONNECT = 24 # Make sure that return and flow side are connected to the central pump, respectively - -JAC_DERIV_DM = 25 # Slot for the derivative by mass -JAC_DERIV_DP = 26 # Slot for the derivative by pressure from_node -JAC_DERIV_DP1 = 27 # Slot for the derivative by pressure to_node -JAC_DERIV_DM_NODE = 28 # Slot for the derivative by mass for the nodes connected to branch -LOAD_VEC_BRANCHES = 29 # Slot for the load vector for the branches -LOAD_VEC_NODES_FROM = 30 # Slot for the load vector of the from nodes connected to branch -LOAD_VEC_NODES_TO = 31 # Slot for the load vector of the to nodes connected to branch - -JAC_DERIV_DT = 32 -JAC_DERIV_DTOUT = 33 -JAC_DERIV_DT_NODE = 34 # Slot for the node equation derivative of T for the nodes branch is connected from -JAC_DERIV_DTOUT_NODE = 35 # Slot for the node equation derivative of T for the corresponding branch -LOAD_VEC_BRANCHES_T = 36 -LOAD_VEC_NODES_TO_T = 37 # Slot for the load vector of the to nodes connected to branch - -DP_FRICT_LOSS = 38 - -branch_cols = 39 +from pandapipes.idx import IndexMeta + + +class IdxBranch(metaclass=IndexMeta): + # branch types + PC = 1 # Pressure controller branch + + # branch indices + TABLE_IDX = 0 # number of the table that this branch belongs to + ELEMENT_IDX = 1 # index of the element that this branch belongs to (within the given table) + BRANCH_TYPE = 2 # branch type relevant for the pressure controller + DIRECTED = 3 + FROM_NODE = 4 # f, from bus number + TO_NODE = 5 # t, to bus number + ACTIVE = 6 + LENGTH = 7 # Pipe length in [m] + D = 8 # Diameter in [m] + DO = 9 # Outer Diameter in [m] + K = 10 # Pipe roughness in [m] + RE = 11 # Reynolds number + LAMBDA = 12 # Lambda + LOSS_COEFFICIENT = 13 + ALPHA = 14 # Slot for heat transfer coefficient + QEXT = 15 # heat input into the branch [W] + TEXT = 16 # temperature of surrounding [K] + PL = 17 # Pressure lift [bar] + TL = 18 # Temperature lift [K] + + MDOTINIT = 19 # mass in [m/s] + TOUTINIT = 20 # Internal slot for outlet pipe temperature + + FROM_NODE_T_SWITCHED = 21 # flag to indicate if the from and to node are switched in the thermal calculation + FLOW_RETURN_CONNECT = 22 # Make sure that return and flow side are connected to the central pump, respectively + + DP_FRICT_LOSS = 23 + + branch_cols = 24 diff --git a/src/pandapipes/idx_node.py b/src/pandapipes/idx_node.py index 7ee8d88db..2d7ce4777 100644 --- a/src/pandapipes/idx_node.py +++ b/src/pandapipes/idx_node.py @@ -2,34 +2,34 @@ # and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -# node types -P = 1 # Reference node, pressure is fixed -L = 2 # All other nodes -T = 3 # Reference node with fixed temperature, otherwise 0 -PC = 4 # Controlled node with fixed pressure p -GE = 5 +from pandapipes.idx import IndexMeta -# node indices -TABLE_IDX = 0 # number of the table that this node belongs to -ELEMENT_IDX = 1 # index of the element that this node belongs to (within the given table) -NODE_TYPE = 2 # junction type -NODE_TYPE_T = 3 -ACTIVE = 4 -HEIGHT = 5 -PAMB = 6 # Ambient pressure in [bar] -LOAD = 7 -LOAD_T = 8 # Heat power drawn in [W] -EXT_GRID_OCCURENCE = 9 -EXT_GRID_OCCURENCE_T = 10 -INFEED = 11 -VAR_MASS_SLACK = 12 #required as slack do not necesseraly allow mass different from zero -PINIT = 13 -MDOTSLACKINIT = 14 -TINIT = 15 +class IdxNode(metaclass=IndexMeta): + # node types + P = 1 # Reference node, pressure is fixed + L = 2 # All other nodes + T = 3 # Reference node with fixed temperature, otherwise 0 + PC = 4 # Controlled node with fixed pressure p + GE = 5 -JAC_DERIV_MSL = 16 + # node indices + TABLE_IDX = 0 # number of the table that this node belongs to + ELEMENT_IDX = 1 # index of the element that this node belongs to (within the given table) + NODE_TYPE = 2 # junction type + NODE_TYPE_T = 3 + ACTIVE = 4 + HEIGHT = 5 + PAMB = 6 # Ambient pressure in [bar] + LOAD = 7 + LOAD_T = 8 # Heat power drawn in [W] + INFEED = 9 -JAC_DERIV_DT_N = 17 + PINIT = 10 + TINIT = 11 + MDOTSLACKINIT = 12 + # a real ext_grid sits here - MDOTSLACKINIT may absorb residual mass; otherwise (e.g. a + # circ_pump's own pressure-anchor node) it must be 0 + COUNT_VAR_MASS_SLACK = 13 -node_cols = 18 \ No newline at end of file + node_cols = 14 diff --git a/src/pandapipes/io/convert_format.py b/src/pandapipes/io/convert_format.py index 0950f3c95..d561d15a5 100644 --- a/src/pandapipes/io/convert_format.py +++ b/src/pandapipes/io/convert_format.py @@ -20,9 +20,7 @@ def convert_format(net): - """ - Converts old nets to new format to ensure consistency. The converted net is returned. - """ + """Convert old nets to new format to ensure consistency. The converted net is returned.""" _add_sector(net) add_default_components(net, overwrite=False) format_version = version.parse(__format_version__) diff --git a/src/pandapipes/io/file_io.py b/src/pandapipes/io/file_io.py index 28821325d..7817f8121 100644 --- a/src/pandapipes/io/file_io.py +++ b/src/pandapipes/io/file_io.py @@ -20,8 +20,7 @@ def to_pickle(net, filename): - """ - Saves a pandapipes Network with the pickle library. + """Saves a pandapipes Network with the pickle library. :param net: The pandapipes Network to save. :type net: pandapipesNet @@ -52,10 +51,11 @@ def to_json( indent: Union[int, str, None] = 2, sort_keys: bool = False, ): - """ - Saves a pandapipes Network in JSON format. The index columns of all pandas DataFrames will be - saved in ascending order. net elements which name begins with "_" (internal elements) will not - be saved. Std types will also not be saved. + """Saves a pandapipes Network in JSON format. + + The index columns of all pandas DataFrames will be saved in ascending order. net elements + which name begins with "_" (internal elements) will not be saved. Std types will also not be + saved. :param net: The pandapipes Network to save. :type net: pandapipesNet @@ -89,8 +89,7 @@ def to_json( def from_pickle(filename): - """ - Load a pandapipes format Network from pickle file. + """Load a pandapipes format Network from pickle file. :param filename: The absolute or relative path to the input file or file-like object :type filename: str, file-object @@ -109,8 +108,8 @@ def from_pickle(filename): def from_json(filename, convert=True, encryption_key=None, ignore_unknown_objects=False): - """ - Load a pandapipes network from a JSON file or string. + """Load a pandapipes network from a JSON file or string. + The index of the returned network is not necessarily in the same order as the original network. Index columns of all pandas DataFrames are sorted in ascending order. @@ -143,8 +142,8 @@ def from_json(filename, convert=True, encryption_key=None, ignore_unknown_object def from_json_string(json_string, convert=False, encryption_key=None, ignore_unknown_objects=False): - """ - Load a pandapipes network from a JSON string. + """Load a pandapipes network from a JSON string. + The index of the returned network is not necessarily in the same order as the original network. Index columns of all pandas DataFrames are sorted in ascending order. diff --git a/src/pandapipes/io/io_utils.py b/src/pandapipes/io/io_utils.py index 233a3b7d6..440d6476c 100644 --- a/src/pandapipes/io/io_utils.py +++ b/src/pandapipes/io/io_utils.py @@ -44,7 +44,7 @@ class FromSerializableRegistryPpipe(FromSerializableRegistry): omit_modules = '' def __init__(self, obj, d, ppipes_hook, ignore_unknown_objects=False, omit_modules=None): - """ + """Initialize the registry with the object, data, and hook to use for deserialization. :param obj: object the data is written to :type obj: object diff --git a/src/pandapipes/multinet/control/controller/multinet_control.py b/src/pandapipes/multinet/control/controller/multinet_control.py index 1a957bb69..a94366a2e 100644 --- a/src/pandapipes/multinet/control/controller/multinet_control.py +++ b/src/pandapipes/multinet/control/controller/multinet_control.py @@ -10,8 +10,7 @@ class P2GControlMultiEnergy(Controller): - """ - A controller to be used in a multinet. Converts power consumption to gas production. + """A controller to be used in a multinet. Converts power consumption to gas production. This controller couples a power network (from pandapower) and a gas network (from pandapipes) that are stored in a multinet. Requires one or multiple 'load' elements in the @@ -59,12 +58,11 @@ class P2GControlMultiEnergy(Controller): :param kwargs: optional additional controller arguments that were implemented by users :type kwargs: any """ + def __init__(self, multinet, element_index_power, element_index_gas, efficiency, name_power_net='power', name_gas_net='gas', in_service=True, order=0, level=0, drop_same_existing_ctrl=False, initial_run=True, name="P2GControlMultiEnergy"): - """ - see class docstring - """ + """See class docstring.""" super().__init__( multinet, name, in_service, order, level, drop_same_existing_ctrl=drop_same_existing_ctrl, initial_run=initial_run, @@ -117,8 +115,7 @@ def conversion_factor_mw_to_kgps(self): class G2PControlMultiEnergy(Controller): - """ - A controller to be used in a multinet. Connects power generation and gas consumption. + """A controller to be used in a multinet. Connects power generation and gas consumption. This controller couples a gas network (from pandapipes) and a power network (from pandapower) that are stored in a multinet. Requires one or multiple 'sink' elements in the gas @@ -185,9 +182,7 @@ def __init__(self, multinet, element_index_power, element_index_gas, efficiency, name_power_net='power', name_gas_net='gas', element_type_power="sgen", in_service=True, order=0, level=0, drop_same_existing_ctrl=False, initial_run=True, calc_gas_from_power=False, name="G2PControlMultiEnergy"): - """ - see class docstring - """ + """See class docstring.""" super().__init__( multinet, name, in_service, order, level, drop_same_existing_ctrl=drop_same_existing_ctrl, initial_run=initial_run, @@ -268,8 +263,7 @@ def conversion_factor_kgps_to_mw(self): class GasToGasConversion(Controller): - """ - A controller to be used in a multinet with two gas nets that have different gases. + """A controller to be used in a multinet with two gas nets that have different gases. This controller represents a gas conversion unit (e.g. methanization or steam methane reformer) and couples two pandapipes-gas networks that are stored together in a multinet. @@ -321,9 +315,7 @@ class GasToGasConversion(Controller): def __init__(self, multinet, element_index_from, element_index_to, efficiency, name_gas_net_from='gas1', name_gas_net_to='gas2', in_service=True, order=0, level=0, drop_same_existing_ctrl=False, initial_run=True, name="GasToGasConverter"): - """ - see class docstring - """ + """See class docstring.""" super().__init__( multinet, name, in_service, order, level, drop_same_existing_ctrl=drop_same_existing_ctrl, initial_run=initial_run, @@ -384,8 +376,7 @@ def coupled_p2g_const_control(multinet, element_index_power, element_index_gas, data_source=None, scale_factor=1.0, in_service=True, order=(0, 1), level=0, drop_same_existing_ctrl=False, matching_params=None, initial_run=False, **kwargs): - """ - Creates a ConstController (load values) and a P2G Controller (corresponding gas mass flows). + """Creates a ConstController (load values) and a P2G Controller (corresponding gas mass flows). The ConstController updates load values of a given electric load in accordance to the profile given in the datasource. @@ -466,8 +457,7 @@ def coupled_g2p_const_control(multinet, element_index_power, element_index_gas, power_led=False, in_service=True, order=(0, 1), level=0, drop_same_existing_ctrl=False, matching_params=None, initial_run=False, **kwargs): - """ - Creates a ConstController (gas consumption) and a G2P Controller (corresponding power output). + """Creates a ConstController (gas consumption) and a G2P Controller (corresponding power output). The ConstController updates gas consumption values of a given sink element in accordance to the profile given in the datasource. diff --git a/src/pandapipes/multinet/control/run_control_multinet.py b/src/pandapipes/multinet/control/run_control_multinet.py index e2c7e864a..213913062 100644 --- a/src/pandapipes/multinet/control/run_control_multinet.py +++ b/src/pandapipes/multinet/control/run_control_multinet.py @@ -22,9 +22,7 @@ def _evaluate_multinet(multinet, levelorder, ctrl_variables, **kwargs): - """ - Within a control loop after all controllers applied their their action "_evaluate_multinet" - checks if all nets affectd in one level did converge or not + """Within a control loop after all controllers applied their their action "_evaluate_multinet" checks if all nets affectd in one level did converge or not. :param multinet: multinet with multinet controllers, net distinct controllers and several \ pandapipes/pandapower nets @@ -55,9 +53,7 @@ def _evaluate_multinet(multinet, levelorder, ctrl_variables, **kwargs): def _relevant_nets(multinet, levelorder): - """ - This function determines the relevant nets in each level, i.e. only the nets affected in each - level are investigated and checked. + """Determine the relevant nets in each level, i.e. only the nets affected in each level are investigated and checked. :param multinet: multinet with multinet controllers, net distinct controllers and several \ pandapipes/pandapower nets @@ -87,8 +83,7 @@ def _relevant_nets(multinet, levelorder): def net_initialization_multinet(multinet, ctrl_variables, **kwargs): - """ - If one controller affecting a net requires an initial_run, a loadflow/pipeflow is conducted. + """If one controller affecting a net requires an initial_run, a loadflow/pipeflow is conducted. :param multinet: multinet with multinet controllers, net distinct controllers and several \ pandapipes/pandapower nets @@ -115,8 +110,7 @@ def net_initialization_multinet(multinet, ctrl_variables, **kwargs): def run_control(multinet, ctrl_variables=None, max_iter=30, **kwargs): - """ - Main function to call a multnet with controllers. + """Main function to call a multnet with controllers. Function is running control loops for the controllers specified in net.controller Runs controller until each one converged or max_iter is hit. @@ -170,8 +164,7 @@ def run_control(multinet, ctrl_variables=None, max_iter=30, **kwargs): def get_controller_order_multinet(multinet): - """ - Defining the controller order per level. + """Defining the controller order per level. Takes the order and level columns from net.controller. If levels are specified, the levels and orders are executed in ascending order. @@ -183,7 +176,6 @@ def get_controller_order_multinet(multinet): each level :rtype: list """ - net_list = [] controller_list = [] @@ -238,8 +230,7 @@ def prepare_ctrl_variables_for_net(multinet, net_name, ctrl_variables, **kwargs) def prepare_run_ctrl(multinet, ctrl_variables=None, **kwargs): - """ - Prepares run control functions. + """Prepares run control functions. Internal variables needed: - level (list): gives a list of levels to be investigated @@ -263,7 +254,6 @@ def prepare_run_ctrl(multinet, ctrl_variables=None, **kwargs): :return: adapted ctrl_variables for all nets with all required boundary information :rtype: dict """ - # sort controller_order by order if not already done if ctrl_variables is None: ctrl_variables = {'nets': dict()} diff --git a/src/pandapipes/multinet/create_multinet.py b/src/pandapipes/multinet/create_multinet.py index 1f67182d9..f1d6eec22 100644 --- a/src/pandapipes/multinet/create_multinet.py +++ b/src/pandapipes/multinet/create_multinet.py @@ -15,8 +15,7 @@ def create_empty_multinet(name=""): - """ - This function initializes the multinet datastructure. + """Initializes the multinet datastructure. :param name: Name for the multi net :type name: string, default None @@ -34,8 +33,7 @@ def create_empty_multinet(name=""): def add_net_to_multinet(multinet, net, net_name='power', overwrite=False): - """ - Add a pandapipes or pandapower net to the multinet structure. + """Add a pandapipes or pandapower net to the multinet structure. :param multinet: multinet to which a pandapipes/pandapower net will be added :type multinet: pandapipes.MultiNet @@ -56,8 +54,7 @@ def add_net_to_multinet(multinet, net, net_name='power', overwrite=False): def add_nets_to_multinet(multinet, overwrite=False, **networks): - """ - Add multiple nets to a multinet. 'networks' is one or more keyword arguments with nets. + """Add multiple nets to a multinet. 'networks' is one or more keyword arguments with nets. :param multinet: multinet to which several pandapipes/pandapower nets are added :type multinet: pandapipes.MultiNet diff --git a/src/pandapipes/multinet/multinet.py b/src/pandapipes/multinet/multinet.py index a5574fa94..1723b132f 100644 --- a/src/pandapipes/multinet/multinet.py +++ b/src/pandapipes/multinet/multinet.py @@ -20,8 +20,7 @@ class MultiNet(ADict): - """ - A 'MultiNet' is a frame for different pandapipes & pandapower nets and coupling controllers. + """A 'MultiNet' is a frame for different pandapipes & pandapower nets and coupling controllers. Usually, a multinet is a multi energy net which one net per energy carrier. The coupled simulation can be run with @@ -31,7 +30,7 @@ class MultiNet(ADict): """ def __init__(self, *args, **kwargs): - """ + """Create a MultiNet, optionally as a copy of an existing MultiNet. :param args: item of the ADict :type args: variable @@ -48,13 +47,11 @@ def deepcopy(self): return copy.deepcopy(self) def __repr__(self): # pragma: no cover - """ - defines the representation of the multinet in the console + """Defines the representation of the multinet in the console. :return: representation :rtype: str """ - r = "This multi net includes following nets:" for cat in self.nets: if isinstance(self['nets'][cat], pandapowerNet): @@ -78,8 +75,7 @@ def __repr__(self): # pragma: no cover def get_default_multinet_structure(): - """ - Return the default structure of an empty multinet with categories and data types. + """Return the default structure of an empty multinet with categories and data types. :return: default structure of an empty multinet :rtype: dict diff --git a/src/pandapipes/multinet/timeseries/run_time_series_multinet.py b/src/pandapipes/multinet/timeseries/run_time_series_multinet.py index 9b06a8d96..03bfc6379 100644 --- a/src/pandapipes/multinet/timeseries/run_time_series_multinet.py +++ b/src/pandapipes/multinet/timeseries/run_time_series_multinet.py @@ -22,8 +22,7 @@ def _call_output_writer(multinet, time_step, pf_converged, ctrl_converged, ts_variables): - """ - Calling the output writer routine for each net in multinet. + """Calling the output writer routine for each net in multinet. :param multinet: multinet with multinet controllers, net distinct controllers and several pandapipes/pandapower nets :type multinet: pandapipes.Multinet @@ -46,8 +45,8 @@ def _call_output_writer(multinet, time_step, pf_converged, ctrl_converged, ts_va def init_time_series(multinet, time_steps, continue_on_divergence=False, verbose=True, **kwargs): - """ - Initializes the time series calculation. + """Initializes the time series calculation. + Besides it creates the dict ts_variables, which includes necessary variables for the time series / control loop. :param multinet: multinet with multinet controllers, net distinct controllers and several pandapipes/pandapower nets @@ -102,8 +101,8 @@ def init_time_series(multinet, time_steps, continue_on_divergence=False, verbose def run_timeseries(multinet, time_steps=None, continue_on_divergence=False, verbose=True, **kwargs): - """ - Time Series main function. + """Time Series main function. + Runs multiple run functions for each net in multinet. Within each time step several controller loops are conducted till all controllers and each net is converged. A normal pp.runpp/pps.pipeflow can be optionally replaced by other run functions by setting the run function in diff --git a/src/pandapipes/networks/simple_gas_networks.py b/src/pandapipes/networks/simple_gas_networks.py index 6f84639ff..0f2ed6242 100644 --- a/src/pandapipes/networks/simple_gas_networks.py +++ b/src/pandapipes/networks/simple_gas_networks.py @@ -21,7 +21,7 @@ # -------------- combined networks -------------- def gas_3parallel(method="nikuradse"): - """ + """Load a STANET network with 3 parallel pipes, converted to a pandapipes network. :param method: Which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -38,7 +38,7 @@ def gas_3parallel(method="nikuradse"): def gas_versatility(): - """ + """Load a STANET versatility test network, converted to a pandapipes network. :return: net - STANET network converted to a pandapipes network (method = "prandtl-colebrook") :rtype: pandapipesNet @@ -54,7 +54,7 @@ def gas_versatility(): # -------------- meshed networks -------------- def gas_meshed_delta(): - """ + """Load a meshed, delta-shaped STANET network, converted to a pandapipes network. :return: net - STANET network converted to a pandapipes network (method = "prandtl-colebrook") :rtype: pandapipesNet @@ -68,7 +68,7 @@ def gas_meshed_delta(): def gas_meshed_pumps(): - """ + """Load a meshed STANET network with pumps, converted to a pandapipes network. :return: net - STANET network converted to a pandapipes network (method = "nikuradse") :rtype: pandapipesNet @@ -82,7 +82,7 @@ def gas_meshed_pumps(): def gas_meshed_square(method="nikuradse"): - """ + """Load a meshed, square-shaped STANET network, converted to a pandapipes network. :param method: Which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -99,7 +99,7 @@ def gas_meshed_square(method="nikuradse"): def gas_meshed_two_valves(method="nikuradse"): - """ + """Load a meshed STANET network with two valves, converted to a pandapipes network. :param method: Which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -117,7 +117,7 @@ def gas_meshed_two_valves(method="nikuradse"): # -------------- one pipe -------------- def gas_one_pipe1(method="nikuradse"): - """ + """Load a STANET network with one pipe (variant 1), converted to a pandapipes network. :param method: Which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -134,7 +134,7 @@ def gas_one_pipe1(method="nikuradse"): def gas_one_pipe2(method="nikuradse"): - """ + """Load a STANET network with one pipe (variant 2), converted to a pandapipes network. :param method: Which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -152,7 +152,7 @@ def gas_one_pipe2(method="nikuradse"): # -------------- strand net -------------- def gas_strand_2pipes(method="nikuradse"): - """ + """Load a strand-shaped STANET network with two pipes, converted to a pandapipes network. :param method: Which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -169,7 +169,7 @@ def gas_strand_2pipes(method="nikuradse"): def gas_strand_pump(): - """ + """Load a strand-shaped STANET network with a pump, converted to a pandapipes network. :return: net - STANET network converted to a pandapipes network (method = "nikuradse") :rtype: pandapipesNet @@ -184,7 +184,7 @@ def gas_strand_pump(): # -------------- t_cross -------------- def gas_tcross1(method="nikuradse"): - """ + """Load a T-cross-shaped STANET network (variant 1), converted to a pandapipes network. :param method: If results_from = "stanet", which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -201,7 +201,7 @@ def gas_tcross1(method="nikuradse"): def gas_tcross2(method="nikuradse"): - """ + """Load a T-cross-shaped STANET network (variant 2), converted to a pandapipes network. :param method: If results_from = "stanet", which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -219,7 +219,7 @@ def gas_tcross2(method="nikuradse"): # -------------- two pressure junctions -------------- def gas_2eg_hnet(method="nikuradse"): - """ + """Load an H-shaped STANET network with two pressure junctions, converted to a pandapipes network. :param method: If results_from = "stanet", which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -243,8 +243,7 @@ def schutterwald(include_houses=True, max_length_house_conn_m=None): def schutterwald_gas(include_houses=True, max_length_house_conn_m=None): - """ - Load natural gas distribution network for a town in the MV Oberrhein region (cf. pandapower). + """Load natural gas distribution network for a town in the MV Oberrhein region (cf. pandapower). The default pressure is set to 1 bar. Geodata is provided. Around 1500 houses are connected with theoretical house connection pipes. It is recommended diff --git a/src/pandapipes/networks/simple_heat_transfer_networks.py b/src/pandapipes/networks/simple_heat_transfer_networks.py index 0057af78b..8f94481e2 100644 --- a/src/pandapipes/networks/simple_heat_transfer_networks.py +++ b/src/pandapipes/networks/simple_heat_transfer_networks.py @@ -19,7 +19,7 @@ "openmodelica_test_networks", "heat_transfer_cases") def heat_transfer_delta(): - """ + """Load a delta-shaped OpenModelica heat transfer test network, converted to a pandapipes network. :return: net - OpenModelica network converted to a pandapipes network :rtype: pandapipesNet @@ -32,7 +32,7 @@ def heat_transfer_delta(): def heat_transfer_delta_2sinks(): - """ + """Load a delta-shaped OpenModelica heat transfer test network with two sinks, converted to a pandapipes network. :return: net - OpenModelica network converted to a pandapipes network :rtype: pandapipesNet @@ -45,7 +45,7 @@ def heat_transfer_delta_2sinks(): def heat_transfer_heights(): - """ + """Load an OpenModelica heat transfer test network with varying heights, converted to a pandapipes network. :return: net - OpenModelica network converted to a pandapipes network :rtype: pandapipesNet @@ -58,7 +58,7 @@ def heat_transfer_heights(): def heat_transfer_one_pipe(): - """ + """Load an OpenModelica heat transfer test network with one pipe, converted to a pandapipes network. :return: net - OpenModelica network converted to a pandapipes network :rtype: pandapipesNet @@ -71,7 +71,7 @@ def heat_transfer_one_pipe(): def heat_transfer_one_source(): - """ + """Load an OpenModelica heat transfer test network with one source, converted to a pandapipes network. :return: net - OpenModelica network converted to a pandapipes network :rtype: pandapipesNet @@ -84,7 +84,7 @@ def heat_transfer_one_source(): def heat_transfer_section_variation(): - """ + """Load an OpenModelica heat transfer test network with pipe section variation, converted to a pandapipes network. :return: net - OpenModelica network converted to a pandapipes network :rtype: pandapipesNet @@ -97,7 +97,7 @@ def heat_transfer_section_variation(): def heat_transfer_t_cross(): - """ + """Load a T-cross-shaped OpenModelica heat transfer test network, converted to a pandapipes network. :return: net - OpenModelica network converted to a pandapipes network :rtype: pandapipesNet @@ -110,7 +110,7 @@ def heat_transfer_t_cross(): def heat_transfer_two_pipes(): - """ + """Load an OpenModelica heat transfer test network with two pipes, converted to a pandapipes network. :return: net - OpenModelica network converted to a pandapipes network :rtype: pandapipesNet @@ -123,8 +123,7 @@ def heat_transfer_two_pipes(): def schutterwald_heat(tflow_degC=70, treturn_degC=None, u_w_per_m2k=1.): - """ - Load heat distribution network for a town in the MV Oberrhein region (cf. pandapower). + """Load heat distribution network for a town in the MV Oberrhein region (cf. pandapower). This network is derived from the gas distribution network given by `schutterwald_gas` diff --git a/src/pandapipes/networks/simple_water_networks.py b/src/pandapipes/networks/simple_water_networks.py index cda3cca8b..3e97512d4 100644 --- a/src/pandapipes/networks/simple_water_networks.py +++ b/src/pandapipes/networks/simple_water_networks.py @@ -24,7 +24,7 @@ # -------------- combined networks -------------- def water_district_grid(method="nikuradse"): - """ + """Load a STANET district test network, converted to a pandapipes network. :param method: Which results should be loaded: nikuradse or prandtl-colebrook :type method: str, default "nikuradse" @@ -41,7 +41,7 @@ def water_district_grid(method="nikuradse"): def water_combined_mixed(method="colebrook"): - """ + """Load an OpenModelica water network with a mixed set of components, converted to a pandapipes network. :param method: Which results should be loaded: prandtl-colebrook or swamee-jain :type method: str, default "colebrook" @@ -59,7 +59,7 @@ def water_combined_mixed(method="colebrook"): def water_combined_versatility(results_from="openmodelica", method="colebrook"): - """ + """Load a versatility test network from STANET or OpenModelica, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -84,7 +84,7 @@ def water_combined_versatility(results_from="openmodelica", method="colebrook"): # -------------- meshed networks -------------- def water_meshed_delta(results_from="openmodelica", method="colebrook"): - """ + """Load a meshed, delta-shaped water test network, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -109,7 +109,7 @@ def water_meshed_delta(results_from="openmodelica", method="colebrook"): def water_meshed_pumps(results_from="openmodelica", method="colebrook"): - """ + """Load a meshed water test network with pumps, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -133,8 +133,8 @@ def water_meshed_pumps(results_from="openmodelica", method="colebrook"): return from_json(os.path.join(water_modelica_swamee_path, "meshed_networks", "pumps.json")) def water_meshed_heights(method="colebrook"): + """Load a meshed OpenModelica water test network with varying heights, converted to a pandapipes network. - """ :param method: which results should be loaded: prandtl-colebrook or swamee-jain :type method: str, default "colebrook" :return: net - OpenModelica network converted to a pandapipes network @@ -151,7 +151,7 @@ def water_meshed_heights(method="colebrook"): def water_meshed_2valves(results_from="openmodelica", method="colebrook"): - """ + """Load a meshed water test network with two valves, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -177,7 +177,7 @@ def water_meshed_2valves(results_from="openmodelica", method="colebrook"): # -------------- one pipe -------------- def water_one_pipe1(results_from="openmodelica", method="colebrook"): - """ + """Load a water test network with one pipe (variant 1), converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -202,7 +202,7 @@ def water_one_pipe1(results_from="openmodelica", method="colebrook"): def water_one_pipe2(results_from="openmodelica", method="colebrook"): - """ + """Load a water test network with one pipe (variant 2), converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -227,7 +227,7 @@ def water_one_pipe2(results_from="openmodelica", method="colebrook"): def water_one_pipe3(results_from="openmodelica", method="colebrook"): - """ + """Load a water test network with one pipe (variant 3), converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -253,7 +253,7 @@ def water_one_pipe3(results_from="openmodelica", method="colebrook"): # -------------- strand net -------------- def water_simple_strand_net( results_from="openmodelica", method="colebrook"): - """ + """Load a simple, strand-shaped water test network, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -278,7 +278,7 @@ def water_simple_strand_net( results_from="openmodelica", method="colebrook"): def water_strand_2pipes(results_from="openmodelica", method="colebrook"): - """ + """Load a strand-shaped water test network with two pipes, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -303,7 +303,7 @@ def water_strand_2pipes(results_from="openmodelica", method="colebrook"): def water_strand_cross(results_from="openmodelica", method="colebrook"): - """ + """Load a strand-shaped water test network with a cross junction, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -327,7 +327,8 @@ def water_strand_cross(results_from="openmodelica", method="colebrook"): def water_strand_net_2pumps(method="colebrook"): - """ + """Load a strand-shaped OpenModelica water test network with two pumps, converted to a pandapipes network. + :param method: Which results should be loaded: prandtl-colebrook or swamee-jain :type method: str, default "colebrook" :return: net - OpenModelica network converted to a pandapipes network @@ -345,7 +346,7 @@ def water_strand_net_2pumps(method="colebrook"): def water_strand_pump(): - """ + """Load a strand-shaped STANET water test network with a pump, converted to a pandapipes network. :return: net - STANET network converted to a pandapipes network (method = "nikuradse") :rtype: pandapipesNet @@ -360,7 +361,7 @@ def water_strand_pump(): # -------------- t_cross -------------- def water_tcross(results_from="openmodelica", method="colebrook"): - """ + """Load a T-cross-shaped water test network, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" @@ -385,7 +386,8 @@ def water_tcross(results_from="openmodelica", method="colebrook"): def water_tcross_valves(method="colebrook"): - """ + """Load a T-cross-shaped OpenModelica water test network with valves, converted to a pandapipes network. + :param method: Which results should be loaded: prandtl-colebrook or swamee-jain :type method: str, default "colebrook" :return: net - OpenModelica network converted to a pandapipes network @@ -404,7 +406,7 @@ def water_tcross_valves(method="colebrook"): # -------------- two pressure junctions -------------- def water_2eg_two_pipes(results_from="openmodelica", method="colebrook"): - """ + """Load a water test network with two pressure junctions and two pipes, converted to a pandapipes network. :param results_from: Which converted net should be loaded: openmodelica or stanet :type results_from: str, default "openmodelica" diff --git a/src/pandapipes/pandapipes_net.py b/src/pandapipes/pandapipes_net.py index 152497056..9342386da 100644 --- a/src/pandapipes/pandapipes_net.py +++ b/src/pandapipes/pandapipes_net.py @@ -60,6 +60,7 @@ class Sector(StrEnum): class pandapipesNet(ADict): def __init__(self, *args, **kwargs): + """Initialize the net, deep-copying data from *args[0]* if it is already a pandapipesNet.""" super().__init__(*args, **kwargs) if isinstance(args[0], self.__class__): net = args[0] @@ -70,6 +71,7 @@ def deepcopy(self): return copy.deepcopy(self) def __repr__(self): # pragma: no cover + """Return a human-readable summary of the net's non-empty tables, fluid, and components.""" r = "This pandapipes network includes the following parameter tables:" par = [] res = [] diff --git a/src/pandapipes/pf/__init__.py b/src/pandapipes/pf/__init__.py index 687433f8f..43c373b68 100644 --- a/src/pandapipes/pf/__init__.py +++ b/src/pandapipes/pf/__init__.py @@ -3,6 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -from pandapipes.pf.build_system_matrix import * from pandapipes.pf.internals_toolbox import * from pandapipes.pf.pipeflow_setup import * diff --git a/src/pandapipes/pf/build_system_matrix.py b/src/pandapipes/pf/build_system_matrix.py deleted file mode 100644 index efec0fb8e..000000000 --- a/src/pandapipes/pf/build_system_matrix.py +++ /dev/null @@ -1,272 +0,0 @@ -# Copyright (c) 2020-2026 by Fraunhofer Institute for Energy Economics -# and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. - -import numpy as np -from scipy.sparse import csr_matrix - -from pandapipes.idx_branch import (FROM_NODE, TO_NODE, JAC_DERIV_DM, JAC_DERIV_DP, JAC_DERIV_DP1, \ - JAC_DERIV_DM_NODE, LOAD_VEC_NODES_FROM, LOAD_VEC_NODES_TO, LOAD_VEC_BRANCHES, JAC_DERIV_DT, JAC_DERIV_DTOUT, - PC as PC_BRANCH, JAC_DERIV_DTOUT_NODE, JAC_DERIV_DT_NODE, LOAD_VEC_NODES_TO_T, - LOAD_VEC_BRANCHES_T, BRANCH_TYPE) - -from pandapipes.idx_node import (P, PC as PC_NODE, NODE_TYPE, T, NODE_TYPE_T, LOAD, LOAD_T, INFEED, - MDOTSLACKINIT, JAC_DERIV_MSL, JAC_DERIV_DT_N) -from pandapipes.pf.internals_toolbox import _sum_by_group_sorted, _sum_by_group, \ - get_from_nodes_corrected, get_to_nodes_corrected -from pandapipes.pf.pipeflow_setup import get_net_option - - -def build_system_matrix(net, branch_pit, node_pit, heat_mode): - """ - Builds the system matrix. - - :param net: The pandapipes network - :type net: pandapipesNet - :param branch_pit: pandapipes internal table for branching components such as pipes or valves - :type branch_pit: numpy.ndarray - :param node_pit: pandapipes internal table for node components - :type node_pit: numpy.ndarray - :param heat_mode: Is it a heat network calculation: True or False - :type heat_mode: bool - :return: system_matrix, load_vector - :rtype: system_matrix - scipy.sparse.csr.csr_matrix, load_vector - numpy.ndarray - """ - update_option = get_net_option(net, "only_update_hydraulic_matrix") - update_only = update_option and "hydraulic_data_sorting" in net["_internal_data"] \ - and "hydraulic_matrix" in net["_internal_data"] - use_numba = get_net_option(net, "use_numba") - - len_b = len(branch_pit) - len_n = len(node_pit) - branch_matrix_indices = np.arange(len_b) + len_n - ntyp_col, slack_type, pcn_type, pcb_type, branch_type, num_der = \ - (NODE_TYPE, P, PC_NODE, PC_BRANCH, BRANCH_TYPE, 3) \ - if not heat_mode else (NODE_TYPE_T, T, None, None, BRANCH_TYPE, 2) - pc_nodes = np.where(node_pit[:, ntyp_col] == pcn_type)[0] - - if not heat_mode: - fn = branch_pit[:, FROM_NODE].astype(np.int32) - tn = branch_pit[:, TO_NODE].astype(np.int32) - else: - fn = get_from_nodes_corrected(branch_pit) - tn = get_to_nodes_corrected(branch_pit) - pc_branch_mask = branch_pit[:, branch_type] == pcb_type - slack_nodes = np.where(node_pit[:, ntyp_col] == slack_type)[0] - pc_matrix_indices = branch_matrix_indices[pc_branch_mask] - - # size of the matrix - if not heat_mode: - len_sl = len(slack_nodes) - slack_mass_matrix_indices = np.arange(len_sl) + len_b + len_n - slack_masses_from, slack_branches_from = np.where(branch_pit[:, FROM_NODE] == slack_nodes[:, None]) - slack_masses_to, slack_branches_to = np.where(branch_pit[:, TO_NODE] == slack_nodes[:, None]) - not_slack_fn_branch_mask = node_pit[fn, ntyp_col] != slack_type - not_slack_tn_branch_mask = node_pit[tn, ntyp_col] != slack_type - len_fn_not_slack = np.sum(not_slack_fn_branch_mask) - len_tn_not_slack = np.sum(not_slack_tn_branch_mask) - len_fn1 = num_der * len_b + len_fn_not_slack - len_tn1 = len_fn1 + len_tn_not_slack - len_pc = len_tn1 + pc_nodes.shape[0] - len_slack = len_pc + slack_nodes.shape[0] - len_fsb = len_slack + len(slack_branches_from) - len_tsb = len_fsb + len(slack_branches_to) - full_len = len_tsb + slack_nodes.shape[0] - else: - len_sl = 0 - not_slack_tn_branch_mask = ~node_pit[tn, INFEED].astype(np.bool_) - not_slack_mask = ~node_pit[:, INFEED].astype(np.bool_) - len_tn_not_slack = np.sum(not_slack_tn_branch_mask) - len_not_slack = np.sum(not_slack_mask) - infeed_node = np.arange(len_n)[node_pit[:, INFEED].astype(np.bool_)] - len_tn = num_der * len_b + len_tn_not_slack - len_tout = len_tn + len_tn_not_slack - len_nt = len_tout + len_not_slack - full_len = len_nt + slack_nodes.shape[0] - - system_data = np.zeros(full_len, dtype=np.float64) - - # entries in the matrix - if not heat_mode: - - # branch equations - # ---------------- - # branch_dF_dm - system_data[:len_b] = branch_pit[:, JAC_DERIV_DM] - # branch_dF_dp_from - system_data[len_b:2 * len_b] = branch_pit[:, JAC_DERIV_DP] - # branch_dF_dp_to - system_data[2 * len_b:3 * len_b] = branch_pit[:, JAC_DERIV_DP1] - - # node equations - # -------------- - # from_node_dF_dm - system_data[3 * len_b:len_fn1] = branch_pit[not_slack_fn_branch_mask, JAC_DERIV_DM_NODE] * (-1) - # to_node_dF_dm - system_data[len_fn1:len_tn1] = branch_pit[not_slack_tn_branch_mask, JAC_DERIV_DM_NODE] - - # fixed pressure equations - # ------------------------ - # pc_nodes and slack_nodes - system_data[len_tn1:len_slack] = 1 - - # mass flow slack equation - # -------------- - # from_slack_dF_dm - system_data[len_slack:len_fsb] = branch_pit[slack_branches_from, JAC_DERIV_DM_NODE] * (-1) - # to_slack_dF_dm - system_data[len_fsb:len_tsb] = branch_pit[slack_branches_to, JAC_DERIV_DM_NODE] - # slackmass_dF_dmslack - system_data[len_tsb:] = node_pit[slack_nodes, JAC_DERIV_MSL] - else: - - # branch equations - # ---------------- - # branch_dF_dT_from - system_data[:len_b] = branch_pit[:, JAC_DERIV_DT] - # branch_dF_dT_out - system_data[len_b:2 * len_b] = branch_pit[:, JAC_DERIV_DTOUT] - - # node equations - # -------------- - # node_dF_dT_to - system_data[2 * len_b:len_tn] = branch_pit[not_slack_tn_branch_mask, JAC_DERIV_DT_NODE] - # node_dF_dT_out - system_data[len_tn:len_tout] = branch_pit[not_slack_tn_branch_mask, JAC_DERIV_DTOUT_NODE] - # node_dF_dT - system_data[len_tout:len_nt] = node_pit[not_slack_mask, JAC_DERIV_DT_N] - # fixed temperature equations - # --------------------------- - # t_nodes - system_data[len_nt:] = 1 - - # position in the matrix - if not update_only: - system_cols = np.zeros(full_len, dtype=np.int32) - system_rows = np.zeros(full_len, dtype=np.int32) - - if not heat_mode: - - # branch equations - # ---------------- - # branch_dF_dm - system_cols[:len_b] = branch_matrix_indices - system_rows[:len_b] = branch_matrix_indices - # branch_dF_dp_from - system_cols[len_b:2 * len_b] = fn - system_rows[len_b:2 * len_b] = branch_matrix_indices - # branch_dF_dp_to - system_cols[2 * len_b:3 * len_b] = tn - system_rows[2 * len_b:3 * len_b] = branch_matrix_indices - - # node equations - # -------------- - # from_node_dF_dm - system_cols[3 * len_b:len_fn1] = branch_matrix_indices[not_slack_fn_branch_mask] - system_rows[3 * len_b:len_fn1] = fn[not_slack_fn_branch_mask] - # to_node_dF_dm - system_cols[len_fn1:len_tn1] = branch_matrix_indices[not_slack_tn_branch_mask] - system_rows[len_fn1:len_tn1] = tn[not_slack_tn_branch_mask] - - # fixed pressure equations - # ----------------------- - # pc_nodes - system_cols[len_tn1:len_pc] = pc_nodes - system_rows[len_tn1:len_pc] = pc_matrix_indices - # slack_nodes - system_cols[len_pc:len_slack] = slack_nodes - system_rows[len_pc:len_slack] = slack_nodes - - # mass flow slack equation - # -------------- - # from_slack_dF_dm - system_cols[len_slack:len_fsb] = branch_matrix_indices[slack_branches_from] - system_rows[len_slack:len_fsb] = slack_mass_matrix_indices[slack_masses_from] - # to_slack_dF_dm - system_cols[len_fsb:len_tsb] = branch_matrix_indices[slack_branches_to] - system_rows[len_fsb:len_tsb] = slack_mass_matrix_indices[slack_masses_to] - # slackmass_dF_dmslack - system_cols[len_tsb:] = slack_mass_matrix_indices - system_rows[len_tsb:] = slack_mass_matrix_indices - - else: - # branch equations - # ---------------- - # branch_dF_dT_from - system_cols[:len_b] = fn - system_rows[:len_b] = branch_matrix_indices - # branch_dF_dT_out - system_cols[len_b:2 * len_b] = branch_matrix_indices - system_rows[len_b:2 * len_b] = branch_matrix_indices - - # node equations - # -------------- - # node_dF_dT_to - system_cols[2 * len_b:len_tn] = tn[not_slack_tn_branch_mask] - system_rows[2 * len_b:len_tn] = tn[not_slack_tn_branch_mask] - # node_dF_dT_out - system_cols[len_tn:len_tout] = branch_matrix_indices[not_slack_tn_branch_mask] - system_rows[len_tn:len_tout] = tn[not_slack_tn_branch_mask] - # node_dF_dT - system_cols[len_tout:len_nt] = np.arange(len_n)[not_slack_mask] - system_rows[len_tout:len_nt] = np.arange(len_n)[not_slack_mask] - - # fixed temperature equations - # --------------------------- - # t_nodes (overwrites only infeeding nodes' equation) - system_cols[len_nt:] = slack_nodes - system_rows[len_nt:] = infeed_node - - if not update_option: - system_matrix = csr_matrix((system_data, (system_rows, system_cols)), - shape=(len_n + len_b + len_sl, len_n + len_b + len_sl)) - - else: - data_order = np.lexsort([system_cols, system_rows]) - system_data = system_data[data_order] - system_cols = system_cols[data_order] - system_rows = system_rows[data_order] - - row_counter = np.zeros(len_b + len_n + len_sl + 1, dtype=np.int32) - unique_rows, row_counts = _sum_by_group_sorted(system_rows, np.ones_like(system_rows)) - row_counter[unique_rows + 1] += row_counts - ptr = row_counter.cumsum() - system_matrix = csr_matrix((system_data, system_cols, ptr), - shape=(len_n + len_b + len_sl, len_n + len_b + len_sl)) - net["_internal_data"]["hydraulic_data_sorting"] = data_order - net["_internal_data"]["hydraulic_matrix"] = system_matrix - else: - data_order = net["_internal_data"]["hydraulic_data_sorting"] - system_data = system_data[data_order] - system_matrix = net["_internal_data"]["hydraulic_matrix"] - system_matrix.data = system_data - - # load vector on the right side - if not heat_mode: - load_vector = np.empty(len_n + len_b + len_sl) - load_vector[len_n:len_b + len_n] = branch_pit[:, LOAD_VEC_BRANCHES] - load_vector[:len_n] = node_pit[:, LOAD] * (-1) - fn_unique, fn_sums = _sum_by_group(use_numba, fn, branch_pit[:, LOAD_VEC_NODES_FROM]) - tn_unique, tn_sums = _sum_by_group(use_numba, tn, branch_pit[:, LOAD_VEC_NODES_TO]) - load_vector[fn_unique] -= fn_sums - load_vector[tn_unique] += tn_sums - load_vector[slack_nodes] = 0 - load_vector[pc_matrix_indices] = 0 - - load_vector[slack_mass_matrix_indices] = node_pit[slack_nodes, LOAD] * (-1) - fsb_unique, fsb_sums = _sum_by_group(use_numba, slack_masses_from, - branch_pit[slack_branches_from, LOAD_VEC_NODES_FROM]) - tsb_unique, tsb_sums = _sum_by_group(use_numba, slack_masses_to, - branch_pit[slack_branches_to, LOAD_VEC_NODES_TO]) - load_vector[slack_mass_matrix_indices[fsb_unique]] -= fsb_sums - load_vector[slack_mass_matrix_indices[tsb_unique]] += tsb_sums - load_vector[slack_mass_matrix_indices] -= node_pit[slack_nodes, MDOTSLACKINIT] - else: - load_vector = np.zeros(len_n + len_b) - load_vector[len_n:] = branch_pit[:, LOAD_VEC_BRANCHES_T] - load_vector[:len_n] = node_pit[:, LOAD_T] * (-1) - tn_unique, tn_sums = _sum_by_group(use_numba, tn, branch_pit[:, LOAD_VEC_NODES_TO_T]) - load_vector[tn_unique] += tn_sums - load_vector[infeed_node] = 0 - - return system_matrix, load_vector diff --git a/src/pandapipes/pf/calculation.py b/src/pandapipes/pf/calculation.py new file mode 100644 index 000000000..ec09e9ec0 --- /dev/null +++ b/src/pandapipes/pf/calculation.py @@ -0,0 +1,485 @@ +# Copyright (c) 2020-2026 by Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +import numpy as np +from scipy.sparse import csr_matrix +from scipy.sparse.linalg import spsolve + +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.system_index import HydraulicSystemIndex, HeatSystemIndex, ComponentRegistry +from pandapipes.pf.pipeflow_setup import ( + get_net_options, get_net_option, get_lookup, reduce_pit, create_internal_results, + identify_active_nodes_branches, hydraulic_slack_mask, heat_transfer_slack_mask, set_net_option, + check_infeed_number, compute_infeed_nodes, write_internal_results, PipeflowNotConverged +) +from pandapipes.pf.result_extraction import ( + extract_results_active_pit_hydraulics, extract_results_active_pit_heat_transfer +) +try: + import pandaplan.core.pplog as logging +except ImportError: + import logging + +logger = logging.getLogger(__name__) + + + +def execute_hydraulics(net): + calc = HydraulicCalculation() + calc.run(net) + if net.converged: + calc.on_converged(net) + calc.rerun(net) + if not net.converged: + calc.handle_non_convergence() + calc.extract_results(net) + + +def execute_heat(net): + calc = ThermalCalculation() + calc.run(net) + if net.converged: + calc.rerun(net) + if not net.converged: + calc.handle_non_convergence() + calc.extract_results(net) + + +def execute_bidirectional(net): + calc = BidirectionalCalculation() + calc.run(net) + if not net.converged: + calc.handle_non_convergence() + calc.extract_results(net) + + +class Calculation: + """Base class for one Newton-Raphson nonlinear solve (hydraulics, heat transfer, bidirectional). + + A subclass declares, as class attributes, what used to be passed around as parallel + lists (``solver_vars``/``tols``/``pit_names``/``iter_name``) and implements + :meth:`solve_step` to perform one linearized assemble-and-solve pass. + + Newton-Raphson iteration recap: + 1. Build the Jacobian df/dx at the current guess x + 2. Solve J @ dx = -f(x) (here: spsolve) + 3. Update x -= dx * alpha, repeat until the residual/variable changes fall below tol + + Class attributes to set on subclasses + -------------------------------------- + MODE : str identifies this calculation in logs/internal results (e.g. "hydraulics") + ITER : str net option name holding the max-iteration count (e.g. "max_iter_hyd") + VARS : list[str] names of the variables tracked for convergence (e.g. ["mdot", "p"]) + TOLS : list[str] net option names holding the tolerance for each VARS entry + PITS : list[str] which pit ("branch"/"node") each VARS entry lives in + COLS : list[int] PIT column index for each VARS entry (used for damping-fallback writes) + """ + + MODE = None + ITER = None + VARS = [] + TOLS = [] + PITS = [] + COLS = [] + + def handle_non_convergence(self): + raise PipeflowNotConverged("The calculation did not converge to a solution.") + + def prepare(self, net): + """One-time setup run before the Newton-Raphson loop starts. + + E.g. connectivity identification, pit reduction. Default: no-op. + """ + + def on_converged(self, net): + """Hook run once, immediately after a successful Newton-Raphson solve. Default: no-op.""" + + def rerun(self, net): + """Hook run after a successful solve to let components request a full rerun. + + E.g. a pressure control adjusting its target. Default: no-op. + """ + + def extract_results(self, net): + """Write the converged results from "_active_pit" back into the general pit structure.""" + raise NotImplementedError + + def solve_step(self, net): + """Perform one linearized solve (assemble Jacobian, spsolve, update pit values). + + :param net: the pandapipesNet to solve on + :return: (results, residual, filtered) where results is a flat list of + [var1_new, var1_old, var2_new, var2_old, ...] (one pair per solver_var, + in the same order as ``solver_vars``), residual is the raw load-vector + residual, and filtered contains a row-index array (or None) per solver_var + selecting which pit rows that var's damping-fallback should write back to. + """ + raise NotImplementedError + + def tols(self, net): + return list(get_net_options(net, *self.TOLS)) + + def run(self, net): + """Run the Newton-Raphson loop until convergence or ITER's max iterations.""" + net.converged = False + self.prepare(net) + max_iter, nonlinear_method, tol_res = get_net_options( + net, self.ITER, "nonlinear_method", "tol_res" + ) + tols = self.tols(net) + niter = 0 + errors = {var: [] for var in self.VARS} + create_internal_results(net) + residual_norm = None + + while not net.converged and niter < max_iter: + logger.debug("niter %d", niter) + results, residual, filtered = self.solve_step(net) + residual_norm = np.max(np.abs(residual)) + logger.debug("residual: %s", residual_norm.round(4)) + + results = np.array(results, object) + pos = np.arange(len(self.VARS) * 2) + vals_new = results[pos[::2]] + vals_old = results[pos[1::2]] + for var, val_new, val_old in zip(self.VARS, vals_new, vals_old): + dval = val_new - val_old + errors[var].append(np.max(np.abs(dval)) if len(dval) else 0) + + self._finalize_iteration(net, niter, residual_norm, nonlinear_method, errors, tols, + tol_res, vals_old, filtered) + niter += 1 + + write_internal_results(net, **errors) + kwargs = { + f'residual_norm_{self.MODE}': residual_norm, + f'iterations_{self.MODE}': niter, + } + write_internal_results(net, **kwargs) + self._log_final_results(net, niter, residual_norm, tols) + + def _finalize_iteration(self, net, niter, residual_norm, nonlinear_method, errors, tols, tol_res, + vals_old, filtered): + if nonlinear_method == "automatic": + errors_increased = set_damping_factor(net, niter, errors) + logger.debug("alpha: %s", get_net_option(net, "alpha")) + for error_increased, val, pit, col, f in zip( + errors_increased, vals_old, self.PITS, self.COLS, filtered + ): + if error_increased: + if f is None: + # todo: not working in bidirectional mode as bidirectional is not + # distinguishing between hydraulics and heat transfer active pit + net["_active_pit"][pit][:, col] = val + else: + net["_active_pit"][pit][f, col] = val + if get_net_option(net, "alpha") != 1: + net.converged = False + return + elif nonlinear_method != "constant": + logger.warning("No proper nonlinear method chosen. Using constant settings.") + converged = True + for var, error, tol in zip(self.VARS, errors.values(), tols): + converged = error[niter] <= tol + if not converged: + break + logger.debug("error_%s: %s", var, error[niter]) + net.converged = converged and residual_norm <= tol_res + + def _log_final_results(self, net, niter, residual_norm, tols): + logger.debug("--------------------------------------------------------------------------------") + if not net.converged: + logger.debug( + "Maximum number of iterations reached but %s solver did not converge.", self.MODE) + logger.debug("Norm of residual: %s", residual_norm) + else: + logger.debug("Calculation completed. Preparing results...") + logger.debug("Converged after %d iterations.", niter) + logger.debug("Norm of residual: %s", residual_norm) + for var, tol in zip(self.VARS, tols): + logger.debug("tolerance for %s: %s", var, tol) + + +class HydraulicCalculation(Calculation): + """Newton-Raphson solve for pressure/mdot (see :func:`solve_hydraulics`).""" + + MODE = 'hydraulics' + ITER = 'max_iter_hyd' + VARS = ['mdot', 'p', 'mdotslack'] + TOLS = ['tol_m', 'tol_p', 'tol_m'] + PITS = ['branch', 'node', 'node'] + COLS = [IdxBranch.MDOTINIT, IdxNode.PINIT, IdxNode.MDOTSLACKINIT] + + def handle_non_convergence(self): + raise PipeflowNotConverged("The hydraulic calculation did not converge to a solution.") + + def prepare(self, net): + net["_lookups"]["node_active_hydraulics"], net["_lookups"]["branch_active_hydraulics"] = \ + identify_active_nodes_branches(net, hydraulic_slack_mask(net)) + reduce_pit(net, "hydraulics") + + def solve_step(self, net): + return solve_hydraulics(net) + + def rerun(self, net): + rerun = False + options = net["_options"] + branch_pit = net["_active_pit"]["branch"] + node_pit = net["_active_pit"]["node"] + branch_lookups = get_lookup(net, "branch", "from_to_active_hydraulics") + for comp in net['component_list']: + rerun |= comp.rerun_hydraulics(net, branch_pit, node_pit, branch_lookups, options) + if rerun: + extract_results_active_pit_hydraulics(net) + execute_hydraulics(net) + + def extract_results(self, net): + extract_results_active_pit_hydraulics(net) + + +class ThermalCalculation(Calculation): + """Newton-Raphson solve for branch outlet / node temperature (see :func:`solve_temperature`).""" + + MODE = 'heat' + ITER = 'max_iter_therm' + VARS = ['Tout', 'T'] + TOLS = ['tol_T', 'tol_T'] + PITS = ['branch', 'node'] + COLS = [IdxBranch.TOUTINIT, IdxNode.TINIT] + + def handle_non_convergence(self): + raise PipeflowNotConverged("The heat transfer calculation did not converge to a solution.") + + def prepare(self, net): + # heat transfer only makes physical sense on branches that are also hydraulically active + # (a temperature slack always needs a reachable pressure slack to actually move the fluid) - + # narrow down the hydraulic connectivity further via the heat-transfer slacks. If hydraulics + # hasn't run yet in this pipeflow() call (standalone mode='heat'), compute it once here. + if "node_active_hydraulics" not in net["_lookups"]: + net["_lookups"]["node_active_hydraulics"], net["_lookups"]["branch_active_hydraulics"] = \ + identify_active_nodes_branches(net, hydraulic_slack_mask(net)) + nodes_hyd = net["_lookups"]["node_active_hydraulics"] + branches_hyd = net["_lookups"]["branch_active_hydraulics"] + + net["_lookups"]["node_active_heat_transfer"], net["_lookups"]["branch_active_heat_transfer"] = \ + identify_active_nodes_branches(net, heat_transfer_slack_mask(net), nodes_hyd, branches_hyd) + reduce_pit(net, "heat_transfer") + + def solve_step(self, net): + return solve_temperature(net) + + def rerun(self, net): + rerun = False + options = net["_options"] + branch_pit = net["_active_pit"]["branch"] + node_pit = net["_active_pit"]["node"] + branch_lookups = get_lookup(net, "branch", "from_to_active_heat_transfer") + for comp in net['component_list']: + rerun |= comp.rerun_hydraulics(net, branch_pit, node_pit, branch_lookups, options) + if rerun: + extract_results_active_pit_heat_transfer(net) + execute_heat(net) + + def extract_results(self, net): + extract_results_active_pit_heat_transfer(net) + + +class BidirectionalCalculation(Calculation): + """Newton-Raphson solve alternating hydraulics and heat transfer (see :func:`solve_bidirectional`).""" + + MODE = 'bidirectional' + ITER = 'max_iter_bidirect' + # solve_bidirectional() concatenates solve_hydraulics()'s 3 pairs (mdot, p, mdotslack) with + # solve_temperature()'s 2 pairs (Tout, T) - VARS/TOLS/PITS/COLS/filtered must list all 5 in + # that same order (this is exactly HydraulicCalculation's VARS/TOLS/PITS/COLS followed by + # ThermalCalculation's), or Calculation.run()'s positional un-interleaving + # (results[0::2]/results[1::2]) pairs each value array with the wrong variable name/pit/col - + # a previous version listed only 4 entries (['mdot', 'p', 'TOUT', 'T']), which silently + # shifted every entry from 'mdotslack' onward: 'TOUT' was actually paired with mdotslack's + # values/branch pit/TOUTINIT col, 'T' was paired with Tout's values, and the real T pair was + # dropped entirely (never damped, never convergence-checked). With + # nonlinear_method="automatic", the mismatched (branch pit, slack_nodes) combination for the + # 'TOUT' slot could then write mdotslack's node-indexed damping-fallback values into the + # branch pit at those same (node-range) row indices, raising IndexError once a slack node's + # index exceeded the branch pit's row count. + VARS = ['mdot', 'p', 'mdotslack', 'Tout', 'T'] + TOLS = ['tol_m', 'tol_p', 'tol_m', 'tol_T', 'tol_T'] + PITS = ['branch', 'node', 'node', 'branch', 'node'] + COLS = [IdxBranch.MDOTINIT, IdxNode.PINIT, IdxNode.MDOTSLACKINIT, IdxBranch.TOUTINIT, + IdxNode.TINIT] + + def handle_non_convergence(self): + raise PipeflowNotConverged("The bidrectional calculation did not converge to a solution.") + + def prepare(self, net): + # hydraulics and heat transfer each get their own, independent connectivity check, done + # once here rather than (for heat) recomputed on every solve_bidirectional() iteration + net["_lookups"]["node_active_hydraulics"], net["_lookups"]["branch_active_hydraulics"] = \ + identify_active_nodes_branches(net, hydraulic_slack_mask(net)) + net["_lookups"]["node_active_heat_transfer"], net["_lookups"]["branch_active_heat_transfer"] = \ + identify_active_nodes_branches(net, heat_transfer_slack_mask(net)) + + def solve_step(self, net): + return solve_bidirectional(net) + + def extract_results(self, net): + pass # solve_bidirectional() already extracts both results every iteration + +def solve_bidirectional(net): + reduce_pit(net, "hydraulics") + res_hyd, residual_hyd, filter_hyd = solve_hydraulics(net) + extract_results_active_pit_hydraulics(net) + + reduce_pit(net, "heat_transfer") + res_heat, residual_heat, filter_heat = solve_temperature(net) + extract_results_active_pit_heat_transfer(net) + + residual = np.concatenate([residual_hyd, residual_heat]) + res = res_hyd + res_heat + filtered = filter_hyd + filter_heat + return res, residual, filtered + +def solve_hydraulics(net): + """Create and solve the linearized system of equations to calculate hydraulic magnitudes. + + Builds a jacobian (scipy sparse matrix) and load vector (numpy array) to calculate + pressure and velocity for the network nodes and branches. + + :param net: The pandapipesNet for which to solve the hydraulic matrix + :type net: pandapipesNet + :return: (results, residual, filtered) - see Calculation.solve_step for the exact shape + """ + options = net["_options"] + + connected_restarted = True + while connected_restarted: + branch_pit = net["_active_pit"]["branch"] + node_pit = net["_active_pit"]["node"] + connected_restarted = _restart_connectivity_check(net) + + sys_idx = HydraulicSystemIndex(node_pit, branch_pit) + eq_registry = ComponentRegistry() + + for comp in net['component_list']: + comp.register_hydraulic_equations(net, branch_pit, node_pit, sys_idx, eq_registry) + + sz = sys_idx.size() + rows, cols, data, epsilon = eq_registry.assemble(sz) + jacobian = csr_matrix((data, (rows, cols)), shape=(sz, sz)) + + m_init_old = branch_pit[:, IdxBranch.MDOTINIT].copy() + p_init_old = node_pit[:, IdxNode.PINIT].copy() + slack_nodes = np.where(node_pit[:, IdxNode.NODE_TYPE] == IdxNode.P)[0] + msl_init_old = node_pit[slack_nodes, IdxNode.MDOTSLACKINIT].copy() + + x = spsolve(jacobian, epsilon) + + branch_pit[:, IdxBranch.MDOTINIT] -= x[len(node_pit):len(node_pit) + len(branch_pit)] * options["alpha"] + node_pit[:, IdxNode.PINIT] -= x[:len(node_pit)] * options["alpha"] + node_pit[slack_nodes, IdxNode.MDOTSLACKINIT] -= x[len(node_pit) + len(branch_pit):] + + filtered = [None, None, slack_nodes] + + return [branch_pit[:, IdxBranch.MDOTINIT], m_init_old, node_pit[:, IdxNode.PINIT], p_init_old, + node_pit[slack_nodes, IdxNode.MDOTSLACKINIT], msl_init_old], epsilon, filtered + +def solve_temperature(net): + """Build and solve a linearized system of equations to calculate temperature values. + + Uses the underlying net and the necessary graph data structures. Returned are the + solution vectors for the new iteration, the original solution vectors and a vector + containing component indices for the system matrix entries. + + :param net: The pandapipesNet for which to solve the temperature matrix + :type net: pandapipesNet + :return: branch_pit + """ + options = net["_options"] + branch_pit = net["_active_pit"]["branch"] + node_pit = net["_active_pit"]["node"] + + # Negative velocity values are turned to positive ones (including exchange of from_node and + # to_node for temperature calculation + branch_pit[:, IdxBranch.FROM_NODE_T_SWITCHED] = branch_pit[:, IdxBranch.MDOTINIT] < -2e-11 + + node_pit[:, IdxNode.INFEED] = False + compute_infeed_nodes(branch_pit, node_pit) + + sys_idx = HeatSystemIndex(node_pit, branch_pit) + eq_registry = ComponentRegistry() + + for comp in net['component_list']: + comp.register_thermal_equations(net, branch_pit, node_pit, sys_idx, eq_registry) + + t_init_old = node_pit[:, IdxNode.TINIT].copy() + t_out_old = branch_pit[:, IdxBranch.TOUTINIT].copy() + filtered = [None, None] + if not check_infeed_number(node_pit): + return [branch_pit[:, IdxBranch.TOUTINIT], t_out_old, node_pit[:, IdxNode.TINIT], t_init_old], np.array([ + np.nan]), filtered + + sz = sys_idx.size() + rows, cols, data, epsilon = eq_registry.assemble(sz) + jacobian = csr_matrix((data, (rows, cols)), shape=(sz, sz)) + + x = spsolve(jacobian, epsilon) + + if np.any(np.isnan(x)): + return [branch_pit[:, IdxBranch.TOUTINIT], t_out_old, node_pit[:, IdxNode.TINIT], t_init_old], np.array([ + np.nan]), filtered + + node_pit[:, IdxNode.TINIT] -= x[:len(node_pit)] * options["alpha"] + branch_pit[:, IdxBranch.TOUTINIT] -= x[len(node_pit):] * options["alpha"] + + return [branch_pit[:, IdxBranch.TOUTINIT], t_out_old, node_pit[:, IdxNode.TINIT], t_init_old], epsilon, filtered + + +def set_damping_factor(net, niter, errors): + """Set the value of the damping factor (factor for the newton step width) from current results. + + :param net: the net for which to perform the pipeflow + :type net: pandapipesNet + :param niter: + :type niter: + :param errors: an array containing the current residuals of all field variables solved for + :return: No Output. + + Example + ------- + set_damping_factor(net, niter, [error_p, error_v]) + + """ + error_increased = [] + for error in errors.values(): + error_increased.append(error[niter] > error[niter - 1]) + current_alpha = get_net_option(net, "alpha") + if np.all(error_increased): + set_net_option(net, "alpha", current_alpha / 10 if current_alpha >= 0.1 else current_alpha) + else: + set_net_option(net, "alpha", current_alpha * 10 if current_alpha <= 0.1 else 1.0) + return error_increased + + +def _restart_connectivity_check(net): + nodes_connected = get_lookup(net, "node", "active_hydraulics") + branches_connected = get_lookup(net, "branch", "active_hydraulics") + rows_nodes = np.arange(net["_pit"]["node"].shape[0])[nodes_connected] + rows_branches = np.arange(net["_pit"]["branch"].shape[0])[branches_connected] + active_node_pit = net["_active_pit"]["node"] + active_branch_pit = net["_active_pit"]["branch"] + node_pit = net["_pit"]["node"][rows_nodes, IdxNode.ACTIVE] + branch_pit = net["_pit"]["branch"][rows_branches, IdxBranch.ACTIVE] + mask_diff_node = active_node_pit[:, IdxNode.ACTIVE] != node_pit + mask_diff_branch = active_branch_pit[:, IdxBranch.ACTIVE] != branch_pit + if np.any(mask_diff_node) | np.any(mask_diff_branch): + net["_pit"]["node"][rows_nodes, IdxNode.ACTIVE] = active_node_pit[:, IdxNode.ACTIVE] + net["_pit"]["node"][rows_nodes, IdxNode.NODE_TYPE] = active_node_pit[:, IdxNode.NODE_TYPE] + net["_pit"]["branch"][rows_branches, IdxBranch.ACTIVE] = active_branch_pit[:, IdxBranch.ACTIVE] + net["_pit"]["branch"][rows_branches, IdxBranch.BRANCH_TYPE] = active_branch_pit[:, IdxBranch.BRANCH_TYPE] + net["_lookups"]["node_active_hydraulics"], net["_lookups"]["branch_active_hydraulics"] = \ + identify_active_nodes_branches(net, hydraulic_slack_mask(net)) + reduce_pit(net, "hydraulics") + return True + return False diff --git a/src/pandapipes/pf/derivative_calculation.py b/src/pandapipes/pf/derivative_calculation.py index de8dc7ac5..2c5279088 100644 --- a/src/pandapipes/pf/derivative_calculation.py +++ b/src/pandapipes/pf/derivative_calculation.py @@ -1,34 +1,33 @@ +# pylint: disable=import-outside-toplevel +# Every local import below picks between a numba and a plain-numpy implementation based on a +# runtime option (options["use_numba"]). This can't be hoisted to module level: when numba isn't +# installed, derivative_toolbox_numba's @jit(...) decorators fall back to plain numpy types for +# their explicit signatures (e.g. float64[:, :]), and numpy's own float64 doesn't support that +# subscript syntax at all ("TypeError: There are no type variables left in numpy.float64" on +# numpy>=2) - so importing that module eagerly would break `import pandapipes` outright whenever +# numba isn't installed, not just waste time JIT-compiling functions nobody asked for. Verified by +# actually trying it: any use_numba dispatch import from derivative_toolbox_numba hoisted to this +# file's top level reproduces that exact crash immediately. import numpy as np from pandapipes.constants import NORMAL_TEMPERATURE -from pandapipes.idx_branch import (LENGTH, D, K, RE, LAMBDA, LOAD_VEC_BRANCHES, JAC_DERIV_DM, JAC_DERIV_DP, - JAC_DERIV_DP1, JAC_DERIV_DM_NODE, FROM_NODE, TO_NODE, TOUTINIT, AREA, - LOAD_VEC_BRANCHES_T, JAC_DERIV_DT, LOAD_VEC_NODES_TO_T, - LOAD_VEC_NODES_FROM, LOAD_VEC_NODES_TO, JAC_DERIV_DT_NODE, JAC_DERIV_DTOUT_NODE, - JAC_DERIV_DTOUT, MDOTINIT, DP_FRICT_LOSS) -from pandapipes.idx_node import TINIT as TINIT_NODE, INFEED, LOAD_T, JAC_DERIV_DT_N -from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected -from pandapipes.pf.pipeflow_setup import get_net_option, get_lookup +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected, _sum_by_group, \ + branch_area +from pandapipes.pf.pipeflow_setup import get_net_option, get_lookup, PipeflowNotConverged from pandapipes.properties.fluids import get_fluid from pandapipes.properties.properties_toolbox import get_branch_real_density, get_branch_real_eta, get_branch_cp -from scipy.optimize import newton -def calculate_derivatives_hydraulic(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - options): - """ - Function which creates derivatives. +def calculate_derivatives_hydraulic(net, branch_pit_slice, node_pit, options): + """Compute hydraulic derivatives for *branch_pit_slice* (a view of the global branch pit) and write results back in-place via the view. :param net: The pandapipes network :type net: pandapipesNet - :param branch_pit: - :type branch_pit: - :param node_pit: - :type node_pit: - :param options: - :type options: - :return: No Output. + :param branch_pit_slice: view of the global branch pit for the component's active branches + :param node_pit: global node internal table + :param options: solver options dict (use_numba, friction_model, …) + :return: df_dm, df_dp, df_dp1, df_dm_nodes, load_vec, load_vec_nodes_from, load_vec_nodes_to """ if options["use_numba"]: from pandapipes.pf.derivative_toolbox_numba import ( @@ -43,103 +42,140 @@ def calculate_derivatives_hydraulic(net, gas_mode = fluid.is_gas friction_model = options["friction_model"] - from_nodes = branch_pit[:, FROM_NODE].astype(np.int32) - to_nodes = branch_pit[:, TO_NODE].astype(np.int32) - tinit_branch, height_difference, p_init_i_abs, p_init_i1_abs = get_derived_values(node_pit, from_nodes, to_nodes, - options["use_numba"]) + b_pit = branch_pit_slice + from_nodes = b_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + to_nodes = b_pit[:, IdxBranch.TO_NODE].astype(np.int32) + tinit_branch, height_difference, p_init_i_abs, p_init_i1_abs = get_derived_values( + node_pit, from_nodes, to_nodes, options["use_numba"]) if gas_mode: p_m, der_p_m, der_p_m1 = calc_medium_pressure_with_derivative(p_init_i_abs, p_init_i1_abs) else: p_m, der_p_m, der_p_m1 = (p_init_i_abs + p_init_i1_abs) / 2, None, None - rho = get_branch_real_density(fluid, node_pit, branch_pit) - eta = get_branch_real_eta(fluid, node_pit, branch_pit, p_m) - - # Darcy Friction factor: lambda - lambda_, re = calc_lambda(branch_pit[:, MDOTINIT], eta, branch_pit[:, D], branch_pit[:, K], gas_mode, - friction_model, branch_pit[:, LENGTH], options, branch_pit[:, AREA]) - der_lambda = calc_der_lambda(branch_pit[:, MDOTINIT], eta, branch_pit[:, D], branch_pit[:, K], friction_model, - lambda_, branch_pit[:, AREA], re, branch_pit[:, LENGTH]) - branch_pit[:, RE] = re - branch_pit[:, LAMBDA] = lambda_ + rho = get_branch_real_density(fluid, node_pit, b_pit) + eta = get_branch_real_eta(fluid, node_pit, b_pit, p_m) + + # computed once here, then threaded through to calc_lambda/calc_der_lambda AND + # derivatives_hydraulic_incomp/comp below - not re-derived at each of those call sites for + # the same branch set/iteration (D never changes mid-solve here, only across outer + # optimize_dn iterations, so this is safe to reuse for the remainder of this call, but never + # cached anywhere longer-lived than that - see branch_area's own docstring for why) + area = branch_area(b_pit) + lambda_, re = calc_lambda(b_pit[:, IdxBranch.MDOTINIT], eta, b_pit[:, IdxBranch.D], b_pit[:, IdxBranch.K], gas_mode, + friction_model, b_pit[:, IdxBranch.LENGTH], options, area) + der_lambda = calc_der_lambda(b_pit[:, IdxBranch.MDOTINIT], eta, b_pit[:, IdxBranch.D], b_pit[:, IdxBranch.K], friction_model, + lambda_, area, re, b_pit[:, IdxBranch.LENGTH]) + b_pit[:, IdxBranch.RE] = re + b_pit[:, IdxBranch.LAMBDA] = lambda_ if not gas_mode: load_vec, load_vec_nodes_from, load_vec_nodes_to, df_dm, df_dm_nodes, df_dp, df_dp1, dp_frict_loss = ( - derivatives_hydraulic_incomp(branch_pit, der_lambda, p_init_i_abs, p_init_i1_abs, height_difference, rho)) + derivatives_hydraulic_incomp(b_pit, der_lambda, p_init_i_abs, p_init_i1_abs, height_difference, rho, area)) else: - rho_n = np.full(len(branch_pit), fluid.get_density(NORMAL_TEMPERATURE)) + rho_n = np.full(len(b_pit), fluid.get_density(NORMAL_TEMPERATURE)) comp_fact = fluid.get_compressibility(p_m, tinit_branch) dc = fluid.get_der_compressibility() - # TODO: this might not be required der_comp = dc * der_p_m der_comp1 = dc * der_p_m1 load_vec, load_vec_nodes_from, load_vec_nodes_to, df_dm, df_dm_nodes, df_dp, df_dp1, dp_frict_loss = ( - derivatives_hydraulic_comp(node_pit, branch_pit, lambda_, der_lambda, p_init_i_abs, p_init_i1_abs, - height_difference, comp_fact, der_comp, der_comp1, rho, rho_n)) - - branch_pit[:, LOAD_VEC_BRANCHES] = load_vec - branch_pit[:, JAC_DERIV_DM] = df_dm - branch_pit[:, JAC_DERIV_DP] = df_dp - branch_pit[:, JAC_DERIV_DP1] = df_dp1 - branch_pit[:, LOAD_VEC_NODES_FROM] = load_vec_nodes_from - branch_pit[:, LOAD_VEC_NODES_TO] = load_vec_nodes_to - branch_pit[:, JAC_DERIV_DM_NODE] = df_dm_nodes - branch_pit[:, DP_FRICT_LOSS] = dp_frict_loss - - -def calculate_derivatives_thermal(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - options): - node_pit_old_lookup = get_lookup(net, "node", "old_pit_cols") + derivatives_hydraulic_comp(node_pit, b_pit, lambda_, der_lambda, p_init_i_abs, p_init_i1_abs, + height_difference, comp_fact, der_comp, der_comp1, rho, rho_n, area)) + + b_pit[:, IdxBranch.DP_FRICT_LOSS] = dp_frict_loss + + return df_dm, df_dp, df_dp1, df_dm_nodes, load_vec, load_vec_nodes_from, load_vec_nodes_to + + +def calculate_derivatives_branch_thermal(net, branch_pit_slice, node_pit, branch_pit_old_slice, options): + """Compute branch-level thermal derivatives for *branch_pit_slice*. + + Stagnant node equations are excluded — see calculate_derivatives_node_thermal. + + :return: fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout + """ branch_pit_old_lookup = get_lookup(net, "branch", "old_pit_cols") if options["use_numba"]: - from pandapipes.pf.derivative_toolbox_numba import derivatives_thermal_numba as derivatives_termal + from pandapipes.pf.derivative_toolbox_numba import derivatives_branch_thermal_numba as deriv_fn else: - from pandapipes.pf.derivative_toolbox import derivatives_thermal_np as derivatives_termal + from pandapipes.pf.derivative_toolbox import derivatives_branch_thermal_np as deriv_fn + fluid = get_fluid(net) - cp_b = get_branch_cp(fluid, node_pit, branch_pit) - # this is not required currently, but useful when implementing leakages - # m_init_i = np.abs(branch_pit[:, MDOTINIT]) - # m_init_i1 = np.abs(branch_pit[:, MDOTINIT]) - from_nodes = get_from_nodes_corrected(branch_pit) - to_nodes = get_to_nodes_corrected(branch_pit) - t_init_i = node_pit[from_nodes, TINIT_NODE] - t_init_i1 = branch_pit[:, TOUTINIT] - t_init_nt = node_pit[to_nodes, TINIT_NODE] - t_init_n = node_pit[:, TINIT_NODE] + b_pit = branch_pit_slice + b_pit_old = branch_pit_old_slice + + from_nodes = get_from_nodes_corrected(b_pit) + to_nodes = get_to_nodes_corrected(b_pit) + t_init_i = node_pit[from_nodes, IdxNode.TINIT] + t_init_i1 = b_pit[:, IdxBranch.TOUTINIT] + t_init_nt = node_pit[to_nodes, IdxNode.TINIT] + cp_b = get_branch_cp(fluid, node_pit, b_pit) cp_i1 = fluid.get_heat_capacity(t_init_i1) cp_nt = fluid.get_heat_capacity(t_init_nt) cp_n = fluid.get_heat_capacity((cp_i1 + cp_nt) / 2) + rho = get_branch_real_density(fluid, node_pit, b_pit) transient = get_net_option(net, "transient") dt = get_net_option(net, "dt") - rho = get_branch_real_density(fluid, node_pit, branch_pit) amb = get_net_option(net, 'ambient_temperature') - fn, dfn_dt, fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout, infeed = ( - derivatives_termal(node_pit, branch_pit, - node_pit_old, node_pit_old_lookup, - branch_pit_old, branch_pit_old_lookup, - from_nodes, to_nodes, - t_init_i, t_init_i1, t_init_nt, t_init_n, - cp_n, cp_b, - rho, dt, transient, amb)) + return deriv_fn( + b_pit, + b_pit_old, branch_pit_old_lookup, + t_init_i, t_init_i1, t_init_nt, + cp_n, cp_b, + rho, dt, transient, amb, + ) - node_pit[:, LOAD_T] = fn - node_pit[:, JAC_DERIV_DT_N] = dfn_dt - branch_pit[:, LOAD_VEC_BRANCHES_T] = fb - branch_pit[:, JAC_DERIV_DT] = dfb_dt - branch_pit[:, JAC_DERIV_DTOUT] = dfb_dtout +def calculate_derivatives_node_thermal(net, branch_pit, node_pit, node_pit_old, options): + """Compute stagnant node thermal derivatives from the FULL active thermal branch pit. - branch_pit[:, LOAD_VEC_NODES_TO_T] = fnt - branch_pit[:, JAC_DERIV_DT_NODE] = dfnt_dt - branch_pit[:, JAC_DERIV_DTOUT_NODE] = dfnt_dtout + Must be called with the complete branch pit so that nodes_flow is computed globally. - node_pit[:, INFEED] = False - node_pit[infeed, INFEED] = True + :return: fn_node, dfn_dt (arrays indexed by global node index) + """ + node_pit_old_lookup = get_lookup(net, "node", "old_pit_cols") + + if options["use_numba"]: + from pandapipes.pf.derivative_toolbox_numba import derivatives_node_thermal_numba as deriv_fn + else: + from pandapipes.pf.derivative_toolbox import derivatives_node_thermal_np as deriv_fn + + fluid = get_fluid(net) + from_nodes = get_from_nodes_corrected(branch_pit) + to_nodes = get_to_nodes_corrected(branch_pit) + t_init_i = node_pit[from_nodes, IdxNode.TINIT] + t_init_n = node_pit[:, IdxNode.TINIT] + cp_b = get_branch_cp(fluid, node_pit, branch_pit) + rho = get_branch_real_density(fluid, node_pit, branch_pit) + transient = get_net_option(net, "transient") + dt = get_net_option(net, "dt") + amb = get_net_option(net, 'ambient_temperature') + + return deriv_fn( + node_pit, branch_pit, + node_pit_old, node_pit_old_lookup, + from_nodes, to_nodes, + t_init_i, t_init_n, + cp_b, rho, dt, transient, amb, + ) + + +def calculate_load_hydraulic(net, loads, sign, junction_table_name): + """Compute the aggregated nodal mass-flow loads for a ConstFlow-type component. + + Returns the active node-pit indices and the corresponding summed load values + (sign-corrected, NaN-safe, filtered to hydraulically active nodes). + """ + helper = loads.in_service.values * loads.scaling.values * sign + mf = np.nan_to_num(loads.mdot_kg_per_s.values) + juncts, loads_sum = _sum_by_group( + get_net_option(net, "use_numba"), loads.junction.values, -mf * helper) + junction_idx_lookup = get_lookup(net, "node", "index_active_hydraulics")[junction_table_name] + index = junction_idx_lookup[juncts] + valid = index >= 0 + return index[valid].astype(np.int32), loads_sum[valid] def get_derived_values(node_pit, from_nodes, to_nodes, use_numba): @@ -151,10 +187,11 @@ def get_derived_values(node_pit, from_nodes, to_nodes, use_numba): def calc_lambda(m, eta, d, k, gas_mode, friction_model, lengths, options, area): - """ - Function calculates the friction factor of a pipe. Turbulence is calculated based on - Nikuradse. If v equals 0, a value of 0.001 is used in order to avoid division by zero. - This should not be a problem as the pressure loss term will equal zero (lambda * u^2). + """Function calculates the friction factor of a pipe. + + Turbulence is calculated based on Nikuradse. If v equals 0, a value of 0.001 is used in order + to avoid division by zero. This should not be a problem as the pressure loss term will equal + zero (lambda * u^2). :param m: :type m: @@ -180,21 +217,21 @@ def calc_lambda(m, eta, d, k, gas_mode, friction_model, lengths, options, area): if options["use_numba"]: from pandapipes.pf.derivative_toolbox_numba import ( calc_lambda_nikuradse_incomp_numba as calc_lambda_nikuradse_incomp, - calc_lambda_nikuradse_comp_numba as calc_lambda_nikuradse_comp) + calc_lambda_nikuradse_comp_numba as calc_lambda_nikuradse_comp, + colebrook_numba as colebrook) else: from pandapipes.pf.derivative_toolbox import (calc_lambda_nikuradse_incomp_np as calc_lambda_nikuradse_incomp, - calc_lambda_nikuradse_comp_np as calc_lambda_nikuradse_comp) + calc_lambda_nikuradse_comp_np as calc_lambda_nikuradse_comp, + colebrook_np as colebrook) if gas_mode: re, lambda_laminar, lambda_nikuradse = calc_lambda_nikuradse_comp(m, d, k, eta, area) else: re, lambda_laminar, lambda_nikuradse = calc_lambda_nikuradse_incomp(m, d, k, eta, area) if friction_model == "colebrook": - # TODO: move this import to top level if possible - from pandapipes.pipeflow import PipeflowNotConverged max_iter = options.get("max_iter_colebrook", 100) tolerance = options.get("tolerance_colebrook", 1e-4) - converged, lambda_colebrook = colebrook_white(re, d, k, lambda_nikuradse, max_iter, lengths, tolerance) + converged, lambda_colebrook = colebrook(re, d, k, lambda_nikuradse, max_iter, lengths, tolerance) if not converged: raise PipeflowNotConverged("The Colebrook-White algorithm did not converge. There might be model " "inconsistencies. The maximum iterations can be given as 'max_iter_colebrook' " @@ -211,10 +248,10 @@ def calc_lambda(m, eta, d, k, gas_mode, friction_model, lengths, options, area): def calc_der_lambda(m, eta, d, k, friction_model, lambda_pipe, area, re, lengths): - """ - Function calculates the derivative of lambda with respect to v. Turbulence is calculated based - on Nikuradse. This should not be a problem as the pressure loss term will equal zero - (lambda * u^2). + """Function calculates the derivative of lambda with respect to v. + + Turbulence is calculated based on Nikuradse. This should not be a problem as the pressure loss + term will equal zero (lambda * u^2). :param m: :type m: @@ -233,7 +270,6 @@ def calc_der_lambda(m, eta, d, k, friction_model, lambda_pipe, area, re, lengths :return: :rtype: """ - b_term = np.zeros_like(m) df_dm = np.zeros_like(m) df_dlambda = np.zeros_like(m) @@ -242,75 +278,48 @@ def calc_der_lambda(m, eta, d, k, friction_model, lambda_pipe, area, re, lengths if friction_model == "colebrook": pos &= ~np.isclose(lengths, 0, rtol=1e-10, atol=1e-11) - b_term[pos] = (2.51 * eta[pos] * area[pos] / (m[pos] * d[pos] * np.sqrt(lambda_pipe[pos])) + k[pos] / ( + m_abs = np.abs(m) + # b_term is "2.51/(Re*sqrt(lambda)) + k/(3.71*d)" from the colebrook-white implicit + # equation F(lambda, m) = 0 (see colebrook_white_implicit/cw_derivative in + # derivative_toolbox.colebrook_np); Re is defined + # via |m| (see calc_lambda_nikuradse_*_np's own re = m_abs*d/(eta*area)), so this needs + # m_abs here too, not signed m - a previous version used signed m, which is only correct + # for m > 0 and flips b_term's first term to the wrong sign for m < 0. + b_term[pos] = (2.51 * eta[pos] * area[pos] / (m_abs[pos] * d[pos] * np.sqrt(lambda_pipe[pos])) + k[pos] / ( 3.71 * d[pos])) - df_dm[pos] = -2 * 2.51 * eta[pos] * area[pos] / (m[pos] ** 2 * np.sqrt(lambda_pipe[pos]) * d[pos]) / ( - np.log(10) * b_term[pos]) + # dF/dm - the 1/|m| term in b_term contributes a sign(m) chain-rule factor (d|m|/dm); a + # previous version omitted it, same class of bug as the "nikuradse"/"swamee-jain" branches + # below. + df_dm[pos] = -np.sign(m[pos]) * 2 * 2.51 * eta[pos] * area[pos] / ( + m[pos] ** 2 * np.sqrt(lambda_pipe[pos]) * d[pos]) / (np.log(10) * b_term[pos]) - df_dlambda[pos] = -0.5 * lambda_pipe[pos] ** (-3 / 2) - (2.51 * eta[pos] * area[pos] / (d[pos] * m[pos])) * \ + df_dlambda[pos] = -0.5 * lambda_pipe[pos] ** (-3 / 2) - (2.51 * eta[pos] * area[pos] / (d[pos] * m_abs[pos])) * \ lambda_pipe[pos] ** (-3 / 2) / (np.log(10) * b_term[pos]) - lambda_der[pos] = df_dm[pos] / df_dlambda[pos] + # implicit function theorem: F(lambda(m), m) = 0 => dlambda/dm = -(dF/dm)/(dF/dlambda) - + # a previous version omitted the leading minus sign, verified against finite differences + # of calc_lambda(..., friction_model="colebrook") for both signs of m (see git history). + lambda_der[pos] = -df_dm[pos] / df_dlambda[pos] return lambda_der elif friction_model == "swamee-jain": param = (k[pos] / (3.7 * d[pos]) + 5.74 * ((eta[pos] * area[pos]) / (np.abs(m[pos]) * d[pos])) ** 0.9) # 0.5 / (log(10) * log(param)^3 * param) * 5.166 * abs(eta)^0.9 / (abs(rho * d)^0.9 # * abs(v_corr)^1.9) - lambda_der[pos] = 0.5 * np.log(10) ** 2 / (np.log(param) ** 3) / param * 5.166 * ( + # lambda_swamee_jain is a function of |m| only, so d(lambda)/dm picks up a d|m|/dm = + # sign(m) chain-rule factor - verified against finite differences of calc_lambda(..., + # friction_model="swamee-jain") for both signs of m (see git history); a previous version + # of this branch omitted it, returning the same value for m < 0 as for m > 0 instead of + # flipping its sign. + lambda_der[pos] = np.sign(m[pos]) * 0.5 * np.log(10) ** 2 / (np.log(param) ** 3) / param * 5.166 * ( (eta[pos] * area[pos]) / (d[pos])) ** 0.9 * np.abs(m[pos]) ** -1.9 return lambda_der else: - lambda_der[pos] = -(64 * eta[pos] * area[pos]) / (m[pos] ** 2 * d[pos]) + # lambda_laminar = 64/Re = 64*eta*area/(|m|*d), so d(lambda_laminar)/dm carries a + # sign(m) factor from d(1/|m|)/dm = -sign(m)/m**2 - verified against finite differences + # of calc_lambda(..., friction_model=None/"nikuradse") for both signs of m (see git + # history); a previous version of this branch omitted sign(m), returning the m > 0 value + # unchanged for m < 0 instead of flipping its sign. + lambda_der[pos] = -np.sign(m[pos]) * (64 * eta[pos] * area[pos]) / (m[pos] ** 2 * d[pos]) return lambda_der - - -def colebrook_white(re, d, k, lambda_nikuradse, max_iter, lengths, tolerance=1e-4): - """ - Function calculates the friction factor of a pipe using the Colebrook-White equation. It is an - implicit equation which is solved using the Newton-Raphson method. For pipes with zero flow or - zero length, the initial guess is returned. This should be uncritical, as the pressure loss - term will equal zero (lambda * u^2 * l / d). - - :param re: Reynolds number [dimensionless] - :type re: np.array - :param d: Diameter [m] - :type d: np.array - :param k: Roughness [m] - :type k: np.array - :param lambda_nikuradse: Initial guess for lambda (from Nikuradse) - :type lambda_nikuradse: np.array - :param max_iter: Maximum number of iterations for the Colebrook-White calculation - :type max_iter: int - :param lengths: Length of the pipes [m] - only used to identify zero-length pipes - :type lengths: np.array - :param tolerance: Tolerance for the Colebrook-White calculation - :type tolerance: float - :return: lambda_cb, converged - 1. lambda_cb: Friction factor according to Colebrook-White - 2. converged: True, if the Colebrook-White calculation converged for all pipes - :rtype: (np.array, bool) - """ - - def colebrook_white_implicit(lambda_cb, re_nz, k_nz, d_nz): - return lambda_cb ** (-1 / 2) + 2 * np.log10(2.51 / (re_nz * np.sqrt(lambda_cb)) + k_nz / (3.71 * d_nz)) - - def cw_derivative(lambda_cb, re_nz, k_nz, d_nz): - return -1 / 2 * lambda_cb ** (-3 / 2) - (2.51 / re_nz) * lambda_cb ** (-3 / 2) / ( - np.log(10) * (2.51 / (re_nz * np.sqrt(lambda_cb)) + k_nz / (3.71 * d_nz))) - - mask = ~np.isclose(re, 0) & ~np.isclose(lengths, 0, rtol=1e-10, atol=1e-11) - lambda_res = lambda_nikuradse - - res = newton(colebrook_white_implicit, lambda_res[mask], maxiter=max_iter, args=(re[mask], k[mask], d[mask]), - tol=tolerance, full_output=True, fprime=cw_derivative) # , fprime2=cw_derivative_2) - - if lambda_res[mask].size == 1: - lambda_res[mask] = res[0] - converged = res[1].converged - else: - lambda_res[mask] = res.root - converged = np.all(res.converged) - - return converged, lambda_res diff --git a/src/pandapipes/pf/derivative_toolbox.py b/src/pandapipes/pf/derivative_toolbox.py index 26c4c25bf..d9b1abe78 100644 --- a/src/pandapipes/pf/derivative_toolbox.py +++ b/src/pandapipes/pf/derivative_toolbox.py @@ -4,42 +4,49 @@ import logging import numpy as np -from numpy import linalg -from pandapipes.pf.internals_toolbox import _sum_by_group +from scipy.optimize import newton +from pandapipes.pf.internals_toolbox import _sum_by_group, branch_area +from pandapipes.pf.pipeflow_setup import branches_not_zero_flow from pandapipes.constants import P_CONVERSION, GRAVITATION_CONSTANT, NORMAL_PRESSURE, \ NORMAL_TEMPERATURE -from pandapipes.idx_branch import LENGTH, LAMBDA, D, LOSS_COEFFICIENT as LC, PL, AREA, \ - MDOTINIT, TOUTINIT, FROM_NODE, TEXT, ALPHA, TL, QEXT, DO, DP_FRICT_LOSS -from pandapipes.idx_node import HEIGHT, PINIT, PAMB, TINIT as TINIT_NODE +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode logger = logging.getLogger(__name__) def derivatives_hydraulic_incomp_np(branch_pit, der_lambda, p_init_i_abs, p_init_i1_abs, - height_difference, rho): + height_difference, rho, area): # Formulas for pressure loss in incompressible flow # Use medium density ((rho_from + rho_to) / 2) for Darcy Weisbach according to # https://www.schweizer-fn.de/rohr/rohrleitung/rohrleitung.php#fluessigkeiten - m_init_abs = np.abs(branch_pit[:, MDOTINIT]) + m_init_abs = np.abs(branch_pit[:, IdxBranch.MDOTINIT]) m_abs_deriv = np.maximum(m_init_abs, 1e-8) - m_init2 = m_init_abs * branch_pit[:, MDOTINIT] + m_init2 = m_init_abs * branch_pit[:, IdxBranch.MDOTINIT] p_diff = p_init_i_abs - p_init_i1_abs + length = branch_pit[:, IdxBranch.LENGTH] + lambd = branch_pit[:, IdxBranch.LAMBDA] + lc = branch_pit[:, IdxBranch.LOSS_COEFFICIENT] + pl = branch_pit[:, IdxBranch.PL] + + d = branch_pit[:, IdxBranch.D] + const_height = rho * GRAVITATION_CONSTANT * height_difference / P_CONVERSION - friction_term = np.divide(branch_pit[:, LENGTH] * branch_pit[:, LAMBDA], branch_pit[:, D]) + branch_pit[:, LC] - const_term = np.divide(1, branch_pit[:, AREA] ** 2 * rho * P_CONVERSION * 2) + friction_term = length * lambd / d + lc + const_term = 1 / (area ** 2 * rho * P_CONVERSION * 2) df_dm = - const_term * (2 * m_abs_deriv * friction_term + der_lambda - * np.divide(branch_pit[:, LENGTH], branch_pit[:, D]) * m_init2) + * length / d * m_init2) - load_vec = p_diff + branch_pit[:, PL] + const_height - const_term * m_init2 * friction_term + load_vec = p_diff + pl + const_height - const_term * m_init2 * friction_term df_dp = np.ones_like(der_lambda) df_dp1 = np.ones_like(der_lambda) * (-1) df_dm_nodes = np.ones_like(der_lambda) - load_vec_nodes_from = branch_pit[:, MDOTINIT] - load_vec_nodes_to = branch_pit[:, MDOTINIT] + load_vec_nodes_from = branch_pit[:, IdxBranch.MDOTINIT] + load_vec_nodes_to = branch_pit[:, IdxBranch.MDOTINIT] dp_frict_loss = const_term * m_init2 * friction_term @@ -47,72 +54,63 @@ def derivatives_hydraulic_incomp_np(branch_pit, der_lambda, p_init_i_abs, p_init def derivatives_hydraulic_comp_np(node_pit, branch_pit, lambda_, der_lambda, p_init_i_abs, p_init_i1_abs, - height_difference, comp_fact, der_comp, der_comp1, rho, rho_n): + height_difference, comp_fact, der_comp, der_comp1, rho, rho_n, area): # Formulas for gas pressure loss according to laminar version - m_init_abs = np.abs(branch_pit[:, MDOTINIT]) + m_init_abs = np.abs(branch_pit[:, IdxBranch.MDOTINIT]) m_abs_deriv = np.maximum(m_init_abs, 1e-8) - m_init2 = branch_pit[:, MDOTINIT] * m_init_abs + m_init2 = branch_pit[:, IdxBranch.MDOTINIT] * m_init_abs p_diff = p_init_i_abs - p_init_i1_abs p_sum = p_init_i_abs + p_init_i1_abs p_sum_div = np.divide(1, p_sum) - from_nodes = branch_pit[:, FROM_NODE].astype(np.int32) - tm = (node_pit[from_nodes, TINIT_NODE] + branch_pit[:, TOUTINIT]) / 2 + from_nodes = branch_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + tm = (node_pit[from_nodes, IdxNode.TINIT] + branch_pit[:, IdxBranch.TOUTINIT]) / 2 const_height = rho * GRAVITATION_CONSTANT * height_difference / P_CONVERSION - friction_term = np.divide(lambda_ * branch_pit[:, LENGTH], branch_pit[:, D]) + branch_pit[:, LC] - normal_term = np.divide(NORMAL_PRESSURE, NORMAL_TEMPERATURE * P_CONVERSION * rho_n * branch_pit[:, AREA] ** 2) + friction_term = np.divide(lambda_ * branch_pit[:, IdxBranch.LENGTH], branch_pit[:, IdxBranch.D]) + branch_pit[:, IdxBranch.LOSS_COEFFICIENT] + normal_term = np.divide(NORMAL_PRESSURE, NORMAL_TEMPERATURE * P_CONVERSION * rho_n) + const_term = normal_term / area ** 2 - const_term_p = normal_term * m_init2 * friction_term * tm + const_term_p = const_term * m_init2 * friction_term * tm df_dp = 1. - const_term_p * p_sum_div * (der_comp - comp_fact * p_sum_div) df_dp1 = -1. - const_term_p * p_sum_div * (der_comp1 - comp_fact * p_sum_div) - const_term_m = normal_term * p_sum_div * tm * comp_fact + const_term_m = const_term * p_sum_div * tm * comp_fact df_dm = - const_term_m * (2 * m_abs_deriv * friction_term + - np.divide(der_lambda * branch_pit[:, LENGTH] * m_init2, branch_pit[:, D])) + np.divide(der_lambda * branch_pit[:, IdxBranch.LENGTH] * m_init2, branch_pit[:, IdxBranch.D])) df_dm[np.isclose(m_init_abs, 0)] = 1. - load_vec = p_diff + branch_pit[:, PL] + const_height \ - - normal_term * comp_fact * m_init2 * friction_term * p_sum_div * tm + load_vec = p_diff + branch_pit[:, IdxBranch.PL] + const_height \ + - const_term * comp_fact * m_init2 * friction_term * p_sum_div * tm df_dm_nodes = np.ones_like(lambda_) - load_vec_nodes_from = branch_pit[:, MDOTINIT] - load_vec_nodes_to = branch_pit[:, MDOTINIT] - dp_frict_loss = normal_term * comp_fact * m_init2 * friction_term * p_sum_div * tm + load_vec_nodes_from = branch_pit[:, IdxBranch.MDOTINIT] + load_vec_nodes_to = branch_pit[:, IdxBranch.MDOTINIT] + dp_frict_loss = const_term * comp_fact * m_init2 * friction_term * p_sum_div * tm return load_vec, load_vec_nodes_from, load_vec_nodes_to, df_dm, df_dm_nodes, df_dp, df_dp1, dp_frict_loss -def derivatives_thermal_np(node_pit, branch_pit, - node_pit_old, node_pit_old_lookup, - branch_pit_old, branch_pit_old_lookup, - from_nodes, to_nodes, - t_init_i, t_init_i1, t_init_nt, t_init_n, - cp_n, cp_b, - rho, dt, transient, amb): - # this is not required currently, but useful when implementing leakages - # m_init_i = np.abs(branch_pit[:, MDOTINIT]) - # m_init_i1 = np.abs(branch_pit[:, MDOTINIT]) - mdot = np.abs(branch_pit[:, MDOTINIT]) - t_amb = branch_pit[:, TEXT] - length = branch_pit[:, LENGTH] - alpha = branch_pit[:, ALPHA] * np.pi * branch_pit[:, DO] - tl = branch_pit[:, TL] - qext = branch_pit[:, QEXT] - - branches_flow = _branches_not_zero_flow(branch_pit) - # ToDo: Is it relevant to consider slack streams? - nodes_flow = np.isin(np.arange(len(node_pit)), - np.concatenate([from_nodes[branches_flow], to_nodes[branches_flow]])) - - fn = np.zeros_like(t_init_n) - dfn_dt = np.zeros_like(t_init_n) +def derivatives_branch_thermal_np(branch_pit, + branch_pit_old, branch_pit_old_lookup, + t_init_i, t_init_i1, t_init_nt, + cp_n, cp_b, + rho, dt, transient, amb): + """Branch-level thermal derivatives: fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout.""" + mdot = np.abs(branch_pit[:, IdxBranch.MDOTINIT]) + t_amb = branch_pit[:, IdxBranch.TEXT] + length = branch_pit[:, IdxBranch.LENGTH] + alpha = branch_pit[:, IdxBranch.ALPHA] * np.pi * branch_pit[:, IdxBranch.DO] + tl = branch_pit[:, IdxBranch.TL] + qext = branch_pit[:, IdxBranch.QEXT] + + branches_flow = branches_not_zero_flow(branch_pit) fnt = cp_n * mdot * (t_init_i1 - t_init_nt) dfnt_dt = - cp_n * mdot dfnt_dtout = cp_n * mdot if transient: - area = branch_pit[:, AREA] - tvor = branch_pit_old[:, branch_pit_old_lookup[TOUTINIT]] + area = branch_area(branch_pit) + tvor = branch_pit_old[:, branch_pit_old_lookup[IdxBranch.TOUTINIT]] fb = ( rho * area * cp_b * (t_init_i1 - tvor) * (1 / dt) * length @@ -125,7 +123,7 @@ def derivatives_thermal_np(node_pit, branch_pit, if np.any(~branches_flow): # TODO: maybe replace this statement with a component lookup - zero_length = np.isclose(branch_pit[:, LENGTH], 0, atol=1e-10) + zero_length = np.isclose(branch_pit[:, IdxBranch.LENGTH], 0, atol=1e-10) mask = zero_length & ~branches_flow if np.any(mask): fb[mask] = ( @@ -134,44 +132,10 @@ def derivatives_thermal_np(node_pit, branch_pit, ) dfb_dt[mask] = 0 dfb_dtout[mask] = (rho[mask] * area[mask] * cp_b[mask] / dt + - alpha[mask]) - - if np.any(~nodes_flow): - fn_zero = ~nodes_flow[from_nodes] - tn_zero = ~nodes_flow[to_nodes] - - t_from_node_vor_zero = node_pit_old[from_nodes[fn_zero], node_pit_old_lookup[TINIT_NODE]] - t_to_node_vor_zero = node_pit_old[to_nodes[tn_zero], node_pit_old_lookup[TINIT_NODE]] - t_to_node = node_pit[to_nodes[tn_zero], TINIT_NODE] - - fn_eq = (rho[fn_zero] * area[fn_zero] * cp_b[fn_zero] * (1 / dt) - * (t_init_i[fn_zero] - t_from_node_vor_zero) - - alpha[fn_zero] * (t_amb[fn_zero] - t_init_i[fn_zero])) - - tn_eq = (rho[tn_zero] * area[tn_zero] * cp_b[tn_zero] * (1 / dt) - * (t_to_node - t_to_node_vor_zero) - - alpha[tn_zero] * (t_amb[tn_zero] - t_to_node)) - - fn_deriv = (rho[fn_zero] * area[fn_zero] * cp_b[fn_zero] * (1 / dt) + alpha[fn_zero]) - tn_deriv = (rho[tn_zero] * area[tn_zero] * cp_b[tn_zero] * (1 / dt) + alpha[tn_zero]) - - fn_nodes, fn_eq_sum, fn_deriv_sum = _sum_by_group(False, - from_nodes[fn_zero], fn_eq, fn_deriv - ) - - tn_nodes, tn_eq_sum, tn_deriv_sum = _sum_by_group(False, - to_nodes[tn_zero], tn_eq, tn_deriv - ) - - fn[~nodes_flow] = 0 - fn[fn_nodes] += fn_eq_sum - fn[tn_nodes] += tn_eq_sum - dfn_dt[~nodes_flow] = 0 - dfn_dt[fn_nodes] -= fn_deriv_sum - dfn_dt[tn_nodes] -= tn_deriv_sum + alpha[mask]) else: - non_zero_length_mask = ~np.isclose(branch_pit[:, LENGTH], 0, rtol=1e-6, atol=1e-10) - if np.any(non_zero_length_mask & (np.abs(branch_pit[:, QEXT]) > 1e-12)): + non_zero_length_mask = ~np.isclose(branch_pit[:, IdxBranch.LENGTH], 0, rtol=1e-6, atol=1e-10) + if np.any(non_zero_length_mask & (np.abs(branch_pit[:, IdxBranch.QEXT]) > 1e-12)): logger.warning( "A branch with non zero length has a non zero external heat load. This might lead " "to errors in the calculation, as the overlap of temperature reduction from heat " @@ -181,8 +145,8 @@ def derivatives_thermal_np(node_pit, branch_pit, fb = np.zeros_like(cp_b) fb[branches_flow] = ( - t_amb[branches_flow] + (t_init_i[branches_flow] - t_amb[branches_flow]) - * np.exp(- alpha[branches_flow] * length[branches_flow] / (cp_b[branches_flow] * mdot[branches_flow])) + t_amb[branches_flow] + (t_init_i[branches_flow] - t_amb[branches_flow]) + * np.exp(- alpha[branches_flow] * length[branches_flow] / (cp_b[branches_flow] * mdot[branches_flow])) - t_init_i1[branches_flow] + tl[branches_flow] - qext[branches_flow] / (cp_b[branches_flow] * mdot[branches_flow]) ) @@ -192,23 +156,70 @@ def derivatives_thermal_np(node_pit, branch_pit, (cp_b[branches_flow] * mdot[branches_flow])) dfb_dtout = - np.ones_like(cp_b) - fn[~nodes_flow] = amb - t_init_n[~nodes_flow] - dfn_dt[~nodes_flow] = np.ones(np.sum(~nodes_flow)) fnt[~branches_flow] = 0 dfnt_dt[~branches_flow] = 0 dfnt_dtout[~branches_flow] = 0 - infeed = np.setdiff1d(from_nodes[branches_flow], to_nodes[branches_flow]) + return fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout + + +def derivatives_node_thermal_np(node_pit, branch_pit, + node_pit_old, node_pit_old_lookup, + from_nodes, to_nodes, + t_init_i, t_init_n, + cp_b, rho, dt, transient, amb): + """Node stagnant thermal derivatives: fn, dfn_dt. Must be called with the full branch pit.""" + branches_flow = branches_not_zero_flow(branch_pit) + nodes_flow = np.isin(np.arange(len(node_pit)), + np.concatenate([from_nodes[branches_flow], to_nodes[branches_flow]])) + + fn = np.zeros_like(t_init_n) + dfn_dt = np.zeros_like(t_init_n) + + if transient: + area = branch_area(branch_pit) + alpha = branch_pit[:, IdxBranch.ALPHA] * np.pi * branch_pit[:, IdxBranch.DO] + t_amb = branch_pit[:, IdxBranch.TEXT] + + if np.any(~nodes_flow): + fn_zero = ~nodes_flow[from_nodes] + tn_zero = ~nodes_flow[to_nodes] + + t_from_node_vor_zero = node_pit_old[from_nodes[fn_zero], node_pit_old_lookup[IdxNode.TINIT]] + t_to_node_vor_zero = node_pit_old[to_nodes[tn_zero], node_pit_old_lookup[IdxNode.TINIT]] + t_to_node = node_pit[to_nodes[tn_zero], IdxNode.TINIT] + + fn_eq = (rho[fn_zero] * area[fn_zero] * cp_b[fn_zero] * (1 / dt) + * (t_init_i[fn_zero] - t_from_node_vor_zero) + - alpha[fn_zero] * (t_amb[fn_zero] - t_init_i[fn_zero])) + + tn_eq = (rho[tn_zero] * area[tn_zero] * cp_b[tn_zero] * (1 / dt) + * (t_to_node - t_to_node_vor_zero) + - alpha[tn_zero] * (t_amb[tn_zero] - t_to_node)) + + fn_deriv = (rho[fn_zero] * area[fn_zero] * cp_b[fn_zero] * (1 / dt) + alpha[fn_zero]) + tn_deriv = (rho[tn_zero] * area[tn_zero] * cp_b[tn_zero] * (1 / dt) + alpha[tn_zero]) - return fn, dfn_dt, fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout, infeed + fn_nodes, fn_eq_sum, fn_deriv_sum = _sum_by_group(False, from_nodes[fn_zero], fn_eq, fn_deriv) + tn_nodes, tn_eq_sum, tn_deriv_sum = _sum_by_group(False, to_nodes[tn_zero], tn_eq, tn_deriv) + + fn[fn_nodes] += fn_eq_sum + fn[tn_nodes] += tn_eq_sum + dfn_dt[fn_nodes] += fn_deriv_sum + dfn_dt[tn_nodes] += tn_deriv_sum + else: + fn[~nodes_flow] = amb - t_init_n[~nodes_flow] + dfn_dt[~nodes_flow] = - np.ones(np.sum(~nodes_flow)) + + return fn, dfn_dt def calc_lambda_nikuradse_incomp_np(m, d, k, eta, area): m_abs = np.abs(m) - re = np.divide(m_abs * d, eta * area) + re = m_abs * d / (eta * area) lambda_laminar = np.zeros_like(m) lambda_laminar[~np.isclose(re, 0)] = 64 / re[~np.isclose(re, 0)] - lambda_nikuradse = np.divide(1, (-2 * np.log10(k / (3.71 * d))) ** 2) + lambda_nikuradse = 1 / ((-2 * np.log10(k / (3.71 * d))) ** 2) return re, lambda_laminar, lambda_nikuradse @@ -246,74 +257,61 @@ def calc_medium_pressure_with_derivative_np(p_init_i_abs, p_init_i1_abs): return p_m, der_p_m, der_p_m1 -def colebrook_np(re, d, k, lambda_nikuradse, dummy, max_iter): +def colebrook_np(re, d, k, lambda_nikuradse, max_iter, lengths, tolerance=1e-4): + """Function calculates the friction factor of a pipe using the Colebrook-White equation. + + It is an implicit equation which is solved using the Newton-Raphson method. For pipes with + zero flow or zero length, the initial guess is returned. This should be uncritical, as the + pressure loss term will equal zero (lambda * u^2 * l / d). + + :param re: Reynolds number [dimensionless] + :type re: np.array + :param d: Diameter [m] + :type d: np.array + :param k: Roughness [m] + :type k: np.array + :param lambda_nikuradse: Initial guess for lambda (from Nikuradse) + :type lambda_nikuradse: np.array + :param max_iter: Maximum number of iterations for the Colebrook-White calculation + :type max_iter: int + :param lengths: Length of the pipes [m] - only used to identify zero-length pipes + :type lengths: np.array + :param tolerance: Tolerance for the Colebrook-White calculation + :type tolerance: float + :return: lambda_cb, converged + 1. lambda_cb: Friction factor according to Colebrook-White + 2. converged: True, if the Colebrook-White calculation converged for all pipes + :rtype: (np.array, bool) """ + def colebrook_white_implicit(lambda_cb, re_nz, k_nz, d_nz): + return lambda_cb ** (-1 / 2) + 2 * np.log10(2.51 / (re_nz * np.sqrt(lambda_cb)) + k_nz / (3.71 * d_nz)) - :param re: - :type re: - :param d: - :type d: - :param k: - :type k: - :param lambda_nikuradse: - :type lambda_nikuradse: - :param dummy: - :type dummy: - :param max_iter: - :type max_iter: - :return: lambda_cb - :rtype: - """ - lambda_cb = lambda_nikuradse - converged = False - error_lambda = [] - niter = 0 - mask = ~np.isclose(re, 0) - f = np.zeros_like(lambda_cb) - df = np.zeros_like(lambda_cb) - x = np.zeros_like(lambda_cb) - re_nz = re[mask] - k_nz = k[mask] - d_nz = d[mask] - # Inner Newton-loop for calculation of lambda - while not converged and niter < max_iter: - - f[mask] = lambda_cb[mask] ** (-1 / 2) + 2 * np.log10(2.51 / (re_nz * np.sqrt(lambda_cb[mask])) + k_nz / (3.71 * d_nz)) - - df[mask]= -1 / 2 * lambda_cb[mask] ** (-3 / 2) - (2.51 / re_nz) * lambda_cb[mask] ** (-3 / 2) \ - / (np.log(10) * (2.51 / (re_nz * np.sqrt(lambda_cb[mask])) + k_nz / (3.71 * d_nz))) + def cw_derivative(lambda_cb, re_nz, k_nz, d_nz): + return -1 / 2 * lambda_cb ** (-3 / 2) - (2.51 / re_nz) * lambda_cb ** (-3 / 2) / ( + np.log(10) * (2.51 / (re_nz * np.sqrt(lambda_cb)) + k_nz / (3.71 * d_nz))) - x[mask] = - f[mask] / df[mask] + mask = ~np.isclose(re, 0) & ~np.isclose(lengths, 0, rtol=1e-10, atol=1e-11) + lambda_res = lambda_nikuradse - lambda_cb_old = lambda_cb - lambda_cb = lambda_cb + x + if not mask.any(): + return True, lambda_res - dx = np.abs(lambda_cb - lambda_cb_old) * dummy - error_lambda.append(linalg.norm(dx) / (len(dx))) + res = newton(colebrook_white_implicit, lambda_res[mask], maxiter=max_iter, args=(re[mask], k[mask], d[mask]), + tol=tolerance, full_output=True, fprime=cw_derivative) # , fprime2=cw_derivative_2) - if error_lambda[niter] <= 1e-4: - converged = True - - niter += 1 + if lambda_res[mask].size == 1: + lambda_res[mask] = res[0] + converged = res[1].converged + else: + lambda_res[mask] = res.root + converged = np.all(res.converged) - return converged, lambda_cb + return converged, lambda_res def calc_derived_values_np(node_pit, from_nodes, to_nodes): - tinit_branch = (node_pit[from_nodes, TINIT_NODE] + node_pit[to_nodes, TINIT_NODE]) / 2 - height_difference = node_pit[from_nodes, HEIGHT] - node_pit[to_nodes, HEIGHT] - p_init_i_abs = node_pit[from_nodes, PINIT] + node_pit[from_nodes, PAMB] - p_init_i1_abs = node_pit[to_nodes, PINIT] + node_pit[to_nodes, PAMB] - return tinit_branch, height_difference, p_init_i_abs, p_init_i1_abs - -def _branches_not_zero_flow(branch_pit): - """ - Simple function to identify branches with flow based on the calculated velocity. - - :param branch_pit: The pandapipes internal table of the network (including hydraulics results) - :type branch_pit: np.array - :return: branches_connected_flow - lookup array if branch is connected wrt. flow - :rtype: np.array - """ - # TODO: is this formulation correct or could there be any caveats? - return ~np.isnan(branch_pit[:, MDOTINIT]) & ~np.isclose(branch_pit[:, MDOTINIT], 0, rtol=1e-10, atol=1e-10) \ No newline at end of file + tinit_branch = (node_pit[from_nodes, IdxNode.TINIT] + node_pit[to_nodes, IdxNode.TINIT]) / 2 + height_difference = node_pit[from_nodes, IdxNode.HEIGHT] - node_pit[to_nodes, IdxNode.HEIGHT] + p_init_i_abs = node_pit[from_nodes, IdxNode.PINIT] + node_pit[from_nodes, IdxNode.PAMB] + p_init_i1_abs = node_pit[to_nodes, IdxNode.PINIT] + node_pit[to_nodes, IdxNode.PAMB] + return tinit_branch, height_difference, p_init_i_abs, p_init_i1_abs \ No newline at end of file diff --git a/src/pandapipes/pf/derivative_toolbox_numba.py b/src/pandapipes/pf/derivative_toolbox_numba.py index 8e24f11d6..8209a3366 100644 --- a/src/pandapipes/pf/derivative_toolbox_numba.py +++ b/src/pandapipes/pf/derivative_toolbox_numba.py @@ -1,11 +1,9 @@ import numpy as np -from numpy import linalg from pandapipes.constants import P_CONVERSION, GRAVITATION_CONSTANT, NORMAL_PRESSURE, \ NORMAL_TEMPERATURE -from pandapipes.idx_branch import LENGTH, LAMBDA, D, LOSS_COEFFICIENT as LC, PL, AREA, \ - MDOTINIT, FROM_NODE, TOUTINIT, TEXT, ALPHA, TL, QEXT, DO -from pandapipes.idx_node import HEIGHT, PAMB, PINIT, TINIT as TINIT_NODE +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode try: from numba import jit @@ -15,10 +13,36 @@ from numpy import int32, float64, int64, bool from typing import Optional as optional - -@jit((float64[:, :], float64[:], float64[:], float64[:], float64[:], float64[:]), nopython=True, cache=False) +# numba's nopython mode can resolve a plain module-level int global, or a plain int accessed via +# module.CONST, as a compile-time constant - but IdxBranch/IdxNode are classes (see +# pandapipes.idx.IndexMeta), and numba has no typing rule for an arbitrary class object used as a +# global, so `IdxBranch.MDOTINIT` inside a @jit(nopython=True) function body fails to compile +# ("Untyped global name 'IdxBranch'..."). Bind the specific columns this module's jitted functions +# need as plain module-level ints once here, and reference those bare names inside every +# @jit(nopython=True) function below instead of the class attribute. +BRANCH_MDOTINIT = IdxBranch.MDOTINIT +BRANCH_LENGTH = IdxBranch.LENGTH +BRANCH_D = IdxBranch.D +BRANCH_DO = IdxBranch.DO +BRANCH_LOSS_COEFFICIENT = IdxBranch.LOSS_COEFFICIENT +BRANCH_LAMBDA = IdxBranch.LAMBDA +BRANCH_PL = IdxBranch.PL +BRANCH_TL = IdxBranch.TL +BRANCH_QEXT = IdxBranch.QEXT +BRANCH_TEXT = IdxBranch.TEXT +BRANCH_ALPHA = IdxBranch.ALPHA +BRANCH_FROM_NODE = IdxBranch.FROM_NODE +BRANCH_TOUTINIT = IdxBranch.TOUTINIT + +NODE_TINIT = IdxNode.TINIT +NODE_HEIGHT = IdxNode.HEIGHT +NODE_PINIT = IdxNode.PINIT +NODE_PAMB = IdxNode.PAMB + + +@jit((float64[:, :], float64[:], float64[:], float64[:], float64[:], float64[:], float64[:]), nopython=True, cache=False) def derivatives_hydraulic_incomp_numba(branch_pit, der_lambda, p_init_i_abs, p_init_i1_abs, - height_difference, rho): + height_difference, rho, area): le = der_lambda.shape[0] load_vec = np.zeros_like(der_lambda) df_dm = np.zeros_like(der_lambda) @@ -30,30 +54,30 @@ def derivatives_hydraulic_incomp_numba(branch_pit, der_lambda, p_init_i_abs, p_i dp_frict_loss = np.zeros_like(der_lambda) for i in range(le): - m_init_abs = np.abs(branch_pit[i][MDOTINIT]) + m_init_abs = np.abs(branch_pit[i][BRANCH_MDOTINIT]) m_abs_deriv = max(m_init_abs, 1e-8) - m_init2 = m_init_abs * branch_pit[i][MDOTINIT] + m_init2 = m_init_abs * branch_pit[i][BRANCH_MDOTINIT] p_diff = p_init_i_abs[i] - p_init_i1_abs[i] const_height = rho[i] * GRAVITATION_CONSTANT * height_difference[i] / P_CONVERSION - friction_term = np.divide(branch_pit[i][LENGTH] * branch_pit[i][LAMBDA], branch_pit[i][D]) \ - + branch_pit[i][LC] - const_term = np.divide(1, branch_pit[i][AREA] ** 2 * rho[i] * P_CONVERSION * 2) + friction_term = np.divide(branch_pit[i][BRANCH_LENGTH] * branch_pit[i][BRANCH_LAMBDA], branch_pit[i][BRANCH_D]) \ + + branch_pit[i][BRANCH_LOSS_COEFFICIENT] + const_term = np.divide(1, area[i] ** 2 * rho[i] * P_CONVERSION * 2) df_dm[i] = -1. * const_term * (2 * m_abs_deriv * friction_term + der_lambda[i] - * np.divide(branch_pit[i][LENGTH], branch_pit[i][D]) * m_init2) + * np.divide(branch_pit[i][BRANCH_LENGTH], branch_pit[i][BRANCH_D]) * m_init2) - load_vec[i] = p_diff + branch_pit[i][PL] + const_height - const_term * m_init2 * friction_term + load_vec[i] = p_diff + branch_pit[i][BRANCH_PL] + const_height - const_term * m_init2 * friction_term - load_vec_nodes_from[i] = branch_pit[i][MDOTINIT] - load_vec_nodes_to[i] = branch_pit[i][MDOTINIT] + load_vec_nodes_from[i] = branch_pit[i][BRANCH_MDOTINIT] + load_vec_nodes_to[i] = branch_pit[i][BRANCH_MDOTINIT] dp_frict_loss[i] = const_term * m_init2 * friction_term return load_vec, load_vec_nodes_from, load_vec_nodes_to, df_dm, df_dm_nodes, df_dp, df_dp1, dp_frict_loss @jit((float64[:, :], float64[:, :], float64[:], float64[:], float64[:], float64[:], float64[:], float64[:], - float64[:], float64[:], float64[:], float64[:]), nopython=True, cache=False) + float64[:], float64[:], float64[:], float64[:], float64[:]), nopython=True, cache=False) def derivatives_hydraulic_comp_numba(node_pit, branch_pit, lambda_, der_lambda, p_init_i_abs, p_init_i1_abs, - height_difference, comp_fact, der_comp, der_comp1, rho, rho_n): + height_difference, comp_fact, der_comp, der_comp1, rho, rho_n, area): le = lambda_.shape[0] load_vec = np.zeros_like(lambda_) df_dm = np.zeros_like(lambda_) @@ -62,28 +86,27 @@ def derivatives_hydraulic_comp_numba(node_pit, branch_pit, lambda_, der_lambda, load_vec_nodes_from = np.zeros_like(der_lambda) load_vec_nodes_to = np.zeros_like(der_lambda) df_dm_nodes = np.ones_like(der_lambda) - from_nodes = branch_pit[:, FROM_NODE].astype(np.int32) + from_nodes = branch_pit[:, BRANCH_FROM_NODE].astype(np.int32) dp_frict_loss = np.zeros_like(der_lambda) # Formulas for gas pressure loss according to laminar version for i in range(le): # compressibility settings - m_init_abs = np.abs(branch_pit[i][MDOTINIT]) + m_init_abs = np.abs(branch_pit[i][BRANCH_MDOTINIT]) m_abs_deriv = max(m_init_abs, 1e-8) - m_init2 = branch_pit[i][MDOTINIT] * m_init_abs + m_init2 = branch_pit[i][BRANCH_MDOTINIT] * m_init_abs p_diff = p_init_i_abs[i] - p_init_i1_abs[i] p_sum = p_init_i_abs[i] + p_init_i1_abs[i] p_sum_div = np.divide(1, p_sum) fn = from_nodes[i] - tm = (node_pit[fn, TINIT_NODE] + branch_pit[i][TOUTINIT]) / 2 + tm = (node_pit[fn, NODE_TINIT] + branch_pit[i][BRANCH_TOUTINIT]) / 2 const_height = rho[i] * GRAVITATION_CONSTANT * height_difference[i] / P_CONVERSION - friction_term = np.divide(lambda_[i] * branch_pit[i][LENGTH], branch_pit[i][D]) + \ - branch_pit[i][LC] - normal_term = np.divide(NORMAL_PRESSURE, NORMAL_TEMPERATURE * P_CONVERSION * rho_n[i] * - branch_pit[i][AREA] ** 2) + friction_term = np.divide(lambda_[i] * branch_pit[i][BRANCH_LENGTH], branch_pit[i][BRANCH_D]) + \ + branch_pit[i][BRANCH_LOSS_COEFFICIENT] + normal_term = np.divide(NORMAL_PRESSURE, NORMAL_TEMPERATURE * P_CONVERSION * rho_n[i] * area[i] ** 2) - load_vec[i] = p_diff + branch_pit[i][PL] + const_height \ + load_vec[i] = p_diff + branch_pit[i][BRANCH_PL] + const_height \ - normal_term * comp_fact[i] * m_init2 * friction_term * p_sum_div * tm const_term = normal_term * m_init2 * friction_term * tm @@ -91,10 +114,17 @@ def derivatives_hydraulic_comp_numba(node_pit, branch_pit, lambda_, der_lambda, df_dp1[i] = -1. - const_term * p_sum_div * (der_comp1[i] - comp_fact[i] * p_sum_div) df_dm[i] = -1. * normal_term * comp_fact[i] * p_sum_div * tm * (2 * m_abs_deriv * friction_term - + np.divide(der_lambda[i] * branch_pit[i][LENGTH] * m_init2, branch_pit[i][D])) - - load_vec_nodes_from[i] = branch_pit[i][MDOTINIT] - load_vec_nodes_to[i] = branch_pit[i][MDOTINIT] + + np.divide(der_lambda[i] * branch_pit[i][BRANCH_LENGTH] * m_init2, branch_pit[i][BRANCH_D])) + # matches derivatives_hydraulic_comp_np's df_dm[np.isclose(m_init_abs, 0)] = 1. - at + # exactly zero flow, m_init2 is 0 so only the friction_term part of df_dm survives, and + # for a zero-length/zero-loss-coefficient branch (e.g. a parallel dummy branch) + # friction_term is 0 too, leaving df_dm exactly 0: a singular Jacobian diagonal entry for + # that branch's momentum row. + if m_init_abs <= 1e-8: + df_dm[i] = 1. + + load_vec_nodes_from[i] = branch_pit[i][BRANCH_MDOTINIT] + load_vec_nodes_to[i] = branch_pit[i][BRANCH_MDOTINIT] dp_frict_loss[i] = normal_term * comp_fact[i] * m_init2 * friction_term * p_sum_div * tm return load_vec, load_vec_nodes_from, load_vec_nodes_to, df_dm, df_dm_nodes, df_dp, df_dp1, dp_frict_loss @@ -107,7 +137,7 @@ def _make_lookups(branch_pit, to_nodes, from_nodes): club_from = np.zeros(max_val_from + 1, dtype=bool) branches_flow = np.zeros_like(to_nodes, dtype=bool) for i in range(len(to_nodes)): - mdot = branch_pit[i, MDOTINIT] + mdot = branch_pit[i, BRANCH_MDOTINIT] branches_flow[i] = (not np.isnan(mdot)) and (abs(mdot) > 1e-10) if branches_flow[i]: club_to[to_nodes[i]] = True @@ -157,26 +187,26 @@ def derivatives_thermal_numba(node_pit, branch_pit, if ~transient and ~nodes_flow[i]: fn[i] = amb - t_init_n[i] - dfn_dt[i] = 1. + dfn_dt[i] = -1. for i in range(b): # this is not required currently, but useful when implementing leakages - # m_init_i = np.abs(branch_pit[:, MDOTINIT]) - # m_init_i1 = np.abs(branch_pit[:, MDOTINIT]) - mdot = np.abs(branch_pit[i][MDOTINIT]) - t_amb = branch_pit[i][TEXT] - length = branch_pit[i][LENGTH] - alpha = branch_pit[i][ALPHA] * np.pi * branch_pit[i][DO] - tl = branch_pit[i][TL] - qext = branch_pit[i][QEXT] + # m_init_i = np.abs(branch_pit[:, BRANCH_MDOTINIT]) + # m_init_i1 = np.abs(branch_pit[:, BRANCH_MDOTINIT]) + mdot = np.abs(branch_pit[i][BRANCH_MDOTINIT]) + t_amb = branch_pit[i][BRANCH_TEXT] + length = branch_pit[i][BRANCH_LENGTH] + alpha = branch_pit[i][BRANCH_ALPHA] * np.pi * branch_pit[i][BRANCH_DO] + tl = branch_pit[i][BRANCH_TL] + qext = branch_pit[i][BRANCH_QEXT] fnt[i] = cp_n[i] * mdot * (t_init_i1[i] - t_init_nt[i]) dfnt_dt[i] = - cp_n[i] * mdot dfnt_dtout[i] = cp_n[i] * mdot if transient: - area = branch_pit[i][AREA] - tvor = branch_pit_old[i][branch_pit_old_lookup[TOUTINIT]] + area = np.pi * (branch_pit[i][BRANCH_D] / 2) ** 2 + tvor = branch_pit_old[i][branch_pit_old_lookup[BRANCH_TOUTINIT]] fb[i] = ( rho[i] * area * cp_b[i] * (t_init_i1[i] - tvor) * (1 / dt) * length @@ -187,7 +217,7 @@ def derivatives_thermal_numba(node_pit, branch_pit, dfb_dt[i] = - cp_b[i] * mdot dfb_dtout[i] = rho[i] * area * cp_b[i] / dt * length + cp_b[i] * mdot + alpha * length - if ~branches_flow[i] & (abs(branch_pit[i][LENGTH] < 1.e-8)): + if ~branches_flow[i] & (abs(branch_pit[i][BRANCH_LENGTH] < 1.e-8)): fb[i] = rho[i] * area * cp_b[i] * (t_init_i1[i] - tvor) * (1 / dt) - alpha * (t_amb - t_init_i1[i]) + qext dfb_dt[i] = 0 dfb_dtout[i] = rho[i] * area * cp_b[i] / dt + alpha @@ -195,19 +225,19 @@ def derivatives_thermal_numba(node_pit, branch_pit, fn_zero = ~nodes_flow[from_nodes[i]] tn_zero = ~nodes_flow[to_nodes[i]] if fn_zero: - t_from_node_vor_zero = node_pit_old[from_nodes[i], node_pit_old_lookup[TINIT_NODE]] + t_from_node_vor_zero = node_pit_old[from_nodes[i], node_pit_old_lookup[NODE_TINIT]] fn_eq = (rho[i] * area * cp_b[i] * (1 / dt) * (t_init_i[i] - t_from_node_vor_zero) - alpha * (t_amb - t_init_i[i])) fn_deriv = rho[i] * area * cp_b[i] * (1 / dt) + alpha - dfn_dt[from_nodes[i]] -= fn_deriv + dfn_dt[from_nodes[i]] += fn_deriv fn[from_nodes[i]] += fn_eq if tn_zero: - t_to_node_vor_zero = node_pit_old[to_nodes[i], node_pit_old_lookup[TINIT_NODE]] - t_to_node = node_pit[to_nodes[i], TINIT_NODE] + t_to_node_vor_zero = node_pit_old[to_nodes[i], node_pit_old_lookup[NODE_TINIT]] + t_to_node = node_pit[to_nodes[i], NODE_TINIT] tn_eq = (rho[i] * area * cp_b[i] * (1 / dt) * (t_to_node - t_to_node_vor_zero) - alpha * (t_amb - t_to_node)) tn_deriv = (rho[i]* area * cp_b[i] * (1 / dt) + alpha) - dfn_dt[to_nodes[i]] -= tn_deriv + dfn_dt[to_nodes[i]] += tn_deriv fn[to_nodes[i]] += tn_eq else: if branches_flow[i]: @@ -226,6 +256,140 @@ def derivatives_thermal_numba(node_pit, branch_pit, return fn, dfn_dt, fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout, infeed +@jit((float64[:, :], + float64[:, :], int32[:], + float64[:], float64[:], float64[:], + float64[:], float64[:], + float64[:], optional(float64), bool, float64), nopython=True, cache=False) +def derivatives_branch_thermal_numba(branch_pit, + branch_pit_old, branch_pit_old_lookup, + t_init_i, t_init_i1, t_init_nt, + cp_n, cp_b, + rho, dt, transient, amb): + b = t_init_nt.shape[0] + + fnt = np.zeros_like(t_init_nt) + dfnt_dt = np.zeros_like(t_init_nt) + dfnt_dtout = np.zeros_like(t_init_nt) + fb = np.zeros_like(t_init_nt) + dfb_dt = np.zeros_like(t_init_nt) + dfb_dtout = np.zeros_like(t_init_nt) + + branches_flow = np.zeros(b, dtype=bool) + for i in range(b): + mdot_val = branch_pit[i, BRANCH_MDOTINIT] + branches_flow[i] = (not np.isnan(mdot_val)) and (abs(mdot_val) > 1e-10) + + for i in range(b): + mdot = np.abs(branch_pit[i][BRANCH_MDOTINIT]) + t_amb = branch_pit[i][BRANCH_TEXT] + length = branch_pit[i][BRANCH_LENGTH] + alpha = branch_pit[i][BRANCH_ALPHA] * np.pi * branch_pit[i][BRANCH_DO] + tl = branch_pit[i][BRANCH_TL] + qext = branch_pit[i][BRANCH_QEXT] + + fnt[i] = cp_n[i] * mdot * (t_init_i1[i] - t_init_nt[i]) + dfnt_dt[i] = -cp_n[i] * mdot + dfnt_dtout[i] = cp_n[i] * mdot + + if transient: + area = np.pi * (branch_pit[i][BRANCH_D] / 2) ** 2 + tvor = branch_pit_old[i][branch_pit_old_lookup[BRANCH_TOUTINIT]] + + fb[i] = ( + rho[i] * area * cp_b[i] * (t_init_i1[i] - tvor) * (1 / dt) * length + + cp_b[i] * mdot * (-t_init_i[i] + t_init_i1[i] - tl) + - alpha * (t_amb - t_init_i1[i]) * length + qext + ) + dfb_dt[i] = -cp_b[i] * mdot + dfb_dtout[i] = rho[i] * area * cp_b[i] / dt * length + cp_b[i] * mdot + alpha * length + + if not branches_flow[i] and abs(branch_pit[i][BRANCH_LENGTH]) < 1e-8: + fb[i] = (rho[i] * area * cp_b[i] * (t_init_i1[i] - tvor) * (1 / dt) + - alpha * (t_amb - t_init_i1[i]) + qext) + dfb_dt[i] = 0 + dfb_dtout[i] = rho[i] * area * cp_b[i] / dt + alpha + else: + if branches_flow[i]: + fb[i] = ( + t_amb + (t_init_i[i] - t_amb) * np.exp(-alpha * length / (cp_b[i] * mdot)) + - t_init_i1[i] + tl - qext / (cp_b[i] * mdot) + ) + dfb_dt[i] = np.exp(-alpha * length / (cp_b[i] * mdot)) + else: + fb[i] = amb - t_init_i1[i] + fnt[i] = 0. + dfnt_dt[i] = 0. + dfnt_dtout[i] = 0. + dfb_dtout[i] = -1. + + return fnt, dfnt_dt, dfnt_dtout, fb, dfb_dt, dfb_dtout + + +@jit((float64[:, :], float64[:, :], + float64[:, :], int32[:], + int32[:], int32[:], + float64[:], float64[:], + float64[:], float64[:], + optional(float64), bool, float64), nopython=True, cache=False) +def derivatives_node_thermal_numba(node_pit, branch_pit, + node_pit_old, node_pit_old_lookup, + from_nodes, to_nodes, + t_init_i, t_init_n, + cp_b, rho, + dt, transient, amb): + n = t_init_n.shape[0] + b = from_nodes.shape[0] + + fn = np.zeros_like(t_init_n) + dfn_dt = np.zeros_like(t_init_n) + + if b == 0: + if not transient: + for i in range(n): + fn[i] = amb - t_init_n[i] + dfn_dt[i] = -1. + return fn, dfn_dt + + club_to, club_from, _ = _make_lookups(branch_pit, to_nodes, from_nodes) + nodes_flow = np.zeros(n, dtype=bool) + for i in range(n): + result_from = club_from[i] if (i < len(club_from)) else False + result_to = club_to[i] if (i < len(club_to)) else False + nodes_flow[i] = result_from | result_to + + if not transient and not nodes_flow[i]: + fn[i] = amb - t_init_n[i] + dfn_dt[i] = -1. + + if transient: + for i in range(b): + area = np.pi * (branch_pit[i][BRANCH_D] / 2) ** 2 + t_amb = branch_pit[i][BRANCH_TEXT] + alpha = branch_pit[i][BRANCH_ALPHA] * np.pi * branch_pit[i][BRANCH_DO] + + fn_zero = not nodes_flow[from_nodes[i]] + tn_zero = not nodes_flow[to_nodes[i]] + + if fn_zero: + t_from_vor = node_pit_old[from_nodes[i], node_pit_old_lookup[NODE_TINIT]] + fn_eq = (rho[i] * area * cp_b[i] * (1 / dt) * (t_init_i[i] - t_from_vor) + - alpha * (t_amb - t_init_i[i])) + fn_deriv = rho[i] * area * cp_b[i] * (1 / dt) + alpha + dfn_dt[from_nodes[i]] += fn_deriv + fn[from_nodes[i]] += fn_eq + if tn_zero: + t_to_vor = node_pit_old[to_nodes[i], node_pit_old_lookup[NODE_TINIT]] + t_to = node_pit[to_nodes[i], NODE_TINIT] + tn_eq = (rho[i] * area * cp_b[i] * (1 / dt) * (t_to - t_to_vor) + - alpha * (t_amb - t_to)) + tn_deriv = rho[i] * area * cp_b[i] * (1 / dt) + alpha + dfn_dt[to_nodes[i]] += tn_deriv + fn[to_nodes[i]] += tn_eq + + return fn, dfn_dt + + @jit((float64[:], float64[:], float64[:], float64[:], float64[:]), nopython=True) def calc_lambda_nikuradse_incomp_numba(m, d, k, eta, area): lambda_nikuradse = np.zeros_like(m) @@ -273,17 +437,45 @@ def calc_medium_pressure_with_derivative_numba(p_init_i_abs, p_init_i1_abs): return p_m, der_p_m, der_p_m1 -@jit((float64[:], float64[:], float64[:], float64[:], float64[:], int64), nopython=True) -def colebrook_numba(re, d, k, lambda_nikuradse, dummy, max_iter): +@jit((float64[:], float64[:], float64[:], float64[:], int64, float64[:], float64), nopython=True, cache=False) +def colebrook_numba(re, d, k, lambda_nikuradse, max_iter, lengths, tolerance): + """Numba counterpart of derivative_toolbox.colebrook_np, for calc_lambda's use_numba=True dispatch. + + Imported there as ``colebrook_numba as colebrook``, mirroring ``colebrook_np as + colebrook`` on the non-numba side - both share this same positional signature. + scipy.optimize.newton (colebrook_np's own Newton solver) isn't numba-jittable, so this + reimplements the same implicit equation + ``lambda_cb**(-1/2) + 2*log10(2.51/(re*sqrt(lambda_cb)) + k/(3.71*d)) = 0`` as a hand-rolled, + jit-compiled Newton loop instead. Mirrors colebrook_np's own masking - branches with ~zero Re + or ~zero length are left at their input lambda_nikuradse guess rather than Newton-iterated + (colebrook_np: ``~np.isclose(re, 0) & ~np.isclose(lengths, 0, rtol=1e-10, atol=1e-11)``, + reproduced here via explicit thresholds since np.isclose itself isn't numba-jittable) - and + takes the same configurable ``tolerance`` (net option "tolerance_colebrook"). + + :return: (converged, lambda_cb) - converged is False if max_iter is reached before every + active branch's Newton step drops below tolerance. + """ + n = re.shape[0] lambda_cb = lambda_nikuradse.copy() - lambda_cb_old = lambda_nikuradse.copy() + active = np.zeros(n, dtype=bool) + n_active = 0 + for i in range(n): + if abs(re[i]) > 1e-8 and abs(lengths[i]) > 1e-11: + active[i] = True + n_active += 1 + + if n_active == 0: + return True, lambda_cb + converged = False niter = 0 # Inner Newton-loop for calculation of lambda while not converged and niter < max_iter: - for i in range(len(lambda_cb)): - if (abs(re[i]) < 1.e-8): continue + max_step = 0.0 + for i in range(n): + if not active[i]: + continue sqt = np.sqrt(lambda_cb[i]) add_val = np.divide(k[i], (3.71 * d[i])) sqt_div = np.divide(1, sqt) @@ -293,15 +485,13 @@ def colebrook_numba(re, d, k, lambda_nikuradse, dummy, max_iter): f = sqt_div + 2 * np.log10(2.51 * re_div * sqt_div + add_val) df_dlambda_cb = - 0.5 * sqt_div3 - 2.51 * re_div * sqt_div3 * np.divide( 1, np.log(10) * (2.51 * re_div * sqt_div + add_val)) - x = - f / df_dlambda_cb - - lambda_cb_old[i] = lambda_cb[i] - lambda_cb[i] += x + step = - f / df_dlambda_cb + lambda_cb[i] += step - dx = np.abs(lambda_cb - lambda_cb_old) * dummy - error_lambda = linalg.norm(dx) / dx.shape[0] + abs_step = abs(step) + max_step = max(max_step, abs_step) - if error_lambda <= 1e-4: + if max_step < tolerance: converged = True niter += 1 @@ -319,8 +509,8 @@ def calc_derived_values_numba(node_pit, from_nodes, to_nodes): for i in range(le): fn = from_nodes[i] tn = to_nodes[i] - tinit_branch[i] = (node_pit[fn, TINIT_NODE] + node_pit[tn, TINIT_NODE]) / 2 - height_difference[i] = node_pit[fn, HEIGHT] - node_pit[tn, HEIGHT] - p_init_i_abs[i] = node_pit[fn, PINIT] + node_pit[fn, PAMB] - p_init_i1_abs[i] = node_pit[tn, PINIT] + node_pit[tn, PAMB] + tinit_branch[i] = (node_pit[fn, NODE_TINIT] + node_pit[tn, NODE_TINIT]) / 2 + height_difference[i] = node_pit[fn, NODE_HEIGHT] - node_pit[tn, NODE_HEIGHT] + p_init_i_abs[i] = node_pit[fn, NODE_PINIT] + node_pit[fn, NODE_PAMB] + p_init_i1_abs[i] = node_pit[tn, NODE_PINIT] + node_pit[tn, NODE_PAMB] return tinit_branch, height_difference, p_init_i_abs, p_init_i1_abs diff --git a/src/pandapipes/pf/internals_toolbox.py b/src/pandapipes/pf/internals_toolbox.py index a5ecdf5a9..f92625e23 100644 --- a/src/pandapipes/pf/internals_toolbox.py +++ b/src/pandapipes/pf/internals_toolbox.py @@ -5,7 +5,7 @@ import numpy as np import logging -from pandapipes.idx_branch import FROM_NODE_T_SWITCHED, TO_NODE, FROM_NODE +from pandapipes.idx_branch import IdxBranch try: from numba import jit @@ -18,9 +18,27 @@ logger = logging.getLogger(__name__) +def branch_area(branch_pit): + """Pipe cross-sectional area (pi*(D/2)**2) per branch row. + + A pure function of D, deliberately NOT cached as its own pit column (that AREA column + existed once and was removed, see git history "remove area and scale jacobi matrix"): D + can change mid-solve (e.g. optimize_dn's diameter sizing mutates it every outer + iteration), and a cached AREA column would need to be kept in perfect sync everywhere D + is written, or silently go stale - a correctness hazard worse than the recompute it would + save. Callers that need this more than once within the same computation (e.g. + calculate_derivatives_hydraulic, which uses it for both calc_lambda and its own + const_term) should call this once and pass the result along, rather than recomputing the + formula inline at each use site - that repeated-inline-formula pattern is what this + function replaces. + """ + return np.pi * (branch_pit[:, IdxBranch.D] / 2) ** 2 + + def _sum_by_group_sorted(indices, *values): - """Auxiliary function to sum up values by some given indices (both as numpy arrays). Expects the - indices and values to already be sorted. + """Auxiliary function to sum up values by some given indices (both as numpy arrays). + + Expects the indices and values to already be sorted. :param indices: :type indices: @@ -56,8 +74,7 @@ def _sum_by_group_sorted(indices, *values): def _sum_by_group_np(indices, *values): - """ - Auxiliary function to sum up values by some given indices (both as numpy arrays). + """Auxiliary function to sum up values by some given indices (both as numpy arrays). :param indices: :type indices: @@ -66,7 +83,6 @@ def _sum_by_group_np(indices, *values): :return: :rtype: """ - # sort indices and values by indices order = np.argsort(indices) indices = indices[order] @@ -95,8 +111,7 @@ def _sum_by_group_numba(indices, *values): return _sum_by_group_np(indices, *values) def _sum_by_group(use_numba, indices, *values): - """ - Auxiliary function to sum up values by some given indices (both as numpy arrays). + """Auxiliary function to sum up values by some given indices (both as numpy arrays). :param use_numba: :type use_numba: @@ -117,10 +132,10 @@ def _sum_by_group(use_numba, indices, *values): def select_from_pit(table_index_array, input_array, data): - """ - Auxiliary function to retrieve values from a table like a pit. Each data entry corresponds - to a table_index_array entry. Example: velocities are indexed by the corresponding - from_nodes stored in the pipe pit. + """Auxiliary function to retrieve values from a table like a pit. + + Each data entry corresponds to a table_index_array entry. Example: velocities are indexed + by the corresponding from_nodes stored in the pipe pit. The function inputs another array which consists of some table_index_array entries the user wants to retrieve. The function is used in pandapipes results evaluation. The input array is @@ -164,8 +179,7 @@ def max_nb(arr): def get_from_nodes_corrected(branch_pit, switch_from_to_col=None): - """ - Function to get corrected from nodes from the branch pit. + """Function to get corrected from nodes from the branch pit. Usually, this should be used if the velocity in a branch is negative, so that the\ flow goes from the to_node to the from_node. The parameter switch_from_to_col indicates\ @@ -180,14 +194,13 @@ def get_from_nodes_corrected(branch_pit, switch_from_to_col=None): :rtype: """ if switch_from_to_col is None: - switch_from_to_col = branch_pit[:, FROM_NODE_T_SWITCHED] - from_node_col = switch_from_to_col.astype(np.int32) * (TO_NODE - FROM_NODE) + FROM_NODE + switch_from_to_col = branch_pit[:, IdxBranch.FROM_NODE_T_SWITCHED] + from_node_col = switch_from_to_col.astype(np.int32) * (IdxBranch.TO_NODE - IdxBranch.FROM_NODE) + IdxBranch.FROM_NODE return branch_pit[np.arange(len(branch_pit)), from_node_col].astype(np.int32) def get_to_nodes_corrected(branch_pit, switch_from_to_col=None): - """ - Function to get corrected to nodes from the branch pit. + """Function to get corrected to nodes from the branch pit. Usually, this should be used if the velocity in a branch is negative, so that the\ flow goes from the to_node to the from_node. The parameter switch_from_to_col indicates\ @@ -202,6 +215,6 @@ def get_to_nodes_corrected(branch_pit, switch_from_to_col=None): :rtype: """ if switch_from_to_col is None: - switch_from_to_col = branch_pit[:, FROM_NODE_T_SWITCHED] - to_node_col = switch_from_to_col.astype(np.int32) * (FROM_NODE - TO_NODE) + TO_NODE + switch_from_to_col = branch_pit[:, IdxBranch.FROM_NODE_T_SWITCHED] + to_node_col = switch_from_to_col.astype(np.int32) * (IdxBranch.FROM_NODE - IdxBranch.TO_NODE) + IdxBranch.TO_NODE return branch_pit[np.arange(len(branch_pit)), to_node_col].astype(np.int32) diff --git a/src/pandapipes/pf/pipeflow_setup.py b/src/pandapipes/pf/pipeflow_setup.py index 34ebd64d5..f1ffcde68 100644 --- a/src/pandapipes/pf/pipeflow_setup.py +++ b/src/pandapipes/pf/pipeflow_setup.py @@ -8,20 +8,11 @@ from pandapower.auxiliary import ppException from scipy.sparse import coo_matrix, csgraph -from pandapipes.idx_branch import ( - TOUTINIT, - FROM_NODE, - TO_NODE, - branch_cols, - DIRECTED, - ACTIVE as ACTIVE_BR, - FLOW_RETURN_CONNECT, - ACTIVE, - ELEMENT_IDX as ELEMENT_IDX_BR, -) -from pandapipes.idx_node import NODE_TYPE, P, NODE_TYPE_T, node_cols, T, ACTIVE as ACTIVE_ND, \ - TABLE_IDX as TABLE_IDX_ND, ELEMENT_IDX as ELEMENT_IDX_ND, INFEED, GE, TINIT +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode from pandapipes.properties.fluids import get_fluid +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected +from pandapipes.pf.system_index import PitEntries, PitRegistry try: import numba @@ -45,15 +36,13 @@ "max_iter_bidirect": 10, "error_flag": False, "alpha": 1, "nonlinear_method": "constant", "mode": "hydraulics", "ambient_temperature": 293.15, "check_connectivity": True, - "max_iter_colebrook": 10, "only_update_hydraulic_matrix": False, - "reuse_internal_data": False, "use_numba": True, + "max_iter_colebrook": 10, "use_numba": True, "quit_on_inconsistency_connectivity": False, "calc_compression_power": True, "transient": False, "dt": None, "tolerance_colebrook": 1e-4,} def get_net_option(net, option_name): - """ - Returns the requested option of the given net. Raises a UserWarning if the option was not found. + """Returns the requested option of the given net. Raises a UserWarning if the option was not found. :param net: pandapipesNet for which option is requested :type net: pandapipesNet @@ -68,9 +57,7 @@ def get_net_option(net, option_name): def get_net_options(net, *option_names): - """ - Returns several requested options of the given net. Raises a UserWarning if any of the options - was not found. + """Returns several requested options of the given net. Raises a UserWarning if any of the options was not found. :param net: pandapipesNet for which option is requested :type net: pandapipesNet @@ -82,8 +69,7 @@ def get_net_options(net, *option_names): def set_net_option(net, option_name, option_value): - """ - Auxiliary function to set the value of a specific option (options are saved in a dict). + """Auxiliary function to set the value of a specific option (options are saved in a dict). :param net: pandapipesNet for which option shall be set :type net: pandapipesNet @@ -96,9 +82,7 @@ def set_net_option(net, option_name, option_value): def add_table_lookup(table_lookup, table_name, table_number): - """ - Auxiliary function to add a lookup between table name in the pandapipes net and table number in - the internal structure (pit). + """Auxiliary function to add a lookup between table name in the pandapipes net and table number in the internal structure (pit). :param table_lookup: The lookup dictionary from table names to internal number (n2t) and vice \ versa (t2n) @@ -114,8 +98,7 @@ def add_table_lookup(table_lookup, table_name, table_number): def get_table_number(table_lookup, table_name): - """ - Auxiliary function to retrieve the internal pit number for a given pandapipes net table name \ + """Auxiliary function to retrieve the internal pit number for a given pandapipes net table name \ from the table lookup. :param table_lookup: The lookup dictionary from table names to internal number (n2t) and vice \ @@ -132,8 +115,7 @@ def get_table_number(table_lookup, table_name): def get_table_name(table_lookup, table_number): - """ - Auxiliary function to retrieve the pandapipes net table name for a given internal pit number \ + """Auxiliary function to retrieve the pandapipes net table name for a given internal pit number \ from the table lookup. :param table_lookup: The lookup dictionary from table names to internal number (n2t) and vice \ @@ -151,8 +133,7 @@ def get_table_name(table_lookup, table_number): def get_lookup(net, pit_type="node", lookup_type="index"): - """ - Returns internal lookups which are mostly defined in the function `create_lookups`. + """Returns internal lookups which are mostly defined in the function `create_lookups`. :param net: The pandapipes net for which the lookup is requested :type net: pandapipesNet @@ -167,29 +148,29 @@ def get_lookup(net, pit_type="node", lookup_type="index"): """ pit_type = pit_type.lower() lookup_type = lookup_type.lower() - all_lookup_types = ["index", "table", "from_to", "active_hydraulics", "active_heat_transfer", + all_lookup_types = ["index", "table", "from_to", "active", + "active_hydraulics", "active_heat_transfer", "length", "from_to_active_hydraulics", "from_to_active_heat_transfer", - "index_active_hydraulics", "index_active_heat_transfer", "zero_flow", - "old_pit_cols"] + "index_active_hydraulics", "index_active_heat_transfer", "old_pit_cols"] if lookup_type not in all_lookup_types: type_names = "', '".join(all_lookup_types) - logger.error("No lookup type '%s' exists. Please choose one of '%s'." - % (lookup_type, type_names)) + logger.error("No lookup type '%s' exists. Please choose one of '%s'.", + lookup_type, type_names) return None if pit_type not in ["node", "branch"]: - logger.error("No pit type '%s' exists. Please choose one of 'node' and 'branch'." - % pit_type) + logger.error("No pit type '%s' exists. Please choose one of 'node' and 'branch'.", + pit_type) return None return net["_lookups"]["%s_%s" % (pit_type, lookup_type)] def set_user_pf_options(net, reset=False, **kwargs): - """ - This function sets the "user_pf_options" dictionary for net. These options overrule - net._internal_options once they are added to net. These options are used in configuration of - load flow calculation. - At the same time, user-defined arguments for `pandapipes.pipeflow()` always have a higher - priority. To remove user_pf_options, set "reset = True" and provide no additional arguments. + """Set the "user_pf_options" dictionary for net. + + These options overrule net._internal_options once they are added to net. These options are + used in configuration of load flow calculation. At the same time, user-defined arguments for + `pandapipes.pipeflow()` always have a higher priority. To remove user_pf_options, set + "reset = True" and provide no additional arguments. :param net: pandapipes network for which to create user options :type net: pandapipesNet @@ -201,18 +182,16 @@ def set_user_pf_options(net, reset=False, **kwargs): if reset or 'user_pf_options' not in net.keys(): net['user_pf_options'] = dict() - additional_kwargs = set(kwargs.keys()) - set(default_options.keys()) - {"fluid", "hyd_flag"} + additional_kwargs = set(kwargs.keys()) - set(default_options.keys()) - {"fluid"} if len(additional_kwargs) > 0: - logger.info('parameters %s are not in the list of standard options' - % list(additional_kwargs)) + logger.info('parameters %s are not in the list of standard options', + list(additional_kwargs)) net.user_pf_options.update(kwargs) def init_options(net, **kwargs): - """ - Initializes physical and mathematical constants included in pandapipes. In addition, options - for the nonlinear and time-dependent solver are also set. + """Initializes physical and mathematical constants included in pandapipes. In addition, options for the nonlinear and time-dependent solver are also set. Those are the options that can be set and their default values: @@ -253,11 +232,6 @@ def init_options(net, **kwargs): solely hydraulics ('hydraulics'), solely heat transfer('heat') or both combined sequentially \ ('sequential') or bidirectionally ('bidirectional'). - - **only_update_hydraulic_matrix** (bool): False - If True, the system matrix is not \ - created in every iteration, but only the data is updated according to a lookup that\ - is identified in the first iteration. This speeds up calculation, but has not yet\ - been tested extensively. - - **check_connectivity** (bool): True - If True, a connectivity check is performed at the\ beginning of the pipeflow and parts of the net that are not connected to external\ grids are set inactive. @@ -300,8 +274,6 @@ def init_options(net, **kwargs): for k in keys_to_exclude: opts.pop(k, None) - if not opts["only_update_hydraulic_matrix"]: - opts["reuse_internal_data"] = False if not numba_installed: if opts["use_numba"]: logger.info( @@ -345,8 +317,7 @@ def _mode_check(opts): opts["mode"] = "sequential" def create_internal_results(net): - """ - Initializes a dictionary that shall contain some internal results later. + """Initializes a dictionary that shall contain some internal results later. :param net: pandapipes net to which internal result dict will be added :type net: pandapipesNet @@ -356,9 +327,9 @@ def create_internal_results(net): def write_internal_results(net, **kwargs): - """ - Adds specified values to the internal result dictionary of the given pandapipes net. If internal - results are not yet defined for the net, they are created as well. + """Add specified values to the internal result dictionary of the given pandapipes net. + + If internal results are not yet defined for the net, they are created as well. :param net: pandapipes net for which to update internal result dict :type net: pandapipesNet @@ -371,11 +342,30 @@ def write_internal_results(net, **kwargs): net["_internal_results"].update(kwargs) -def initialize_pit(net): +def _drop_pit_column(registry, col): + """Remove all (row, col) entries targeting ``col`` from a PitRegistry, preserving any other columns bundled in the same PitEntries. + + Used so a reused pit's already-solved values (e.g. MDOTINIT/PINIT for a standalone + heat-transfer run) aren't reset to fresh initial guesses by component registration. + + :param registry: the PitRegistry to filter in place + :type registry: pandapipes.pf.system_index.PitRegistry + :param col: PIT column index to drop + :type col: int + :return: No output """ - Initializes and fills the internal structure which is called pit (pandapipes internal tables). - The structure is a dictionary which should contain one array for all nodes and one array for all - branches of the net (c.f. also `create_empty_pit`). + for bucket in (registry.normal, registry.overrides): + for i, e in enumerate(bucket): + keep = e.cols != col + if not np.all(keep): + bucket[i] = PitEntries(e.rows[keep], e.cols[keep], e.data[keep], e.mode) + + +def initialize_pit(net): + """Initializes and fills the internal structure which is called pit (pandapipes internal tables). + + The structure is a dictionary which should contain one array for all nodes and one array for + all branches of the net (c.f. also `create_empty_pit`). :param net: The pandapipes network for which to create and fill the internal structure :type net: pandapipesNet @@ -383,35 +373,70 @@ def initialize_pit(net): :rtype: tuple(np.array) """ - if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0: - create_lookups(net) - pit = create_empty_pit(net) + if (not get_net_option(net, "transient") or + get_net_option(net, "simulation_time_step") == 0 + ): + if get_net_option(net, "mode") == "heat": + if "_pit" not in net: + raise UserWarning("There are no hydraulic results given!") + # net.converged reflects the outcome of whichever hydraulics run last populated + # "_pit" (Calculation.run() sets it at the start and updates it every iteration of + # that SAME call, so it can't be stale from some unrelated, older run) - a caller that + # catches PipeflowNotConverged from a hydraulics run and continues (e.g. to keep + # processing a batch of nets) would otherwise silently get a standalone heat-transfer + # solve built on top of the unconverged, physically meaningless mdot/p values that + # non-convergent run still left behind, with a correct-looking net.converged=True + # from the heat run's OWN convergence masking that the underlying hydraulics never + # actually converged. + if not net.converged: + raise PipeflowNotConverged( + "The hydraulic calculation has not converged - a standalone heat-transfer " + "run cannot be based on its results. Run a converged hydraulics/sequential " + "pipeflow first.") + pit = net["_pit"] + else: + pit = create_empty_pit(net) else: pit = net["_pit"] if get_net_option(net, "transient") and get_net_option(net,"simulation_time_step") != 0 and net.converged: - create_old_pit(net, [TINIT], [TOUTINIT]) + create_old_pit(net, [IdxNode.TINIT], [IdxBranch.TOUTINIT]) + + node_pit = pit["node"] + branch_pit = pit["branch"] + branch_registry = PitRegistry() + node_registry = PitRegistry() for comp in net['component_list']: - comp.create_pit_node_entries(net, pit["node"]) - comp.create_pit_branch_entries(net, pit["branch"]) + comp.register_pit_branch_entries(net, branch_pit, node_pit, branch_registry) + comp.register_pit_node_entries(net, node_pit, node_registry) comp.create_component_array(net, pit["components"]) + if get_net_option(net, "mode") == "heat": + # a standalone heat-transfer run reuses the pit from a prior hydraulic solve (see above) - + # keep its already-solved MDOTINIT/PINIT instead of letting component registration reset + # them to fresh initial guesses + _drop_pit_column(branch_registry, IdxBranch.MDOTINIT) + _drop_pit_column(node_registry, IdxNode.PINIT) + + branch_registry.apply(branch_pit) + node_registry.apply(node_pit) + if not get_net_option(net, "transient") or get_net_option(net, "simulation_time_step") == 0 or not net.converged: # This needs to be done after the pit values are set - create_old_pit(net, [TINIT], [TOUTINIT]) + create_old_pit(net, [IdxNode.TINIT], [IdxBranch.TOUTINIT]) if len(pit["node"]) == 0: logger.warning("There are no nodes defined. " "You need at least one node! " "Without any nodes, you are not able to conduct a pipeflow!") - return + def create_empty_pit(net): - """ - Creates an empty internal structure which is called pit (pandapipes internal tables). The\ - structure is a dictionary which should contain one array for all nodes and one array for all\ - branches of the net. It is very often referred to within the pipeflow. So the structure in\ + """Creates an empty internal structure which is called pit (pandapipes internal tables). + + The structure is a dictionary which should contain one array for all nodes and one array for\ + all branches of the net. It is very often referred to within the pipeflow. So the structure in\ general looks like this: >>> net["_pit"] = {"node": np.array((no_nodes, col_nodes), dtype=np.float64), @@ -426,16 +451,17 @@ def create_empty_pit(net): node_length = get_lookup(net, "node", "length") branch_length = get_lookup(net, "branch", "length") # init empty pit - pit = {"node": np.empty((node_length, node_cols), dtype=np.float64), - "branch": np.empty((branch_length, branch_cols), dtype=np.float64), + pit = {"node": np.zeros((node_length, IdxNode.node_cols), dtype=np.float64), + "branch": np.zeros((branch_length, IdxBranch.branch_cols), dtype=np.float64), "components": {}} net["_pit"] = pit return pit def create_old_pit(net, required_node_cols=None, required_branch_cols=None): - """ - Creates an empty internal partial structure of the given internal structure which is called \ - old_pit (old pandapipes internal tables). The structure is a dictionary which should contain \ + """Creates an empty internal partial structure of the given internal structure which is called \ + old_pit (old pandapipes internal tables). + + The structure is a dictionary which should contain \ one array for all nodes and one array for all branches of the net. \ In general looks like this: @@ -469,8 +495,7 @@ def create_old_pit(net, required_node_cols=None, required_branch_cols=None): return pit def init_all_result_tables(net): - """ - Initialize the result tables of all components in the net. + """Initialize the result tables of all components in the net. :param net: pandapipes net for which to extract results into net.res_xy :type net: pandapipesNet @@ -482,8 +507,8 @@ def init_all_result_tables(net): def create_lookups(net): - """ - Create all lookups necessary for the pipeflow of the given net. + """Create all lookups necessary for the pipeflow of the given net. + The lookups are usually: - node_from_to: The start and end indices of all node component tables within the pit @@ -511,6 +536,7 @@ def create_lookups(net): internal_nodes = dict() internal_branches = dict() + # Phase 1: node and branch lookups for comp in net['component_list']: branch_from, branch_table_nr = comp.create_branch_lookups( net, branch_ft_lookups, branch_table_lookups, branch_idx_lookups, branch_from, branch_table_nr, @@ -526,126 +552,87 @@ def create_lookups(net): "internal_nodes": internal_nodes, "internal_branches": internal_branches} -def identify_active_nodes_branches(net, hydraulic=True): - """ - Function that creates the connectivity lookup for nodes and branches. If the option \ - "check_connectivity" is set, a full connectivity check is performed based on a sparse matrix \ - graph search. Otherwise, only the nodes and branches are identified that are inactive, which \ - means:\ - - in case of hydraulics, just use the "ACTIVE" identifier of the respective components\ - - in case of heat transfer, use the hydraulic result to check which branches are traversed \ - by the fluid and a simple rule to make sure that active nodes are connected to at least one\ - traversed branch\ - The result of this connectivity search is stored in the lookups (e.g. as \ - net["_lookups"]["node_active_hydraulics"]) +def hydraulic_slack_mask(net): + """Boolean mask over node_pit marking the hydraulic slacks (P-type nodes).""" + node_pit = net["_pit"]["node"] + return node_pit[:, IdxNode.NODE_TYPE] == IdxNode.P + + +def heat_transfer_slack_mask(net): + """Boolean mask over node_pit marking the heat-transfer slacks (T-/GE-type nodes).""" + node_pit = net["_pit"]["node"] + return (node_pit[:, IdxNode.NODE_TYPE_T] == IdxNode.T) | (node_pit[:, IdxNode.NODE_TYPE_T] == IdxNode.GE) + + +def identify_active_nodes_branches(net, slack_mask, active_node_lookup=None, active_branch_lookup=None): + """Create the connectivity lookup for nodes and branches. + + If the option "check_connectivity" is set, a full connectivity check is performed based on a + sparse matrix graph search starting from the nodes marked by ``slack_mask``. Otherwise, just + the "ACTIVE" identifier of the respective components is used. + + Hydraulics and heat transfer only differ in which nodes count as slacks (see + :func:`hydraulic_slack_mask` / :func:`heat_transfer_slack_mask`). Heat transfer additionally + narrows down a prior (hydraulic) result: pass its (nodes_connected, branches_connected) in via + ``active_node_lookup``/``active_branch_lookup`` so only branches actually reachable under both + the hydraulic AND the heat-transfer slack definition end up active. :param net: the pandapipes net for which to identify the connectivity :type net: pandapipes.pandapipesNet - :param hydraulic: flag for the mode (if True, do the check for the hydraulic simulation, \ - otherwise for the heat transfer simulation with other considerations) - :type hydraulic: bool, default True - :return: No output + :param slack_mask: boolean array over node_pit marking which nodes act as slacks + :type slack_mask: np.array(bool) + :param active_node_lookup: starting node connectivity to narrow down further; defaults to the + raw "ACTIVE" node identifier if not given + :type active_node_lookup: np.array(bool), optional + :param active_branch_lookup: starting branch connectivity to narrow down further; defaults to + the raw "ACTIVE" branch identifier if not given + :type active_branch_lookup: np.array(bool), optional + :return: (nodes_connected, branches_connected) + :rtype: tuple(np.array) """ - node_pit = net["_pit"]["node"] branch_pit = net["_pit"]["branch"] - if hydraulic: - nodes_connected = node_pit[:, ACTIVE_ND].astype(np.bool_) - branches_connected = branch_pit[:, ACTIVE_BR].astype(np.bool_) - if get_net_option(net, "check_connectivity"): - nodes_connected, branches_connected = check_connectivity(net, branch_pit, node_pit, - branches_connected, nodes_connected, - mode="hydraulics") - else: - nodes_connected = get_lookup(net, "node", "active_hydraulics") - branches_connected = get_lookup(net, "branch", "active_hydraulics") - if get_net_option(net, "check_connectivity"): - nodes_connected, branches_connected = check_connectivity(net, branch_pit, node_pit, - branches_connected, nodes_connected, - mode="heat_transfer") - - mode = "hydraulics" if hydraulic else "heat_transfer" + if active_node_lookup is None: + active_node_lookup = node_pit[:, IdxNode.ACTIVE].astype(np.bool_) + if active_branch_lookup is None: + active_branch_lookup = branch_pit[:, IdxBranch.ACTIVE].astype(np.bool_) + + nodes_connected = active_node_lookup + branches_connected = active_branch_lookup + if get_net_option(net, "check_connectivity"): + slacks = np.where(slack_mask & nodes_connected)[0] + nodes_connected, branches_connected = perform_connectivity_search( + net, node_pit, branch_pit, slacks, nodes_connected, branches_connected + ) + if np.all(~nodes_connected): - mode = 'hydraulic' if hydraulic else 'heat transfer' raise PipeflowNotConverged(" All nodes are set out of service. Probably they are not supplied." - " Therefore, the %s pipeflow did not converge. " - " Have you forgotten to define a supply component or is it not properly connected?" % mode) - net["_lookups"]["node_active_" + mode] = nodes_connected - net["_lookups"]["branch_active_" + mode] = branches_connected + " Therefore, the pipeflow did not converge. " + " Have you forgotten to define a supply component or is it not properly connected?") + return nodes_connected, branches_connected -def check_connectivity(net, branch_pit, node_pit, - branches_connected, nodes_connected, - mode="hydraulics"): - """ - Perform a connectivity check which means that network nodes are identified that don't have any - connection to an external grid component. Quick overview over the steps of this function: - - - Build a sparse matrix graph (scipy.sparse.csr_matrix) from all branches that are in_service\ - (nodes of this graph are taken from FROM_NODE and TO_NODE column in pit). - - Add a node that represents all external grids and connect all nodes that are connected to\ - external grids to that node. - - Perform a breadth first order search to identify all nodes that are reachable from the \ - added external grid node. - - Create masks for existing nodes and branches to show if they are reachable from an \ - external grid. - - Compare the reachable nodes with the initial in_service nodes.\n - - If nodes are reachable that were set out of service by the user, they are either set \ - in_service or an error is raised. The behavior depends on the pipeflow option \ - **quit_on_inconsistency_connectivity**. - - If nodes are not reachable that were set in_service by the user, they will be set out of\ - service automatically (this is the desired functionality of the connectivity check). - - :param net: The pandapipesNet for which to perform the check - :type net: pandapipesNet - :param branch_pit: Internal array with branch entries - :type branch_pit: np.array - :param node_pit: Internal array with node entries - :type node_pit: np.array - :param branches_connected: Array of bool if branches are connected or not - :type branches_connected: np.array(bool) - :param nodes_connected: Array of bool if nodes are connected or not - :type nodes_connected: np.array(bool) - :param mode: two modes exist: "hydraulics" and "heat_transfer", representing the two modes of \ - the pipeflow calculation. - :type mode: str - :return: (nodes_connected, branches_connected) - Lookups of np.arrays stating which of the - internal nodes and branches are reachable from any of the hyd_slacks (np mask). - :rtype: tuple(np.array) - """ - if mode == "hydraulics": - slacks = np.where((node_pit[:, NODE_TYPE] == P) & nodes_connected)[0] - else: - slacks = np.where(((node_pit[:, NODE_TYPE_T] == T) | (node_pit[:, NODE_TYPE_T] == GE)) & nodes_connected)[0] - - return perform_connectivity_search(net, node_pit, branch_pit, slacks, - nodes_connected, branches_connected, mode=mode) - - -def perform_connectivity_search(net, node_pit, branch_pit, slack_nodes, active_node_lookup, active_branch_lookup, - mode="hydraulics"): - if mode == 'hydraulics': - connect = branch_pit[:, FLOW_RETURN_CONNECT].astype(bool) - active_branch_lookup = active_branch_lookup & ~connect - nodes_connected, branches_connected = ( - _connectivity(net, branch_pit, node_pit, active_branch_lookup, active_node_lookup, slack_nodes, mode)) - from_nodes = branch_pit[:, FROM_NODE].astype(np.int32) - to_nodes = branch_pit[:, TO_NODE].astype(np.int32) - branch_active = branch_pit[:, ACTIVE].astype(bool) - active = nodes_connected[from_nodes] & nodes_connected[to_nodes] & branch_active - branches_connected[connect & active] = True - else: - nodes_connected, branches_connected = ( - _connectivity(net, branch_pit, node_pit, active_branch_lookup, active_node_lookup, slack_nodes, mode)) +def perform_connectivity_search(net, node_pit, branch_pit, slack_nodes, active_node_lookup, + active_branch_lookup): + connect = branch_pit[:, IdxBranch.FLOW_RETURN_CONNECT].astype(bool) + active_branch_lookup = active_branch_lookup & ~connect + nodes_connected, branches_connected = _connectivity( + net, branch_pit, node_pit, active_branch_lookup, active_node_lookup, slack_nodes + ) + from_nodes = branch_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + to_nodes = branch_pit[:, IdxBranch.TO_NODE].astype(np.int32) + branch_active = branch_pit[:, IdxBranch.ACTIVE].astype(bool) + active = nodes_connected[from_nodes] & nodes_connected[to_nodes] & branch_active + branches_connected[connect & active] = True return nodes_connected, branches_connected -def _connectivity(net, branch_pit, node_pit, active_branch_lookup, active_node_lookup, slack_nodes, mode): +def _connectivity(net, branch_pit, node_pit, active_branch_lookup, active_node_lookup, slack_nodes): len_nodes = len(node_pit) - from_nodes = branch_pit[:, FROM_NODE].astype(np.int32) - to_nodes = branch_pit[:, TO_NODE].astype(np.int32) - directed = branch_pit[:, DIRECTED].astype(bool) + from_nodes = branch_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + to_nodes = branch_pit[:, IdxBranch.TO_NODE].astype(np.int32) + directed = branch_pit[:, IdxBranch.DIRECTED].astype(bool) nobranch = np.sum(active_branch_lookup) nobranch_ud = np.sum(active_branch_lookup & ~directed) active_from_nodes = from_nodes[active_branch_lookup] @@ -673,38 +660,36 @@ def _connectivity(net, branch_pit, node_pit, active_branch_lookup, active_node_l if not np.all(nodes_connected[active_from_nodes_ud] == nodes_connected[active_to_nodes_ud]): raise ValueError( - "An error occured in the %s connectivity check. Please contact the pandapipes " - "development team!" % mode) + "An error occured in the connectivity check. Please contact the pandapipes " + "development team!") branches_connected = active_branch_lookup & nodes_connected[from_nodes] oos_nodes = np.where(~nodes_connected & active_node_lookup)[0] is_nodes = np.where(nodes_connected & ~active_node_lookup)[0] if len(oos_nodes) > 0: - msg = "\n".join("In table %s: %s" % (tbl, nds) for tbl, nds in + msg = "\n".join(f"In table {tbl}: {nds}" for tbl, nds in get_table_index_list(net, node_pit, oos_nodes)) - logger.info("Setting the following nodes out of service for %s calculation in connectivity" - " check:\n%s" % (mode, msg)) + logger.info("Setting the following nodes out of service in connectivity" + " check:\n%s", msg) if len(is_nodes) > 0: - node_type_message = "\n".join("In table %s: %s" % (tbl, nds) for tbl, nds in + node_type_message = "\n".join(f"In table {tbl}: {nds}" for tbl, nds in get_table_index_list(net, node_pit, is_nodes)) if get_net_option(net, "quit_on_inconsistency_connectivity"): raise UserWarning( - "The following nodes are connected to in_service branches in the %s calculation " + "The following nodes are connected to in_service branches " "although being out of service, which leads to an inconsistency in the connectivity" - " check!\n%s" % (mode, node_type_message)) - logger.info("Setting the following nodes back in service for %s calculation in connectivity" - " check as they are connected to in_service branches:\n%s" - % (mode, node_type_message)) + f" check!\n{node_type_message}") + logger.info("Setting the following nodes back in service in connectivity" + " check as they are connected to in_service branches:\n%s", + node_type_message) return nodes_connected, branches_connected def get_table_index_list(net, pit_array, pit_indices, pit_type="node"): - """ - Auxiliary function to get a list of tables and the table indices that belong to a number of pit - indices. + """Auxiliary function to get a list of tables and the table indices that belong to a number of pit indices. :param net: pandapipes net for which the list is requested :type net: pandapipesNet @@ -717,10 +702,10 @@ def get_table_index_list(net, pit_array, pit_indices, pit_type="node"): :return: List of table names and table indices belonging to the pit indices """ int_pit = pit_array[pit_indices, :] - tables = np.unique(int_pit[:, TABLE_IDX_ND]) + tables = np.unique(int_pit[:, IdxNode.TABLE_IDX]) table_lookup = get_lookup(net, pit_type, "table") - return [(get_table_name(table_lookup, tbl), list(int_pit[int_pit[:, TABLE_IDX_ND] == tbl, - ELEMENT_IDX_ND].astype(np.int32))) + return [(get_table_name(table_lookup, tbl), list(int_pit[int_pit[:, IdxNode.TABLE_IDX] == tbl, + IdxNode.ELEMENT_IDX].astype(np.int32))) for tbl in tables] @@ -761,18 +746,19 @@ def reduce_lookups(net, comp_type, mode, comp_pit, active_pit, comp_pit_old, act net["_lookups"][comp_type + "_from_to_active_" + mode] = ft_active -def reduce_pit(net, mode="hydraulics"): - """ - Create an internal ("active") pit with all nodes and branches that are actually in_service. This - is also done for different lookups (e.g. the from_to indices for this pit and the node index - lookup). A specialty that needs to be considered is that from_nodes and to_nodes change to new - indices. +def reduce_pit(net, mode): + """Create an internal ("active") pit with all nodes and branches that are actually in_service. + + This is also done for different lookups (e.g. the from_to indices for this pit and the node + index lookup). A specialty that needs to be considered is that from_nodes and to_nodes change + to new indices. Requires that the "node_active"/"branch_active" lookups have already been + populated by identify_active_nodes_branches. :param net: The pandapipesNet for which the pit shall be reduced :type net: pandapipesNet - :param mode: the mode of the calculation (either "hydraulics" or "heat_transfer") for storing /\ - retrieving correct lookups - :type mode: str, default "hydraulics" + :param mode: the mode of the calculation ("hydraulics" or "heat_transfer") for storing / + retrieving the correct lookups + :type mode: str :return: No output """ active_pit, active_pit_old = dict(), dict() @@ -780,8 +766,8 @@ def reduce_pit(net, mode="hydraulics"): branches_connected = get_lookup(net, "branch", "active_" + mode) for (comp_type, connected_elms, idx_col) in [ - ("branch", branches_connected, ELEMENT_IDX_BR), - ("node", nodes_connected, ELEMENT_IDX_ND) + ("branch", branches_connected, IdxBranch.ELEMENT_IDX), + ("node", nodes_connected, IdxNode.ELEMENT_IDX) ]: comp_pit = net["_pit"][comp_type] comp_pit_old = net["_old_pit"][comp_type] @@ -795,27 +781,53 @@ def reduce_pit(net, mode="hydraulics"): if not np.all(nodes_connected): reduced_node_lookup = np.cumsum(nodes_connected) - 1 - active_pit["branch"][:, FROM_NODE] = reduced_node_lookup[ - net["_pit"]["branch"][branches_connected, FROM_NODE].astype(np.int32)] - active_pit["branch"][:, TO_NODE] = reduced_node_lookup[ - net["_pit"]["branch"][branches_connected, TO_NODE].astype(np.int32)] + active_pit["branch"][:, IdxBranch.FROM_NODE] = reduced_node_lookup[ + net["_pit"]["branch"][branches_connected, IdxBranch.FROM_NODE].astype(np.int32)] + active_pit["branch"][:, IdxBranch.TO_NODE] = reduced_node_lookup[ + net["_pit"]["branch"][branches_connected, IdxBranch.TO_NODE].astype(np.int32)] net["_active_pit"] = active_pit net["_active_old_pit"] = active_pit_old +def branches_not_zero_flow(branch_pit): + """Simple function to identify branches with flow based on the calculated velocity. + + :param branch_pit: The pandapipes internal table of the network (including hydraulics results) + :type branch_pit: np.array + :return: branches_connected_flow - lookup array if branch is connected wrt. flow + :rtype: np.array + """ + return (~np.isnan(branch_pit[:, IdxBranch.MDOTINIT]) + & ~np.isclose(branch_pit[:, IdxBranch.MDOTINIT], 0, rtol=1e-10, atol=1e-10)) + + +def compute_infeed_nodes(branch_pit, node_pit): + """Mark nodes that feed into the network (source nodes) in node_pit[:, INFEED]. + + A node is considered an infeed if it appears as a from-node of a branch with + active flow but never as a to-node of any such branch. Must be called with the + global branch_pit (not a per-component slice) so that cross-component topology + is taken into account. + """ + branches_flow = branches_not_zero_flow(branch_pit) + from_nodes = get_from_nodes_corrected(branch_pit) + to_nodes = get_to_nodes_corrected(branch_pit) + infeed = np.setdiff1d(from_nodes[branches_flow], to_nodes[branches_flow]) + node_pit[infeed, IdxNode.INFEED] = True + + def check_infeed_number(node_pit): - slack_nodes = node_pit[:, NODE_TYPE_T] == T + slack_nodes = node_pit[:, IdxNode.NODE_TYPE_T] == IdxNode.T if len(node_pit) == np.sum(slack_nodes): - node_pit[slack_nodes, INFEED] = True - infeed_nodes = node_pit[:, INFEED] + node_pit[slack_nodes, IdxNode.INFEED] = True + infeed_nodes = node_pit[:, IdxNode.INFEED] if np.sum(infeed_nodes) != np.sum(slack_nodes): return False return True class PipeflowNotConverged(ppException): - """ - Exception being raised in case pipeflow did not converge. - """ + """Exception being raised in case pipeflow did not converge.""" + pass diff --git a/src/pandapipes/pf/result_extraction.py b/src/pandapipes/pf/result_extraction.py index 299debce7..8618f0952 100644 --- a/src/pandapipes/pf/result_extraction.py +++ b/src/pandapipes/pf/result_extraction.py @@ -1,23 +1,9 @@ import numpy as np from pandapipes.constants import NORMAL_PRESSURE, NORMAL_TEMPERATURE -from pandapipes.idx_branch import ( - QEXT, - ELEMENT_IDX, - FROM_NODE, - TO_NODE, - MDOTINIT, - RE, - LAMBDA, - PL, - TOUTINIT, - AREA, - TEXT, - LOSS_COEFFICIENT as LC, - FROM_NODE_T_SWITCHED, DP_FRICT_LOSS, -) -from pandapipes.idx_node import TABLE_IDX as TABLE_IDX_NODE, PINIT, PAMB, TINIT as TINIT_NODE -from pandapipes.pf.internals_toolbox import _sum_by_group +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode +from pandapipes.pf.internals_toolbox import _sum_by_group, branch_area from pandapipes.pf.pipeflow_setup import get_table_number, get_lookup, get_net_option from pandapipes.properties.fluids import get_fluid from pandapipes.properties.properties_toolbox import get_branch_real_density @@ -27,10 +13,20 @@ except ImportError: from pandapower.pf.no_numba import jit +# numba's nopython mode can't type IdxBranch/IdxNode as globals (they're classes, see +# pandapipes.idx.IndexMeta, not plain ints/modules) - bind the columns get_pressures_numba/ +# get_gas_vel_numba need as plain module-level ints, same fix as +# pandapipes.pf.derivative_toolbox_numba, and reference these bare names inside those two +# @jit(nopython=True) functions instead of the class attribute. Every other (non-jitted) function +# in this module keeps using IdxBranch.X/IdxNode.X normally. +BRANCH_FROM_NODE = IdxBranch.FROM_NODE +BRANCH_TOUTINIT = IdxBranch.TOUTINIT +NODE_TINIT = IdxNode.TINIT +NODE_PAMB = IdxNode.PAMB -def extract_all_results(net, calculation_mode): - """ - Extract results from branch pit and node pit and write them to the different tables of the net,\ + +def extract_all_results(net): + """Extract results from branch pit and node pit and write them to the different tables of the net,\ as defined by the component models. :param net: pandapipes net for which to extract results into net.res_xy @@ -40,6 +36,7 @@ def extract_all_results(net, calculation_mode): :return: No output """ + calculation_mode = get_net_option(net, "mode") branch_pit = net["_pit"]["branch"] node_pit = net["_pit"]["node"] branch_results = get_basic_branch_results(net, branch_pit, node_pit) @@ -66,29 +63,29 @@ def extract_all_results(net, calculation_mode): def get_basic_branch_results(net, branch_pit, node_pit): - from_nodes = branch_pit[:, FROM_NODE].astype(np.int32) - to_nodes = branch_pit[:, TO_NODE].astype(np.int32) - t0 = node_pit[from_nodes, TINIT_NODE] - t1 = node_pit[to_nodes, TINIT_NODE] + from_nodes = branch_pit[:, IdxBranch.FROM_NODE].astype(np.int32) + to_nodes = branch_pit[:, IdxBranch.TO_NODE].astype(np.int32) + t0 = node_pit[from_nodes, IdxNode.TINIT] + t1 = node_pit[to_nodes, IdxNode.TINIT] fluid = get_fluid(net) if fluid.is_gas: - vf = branch_pit[:, MDOTINIT] / fluid.get_density(NORMAL_TEMPERATURE) + vf = branch_pit[:, IdxBranch.MDOTINIT] / fluid.get_density(NORMAL_TEMPERATURE) else: - vf = branch_pit[:, MDOTINIT] / get_branch_real_density(fluid, node_pit, branch_pit) - v = vf / branch_pit[:, AREA] - t_outlet = branch_pit[:, TOUTINIT] - branch_results = {"v_mps": v, "mf_from": branch_pit[:, MDOTINIT], "mf_to": -branch_pit[:, MDOTINIT], - "vf": vf, "p_from": node_pit[from_nodes, PINIT], "p_to": node_pit[to_nodes, PINIT], + vf = branch_pit[:, IdxBranch.MDOTINIT] / get_branch_real_density(fluid, node_pit, branch_pit) + v = vf / branch_area(branch_pit) + t_outlet = branch_pit[:, IdxBranch.TOUTINIT] + branch_results = {"v_mps": v, "mf_from": branch_pit[:, IdxBranch.MDOTINIT], "mf_to": -branch_pit[:, IdxBranch.MDOTINIT], + "vf": vf, "p_from": node_pit[from_nodes, IdxNode.PINIT], "p_to": node_pit[to_nodes, IdxNode.PINIT], "from_nodes": from_nodes, "to_nodes": to_nodes, "temp_from": t0, "temp_to": t1, - "reynolds": branch_pit[:, RE], "lambda": branch_pit[:, LAMBDA], "pl": branch_pit[:, PL], - "t_outlet": t_outlet, "qext": branch_pit[:, QEXT], "loss_coeff": branch_pit[:, LC], - "dp_frict_loss": branch_pit[:, DP_FRICT_LOSS]} + "reynolds": branch_pit[:, IdxBranch.RE], "lambda": branch_pit[:, IdxBranch.LAMBDA], "pl": branch_pit[:, IdxBranch.PL], + "t_outlet": t_outlet, "qext": branch_pit[:, IdxBranch.QEXT], "loss_coeff": branch_pit[:, IdxBranch.LOSS_COEFFICIENT], + "dp_frict_loss": branch_pit[:, IdxBranch.DP_FRICT_LOSS]} return branch_results def get_branch_results_gas(net, branch_pit, node_pit, from_nodes, to_nodes, v_mps, p_from, p_to): - p_abs_from = node_pit[from_nodes, PAMB] + p_from - p_abs_to = node_pit[to_nodes, PAMB] + p_to + p_abs_from = node_pit[from_nodes, IdxNode.PAMB] + p_from + p_abs_to = node_pit[to_nodes, IdxNode.PAMB] + p_to mask = ~np.isclose(p_abs_from, p_abs_to) p_abs_mean = np.empty_like(p_abs_to) p_abs_mean[~mask] = p_abs_from[~mask] @@ -96,10 +93,10 @@ def get_branch_results_gas(net, branch_pit, node_pit, from_nodes, to_nodes, v_mp / (p_abs_from[mask] ** 2 - p_abs_to[mask] ** 2) fluid = get_fluid(net) - switched_t = branch_pit[:, FROM_NODE_T_SWITCHED].astype(np.bool_) - t_from = node_pit[from_nodes, TINIT_NODE] - t_from[switched_t] = node_pit[to_nodes[switched_t], TINIT_NODE] - t_to = branch_pit[:, TOUTINIT] + switched_t = branch_pit[:, IdxBranch.FROM_NODE_T_SWITCHED].astype(np.bool_) + t_from = node_pit[from_nodes, IdxNode.TINIT] + t_from[switched_t] = node_pit[to_nodes[switched_t], IdxNode.TINIT] + t_to = branch_pit[:, IdxBranch.TOUTINIT] tm = (t_from + t_to) / 2 numerator_from = NORMAL_PRESSURE * t_from / NORMAL_TEMPERATURE numerator_to = NORMAL_PRESSURE * t_to / NORMAL_TEMPERATURE @@ -125,10 +122,10 @@ def get_branch_results_gas_numba(net, branch_pit, node_pit, from_nodes, to_nodes fluid = get_fluid(net) args_from, args_to, args_mean = [p_abs_from], [p_abs_to], [p_abs_mean] if hasattr(fluid.all_properties["compressibility"], "allow_2d"): - switched_t = branch_pit[:, FROM_NODE_T_SWITCHED].astype(np.bool_) - t_from = node_pit[from_nodes, TINIT_NODE] - t_from[switched_t] = node_pit[to_nodes[switched_t], TINIT_NODE] - t_to = branch_pit[:, TOUTINIT] + switched_t = branch_pit[:, IdxBranch.FROM_NODE_T_SWITCHED].astype(np.bool_) + t_from = node_pit[from_nodes, IdxNode.TINIT] + t_from[switched_t] = node_pit[to_nodes[switched_t], IdxNode.TINIT] + t_to = branch_pit[:, IdxBranch.TOUTINIT] args_from.append(t_from) args_to.append(t_to) args_mean.append((t_from + t_to) / 2) @@ -149,8 +146,8 @@ def get_pressures_numba(node_pit, from_nodes, to_nodes, v_mps, p_from, p_to): p_abs_from, p_abs_to, p_abs_mean = [np.empty_like(v_mps) for _ in range(3)] for i in range(len(v_mps)): - p_abs_from[i] = node_pit[from_nodes[i], PAMB] + p_from[i] - p_abs_to[i] = node_pit[to_nodes[i], PAMB] + p_to[i] + p_abs_from[i] = node_pit[from_nodes[i], NODE_PAMB] + p_from[i] + p_abs_to[i] = node_pit[to_nodes[i], NODE_PAMB] + p_to[i] if np.less_equal(np.abs(p_abs_from[i] - p_abs_to[i]), 1e-8 + 1e-5 * abs(p_abs_to[i])): p_abs_mean[i] = p_abs_from[i] else: @@ -165,10 +162,10 @@ def get_gas_vel_numba(node_pit, branch_pit, comp_from, comp_to, comp_mean, p_abs p_abs_mean, v_mps): v_gas_from, v_gas_to, v_gas_mean, normfactor_from, normfactor_to, normfactor_mean = \ [np.empty_like(v_mps) for _ in range(6)] - from_nodes = branch_pit[:, FROM_NODE].astype(np.int32) + from_nodes = branch_pit[:, BRANCH_FROM_NODE].astype(np.int32) for i in range(len(v_mps)): - t_from = node_pit[from_nodes[i], TINIT_NODE] - t_to = branch_pit[i, TOUTINIT] + t_from = node_pit[from_nodes[i], NODE_TINIT] + t_to = branch_pit[i, BRANCH_TOUTINIT] tm = (t_from + t_to) / 2 numerator_from = np.divide(NORMAL_PRESSURE * t_from, NORMAL_TEMPERATURE) numerator_to = np.divide(NORMAL_PRESSURE * t_to, NORMAL_TEMPERATURE) @@ -199,7 +196,7 @@ def extract_branch_results_with_internals(net, branch_results, table_name, # since the function _sum_by_group sorts the entries by an index (in this case the index of the # respective table), the placement of the indices mus be known to allocate the values correctly placement_table = np.argsort(net[table_name].index.values) - idx_pit = branch_pit[f:t, ELEMENT_IDX] + idx_pit = branch_pit[f:t, IdxBranch.ELEMENT_IDX] node_pit = net["_pit"]["node"] @@ -223,7 +220,7 @@ def extract_branch_results_with_internals(net, branch_results, table_name, # single from_node that is the exterior node (e.g. junction vs. internal pipe_node) # result has to be extracted from the node_pit end_nodes = branch_results[node_name][f:t] - end_nodes_external = node_pit[end_nodes, TABLE_IDX_NODE] != ext_node_tbl_idx + end_nodes_external = node_pit[end_nodes, IdxNode.TABLE_IDX] != ext_node_tbl_idx considered = end_nodes_external & comp_connected external_active = comp_connected[end_nodes_external] for res_name, entry in res_ext: @@ -258,9 +255,9 @@ def extract_branch_results_with_internals(net, branch_results, table_name, def extract_branch_results_without_internals(net, branch_results, required_results_hydraulic, required_results_heat, table_name, simulation_mode): - """ - Extract the results from the branch result array derived from the pit to the result table of the - net (only for branch components without internal nodes). Here, we need to consider which results + """Extract the branch results derived from the pit into the net's result table. + + Only for branch components without internal nodes. Here, we need to consider which results exist for hydraulic calculation and for heat transfer calculation (wrt. connectivity). :param net: The pandapipes net that the internal structure belongs to @@ -305,39 +302,56 @@ def extract_branch_results_without_internals(net, branch_results, required_resul branch_results[entry][f:t][comp_connected_ht] -def extract_results_active_pit(net, mode="hydraulics"): - """ - Extract the pipeflow results from the internal pit structure ("_active_pit") to the general pit - structure. +def extract_results_active_pit_hydraulics(net): + """Extract the hydraulic pipeflow results from the internal pit structure ("_active_pit") to the general pit structure. :param net: The pandapipes net that the internal structure belongs to :type net: pandapipesNet - :param mode: defines whether results from hydraulic or temperature calculation are transferred - :type mode: str, default "hydraulics" :return: No output + """ + nodes_connected = get_lookup(net, "node", "active_hydraulics") + branches_connected = get_lookup(net, "branch", "active_hydraulics") + copied_node_cols = np.array([i for i in range(net["_pit"]["node"].shape[1]) + if i not in [IdxNode.TINIT]]) + rows_nodes = np.arange(net["_pit"]["node"].shape[0])[nodes_connected] + + copied_branch_cols = np.array([i for i in range(net["_pit"]["branch"].shape[1]) + if i not in [IdxBranch.FROM_NODE, IdxBranch.TO_NODE, + IdxBranch.TOUTINIT]]) + rows_branches = np.arange(net["_pit"]["branch"].shape[0])[branches_connected] + net["_pit"]["node"][~nodes_connected, IdxNode.PINIT] = np.nan + net["_pit"]["node"][rows_nodes[:, np.newaxis], copied_node_cols[np.newaxis, :]] = \ + net["_active_pit"]["node"][:, copied_node_cols] + net["_pit"]["branch"][~branches_connected, IdxBranch.MDOTINIT] = np.nan + net["_pit"]["branch"][rows_branches[:, np.newaxis], copied_branch_cols[np.newaxis, :]] = \ + net["_active_pit"]["branch"][:, copied_branch_cols] + + +def extract_results_active_pit_heat_transfer(net): + """Extract the heat transfer pipeflow results from the internal pit structure ("_active_pit") to the general pit structure. + + :param net: The pandapipes net that the internal structure belongs to + :type net: pandapipesNet + :return: No output """ - nodes_connected = get_lookup(net, "node", "active_" + mode) - branches_connected = get_lookup(net, "branch", "active_" + mode) - result_node_col = PINIT if mode == "hydraulics" else TINIT_NODE - not_affected_node_col = TINIT_NODE if mode == "hydraulics" else PINIT + nodes_connected = get_lookup(net, "node", "active_heat_transfer") + branches_connected = get_lookup(net, "branch", "active_heat_transfer") copied_node_cols = np.array([i for i in range(net["_pit"]["node"].shape[1]) - if i not in [not_affected_node_col]]) + if i not in [IdxNode.PINIT]]) rows_nodes = np.arange(net["_pit"]["node"].shape[0])[nodes_connected] - result_branch_col = MDOTINIT if mode == "hydraulics" else TOUTINIT - not_affected_branch_col = TOUTINIT if mode == "hydraulics" else MDOTINIT copied_branch_cols = np.array([i for i in range(net["_pit"]["branch"].shape[1]) - if i not in [FROM_NODE, TO_NODE, - not_affected_branch_col]]) + if i not in [IdxBranch.FROM_NODE, IdxBranch.TO_NODE, + IdxBranch.MDOTINIT]]) rows_branches = np.arange(net["_pit"]["branch"].shape[0])[branches_connected] amb = get_net_option(net, 'ambient_temperature') - net["_pit"]["node"][~nodes_connected, result_node_col] = np.nan if mode == "hydraulics" else amb + net["_pit"]["node"][~nodes_connected, IdxNode.TINIT] = amb net["_pit"]["node"][rows_nodes[:, np.newaxis], copied_node_cols[np.newaxis, :]] = \ net["_active_pit"]["node"][:, copied_node_cols] - net["_pit"]["branch"][~branches_connected, result_branch_col] = np.nan if mode == "hydraulics" else \ - net["_pit"]["branch"][~branches_connected, TEXT] + net["_pit"]["branch"][~branches_connected, IdxBranch.TOUTINIT] = \ + net["_pit"]["branch"][~branches_connected, IdxBranch.TEXT] net["_pit"]["branch"][rows_branches[:, np.newaxis], copied_branch_cols[np.newaxis, :]] = \ net["_active_pit"]["branch"][:, copied_branch_cols] diff --git a/src/pandapipes/pf/system_index.py b/src/pandapipes/pf/system_index.py new file mode 100644 index 000000000..bcb7bdb10 --- /dev/null +++ b/src/pandapipes/pf/system_index.py @@ -0,0 +1,397 @@ +# Copyright (c) 2020-2026 by Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +from __future__ import annotations + +from enum import Enum + +import numpy as np +from dataclasses import dataclass, field +from pandapipes.idx_node import IdxNode + +class HydVarEq(str, Enum): + """Variable and equation types for the hydraulic linear system.""" + + NODE = "NODE" + BRANCH = "BRANCH" + SLACK = "SLACK" + PINIT = "PINIT" + MDOTINIT = "MDOTINIT" + MDOTSLACKINIT = "MDOTSLACKINIT" + + +class ThermVarEq(str, Enum): + """Variable and equation types for the thermal linear system.""" + + NODE = "NODE" + BRANCH = "BRANCH" + TINIT = "TINIT" + TOUTINIT = "TOUTINIT" + + +class EqWriteMode(str, Enum): + """Write mode for ComponentEquations entries. + + UNIQUE: exclusive row ownership — conflict check on registration, normal + contributions to those rows are stripped in assemble + ADDITIVE: values accumulated (default) + MEAN: mean of all load contributions to the same row (NaN-filtered); + Jacobian entries averaged per unique (row, col) pair + """ + + UNIQUE = "unique" + ADDITIVE = "additive" + MEAN = "mean" + + +@dataclass +class ComponentEquations: + """Sparse (COO format) contributions of one component to the global Jacobian and load vector. + + ADDITIVE (default): contributions accumulate, no conflict check. + UNIQUE: the component claims those rows exclusively; conflict check on registration, + normal contributions to those rows are stripped in assemble. + MEAN: multiple contributions to the same row are averaged (NaN-filtered). + """ + + rows: np.ndarray # int32, equation row indices (global) + cols: np.ndarray # int32, variable column indices (global) + data: np.ndarray # float64, Jacobian values + load_rows: np.ndarray # int32, load vector positions + load_data: np.ndarray # float64, load vector values + mode: EqWriteMode = EqWriteMode.ADDITIVE + + @classmethod + def empty(cls) -> ComponentEquations: + return cls( + np.empty(0, dtype=np.int32), np.empty(0, dtype=np.int32), + np.empty(0, dtype=np.float64), + np.empty(0, dtype=np.int32), np.empty(0, dtype=np.float64), + ) + + +@dataclass +class ComponentRegistry: + """Two-bucket registry for component equations. + + normal: equations that accumulate (COO summing) + overrides: equations written after normal; UNIQUE overrides strip normal + contributions from their rows + + UNIQUE entries trigger a row-conflict check against previously registered UNIQUE + entries in the same bucket (add → normal, add_override → overrides). + """ + + normal: list[ComponentEquations] = field(default_factory=list) + overrides: list[ComponentEquations] = field(default_factory=list) + + def _check_conflict(self, eq: ComponentEquations, bucket: list) -> None: + existing = [e for e in bucket if e.mode == EqWriteMode.UNIQUE] + if not existing: + return + claimed = np.concatenate([e.rows for e in existing]) + conflict = np.intersect1d(claimed, eq.rows) + if len(conflict): + raise ValueError( + f"Equation conflict: rows {conflict.tolist()} are already claimed " + f"by a UNIQUE entry." + ) + + def add(self, eq: ComponentEquations) -> None: + if eq.mode == EqWriteMode.UNIQUE: + self._check_conflict(eq, self.normal) + self.normal.append(eq) + + def add_override(self, eq: ComponentEquations) -> None: + if eq.mode == EqWriteMode.UNIQUE: + self._check_conflict(eq, self.overrides) + self.overrides.append(eq) + + def assemble(self, size: int) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Build COO Jacobian entries and load vector from all registered components. + + UNIQUE override rows are stripped from the normal pool. + MEAN entries (both buckets combined) are averaged per (row, col) in the Jacobian + and per row in the load vector (NaN-filtered); they replace any prior values at + those positions. + + Returns + ------- + rows, cols, data : int32 / float64 arrays (COO format) + load_vector : float64 array of length *size* + + """ + unique_overrides = [ov for ov in self.overrides if ov.mode == EqWriteMode.UNIQUE] + claimed = ( + np.concatenate([ov.rows for ov in unique_overrides]) + if unique_overrides else np.empty(0, dtype=np.int32) + ) + + n_non_mean = [e for e in self.normal if e.mode != EqWriteMode.MEAN] + n_mean = [e for e in self.normal if e.mode == EqWriteMode.MEAN] + ov_non_mean = [e for e in self.overrides if e.mode != EqWriteMode.MEAN] + ov_mean = [e for e in self.overrides if e.mode == EqWriteMode.MEAN] + + def _cat(entries, attr, dtype): + return (np.concatenate([getattr(e, attr) for e in entries]).astype(dtype) + if entries else np.empty(0, dtype=dtype)) + + n_r = _cat(n_non_mean, 'rows', np.int32) + n_c = _cat(n_non_mean, 'cols', np.int32) + n_d = _cat(n_non_mean, 'data', np.float64) + n_lr = _cat(n_non_mean, 'load_rows', np.int32) + n_ld = _cat(n_non_mean, 'load_data', np.float64) + + if len(claimed): + keep = ~np.isin(n_r, claimed) + n_r, n_c, n_d = n_r[keep], n_c[keep], n_d[keep] + keep_lr = ~np.isin(n_lr, claimed) + n_lr, n_ld = n_lr[keep_lr], n_ld[keep_lr] + + ov_r = _cat(ov_non_mean, 'rows', np.int32) + ov_c = _cat(ov_non_mean, 'cols', np.int32) + ov_d = _cat(ov_non_mean, 'data', np.float64) + ov_lr = _cat(ov_non_mean, 'load_rows', np.int32) + ov_ld = _cat(ov_non_mean, 'load_data', np.float64) + + rows = np.concatenate([n_r, ov_r]).astype(np.int32) + cols = np.concatenate([n_c, ov_c]).astype(np.int32) + data = np.concatenate([n_d, ov_d]).astype(np.float64) + + load = np.zeros(size, dtype=np.float64) + np.add.at(load, n_lr, n_ld) + + load_ov = np.zeros(size, dtype=np.float64) + np.add.at(load_ov, ov_lr, ov_ld) + if len(ov_lr): + ov_set = np.zeros(size, dtype=bool) + ov_set[ov_lr] = True + load[ov_set] = load_ov[ov_set] + + all_mean = n_mean + ov_mean + if all_mean: + m_r = _cat(all_mean, 'rows', np.int32) + m_c = _cat(all_mean, 'cols', np.int32) + m_d = _cat(all_mean, 'data', np.float64) + m_lr = _cat(all_mean, 'load_rows', np.int32) + m_ld = _cat(all_mean, 'load_data', np.float64) + + rc = m_r.astype(np.int64) * size + m_c.astype(np.int64) + u_rc, rc_inv, rc_cnt = np.unique(rc, return_inverse=True, return_counts=True) + jac_s = np.zeros(len(u_rc), dtype=np.float64) + np.add.at(jac_s, rc_inv, m_d) + rows = np.concatenate([rows, (u_rc // size).astype(np.int32)]) + cols = np.concatenate([cols, (u_rc % size).astype(np.int32)]) + data = np.concatenate([data, jac_s / rc_cnt]) + + valid = ~np.isnan(m_ld) + if valid.any(): + vr, vd = m_lr[valid], m_ld[valid] + u_r, r_inv, r_cnt = np.unique(vr, return_inverse=True, return_counts=True) + ld_s = np.zeros(len(u_r), dtype=np.float64) + np.add.at(ld_s, r_inv, vd) + load[u_r] = ld_s / r_cnt + + return rows, cols, data, load + + +class PitWriteMode(str, Enum): + """Write mode for PIT entries. + + UNIQUE: exclusive write — conflict check on registration, direct assignment in apply + ADDITIVE: values accumulated with np.add.at (default) + MEAN: mean of all values written to the same (row, col) position + """ + + UNIQUE = "unique" + ADDITIVE = "additive" + MEAN = "mean" + + +@dataclass +class PitEntries: + """COO-format data for writing into a PIT (node or branch) array.""" + + rows: np.ndarray # int32, row indices into the PIT + cols: np.ndarray # int32, column indices into the PIT + data: np.ndarray # values to write + mode: PitWriteMode = PitWriteMode.UNIQUE + + +@dataclass +class PitRegistry: + """Two-bucket registry for PIT initialization. + + normal: base entries written first + overrides: entries written second, winning over normal entries at the same positions + + UNIQUE entries trigger a (row, col) conflict check against all previously registered + UNIQUE entries in both buckets. + """ + + normal: list[PitEntries] = field(default_factory=list) + overrides: list[PitEntries] = field(default_factory=list) + + def _check_conflict(self, entries: PitEntries, bucket: list) -> None: + existing = [e for e in bucket if e.mode == PitWriteMode.UNIQUE] + if not existing: + return + claimed = set(zip( + np.concatenate([e.rows for e in existing]).tolist(), + np.concatenate([e.cols for e in existing]).tolist(), + )) + conflict = claimed & set(zip(entries.rows.tolist(), entries.cols.tolist())) + if conflict: + raise ValueError( + f"PIT conflict: (row, col) pairs {conflict} are already " + f"claimed by a unique entry." + ) + + def add(self, entries: PitEntries) -> None: + if entries.mode == PitWriteMode.UNIQUE: + self._check_conflict(entries, self.normal) + self.normal.append(entries) + + def add_override(self, entries: PitEntries) -> None: + if entries.mode == PitWriteMode.UNIQUE: + self._check_conflict(entries, self.overrides) + self.overrides.append(entries) + + def apply(self, pit: np.ndarray) -> None: + mean_entries = [] + for e in self.normal + self.overrides: + if e.mode == PitWriteMode.UNIQUE: + pit[e.rows, e.cols] = e.data + elif e.mode == PitWriteMode.ADDITIVE: + np.add.at(pit, (e.rows, e.cols), e.data) + else: + mean_entries.append(e) + + if mean_entries: + all_rows = np.concatenate([e.rows for e in mean_entries]) + all_cols = np.concatenate([e.cols for e in mean_entries]) + all_data = np.concatenate([e.data for e in mean_entries]) + valid = ~np.isnan(all_data) + all_rows, all_cols, all_data = all_rows[valid], all_cols[valid], all_data[valid] + if len(all_rows): + keys = all_rows * pit.shape[1] + all_cols + unique_keys, inverse, counts = np.unique(keys, return_inverse=True, return_counts=True) + sums = np.zeros(len(unique_keys)) + np.add.at(sums, inverse, all_data) + pit[unique_keys // pit.shape[1], unique_keys % pit.shape[1]] = sums / counts + + +class BaseSystemIndex: + """Central registry of all variables and equations in the linear system. + + Variables and equations are registered via ``_register()`` using ``HydVarEq`` + (or integer PIT constants for thermal) as keys. In this square system each + variable has exactly one equation — the numerical indices are identical. + """ + + def __init__(self) -> None: + """Initialize an empty variable/equation block registry.""" + self._blocks: dict = {} + self._size: int = 0 + + def _block_key(self, key): + """Actual dict key ``_blocks`` is stored/looked-up under for variable/equation *key*. + + Overridable so subclasses can namespace keys (e.g. combined_pipeflow's + ``HydThermSystemIndex``, which needs ``HydVarEq.NODE`` and ``ThermVarEq.NODE`` to resolve + to different blocks despite being equal as plain strings). All of ``idx``/``_register``/ + ``_register_sparse`` go through this, so overriding it here is enough - no need to + separately override each of them. + """ + return key + + def idx(self, var, subset: np.ndarray | None = None) -> np.ndarray: + """Matrix index for variable/equation *var* (optionally filtered to *subset* positions).""" + arr = self._blocks[self._block_key(var)] + return arr if subset is None else arr[subset] + + def size(self) -> int: + """Total number of rows/columns in the global matrix.""" + return self._size + + def _register(self, key, indices: np.ndarray) -> None: + """Register a variable or equation block and update _size.""" + idx = indices.astype(np.int32) + self._blocks[self._block_key(key)] = idx + if len(idx): + self._size = max(self._size, int(idx[-1]) + 1) + + def _register_sparse(self, key, full_size: int, node_indices: np.ndarray, + values: np.ndarray) -> None: + """Register a variable/equation block that only exists for a SUBSET of nodes. + + E.g. MDOTSLACKINIT/SLACK, only defined at P-type nodes - but sized like the FULL node + array (``full_size``), with -1 at every position outside ``node_indices``. This lets + ``idx(key, some_node_indices)`` be called with raw node indices directly, exactly like + PINIT/NODE, instead of requiring callers to translate to a rank-within-subset first. + """ + arr = np.full(full_size, -1, dtype=np.int32) + arr[node_indices] = values + self._blocks[self._block_key(key)] = arr + if len(values): + self._size = max(self._size, int(values.max()) + 1) + + +class HydraulicSystemIndex(BaseSystemIndex): + """Variable / equation registry for the hydraulic solve. + + Layout (columns = rows in square system): + 0 .. len_n-1 PINIT / NODE pressure / node mass-balance + len_n .. len_n+len_b-1 MDOTINIT / BRANCH mass flow / branch momentum + len_n+len_b .. ... MDOTSLACKINIT / SLACK slack-mass variables (P-type nodes) + + ``slack_nodes`` is the sorted array of node_pit row indices with NODE_TYPE == P. + + ``MDOTSLACKINIT``/``SLACK`` only exist at P-type nodes, but are sized like the full node + array (with -1 at every non-slack position) so ``idx(HydVarEq.MDOTSLACKINIT, some_nodes)`` + works with raw node indices directly, same as ``PINIT``/``NODE`` - a caller (e.g. ExtGrid, + CirculationPump) never needs to translate its own node indices into a rank-within-slack_nodes + first, it just needs to know which of ITS OWN nodes are P-type slack nodes at all. + """ + + def __init__(self, node_pit: np.ndarray, branch_pit: np.ndarray) -> None: + """Build the variable/equation index for a hydraulic solve over *node_pit*/*branch_pit*.""" + super().__init__() + self.slack_nodes = np.where(node_pit[:, IdxNode.NODE_TYPE] == IdxNode.P)[0].astype(np.int32) + + len_n = len(node_pit) + len_b = len(branch_pit) + len_s = len(self.slack_nodes) + slack_vals = np.arange(len_s, dtype=np.int32) + len_n + len_b + + self._register(HydVarEq.PINIT, np.arange(len_n)) + self._register(HydVarEq.MDOTINIT, np.arange(len_b) + len_n) + self._register_sparse(HydVarEq.MDOTSLACKINIT, len_n, self.slack_nodes, slack_vals) + + self._register(HydVarEq.NODE, np.arange(len_n)) + self._register(HydVarEq.BRANCH, np.arange(len_b) + len_n) + self._register_sparse(HydVarEq.SLACK, len_n, self.slack_nodes, slack_vals) + + +class HeatSystemIndex(BaseSystemIndex): + """Variable / equation registry for the thermal solve. + + Layout (columns = rows in square system): + 0 .. len_n-1 TINIT / NODE node temperature / node energy balance + len_n .. len_n+len_b-1 TOUTINIT / BRANCH branch outlet temperature / branch energy + """ + + def __init__(self, node_pit: np.ndarray, branch_pit: np.ndarray) -> None: + """Build the variable/equation index for a thermal solve over *node_pit*/*branch_pit*.""" + super().__init__() + self.slack_nodes = np.where(node_pit[:, IdxNode.NODE_TYPE_T] == IdxNode.T)[0].astype(np.int32) + + len_n = len(node_pit) + len_b = len(branch_pit) + + self._register(ThermVarEq.TINIT, np.arange(len_n)) + self._register(ThermVarEq.TOUTINIT, np.arange(len_b) + len_n) + + self._register(ThermVarEq.NODE, self._blocks[ThermVarEq.TINIT]) + self._register(ThermVarEq.BRANCH, self._blocks[ThermVarEq.TOUTINIT]) diff --git a/src/pandapipes/pipeflow.py b/src/pandapipes/pipeflow.py index 5918d8de2..3f74cb0fc 100644 --- a/src/pandapipes/pipeflow.py +++ b/src/pandapipes/pipeflow.py @@ -2,21 +2,12 @@ # and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. -import numpy as np -from scipy.sparse.linalg import spsolve - -from pandapipes.idx_branch import MDOTINIT, TOUTINIT, FROM_NODE_T_SWITCHED, ACTIVE as ACTIVE_BRANCH, BRANCH_TYPE -from pandapipes.idx_node import PINIT, TINIT, MDOTSLACKINIT, NODE_TYPE, P, ACTIVE as ACTIVE_NODE -from pandapipes.pf.build_system_matrix import build_system_matrix -from pandapipes.pf.derivative_calculation import (calculate_derivatives_hydraulic, - calculate_derivatives_thermal) +from pandapipes.pf.calculation import execute_heat, execute_hydraulics, execute_bidirectional from pandapipes.pf.pipeflow_setup import ( - get_net_option, get_net_options, set_net_option, init_options, create_internal_results, - write_internal_results, get_lookup, create_lookups, initialize_pit, reduce_pit, - set_user_pf_options, init_all_result_tables, identify_active_nodes_branches, - check_infeed_number, PipeflowNotConverged + get_net_option, init_options, create_lookups, initialize_pit, init_all_result_tables, + ) -from pandapipes.pf.result_extraction import extract_all_results, extract_results_active_pit +from pandapipes.pf.result_extraction import extract_all_results try: import pandaplan.core.pplog as logging @@ -27,29 +18,27 @@ def set_logger_level_pipeflow(level): - """ - Set logger level from outside to reduce/extend pipeflow() printout. + """Set logger level from outside to reduce/extend pipeflow() printout. + :param level: levels according to 'logging' (i.e. DEBUG, INFO, WARNING, ERROR and CRITICAL) :type level: str :return: No output - EXAMPLE: + Example + ------- set_logger_level_pipeflow('WARNING') """ logger.setLevel(level) -def pipeflow(net, sol_vec=None, **kwargs): - """ - The main method used to start the solver to calculate the velocity, pressure and temperature\ +def pipeflow(net, **kwargs): + """The main method used to start the solver to calculate the velocity, pressure and temperature\ distribution for a given net. Different options can be entered for \\**kwargs, which control\ the solver behaviour (see function :func:`init_options` for more information). :param net: The pandapipes net for which to perform the pipeflow :type net: pandapipesNet - :param sol_vec: Initializes the start values for the heating network calculation - :type sol_vec: numpy.ndarray, default None :param kwargs: A list of options controlling the solver behaviour :return: No output @@ -57,418 +46,38 @@ def pipeflow(net, sol_vec=None, **kwargs): >>> pipeflow(net, mode="hydraulics") """ - # Inputs & initialization of variables - # ------------------------------------------------------------------------------------------ + init_pipeflow(net, **kwargs) - # Init physical constants and options - init_options(net, **kwargs) + execute_pipeflow(net) - # init result tables - init_all_result_tables(net) + extract_all_results(net) + + +def init_pipeflow(net, **kwargs): + """Inputs & initialization of variables: physical constants/options, result tables, lookups and the internal PIT (pandapipes internal tables) arrays. + :param net: The pandapipes net for which to perform the pipeflow + :type net: pandapipesNet + :param kwargs: A list of options controlling the solver behaviour + :return: No output + """ + init_options(net, **kwargs) + init_all_result_tables(net) create_lookups(net) initialize_pit(net) - net.converged = False +def execute_pipeflow(net): calculation_mode = get_net_option(net, "mode") - calculate_hydraulics = calculation_mode in ["hydraulics", 'sequential'] - calculate_heat = calculation_mode in ["heat", 'sequential'] + calculate_hydraulics = calculation_mode in ["hydraulics", "sequential"] + calculate_heat = calculation_mode in ["heat", "sequential"] calculate_bidrect = calculation_mode == "bidirectional" - - # TODO: This is not necessary in every time step, but we need the result! The result of the - # connectivity check is currently not saved anywhere! - # cannot be moved to calculate_hydraulics as the active node/branch hydraulics lookup is also required to - # determine the active node/branch heat transfer lookup - identify_active_nodes_branches(net) - - if calculation_mode == 'heat': - use_given_hydraulic_results(net, sol_vec) - if not (calculate_hydraulics | calculate_heat | calculate_bidrect): raise UserWarning("No proper calculation mode chosen.") elif calculate_bidrect: - bidirectional(net) + execute_bidirectional(net) else: if calculate_hydraulics: - hydraulics(net) + execute_hydraulics(net) if calculate_heat: - heat_transfer(net) - - extract_all_results(net, calculation_mode) - - -def use_given_hydraulic_results(net, sol_vec): - node_pit = net["_pit"]["node"] - branch_pit = net["_pit"]["branch"] - - if not net.user_pf_options["hyd_flag"]: - raise UserWarning("Converged flag not set. Make sure that hydraulic calculation " - "results are available.") - else: - node_pit[:, PINIT] = sol_vec[:len(node_pit)] - branch_pit[:, MDOTINIT] = sol_vec[len(node_pit):] - - -def newton_raphson(net, funct, mode, solver_vars, tols, pit_names, iter_name): - max_iter, nonlinear_method, tol_res = get_net_options( - net, iter_name, "nonlinear_method", "tol_res" - ) - niter = 0 - # This branch is used to stop the solver after a specified error tolerance is reached - errors = {var: [] for var in solver_vars} - create_internal_results(net) - residual_norm = None - # This loop is left as soon as the solver converged - # Assumes this loop is the Newton-Raphson iteration loop - # 1: ODE -> integrate to get function y(0) - # 2: Build Jacobian matrix df1/dx1, df1/dx2 etc. (this means take derivative of each variable x1,x2,x3...) - # 3: Consider initial guess for x1,x2,x3,... this is a vector x(0) = [x1,x2,x3,x4,] - # 4: Compute value of Jacobian at these guesses x(0) above - # 5: Take inverse of Jacobian (not always able to thus LU decomposition, spsolve...) - # 6: Evaluate function from step 1 at the initial guesses from step 3 - # 7 The first iteration is the: initial_guess_vector - Jacobian@initial_guess * function vector@initial_guess - # x(1) = x(0) - J^-1(x(0) *F(0) - # The repeat from step 3 again until error convergence - # x(2) = x(1) - J^-1(x(1) *F(1) - # note: Jacobian equations don't change, just the X values subbed in at each iteration which - # makes the jacobian different - while not net.converged and niter < max_iter: - logger.debug("niter %d" % niter) - - # solve_hydraulics is where the calculation takes place - results, residual, filtered = funct(net) - residual_norm = np.max(np.abs(residual)) - logger.debug("residual: %s" % residual_norm.round(4)) - pos = np.arange(len(solver_vars) * 2) - results = np.array(results, object) - vals_new = results[pos[::2]] - vals_old = results[pos[1::2]] - for var, val_new, val_old in zip(solver_vars, vals_new, vals_old): - dval = val_new - val_old - errors[var].append(np.max(np.abs(dval)) if len(dval) else 0) - finalize_iteration( - net, niter, residual_norm, nonlinear_method, errors=errors, tols=tols, tol_res=tol_res, - vals_old=vals_old, solver_vars=solver_vars, pit_names=pit_names, filtered=filtered - ) - niter += 1 - write_internal_results(net, **errors) - kwargs = dict() - kwargs['residual_norm_%s' % mode] = residual_norm - kwargs['iterations_%s' % mode] = niter - write_internal_results(net, **kwargs) - log_final_results(net, mode, niter, residual_norm, solver_vars, tols) - - -def bidirectional(net): - net.converged = False - if not get_net_option(net, "reuse_internal_data") or "_internal_data" not in net: - net["_internal_data"] = dict() - solver_vars = ['mdot', 'p', 'TOUT', 'T'] - tol_m, tol_p, tol_temp = get_net_options(net, 'tol_m', 'tol_p', 'tol_T') - newton_raphson( - net, solve_bidirectional, 'bidirectional', solver_vars, [tol_m, tol_p, tol_temp, tol_temp], - ['branch', 'node', 'branch', 'node'], 'max_iter_bidirect' - ) - if net.converged: - set_user_pf_options(net, hyd_flag=True) - if not get_net_option(net, "reuse_internal_data"): - net.pop("_internal_data", None) - if not net.converged: - raise PipeflowNotConverged("The bidrectional calculation did not converge to a solution.") - - -def hydraulics(net): - # Start of nonlinear loop - # --------------------------------------------------------------------------------------------- - net.converged = False - reduce_pit(net, mode="hydraulics") - if not get_net_option(net, "reuse_internal_data") or "_internal_data" not in net: - net["_internal_data"] = dict() - solver_vars = ['mdot', 'p', 'mdotslack'] - tol_m, tol_p, tol_msl = get_net_options(net, 'tol_m', 'tol_p', 'tol_m') - newton_raphson(net, solve_hydraulics, 'hydraulics', solver_vars, [tol_m, tol_p, tol_msl], - ['branch', 'node', 'node'], 'max_iter_hyd') - if net.converged: - set_user_pf_options(net, hyd_flag=True) - rerun_hydraulics(net) - - if not get_net_option(net, "reuse_internal_data"): - net.pop("_internal_data", None) - - if not net.converged: - msg = "The hydraulic calculation did not converge to a solution." - raise PipeflowNotConverged(msg) - extract_results_active_pit(net, mode="hydraulics") - - -def heat_transfer(net): - # Start of nonlinear loop - # --------------------------------------------------------------------------------------------- - net.converged = False - identify_active_nodes_branches(net, False) - reduce_pit(net, mode="heat_transfer") - if net.fluid.is_gas: - logger.info("Caution! Temperature calculation does currently not affect hydraulic " - "properties!") - solver_vars = ['Tout', 'T'] - tol_temp = next(get_net_options(net, 'tol_T')) - newton_raphson(net, solve_temperature, 'heat', solver_vars, [tol_temp, tol_temp], ['branch', 'node'], - 'max_iter_therm') - - if net.converged: - rerun_heat_transfer(net) - - if not net.converged: - msg = "The heat transfer calculation did not converge to a solution." - raise PipeflowNotConverged(msg) - extract_results_active_pit(net, mode="heat_transfer") - - -def solve_bidirectional(net): - reduce_pit(net, mode="hydraulics") - res_hyd, residual_hyd, filter_hyd = solve_hydraulics(net) - extract_results_active_pit(net, mode="hydraulics") - identify_active_nodes_branches(net, False) - reduce_pit(net, mode="heat_transfer") - res_heat, residual_heat, filter_heat = solve_temperature(net) - extract_results_active_pit(net, mode="heat_transfer") - residual = np.concatenate([residual_hyd, residual_heat]) - res = res_hyd + res_heat - filtered = filter_hyd + filter_heat - return res, residual, filtered - - -def solve_hydraulics(net): - """ - Create and solve the linearized system of equations (based on a jacobian in form of a scipy - sparse matrix and a load vector in form of a numpy array) in order to calculate the hydraulic - magnitudes (pressure and velocity) for the network nodes and branches. - - :param net: The pandapipesNet for which to solve the hydraulic matrix - :type net: pandapipesNet - :return: - - """ - options = net["_options"] - - connected_restarted = True - while connected_restarted: - branch_pit = net["_active_pit"]["branch"] - node_pit = net["_active_pit"]["node"] - branch_pit_old = net["_active_old_pit"]["branch"] - node_pit_old = net["_active_old_pit"]["node"] - branch_lookups = get_lookup(net, "branch", "from_to_active_hydraulics") - for comp in net['component_list']: - comp.adaption_before_derivatives_hydraulic(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - branch_lookups, - options) - calculate_derivatives_hydraulic(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - options) - for comp in net['component_list']: - comp.adaption_after_derivatives_hydraulic( - net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - branch_lookups, options) - - connected_restarted = _restart_connectivity_check(net) - # epsilon is node [pressure] slack nodes and load vector branch prsr difference - # jacobian is the derivatives - jacobian, epsilon = build_system_matrix(net, branch_pit, node_pit, False) - - m_init_old = branch_pit[:, MDOTINIT].copy() - p_init_old = node_pit[:, PINIT].copy() - slack_nodes = np.where(node_pit[:, NODE_TYPE] == P)[0] - msl_init_old = node_pit[slack_nodes, MDOTSLACKINIT].copy() - - # x is next step pressures and velocity - x = spsolve(jacobian, epsilon) - - branch_pit[:, MDOTINIT] -= x[len(node_pit):len(node_pit) + len(branch_pit)] * options["alpha"] - node_pit[:, PINIT] -= x[:len(node_pit)] * options["alpha"] - node_pit[slack_nodes, MDOTSLACKINIT] -= x[len(node_pit) + len(branch_pit):] - - filtered = [None, None, slack_nodes] - return [branch_pit[:, MDOTINIT], m_init_old, node_pit[:, PINIT], p_init_old, node_pit[slack_nodes, MDOTSLACKINIT] - ,msl_init_old], epsilon, filtered - -def rerun_hydraulics(net): - rerun = False - options = net["_options"] - branch_pit = net["_active_pit"]["branch"] - node_pit = net["_active_pit"]["node"] - branch_lookups = get_lookup(net, "branch", "from_to_active_hydraulics") - for comp in net['component_list']: - rerun |= comp.rerun_hydraulics(net, branch_pit, node_pit, branch_lookups, options) - if rerun: - extract_results_active_pit(net, 'hydraulics') - identify_active_nodes_branches(net) - hydraulics(net) - -def rerun_heat_transfer(net): - rerun = False - options = net["_options"] - branch_pit = net["_active_pit"]["branch"] - node_pit = net["_active_pit"]["node"] - branch_lookups = get_lookup(net, "branch", "from_to_active_heat_transfer") - for comp in net['component_list']: - rerun |= comp.rerun_hydraulics(net, branch_pit, node_pit, branch_lookups, options) - if rerun: - extract_results_active_pit(net, 'heat_transfer') - identify_active_nodes_branches(net, False) - heat_transfer(net) - -def _restart_connectivity_check(net): - nodes_connected = get_lookup(net, "node", "active_hydraulics") - branches_connected = get_lookup(net, "branch", "active_hydraulics") - rows_nodes = np.arange(net["_pit"]["node"].shape[0])[nodes_connected] - rows_branches = np.arange(net["_pit"]["branch"].shape[0])[branches_connected] - active_node_pit = net["_active_pit"]["node"] - active_branch_pit = net["_active_pit"]["branch"] - node_pit = net["_pit"]["node"][rows_nodes, ACTIVE_NODE] - branch_pit = net["_pit"]["branch"][rows_branches, ACTIVE_BRANCH] - mask_diff_node = active_node_pit[:, ACTIVE_NODE] != node_pit - mask_diff_branch = active_branch_pit[:, ACTIVE_BRANCH] != branch_pit - if np.any(mask_diff_node) | np.any(mask_diff_branch): - net["_pit"]["node"][rows_nodes, ACTIVE_NODE] = active_node_pit[:, ACTIVE_NODE] - net["_pit"]["node"][rows_nodes, NODE_TYPE] = active_node_pit[:, NODE_TYPE] - net["_pit"]["branch"][rows_branches, ACTIVE_BRANCH] = active_branch_pit[:, ACTIVE_BRANCH] - net["_pit"]["branch"][rows_branches, BRANCH_TYPE] = active_branch_pit[:, BRANCH_TYPE] - identify_active_nodes_branches(net, True) - reduce_pit(net, mode='hydraulics') - return True - return False - - -def solve_temperature(net): - """ - This function contains the procedure to build and solve a linearized system of equation based on - an underlying net and the necessary graph data structures. Temperature values are calculated. - Returned are the solution vectors for the new iteration, the original solution vectors and a - vector containing component indices for the system matrix entries - - :param net: The pandapipesNet for which to solve the temperature matrix - :type net: pandapipesNet - :return: branch_pit - - """ - - options = net["_options"] - branch_pit = net["_active_pit"]["branch"] - node_pit = net["_active_pit"]["node"] - branch_pit_old = net["_active_old_pit"]["branch"] - node_pit_old = net["_active_old_pit"]["node"] - - - branch_lookups = get_lookup(net, "branch", "from_to_active_heat_transfer") - - # Negative velocity values are turned to positive ones (including exchange of from_node and - # to_node for temperature calculation - branch_pit[:, FROM_NODE_T_SWITCHED] = branch_pit[:, MDOTINIT] < -2e-11 - - for comp in net['component_list']: - comp.adaption_before_derivatives_thermal(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - branch_lookups, options) - calculate_derivatives_thermal(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - options) - for comp in net['component_list']: - comp.adaption_after_derivatives_thermal(net, - branch_pit, node_pit, - branch_pit_old, node_pit_old, - branch_lookups, options) - - t_init_old = node_pit[:, TINIT].copy() - t_out_old = branch_pit[:, TOUTINIT].copy() - filtered = [None, None] - if not check_infeed_number(node_pit): - return [branch_pit[:, TOUTINIT], t_out_old, node_pit[:, TINIT], t_init_old], np.array([ - np.nan]), filtered - - jacobian, epsilon = build_system_matrix(net, branch_pit, node_pit, True) - - x = spsolve(jacobian, epsilon) - - if np.any(np.isnan(x)): - return [branch_pit[:, TOUTINIT], t_out_old, node_pit[:, TINIT], t_init_old], np.array([ - np.nan]), filtered - - node_pit[:, TINIT] -= x[:len(node_pit)] * options["alpha"] - branch_pit[:, TOUTINIT] -= x[len(node_pit):] * options["alpha"] - - return [branch_pit[:, TOUTINIT], t_out_old, node_pit[:, TINIT], t_init_old], epsilon, filtered - - -def set_damping_factor(net, niter, errors): - """ - Set the value of the damping factor (factor for the newton step width) from current results. - - :param net: the net for which to perform the pipeflow - :type net: pandapipesNet - :param niter: - :type niter: - :param errors: an array containing the current residuals of all field variables solved for - :return: No Output. - - EXAMPLE: - set_damping_factor(net, niter, [error_p, error_v]) - """ - error_increased = [] - for error in errors.values(): - error_increased.append(error[niter] > error[niter - 1]) - current_alpha = get_net_option(net, "alpha") - if np.all(error_increased): - set_net_option(net, "alpha", current_alpha / 10 if current_alpha >= 0.1 else current_alpha) - else: - set_net_option(net, "alpha", current_alpha * 10 if current_alpha <= 0.1 else 1.0) - return error_increased - - -def finalize_iteration(net, niter, residual_norm, nonlinear_method, errors, tols, tol_res, vals_old, - solver_vars, pit_names, filtered): - # Control of damping factor - if nonlinear_method == "automatic": - errors_increased = set_damping_factor(net, niter, errors) - logger.debug("alpha: %s" % get_net_option(net, "alpha")) - for error_increased, var, val, pit, f in zip(errors_increased, solver_vars, vals_old, - pit_names, filtered): - if error_increased: - if f is None: - # todo: not working in bidirectional mode as bidirectional is not distinguishing \ - # between hydraulics and heat transfer active pit - net["_active_pit"][pit][:, globals()[var.upper() + 'INIT']] = val - else: - net["_active_pit"][pit][f, globals()[var.upper() + 'INIT']] = val - if get_net_option(net, "alpha") != 1: - net.converged = False - return - elif nonlinear_method != "constant": - logger.warning("No proper nonlinear method chosen. Using constant settings.") - converged = True - for error, var, tol in zip(errors.values(), solver_vars, tols): - converged = error[niter] <= tol - if not converged: break - logger.debug("error_%s: %s" % (var, error[niter])) - net.converged = converged and residual_norm <= tol_res - - -def log_final_results(net, solver, niter, residual_norm, solver_vars, tols): - logger.debug("--------------------------------------------------------------------------------") - if not net.converged: - logger.debug( - "Maximum number of iterations reached but %s solver did not converge." % solver) - logger.debug("Norm of residual: %s" % residual_norm) - else: - logger.debug("Calculation completed. Preparing results...") - logger.debug("Converged after %d iterations." % niter) - logger.debug("Norm of residual: %s" % residual_norm) - for var, tol in zip(solver_vars, tols): - logger.debug("tolerance for %s: %s" % (var, tol)) + execute_heat(net) diff --git a/src/pandapipes/plotting/__init__.py b/src/pandapipes/plotting/__init__.py index fddc04e54..ac627fa8e 100644 --- a/src/pandapipes/plotting/__init__.py +++ b/src/pandapipes/plotting/__init__.py @@ -16,17 +16,16 @@ class GC(GraphicsContextBase): - """ - - """ + """Graphics context with a rounded line cap style.""" def __init__(self): + """Initialize the graphics context and set the cap style to round.""" super().__init__() self._capstyle = 'round' def custom_new_gc(self): - """ + """Return a new custom graphics context. :param self: :type self: diff --git a/src/pandapipes/plotting/collections.py b/src/pandapipes/plotting/collections.py index 32a27820e..5ac4980c9 100644 --- a/src/pandapipes/plotting/collections.py +++ b/src/pandapipes/plotting/collections.py @@ -24,8 +24,7 @@ def create_junction_collection(net, junctions=None, size=5, patch_type="circle", z=None, cmap=None, norm=None, infofunc=None, picker=False, junction_geodata=None, cbar_title="Junction Pressure [bar]", **kwargs): - """ - Creates a matplotlib patch collection of pandapipes junctions. + """Create a matplotlib patch collection of pandapipes junctions. :param net: The pandapipes network :type net: pandapipesNet @@ -92,8 +91,7 @@ def create_pipe_collection(net, pipes=None, pipe_geodata=None, junction_geodata= use_junction_geodata=False, infofunc=None, cmap=None, norm=None, picker=False, z=None, cbar_title="Pipe Loading [%]", clim=None, **kwargs): - """ - Creates a matplotlib pipe collection of pandapipes pipes. + """Create a matplotlib pipe collection of pandapipes pipes. :param net: The pandapipes network :type net: pandapipesNet @@ -169,8 +167,7 @@ def create_pipe_collection(net, pipes=None, pipe_geodata=None, junction_geodata= def create_sink_collection(net, sinks=None, size=1., infofunc=None, picker=False, orientation=(np.pi*5/6), cmap=None, norm=None, z=None, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes sinks. + """Create a matplotlib patch collection of pandapipes sinks. :param net: The pandapipes network :type net: pandapipesNet @@ -223,8 +220,7 @@ def create_sink_collection(net, sinks=None, size=1., infofunc=None, picker=False def create_source_collection(net, sources=None, size=1., infofunc=None, picker=False, orientation=(np.pi*7/6), cmap=None, norm=None, z=None, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes sources. + """Create a matplotlib patch collection of pandapipes sources. :param net: The pandapipes network :type net: pandapipesNet @@ -277,10 +273,10 @@ def create_source_collection(net, sources=None, size=1., infofunc=None, picker=F def create_ext_grid_collection(net, size=1., infofunc=None, orientation=0, picker=False, ext_grids=None, ext_grid_junctions=None, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes ext_grid. Parameters - ext_grids, ext_grid_junctions can be used to specify, which ext_grids the collection should be - created for. + """Create a matplotlib patch collection of pandapipes ext_grid. + + Parameters ext_grids, ext_grid_junctions can be used to specify, which ext_grids the + collection should be created for. :param net: The pandapipes network :type net: pandapipesNet @@ -327,8 +323,8 @@ def create_ext_grid_collection(net, size=1., infofunc=None, orientation=0, picke def create_heat_exchanger_collection(net, heat_ex=None, size=5., junction_geodata=None, infofunc=None, picker=False, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes junction-junction heat_exchangers. + """Create a matplotlib patch collection of pandapipes junction-junction heat_exchangers. + Heat_exchangers are plotted in the center between two junctions with a "helper" line (dashed and thin) being drawn between the junctions as well. @@ -382,10 +378,10 @@ def create_heat_exchanger_collection(net, heat_ex=None, size=5., junction_geodat def create_valve_collection(net, valves=None, size=5., junction_geodata=None, infofunc=None, picker=False, fill_closed=True, respect_valves=False, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes junction-junction valves. Valves are - plotted in the center between two junctions with a "helper" line (dashed and thin) being drawn - between the junctions as well. + """Create a matplotlib patch collection of pandapipes junction-junction valves. + + Valves are plotted in the center between two junctions with a "helper" line (dashed and thin) + being drawn between the junctions as well. :param net: The pandapipes network :type net: pandapipesNet @@ -458,8 +454,7 @@ def create_valve_collection(net, valves=None, size=5., junction_geodata=None, in def create_flow_control_collection(net, flow_controllers=None, size=5., junction_geodata=None, infofunc=None, picker=False, fill_closed=True, respect_in_service=False, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes flow control components. + """Create a matplotlib patch collection of pandapipes flow control components. They are plotted in the center between two junctions and look like a valve with a T on top, if the flow control is active and an I on top, if the flow control is not active. @@ -525,8 +520,7 @@ def create_flow_control_collection(net, flow_controllers=None, size=5., junction def create_pump_collection(net, pumps=None, table_name='pump', size=5., junction_geodata=None, infofunc=None, picker=False, fj_col="from_junction", tj_col="to_junction", **kwargs): - """ - Creates a matplotlib patch collection of pandapipes pumps. + """Create a matplotlib patch collection of pandapipes pumps. :param net: The pandapipes network :type net: pandapipesNet @@ -633,9 +627,9 @@ def create_pressure_control_collection(net, pcs=None, table_name='press_control' def create_compressor_collection(net, cmprs=None, table_name='compressor', size=5., junction_geodata=None, color='k', infofunc=None, picker=False, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes compressors. Compressors are - plotted in the center between two junctions. + """Create a matplotlib patch collection of pandapipes compressors. + + Compressors are plotted in the center between two junctions. :param net: The pandapipes network :type net: pandapipesNet @@ -684,9 +678,9 @@ def create_compressor_collection(net, cmprs=None, table_name='compressor', size= def create_heat_consumer_collection(net, hec=None, table_name='heat_consumer', size=5., junction_geodata=None, color='k', infofunc=None, picker=False, **kwargs): - """ - Creates a matplotlib patch collection of pandapipes heat_consumers. Heat consumers are - plotted in the center between two junctions. + """Create a matplotlib patch collection of pandapipes heat_consumers. + + Heat consumers are plotted in the center between two junctions. :param net: The pandapipes network :type net: pandapipesNet diff --git a/src/pandapipes/plotting/generic_geodata.py b/src/pandapipes/plotting/generic_geodata.py index 0eea0ffde..7d2aed179 100644 --- a/src/pandapipes/plotting/generic_geodata.py +++ b/src/pandapipes/plotting/generic_geodata.py @@ -20,8 +20,8 @@ def build_igraph_from_ppipes(net, junctions=None, weight_column_lookup="length_km", edge_factories_override=None, additional_edge_factories=None, exclude_branch_elements=(), ignore_in_service_branch_elements=()): - """ - This function uses the igraph library to create an igraph graph for a given pandapipes network. + """Use the igraph library to create an igraph graph for a given pandapipes network. + Any branch component is respected. Performance vs. networkx: https://graph-tool.skewed.de/performance @@ -37,7 +37,6 @@ def build_igraph_from_ppipes(net, junctions=None, weight_column_lookup="length_k :Example: graph, meshed, roots = build_igraph_from_pp(net) """ - try: import igraph as ig except (DeprecationWarning, ImportError): @@ -93,10 +92,10 @@ def build_igraph_from_ppipes(net, junctions=None, weight_column_lookup="length_k def create_generic_coordinates(net, mg=None, library="igraph", geodata_table="junction_geodata", junctions=None, overwrite=False, **kwargs): - """ - This function will add arbitrary geo-coordinates for all junctions based on an analysis of - branches and rings. It will remove out of service junctions/pipes from the net. The coordinates - will be created either by igraph or by using networkx library. + """Add arbitrary geo-coordinates for all junctions based on an analysis of branches and rings. + + It will remove out of service junctions/pipes from the net. The coordinates will be created + either by igraph or by using networkx library. :param net: pandapipes network :type net: pandapipesNet diff --git a/src/pandapipes/plotting/geo.py b/src/pandapipes/plotting/geo.py index 631f9b502..23a6e9a42 100644 --- a/src/pandapipes/plotting/geo.py +++ b/src/pandapipes/plotting/geo.py @@ -8,8 +8,7 @@ def convert_gis_to_geodata(net, node_geodata=True, branch_geodata=True): - """ - Extracts information on bus and line geodata from the geometries of a geopandas geodataframe. + """Extract information on bus and line geodata from the geometries of a geopandas geodataframe. :param net: The net for which to convert the geodata :type net: pandapowerNet @@ -26,9 +25,7 @@ def convert_gis_to_geodata(net, node_geodata=True, branch_geodata=True): def convert_geodata_to_gis(net, epsg=31467, node_geodata=True, branch_geodata=True): - """ - Transforms the bus and line geodata of a net into a geopandaas geodataframe with the respective - geometries. + """Transforms the bus and line geodata of a net into a geopandaas geodataframe with the respective geometries. :param net: The net for which to convert the geodata :type net: pandapowerNet @@ -48,8 +45,7 @@ def convert_geodata_to_gis(net, epsg=31467, node_geodata=True, branch_geodata=Tr def convert_epsg_junction_geodata(net, epsg_in=4326, epsg_out=31467): - """ - Converts bus geodata in net from epsg_in to epsg_out + """Converts bus geodata in net from epsg_in to epsg_out. :param net: The pandapipes network :type net: pandapipesNet diff --git a/src/pandapipes/plotting/patch_makers.py b/src/pandapipes/plotting/patch_makers.py index 80527f114..95c64a814 100644 --- a/src/pandapipes/plotting/patch_makers.py +++ b/src/pandapipes/plotting/patch_makers.py @@ -104,8 +104,7 @@ def heat_exchanger_patches(coords, size, **kwargs): def source_patches(node_coords, size, angles, **kwargs): - """ - Creation function of patches for sources. + """Creation function of patches for sources. :param node_coords: coordinates of the nodes that the sources belong to. :type node_coords: iterable @@ -220,8 +219,8 @@ def pressure_control_patches(coords, size, **kwargs): return lines, polys, {} def heat_consumer_patches(coords, size, **kwargs): - """ - Creates patches for matplotlib plotting of heat consumer component + """Creates patches for matplotlib plotting of heat consumer component. + :param coords: :type coords: :param size: @@ -279,8 +278,8 @@ def heat_consumer_patches(coords, size, **kwargs): def create_triangles(center, size, direc, normal, edgecolor): - """ - Creates a pathpatch for triangles + """Creates a pathpatch for triangles. + :param center: :type center: :param size: @@ -294,7 +293,6 @@ def create_triangles(center, size, direc, normal, edgecolor): :return: :rtype: """ - radius = size # Create the first triangle diff --git a/src/pandapipes/plotting/pipeflow_results.py b/src/pandapipes/plotting/pipeflow_results.py index 69daaecb6..e55b3ec93 100644 --- a/src/pandapipes/plotting/pipeflow_results.py +++ b/src/pandapipes/plotting/pipeflow_results.py @@ -11,17 +11,17 @@ def pressure_profile_to_junction_geodata(net): - """ - Calculates pressure profile for a pandapipes network. + """Calculates pressure profile for a pandapipes network. - INPUT: + INPUT: **net** (pandapipesNet) - Variable that contains a pandapipes network. - OUTPUT: - **bgd** - Returns a pandas DataFrame containing distance to the closest ext_grid as x \ + OUTPUT: + **bgd** - Returns a pandas DataFrame containing distance to the closest ext_grid as x coordinate and pressure level as y coordinate for each junction. - EXAMPLE: + Example + ------- import pandapipes.networks as nw import pandapipes.plotting as plotting import pandapipes as pp diff --git a/src/pandapipes/plotting/plotting_toolbox.py b/src/pandapipes/plotting/plotting_toolbox.py index 11a74811f..14cf62b22 100644 --- a/src/pandapipes/plotting/plotting_toolbox.py +++ b/src/pandapipes/plotting/plotting_toolbox.py @@ -14,9 +14,7 @@ def get_collection_sizes(net, junction_size=1.0, ext_grid_size=1.0, sink_size=1.0, source_size=1.0, valve_size=2.0, pump_size=1.0, heat_exchanger_size=1.0, pressure_control_size=1.0, compressor_size=1.0, flow_control_size=1.0, heat_consumer_size=1.0): - """ - Calculates the size for most collection types according to the distance between min and max - geocoord so that the collections fit the plot nicely + """Calculates the size for most collection types according to the distance between min and max geocoord so that the collections fit the plot nicely. .. note: This is implemented because if you would choose a fixed values (e.g.\ junction_size = 0.2), the size could be too small for large networks and vice versa @@ -64,9 +62,9 @@ def get_collection_sizes(net, junction_size=1.0, ext_grid_size=1.0, sink_size=1. def coords_from_node_geodata(element_indices, from_nodes, to_nodes, node_geodata, table_name, node_name="Bus", ignore_zero_length=True): - """ - Auxiliary function to get the node coordinates for a number of branches with respective from - and to nodes. The branch elements for which there is no geodata available are not included in + """Auxiliary function to get the node coordinates for a number of branches with respective from and to nodes. + + The branch elements for which there is no geodata available are not included in the final list of coordinates. :param element_indices: Indices of the branch elements for which to find node geodata diff --git a/src/pandapipes/plotting/simple_plot.py b/src/pandapipes/plotting/simple_plot.py index dc31e1ed7..f3131ab19 100644 --- a/src/pandapipes/plotting/simple_plot.py +++ b/src/pandapipes/plotting/simple_plot.py @@ -33,9 +33,9 @@ def simple_plot(net, respect_valves=False, respect_in_service=True, pipe_width=2 ext_grid_color='orange', valve_color='silver', pump_color='silver', heat_exchanger_color='silver', pressure_control_color='silver', compressor_color='silver', flow_control_color='silver', heat_consumer_color='silver',library="igraph", show_plot=True, ax=None, **kwargs): - """ - Plots a pandapipes network as simple as possible. If no geodata is available, artificial - geodata is generated. For advanced plotting see + """Plots a pandapipes network as simple as possible. + + If no geodata is available, artificial geodata is generated. For advanced plotting see the `tutorial `_. :param net: The pandapipes format network. @@ -157,10 +157,10 @@ def create_simple_collections(net, respect_valves=False, respect_in_service=True heat_exchanger_color='silver', pressure_control_color='silver', compressor_color='silver', flow_control_color='silver', heat_consumer_color='silver', library="igraph", as_dict=True, **kwargs): - """ - Plots a pandapipes network as simple as possible. + """Plots a pandapipes network as simple as possible. + If no geodata is available, artificial geodata is generated. For advanced plotting see the - tutorial + tutorial. :param net: The pandapipes format network. :type net: pandapipesNet @@ -235,7 +235,6 @@ def create_simple_collections(net, respect_valves=False, respect_in_service=True :return: collections - list of simple collections for the given network """ # don't hide lines if switches are plotted - # create geocoord if none are available if len(net.junction_geodata) == 0 and len(net.pipe_geodata) == 0: logger.warning("No or insufficient geodata available --> Creating artificial coordinates." + diff --git a/src/pandapipes/properties/fluids.py b/src/pandapipes/properties/fluids.py index 5e90b918c..ec459c56d 100644 --- a/src/pandapipes/properties/fluids.py +++ b/src/pandapipes/properties/fluids.py @@ -23,12 +23,10 @@ class Fluid(JSONSerializableClass): - """ - - """ + """Fluid used within a pandapipes network, storing its properties.""" def __init__(self, name, fluid_type, **kwargs): - """ + """Initialize the fluid with a name, type and properties. :param name: :type name: @@ -52,21 +50,18 @@ def __init__(self, name, fluid_type, **kwargs): "cause problems when trying to ask for values." % prop_name) def __repr__(self): - """ - Definition of fluid representation in the console. + """Definition of fluid representation in the console. :return: representation of fluid in the console :rtype: str """ - r = "Fluid %s (%s) with properties:" % (self.name, self.fluid_type) for key in self.all_properties.keys(): r += "\n - %s (%s)" % (key, self.all_properties[key].__class__.__name__[13:]) return r def add_property(self, property_name, prop, overwrite=True, warn_on_duplicates=True): - """ - This function adds a new property. + """Add a new property. :param property_name: Name of the new property :type property_name: str @@ -81,7 +76,6 @@ def add_property(self, property_name, prop, overwrite=True, warn_on_duplicates=T :Example: >>> fluid.add_property('water_density', pandapipes.FluidPropertyConstant(998.2061),\ overwrite=True, warn_on_duplicates=False) - """ if property_name in self.all_properties: if warn_on_duplicates: @@ -92,8 +86,7 @@ def add_property(self, property_name, prop, overwrite=True, warn_on_duplicates=T self.all_properties[property_name] = prop def get_property(self, property_name, *at_values): - """ - This function returns the value of the requested property. + """Return the value of the requested property. :param property_name: Name of the searched property :type property_name: str @@ -102,34 +95,28 @@ def get_property(self, property_name, *at_values): :return: Returns property at the certain value :rtype: float, array """ - if property_name not in self.all_properties: raise UserWarning("The property %s was not defined for the fluid %s" % (property_name, self.name)) return self.all_properties[property_name].get_at_value(*at_values) def get_density(self, temperature): - """ - This function returns the density at a certain temperature. + """Return the density at a certain temperature. :param temperature: Temperature at which the density is queried :type temperature: float :return: Density at the required temperature - """ - return self.get_property("density", temperature) def get_viscosity(self, temperature, p_bar=None): - """ - This function returns the viscosity at a certain temperature. + """Return the viscosity at a certain temperature. :param temperature: Temperature at which the viscosity is queried :type temperature: float or array of floats :param p_bar: Pressure at which the viscosity is queried :type p_bar: float or array of floats :return: Viscosity at the required temperature - """ visc_prop = self.all_properties.get("viscosity") if visc_prop is None: @@ -141,37 +128,29 @@ def get_viscosity(self, temperature, p_bar=None): return visc_prop.get_at_value(*args) def get_heat_capacity(self, temperature): - """ - This function returns the heat capacity at a certain temperature. + """Return the heat capacity at a certain temperature. :param temperature: Temperature at which the heat capacity is queried :type temperature: float :return: Heat capacity at the required temperature - """ - return self.get_property("heat_capacity", temperature) def get_molar_mass(self): - """ - This function returns the molar mass. + """Return the molar mass. :return: molar mass - """ - return self.get_property("molar_mass") def get_compressibility(self, p_bar, t_k=None): - """ - This function returns the compressibility at a certain pressure. + """Return the compressibility at a certain pressure. :param p_bar: pressure at which the compressibility is queried :type p_bar: float or array of floats :param t_k: temperature at which the compressibility is queried (optional) :type t_k: float or array of floats or None :return: compressibility at the required pressure - """ comp_prop = self.all_properties.get("compressibility") if comp_prop is None: @@ -183,29 +162,22 @@ def get_compressibility(self, p_bar, t_k=None): return comp_prop.get_at_value(*args) def get_der_compressibility(self): - """ - This function returns the derivative of the compressibility with respect to pressure. + """Return the derivative of the compressibility with respect to pressure. :return: derivative of the compressibility - """ - return self.get_property("der_compressibility") class FluidProperty(JSONSerializableClass): - """ - Property Base Class - """ + """Base class for fluid properties.""" def __init__(self): - """ - - """ + """Initialize the fluid property.""" super().__init__() def get_at_value(self, *args): - """ + """Return the property value at the given argument(s). :param args: :type args: @@ -215,7 +187,7 @@ def get_at_value(self, *args): raise NotImplementedError("Please implement a proper fluid property!") def get_at_integral_value(self, *args): - """ + """Return the property's integral value between the given argument(s). :param args: :type args: @@ -226,14 +198,13 @@ def get_at_integral_value(self, *args): class FluidPropertyInterExtra(FluidProperty): - """ - Creates Property with interpolated or extrapolated values. - """ + """Creates Property with interpolated or extrapolated values.""" + json_excludes = JSONSerializableClass.json_excludes + ["prop_getter"] prop_getter_entries = {"x": "x", "y": "y", "_fill_value_orig": "fill_value"} def __init__(self, x_values, y_values, method="interpolate_extrapolate"): - """ + """Initialize the interpolated or extrapolated fluid property. :param x_values: :type x_values: @@ -249,7 +220,7 @@ def __init__(self, x_values, y_values, method="interpolate_extrapolate"): self.prop_getter = interp1d(x_values, y_values) def get_at_value(self, arg): - """ + """Return the interpolated or extrapolated y-value(s) for the given x-value(s). :param arg: Name of the property and one or more values (x-values) for which the y-values \ of the property are to be displayed @@ -260,7 +231,7 @@ def get_at_value(self, arg): return self.prop_getter(arg) def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): - """ + """Return the integral of the property between the given limits. :param upper_limit_arg: one or more values of upper limit values for which the function \ of the property should calculate the integral for @@ -274,16 +245,13 @@ def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): :Example: >>> comp_fact = get_fluid(net).all_properties["heat_capacity"].get_at_integral_value(\ t_upper_k, t_lower_k) - """ mean = (self.prop_getter(upper_limit_arg) + self.prop_getter(upper_limit_arg)) / 2 return mean * (upper_limit_arg-lower_limit_arg) @classmethod def from_path(cls, path, method="interpolate_extrapolate"): - """ - Reads a text file with temperature values in the first column and property values in - second column. + """Reads a text file with temperature values in the first column and property values in second column. :param path: Target path of the txt file :type path: str @@ -315,12 +283,10 @@ def from_dict(cls, d): class FluidPropertyConstant(FluidProperty): - """ - Creates Property with a constant value. - """ + """Creates Property with a constant value.""" def __init__(self, value, warn_dependent_variables=False): - """ + """Initialize the constant fluid property. :param value: :type value: @@ -330,7 +296,7 @@ def __init__(self, value, warn_dependent_variables=False): self.warn_dependent_variables = warn_dependent_variables def get_at_value(self, *args): - """ + """Return the constant value of the property. :param args: Name of the property :type args: str @@ -356,7 +322,7 @@ def get_at_value(self, *args): return output def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): - """ + """Return the integral of the constant property between the given limits. :param upper_limit_arg: one or more values of upper limit values for which the function \ of the property should calculate the integral for @@ -370,7 +336,6 @@ def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): :Example: >>> comp_fact = get_fluid(net).all_properties["heat_capacity"].get_at_integral_value(\ t_upper_k, t_lower_k) - """ if isinstance(upper_limit_arg, pd.Series): ul = self.value * upper_limit_arg.values @@ -384,9 +349,7 @@ def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): @classmethod def from_path(cls, path): - """ - Reads a text file with temperature values in the first column and property values in - second column. + """Reads a text file with temperature values in the first column and property values in second column. :param path: :type path: @@ -405,25 +368,22 @@ def from_dict(cls, d): class FluidPropertyLinear(FluidProperty): - """ - Creates Property with a linear course. - """ + """Creates Property with a linear course.""" def __init__(self, slope, offset): - """ + """Initialize the linear fluid property. :param slope: :type slope: :param offset: :type offset: - """ super(FluidPropertyLinear, self).__init__() self.slope = slope self.offset = offset def get_at_value(self, arg): - """ + """Return the linear function value at the given x-value(s). :param arg: Name of the property and one or more values (x-values) for which the function \ of the property should be calculated @@ -433,7 +393,6 @@ def get_at_value(self, arg): :Example: >>> comp_fact = get_fluid(net).all_properties["compressibility"].get_at_value(p_bar) - """ if isinstance(arg, pd.Series): return self.offset + self.slope * arg.values @@ -441,7 +400,7 @@ def get_at_value(self, arg): return self.offset + self.slope * np.array(arg) def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): - """ + """Return the integral of the linear property between the given limits. :param upper_limit_arg: one or more values of upper limit values for which the function \ of the property should calculate the integral for @@ -455,7 +414,6 @@ def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): :Example: >>> comp_fact = get_fluid(net).all_properties["heat_capacity"].get_at_integral_value(\ t_upper_k, t_lower_k) - """ if isinstance(upper_limit_arg, pd.Series): ul = self.offset * upper_limit_arg.values + 0.5 * self.slope * np.power( @@ -473,9 +431,7 @@ def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): @classmethod def from_path(cls, path): - """ - Reads a text file with temperature values in the first column and property values in - second column. + """Reads a text file with temperature values in the first column and property values in second column. :param path: :type path: @@ -487,12 +443,10 @@ def from_path(cls, path): class FluidPropertyPolynominal(FluidProperty): - """ - Creates Property with a polynominal course. - """ + """Creates Property with a polynominal course.""" def __init__(self, x_values, y_values, polynominal_degree): - """ + """Initialize the polynomial fluid property. :param x_values: :type x_values: @@ -507,7 +461,7 @@ def __init__(self, x_values, y_values, polynominal_degree): self.prop_int_getter = np.polyint(self.prop_getter) def get_at_value(self, arg): - """ + """Return the polynomial value at the given x-value(s). :param arg: Name of the property and one or more values (x-values) for which the function \ of the property should be calculated @@ -517,12 +471,11 @@ def get_at_value(self, arg): :Example: >>> comp_fact = get_fluid(net).all_properties["heat_capacity"].get_at_value(t_k) - """ return self.prop_getter(arg) def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): - """ + """Return the integral of the polynomial property between the given limits. :param upper_limit_arg: one or more values of upper limit values for which the function \ of the property should calculate the integral for @@ -536,15 +489,12 @@ def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): :Example: >>> comp_fact = get_fluid(net).all_properties["heat_capacity"].get_at_integral_value(\ t_upper_k, t_lower_k) - """ return self.prop_int_getter(upper_limit_arg) - self.prop_int_getter(lower_limit_arg) @classmethod def from_path(cls, path, polynominal_degree): - """ - Reads a text file with temperature values in the first column and property values in - second column. + """Reads a text file with temperature values in the first column and property values in second column. :param path: Target path of the txt file :type path: str @@ -558,12 +508,10 @@ def from_path(cls, path, polynominal_degree): class FluidPropertySutherland(FluidProperty): - """ - Creates Property with a Sutherland model (mainly used for viscosity). - """ + """Creates Property with a Sutherland model (mainly used for viscosity).""" def __init__(self, eta0, t0, t_sutherland): - """ + """Initialize the Sutherland fluid property model. :param value: :type value: @@ -574,7 +522,7 @@ def __init__(self, eta0, t0, t_sutherland): self.t_sutherland = t_sutherland def get_at_value(self, *args): - """ + """Return the Sutherland-model value of the property at the given temperature. :param arg: Name of the property :type arg: str @@ -604,8 +552,7 @@ def get_at_integral_value(self, upper_limit_arg, lower_limit_arg): def create_constant_property(net, property_name, value, overwrite=True, warn_on_duplicates=True): - """ - Creates a property with a constant value. + """Creates a property with a constant value. :param net: Name of the network to which the property is added :type net: pandapipesNet @@ -627,8 +574,7 @@ def create_constant_property(net, property_name, value, overwrite=True, warn_on_ def create_linear_property(net, property_name, slope, offset, overwrite=True, warn_on_duplicates=True): - """ - Creates a property with a linear correlation. + """Creates a property with a linear correlation. :param net: Name of the network to which the property is added :type net: pandapipesNet @@ -651,8 +597,7 @@ def create_linear_property(net, property_name, slope, offset, overwrite=True, def create_constant_fluid(name=None, fluid_type=None, **kwargs): - """ - Creates a constant fluid. + """Creates a constant fluid. :param name: Name of the fluid :type name: str @@ -670,15 +615,13 @@ def create_constant_fluid(name=None, fluid_type=None, **kwargs): def call_lib(fluid_name): - """ - Creates a fluid with default fluid properties. + """Creates a fluid with default fluid properties. :param fluid_name: Fluid which should be used :type fluid_name: str :return: Fluid - Chosen fluid with default fluid properties :rtype: Fluid """ - def interextra_property(prop): return FluidPropertyInterExtra.from_path( os.path.join(pp_dir, "properties", fluid_name, prop + ".txt")) @@ -722,8 +665,7 @@ def linear_property(prop): def get_fluid(net): - """ - This function shows which fluid is used in the net. + """Return the fluid used in the net. :param net: Current network :type net: pandapipesNet @@ -740,8 +682,7 @@ def get_fluid(net): def _add_fluid_to_net(net, fluid, overwrite=True): - """ - Adds a fluid to a net. If overwrite is False, a warning is printed and the fluid is not set. + """Adds a fluid to a net. If overwrite is False, a warning is printed and the fluid is not set. :param net: The pandapipes network for which to set fluid :type net: pandapipesNet diff --git a/src/pandapipes/properties/properties_toolbox.py b/src/pandapipes/properties/properties_toolbox.py index 834fa2c1d..f490bff7b 100644 --- a/src/pandapipes/properties/properties_toolbox.py +++ b/src/pandapipes/properties/properties_toolbox.py @@ -5,15 +5,14 @@ import numpy as np from pandapipes.constants import NORMAL_TEMPERATURE, NORMAL_PRESSURE -from pandapipes.idx_branch import TOUTINIT, TO_NODE -from pandapipes.idx_node import TINIT, PINIT, PAMB +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode from pandapipes.pf.internals_toolbox import get_from_nodes_corrected, get_to_nodes_corrected def calculate_mixture_viscosity(components_viscosities, components_molar_proportions, components_molar_mass): - """ - Todo: Fill out parameters. + """Todo: Fill out parameters. :param components_viscosities: :type components_viscosities: @@ -45,8 +44,7 @@ def calculate_mixture_viscosity(components_viscosities, components_molar_proport def calculate_mixture_density(components_density, components_mass_proportions): - """ - Todo: Fill out parameters. + """Todo: Fill out parameters. :param components_density: :type components_density: @@ -72,8 +70,7 @@ def calculate_mixture_density(components_density, components_mass_proportions): def calculate_mixture_heat_capacity(components_capacity, components_mass_proportions): - """ - Todo: Fill out parameters. + """Todo: Fill out parameters. :param components_capacity: :type components_capacity: @@ -100,8 +97,7 @@ def calculate_mixture_heat_capacity(components_capacity, components_mass_proport def calculate_mixture_molar_mass(components_molar_mass, components_molar_proportions=None, components_mass_proportions=None): - """ - Todo: Fill out parameters. + """Todo: Fill out parameters. :param components_molar_mass: :type components_molar_mass: @@ -131,8 +127,7 @@ def calculate_mixture_molar_mass(components_molar_mass, components_molar_proport def calculate_mass_fraction_from_molar_fraction(component_molar_proportions, component_molar_mass): - """ - Todo: Fill out parameters. + """Todo: Fill out parameters. :param component_molar_proportions: :type component_molar_proportions: @@ -151,12 +146,12 @@ def calculate_mass_fraction_from_molar_fraction(component_molar_proportions, com def get_branch_real_density(fluid, node_pit, branch_pit): from_nodes = get_from_nodes_corrected(branch_pit) - t_from = node_pit[from_nodes, TINIT] - t_to = branch_pit[:, TOUTINIT] + t_from = node_pit[from_nodes, IdxNode.TINIT] + t_to = branch_pit[:, IdxBranch.TOUTINIT] if fluid.is_gas: - from_p = node_pit[from_nodes, PINIT] + node_pit[from_nodes, PAMB] + from_p = node_pit[from_nodes, IdxNode.PINIT] + node_pit[from_nodes, IdxNode.PAMB] to_nodes = get_to_nodes_corrected(branch_pit) - to_p = node_pit[to_nodes, PINIT] + node_pit[to_nodes, PAMB] + to_p = node_pit[to_nodes, IdxNode.PINIT] + node_pit[to_nodes, IdxNode.PAMB] normal_rho = fluid.get_density(NORMAL_TEMPERATURE) from_rho = np.divide(normal_rho * NORMAL_TEMPERATURE * from_p, t_from * NORMAL_PRESSURE * fluid.get_compressibility(from_p, t_from)) @@ -170,16 +165,16 @@ def get_branch_real_density(fluid, node_pit, branch_pit): def get_branch_real_eta(fluid, node_pit, branch_pit, pm): from_nodes = get_from_nodes_corrected(branch_pit) - t_from = node_pit[from_nodes, TINIT] - t_to = branch_pit[:, TOUTINIT] + t_from = node_pit[from_nodes, IdxNode.TINIT] + t_to = branch_pit[:, IdxBranch.TOUTINIT] tm = (t_from + t_to) / 2 eta = fluid.get_viscosity(tm, p_bar=pm) return eta def get_branch_cp(fluid, node_pit, branch_pit): from_nodes = get_from_nodes_corrected(branch_pit) - t_from = node_pit[from_nodes, TINIT] - t_to = branch_pit[:, TOUTINIT] + t_from = node_pit[from_nodes, IdxNode.TINIT] + t_to = branch_pit[:, IdxBranch.TOUTINIT] cp_from = fluid.get_heat_capacity(t_from) cp_to = fluid.get_heat_capacity(t_to) cp = (cp_from + cp_to) / 2 diff --git a/src/pandapipes/std_types/std_type_class.py b/src/pandapipes/std_types/std_type_class.py index 40f4c45ee..17989be3b 100644 --- a/src/pandapipes/std_types/std_type_class.py +++ b/src/pandapipes/std_types/std_type_class.py @@ -15,12 +15,10 @@ class StdType(JSONSerializableClass): - """ - - """ + """Base class for a standard type object.""" def __init__(self, name, component, sector=Sector.ALL): - """ + """Initialize a standard type object. :param name: name of the standard type object :type name: str @@ -46,8 +44,7 @@ def from_dict(cls, d): class InterpolationStdType(StdType): def __init__(self, name, component, int_fct, sector=Sector.ALL): - """ - The interpolation standrad type object interpolates and extrapolates between the given values + """The interpolation standrad type object interpolates and extrapolates between the given values. :param name: Name of the interpolation standard type object :type name: str @@ -107,8 +104,7 @@ def load_data(cls, path): class RegressionStdType(StdType): def __init__(self, name, component, reg_par, sector=Sector.ALL): - """ - The regression standrad type object creates a regression based on the given data and regression parameters + """The regression standrad type object creates a regression based on the given data and regression parameters. :param name: Name of the regression object :type name: str @@ -121,7 +117,6 @@ def __init__(self, name, component, reg_par, sector=Sector.ALL): :return: An object of the regression standard type class :rtype: RegressionStdType """ - super(RegressionStdType, self).__init__(name, component, sector) self.reg_par = reg_par self._x_values = None @@ -169,8 +164,9 @@ def load_data(cls, path): class PumpStdType(RegressionStdType): def __init__(self, name, reg_par, sector=Sector.ALL): - """ - Creates a concrete pump std type. The class is a child class of the RegressionStdType, therefore, the here + """Create a concrete pump std type. + + The class is a child class of the RegressionStdType, therefore, the here derived values are calculated based on a previously performed regression. The regression parameters need to be passed or alternatively, can be determined through the here defined class methods. @@ -185,8 +181,7 @@ def __init__(self, name, reg_par, sector=Sector.ALL): super(PumpStdType, self).__init__(name, 'pump', reg_par, sector) def get_pressure(self, vdot_m3_per_s): - """ - Calculate the pressure lift based on a polynomial from a regression. + """Calculate the pressure lift based on a polynomial from a regression. It is ensured that the pressure lift is always >= 0. For reverse flows, bypassing is assumed. @@ -233,8 +228,7 @@ def from_list(cls, name, x_values, y_values, degree, sector=Sector.ALL): @classmethod def load_data(cls, path): - """ - load_data. + """load_data. :param path: :type path: @@ -247,8 +241,7 @@ def load_data(cls, path): def regression_function(x_values, y_values, degree): - """ - Regression function: performs a regression based on the given x-, y-values and the polynominal degree. + """Regression function: performs a regression based on the given x-, y-values and the polynominal degree. :param x_values: given data on x-axis :type x_values: array_like @@ -266,8 +259,7 @@ def regression_function(x_values, y_values, degree): def interpolation_function(x_values, y_values, fill_value='extrapolate'): - """ - interpolation function: performs an interpolation based on the given x- and y-values. + """interpolation function: performs an interpolation based on the given x- and y-values. :param x_values: given data on x-axis :type x_values: array_like @@ -286,8 +278,7 @@ def _retrieve_data(loaded_data): return data_list def get_data(path, std_type_category): - """ - retrieve data + """retrieve data. :param path: path the data can be retrieved from :type path: str diff --git a/src/pandapipes/std_types/std_types.py b/src/pandapipes/std_types/std_types.py index 6c2b32d2b..c14493c6e 100644 --- a/src/pandapipes/std_types/std_types.py +++ b/src/pandapipes/std_types/std_types.py @@ -21,8 +21,7 @@ def create_std_type(net, component, std_type_name, typedata, overwrite=False, check_required=True): - """ - Create a new standard type for a specific component with the given data. + """Create a new standard type for a specific component with the given data. :param net: The pandapipes network :type net: pandapipesNet @@ -65,8 +64,7 @@ def create_std_type(net, component, std_type_name, typedata, overwrite=False, ch def create_std_types(net, component, type_dict, overwrite=False): - """ - Create several new standard types for a specific component with the given data. + """Create several new standard types for a specific component with the given data. :param net: The pandapipes network :type net: pandapipesNet @@ -82,8 +80,7 @@ def create_std_types(net, component, type_dict, overwrite=False): def copy_std_types(to_net, from_net, component, overwrite=False): - """ - Transfers all standard types of one network to another. + """Transfers all standard types of one network to another. :param to_net: The pandapipes network to which the standard types are copied :type to_net: pandapipesNet @@ -106,9 +103,7 @@ def copy_std_types(to_net, from_net, component, overwrite=False): def load_std_type(net, name, component): - """ - Loads standard type data from the data base. Issues a warning if - stdtype is unknown. + """Loads standard type data from the data base. Issues a warning if stdtype is unknown. :param net: The pandapipes network :type net: pandapipesNet @@ -128,8 +123,7 @@ def load_std_type(net, name, component): def std_type_exists(net, name, component): - """ - Checks if a standard type exists. + """Checks if a standard type exists. :param net: The pandapipes network :type net: pandapipesNet @@ -145,8 +139,7 @@ def std_type_exists(net, name, component): def delete_std_type(net, name, component): - """ - Deletes standard type parameters from database. + """Deletes standard type parameters from database. :param net: pandapipes Network :type net: pandapipesNet @@ -163,8 +156,7 @@ def delete_std_type(net, name, component): def available_std_types(net, component): - """ - Returns all standard types available for this network as a table. + """Returns all standard types available for this network as a table. :param net: pandapipes Network :type net: pandapipesNet @@ -186,9 +178,7 @@ def available_std_types(net, component): def change_std_type(net, cid, name, component): - """ - Changes the type of a given component in pandapower. Changes only parameter that are given - for the type. + """Changes the type of a given component in pandapower. Changes only parameter that are given for the type. :param net: pandapipes network :type net: pandapipesNet @@ -210,8 +200,7 @@ def change_std_type(net, cid, name, component): def create_pump_std_type(net, name, pump_object, overwrite=False): - """ - Create a new pump standard type object and add it to the pump standard types in net. + """Create a new pump standard type object and add it to the pump standard types in net. :param net: The pandapipes network to which the standard type is added. :type net: pandapipesNet @@ -231,11 +220,10 @@ def create_pump_std_type(net, name, pump_object, overwrite=False): def add_basic_std_types(net): - """ + """Add basic standard types to a pandapipes network. :param net: pandapipes network in which the standard types should be added :type net: pandapipesNet - """ pump_files = os.listdir(os.path.join(pp_dir, "std_types", "library", "Pump")) for pump_file in pump_files: diff --git a/src/pandapipes/test/api/test_components/test_circ_pump_pressure.py b/src/pandapipes/test/api/test_components/test_circ_pump_pressure.py index 43cc4d6a9..28e6e9333 100644 --- a/src/pandapipes/test/api/test_components/test_circ_pump_pressure.py +++ b/src/pandapipes/test/api/test_components/test_circ_pump_pressure.py @@ -60,3 +60,56 @@ def test_circulation_pump_constant_pressure(use_numba): assert np.all(v_diff < 0.01) assert np.all(mdot_diff < 0.01) assert np.all(deltap_diff < 0.01) + + +def _build_circ_pump_with_colocated_ext_grid(n_ext_grids): + """A circ pump loop (j1..j4) plus an extra branch off the pump's own flow junction (j1) with + a sink demand (1.5 kg/s) that only a real ext_grid can supply - forces MDOTSLACKINIT at j1 to + be genuinely nonzero, so a diluted/wrong split is actually observable (with only the loop's + own sink/source, which cancel out exactly, MDOTSLACKINIT stays at 0 regardless of any + dilution and the bug is invisible). ``n_ext_grids`` real ext_grids are co-located directly at + j1, the pump's own NODE_TYPE=P anchor junction.""" + net = pandapipes.create_empty_network("net", add_stdtypes=False) + j1 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j2 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j3 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j4 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j5 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + pandapipes.create_pipe_from_parameters(net, j1, j2, k_mm=1., length_km=0.4338, inner_diameter_mm=102.2) + pandapipes.create_pipe_from_parameters(net, j3, j4, k_mm=1., length_km=0.2637, inner_diameter_mm=102.2) + pandapipes.create_pipe_from_parameters(net, j1, j5, k_mm=1., length_km=0.3, inner_diameter_mm=80) + pandapipes.create_circ_pump_const_pressure(net, j4, j1, 5, 2, 300, type='pt') + pandapipes.create_heat_exchanger(net, j2, j3, qext_w=200000, inner_diameter_mm=100) + pandapipes.create_sink(net, j1, 2) + pandapipes.create_source(net, j4, 2) + pandapipes.create_sink(net, j5, 1.5) + for _ in range(n_ext_grids): + pandapipes.create_ext_grid(net, j1, p_bar=5, t_k=300, type="p") + pandapipes.create_fluid_from_lib(net, "water", overwrite=True) + return net + + +@pytest.mark.parametrize("n_ext_grids", [1, 2]) +def test_circulation_pump_colocated_ext_grid_reports_full_share(n_ext_grids): + """Regression test: register_circ_pump_slack_equations used to add its own +1 + Jacobian coefficient to the MDOTSLACKINIT node-balance row/column even at nodes where a real + ext_grid is also present (COUNT_VAR_MASS_SLACK != 0) - despite its own docstring saying this case + should be skipped. Since ExtGrid.extract_results reads MDOTSLACKINIT directly as "this + ext_grid's own share" of an N-way split among however many components register a +1 there, + the circ pump's extra, unneeded registration silently diluted that split to N+1, understating + every co-located ext_grid's reported mdot_kg_per_s - without corrupting the rest of the + hydraulic solution (pressures/branch flows elsewhere are unaffected either way, since Newton + still balances the AGGREGATE mass injected at that node regardless of how many entities share + credit for it - only the per-instance split was wrong).""" + required_total_mdot = 1.5 # the sink at j5 - the only demand a real ext_grid can supply here + net = _build_circ_pump_with_colocated_ext_grid(n_ext_grids) + + pandapipes.pipeflow(net, max_iter_hyd=25, stop_condition="tol", friction_model="nikuradse", + mode='hydraulics', nonlinear_method="automatic", tol_p=1e-8, tol_m=1e-8) + + assert net.converged + # each of the n_ext_grids splits the required total evenly - the circ pump's own anchor node + # must not count as an extra (n_ext_grids + 1)-th sharer + expected_each = required_total_mdot / n_ext_grids + assert np.allclose(-net.res_ext_grid.mdot_kg_per_s.values, expected_each, atol=1e-6) + assert np.isclose(-net.res_ext_grid.mdot_kg_per_s.sum(), required_total_mdot, atol=1e-6) diff --git a/src/pandapipes/test/api/test_components/test_compressor.py b/src/pandapipes/test/api/test_components/test_compressor.py index 342f552b1..504315fcc 100644 --- a/src/pandapipes/test/api/test_components/test_compressor.py +++ b/src/pandapipes/test/api/test_components/test_compressor.py @@ -64,5 +64,53 @@ def test_compressor_pressure_ratio(use_numba): "pressure lift on rev. flow should be 0" +@pytest.mark.parametrize("use_numba", [True, False]) +def test_compressor_pressure_ratio_after_index_gap(use_numba): + """ + Compressor._compute_pl (inherited call site in Pump.register_hydraulic_equations) + looks up get_component_array(net, "compressor")[tbl_idx, PRESSURE_RATIO], where + tbl_idx comes from IdxBranch.ELEMENT_IDX. That array is built positionally (row i = + i-th row of net.compressor), but ELEMENT_IDX stores the pandas *index label* of the + compressor, not its position - so once net.compressor's index isn't 0..n-1 anymore, + the lookup goes out of bounds (or, if still in bounds, silently applies a different + compressor's pressure_ratio). + + A non-contiguous index arises from perfectly ordinary usage: dropping a compressor + and adding a replacement, since pandas keeps counting new row labels upward instead + of reusing the freed one. + """ + net = pandapipes.create_empty_network("net", fluid="hgas") + + j1, j2, j3, j4, j5, j6 = pandapipes.create_junctions(net, 6, pn_bar=5, tfluid_k=283.15) + pandapipes.create_pipe_from_parameters(net, j1, j2, length_km=0.1, inner_diameter_mm=102.2) + pandapipes.create_pipe_from_parameters(net, j5, j6, length_km=0.1, inner_diameter_mm=102.2) + pandapipes.create_ext_grid(net, j1, 5, 283.15, type="p") + pandapipes.create_sink(net, j6, 0.02333) + + r0, r1, r2 = 1.2, 1.5, 1.1 + c0 = pandapipes.create_compressor(net, j2, j3, pressure_ratio=r0) + c1 = pandapipes.create_compressor(net, j3, j4, pressure_ratio=r1) + c2 = pandapipes.create_compressor(net, j4, j5, pressure_ratio=r2) + + # same 3 compressors/topology/ratios as above, just re-labeled: drop the middle + # compressor and add an equivalent replacement -> index becomes [0, 2, 3] instead of + # [0, 1, 2] + net.compressor.drop(index=[c1], inplace=True) + c1 = pandapipes.create_compressor(net, j3, j4, pressure_ratio=r1) + assert net.compressor.index.tolist() == [c0, c2, c1] + + max_iter_hyd = 5 if use_numba else 5 + pandapipes.pipeflow(net, max_iter_hyd=max_iter_hyd, use_numba=use_numba) + net.res_junction["abs_p_bar"] = net.res_junction.p_bar + \ + p_correction_height_air(net.junction.height_m) + + assert np.isclose(net.res_compressor.at[c0, "deltap_bar"], + net.res_junction.at[j2, "abs_p_bar"] * (r0 - 1)) + assert np.isclose(net.res_compressor.at[c1, "deltap_bar"], + net.res_junction.at[j3, "abs_p_bar"] * (r1 - 1)) + assert np.isclose(net.res_compressor.at[c2, "deltap_bar"], + net.res_junction.at[j4, "abs_p_bar"] * (r2 - 1)) + + if __name__ == '__main__': n = pytest.main(["test_compressor.py"]) \ No newline at end of file diff --git a/src/pandapipes/test/api/test_components/test_ext_grid.py b/src/pandapipes/test/api/test_components/test_ext_grid.py index 1ffd7dd97..789508a17 100644 --- a/src/pandapipes/test/api/test_components/test_ext_grid.py +++ b/src/pandapipes/test/api/test_components/test_ext_grid.py @@ -45,6 +45,26 @@ def test_ext_grid_sorting(use_numba): assert np.isclose(net.res_ext_grid.at[4, "mdot_kg_per_s"], -0.05, atol=1e-12, rtol=1e-12) +@pytest.mark.parametrize("n_ext_grids", [2, 3, 5]) +@pytest.mark.parametrize("use_numba", [True, False]) +def test_multiple_ext_grids_single_junction_split_evenly(n_ext_grids, use_numba): + """N ext_grids directly on the SAME junction (no pipe in between at all) are indistinguishable + to the pressure-based Newton system - all see the same pressure, so the node's mass balance + (one equation) can't attribute the sink's demand to any one of them over another. Confirms the + actual, otherwise-undocumented tie-breaking behavior: the demand is split evenly, N-way, + regardless of ext_grid creation order or count.""" + net = pandapipes.create_empty_network(fluid="water") + j0 = pandapipes.create_junction(net, 5, 285.15) + for _ in range(n_ext_grids): + pandapipes.create_ext_grid(net, j0, p_bar=5, t_k=285.15, type="pt") + pandapipes.create_sink(net, j0, mdot_kg_per_s=5.0) + + pandapipes.pipeflow(net, use_numba=use_numba) + + assert np.allclose(net.res_ext_grid.mdot_kg_per_s.values, -5.0 / n_ext_grids) + assert np.isclose(net.res_ext_grid.mdot_kg_per_s.sum(), -5.0) + + @pytest.mark.parametrize("use_numba", [True, False]) def test_p_type(use_numba): """ diff --git a/src/pandapipes/test/api/test_components/test_flow_control.py b/src/pandapipes/test/api/test_components/test_flow_control.py index 1620d05d5..b27b8ee55 100644 --- a/src/pandapipes/test/api/test_components/test_flow_control.py +++ b/src/pandapipes/test/api/test_components/test_flow_control.py @@ -92,3 +92,42 @@ def test_flow_control_simple_gas_two_eg(use_numba): assert np.allclose(net.res_pipe.loc[[p12, p34], "mdot_from_kg_per_s"], [0.05, -0.01]) assert np.allclose(net.res_flow_control["mdot_from_kg_per_s"].values, [0.03]) assert np.allclose(net.res_ext_grid["mdot_kg_per_s"].values, [-0.05, -0.01]) + + +@pytest.mark.parametrize("use_numba", [True, False]) +def test_flow_control_after_index_gap(use_numba): + """ + FlowControlComponent.register_hydraulic_equations used to look up control_active / + controlled_mdot_kg_per_s via net[table_name].values[tbl_idx], where tbl_idx came from + IdxBranch.ELEMENT_IDX - the pandas *index label* of the flow control, not its position + in net.flow_control. .values is positional, so as soon as the table's index isn't + 0..n-1 anymore (e.g. after dropping a flow control and adding a replacement, since + pandas keeps counting new row labels upward instead of reusing the freed one), the + lookup goes out of bounds or silently picks up a different flow control's set point. + """ + net = pandapipes.create_empty_network("net", add_stdtypes=True, fluid="hgas") + + j = pandapipes.create_junctions(net, 8, pn_bar=1, tfluid_k=298) + j1, j2, j3, j4, j5, j6, j7, j8 = j + + p12, p23, p34, p45, p56, p58, p67 = pandapipes.create_pipes_from_parameters( + net, [j1, j2, j3, j4, j5, j5, j6], [j2, j3, j4, j5, j6, j8, j7], 0.2, 100, k_mm=0.1) + + fcs = pandapipes.create_flow_controls(net, [j2, j3, j4], [j6, j5, j8], [0.03, 0.02, 0.03], 0.1) + + pandapipes.create_ext_grid(net, j1, p_bar=1, t_k=298) + pandapipes.create_sinks(net, [j3, j4, j5, j7, j8], [0.02, 0.04, 0.03, 0.04, 0.02]) + + # same 3 flow controls/topology/set points as above, just re-labeled: drop the first + # flow control and add an equivalent replacement -> index becomes [1, 2, 3] instead of + # [0, 1, 2] + net.flow_control.drop(index=[fcs[0]], inplace=True) + pandapipes.create_flow_control(net, j2, j6, controlled_mdot_kg_per_s=0.03, diameter_m=0.1) + assert net.flow_control.index.tolist() == [1, 2, 3] + + max_iter_hyd = 4 if use_numba else 4 + pandapipes.pipeflow(net, max_iter_hyd=max_iter_hyd, mode="hydraulics", use_numba=use_numba) + + assert np.allclose(net.res_pipe.loc[[p12, p23, p34, p45, p56, p58, p67], "mdot_from_kg_per_s"], + [0.15, 0.12, 0.08, 0.01, 0.01, -0.01, 0.04]) + assert np.allclose(net.res_flow_control["mdot_from_kg_per_s"].values, [0.02, 0.03, 0.03]) diff --git a/src/pandapipes/test/api/test_components/test_heat_consumer.py b/src/pandapipes/test/api/test_components/test_heat_consumer.py index f6f4aefb6..e28eaf0a1 100644 --- a/src/pandapipes/test/api/test_components/test_heat_consumer.py +++ b/src/pandapipes/test/api/test_components/test_heat_consumer.py @@ -78,7 +78,7 @@ def test_heat_consumer_equivalence2(use_numba): pandapipes.create_heat_consumer(net, juncs[1], juncs[4], controlled_mdot_kg_per_s=mdot[0], qext_w=qext[0]) pandapipes.create_heat_consumer(net, juncs[2], juncs[3], controlled_mdot_kg_per_s=mdot[1], qext_w=qext[1]) - pandapipes.pipeflow(net, mode="bidirectional", iter=7, use_numba=use_numba) + pandapipes.pipeflow(net, mode="bidirectional", iter=5, use_numba=use_numba) tout1 = net.res_heat_consumer.t_outlet_k.iloc[1] dt1 = net.res_heat_consumer.deltat_k.iloc[1] @@ -88,15 +88,15 @@ def test_heat_consumer_equivalence2(use_numba): pandapipes.create_heat_consumer(net3, juncs[1], juncs[4], controlled_mdot_kg_per_s=mdot[0], qext_w=qext[0]) pandapipes.create_heat_consumer(net3, juncs[2], juncs[3], deltat_k=dt1, qext_w=qext[1]) - pandapipes.pipeflow(net3, mode="bidirectional", iter=7, use_numba=use_numba) + pandapipes.pipeflow(net3, mode="bidirectional", iter=5, use_numba=use_numba) pandapipes.create_heat_consumer(net4, juncs[1], juncs[4], controlled_mdot_kg_per_s=mdot[0], qext_w=qext[0]) pandapipes.create_heat_consumer(net4, juncs[2], juncs[3], controlled_mdot_kg_per_s=mdot[1], treturn_k=tout1) - pandapipes.pipeflow(net4, mode="bidirectional", iter=9, use_numba=use_numba) + pandapipes.pipeflow(net4, mode="bidirectional", iter=5, use_numba=use_numba) pandapipes.create_heat_consumer(net5, juncs[1], juncs[4], controlled_mdot_kg_per_s=mdot[0], qext_w=qext[0]) pandapipes.create_heat_consumer(net5, juncs[2], juncs[3], controlled_mdot_kg_per_s=mdot[1], deltat_k=dt1) - pandapipes.pipeflow(net5, mode="bidirectional", iter=7, use_numba=use_numba) + pandapipes.pipeflow(net5, mode="bidirectional", iter=5, use_numba=use_numba) assert np.allclose(net2.res_junction, net.res_junction) assert np.allclose(net2.res_pipe, net.res_pipe) @@ -188,6 +188,60 @@ def test_heat_consumer_qext_zero(): assert net.res_junction.at[juncs[4], 't_k'] != 263.4459264973806 + +def test_heat_consumer_qe_tr_degenerate_ignores_stale_mdot(): + """Regression test: HeatConsumer.register_hydraulic_equations's QE_TR branch used to compute + the branch's own load (and, via an unrelated numpy view-aliasing accident in the node-balance + load construction a few lines further down, the pit's real MDOTINIT too) straight from + whatever mass flow happened to already be sitting in the pit for a degenerate row (t_out >= + t_in, or qext_w == 0 - no valid mdot = qext/(cp*(t_in-t_out)) exists there). A prior working + version reset MDOTINIT to 0 for exactly these rows before using it. + + A plain end-to-end pipeflow() can't exercise this: every hydraulics-mode run starts from a + fresh, zero-filled pit (create_empty_pit()), and qext_w == 0 is a static, per-row property + that's already degenerate on iteration 0 - MDOTINIT never gets the chance to become nonzero + before the degenerate branch first runs. This test pokes a stale mass flow into the pit + directly instead, to stand in for what a still-converging Newton iteration (or the t_out >= + t_in half of the same condition, which - unlike qext_w == 0 - genuinely can flip mid-solve as + temperatures evolve) would otherwise have already accumulated in that pit slot by the time + this row goes degenerate.""" + from pandapipes.idx_branch import IdxBranch + from pandapipes.pf.pipeflow_setup import get_lookup + from pandapipes.pf.system_index import ComponentRegistry, HydraulicSystemIndex, HydVarEq + from pandapipes.component_models.heat_consumer_component import HeatConsumer + + net = pandapipes.create_empty_network("net", add_stdtypes=False, fluid="water") + juncs = pandapipes.create_junctions(net, 6, pn_bar=5, tfluid_k=286, system=["flow"] * 3 + ["return"] * 3) + pandapipes.create_pipes_from_parameters(net, juncs[[0, 1, 3, 4]], juncs[[1, 2, 4, 5]], k_mm=0.1, length_km=1, + inner_diameter_mm=102.2, system=["flow"] * 2 + ["return"] * 2, u_w_per_m2k=10, + text_k=273.15) + pandapipes.create_circ_pump_const_pressure(net, juncs[-1], juncs[0], 5, 2, 300, type='pt') + pandapipes.create_heat_consumer(net, juncs[1], juncs[4], treturn_k=263.4459264973806, qext_w=0) + pandapipes.create_heat_consumer(net, juncs[2], juncs[3], controlled_mdot_kg_per_s=1, qext_w=7500) + + # one plain hydraulics run just to populate the lookups/_active_pit structures this internal + # API needs - the degenerate consumer converges to MDOTINIT == 0 here, same as always + pandapipes.pipeflow(net, mode="hydraulics", max_iter_hyd=10) + + f, _ = get_lookup(net, "branch", "from_to_active_hydraulics")[HeatConsumer.table_name()] + branch_pit = net["_active_pit"]["branch"] + node_pit = net["_active_pit"]["node"] + assert branch_pit[f, IdxBranch.QEXT] == 0 # confirms row f is the degenerate (qext_w=0) consumer + + stale_mdot = 5.0 + branch_pit[f, IdxBranch.MDOTINIT] = stale_mdot + + sys_idx = HydraulicSystemIndex(node_pit, branch_pit) + registry = ComponentRegistry() + HeatConsumer.register_hydraulic_equations(net, branch_pit, node_pit, sys_idx, registry) + + branch_eq_row = sys_idx.idx(HydVarEq.BRANCH, np.array([f], dtype=np.int32))[0] + load = next(eq.load_data[eq.load_rows == branch_eq_row][0] + for eq in registry.normal if np.any(eq.load_rows == branch_eq_row)) + + assert load == 0.0 + assert branch_pit[f, IdxBranch.MDOTINIT] == 0.0 + def test_heat_consumer_result_extraction(): net = pandapipes.create_empty_network("net", add_stdtypes=False, fluid="water") diff --git a/src/pandapipes/test/api/test_components/test_pressure_control.py b/src/pandapipes/test/api/test_components/test_pressure_control.py index eab7cf1a9..1e4cd2e79 100644 --- a/src/pandapipes/test/api/test_components/test_pressure_control.py +++ b/src/pandapipes/test/api/test_components/test_pressure_control.py @@ -96,3 +96,46 @@ def test_non_working_distance_control(): with pytest.raises(UserWarning) as e: pandapipes.pipeflow(net) assert "The following controlled junction(s) were identified as disconnected" in str(e.value) + + +@pytest.mark.parametrize("use_numba", [True, False]) +def test_pressure_control_after_index_gap(use_numba): + """ + PressureControlComponent.register_hydraulic_equations used to look up + control_active/in_service/controlled_junction/controlled_p_bar via + net[table_name].values[tbl_idx], where tbl_idx came from IdxBranch.ELEMENT_IDX - the + pandas *index label* of the press_control row, not its position in net.press_control. + .values is positional, so as soon as the table's index isn't 0..n-1 anymore (e.g. + after dropping a press_control and adding a replacement, since pandas keeps counting + new row labels upward instead of reusing the freed one), the lookup goes out of + bounds or silently picks up a different press_control's set point. + """ + net = pandapipes.create_empty_network("net", add_stdtypes=False, fluid="lgas") + + j0, j1, j2, j3, j4 = [pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + for _ in range(5)] + + pandapipes.create_ext_grid(net, j0, 32, 283.15, type="p") + + pc_a = pandapipes.create_pressure_control(net, j0, j1, j1, 20.) + pandapipes.create_pipe_from_parameters(net, j1, j2, k_mm=1., length_km=5., + inner_diameter_mm=102.2) + pandapipes.create_sink(net, j2, 0.5) + + pandapipes.create_pressure_control(net, j0, j3, j3, 15.) + pandapipes.create_pipe_from_parameters(net, j3, j4, k_mm=1., length_km=5., + inner_diameter_mm=102.2) + pandapipes.create_sink(net, j4, 0.3) + + # same 2 press_controls/topology/set points as above, just re-labeled: drop the first + # press_control and add an equivalent replacement -> index becomes [1, 2] instead of + # [0, 1] + net.press_control.drop(index=[pc_a], inplace=True) + pandapipes.create_pressure_control(net, j0, j1, j1, 20.) + assert net.press_control.index.tolist() == [1, 2] + + max_iter_hyd = 4 if use_numba else 4 + pandapipes.pipeflow(net, max_iter_hyd=max_iter_hyd, use_numba=use_numba) + + assert np.isclose(net.res_junction.at[j1, "p_bar"], 20.) + assert np.isclose(net.res_junction.at[j3, "p_bar"], 15.) diff --git a/src/pandapipes/test/api/test_components/test_pump.py b/src/pandapipes/test/api/test_components/test_pump.py index b2af40658..6c5a32165 100644 --- a/src/pandapipes/test/api/test_components/test_pump.py +++ b/src/pandapipes/test/api/test_components/test_pump.py @@ -200,7 +200,7 @@ def test_pump_bypass_high_vdot(use_numba): def test_compression_power(use_numba): # based on example by "oporras" from pandapipes.component_models import R_UNIVERSAL - from pandapipes.idx_node import PAMB + from pandapipes.idx_node import IdxNode height_asl_m = 2842 net = pandapipes.create_empty_network(fluid="methane") @@ -225,8 +225,8 @@ def test_compression_power(use_numba): use_numba=use_numba) # Local ambiental (atmospheric) pressure - p_amb_bar_j1 = net["_pit"]['node'][1][PAMB] - p_amb_bar_j2 = net["_pit"]['node'][2][PAMB] + p_amb_bar_j1 = net["_pit"]['node'][1][IdxNode.PAMB] + p_amb_bar_j2 = net["_pit"]['node'][2][IdxNode.PAMB] # Isentropic power for the compression R_spec = R_UNIVERSAL * 1e3 / pandapipes.get_fluid(net).get_molar_mass() @@ -241,5 +241,39 @@ def test_compression_power(use_numba): assert np.isclose(pow_pump_MW[0], net.res_pump.compr_power_mw[0]) +@pytest.mark.parametrize("use_numba", [True, False]) +def test_pump_std_type_after_index_gap(use_numba): + """ + Pump._compute_pl looks up the pump's characteristic curve via + get_component_array(net, "pump")[tbl_idx, cls.STD_TYPE], where tbl_idx comes from + IdxBranch.ELEMENT_IDX. That array is built positionally (row i = i-th row of + net.pump), but ELEMENT_IDX stores the pandas *index label* of the pump, not its + position - so as soon as net.pump's index isn't 0..n-1 anymore, the lookup goes out + of bounds (or, if still in bounds, silently picks up a different pump's data). + + A non-contiguous index arises from perfectly ordinary usage: dropping a pump and + adding a replacement, since pandas keeps counting new row labels upward instead of + reusing the freed one. + """ + net = pandapipes.create_empty_network("net", add_stdtypes=True, fluid="water") + + j1, j2, j3, j4, j5 = pandapipes.create_junctions(net, 5, pn_bar=5, tfluid_k=283.15) + pandapipes.create_pipe(net, j1, j2, std_type='125_PE_80_SDR_11', k_mm=1., length_km=0.1) + pandapipes.create_ext_grid(net, j1, 5, 283.15) + pandapipes.create_pump(net, j2, j3, std_type='P1') + pandapipes.create_pump(net, j3, j4, std_type='P1') + pandapipes.create_pump(net, j4, j5, std_type='P1') + pandapipes.create_sink(net, j5, 0.5) + + # same 3 pumps/topology/std_types as above, just re-labeled: drop the middle pump and + # add an equivalent replacement -> index becomes [0, 2, 3] instead of [0, 1, 2] + net.pump.drop(index=[1], inplace=True) + pandapipes.create_pump(net, j3, j4, std_type='P1') + assert net.pump.index.tolist() == [0, 2, 3] + + pandapipes.pipeflow(net, use_numba=use_numba) + assert net.converged + + if __name__ == '__main__': n = pytest.main(["test_pump.py"]) diff --git a/src/pandapipes/test/api/test_convert_format.py b/src/pandapipes/test/api/test_convert_format.py index ad8b78e0c..ed04db31b 100644 --- a/src/pandapipes/test/api/test_convert_format.py +++ b/src/pandapipes/test/api/test_convert_format.py @@ -27,7 +27,7 @@ def test_convert_format(pp_version, use_numba): if version.parse(pp_version) >= version.parse(minimal_version_two_nets): names = ["_gas", "_water"] net_gas = release_control_test_network_gas(max_iter_hyd=6) - net_water = release_control_test_network_water(max_iter_hyd=11) + net_water = release_control_test_network_water(max_iter_hyd=12) else: names = [""] net_old = release_control_test_network(max_iter_hyd=12) @@ -37,7 +37,7 @@ def test_convert_format(pp_version, use_numba): max_iter_hyd = 6 if use_numba else 6 elif "_water" in name: net_ref = net_water - max_iter_hyd = 11 if use_numba else 11 + max_iter_hyd = 12 if use_numba else 12 else: net_ref = net_old max_iter_hyd = 12 if use_numba else 12 diff --git a/src/pandapipes/test/api/test_time_series.py b/src/pandapipes/test/api/test_time_series.py index 2045ec107..162b502ee 100644 --- a/src/pandapipes/test/api/test_time_series.py +++ b/src/pandapipes/test/api/test_time_series.py @@ -6,8 +6,8 @@ def test_person_run_fct_time_series(): - def person_run_fct(net, sol_vec=None, **kwargs): - pps.pipeflow(net, sol_vec, **kwargs) + def person_run_fct(net, **kwargs): + pps.pipeflow(net, **kwargs) net.res_junction.p_bar.values[:] = 15. net = ntw.water_strand_net_2pumps() diff --git a/src/pandapipes/test/data/test_time_series_results/res_ext_grid/mdot_kg_per_s.csv b/src/pandapipes/test/data/test_time_series_results/res_ext_grid/mdot_kg_per_s.csv index 1b51b32e1..1d0135d6e 100644 --- a/src/pandapipes/test/data/test_time_series_results/res_ext_grid/mdot_kg_per_s.csv +++ b/src/pandapipes/test/data/test_time_series_results/res_ext_grid/mdot_kg_per_s.csv @@ -1,26 +1,26 @@ ;0;1 -0;0.0020807796185086775;-0.002563393101122524 -1;0.0025350323769641374;-0.003164959981473124 -2;0.002243709815219107;-0.0035322368740594613 -3;0.002500824133876919;-0.0021630189745438905 -4;0.0025610326889993706;-0.003319364162355515 -5;0.0018504938243137548;-0.002546801903639912 -6;0.002608224373985408;-0.003228736625115307 -7;0.0018581630308334184;-0.002730505954211182 -8;0.002378139424377208;-0.002227064525611204 -9;0.00204565872422087;-0.0029085878440074683 -10;0.0018557665096296996;-0.0018700646206081908 -11;0.0024019908752595893;-0.0028695007867939096 -12;0.0027426452235494534;-0.003158976459073568 -13;0.002787675098012614;-0.002146243976895879 -14;0.001974852086125202;-0.0028669611922561367 -15;0.0025780104285344782;-0.0023665954598064975 -16;0.0017747700490977508;-0.002495450055568615 -17;0.002200603534398156;-0.0023877390699619436 -18;0.0022919100811392493;-0.0025676203019219515 -19;0.0019621499076996553;-0.0037599529094508566 -20;0.0027208933894369183;-0.0029308021608315657 -21;0.0025066348665121075;-0.0024286151350168456 -22;0.001627087459428073;-0.002035768982552139 -23;0.0017875551508075127;-0.0035005980797811103 -24;0.0017923427935905452;-0.002637175594901118 +0;0.002080348439621015;-0.002562961922234862 +1;0.002534572226109927;-0.0031644998306189142 +2;0.0022432308219292123;-0.0035317578807695666 +3;0.0025003908588324204;-0.002162585699499392 +4;0.002560599490954188;-0.003318930964310332 +5;0.0018500426335101151;-0.002546350712836272 +6;0.002607755912047854;-0.003228268163177752 +7;0.0018577015906378717;-0.0027300445140156352 +8;0.002377707521570872;-0.0022266326228048672 +9;0.0020452027676236815;-0.0029081318874102794 +10;0.001855314638596678;-0.0018696127495751692 +11;0.002401531700311636;-0.0028690416118459564 +12;0.0027421828705003143;-0.0031585141060244287 +13;0.0027872295746058087;-0.0021457984534890753 +14;0.001974401018786066;-0.002866510124917001 +15;0.002577541272618732;-0.0023661263038907508 +16;0.001774290390697185;-0.0024949703971680493 +17;0.0022001568954281474;-0.0023872924309919353 +18;0.002291458532321177;-0.0025671687531038804 +19;0.001961696737075591;-0.003759499738826793 +20;0.0027204495586267744;-0.0029303583300214217 +21;0.0025061730925189742;-0.0024281533610237124 +22;0.0016266567179386436;-0.00203533824106271 +23;0.0017870878180704995;-0.003500130747044098 +24;0.0017918645128663258;-0.0026366973141768993 diff --git a/src/pandapipes/test/data/test_time_series_results/res_junction/p_bar.csv b/src/pandapipes/test/data/test_time_series_results/res_junction/p_bar.csv index 3a8d91bb6..b9b2482a1 100644 --- a/src/pandapipes/test/data/test_time_series_results/res_junction/p_bar.csv +++ b/src/pandapipes/test/data/test_time_series_results/res_junction/p_bar.csv @@ -1,26 +1,26 @@ ;0;1;2;3;4;5;6;7;8;9;10;11 -0;2.9959416224802924;9.004024534565954;3.0;3.0;3.0024450083484955;3.000120249176727;3.0000561746547136;3.000055667179997;2.999930178360433;3.0001119462422756;3.0001202491767267;3.000055667179997 -1;2.995647297472975;8.99639860895899;3.0;3.0;3.00235156929427;3.0000956814241126;3.0001767915363486;3.000176715016638;3.0001694351462813;3.0000637629878253;3.0000956814241126;3.000176715016638 -2;2.99562408481212;8.995070795308996;3.0;3.0;3.0022911259613547;3.000075645235512;3.0001047758663177;3.0001045494521987;3.0000643429252842;3.00001765808395;3.000075645235512;3.000104549452199 -3;2.9965594957188806;8.998205135673661;3.0;3.0;3.0024408824489512;3.0001147244025743;3.0001558594721334;3.0001557324202226;3.0001409127683325;3.000102147277339;3.0001147244025743;3.000155732420222 -4;2.995806956952378;8.996957969075888;3.0;3.0;3.002439717022559;3.000079969896599;3.000173593308014;3.0001734123852226;3.000149925645495;3.00001652337467;3.000079969896599;3.0001734123852226 -5;2.996696913148848;9.001770100519959;3.0;3.0;3.002373846213999;3.0000842248014563;3.000023185009791;3.0000225800426352;2.999877784141117;3.000034863525852;3.0000842248014563;3.0000225800426352 -6;2.9960676290884054;9.005090265034754;3.0;3.0;3.0023279379011876;3.000074249012338;3.0002026697387256;3.0002025000126635;3.000173885017965;3.0000128592372133;3.0000742490123375;3.000202500012663 -7;2.9958795152346895;8.998292309988367;3.0;3.0;3.0023412320064824;3.000112246028886;3.0000262467725447;3.0000258485311386;2.9999397327853283;3.00010322002908;3.000112246028886;3.000025848531139 -8;2.9963979871104427;8.995107695649331;3.0;3.0;3.0024437605813943;3.0001191272974665;3.000122310456227;3.0001221867468892;3.0001064936095942;3.000110220524197;3.0001191272974665;3.000122186746889 -9;2.9957629876470655;8.997642839440072;3.0;3.0;3.0023607593070407;3.000106333429599;3.0000561473267315;3.0000557480900554;2.9999674509703125;3.000088001241646;3.000106333429599;3.000055748090056 -10;2.996744663641049;9.001701384524265;3.0;3.0;3.0023728661399427;3.000124129746841;3.0000239901308823;3.0000236384127508;2.9999514522814956;3.0001233510181997;3.0001241297468413;3.0000236384127503 -11;2.9957821426600546;9.00127236176396;3.0;3.0;3.00235539980266;3.0001079274993248;3.0001382677079107;3.0001382197337922;3.000131808363351;3.0000911061121287;3.0001079274993248;3.0001382197337922 -12;2.995421148123939;9.001165038594872;3.0;3.0;3.0023470288881016;3.0001060878378283;3.000245081566126;3.000245044283126;3.0002449598229073;3.000092748171026;3.0001060878378283;3.000245044283126 -13;2.996457471097784;8.997482312405543;3.0;3.0;3.002401022025297;3.000121368597718;3.0002530147671727;3.0002529726142413;3.000249830603494;3.0001164937265297;3.000121368597718;3.0002529726142417 -14;2.9956734599560346;9.002659156095028;3.0;3.0;3.002376777527696;3.000113246926049;3.000042309907021;3.000042041946353;2.9999969865788896;3.0001030807053497;3.0001132469260487;3.000042041946353 -15;2.996651630080851;9.000803386657854;3.0;3.0;3.002325686741414;3.000097031137124;3.000193376631007;3.000193188506856;3.0001649293211408;3.000068649348824;3.0000970311371242;3.0001931885068562 -16;2.9968267007174707;9.001832541509225;3.0;3.0;3.0022856044335455;3.0000809783709546;3.0000179192085117;3.0000173139883817;2.9998729552793892;3.0000279706261197;3.000080978370955;3.0000173139883817 -17;2.9964344508091716;9.00242036796325;3.0;3.0;3.0023941914159984;3.0001067115929207;3.000084886828475;3.0000847875370824;3.0000699481137825;3.0000904031696707;3.0001067115929207;3.0000847875370824 -18;2.996126089888356;8.999018019244666;3.0;3.0;3.0023782936760086;3.0001106993481566;3.000107556100069;3.000107293719891;3.0000533886291985;3.0000978471756756;3.0001106993481566;3.000107293719891 -19;2.9952824278528696;8.993840948597624;3.0;3.0;3.002366205415858;3.0000764493760315;3.0000409132424792;3.0000405453181505;2.9999686161116546;3.000011244321435;3.0000764493760315;3.0000405453181505 -20;2.9957856792570534;8.998804116813307;3.0;3.0;3.002405990313864;3.0001038729234066;3.00022909669057;3.0002290603006085;3.0002252886739114;3.000080335253646;3.000103872923406;3.0002290603006085 -21;2.996540083780671;8.99935595617936;3.0;3.0;3.002347636866714;3.0000988279884866;3.0001688045879753;3.000168791313647;3.0001682961366405;3.0000723751190415;3.0000988279884866;3.0001687913136466 -22;2.9968855642903516;9.003876922051514;3.0;3.0;3.002386482010279;3.000105341379134;2.999996886418903;2.9999964795356533;2.999915827448578;3.0000878459943117;3.0001053413791343;2.9999964795356537 -23;2.99567724539853;8.994551451516305;3.0;3.0;3.0023197901827974;3.0000752701689275;3.000017695584457;3.0000173777159818;2.9999487267728133;3.0000052438926956;3.000075270168927;3.0000173777159813 -24;2.996230911249326;9.000093604352083;3.0;3.0;3.0022898165912024;3.000101143131245;3.000020033565325;3.0000195630461297;2.999914625886204;3.000084967719981;3.0001011431312454;3.0000195630461297 +0;2.9959416222972037;9.004024563223036;3.0;3.0;3.002445555024778;3.000120244491926;3.000056210217363;3.0000557027426504;2.999930213924199;3.000111941557465;3.0001202444919257;3.0000557027426504 +1;2.995647297299032;8.996398638671907;3.0;3.0;3.0023521414453818;3.0000956738013005;3.000176851490687;3.000176774970977;3.000169495100729;3.000063755364952;3.0000956738013005;3.0001767749709773 +2;2.9956240846424746;8.995070825359157;3.0;3.0;3.002291713601722;3.0000756346982653;3.000104825848452;3.000104599434337;3.0000643929079223;3.000017647546551;3.0000756346982658;3.000104599434337 +3;2.996559495516607;8.998205164911417;3.0;3.0;3.002441431279963;3.0001147190242405;3.000155912987427;3.000155785935518;3.0001409662838263;3.0001021418989873;3.00011471902424;3.000155785935518 +4;2.995806956768446;8.996957998368202;3.0;3.0;3.0024402651375484;3.0000799599658814;3.0001736493175053;3.000173468394717;3.000149981655316;3.000016513443795;3.0000799599658814;3.0001734683947165 +5;2.9966969129488072;9.001770129645788;3.0;3.0;3.002374409684358;3.0000842156200878;3.0000232132539124;3.000022608286762;2.999877812386262;3.0000348543443707;3.0000842156200878;3.000022608286762 +6;2.9960676289107924;9.005090294122727;3.0;3.0;3.002328517043855;3.000074238185046;3.0002027345004314;3.000202564774372;3.000173949780135;3.0000128484097557;3.0000742381850456;3.000202564774372 +7;2.9958795150566626;8.99829233952387;3.0;3.0;3.00234180471045;3.000112240764996;3.0000262766393426;3.0000258783979397;2.9999397626527697;3.0001032147651787;3.0001122407649956;3.00002587839794 +8;2.9963979869103308;8.99510772511848;3.0;3.0;3.002444308009262;3.000119122451349;3.0001223586108225;3.000122234901487;3.0001065417643806;3.0001102156780686;3.000119122451349;3.000122234901487 +9;2.9957629874702163;8.997642868968025;3.0;3.0;3.002361327472019;3.0001063272942745;3.0000561848855485;3.0000557856488754;2.9999674885299585;3.000087995106294;3.0001063272942745;3.000055785648876 +10;2.996744663439757;9.001701413680813;3.0;3.0;3.0023734306796164;3.0001241258460025;3.000024018688602;3.0000236669704727;2.9999514808397314;3.0001233471173605;3.000124125846003;3.0000236669704727 +11;2.9957821424855395;9.001272391050929;3.0;3.0;3.0023559713151657;3.0001079215346182;3.0001383215454274;3.0001382735713094;3.0001318622009543;3.0000911001473973;3.0001079215346182;3.0001382735713094 +12;2.995421147959044;9.001165067927337;3.0;3.0;3.0023476033240524;3.000106081630131;3.0002451512707284;3.0002451139877304;3.000245029527512;3.000092741963307;3.0001060816301304;3.00024511398773 +13;2.9964574709002187;8.997482341879081;3.0;3.0;3.0024015818107683;3.000121364144146;3.0002530829512684;3.000253040798338;3.000249898787644;3.000116489272952;3.000121364144146;3.0002530407983383 +14;2.9956734597834065;9.002659185118304;3.0;3.0;3.0023773415099098;3.0001132416409644;3.0000423436438304;3.0000420756831647;2.99999702031608;3.000103075420251;3.000113241640965;3.0000420756831643 +15;2.9966516298846173;9.000803416164022;3.0;3.0;3.002326266949122;3.000097023746348;3.0001934402032933;3.0001932520791446;3.000164992893877;3.000068641957996;3.000097023746348;3.0001932520791446 +16;2.9968267005191045;9.001832571006391;3.0;3.0;3.0022861923977304;3.0000809687772247;3.000017947370171;3.000017342150046;2.999872983442065;3.000027961032263;3.0000809687772247;3.0000173421500453 +17;2.9964344506156513;9.002420397000973;3.0;3.0;3.0023947518140512;3.000106705381898;3.00008492977963;3.0000848304882384;3.0000699910650965;3.0000903969586226;3.000106705381898;3.000084830488238 +18;2.9961260897019333;8.999018048632056;3.0;3.0;3.002378858395811;3.0001106936870103;3.000107603814729;3.0001073414345547;3.0000534363445035;3.000097841514511;3.0001106936870103;3.0001073414345547 +19;2.9952824276847076;8.993840978365146;3.0;3.0;3.0023667702514714;3.0000764390275094;3.000040946731321;3.0000405788069964;2.9999686496010995;3.000011233972745;3.0000764390275094;3.0000405788069964 +20;2.9957856790772706;8.99880414611355;3.0;3.0;3.00240654847847;3.0001038662684274;3.0002291616169514;3.0002291252269906;3.0002253536003543;3.000080328598628;3.000103866268428;3.00022912522699 +21;2.9965400835853027;8.999355985703732;3.0;3.0;3.0023482106048744;3.0000988208074872;3.00016886355509;3.0001688502807613;3.000168355103763;3.000072367937995;3.0000988208074872;3.0001688502807617 +22;2.99688556408614;9.003876950328744;3.0;3.0;3.002387022315399;3.0001053360224037;2.9999967534334213;2.9999963465501573;2.9999156944604097;3.0000878406375575;3.0001053360224033;2.9999963465501573 +23;2.995677245224716;8.994551481416291;3.0;3.0;3.0023203670515164;3.0000752596745754;3.000017722946078;3.000017405077605;2.999948754134905;3.00000523339816;3.000075259674575;3.000017405077605 +24;2.996230911066786;9.000093633965083;3.0;3.0;3.002290403708887;3.000101136538526;3.000020062411868;3.0000195918926766;2.999914654733505;3.0000849611272358;3.0001011365385266;3.0000195918926766 diff --git a/src/pandapipes/test/data/test_time_series_results/res_pipe/lambda.csv b/src/pandapipes/test/data/test_time_series_results/res_pipe/lambda.csv deleted file mode 100644 index 0e8b55191..000000000 --- a/src/pandapipes/test/data/test_time_series_results/res_pipe/lambda.csv +++ /dev/null @@ -1,26 +0,0 @@ -;0;1;2;3;4;5;6;7;8;9;10;11 -0;0.09229525756219129;0.08105544900833954;0.06814728600415161;0.16797121439793494;0.06823026590550574;0.30653050151420275;0.1778861492295314;0.09874692171225134;0.06019416300332284;0.08430604906319299;0.05438483050699441;0.04856042729275657 -1;0.09229738031827592;0.08168247367973232;0.06838010582695286;0.10497462467693475;0.07106803462076212;0.14187654871822433;0.8283554738963633;0.46541267929086727;0.05983592520650009;0.08601333439634354;0.05438483050699441;0.04856042729275657 -2;0.09229774538943965;0.08211063948118887;0.06852210901221789;0.12655535087461706;0.07447755933510736;0.11207873887073297;0.32318058925773463;0.15390962788659202;0.05980930205826568;0.08591476712493984;0.05438483050699441;0.04856042729275657 -3;0.09229688461990826;0.0810823427462502;0.06814272227664604;0.10938634705300923;0.06877963733951431;0.23299987691363644;0.5254040184502689;0.27531027826306465;0.06109968029901039;0.08510023414575575;0.05438483050699441;0.04856042729275657 -4;0.09229723360782145;0.08109001769356339;0.06803349000257804;0.1055860711750071;0.07361932073372963;0.10870397930741574;0.38836248836266085;0.20560201521186147;0.06002531980656678;0.08525130059881701;0.05438483050699441;0.04856042729275657 -5;0.09229587978934893;0.08152921226695699;0.06826474524096253;0.2848530455773745;0.07285008856588408;0.11875126818257811;0.15858437486517638;0.0947069947966738;0.06133701583590759;0.08459217107796928;0.05438483050699441;0.04856042729275657 -6;0.09229494665636849;0.08184760538433974;0.06838784667849591;0.10064910417899037;0.07477167731185949;0.1099125810135294;0.40971811580185996;0.1836691927460619;0.06036038449853817;0.08644879182444695;0.05438483050699441;0.04856042729275657 -7;0.09229684903110565;0.08175431406570777;0.0684739947114169;0.2621529727054471;0.06904113072609876;0.2893422765899417;0.2103747329386762;0.11174965995421998;0.06011520820870527;0.08486622360804967;0.05438483050699441;0.04856042729275657 -8;0.0922977522152892;0.08106363631111213;0.06814771736593986;0.11920575068078956;0.06833841858638007;0.29198857529885447;0.537822504210257;0.2649190086607673;0.060839303814165205;0.08481478711588267;0.05438483050699441;0.04856042729275657 -9;0.09229703300913837;0.08161893908247353;0.06838508588344085;0.16802026643283194;0.06970397275691897;0.18688270077972013;0.20999931042092954;0.11076211783487419;0.05997203684432409;0.08494681338778683;0.05438483050699441;0.04856042729275657 -10;0.09229589889368232;0.08153584783865761;0.06840473715860072;0.278334870021704;0.06786918829806594;2.3189281382759437;0.23018543833527816;0.11930237951514361;0.06142320094813117;0.08460572993529276;0.05438483050699441;0.04856042729275657 -11;0.09229601690651582;0.081655891626541;0.06840926055074248;0.11400462589451468;0.06951940923864311;0.19606062554029147;1.280840374910263;0.5155097616263185;0.05999514129388658;0.08565397160454162;0.05438483050699441;0.04856042729275657 -12;0.09229604598137643;0.08171394076790871;0.0684320777798264;0.09532592712953103;0.0697330335742318;0.2247044771682438;1.6284110315901452;31.60584391081722;0.05958569066825876;0.0866594611102253;0.05438483050699441;0.04856042729275657 -13;0.0922970824666477;0.08134556808085178;0.06829841735986053;0.09450405899877817;0.06812435896711164;0.4555393615495754;1.4482175099189678;0.9497712708497561;0.06093300501583873;0.08624596065214006;0.05438483050699441;0.04856042729275657 -14;0.09229563154822826;0.08150920282188374;0.06835360513399238;0.1956896622901122;0.06893440049334702;0.2670915500939726;0.28283744946399075;0.14577034264799335;0.05986620188208903;0.0847154610630674;0.05438483050699441;0.04856042729275657 -15;0.09229614473133282;0.08186346316775603;0.06847529580880386;0.102085171216071;0.07088169959691654;0.14968753098008022;0.3759588359654252;0.1849455802010213;0.0612570965429156;0.08638591274071633;0.05438483050699441;0.04856042729275657 -16;0.09229585243132156;0.08215055715238628;0.06856088607704973;0.34164130351480343;0.07343225953019042;0.1156928723490693;0.15854227360038453;0.09478800894807769;0.061576070426239374;0.085240794646089;0.05438483050699441;0.04856042729275657 -17;0.09229570025689605;0.08139136006408332;0.0682719076733806;0.1381697588333743;0.06965954057664868;0.19954415610357165;0.6537745260094324;0.2750651625793057;0.06089644825219562;0.08490972855707182;0.05438483050699441;0.04856042729275657 -18;0.09229665010411284;0.08149892607465752;0.06833966988307187;0.1252432425973114;0.06920867417378766;0.22989847573259667;0.2875182780257645;0.1344534076582418;0.06044039945389128;0.08521499375946182;0.05438483050699441;0.04856042729275657 -19;0.09229809837880658;0.08158160656275869;0.06826432686841474;0.19946588288352005;0.07431108330631284;0.10772461002537044;0.22272599393375872;0.1194661407375924;0.05944164073177789;0.08478606637966088;0.05438483050699441;0.04856042729275657 -20;0.09229671307039775;0.08131237998157287;0.06822264980914954;0.0971289523911462;0.06999798313024108;0.16388683632715578;1.6666907136985585;0.8079047681773823;0.05999942523516372;0.08599445406001104;0.05438483050699441;0.04856042729275657 -21;0.09229655210354466;0.08170971729886581;0.06840465954810271;0.10654410954136842;0.07063971509334234;0.15475958878284624;4.448432189899709;5.478382206286888;0.061067366400149016;0.08597849545326602;0.05438483050699441;0.04856042729275657 -22;0.0922952922658443;0.08144187827795143;0.06829300838546176;1.3887579216910535;0.06982011264706016;0.1917799860033874;0.2071855705592468;0.11443188239896196;0.0616896944689628;0.08430417705430411;0.05438483050699441;0.04856042729275657 -23;0.09229789409536121;0.08190527733502931;0.06841966143385383;0.3447601833099997;0.07455524088372217;0.10526461505120907;0.2481724815626195;0.12165575365995851;0.059870606526329626;0.08496709223174208;0.05438483050699441;0.04856042729275657 -24;0.09229633924427966;0.08211998234830638;0.06861841286374881;0.31533399679978713;0.07033916283433149;0.20048197100783932;0.18722943491368504;0.10445289342011109;0.06058877027410372;0.08522388581269638;0.05438483050699441;0.04856042729275657 diff --git a/src/pandapipes/test/data/test_time_series_results/res_pipe/reynolds.csv b/src/pandapipes/test/data/test_time_series_results/res_pipe/reynolds.csv deleted file mode 100644 index c801f7b0b..000000000 --- a/src/pandapipes/test/data/test_time_series_results/res_pipe/reynolds.csv +++ /dev/null @@ -1,26 +0,0 @@ -;0;1;2;3;4;5;6;7;8;9;10;11 -0;27567.988113369094;2399.6443875784944;4650.332930284129;563.4478166102137;3253.7125118310773;253.82153000822436;448.3792378277843;1398.7046530583937;6656.783409889754;2138.9503198138145;0.0;0.0 -1;27542.80361303581;2344.524748712249;4572.971844914731;1265.0772957291017;2843.4830529661267;731.4978069751216;80.68525590700976;155.1807156450741;6914.422508925791;2023.4912224713842;0.0;0.0 -2;27538.47702425015;2308.318579976058;4527.03821151667;886.7886731867306;2469.409033970988;1109.302555765574;222.1984942482238;634.1698979070915;6934.367884950754;2029.81695699236;0.0;0.0 -3;27548.68048815652;2397.227105898464;4651.875525084213;1163.6042789194175;3165.306649072709;358.31247863797034;130.54452587608446;287.8733635982171;6083.783018190526;2083.645090674801;0.0;0.0 -4;27544.542714019288;2396.538153912957;4689.105184337612;1249.9697109875535;2553.983484975258;1178.221702905322;181.19396149502245;419.3649211029028;6775.777991608025;2073.447330051368;0.0;0.0 -5;27560.601204381604;2357.7623010887182;4610.97933428753;277.6955597996702;2634.865891308473;994.3070070549716;518.4934445480535;1534.1579667268813;5949.5556591948825;2118.6903179916226;0.0;0.0 -6;27571.680585220503;2330.427288787741;4570.4439112375485;1383.3568522818848;2441.699650087516;1152.5768542067615;170.8633731102466;489.7500931411142;6543.649789426915;1996.010392052355;0.0;0.0 -7;27549.10251653925;2338.3707574425507;4542.497984366655;308.035675358109;3124.8926682766783;272.3897500035219;365.24489714311596;1089.1881704299853;6711.903339496056;2099.641567818773;0.0;0.0 -8;27538.396141613626;2398.907974738583;4650.187177728776;987.3355674125861;3235.9201220112705;269.35602406461965;127.31943441321633;301.9883447554015;6238.185321387227;2103.1906427795006;0.0;0.0 -9;27546.920959790707;2349.9943030125073;4571.345184722233;563.2045979193377;3026.9284831381688;483.02663181126013;366.02912116307783;1107.8065769553955;6814.218009321419;2094.1049620253107;0.0;0.0 -10;27560.374465648492;2357.1860758736257;4564.937669728867;285.7780250393585;3314.5575722026792;28.261769064179852;328.1452142475173;965.1333551742712;5902.2671446796285;2117.7397499980243;0.0;0.0 -11;27558.973919505712;2346.81003865038;4563.465310202066;1073.4689641923292;3053.5834309719803;451.73559806005346;51.377126119884664;138.3725557082557;6797.496300516178;2046.7463369187403;0.0;0.0 -12;27558.628888346044;2341.8252328372314;4556.052780802032;1563.2214395735134;3022.773821473088;375.7640486848256;40.16918262848084;2.0283426909323916;7106.546588371667;1982.9816423551329;0.0;0.0 -13;27546.3345662897;2373.8222971339605;4599.820353787231;1595.2450335157096;3271.3260844134384;159.53951668866142;45.291532114277956;71.36636711180888;6181.726314775441;2008.7171957944813;0.0;0.0 -14;27563.547779737637;2359.5016045723787;4581.64740350988;452.92152570005976;3141.262598605226;300.88377144018557;258.3899885925289;689.803518343511;6891.878986766828;2110.078123225351;0.0;0.0 -15;27557.457087830124;2329.0824106903033;4542.078535318582;1341.7094940754137;2867.2200727586865;671.5444544834704;187.7884672308676;485.0127997719324;5994.08839066483;1999.9323626798612;0.0;0.0 -16;27560.925908125595;2305.00000296667;4514.654988739598;222.79741629446298;2573.1920105055847;1043.9087283991128;518.6703535220909;1531.1843945317046;5820.213367566134;2074.1533050627263;0.0;0.0 -17;27562.732157731283;2369.7972693866814;4608.601164463368;763.8605328955023;3033.302828363887;440.89485630339055;103.45529507699679;288.19110454136586;6203.631328057287;2096.649099204368;0.0;0.0 -18;27551.46172242787;2360.3958996267556;4586.222619639327;903.2096276504872;3099.5367485069564;364.6440133912978;253.5974748302026;785.6317008969826;6490.550006974306;2075.8891049620997;0.0;0.0 -19;27534.294913518213;2353.2200974400193;4611.11832399736;441.13272513286034;2485.3735751938757;1199.8549783647197;341.1947456429173;962.7557783613875;7222.065510549118;2105.177574624258;0.0;0.0 -20;27550.714920184186;2376.748023819271;4625.006195162016;1497.2818993324372;2985.414964521896;584.4641796343825;39.226721694037806;84.77781613612709;6794.404838354339;2024.69984790098;0.0;0.0 -21;27552.624124116446;2342.187196875732;4564.96294015727;1227.010825012031;2898.6442215904294;637.6105018765174;14.501678081389679;11.79638284255251;6102.528340348395;2025.7225652306815;0.0;0.0 -22;27567.57601692015;2365.372620907202;4601.609251711169;47.962597884231506;3010.3926249804304;465.8097279250882;372.0157304133542;1041.6398353625416;5760.687804922804;2139.084150745089;0.0;0.0 -23;27536.715046833287;2325.543636697192;4560.083433389908;220.4043813712333;2462.0295804791986;1257.8669617671794;300.43753980329524;932.0552969056756;6888.61160256733;2092.7163784034374;0.0;0.0 -24;27555.149222588178;2307.5409994572638;4496.408451216739;245.2584957799409;2938.64627162702;438.0646998332584;420.8322472845995;1243.6217352724;6394.334433950387;2075.290548480801;0.0;0.0 diff --git a/src/pandapipes/test/data/test_time_series_results/res_pipe/v_mean_m_per_s.csv b/src/pandapipes/test/data/test_time_series_results/res_pipe/v_mean_m_per_s.csv index 979a1fff0..8c0c2a381 100644 --- a/src/pandapipes/test/data/test_time_series_results/res_pipe/v_mean_m_per_s.csv +++ b/src/pandapipes/test/data/test_time_series_results/res_pipe/v_mean_m_per_s.csv @@ -1,26 +1,26 @@ ;0;1;2;3;4;5;6;7;8;9;10;11 -0;2.209712176790032;-0.11874030406755744;0.23050403231948163;0.02797063658552472;-0.1209988982720476;0.012579144281024721;0.006666476056106008;0.0649880813226263;-0.27506688806710056;0.10602047874316264;0.0;0.0 -1;2.209087623560517;-0.11600422575568965;0.22667963319479967;0.06274396025101457;-0.10575393679922612;0.03625253059786122;0.0011995875721403759;0.007209838843085841;-0.28572334872316196;0.1003011159693128;0.0;0.0 -2;2.20898776523112;-0.11420663781763174;0.22440908301320345;0.04399817318823173;-0.09185154907018903;0.05497694697263145;0.0033035940974039963;0.029464794403769694;-0.286548376182756;0.10061776064116588;0.0;0.0 -3;2.2092219881302397;-0.11862048974530738;0.23058128064623146;0.0577133290816953;-0.11771325565662924;0.017757636915221697;0.0019408801006128389;0.013374931293609827;-0.25137040996919163;0.10327871408428906;0.0;0.0 -4;2.2091168985910015;-0.11858654722958596;0.2324268044527548;0.06199329689763694;-0.0949908543268946;0.05839255124990228;0.0026939023136481597;0.019484096159099046;-0.2799885967305293;0.10277313363277031;0.0;0.0 -5;2.2095365370819406;-0.11666123666625368;0.2285605419964776;0.013810545941692825;-0.09799873427537602;0.049277618367711856;0.007708993313709216;0.07128241427600826;-0.2458201784154771;0.10501987491710298;0.0;0.0 -6;2.209825864474567;-0.11530415319283784;0.2265570973623067;0.06860610428320003;-0.09082072872091583;0.05712166186998788;0.0025402938247206584;0.02275409379780618;-0.2703878129724281;0.09894005427203505;0.0;0.0 -7;2.2092496642069235;-0.11569846604194368;0.2251694838294593;0.015314923169066836;-0.1162135963020054;0.013499393840178842;0.005430480079257142;0.050607129926434374;-0.27734666043062045;0.1040772006188668;0.0;0.0 -8;2.2089597513870483;-0.11870400140560808;0.23049712083032436;0.048977765332357245;-0.12033771125947162;0.01334902219170926;0.0018929466455895872;0.014030847010589667;-0.2577552101894575;0.10424751302934333;0.0;0.0 -9;2.2091908404408724;-0.11627564493498334;0.22659754843848343;0.027960360850565594;-0.11257191716148122;0.02393840570112965;0.005442099416797806;0.05147183053606359;-0.2815785573235391;0.10380148030127796;0.0;0.0 -10;2.209530939715769;-0.1166324633323806;0.2262784519564658;0.014211193827308052;-0.12326233722586132;0.0014006240011981957;0.0048788828126387655;0.04484309742039158;-0.24386488819608707;0.10497286268503486;0.0;0.0 -11;2.2094983030759687;-0.11611747895284402;0.226208005027343;0.053248310302830226;-0.11356308399205725;0.022387638711136887;0.000763856455331191;0.006428977930229531;-0.2808869089507678;0.10145381576819194;0.0;0.0 -12;2.2094909536558602;-0.11587003428722226;0.2258416611220465;0.07751920031627936;-0.11241827202518835;0.018622560752053;0.000597204927034966;9.423706345952633e-05;-0.29367073349680556;0.09829334611148052;0.0;0.0 -13;2.209169074616177;-0.11745862420046016;0.22800533089059621;0.07910496793780626;-0.12165509911737137;0.007906618635245498;0.0006733588013802887;0.0033156840858749696;-0.25542048975974063;0.09956642248688446;0.0;0.0 -14;2.2096109339126824;-0.1167474957866391;0.22710647925668237;0.022494565877710267;-0.11682112554177777;0.014911528490968837;0.0038417403788408564;0.0320501615687465;-0.28479085942287885;0.10459266324587023;0.0;0.0 -15;2.209464787448767;-0.115237309452673;0.2251512659949908;0.06654228563178043;-0.10663710322013277;0.03328127229420383;0.002791932405146434;0.022534048483422442;-0.24766155245640611;0.09913462626932774;0.0;0.0 -16;2.20955978403246;-0.11404146758110954;0.22379539545265809;0.01109194297799185;-0.09570901376863118;0.05173593321273135;0.007711633725926548;0.0711443410566203;-0.24047219960897484;0.10281660052594392;0.0;0.0 -17;2.209587245375644;-0.11725865866396853;0.2284409907503465;0.03790387133605596;-0.1128080279406917;0.021850387229502666;0.0015381562195636864;0.01338993060089085;-0.2563263111620934;0.10392583444335882;0.0;0.0 -18;2.2093033791131806;-0.11679202151504864;0.2273334460294579;0.04481008805648982;-0.1152703637374793;0.018071440078806504;0.0037704240230339334;0.03650204826422716;-0.26819174122200967;0.10289739568613579;0.0;0.0 -19;2.2088685082238;-0.11643610007484559;0.2285682163175941;0.021910390959399034;-0.09244239347628513;0.05946475943664028;0.005072882640266024;0.044732437927162236;-0.2984495941185028;0.10435009536627063;0.0;0.0 -20;2.2092796749426857;-0.11760392099213922;0.2292532089905623;0.07425013864966974;-0.11102790441933966;0.028965585551850997;0.0005831954557392048;0.003938803976535036;-0.2807590395231984;0.10035840368220728;0.0;0.0 -21;2.2093381555165377;-0.11588801514846402;0.2262831467019378;0.0608576249055466;-0.10780439763680802;0.031599515248932515;0.00021560404345536972;0.0005480718837200235;-0.25214554077740786;0.10041197236296609;0.0;0.0 -22;2.2097116516037536;-0.11703440467356464;0.2280885043590036;-0.00231860781256977;-0.1119529160918255;0.02308515570065405;0.0055311896446977715;0.04839820501319223;-0.23801105386798724;0.10604102724663174;0.0;0.0 -23;2.2089380298655428;-0.11506188836688201;0.22604326758720492;0.0109724138947345;-0.09157597961540946;0.06233988651942162;0.004466930445989427;0.04330623643909935;-0.2846557068115981;0.10373496284156362;0.0;0.0 -24;2.209412200503767;-0.11416761645754209;0.22289047254196048;0.012205007425678776;-0.10929255772137338;0.02171015712005417;0.006256965371780336;0.0577828281602837;-0.2642126282156906;0.10287275925080795;0.0;0.0 +0;2.2097120678563194;-0.11869654748285619;0.23053440555695728;0.0279839892382307;-0.12101599725117734;0.012579144295708335;0.006666475997033131;0.0649880807468081;-0.2750668808173086;0.10603381976265218;0.0;0.0 +1;2.209087509395708;-0.11595777691443016;0.22671204861733443;0.06275796144589042;-0.1057721848885672;0.03625253066671805;0.0011995875542204085;0.007209838735382092;-0.2857233411166883;0.10031510563310504;0.0;0.0 +2;2.2089876479133608;-0.11415820229914452;0.2244428267187974;0.04401283263173888;-0.09187054452257531;0.05497694711692651;0.0033035940562611884;0.02946479403673494;-0.28654836835098035;0.10063240818193642;0.0;0.0 +3;2.2092218787096445;-0.11857671670433438;0.23061180163407746;0.05772654989362421;-0.11773043779679554;0.017757636939018807;0.0019408800747328827;0.013374931115266686;-0.2513704027035411;0.10329192351116304;0.0;0.0 +4;2.209116789300227;-0.11854281243707715;0.2324573201928695;0.062006484809967975;-0.09500803353537156;0.05839255139457861;0.0026939022760532993;0.019484095887187945;-0.2799885894760794;0.10278631023506721;0.0;0.0 +5;2.2095364247294933;-0.11661533009309924;0.22859232598546697;0.013824637423739395;-0.09801662705075609;0.04927761848030625;0.00770899325945581;0.07128241377452464;-0.24582017092555478;0.10503395462820882;0.0;0.0 +6;2.2098257489848283;-0.11525690162039828;0.22659009865851734;0.06862032197890812;-0.09083930648292658;0.05712166202377872;0.0025402937837300777;0.022754093430642507;-0.2703878052444889;0.09895426046172326;0.0;0.0 +7;2.209249549947557;-0.11565153038044293;0.22520199013562717;0.01532932091820139;-0.11623189546615331;0.013499393857884531;0.005430480038843515;0.05060712954973299;-0.2773466528110633;0.1040915865168403;0.0;0.0 +8;2.2089596422204703;-0.11866032105536391;0.2305275451138964;0.04899099037946954;-0.1203548389698338;0.013349022207828123;0.001892946622876998;0.014030846842240024;-0.25775520295099397;0.10426072660950526;0.0;0.0 +9;2.209190727094679;-0.11622938949600098;0.22662966822680686;0.027974465026087636;-0.11258999887100311;0.023938405737724922;0.005442099365867523;0.05147183005421577;-0.2815785497722944;0.10381557262675911;0.0;0.0 +10;2.2095308271468705;-0.11658648372053196;0.2263102837065643;0.014225310370341039;-0.12328025683193722;0.0014006240025595405;0.004878882777921533;0.04484309710120369;-0.24386488069094242;0.10498696744224185;0.0;0.0 +11;2.209498189090119;-0.11607108037218242;0.2262403515824557;0.05326233025010006;-0.11358129332177305;0.022387638744409695;0.0007638564450844536;0.0064289778439881335;-0.28088690134281685;0.10146782405511126;0.0;0.0 +12;2.209490839076392;-0.11582340569433126;0.22587423167439125;0.07753322563115035;-0.11243660740174614;0.01862256078085763;0.0005972049166629758;9.423706182285895e-05;-0.2936707258454251;0.09830736011146357;0.0;0.0 +13;2.2091689629705065;-0.11741368899981254;0.22803671515908722;0.07911848668450522;-0.12167276699347898;0.00790661864401934;0.0006733587899408157;0.0033156840295458932;-0.25542048233493847;0.09957993007320351;0.0;0.0 +14;2.2096108214624923;-0.11670168216805425;0.22713825437474455;0.02250857309843237;-0.11683901331021511;0.01491152851060541;0.0038417403465461574;0.032050161299385385;-0.2847908519211347;0.1046066586255049;0.0;0.0 +15;2.209464671701772;-0.11518996941502474;0.22518431610149595;0.0665565428938129;-0.10665570843132532;0.03328127235549295;0.0027919323609226824;0.022534048126486777;-0.24766154472431862;0.09914887197718657;0.0;0.0 +16;2.209559666723109;-0.11399264873279592;0.2238291860338342;0.01110693924810645;-0.0957280355719331;0.05173593333658917;0.007711633671812943;0.07114434055734502;-0.24047219175935544;0.10283158474536955;0.0;0.0 +17;2.209587133652535;-0.11721342331638981;0.2284724537257906;0.03791761236079622;-0.11282574010592838;0.02185038726331806;0.0015381562031021691;0.013389930457590328;-0.2563263037161325;0.10393956376110836;0.0;0.0 +18;2.2093032664808954;-0.11674633945289116;0.2273652550698932;0.04482392943687596;-0.11528827061531913;0.018071440104297634;0.0037704239782075026;0.0365020478304822;-0.26819173371973953;0.10291122538136623;0.0;0.0 +19;2.208868395511225;-0.11639008231101067;0.22860013993983178;0.02192445400554439;-0.09246036481163528;0.05946475959005655;0.0050728825979354355;0.044732437553766775;-0.2984495866230164;0.10436414658032316;0.0;0.0 +20;2.2092795636352225;-0.11755914725545015;0.22928447402551472;0.07426361540893615;-0.11104550522917554;0.028965585599881997;0.0005831954463048038;0.003938803912816678;-0.28075903211724285;0.10037186921296055;0.0;0.0 +21;2.2093380410638686;-0.11584139416191085;0.2263156764947361;0.060871683820903265;-0.10782271008126716;0.03159951530547286;0.00021560404028760972;0.0005480718756674854;-0.2521455331433197;0.10042601969931983;0.0;0.0 +22;2.2097115439341457;-0.11698645153137417;0.22811884751875375;-0.002301026461803478;-0.11196999782240673;0.023085155731466404;0.005531189827982769;0.04839820661681394;-0.2380110467016151;0.10605859805817233;0.0;0.0 +23;2.2089379147225543;-0.11501432102981879;0.22607618946938302;0.010987027858891756;-0.09159451262175695;0.06233988668240325;0.004466930415534826;0.043306236143735605;-0.28465569913831107;0.10374956488068975;0.0;0.0 +24;2.2094120833455224;-0.11411894242510494;0.22292416591642875;0.012219956010296053;-0.1093115248123514;0.02171015715571738;0.006256965326806582;0.05778282774487828;-0.2642126203810903;0.10288769580198101;0.0;0.0 diff --git a/src/pandapipes/test/openmodelica_comparison/pipeflow_openmodelica_comparison.py b/src/pandapipes/test/openmodelica_comparison/pipeflow_openmodelica_comparison.py index 4d539cd0c..166327f78 100644 --- a/src/pandapipes/test/openmodelica_comparison/pipeflow_openmodelica_comparison.py +++ b/src/pandapipes/test/openmodelica_comparison/pipeflow_openmodelica_comparison.py @@ -21,8 +21,7 @@ def pipeflow_openmodelica_comparison(net, log_results=True, friction_model='colebrook', max_iter_hyd=10, max_iter_therm=10, - mode='hydraulics', only_update_hydraulic_matrix=False, - use_numba=True, **kwargs): + mode='hydraulics', use_numba=True, **kwargs): """ Comparison of the calculations of OpenModelica and pandapipes. @@ -34,8 +33,6 @@ def pipeflow_openmodelica_comparison(net, log_results=True, friction_model='cole :type friction_model: str, "colebrook" :param mode: :type mode: str, "nomral" - :param only_update_hydraulic_matrix: - :type only_update_hydraulic_matrix: bool, False :param use_numba: whether to use numba for pipeflow calculations :type use_numba: bool, True :return: p_diff, v_diff_abs @@ -43,8 +40,7 @@ def pipeflow_openmodelica_comparison(net, log_results=True, friction_model='cole """ pp.pipeflow(net, stop_condition="tol", max_iter_hyd=max_iter_hyd, max_iter_therm=max_iter_therm, tol_p=1e-7, tol_m=1e-7, - friction_model=friction_model, mode=mode, use_numba=use_numba, - only_update_hydraulic_matrix=only_update_hydraulic_matrix, **kwargs) + friction_model=friction_model, mode=mode, use_numba=use_numba, **kwargs) logger.debug(net.res_junction) logger.debug(net.res_pipe) diff --git a/src/pandapipes/test/pipeflow_internals/test_component_list_pruning.py b/src/pandapipes/test/pipeflow_internals/test_component_list_pruning.py new file mode 100644 index 000000000..acbf5bdd5 --- /dev/null +++ b/src/pandapipes/test/pipeflow_internals/test_component_list_pruning.py @@ -0,0 +1,187 @@ +# Copyright (c) 2020-2026 by Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +""" +``net.component_list`` normally holds every registered component class (see +``pandapipes_net.get_basic_all_components``), regardless of whether the net actually uses that +component - a net with no valves still carries ``Valve`` in its component_list, its +``register_hydraulic_equations``/``create_pit_branch_entries``/etc. just iterate over zero rows +and contribute nothing. + +This checks that assumption holds: for a variety of networks exercising most component types +(pipe, valve, pump, compressor, flow control, pressure control, mass storage, heat exchanger, +heat consumer, both circulation pump variants), pruning component_list down to only the +components that actually have rows in that net must not change a single computed result compared +to running with the full, default component_list. +""" + +import copy + +import pandas as pd +import pytest + +import pandapipes +import pandapipes.networks.simple_gas_networks as gas_nw +import pandapipes.networks.simple_water_networks as water_nw + + +def _used_component_list(net): + """Every component in ``net.component_list`` that actually has rows in this net.""" + return [comp for comp in net.component_list if len(net[comp.table_name()]) > 0] + + +def _assert_results_match(net_full, net_pruned): + """Compare every result table that has rows in ``net_full`` against ``net_pruned`` - tables + with zero rows are skipped rather than compared, since an unused component's result table may + not even exist in the pruned net (it's only ever created once that component's own + ``extract_results`` runs) while the full net still carries an empty placeholder for it; that + difference is a bookkeeping artifact, not a result difference.""" + res_tables = [ + name for name, table in net_full.items() + if name.startswith("res_") and isinstance(table, pd.DataFrame) and len(table) > 0 + ] + assert res_tables, "the full run produced no result rows at all - nothing to compare" + for name in res_tables: + assert name in net_pruned, f"{name} has rows in the full run but doesn't exist at all " \ + f"in the pruned-component-list run" + pd.testing.assert_frame_equal(net_full[name], net_pruned[name], check_exact=False, + atol=1e-9, rtol=1e-9) + + +def _run_full_vs_pruned(net, pipeflow_kwargs): + net_full = copy.deepcopy(net) + net_pruned = copy.deepcopy(net) + net_pruned.component_list = _used_component_list(net_pruned) + # every net here is built with the full default component_list, so pruning must actually + # remove something, or the test isn't exercising anything + assert len(net_pruned.component_list) < len(net_full.component_list) + + pandapipes.pipeflow(net_full, **pipeflow_kwargs) + pandapipes.pipeflow(net_pruned, **pipeflow_kwargs) + + assert net_full.converged + assert net_pruned.converged + _assert_results_match(net_full, net_pruned) + + +def _net_compressor(): + """Junction, Pipe, ExtGrid, Sink, Compressor (with one reverse-flow bypass compressor).""" + net = pandapipes.create_empty_network("net", add_stdtypes=True, fluid="hgas") + j1, j2, j3, j4, j5, j6 = pandapipes.create_junctions(net, 6, pn_bar=5, tfluid_k=283.15) + pandapipes.create_pipe_from_parameters(net, j1, j2, length_km=0.4338, inner_diameter_mm=102.2) + pandapipes.create_pipe_from_parameters(net, j3, j4, length_km=0.2637, inner_diameter_mm=102.2) + pandapipes.create_ext_grid(net, j1, 5, 283.15, type="p") + pandapipes.create_sink(net, j6, 0.02333) + pandapipes.create_compressor(net, j2, j3, pressure_ratio=1.5) + pandapipes.create_compressor(net, j5, j4, pressure_ratio=1.5) + pandapipes.create_compressor(net, j5, j6, pressure_ratio=1.1) + return net, dict(max_iter_hyd=10) + + +def _net_flow_control_heat_exchanger(): + """Junction, Pipe, ExtGrid, Sink, HeatExchanger, FlowControlComponent (one active, one not).""" + net = pandapipes.create_empty_network("net", add_stdtypes=True, fluid="water") + j1, j2, j3, j4, j5, j6, j7, j8 = pandapipes.create_junctions(net, 8, pn_bar=5, tfluid_k=360) + pandapipes.create_pipes_from_parameters( + net, [j1, j2, j4, j7], [j2, j5, j8, j4], 0.2, 100, k_mm=0.1, u_w_per_m2k=20., text_k=280) + pandapipes.create_heat_exchanger(net, j3, j4, 0.1, 50000, 1) + pandapipes.create_heat_exchanger(net, j6, j7, 0.1, 50000, 1) + pandapipes.create_flow_control(net, j2, j3, 2) + pandapipes.create_flow_control(net, j5, j6, 2, control_active=False) + pandapipes.create_ext_grid(net, j1, p_bar=5, t_k=360, type="pt") + pandapipes.create_sink(net, j8, 3) + return net, dict(max_iter_hyd=10, max_iter_therm=10, mode='sequential') + + +def _net_pressure_control(): + """Junction, Pipe, ExtGrid, Sink, PressureControlComponent.""" + net = pandapipes.create_empty_network("net", add_stdtypes=False) + j1 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j2 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j3 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j4 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + pandapipes.create_pipe_from_parameters(net, j2, j3, k_mm=1., length_km=5., inner_diameter_mm=102.2) + pandapipes.create_pipe_from_parameters(net, j3, j4, k_mm=1., length_km=10., inner_diameter_mm=102.2) + pandapipes.create_pressure_control(net, j1, j2, j4, 20.) + pandapipes.create_ext_grid(net, j1, 32, 283.15, type="p") + pandapipes.create_sink(net, j4, 0.5) + pandapipes.create_fluid_from_lib(net, "lgas", overwrite=True) + return net, dict(stop_condition="tol", max_iter_hyd=10, friction_model="nikuradse", + mode="hydraulics", transient=False, nonlinear_method="automatic", + tol_p=1e-4, tol_m=1e-4) + + +def _net_mass_storage(): + """Junction, Pipe, ExtGrid, MassStorage (one charging, one discharging).""" + net = pandapipes.create_empty_network("net", add_stdtypes=True, fluid="water") + j1, j2, j3 = pandapipes.create_junctions(net, 3, pn_bar=2, tfluid_k=283.15) + pandapipes.create_pipe_from_parameters(net, j1, j2, length_km=1, diameter_m=0.5) + pandapipes.create_pipe_from_parameters(net, j2, j3, length_km=1, diameter_m=0.5) + pandapipes.create_ext_grid(net, j1, 2, 283.15, type="p") + pandapipes.create_mass_storage(net, j2, 0.1) + pandapipes.create_mass_storage(net, j3, -0.2) + return net, dict(max_iter_hyd=10) + + +def _net_circ_pump_mass(): + """Junction, Pipe, CirculationPumpMass, HeatExchanger, Sink, Source.""" + net = pandapipes.create_empty_network("net", add_stdtypes=False) + j1 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j2 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j3 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + j4 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15) + pandapipes.create_pipe_from_parameters(net, j1, j2, k_mm=1., length_km=0.4338, inner_diameter_mm=102.2) + pandapipes.create_pipe_from_parameters(net, j3, j4, k_mm=1., length_km=0.2637, inner_diameter_mm=102.2) + pandapipes.create_circ_pump_const_mass_flow(net, j4, j1, 5, 5, 300, type='pt') + pandapipes.create_heat_exchanger(net, j2, j3, qext_w=200000, inner_diameter_mm=100) + pandapipes.create_sink(net, j1, 2) + pandapipes.create_source(net, j4, 2) + pandapipes.create_fluid_from_lib(net, "water", overwrite=True) + return net, dict(max_iter_hyd=10, max_iter_therm=10, stop_condition="tol", + friction_model="nikuradse", mode='sequential', transient=False, + nonlinear_method="automatic", tol_p=1e-4, tol_m=1e-4) + + +def _net_heat_consumer_circ_pump_pressure(): + """Junction, Pipe, CirculationPumpPressure, HeatConsumer (two, on a flow/return loop).""" + net = pandapipes.create_empty_network("net", add_stdtypes=False, fluid="water") + juncs = pandapipes.create_junctions(net, 6, pn_bar=5, tfluid_k=283.15, + system=["flow"] * 3 + ["return"] * 3) + pandapipes.create_pipes_from_parameters( + net, juncs[[0, 1, 3, 4]], juncs[[1, 2, 4, 5]], k_mm=0.1, length_km=1, + inner_diameter_mm=102.2, system=["flow"] * 2 + ["return"] * 2, u_w_per_m2k=10, text_k=273.15) + pandapipes.create_circ_pump_const_pressure(net, juncs[-1], juncs[0], 5, 2, 400, type='pt') + pandapipes.create_heat_consumer(net, juncs[1], juncs[4], controlled_mdot_kg_per_s=3, qext_w=150000) + pandapipes.create_heat_consumer(net, juncs[2], juncs[3], controlled_mdot_kg_per_s=2, qext_w=75000) + return net, dict(mode='sequential') + + +NETWORK_BUILDERS = { + # bundled STANET/example networks - real, previously-validated topologies + "water_district_grid": lambda: (water_nw.water_district_grid(), + dict(mode='hydraulics', max_iter_hyd=20)), + "water_meshed_2valves": lambda: (water_nw.water_meshed_2valves(results_from="stanet"), + dict(mode='hydraulics', max_iter_hyd=20)), + "gas_meshed_pumps": lambda: (gas_nw.gas_meshed_pumps(), dict(mode='hydraulics', max_iter_hyd=20)), + "gas_versatility": lambda: (gas_nw.gas_versatility(), dict(mode='hydraulics', max_iter_hyd=20)), + # hand-built networks for component types none of the bundled examples exercise + "compressor": _net_compressor, + "flow_control_heat_exchanger": _net_flow_control_heat_exchanger, + "pressure_control": _net_pressure_control, + "mass_storage": _net_mass_storage, + "circ_pump_mass": _net_circ_pump_mass, + "heat_consumer_circ_pump_pressure": _net_heat_consumer_circ_pump_pressure, +} + + +@pytest.mark.parametrize("network_name", list(NETWORK_BUILDERS.keys())) +def test_pruned_component_list_matches_full(network_name): + """A net's component_list normally contains every registered component class regardless of + whether the net uses it - removing the ones with zero rows must not change any result.""" + net, pipeflow_kwargs = NETWORK_BUILDERS[network_name]() + _run_full_vs_pruned(net, pipeflow_kwargs) + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/src/pandapipes/test/pipeflow_internals/test_derivative_calculation.py b/src/pandapipes/test/pipeflow_internals/test_derivative_calculation.py new file mode 100644 index 000000000..c38a965b4 --- /dev/null +++ b/src/pandapipes/test/pipeflow_internals/test_derivative_calculation.py @@ -0,0 +1,62 @@ +# Copyright (c) 2020-2026 by Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel, and University of Kassel. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +""" +Regression test for ``calc_der_lambda`` (dlambda/dm): all three friction models ("nikuradse"/ +default, "swamee-jain", "colebrook") previously returned the same value for m < 0 as for m > 0 +instead of flipping sign (missing sign(m)/np.abs(m) chain-rule factors), and "colebrook" was +additionally missing the leading minus sign from the implicit function theorem +(dlambda/dm = -dF/dm / dF/dlambda). See git history for the derivations. +""" + +import numpy as np +import pytest + +from pandapipes.pf.derivative_calculation import calc_lambda, calc_der_lambda + +EPS = 1e-6 + + +@pytest.mark.parametrize("friction_model", ["nikuradse", "swamee-jain", "colebrook"]) +@pytest.mark.parametrize("m0", [0.8, -0.8, 0.05, -0.05, 2.0, -2.0]) +def test_calc_der_lambda_matches_finite_difference(friction_model, m0): + d = np.array([0.12]) + l = np.array([800.]) + k = np.array([0.1e-3]) + eta = np.array([1e-3]) + opts = {"use_numba": False, "max_iter_colebrook": 100, "tolerance_colebrook": 1e-6} + + def lambd_of_m(m): + m = np.array([m]) + area = np.pi * (d / 2) ** 2 + lambd, _ = calc_lambda(m, eta, d, k, False, friction_model, l, opts, area) + return lambd[0] + + fd = (lambd_of_m(m0 + EPS) - lambd_of_m(m0 - EPS)) / (2 * EPS) + + m = np.array([m0]) + area = np.pi * (d / 2) ** 2 + lambd, re = calc_lambda(m, eta, d, k, False, friction_model, l, opts, area) + der_lambda = calc_der_lambda(m, eta, d, k, friction_model, lambd, area, re, l) + + assert np.isclose(der_lambda[0], fd, rtol=1e-4) + + +@pytest.mark.parametrize("friction_model", ["nikuradse", "swamee-jain", "colebrook"]) +def test_calc_der_lambda_flips_sign_for_negative_m(friction_model): + # a symmetric physical setup (same |m|) must give equal-magnitude, opposite-sign derivatives - + # this is the exact class of bug that motivated the finite-difference test above + d = np.array([0.12]) + l = np.array([800.]) + k = np.array([0.1e-3]) + eta = np.array([1e-3]) + opts = {"use_numba": False, "max_iter_colebrook": 100, "tolerance_colebrook": 1e-6} + area = np.pi * (d / 2) ** 2 + + def der_at(m0): + m = np.array([m0]) + lambd, re = calc_lambda(m, eta, d, k, False, friction_model, l, opts, area) + return calc_der_lambda(m, eta, d, k, friction_model, lambd, area, re, l)[0] + + assert np.isclose(der_at(0.8), -der_at(-0.8), rtol=1e-8) diff --git a/src/pandapipes/test/pipeflow_internals/test_inservice.py b/src/pandapipes/test/pipeflow_internals/test_inservice.py index 53528aba4..733b6e004 100644 --- a/src/pandapipes/test/pipeflow_internals/test_inservice.py +++ b/src/pandapipes/test/pipeflow_internals/test_inservice.py @@ -8,8 +8,7 @@ import pytest import pandapipes -from pandapipes.pf.pipeflow_setup import get_lookup -from pandapipes.pipeflow import PipeflowNotConverged +from pandapipes.pf.pipeflow_setup import get_lookup, PipeflowNotConverged from pandapipes.pipeflow import logger as pf_logger try: @@ -561,7 +560,7 @@ def test_mixed_indexing_oos3(create_mixed_indexing_grid, use_numba): net.pipe.at[7, "in_service"] = False oos_juncs = [6, 15] - max_iter_hyd = 3 if use_numba else 3 + max_iter_hyd = 4 if use_numba else 4 with pytest.raises(PipeflowNotConverged): pandapipes.pipeflow(net, max_iter_hyd=max_iter_hyd, mode="hydraulics", use_numba=use_numba, check_connectivity=False) @@ -582,7 +581,7 @@ def test_mixed_indexing_oos4(create_mixed_indexing_grid, use_numba): net.valve.at[2, "opened"] = False oos_juncs = [15] - max_iter_hyd = 3 if use_numba else 3 + max_iter_hyd = 4 if use_numba else 4 with pytest.raises(PipeflowNotConverged): pandapipes.pipeflow(net, max_iter_hyd=max_iter_hyd, mode="hydraulics", use_numba=use_numba, check_connectivity=False) @@ -603,7 +602,7 @@ def test_mixed_indexing_oos5(create_mixed_indexing_grid, use_numba): net.pipe.at[6, "in_service"] = False oos_juncs = [9, 8, 7] - max_iter_hyd = 3 if use_numba else 3 + max_iter_hyd = 4 if use_numba else 4 with pytest.raises(PipeflowNotConverged): pandapipes.pipeflow(net, max_iter_hyd=max_iter_hyd, mode="hydraulics", use_numba=use_numba, check_connectivity=False) diff --git a/src/pandapipes/test/pipeflow_internals/test_non_convergence.py b/src/pandapipes/test/pipeflow_internals/test_non_convergence.py index 2d6938961..2f8ff3a55 100644 --- a/src/pandapipes/test/pipeflow_internals/test_non_convergence.py +++ b/src/pandapipes/test/pipeflow_internals/test_non_convergence.py @@ -7,7 +7,7 @@ import pandapipes from pandapipes.networks.simple_gas_networks import gas_versatility -from pandapipes.pipeflow import PipeflowNotConverged +from pandapipes.pf.pipeflow_setup import PipeflowNotConverged from pandapipes.properties.fluids import FluidPropertyConstant @@ -16,7 +16,7 @@ def test_pipeflow_non_convergence(use_numba): net = gas_versatility() pandapipes.get_fluid(net).add_property("molar_mass", FluidPropertyConstant(16.6)) - max_iter_hyd = 9 if use_numba else 9 + max_iter_hyd = 10 if use_numba else 10 pandapipes.pipeflow(net, use_numba=use_numba, max_iter_hyd=max_iter_hyd) for comp in net["component_list"]: table_name = comp.table_name() @@ -34,3 +34,27 @@ def test_pipeflow_non_convergence(use_numba): table_name = comp.table_name() assert np.all(net["res_" + table_name].index == net[table_name].index) assert np.all(pd.isnull(net["res_" + table_name])) + + +def test_heat_mode_rejects_unconverged_hydraulics(): + """Regression test: initialize_pit()'s standalone mode="heat" branch used to reuse + net["_pit"] just by checking it exists, without checking net.converged - if a caller caught + PipeflowNotConverged from a failed hydraulics run and later ran mode="heat" on the same net + (e.g. while batch-processing many nets, or just re-trying with different heat options), the + heat solve would silently build on the unconverged, physically meaningless mdot/p values that + non-convergent run still left behind - and then overwrite net.converged with the HEAT run's + own (unrelated) outcome, erasing any sign that the underlying hydraulics never converged.""" + net = pandapipes.create_empty_network("net", add_stdtypes=False, fluid="water") + j1, j2 = pandapipes.create_junctions(net, 2, pn_bar=5, tfluid_k=300) + pandapipes.create_pipe_from_parameters(net, j1, j2, length_km=1, inner_diameter_mm=100) + pandapipes.create_ext_grid(net, j1, p_bar=5, t_k=300, type="pt") + pandapipes.create_sink(net, j2, mdot_kg_per_s=1) + + with pytest.raises(PipeflowNotConverged): + pandapipes.pipeflow(net, mode="hydraulics", max_iter_hyd=1) + assert not net.converged + assert "_pit" in net # the failed run still left a (non-converged) pit behind + + with pytest.raises(PipeflowNotConverged): + pandapipes.pipeflow(net, mode="heat") + assert not net.converged # not silently overwritten by an unrelated heat-run outcome diff --git a/src/pandapipes/test/pipeflow_internals/test_options.py b/src/pandapipes/test/pipeflow_internals/test_options.py index 9f0f0733c..41409b020 100644 --- a/src/pandapipes/test/pipeflow_internals/test_options.py +++ b/src/pandapipes/test/pipeflow_internals/test_options.py @@ -35,7 +35,6 @@ def test_set_user_pf_options(create_test_net, use_numba): pandapipes.pf.pipeflow_setup.set_user_pf_options(net, hello='bye', **test_options) test_options.update({'hello': 'bye'}) - test_options.update({'hyd_flag': True}) assert net.user_pf_options == test_options diff --git a/src/pandapipes/test/pipeflow_internals/test_pipeflow_analytic_comparison.py b/src/pandapipes/test/pipeflow_internals/test_pipeflow_analytic_comparison.py index 66f245d97..746f01069 100644 --- a/src/pandapipes/test/pipeflow_internals/test_pipeflow_analytic_comparison.py +++ b/src/pandapipes/test/pipeflow_internals/test_pipeflow_analytic_comparison.py @@ -11,7 +11,7 @@ import pandapipes from pandapipes.component_models.junction_component import Junction from pandapipes.component_models.pipe_component import Pipe -from pandapipes.idx_node import PINIT, TINIT +from pandapipes.idx_node import IdxNode from pandapipes.pf.pipeflow_setup import get_lookup from pandapipes.properties.fluids import _add_fluid_to_net from pandapipes.test import data_path @@ -78,7 +78,7 @@ def test_gas_internal_nodes(use_numba): density=0.82752, ), ) - max_iter_hyd = 6 if use_numba else 6 + max_iter_hyd = 7 if use_numba else 7 pandapipes.pipeflow( net, max_iter_hyd=max_iter_hyd, @@ -115,9 +115,9 @@ def test_gas_internal_nodes(use_numba): to_junction_nodes = junction_idx_lookup[net["pipe"]["to_junction"].values] p_pandapipes = np.zeros(len(pipe_p_data[0]) + 2) - p_pandapipes[0] = node_pit[from_junction_nodes[0], PINIT] + p_pandapipes[0] = node_pit[from_junction_nodes[0], IdxNode.PINIT] p_pandapipes[1:-1] = pipe_p_data[:] - p_pandapipes[-1] = node_pit[to_junction_nodes[0], PINIT] + p_pandapipes[-1] = node_pit[to_junction_nodes[0], IdxNode.PINIT] p_pandapipes = p_pandapipes + 1.01325 v_pandapipes = pipe_v_data[0, :] @@ -183,9 +183,9 @@ def test_temperature_internal_nodes_single_pipe(use_numba): to_junction_nodes = junction_idx_lookup[net["pipe"]["to_junction"].values] temp_pandapipes = np.zeros(len(pipe_temp_data[0]) + 2) - temp_pandapipes[0] = node_pit[from_junction_nodes[0], TINIT] + temp_pandapipes[0] = node_pit[from_junction_nodes[0], IdxNode.TINIT] temp_pandapipes[1:-1] = pipe_temp_data[:] - temp_pandapipes[-1] = node_pit[to_junction_nodes[0], TINIT] + temp_pandapipes[-1] = node_pit[to_junction_nodes[0], IdxNode.TINIT] temp_diff = np.abs(1 - temp_pandapipes / temp_an) diff --git a/src/pandapipes/test/pipeflow_internals/test_pipeflow_modes.py b/src/pandapipes/test/pipeflow_internals/test_pipeflow_modes.py index b6608e31d..8d38df2a4 100644 --- a/src/pandapipes/test/pipeflow_internals/test_pipeflow_modes.py +++ b/src/pandapipes/test/pipeflow_internals/test_pipeflow_modes.py @@ -11,8 +11,8 @@ import pandapipes from pandapipes.constants import NORMAL_TEMPERATURE -from pandapipes.idx_branch import MDOTINIT, AREA -from pandapipes.idx_node import PINIT +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode from pandapipes.properties import get_fluid from pandapipes.test import data_path @@ -58,9 +58,9 @@ def test_hydraulic_only(simple_test_net, use_numba): v_an = data.loc[0, "pv"] p_an = data.loc[1:3, "pv"] - p_pandapipes = node_pit[:, PINIT] + p_pandapipes = node_pit[:, IdxNode.PINIT] fluid = get_fluid(net) - v_pandapipes = branch_pit[:, MDOTINIT] / branch_pit[:, AREA] / fluid.get_density(NORMAL_TEMPERATURE) + v_pandapipes = branch_pit[:, IdxBranch.MDOTINIT] / (np.pi * (branch_pit[:, IdxBranch.D] / 2) ** 2) / fluid.get_density(NORMAL_TEMPERATURE) p_diff = np.abs(1 - p_pandapipes / p_an) v_diff = np.abs(v_pandapipes - v_an) @@ -103,13 +103,9 @@ def test_heat_only(use_numba): pandapipes.pipeflow(ntw, max_iter_hyd=max_iter_hyd, stop_condition="tol", friction_model="nikuradse", nonlinear_method="automatic", mode="hydraulics", use_numba=use_numba) - p = ntw._pit["node"][:, PINIT] - m = ntw._pit["branch"][:, MDOTINIT] - u = np.concatenate((p, m)) - max_iter_therm = 4 if use_numba else 4 pandapipes.pipeflow(ntw, max_iter_therm=max_iter_therm, - sol_vec=u, stop_condition="tol", friction_model="nikuradse", + stop_condition="tol", friction_model="nikuradse", nonlinear_method="automatic", mode="heat", use_numba=use_numba) temp_net = net.res_junction.t_k @@ -118,3 +114,33 @@ def test_heat_only(use_numba): temp_diff = np.abs(1 - temp_net / temp_ntw) assert np.all(temp_diff < 0.01) + + +def test_bidirectional_automatic_damping_no_crash(): + """Regression test: BidirectionalCalculation.VARS/TOLS/PITS/COLS used to list only 4 entries + (mdot, p, TOUT, T) while solve_bidirectional() actually returns 5 variables' worth of data, in + order (mdot, p, mdotslack, Tout, T) - Calculation.run()'s positional un-interleaving then + silently paired 'TOUT' with mdotslack's values/branch pit/TOUTINIT column and 'T' with Tout's + values, dropping the real T pair entirely. + + MDOTSLACKINIT (the residual mass each ext_grid absorbs) only actually changes between + iterations - and so only actually triggers automatic damping's error-increased check for that + mismatched slot - with more than one ext_grid to balance mass flow between; a single-ext_grid + net never exercises this path since mdotslack then stays at/near 0 throughout. With two + ext_grids and nonlinear_method="automatic", a damping-fallback write for that mismatched + 'TOUT' slot instead wrote mdotslack's (small, node-range) index/shape into the branch pit, + raising IndexError or ValueError (shape mismatch) depending on how the node/branch pit sizes + happened to compare on the network at hand - both symptoms of the same misalignment.""" + net = pandapipes.create_empty_network("net", add_stdtypes=False, fluid="water") + j = pandapipes.create_junctions(net, 6, pn_bar=5, tfluid_k=300) + for a, b in [(0, 1), (1, 4), (4, 5)]: + pandapipes.create_pipe_from_parameters(net, j[a], j[b], length_km=1, inner_diameter_mm=80) + for eg in [0, 5]: + pandapipes.create_ext_grid(net, j[eg], p_bar=5, t_k=300, type="pt") + for s in [1, 4]: + pandapipes.create_sink(net, j[s], mdot_kg_per_s=1) + + pandapipes.pipeflow(net, mode="bidirectional", nonlinear_method="automatic", + max_iter_bidirect=20) + + assert net.converged diff --git a/src/pandapipes/test/pipeflow_internals/test_time_series.py b/src/pandapipes/test/pipeflow_internals/test_time_series.py index d590f3563..cefcc96ed 100644 --- a/src/pandapipes/test/pipeflow_internals/test_time_series.py +++ b/src/pandapipes/test/pipeflow_internals/test_time_series.py @@ -154,7 +154,7 @@ def test_time_series(): time_steps = range(25) # _output_writer(net, time_steps) # , path=os.path.join(ppipe.pp_dir, 'results')) _output_writer(net, time_steps) - max_iter_hyd = 9 + max_iter_hyd = 10 run_timeseries(net, time_steps, max_iter_hyd=max_iter_hyd, calc_compression_power = False) ow = net.output_writer.iat[0, 0] _compare_results(ow) @@ -170,7 +170,7 @@ def test_time_series_default_ow(): _prepare_grid(net) time_steps = range(25) init_default_outputwriter(net, time_steps) - max_iter_hyd = 9 + max_iter_hyd = 11 run_timeseries(net, time_steps, max_iter_hyd=max_iter_hyd, calc_compression_power = False) ow = net.output_writer.iat[0, 0] _compare_results(ow) diff --git a/src/pandapipes/test/pipeflow_internals/test_update_matrix.py b/src/pandapipes/test/pipeflow_internals/test_update_matrix.py index aa8898ac8..de6cda36b 100644 --- a/src/pandapipes/test/pipeflow_internals/test_update_matrix.py +++ b/src/pandapipes/test/pipeflow_internals/test_update_matrix.py @@ -30,8 +30,7 @@ def test_update(use_numba, log_results=False): # before: gas_case3.json net = nw.gas_one_pipe1() max_iter_hyd = 5 if use_numba else 5 - p_diff, v_diff_abs = pipeflow_stanet_comparison(net, log_results, use_numba=use_numba, max_iter_hyd=max_iter_hyd, - only_update_hydraulic_matrix=True) + p_diff, v_diff_abs = pipeflow_stanet_comparison(net, log_results, use_numba=use_numba, max_iter_hyd=max_iter_hyd) assert np.all(p_diff < 0.01) assert np.all(v_diff_abs < 0.05) diff --git a/src/pandapipes/test/stanet_comparison/pipeflow_stanet_comparison.py b/src/pandapipes/test/stanet_comparison/pipeflow_stanet_comparison.py index 1a12de607..8332e678c 100644 --- a/src/pandapipes/test/stanet_comparison/pipeflow_stanet_comparison.py +++ b/src/pandapipes/test/stanet_comparison/pipeflow_stanet_comparison.py @@ -16,7 +16,6 @@ def pipeflow_stanet_comparison(net, log_results=True, friction_model='nikuradse', - only_update_hydraulic_matrix=False, max_iter_hyd=10, **kwargs): """ @@ -28,14 +27,11 @@ def pipeflow_stanet_comparison(net, log_results=True, friction_model='nikuradse' :type plot_net: :param friction_model: :type friction_model: - :param only_update_hydraulic_matrix: - :type only_update_hydraulic_matrix: :return: :rtype: """ pandapipes.pipeflow(net, mode='hydraulics', stop_condition="tol",max_iter_hyd=max_iter_hyd, tol_p=1e-7, - tol_m=1e-7, friction_model=friction_model, - only_update_hydraulic_matrix=only_update_hydraulic_matrix, **kwargs) + tol_m=1e-7, friction_model=friction_model, **kwargs) p_stanet = net.junction.p_stanet p_valid = pd.notnull(p_stanet) diff --git a/src/pandapipes/test/stanet_comparison/test_water_stanet.py b/src/pandapipes/test/stanet_comparison/test_water_stanet.py index 2018260d2..1de52de85 100644 --- a/src/pandapipes/test/stanet_comparison/test_water_stanet.py +++ b/src/pandapipes/test/stanet_comparison/test_water_stanet.py @@ -73,7 +73,7 @@ def test_case_pumps_n(use_numba, log_results=False): :rtype: """ net = nw.water_meshed_pumps(results_from="stanet") - max_iter_hyd = 21 if use_numba else 21 + max_iter_hyd = 22 if use_numba else 22 p_diff, v_diff_abs = pipeflow_stanet_comparison(net, log_results, max_iter_hyd=max_iter_hyd, use_numba=use_numba) diff --git a/src/pandapipes/test/test_toolbox.py b/src/pandapipes/test/test_toolbox.py index 1bc63cac4..2b16e6c4b 100644 --- a/src/pandapipes/test/test_toolbox.py +++ b/src/pandapipes/test/test_toolbox.py @@ -13,8 +13,8 @@ from packaging import version from pandapipes import networks as nw, BranchComponent from pandapipes.component_models import NodeComponent -from pandapipes.idx_branch import branch_cols -from pandapipes.idx_node import node_cols +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode from pandapipes.test.api.test_convert_format import found_versions, folder, minimal_version_two_nets try: @@ -308,13 +308,13 @@ def test_pit_extraction(): if not "_gas" in name: pandapipes.create_ext_grid(net, junction=4, p_bar=6, t_k=290, name="External Grid 2", index=None) pandapipes.create_ext_grid(net, junction=5, p_bar=5, t_k=290, name="External Grid 3") - max_iter_hyd = 11 if '_water' in name else 6 + max_iter_hyd = 12 if '_water' in name else 6 pandapipes.pipeflow(net, max_iter_hyd=max_iter_hyd) node_table, branch_table = pandapipes.get_internal_tables_pandas(net) - assert node_table.shape[1] == node_cols - assert branch_table.shape[1] == branch_cols + assert node_table.shape[1] == IdxNode.node_cols + assert branch_table.shape[1] == IdxBranch.branch_cols for comp in net.component_list: tbl = comp.table_name() diff --git a/src/pandapipes/timeseries/run_time_series.py b/src/pandapipes/timeseries/run_time_series.py index d77e30f06..677559412 100644 --- a/src/pandapipes/timeseries/run_time_series.py +++ b/src/pandapipes/timeseries/run_time_series.py @@ -5,7 +5,8 @@ import tempfile from pandapipes.control import run_control -from pandapipes.pipeflow import PipeflowNotConverged, pipeflow +from pandapipes.pf.pipeflow_setup import PipeflowNotConverged +from pandapipes.pipeflow import pipeflow from pandapower.control import NetCalculationNotConverged from pandapower.control.util.diagnostic import control_diagnostic from pandapower.timeseries.output_writer import OutputWriter @@ -21,8 +22,7 @@ def init_default_outputwriter(net, time_steps, **kwargs): - """ - Creates a default output writer for the time series calculation. + """Creates a default output writer for the time series calculation. :param net: The pandapipes format network :type net: pandapipesNet @@ -65,7 +65,7 @@ def init_default_outputwriter(net, time_steps, **kwargs): def pf_not_converged(time_step, ts_variables): - """ + """Handle a pipeflow non-convergence event at a given time step. :param time_step: Time step to be calculated :type time_step: int @@ -80,8 +80,7 @@ def pf_not_converged(time_step, ts_variables): def init_time_series(net, time_steps, continue_on_divergence=False, verbose=True, **kwargs): - """ - Initializes the time series calculation. + """Initializes the time series calculation. Creates the dict ts_variables, which includes necessary variables for the time series / control function. @@ -100,7 +99,6 @@ def init_time_series(net, time_steps, continue_on_divergence=False, verbose=True :return: ts_variables, kwargs :rtype: dict, dict """ - run = kwargs.pop("run", pipeflow) init_default_outputwriter(net, time_steps, **kwargs) @@ -113,14 +111,18 @@ def init_time_series(net, time_steps, continue_on_divergence=False, verbose=True def run_loop(net, ts_variables, run_control_fct=run_control, output_writer_fct=_call_output_writer, **kwargs): - """ - runs the time series loop which calls pp.runpp (or another run function) in each iteration + """Run the time series loop which calls pp.runpp (or another run function) in each iteration. Parameters ---------- - net - pandapower net - ts_variables - settings for time series - + net : pandapipesNet + The pandapipes network to run the loop over. + ts_variables : dict + Settings for the time series run (as returned by ``init_time_series``). + run_control_fct : callable, default ``run_control`` + Function called once per time step to run control loops. + output_writer_fct : callable, default ``_call_output_writer`` + Function called once per time step to write results to the output writer. """ for i, time_step in enumerate(ts_variables["time_steps"]): print_progress(i, time_step, ts_variables["time_steps"], ts_variables["verbose"], ts_variables=ts_variables, @@ -132,8 +134,7 @@ def run_loop(net, ts_variables, run_control_fct=run_control, output_writer_fct=_ def run_timeseries(net, time_steps=None, continue_on_divergence=False, verbose=True, **kwargs): - """ - Time Series main function + """Time Series main function. Execution of pipe flow calculations for a time series using controllers. Optionally other functions than pipeflow can be called by setting the run function in kwargs. diff --git a/src/pandapipes/toolbox.py b/src/pandapipes/toolbox.py index e646c74b6..5e77ecc74 100644 --- a/src/pandapipes/toolbox.py +++ b/src/pandapipes/toolbox.py @@ -17,9 +17,8 @@ from pandapipes.component_models.abstract_models.branch_models import BranchComponent from pandapipes.component_models.abstract_models.node_element_models import NodeElementComponent from pandapipes.create import create_empty_network -from pandapipes.idx_branch import branch_cols -from pandapipes.idx_node import node_cols, \ - T as TYPE_T, P as TYPE_P, PC as TYPE_PC, L as TYPE_L +from pandapipes.idx_branch import IdxBranch +from pandapipes.idx_node import IdxNode from pandapipes.pandapipes_net import pandapipesNet from pandapipes.topology import create_nxgraph @@ -32,8 +31,7 @@ def nets_equal(net1, net2, check_only_results=False, exclude_elms=None, **kwargs): - """ - Compares the DataFrames of two networks. + """Compares the DataFrames of two networks. The networks are considered equal if they share the same keys and values, except of the 'et' (elapsed time) entry which differs depending on runtime conditions and entries stating with '_'. @@ -51,7 +49,6 @@ def nets_equal(net1, net2, check_only_results=False, exclude_elms=None, **kwargs :return: True, if nets are equal :rtype: Bool """ - eq = isinstance(net1, pandapipesNet) and isinstance(net2, pandapipesNet) exclude_elms = [] if exclude_elms is None else list(exclude_elms) exclude_elms += ["res_" + ex for ex in exclude_elms] @@ -92,9 +89,9 @@ def nets_equal(net1, net2, check_only_results=False, exclude_elms=None, **kwargs def element_junction_tuples(include_node_elements=True, include_branch_elements=True, include_res_elements=False, net=None): - """ - Utility function - Provides the tuples of elements and corresponding columns for junctions they are connected to + """Utility function. + + Provides the tuples of elements and corresponding columns for junctions they are connected to. :param include_node_elements: whether tuples for junction elements e.g. sink, source, are \ included @@ -156,9 +153,9 @@ def element_junction_tuples(include_node_elements=True, include_branch_elements= def pp_elements(junction=True, include_node_elements=True, include_branch_elements=True, include_res_elements=False, net=None): - """ - Provides a list of all pandapipes elements belonging to the desired element types. If a net is - given, the elements are derived from the component list. + """Provides a list of all pandapipes elements belonging to the desired element types. + + If a net is given, the elements are derived from the component list. :param junction: if True, return junction table name :type junction: bool, default True @@ -174,7 +171,6 @@ def pp_elements(junction=True, include_node_elements=True, include_branch_elemen :return: pp_elms - set of table names for the desired element types :rtype: set """ - pp_elms = {"junction"} if junction else set() pp_elms |= set([el[0] for el in element_junction_tuples( include_node_elements, include_branch_elements, include_res_elements, net)]) @@ -182,9 +178,7 @@ def pp_elements(junction=True, include_node_elements=True, include_branch_elemen def reindex_junctions(net, junction_lookup): - """ - Changes the index of net.junction and considers the new junction indices in all other - pandapipes element tables. + """Changes the index of net.junction and considers the new junction indices in all other pandapipes element tables. :param net: pandapipes network :type net: pandapipesNet @@ -194,14 +188,12 @@ def reindex_junctions(net, junction_lookup): :return: junction_lookup - the finally reindexed junction lookup (with corrections if necessary) :rtype: dict """ - junction_lookup = reindex_elements(net, "junction", lookup=junction_lookup) return junction_lookup def reindex_pipes(net, pipe_lookup): - """ - Changes the index of net.pipe and considers the new pipe indices in pandapipes valve table. + """Changes the index of net.pipe and considers the new pipe indices in pandapipes valve table. :param net: pandapipes network :type net: pandapipesNet @@ -211,14 +203,12 @@ def reindex_pipes(net, pipe_lookup): :return: pipe_lookup - the finally reindexed pipe lookup (with corrections if necessary) :rtype: dict """ - pipe_lookup = reindex_elements(net, "pipe", lookup=pipe_lookup) return pipe_lookup def reindex_elements(net, element, lookup): - """ - Changes the index of net[element]. + """Changes the index of net[element]. :param net: pandapipes network :type net: pandapipesNet @@ -228,7 +218,6 @@ def reindex_elements(net, element, lookup): :type lookup: dict :return: No output. """ - if element not in net: return lookup not_fitting_lookup_keys = set(lookup.keys()) - set(net[element].index) @@ -270,9 +259,7 @@ def reindex_elements(net, element, lookup): return lookup def create_continuous_junction_index(net, start=0, store_old_index=False): - """ - Creates a continuous junction index starting at 'start' and replaces all - references of old indices by the new ones. + """Creates a continuous junction index starting at 'start' and replaces all references of old indices by the new ones. :param net: pandapipes network :type net: pandapipesNet @@ -287,9 +274,7 @@ def create_continuous_junction_index(net, start=0, store_old_index=False): return junction_lookup def create_continuous_element_index(net, element, start=0, store_old_index=False): - """ - Creates a continuous element index starting at 'start' and replaces all - references of old indices by the new ones. + """Creates a continuous element index starting at 'start' and replaces all references of old indices by the new ones. :param net: pandapipes network :type net: pandapipesNet @@ -316,9 +301,7 @@ def create_continuous_element_index(net, element, start=0, store_old_index=False return lookup def create_continuous_elements_index(net, start=0, add_df_to_reindex=None, store_old_index=False): - """ - Creating a continuous index for all the elements and replaces all references - of old indices by the new ones. + """Creating a continuous index for all the elements and replaces all references of old indices by the new ones. :param net: pandapipes network with unodered indices :type net: pandapipesNet @@ -348,9 +331,9 @@ def create_continuous_elements_index(net, start=0, add_df_to_reindex=None, store def fuse_junctions(net, j1, j2, drop=True): - """ - Reroutes any connections to junctions in j2 to the given junction j1. Additionally drops the - junctions j2, if drop=True (default). + """Reroutes any connections to junctions in j2 to the given junction j1. + + Additionally drops the junctions j2, if drop=True (default). :param net: pandapipes network :type net: pandapipesNet @@ -378,10 +361,7 @@ def fuse_junctions(net, j1, j2, drop=True): def select_subnet(net, junctions, include_results=False, keep_everything_else=False, remove_internals=True, remove_unused_components=False): - """ - Selects a subnet by a list of junction indices and returns a net with all components connected - to them. - """ + """Selects a subnet by a list of junction indices and returns a net with all components connected to them.""" junctions = list(junctions) if keep_everything_else: @@ -442,9 +422,7 @@ def remove_empty_components(net): def drop_junctions(net, junctions, drop_elements=True): - """ - Drops specified junctions, their junction_geodata and by default drops all elements connected to - them as well. + """Drops specified junctions, their junction_geodata and by default drops all elements connected to them as well. :param net: pandapipes network :type net: pandapipesNet @@ -464,8 +442,7 @@ def drop_junctions(net, junctions, drop_elements=True): def drop_elements_at_junctions(net, junctions, node_elements=True, branch_elements=True): - """ - drop elements connected to given junctions + """Drop elements connected to given junctions. :param net: pandapipes network :type net: pandapipesNet @@ -499,8 +476,7 @@ def drop_elements_at_junctions(net, junctions, node_elements=True, branch_elemen def drop_pipes(net, pipes): - """ - Deletes all pipes and their geodata in the given list of indices. + """Deletes all pipes and their geodata in the given list of indices. :param net: pandapipes network :type net: pandapipesNet @@ -546,14 +522,13 @@ def check_pressure_controllability(net, to_junction, controlled_junction): # logger.info("dropped %d %s elements with %d switches" % (len(trafos), table, num_switches)) -pit_types = {TYPE_P: "P", TYPE_L: "L", TYPE_T: "T", TYPE_PC: "PC"} -int_cols = ["FROM_NODE", "TO_NODE", "ELEMENT_IDX", "EXT_GRID_OCCURENCE", "EXT_GRID_OCCURENCE_T"] +pit_types = {IdxNode.P: "P", IdxNode.L: "L", IdxNode.T: "T", IdxNode.PC: "PC"} +int_cols = ["FROM_NODE", "TO_NODE", "ELEMENT_IDX"] bool_cols = ["ACTIVE"] def get_pit_lookup(pit_type="node"): - """ - Retrieve a lookup for "indices" and "types" from the idx_branch or idx_node files. + """Retrieve a lookup for "indices" and "types" from the idx_branch or idx_node files. :param pit_type: the pit for which the lookup is generated ("branch" or "node") :type pit_type: str, default "node" @@ -581,8 +556,7 @@ def get_pit_lookup(pit_type="node"): def get_internal_tables_pandas(net, convert_types=True): - """ - Convert the internal structure (pit) for nodes and branches into readable pandas DataFrames. + """Convert the internal structure (pit) for nodes and branches into readable pandas DataFrames. :param net: pandapipes network :type net: pandapipesNet @@ -598,8 +572,8 @@ def get_internal_tables_pandas(net, convert_types=True): branch_pit = net["_pit"]["branch"] node_pit = net["_pit"]["node"] - missing_nodes = node_pit.shape[1] - node_cols - missing_branches = branch_pit.shape[1] - branch_cols + missing_nodes = node_pit.shape[1] - IdxNode.node_cols + missing_branches = branch_pit.shape[1] - IdxBranch.branch_cols if missing_nodes > 0: logger.warning("%d node pit entries are missing. Please verify the correctness of the " diff --git a/src/pandapipes/topology/create_graph.py b/src/pandapipes/topology/create_graph.py index 136f17b65..fdf93f6ab 100644 --- a/src/pandapipes/topology/create_graph.py +++ b/src/pandapipes/topology/create_graph.py @@ -46,10 +46,11 @@ def create_nxgraph(net, include_pipes=True, respect_status_pipes=True, weighting_heat_consumers=None, respect_status_junctions=True, nogojunctions=None, notravjunctions=None, multi=True, respect_status_branches_all=None, **kwargs): - """ - Converts a pandapipes network into a NetworkX graph, which is a simplified representation of a - network's topology, reduced to nodes and edges. Junctions are being represented by nodes, edges - represent physical connections between junctions (typically pipes or pumps). + """Converts a pandapipes network into a NetworkX graph. + + This is a simplified representation of a network's topology, reduced to nodes and edges. + Junctions are being represented by nodes, edges represent physical connections between + junctions (typically pipes or pumps). :param net: The pandapipes network to be converted :type net: pandapipesNet diff --git a/src/pandapipes/topology/graph_searches.py b/src/pandapipes/topology/graph_searches.py index 1b022bce5..8c65bfb29 100644 --- a/src/pandapipes/topology/graph_searches.py +++ b/src/pandapipes/topology/graph_searches.py @@ -10,30 +10,30 @@ def calc_distance_to_junction(net, junction, notravjunctions=None, nogojunctions=None, weight="weight"): - """ - Calculates the shortest distance between a source junction and all junctions connected to it. + """Calculates the shortest distance between a source junction and all junctions connected to it. - INPUT: + INPUT: **net** (pandapipesNet) - Variable that contains a pandapipes network. **junction** (integer) - Index of the source junction. - OPTIONAL: + OPTIONAL: **nogojunctions** (integer/list, None) - nogojunctions are not being considered **notravjunctions** (integer/list, None) - lines connected to these junctions are not being considered **weight** (string, None) – Edge data key corresponding to the edge weight - OUTPUT: + OUTPUT: **dist** - Returns a pandas series with containing all distances to the source junction in km. If weight=None dist is the topological distance (int). - EXAMPLE: - import pandapipes.topology as top + Example + ------- + import pandapipes.topology as top - dist = top.calc_distance_to_junction(net, 5) + dist = top.calc_distance_to_junction(net, 5) """ g = create_nxgraph(net, nogojunctions=nogojunctions, @@ -44,31 +44,30 @@ def calc_distance_to_junction(net, junction, notravjunctions=None, nogojunctions def calc_minimum_distance_to_junctions(net, junctions, notravjunctions=None, nogojunctions=None, weight="weight"): - """ - Calculates the shortest distance between multiple source junctions and all junctions connected \ - to it. + """Calculates the shortest distance between multiple source junctions and all junctions connected to it. - INPUT: + INPUT: **net** (pandapipesNet) - Variable that contains a pandapipes network. **junction** (integer) - Index of the source junction. - OPTIONAL: + OPTIONAL: **nogojunctions** (integer/list, None) - nogojunctions are not being considered **notravjunctions** (integer/list, None) - lines connected to these junctions are not being considered **weight** (string, None) – Edge data key corresponding to the edge weight - OUTPUT: + OUTPUT: **dist** - Returns a pandas series with containing all distances to the source junction in km. If weight=None dist is the topological distance (int). - EXAMPLE: - import pandapipes.topology as top + Example + ------- + import pandapipes.topology as top - dist = top.calc_distance_to_junction(net, 5) + dist = top.calc_distance_to_junction(net, 5) """ mg = create_nxgraph(net, notravjunctions=notravjunctions, @@ -80,16 +79,15 @@ def calc_minimum_distance_to_junctions(net, junctions, notravjunctions=None, nog def calc_distance_to_junctions(net, junctions, respect_status_valves=True, notravjunctions=None, nogojunctions=None, weight="weight"): - """ - Calculates the shortest distance between every source junction and all junctions connected to it. + r"""Calculates the shortest distance between every source junction and all junctions connected to it. - INPUT: + INPUT: **net** (pandapipesNet) - Variable that contains a pandapipes network. **junctions** (integer) - Index of the source junctions. - OPTIONAL: + OPTIONAL: **respect_status_valve** (boolean, True) - Flag whether the "opened" column shall be considered and out\ of service valves neglected. @@ -99,14 +97,15 @@ def calc_distance_to_junctions(net, junctions, respect_status_valves=True, notra considered **weight** (string, None) – Edge data key corresponding to the edge weight - OUTPUT: + OUTPUT: **dist** - Returns a pandas series with containing all distances to the source junction in km. If weight=None dist is the topological distance (int). - EXAMPLE: - import pandapipes.topology as top + Example + ------- + import pandapipes.topology as top - dist = top.calc_distance_to_junctions(net, [5, 6]) + dist = top.calc_distance_to_junctions(net, [5, 6]) """ g = create_nxgraph(net, respect_status_valves=respect_status_valves, nogojunctions=nogojunctions, @@ -116,13 +115,12 @@ def calc_distance_to_junctions(net, junctions, respect_status_valves=True, notra def unsupplied_junctions(net, mg=None, slacks=None, respect_valves=True): - """ - Finds junctions, that are not connected to an external grid. + """Finds junctions, that are not connected to an external grid. - INPUT: + INPUT: **net** (pandapipesNet) - variable that contains a pandapipes network - OPTIONAL: + OPTIONAL: **mg** (NetworkX graph) - NetworkX Graph or MultiGraph that represents a pandapipes network. **in_service_only** (boolean, False) - Defines whether only in service junctions should be @@ -134,15 +132,15 @@ def unsupplied_junctions(net, mg=None, slacks=None, respect_valves=True): **respect_valves** (boolean, True) - Fixes how to consider valves - only in case of no given mg. - OUTPUT: + OUTPUT: **uj** (set) - unsupplied junctions - EXAMPLE: - import pandapipes.topology as top + Example + ------- + import pandapipes.topology as top - top.unsupplied_junctions(net) + top.unsupplied_junctions(net) """ - mg = mg or create_nxgraph(net, respect_status_valves=respect_valves) if slacks is None: slacks = set(net.ext_grid[net.ext_grid.in_service].junction.values) @@ -154,10 +152,9 @@ def unsupplied_junctions(net, mg=None, slacks=None, respect_valves=True): def elements_on_path(mg, path, element="pipe", check_element_validity=True): - """ - Finds all elements that connect a given path of junctions. + """Finds all elements that connect a given path of junctions. - INPUT: + INPUT: **mg** (NetworkX graph) - NetworkX Graph or MultiGraph that represents a pandapipes network. **path** (list) - List of connected junctions. @@ -166,16 +163,17 @@ def elements_on_path(mg, path, element="pipe", check_element_validity=True): **check_element_validity** (boolean, True) - Check if element is a valid pandapipes table_name - OUTPUT: + OUTPUT: **elements** (list) - Returns a list of all elements on the path. - EXAMPLE: - import topology as top + Example + ------- + import topology as top - mg = top.create_nxgraph(net) - elements = top.elements_on_path(mg, [4, 5, 6]) + mg = top.create_nxgraph(net) + elements = top.elements_on_path(mg, [4, 5, 6]) - """ + """ if check_element_validity: table_names = get_all_branch_component_table_names() if element not in table_names: diff --git a/src/pandapipes/topology/topology_toolbox.py b/src/pandapipes/topology/topology_toolbox.py index 165b9117e..f73cb575a 100644 --- a/src/pandapipes/topology/topology_toolbox.py +++ b/src/pandapipes/topology/topology_toolbox.py @@ -7,8 +7,7 @@ def get_all_branch_component_models(): - """ - Get all models of available branch components + """Get all models of available branch components. :return: branch model :rtype: list @@ -32,8 +31,7 @@ def get_all_subclasses(cls): def get_all_branch_component_table_names(): - """ - Get all table names of available branch components + """Get all table names of available branch components. :return: table names :rtype: list diff --git a/tutorials/component_architecture_tutorial.ipynb b/tutorials/component_architecture_tutorial.ipynb new file mode 100644 index 000000000..f10f3815d --- /dev/null +++ b/tutorials/component_architecture_tutorial.ipynb @@ -0,0 +1,598 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# pandapipes component architecture: from `build_system_matrix` to `register_*`\n", + "\n", + "This is a developer-facing tutorial, not an end-user \"how to build a network\" tutorial - it\n", + "explains how a component (`ExtGrid`, `Pipe`, `CircPump`, ...) actually turns into rows and columns\n", + "of the sparse Newton system that `pipeflow()` solves. `ExtGrid` is used as the running example\n", + "throughout, because it happens to touch nearly every piece of the machinery: both PIT-write\n", + "phases, all three `PitWriteMode`s, both `EqWriteMode`s that matter in practice, and a\n", + "cross-component coordination pattern with `CircPump` that's worth understanding in its own right.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Two arrays, one net: `node_pit` and `branch_pit`\n", + "\n", + "Before any equations exist, pandapipes builds two big NumPy arrays per solve:\n", + "\n", + "- `node_pit` - one row per junction, columns defined in `idx_node.py`'s `IdxNode` (e.g.\n", + " `IdxNode.PINIT`, `IdxNode.NODE_TYPE`, ...)\n", + "- `branch_pit` - one row per branch element (pipe, valve, pump, ...), columns defined in\n", + " `idx_branch.py`'s `IdxBranch`\n", + "\n", + "Every component reads and writes these through **named columns**, never raw integers - e.g.\n", + "`node_pit[:, IdxNode.PINIT]`, not `node_pit[:, 10]`. Let's build a tiny network and look at the\n", + "raw array to make this concrete." + ] + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "start_time": "2026-08-18T15:09:29.291313Z" + } + }, + "source": [ + "import numpy as np\n", + "import pandapipes\n", + "from pandapipes.idx_node import IdxNode\n", + "\n", + "net = pandapipes.create_empty_network(fluid=\"water\")\n", + "j0 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=285.15)\n", + "j1 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=285.15)\n", + "pandapipes.create_pipe_from_parameters(net, j0, j1, length_km=1.0, k_mm=0.1, inner_diameter_mm=100)\n", + "pandapipes.create_ext_grid(net, j0, p_bar=5, t_k=285.15, type=\"pt\")\n", + "pandapipes.create_sink(net, j1, mdot_kg_per_s=1.0)\n", + "\n", + "pandapipes.pipeflow(net)\n", + "\n", + "node_pit = net[\"_pit\"][\"node\"]\n", + "print(\"node_pit shape:\", node_pit.shape, \" (node_cols =\", IdxNode.node_cols, \")\")\n", + "print(\"PINIT column: \", node_pit[:, IdxNode.PINIT])\n", + "print(\"NODE_TYPE column: \", node_pit[:, IdxNode.NODE_TYPE], \" (IdxNode.P =\", IdxNode.P, \")\")\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`IdxNode`/`IdxBranch` are plain integer constants (via a small `IndexMeta` metaclass) - they\n", + "don't own any behavior, they're purely a naming scheme for array columns.\n", + "\n", + "Getting a component's data into these arrays, and then turning that data into equations, happens\n", + "in **two separate phases**, each with its own registration mechanism." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Phase 1 - filling the PIT: `register_pit_node_entries`\n", + "\n", + "Before the Newton loop starts, every component's `register_pit_*_entries` classmethod runs once,\n", + "writing its input data into `node_pit`/`branch_pit`. This is where `net.ext_grid.p_bar` becomes\n", + "`node_pit[some_row, IdxNode.PINIT]`.\n", + "\n", + "Components don't write to the array directly. They build a `PitEntries` object (COO-style: rows,\n", + "cols, data) and hand it to a `PitRegistry` via `registry.add(...)` or `registry.add_override(...)`.\n", + "The registry defers the actual array write until every component has registered, then applies them\n", + "all via `PitRegistry.apply()` (`pf/system_index.py`).\n", + "\n", + "### `ExtGrid.register_pit_node_entries` (`component_models/ext_grid_component.py`)\n", + "\n", + "```python\n", + "@classmethod\n", + "def register_pit_node_entries(cls, net, node_pit, registry) -> None:\n", + " ext_grids = net[cls.table_name()]\n", + " ext_grids = ext_grids[ext_grids[cls.active_identifier()].values]\n", + " if not len(ext_grids):\n", + " return\n", + "\n", + " junction = ext_grids[cls.get_node_col()].values\n", + " types = ext_grids.type.values\n", + " junction_lookup = get_lookup(net, \"node\", \"index\")[cls.get_connected_node_type().table_name()]\n", + " mask_p = np.isin(types, [\"p\", \"pt\"])\n", + " mask_t = np.isin(types, [\"t\", \"pt\"])\n", + " index_p = junction_lookup[junction[mask_p]]\n", + " index_t = junction_lookup[junction[mask_t]]\n", + "\n", + " registry.add_override(PitEntries(*build_pit_entries(\n", + " index_p,\n", + " [IdxNode.PINIT, IdxNode.NODE_TYPE],\n", + " [ext_grids.p_bar.values[mask_p], float(IdxNode.P)],\n", + " ), mode=PitWriteMode.MEAN))\n", + " registry.add_override(PitEntries(*build_pit_entries(\n", + " index_t,\n", + " [IdxNode.TINIT, IdxNode.NODE_TYPE_T],\n", + " [ext_grids.t_k.values[mask_t], float(IdxNode.T)],\n", + " ), mode=PitWriteMode.MEAN))\n", + " registry.add_override(PitEntries(*build_pit_entries(\n", + " index_p,\n", + " [IdxNode.VAR_MASS_SLACK],\n", + " [1.]),\n", + " mode=PitWriteMode.UNIQUE))\n", + "```\n", + "\n", + "`junction_lookup` translates a junction-table index into a `node_pit` row index (junctions from\n", + "different tables all share one `node_pit`, so this mapping is needed even in the simple case).\n", + "`build_pit_entries(rows, cols, data)` just fans `rows` out against every `(col, data)` pair to\n", + "build the flat COO arrays `PitEntries` wants." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `PitWriteMode` - what happens when a cell gets written more than once\n", + "\n", + "```python\n", + "class PitWriteMode(str, Enum):\n", + " UNIQUE = \"unique\" # exclusive write - conflict check on registration, direct assignment\n", + " ADDITIVE = \"additive\" # values accumulated with np.add.at\n", + " MEAN = \"mean\" # mean of all values written to the same (row, col) position\n", + "```\n", + "\n", + "- **`MEAN`** for `PINIT`/`NODE_TYPE`: if two ext_grids sit at the same junction with the same\n", + " `p_bar`, both write the same value - averaging is a no-op. If they *disagree*, `MEAN` degrades\n", + " gracefully (averages them) instead of raising - a deliberate choice for this cell, not an\n", + " oversight.\n", + "- **`UNIQUE`** for `VAR_MASS_SLACK`: this is a plain \"yes/no\" flag (\"is there a *real* ext_grid at\n", + " this junction\"), not a count to average or accumulate - `UNIQUE` matches that intent directly\n", + " (single, direct write).\n", + "- **`ADDITIVE`** is for genuine counters/sums.\n", + "\n", + "`add()` vs. `add_override()` (both on `PitRegistry`) determine write **order**, not write\n", + "semantics: everything in `add()` (the \"normal\" bucket) is applied before everything in\n", + "`add_override()` (the \"overrides\" bucket), so overrides win when both target the same\n", + "`(row, col)` under `UNIQUE`. `ExtGrid` uses `add_override` throughout because a junction's\n", + "`PINIT`/`NODE_TYPE` default (plain `Junction.register_pit_node_entries`, written via `add()`) must\n", + "yield to whatever an ext_grid dictates for that node.\n", + "\n", + "Let's see `MEAN` in action - two ext_grids at the same junction, splitting one sink's demand:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "net2 = pandapipes.create_empty_network(fluid=\"water\")\n", + "j0 = pandapipes.create_junction(net2, pn_bar=5, tfluid_k=285.15)\n", + "pandapipes.create_ext_grid(net2, j0, p_bar=5, t_k=285.15, type=\"pt\")\n", + "pandapipes.create_ext_grid(net2, j0, p_bar=5, t_k=285.15, type=\"pt\")\n", + "pandapipes.create_sink(net2, j0, mdot_kg_per_s=5.0)\n", + "\n", + "pandapipes.pipeflow(net2)\n", + "print(net2.res_ext_grid)\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Sidebar - a counter that turned out to be dead weight.** `ExtGrid` used to also write\n", + "> `IdxNode.EXT_GRID_OCCURENCE` (`ADDITIVE`, counting how many ext_grid rows share a junction), so\n", + "> `extract_results` could divide a shared `MDOTSLACKINIT` value by that count to report each\n", + "> ext_grid's individual share. Once `register_hydraulic_equations` was rewritten to register once\n", + "> **per ext_grid row** instead of once per de-duplicated junction (see below), the linear system\n", + "> itself produces each ext_grid's own share directly - the count, and the division, became\n", + "> unnecessary and were deleted along with `EXT_GRID_OCCURENCE`/`EXT_GRID_OCCURENCE_T`. Worth\n", + "> remembering next time you touch code like this: a \"helper\" column can silently outlive the one\n", + "> thing that needed it - check whether every column you're about to preserve is still actually\n", + "> *read* anywhere, not just written." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. `HydraulicSystemIndex` - where a variable \"lives\" in the big matrix\n", + "\n", + "Once every component's PIT data is in place (and, for hydraulics, connectivity has been resolved\n", + "and the pit reduced to only the active rows), the Newton loop starts. Every iteration,\n", + "`solve_hydraulics` (`pf/calculation.py`) builds a `HydraulicSystemIndex` for the current pit:\n", + "\n", + "```python\n", + "class HydraulicSystemIndex(BaseSystemIndex):\n", + " \"\"\"\n", + " Layout (columns = rows in square system):\n", + " 0 .. len_n-1 PINIT / NODE pressure / node mass-balance\n", + " len_n .. len_n+len_b-1 MDOTINIT / BRANCH mass flow / branch momentum\n", + " len_n+len_b .. ... MDOTSLACKINIT / SLACK slack-mass variables (P-type nodes)\n", + " \"\"\"\n", + " def __init__(self, node_pit, branch_pit):\n", + " self.slack_nodes = np.where(node_pit[:, IdxNode.NODE_TYPE] == IdxNode.P)[0].astype(np.int32)\n", + " len_n, len_b, len_s = len(node_pit), len(branch_pit), len(self.slack_nodes)\n", + " slack_vals = np.arange(len_s, dtype=np.int32) + len_n + len_b\n", + "\n", + " self._register(HydVarEq.PINIT, np.arange(len_n))\n", + " self._register(HydVarEq.MDOTINIT, np.arange(len_b) + len_n)\n", + " self._register_sparse(HydVarEq.MDOTSLACKINIT, len_n, self.slack_nodes, slack_vals)\n", + "\n", + " self._register(HydVarEq.NODE, np.arange(len_n))\n", + " self._register(HydVarEq.BRANCH, np.arange(len_b) + len_n)\n", + " self._register_sparse(HydVarEq.SLACK, len_n, self.slack_nodes, slack_vals)\n", + "```\n", + "\n", + "Every hydraulic unknown/equation type gets a contiguous block of matrix indices. A component asks\n", + "for \"the column for `PINIT` at these node rows\" via `sys_idx.idx(HydVarEq.PINIT, some_node_indices)`\n", + "- it never computes a matrix index by hand.\n", + "\n", + "`MDOTSLACKINIT`/`SLACK` only exist at P-type (slack) nodes, a small subset of all nodes - but\n", + "`_register_sparse` still sizes their block like the **full** node array, with `-1` at every\n", + "non-slack position:\n", + "\n", + "```python\n", + "def _register_sparse(self, key, full_size, node_indices, values):\n", + " arr = np.full(full_size, -1, dtype=np.int32)\n", + " arr[node_indices] = values\n", + " self._blocks[self._block_key(key)] = arr\n", + " if len(values):\n", + " self._size = max(self._size, int(values.max()) + 1)\n", + "```\n", + "\n", + "That means `sys_idx.idx(HydVarEq.MDOTSLACKINIT, eg_nodes)` works with raw node indices exactly like\n", + "`PINIT` does - no separate \"rank within the slack subset\" translation needed by callers. (This used\n", + "to require `np.searchsorted(slack_nodes, eg_nodes)` in every caller; folding the sparse layout into\n", + "`_register_sparse` once removed that boilerplate everywhere it was needed.)\n", + "\n", + "One consequence worth knowing if you call `sys_idx.idx(key)` **without** a subset: for a sparse\n", + "block, that returns the raw, `-1`-padded, full-`len_n`-sized array, not a compact \"just the slack\n", + "values\" array - anyone doing this (e.g. applying the Newton update for *all* slack nodes at once)\n", + "must pass `slack_nodes` explicitly as the subset, not rely on the bare block.\n", + "\n", + "`_block_key` exists purely so a subclass can namespace keys without re-implementing `idx`/\n", + "`_register`/`_register_sparse` three times: `combined_pipeflow`'s `HydThermSystemIndex` combines\n", + "hydraulic and thermal blocks in one system, where `HydVarEq.NODE` and `ThermVarEq.NODE` are equal\n", + "as plain strings (both enums subclass `str`) and would otherwise collide as dict keys. It overrides\n", + "just `_block_key` to key on `(type(var), var)` instead, and every read/write path picks that up\n", + "automatically." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Phase 2 - the linear system: `register_hydraulic_equations`\n", + "\n", + "`register_hydraulic_equations` runs on every component, every Newton iteration, collecting linear\n", + "contributions (Jacobian entries + load/residual values) into a `ComponentRegistry`, which then gets\n", + "assembled into one sparse matrix and solved for the whole net.\n", + "\n", + "### `ExtGrid.register_hydraulic_equations`\n", + "\n", + "```python\n", + "@classmethod\n", + "def register_hydraulic_equations(cls, net, branch_pit, node_pit, sys_idx, registry):\n", + " # register only for nodes that actually have an active ext_grid row - NOT every P-type\n", + " # node in the system (a circ_pump also marks its own flow junction as NODE_TYPE=P purely\n", + " # to anchor a pressure reference; that node is none of ExtGrid's business)\n", + " ext_grids = net[cls.table_name()]\n", + " ext_grids = ext_grids[ext_grids[cls.active_identifier()].values]\n", + " p_grids = ext_grids[np.isin(ext_grids.type.values, [\"p\", \"pt\"])]\n", + " if not len(p_grids):\n", + " return\n", + "\n", + " junction_lookup = get_lookup(net, \"node\", \"index_active_hydraulics\")[\n", + " cls.get_connected_node_type().table_name()]\n", + " # one entry per ext_grid ROW - deliberately NOT deduplicated by node\n", + " eg_nodes = junction_lookup[p_grids[cls.get_node_col()].values].astype(np.int32)\n", + " eg_nodes = eg_nodes[eg_nodes != -1] # drop disconnected\n", + " if not len(eg_nodes):\n", + " return\n", + "\n", + " p_col = sys_idx.idx(HydVarEq.PINIT, eg_nodes)\n", + " slack_col = sys_idx.idx(HydVarEq.MDOTSLACKINIT, eg_nodes)\n", + " slack_eq = sys_idx.idx(HydVarEq.SLACK, eg_nodes)\n", + " n_eq = sys_idx.idx(HydVarEq.NODE, eg_nodes)\n", + "\n", + " # SLACK row: pressure fix, δPINIT = 0 - MEAN (see below)\n", + " registry.add(ComponentEquations(\n", + " rows=slack_eq.astype(np.int32), cols=p_col.astype(np.int32),\n", + " data=np.ones(len(slack_eq), dtype=np.float64),\n", + " load_rows=slack_eq.astype(np.int32), load_data=np.zeros(len(slack_eq), dtype=np.float64),\n", + " mode=EqWriteMode.MEAN,\n", + " ))\n", + "\n", + " # NODE row: MDOTSLACKINIT joins the mass balance, free to absorb residual - ADDITIVE (default)\n", + " registry.add(ComponentEquations(\n", + " rows=n_eq.astype(np.int32), cols=slack_col.astype(np.int32),\n", + " data=np.ones(len(n_eq), dtype=np.float64),\n", + " load_rows=n_eq.astype(np.int32),\n", + " load_data=node_pit[eg_nodes, IdxNode.MDOTSLACKINIT].astype(np.float64),\n", + " ))\n", + "```\n", + "\n", + "Two non-obvious things about this code, both learned the hard way while building it:\n", + "\n", + "**(a) `\"index_active_hydraulics\"`, never the plain `\"index\"` lookup, in this phase.**\n", + "`register_pit_node_entries` runs *before* connectivity reduction, against the full node array, so\n", + "the plain `\"index\"` lookup is correct there. `register_hydraulic_equations` runs *after*\n", + "reduction, against the smaller *active* pit - using the plain lookup here indexes into the\n", + "wrong-sized array and either crashes outright (`IndexError: index 9 is out of bounds for axis 0\n", + "with size 6`) or, worse, silently resolves to the wrong node. Every hydraulic-phase lookup needs\n", + "the `\"index_active_hydraulics\"` variant (there's a matching `\"index_active_heat_transfer\"` for\n", + "`register_thermal_equations`). `-1` in the lookup means \"disconnected, dropped from the active\n", + "pit\" - filter those out (`eg_nodes[eg_nodes != -1]`) rather than passing them through.\n", + "\n", + "**(b) Registering once per ext_grid *row*, not once per de-duplicated node, is what makes \"multiple\n", + "ext_grids share one junction\" work correctly without any extra code.** If two ext_grids sit at the\n", + "same junction, this method's row-building logic runs its per-row arrays with that junction's node\n", + "index appearing *twice*, and both entries target the *same* matrix row/column. For the `SLACK`\n", + "row, two registrations land on the same `(row, col)` - which is exactly why it must be `MEAN`, not\n", + "`UNIQUE`: `UNIQUE` conflict-checks new rows against rows *other* registered entries already claim,\n", + "and duplicate rows landing there would raise. For the `NODE` row (`ADDITIVE`, the default), two\n", + "contributions to the same `(row, col)` don't conflict - they *sum* when the sparse matrix is built\n", + "(`scipy.sparse.csr_matrix` sums duplicate COO entries), so the row's coefficient on\n", + "`MDOTSLACKINIT` becomes `N` for `N` co-located ext_grids. Given the node's mass balance forces\n", + "`N * MDOTSLACKINIT = (total residual left over by everything else)`, Newton solves directly for\n", + "each ext_grid's own share - `extract_results` just reads `node_pit[eg_nodes, MDOTSLACKINIT]`\n", + "per row, no separate division needed. That's exactly the `net2.res_ext_grid` output you saw\n", + "above: `-2.5` for each of the two co-located ext_grids, straight out of the solve." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Assembly and solve\n", + "\n", + "`solve_hydraulics` (`pf/calculation.py`) ties every component's registrations into one solve:\n", + "\n", + "```python\n", + "sys_idx = HydraulicSystemIndex(node_pit, branch_pit)\n", + "eq_registry = ComponentRegistry()\n", + "\n", + "for comp in net['component_list']:\n", + " comp.register_hydraulic_equations(net, branch_pit, node_pit, sys_idx, eq_registry)\n", + "\n", + "sz = sys_idx.size()\n", + "rows, cols, data, epsilon = eq_registry.assemble(sz)\n", + "jacobian = csr_matrix((data, (rows, cols)), shape=(sz, sz))\n", + "\n", + "x = spsolve(jacobian, epsilon)\n", + "\n", + "branch_pit[:, IdxBranch.MDOTINIT] -= x[mdot_idx] * options[\"alpha\"]\n", + "node_pit[:, IdxNode.PINIT] -= x[p_idx] * options[\"alpha\"]\n", + "node_pit[slack_nodes, IdxNode.MDOTSLACKINIT] -= x[msl_idx]\n", + "```\n", + "\n", + "`ComponentRegistry.assemble()` (`pf/system_index.py`) is where `EqWriteMode` actually gets acted\n", + "on: `UNIQUE`-tagged entries registered via `add_override` strip any competing `ADDITIVE`\n", + "contributions to the same row before concatenation; `MEAN`-tagged entries (from either bucket) are\n", + "pulled into a separate pool, grouped by `(row, col)`, averaged (Jacobian data summed then divided\n", + "by count; load values NaN-filtered, summed, divided by count), and everything else is just\n", + "concatenated and left for `csr_matrix` to sum on construction.\n", + "\n", + "Nothing here is component-specific - `solve_hydraulics` doesn't know or care that `ExtGrid` exists;\n", + "it only knows \"call `register_hydraulic_equations` on everything, assemble what comes back, solve.\"\n", + "That's the entire point of the refactor away from the old `build_system_matrix.py` pattern: each\n", + "component owns its own contribution to the system, described declaratively (rows/cols/data +\n", + "write-mode), instead of every component needing to know how to poke directly into one big,\n", + "centrally-assembled matrix-building function." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Cross-component coordination without tight coupling: the `VAR_MASS_SLACK` case study\n", + "\n", + "`ExtGrid` isn't the only component that marks a node `NODE_TYPE = P`. `CircPump` (both\n", + "`CirculationPumpMass` and `CirculationPumpPressure`, via the shared `CirculationPump` base class in\n", + "`abstract_models/circulation_pump.py`) marks its own flow junction `NODE_TYPE = P` too - not because\n", + "it's an external mass source, but purely to **anchor an absolute pressure reference**: without at\n", + "least one such anchor somewhere in the network, pressure is only ever defined up to an arbitrary\n", + "additive constant (branch/momentum equations only constrain pressure *differences*), and the\n", + "system would be singular.\n", + "\n", + "That distinction - \"P-type node with a genuine external mass connection\" vs. \"P-type node that's\n", + "just a pressure anchor\" - isn't visible from `NODE_TYPE` alone. Both look identical to\n", + "`HydraulicSystemIndex`, which builds `slack_nodes` purely from `NODE_TYPE == P` regardless of which\n", + "component set it. Left alone, a circ-pump-only node would get exactly the same treatment as a real\n", + "ext_grid: `MDOTSLACKINIT` free to absorb whatever residual mass the rest of the network leaves\n", + "over - silently hiding a genuine supply/demand mismatch in a network that has no real external\n", + "connection to explain it.\n", + "\n", + "Let's see this concretely: a circulation pump only (no ext_grid at all), with a sink and source\n", + "that deliberately don't balance (3 kg/s drawn, only 2 kg/s supplied):" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from pandapipes.pf.pipeflow_setup import PipeflowNotConverged\n", + "\n", + "def build_circ_pump_net(sink, source):\n", + " net = pandapipes.create_empty_network(\"net\", add_stdtypes=False)\n", + " j1 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15)\n", + " j2 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15)\n", + " j3 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15)\n", + " j4 = pandapipes.create_junction(net, pn_bar=5, tfluid_k=283.15)\n", + " pandapipes.create_pipe_from_parameters(net, j1, j2, k_mm=1., length_km=0.4338, inner_diameter_mm=102.2)\n", + " pandapipes.create_pipe_from_parameters(net, j3, j4, k_mm=1., length_km=0.2637, inner_diameter_mm=102.2)\n", + " pandapipes.create_circ_pump_const_mass_flow(net, j4, j1, 5, 5, 300, type=\"pt\")\n", + " pandapipes.create_heat_exchanger(net, j2, j3, qext_w=200000, inner_diameter_mm=100)\n", + " pandapipes.create_sink(net, j1, sink)\n", + " pandapipes.create_source(net, j4, source)\n", + " pandapipes.create_fluid_from_lib(net, \"water\", overwrite=True)\n", + " return net, j1\n", + "\n", + "# UNBALANCED: sink (3 kg/s) != source (2 kg/s), and there is no real ext_grid anywhere\n", + "net_bad, j1 = build_circ_pump_net(sink=3, source=2)\n", + "try:\n", + " pandapipes.pipeflow(net_bad, mode=\"sequential\")\n", + " print(\"converged:\", net_bad.converged, \"<- this would be a silent bug\")\n", + "except PipeflowNotConverged:\n", + " print(\"Correctly raised PipeflowNotConverged: the network is genuinely inconsistent\")\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# BALANCED: sink == source -> should converge, and MDOTSLACKINIT at the pump's anchor node\n", + "# should be exactly 0 (it's not a real mass source, just a pressure reference)\n", + "from pandapipes.idx_node import IdxNode as IdxNodeCheck\n", + "\n", + "net_ok, j1_ok = build_circ_pump_net(sink=2, source=2)\n", + "pandapipes.pipeflow(net_ok, mode=\"sequential\")\n", + "print(\"converged:\", net_ok.converged)\n", + "print(\"MDOTSLACKINIT at the circ_pump's anchor node (expect 0.0):\",\n", + " net_ok[\"_pit\"][\"node\"][j1_ok, IdxNodeCheck.MDOTSLACKINIT])\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`VAR_MASS_SLACK` is the flag that lets these two components coordinate without one having to know\n", + "the other's internals: `ExtGrid.register_pit_node_entries` sets it (`UNIQUE`, see above) for every\n", + "node with a real ext_grid; `CircPump._register_slack_equations` reads it to decide how to treat\n", + "`MDOTSLACKINIT` at its own flow junction:\n", + "\n", + "```python\n", + "@classmethod\n", + "def _register_slack_equations(cls, net, node_pit, sys_idx, registry):\n", + " ...\n", + " junction_lookup = get_lookup(net, \"node\", \"index_active_hydraulics\")[\n", + " cls.get_connected_node_type().table_name()]\n", + " pump_nodes = junction_lookup[p_pumps[tn_col].values].astype(np.int32)\n", + " pump_nodes = pump_nodes[pump_nodes != -1]\n", + " if not len(pump_nodes):\n", + " return\n", + "\n", + " # pressure fix - always, for every own flow junction (MEAN: coexists with ExtGrid's own\n", + " # pressure fix if a real ext_grid happens to sit at the same node too)\n", + " p_col = sys_idx.idx(HydVarEq.PINIT, pump_nodes)\n", + " slack_eq = sys_idx.idx(HydVarEq.SLACK, pump_nodes)\n", + " registry.add(ComponentEquations(\n", + " rows=slack_eq.astype(np.int32), cols=p_col.astype(np.int32),\n", + " data=np.ones(len(slack_eq), dtype=np.float64),\n", + " load_rows=slack_eq.astype(np.int32), load_data=np.zeros(len(slack_eq), dtype=np.float64),\n", + " mode=EqWriteMode.MEAN,\n", + " ))\n", + "\n", + " # where there's no real ext_grid at this node (VAR_MASS_SLACK == 0), reset MDOTSLACKINIT to 0\n", + " # before it's read as this iteration's Newton seed, then add it into the node's balance\n", + " # exactly like ExtGrid would (plain add(), ADDITIVE - joins the genuine pipe/sink balance,\n", + " # does not replace or strip it)\n", + " force_zero = np.unique(pump_nodes[node_pit[pump_nodes, IdxNode.VAR_MASS_SLACK] == 0])\n", + " node_pit[force_zero, IdxNode.MDOTSLACKINIT] = 0.\n", + "\n", + " n_eq = sys_idx.idx(HydVarEq.NODE, pump_nodes)\n", + " slack_col = sys_idx.idx(HydVarEq.MDOTSLACKINIT, pump_nodes)\n", + " registry.add(ComponentEquations(\n", + " rows=n_eq.astype(np.int32), cols=slack_col.astype(np.int32),\n", + " data=np.ones(len(n_eq), dtype=np.float64),\n", + " load_rows=n_eq.astype(np.int32),\n", + " load_data=node_pit[pump_nodes, IdxNode.MDOTSLACKINIT].astype(np.float64), # == 0. now\n", + " ))\n", + "```\n", + "\n", + "The pressure fix is registered unconditionally, `MEAN`, for exactly the same reason as `ExtGrid`'s\n", + "own - so it peacefully coexists with `ExtGrid`'s own pressure-fix row if a real ext_grid happens to\n", + "sit at the same junction too, instead of a `UNIQUE`-vs-`UNIQUE` conflict.\n", + "\n", + "The `MDOTSLACKINIT` handling is the interesting part, and got one thing wrong before landing on\n", + "this shape - worth knowing, since it's an easy mistake to repeat:\n", + "\n", + "- **First (broken) attempt:** replace the node's entire balance row with `MDOTSLACKINIT = 0` via\n", + " `add_override(..., mode=UNIQUE)`. This *does* force `MDOTSLACKINIT` to 0, but `UNIQUE`-as-override\n", + " strips **every** other contribution to that row too - including the genuine pipe/sink mass\n", + " balance other components additively contribute there. Result: the node's real demand silently\n", + " stopped being enforced at all, and the unbalanced example above would have *converged* to a\n", + " wrong answer (the sink's extra 1 kg/s just vanishing) instead of correctly failing. The lesson:\n", + " `UNIQUE`/`add_override` is for \"this row is exclusively mine\", not for \"let me also pin one more\n", + " thing using a row something else already needs.\"\n", + "- **Working version (above):** reset `MDOTSLACKINIT` to `0.` directly in `node_pit` *before*\n", + " building the equation, then add it into the balance the same way `ExtGrid` does (`ADDITIVE`, not\n", + " replacing anything). Since `register_hydraulic_equations` runs fresh every Newton iteration, the\n", + " reset happens every iteration, right before the residual for that row is computed - so\n", + " `MDOTSLACKINIT` never gets the chance to settle on a nonzero value that would silently absorb a\n", + " real imbalance. If the surrounding network genuinely doesn't balance, this residual simply never\n", + " reaches zero and `pipeflow()` correctly raises `PipeflowNotConverged` instead of reporting a\n", + " wrong success - exactly what the cell above demonstrated.\n", + "\n", + "The broader point: two components can coordinate through a **shared PIT flag**, each only reading/\n", + "writing the parts of `node_pit` relevant to itself, without either one needing to import the other\n", + "or know about its internal logic. `ExtGrid` doesn't know `CircPump` exists; `CircPump` only cares\n", + "whether *some* component already claimed `VAR_MASS_SLACK` at its own node." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Checklist: adding a new component\n", + "\n", + "- **Table & PIT columns:** does your component need new `IdxNode`/`IdxBranch` columns? Add them at\n", + " the end of the respective file and bump `node_cols`/`branch_cols`. Don't reuse another\n", + " component's column for an unrelated purpose - if two components need the same *kind* of flag\n", + " (like `VAR_MASS_SLACK`), that's a sign it should be a shared, well-named column, not overloaded.\n", + "- **`register_pit_node_entries`/`register_pit_branch_entries`:** write your component's PIT data\n", + " here, using `add()` for baseline values and `add_override()` when you need to win over another\n", + " component's default. Pick `PitWriteMode` by what the *value itself* means: `UNIQUE` for \"exactly\n", + " one true value, conflict if two different sources disagree without a mechanism to merge them\",\n", + " `MEAN` for \"several sources may legitimately target the same cell and averaging is a sane\n", + " fallback\", `ADDITIVE` for genuine sums/counts.\n", + "- **`register_hydraulic_equations`/`register_thermal_equations`:** always use\n", + " `\"index_active_hydraulics\"`/`\"index_active_heat_transfer\"` lookups here, never the plain\n", + " `\"index\"` one - the plain one is for the pre-reduction PIT-fill phase only. Filter out `-1`\n", + " (disconnected) before using indices further.\n", + "- **Choosing `EqWriteMode`:** `ADDITIVE` (default) for anything that should sum with other\n", + " components' contributions to the same row (the normal case - e.g. every branch's contribution to\n", + " its endpoint nodes' mass balance). `MEAN` when multiple components might legitimately target the\n", + " same row with independent \"this is the target value\" claims (pressure/temperature fixes).\n", + " `UNIQUE` only for a row that is genuinely, exclusively yours - and even then, prefer `add()` +\n", + " `ADDITIVE` if you're only *adding* a term, reserving `add_override()` + `UNIQUE` for when you\n", + " deliberately want to override/replace whatever else targets that row (which almost always means\n", + " you're accepting that anything else contributing there gets silently dropped - make sure that's\n", + " really what you want, see the first-attempt mistake in §6).\n", + "- **Needing to coordinate with another component without importing it:** a shared PIT flag column,\n", + " written by whichever component has the authoritative answer and read by whichever needs to adapt\n", + " its own behavior, is the established pattern (`VAR_MASS_SLACK` being the worked example here).\n", + "- **Test both the isolated case and the coexistence case:** if your component can end up at the\n", + " same node as another P-type-marking component (ext_grid + circ_pump being the concrete example),\n", + " write a test for that combination specifically - it's exactly the kind of interaction that looks\n", + " fine in isolation and breaks silently in combination." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}