diff --git a/src/pandapipes/component_models/__init__.py b/src/pandapipes/component_models/__init__.py index 354b58e2e..af213937c 100644 --- a/src/pandapipes/component_models/__init__.py +++ b/src/pandapipes/component_models/__init__.py @@ -17,4 +17,5 @@ from pandapipes.component_models.flow_control_component import * from pandapipes.component_models.mass_storage_component import * from pandapipes.component_models.heat_consumer_component import * +from pandapipes.component_models.heat_generator_component import * from pandapipes.component_models.component_toolbox import * diff --git a/src/pandapipes/component_models/heat_consumer_component.py b/src/pandapipes/component_models/heat_consumer_component.py index 3c9ed5ce5..c125b761e 100644 --- a/src/pandapipes/component_models/heat_consumer_component.py +++ b/src/pandapipes/component_models/heat_consumer_component.py @@ -174,9 +174,10 @@ def adaption_after_derivatives_hydraulic(cls, net, from_nodes = get_from_nodes_corrected(hc_pit) t_in = node_pit[from_nodes, TINIT] t_out = hc_pit[:, TOUTINIT] + qext = hc_pit[:, QEXT] df_dm = - cp * (t_out - t_in) - mask_equal = t_out >= t_in + mask_equal = np.where(qext < 0, t_in >= t_out, t_out >= t_in) mask_zero = hc_pit[:, QEXT] == 0 mask_ign = mask_equal | mask_zero hc_pit[mask & mask_ign, MDOTINIT] = 0 diff --git a/src/pandapipes/component_models/heat_generator_component.py b/src/pandapipes/component_models/heat_generator_component.py new file mode 100644 index 000000000..680f2cc6a --- /dev/null +++ b/src/pandapipes/component_models/heat_generator_component.py @@ -0,0 +1,421 @@ +# Copyright (c) 2020-2023 by Fraunhofer Institute for Energy Economics +# 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 +from numpy import dtype + +from pandapipes.component_models import (get_fluid, BranchWOInternalsComponent, get_component_array, + standard_branch_wo_internals_result_lookup, set_fixed_node_entries) +from pandapipes.component_models.junction_component import Junction +from pandapipes.idx_branch import (D, AREA, MDOTINIT, QEXT, JAC_DERIV_DP1, JAC_DERIV_DM, + JAC_DERIV_DP, LOAD_VEC_BRANCHES, TO_NODE, TOUTINIT, JAC_DERIV_DT, + JAC_DERIV_DTOUT, LOAD_VEC_BRANCHES_T, FLOW_RETURN_CONNECT, PL) +from pandapipes.idx_node import MDOTSLACKINIT, VAR_MASS_SLACK, JAC_DERIV_MSL, NODE_TYPE_T, GE, TINIT +from pandapipes.pf.internals_toolbox import get_from_nodes_corrected +from pandapipes.pf.pipeflow_setup import get_lookup +from pandapipes.pf.result_extraction import extract_branch_results_without_internals +from pandapipes.properties.properties_toolbox import get_branch_cp + +try: + import pandaplan.core.pplog as logging +except ImportError: + import logging + +logger = logging.getLogger(__name__) + +class HeatGenerator(BranchWOInternalsComponent): + """ + + """ + # columns for internal array + MASS = 0 + QEXT = 1 + DELTAT = 2 + TFLOW = 3 + MODE = 4 + + internal_cols = 5 + + # heat generator modes (sum of combinations of given parameters) + MF_DT = 1 + MF_TR = 2 + QE_MF = 3 + QE_DT = 4 + QE_TR = 5 + PR_PL = 6 + + @classmethod + def table_name(cls): + return "heat_generator" + + @classmethod + def get_connected_node_type(cls): + return Junction + + @classmethod + def from_to_node_cols(cls): + return "return_junction", "flow_junction" + + @classmethod + def active_identifier(cls): + return "in_service" + + @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. + """ + + hg_tbl_all = net[cls.table_name()][net[cls.table_name()][cls.active_identifier()].values] + mask_pr_pl = (~hg_tbl_all.preturn_bar.isna()) & (~hg_tbl_all.plift_bar.isna()) + hg_tbl = hg_tbl_all[mask_pr_pl] + + if len(hg_tbl) == 0: + return hg_tbl, np.array([]) + + junction = hg_tbl[cls.from_to_node_cols()[1]].values + + types = np.full(len(hg_tbl), "pt", dtype=object) + p_values = hg_tbl.preturn_bar.values + hg_tbl.plift_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 hg_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. + """ + + hg_tbl = net[cls.table_name()][net[cls.table_name()][cls.active_identifier()].values] + hg_pit = super().create_pit_branch_entries(net, branch_pit) + + qext = hg_tbl.qext_w.values + hg_pit[~np.isnan(qext), QEXT] = -qext[~np.isnan(qext)] + + mdot = hg_tbl.controlled_mdot_kg_per_s.values + hg_pit[~np.isnan(mdot), MDOTINIT] = mdot[~np.isnan(mdot)] + + tflow = hg_tbl.tflow_k.values + hg_pit[~np.isnan(tflow), TOUTINIT] = tflow[~np.isnan(tflow)] + + pl = hg_tbl.plift_bar.values + hg_pit[~np.isnan(pl), PL] = pl[~np.isnan(pl)] + hg_pit[~np.isnan(pl), D] = 0.1 + hg_pit[~np.isnan(pl), AREA] = hg_pit[~np.isnan(pl), D] ** 2 * np.pi / 4 + + pr = hg_tbl.preturn_bar.values + mask_pr_pl = (~np.isnan(pr)) & (~np.isnan(pl)) + hg_pit[~mask_pr_pl, FLOW_RETURN_CONNECT] = True + + return hg_pit + + @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()] + hg_array = np.zeros(shape=(len(tbl), cls.internal_cols), dtype=np.float64) + hg_array[:, cls.DELTAT] = tbl.deltat_k.values + hg_array[:, cls.TFLOW] = tbl.tflow_k.values #TRETURN? + hg_array[:, cls.QEXT] = tbl.qext_w.values + hg_array[:, cls.MASS] = tbl.controlled_mdot_kg_per_s.values + mf = tbl.controlled_mdot_kg_per_s.values + tf = tbl.tflow_k.values + dt = tbl.deltat_k.values + qe = tbl.qext_w.values + pr = tbl.preturn_bar.values + pl = tbl.plift_bar.values + mf = ~np.isnan(mf) + tf = ~np.isnan(tf) + dt = ~np.isnan(dt) + qe = ~np.isnan(qe) + pr = ~np.isnan(pr) + pl = ~np.isnan(pl) + hg_array[pr & pl & tf, cls.MODE] = cls.PR_PL + hg_array[mf & dt, cls.MODE] = cls.MF_DT + hg_array[mf & tf, cls.MODE] = cls.MF_TR + hg_array[qe & mf, cls.MODE] = cls.QE_MF + hg_array[qe & dt, cls.MODE] = cls.QE_DT + hg_array[qe & tf, cls.MODE] = cls.QE_TR + component_pits[cls.table_name()] = hg_array + + @classmethod + def adaption_before_derivatives_hydraulic(cls, net, branch_pit, node_pit, branch_pit_old, node_pit_old,idx_lookups, options): + """ + Perform adaptions to the branch pit before 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 + """ + + f, t = idx_lookups[cls.table_name()] + hc_pit = branch_pit[f:t, :] + hg_array = get_component_array(net, cls.table_name()) + + mask = hg_array[:, cls.MODE] == cls.QE_DT + if np.any(mask): + cp = get_branch_cp(get_fluid(net), node_pit, hc_pit[mask]) + deltat = hg_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 + """ + + f, t = idx_lookups[cls.table_name()] + hg_array = get_component_array(net, cls.table_name()) + + hg_pit = branch_pit[f:t, :] + + mask = hg_array[:, cls.MODE] == cls.QE_TR + if np.any(mask): + cp = get_branch_cp(get_fluid(net), node_pit, hg_pit) + cp_masked = cp[mask] + from_nodes = get_from_nodes_corrected(hg_pit) + from_nodes_masked = from_nodes[mask] + hg_pit[mask, JAC_DERIV_DP] = 0 + hg_pit[mask, JAC_DERIV_DP1] = 0 + t_in = node_pit[from_nodes_masked, TINIT] + t_out = hg_pit[mask, TOUTINIT] + qext = hg_pit[mask, QEXT] + df_dm = - cp_masked * (t_out - t_in) + + mask_equal = np.where(qext < 0, t_in >= t_out, t_out >= t_in) + mask_zero_masked = qext == 0 + mask_ign_masked = mask_equal | mask_zero_masked + + mask_ign = np.zeros_like(mask, dtype=bool) + mask_ign[mask] = mask_ign_masked + + hg_pit[mask & mask_ign, MDOTINIT] = 0 + + mask_valid_masked = ~mask_ign_masked + hg_pit[mask & ~mask_ign, JAC_DERIV_DM] = df_dm[mask_valid_masked] + + mdot_masked = hg_pit[mask, MDOTINIT] + hg_pit[mask, LOAD_VEC_BRANCHES] = -qext + df_dm * mdot_masked + + mask = hg_array[:, cls.MODE] == cls.PR_PL + if np.any(mask): + tn = hg_pit[mask, TO_NODE].astype(np.int32) + slack_mask = node_pit[tn, VAR_MASS_SLACK].astype(bool) + node_pit[tn[~slack_mask], MDOTSLACKINIT] = 0 + hg_pit[mask, JAC_DERIV_DP] = 1 + hg_pit[mask, JAC_DERIV_DP1] = -1 + + @classmethod + def adaption_before_derivatives_thermal(cls, net, branch_pit, node_pit, branch_pit_old, node_pit_old, idx_lookups, options): + """ + Perform adaptions to the branch pit before 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 + """ + f, t = idx_lookups[cls.table_name()] + hg_pit = branch_pit[f:t, :] + hg_array = get_component_array(net, cls.table_name(), mode='heat_transfer') + mask = hg_array[:, cls.MODE] == cls.MF_DT + if np.any(mask): + cp = get_branch_cp(get_fluid(net), node_pit, hg_pit) + q_ext = cp[mask] * hg_pit[mask, MDOTINIT] * hg_array[mask, cls.DELTAT] + hg_pit[mask, QEXT] = q_ext + + mask = hg_array[:, cls.MODE] == cls.MF_TR + if np.any(mask): + cp = get_branch_cp(get_fluid(net), node_pit, hg_pit) + from_nodes = get_from_nodes_corrected(hg_pit[mask]) + t_in = node_pit[from_nodes, TINIT] + t_out = hg_array[mask, cls.TFLOW] + q_ext = cp[mask] * hg_pit[mask, MDOTINIT] * (t_in - t_out) + hg_pit[mask, QEXT] = q_ext + + @classmethod + def adaption_after_derivatives_thermal(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 + """ + f, t = idx_lookups[cls.table_name()] + hg_pit = branch_pit[f:t, :] + hg_array = get_component_array(net, cls.table_name(), mode='heat_transfer') + + mask= hg_array[:, cls.MODE] == cls.QE_TR + if np.any(mask): + mask_ign = hg_pit[:, QEXT] == 0 + mask = mask & ~mask_ign + hg_pit[mask, LOAD_VEC_BRANCHES_T] = 0 + hg_pit[mask, JAC_DERIV_DTOUT] = 1 + hg_pit[mask, JAC_DERIV_DT] = 0 + + mask= hg_array[:, cls.MODE] == cls.PR_PL + if np.any(mask): + hg_pit[mask, LOAD_VEC_BRANCHES_T] = 0 + hg_pit[mask, JAC_DERIV_DTOUT] = 1 + hg_pit[mask, JAC_DERIV_DT] = 0 + + @classmethod + def get_component_input(cls): + """ + + Get component input. + + :return: + :rtype: + """ + + return [("name", dtype(object)), ("return_junction", "u4"), ("flow_junction", "u4"), ("qext_w", "f8"), + ("tflow_k", "f8"), ("controlled_mdot_kg_per_s", "f8"), ("deltat_k", "f8"), ("preturn_bar", "f8"), + ("plift_bar", "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", "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): + """ + + :param net: + :type net: + :param options: + :type options: + :param branch_results: + :type branch_results: + :param mode: + :type mode: + :return: + :rtype: + """ + required_results_hyd, required_results_ht = standard_branch_wo_internals_result_lookup(net) + + extract_branch_results_without_internals(net, branch_results, required_results_hyd, required_results_ht, + cls.table_name(), mode) + + node_pit = net['_pit']['node'] + branch_pit = net['_pit']['branch'] + branch_lookups = get_lookup(net, "branch", "from_to") + f, t = branch_lookups[cls.table_name()] + + 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] + + hg_array = get_component_array(net, cls.table_name(), mode='heat_transfer') + mask_prpl = hg_array[:, cls.MODE] == cls.PR_PL + mask_other = ~mask_prpl + + res_table['deltat_k'].values[:] = t_from - tout + + if np.any(mask_prpl): + fluid = get_fluid(net) + + cp_i = fluid.get_heat_capacity(t_from[mask_prpl]) + cp_i1 = fluid.get_heat_capacity(tout[mask_prpl]) + mass = branch_pit[f:t, MDOTINIT][mask_prpl] + + res_table['qext_w'].values[mask_prpl] = mass * (cp_i1 * tout[mask_prpl] - cp_i * t_from[mask_prpl]) + + mask_reverse = (branch_pit[f:t, MDOTINIT][mask_prpl] < 0) & \ + ~np.isclose(branch_pit[f:t, MDOTINIT][mask_prpl], 0) + if np.any(mask_reverse): + 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_prpl][mask_reverse].tolist()) + ) + + if np.any(mask_other): + res_table['qext_w'].values[mask_other] = -branch_pit[f:t, QEXT][mask_other] diff --git a/src/pandapipes/create.py b/src/pandapipes/create.py index a78eba036..7c80d3c14 100644 --- a/src/pandapipes/create.py +++ b/src/pandapipes/create.py @@ -17,6 +17,8 @@ from pandapipes.component_models.flow_control_component import FlowControlComponent from pandapipes.component_models.heat_consumer_component import HeatConsumer from pandapipes.pandapipes_net import pandapipesNet, get_basic_net_entries, add_default_components, Sector +from pandapipes.component_models.heat_generator_component import HeatGenerator +from pandapipes.pandapipes_net import pandapipesNet, get_basic_net_entries, add_default_components from pandapipes.properties import call_lib from pandapipes.properties.fluids import Fluid, _add_fluid_to_net from pandapipes.std_types.std_type_class import regression_function, PumpStdType @@ -1226,6 +1228,98 @@ def create_heat_consumer(net, from_junction, to_junction, qext_w=None, controlle return index +def create_heat_generator(net, return_junction, flow_junction, qext_w = None, tflow_k=None, controlled_mdot_kg_per_s=None, deltat_k=None, preturn_bar=None, plift_bar=None, name=None, index=None, in_service=True, type="heat_generator", + **kwargs): + + """ + Creates a heat generator element in net["heat_generator"] from heat generator parameters. + + :param net: The net for which this heat generator should be created + :type net: + :param return_junction: ID of the junction on return side which the heat generator will be connected \ + with + :type return_junction: int + :param flow_junction: ID of the junction on the supply side which the heat generator will be \ + connected with + :type flow_junction: int + :param qext_w: External heat flux in [W], only needed for heat generator with power input + :type qext_w: float, default None + :param tflow_k: Supply/Flow temperature set point at heat generator outlet in [K]. + :type tflow_k: float, default None + :param controlled_mdot_kg_per_s: Mass flow set point at heat generator outlet in [kg/s]. + :type controlled_mdot_kg_per_s: float, default None + :param deltat_k: Temperature lift set point at heat generator in [K]. + :type deltat_k: float, default None + :param preturn_bar: Return pressure set point at heat generator inlet in [bar]. + :type preturn_bar: float, default None + :param plift_bar: Pressure lift set point at heat generator in [bar]. + :type plift_bar: float, default None + :param name: Name of the heat generator element + :type name: str, default None + :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 heat generator is in service or False if it is out of service + :type in_service: bool, default True + :param type: Component type, currently not needed for further calculation, but can be used as information + :type type: str, default "heat_generator" + :param kwargs: Additional keyword arguments will be added as further columns to the \ + net["heat_generator"] table + :type kwargs: dict + :return: index - The unique ID of the created heat generator + :rtype: int + + :Example: + >>> pp.create_heat_generator(net, name='Heat_Generator_1', return_junction=0, flow_junction=1, qext_w=None, tflow_k=90+273.15, controlled_mdot_kg_per_s=None, deltat_k=None, preturn_bar=2, plift_bar=5) + Available combination of inputs is specified below. + + """ + + if { + param_name + for param_name, value in { + # Given inputs + "qext_w": qext_w, + "tflow_k": tflow_k, + "controlled_mdot_kg_per_s": controlled_mdot_kg_per_s, + "deltat_k": deltat_k, + "preturn_bar": preturn_bar, + "plift_bar": plift_bar, + }.items() + if value is not None + } not in ( + # Available input combinations + {"qext_w", "tflow_k"}, + {"qext_w", "deltat_k"}, + {"controlled_mdot_kg_per_s", "tflow_k"}, + {"controlled_mdot_kg_per_s", "deltat_k"}, + {"preturn_bar", "plift_bar", "tflow_k"}, + #TODO add plift_bar und t_flow_k as secondary heat generator + #TODO add plift_bar and preturn_bar, but tflow_k = treturn_k as primary heat generator without heat input (useful?) + ): + raise AttributeError( + "Define available combination of these variables: " + "'qext_w'+'tflow_k', " + "'qext_w'+'deltat_k', " + "'controlled_mdot_kg_per_s'+'tflow_k', " + "'controlled_mdot_kg_per_s'+'deltat_k', or " + "'preturn_bar'+'plift_bar'+'tflow_k'" + ) + + add_new_component(net, HeatGenerator) + + index = _get_index_with_check(net, "heat_generator", index, "heat generator") + _check_branch(net, "Heat Generator", index, return_junction, flow_junction) + + v = {"name": name, "return_junction": return_junction, "flow_junction": flow_junction, + "qext_w": qext_w, "controlled_mdot_kg_per_s": controlled_mdot_kg_per_s, "deltat_k": deltat_k, + "tflow_k": tflow_k, "preturn_bar": preturn_bar, "plift_bar": plift_bar, "in_service": bool(in_service), "type": type} + + _set_entries(net, "heat_generator", index, **v, **kwargs) + + return index + + 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): """ diff --git a/src/pandapipes/plotting/collections.py b/src/pandapipes/plotting/collections.py index 32a27820e..b690b17c7 100644 --- a/src/pandapipes/plotting/collections.py +++ b/src/pandapipes/plotting/collections.py @@ -5,7 +5,7 @@ import numpy as np import pandas as pd from pandapipes.plotting.patch_makers import valve_patches, source_patches, heat_exchanger_patches, \ - pump_patches, pressure_control_patches, compressor_patches, flow_control_patches, heat_consumer_patches + pump_patches, heat_generator_patches, pressure_control_patches, compressor_patches, flow_control_patches, heat_consumer_patches from pandapipes.plotting.plotting_toolbox import coords_from_node_geodata from pandapower.plotting.collections import _create_node_collection, add_cmap_to_collection, \ _create_node_element_collection, _create_line2d_collection, _create_complex_branch_collection @@ -577,7 +577,62 @@ def create_pump_collection(net, pumps=None, table_name='pump', size=5., junction patch_edgecolor=patch_edgecolor, line_color=line_color, **kwargs) return pc, lc + +def create_heat_generator_collection(net, generators=None, table_name='heat_generator', 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 heat generators. + :param net: The pandapipes network + :type net: pandapipesNet + :param generators: The generators for which the collections are created. If None, all generators which have\ + entries in the respective junction geodata will be plotted. + :type generators: list, default None + :param table_name: Name of the heat generator table from which to get the data. + :type table_name: str, default 'heat_generator' + :param size: Patch size + :type size: float, default 5. + :param junction_geodata: Coordinates to use for plotting. If None, net["junction_geodata"] is \ + used. + :type junction_geodata: pandas.DataFrame, default None + :param infofunc: infofunction for the patch element + :type infofunc: function, default None + :param fj_col: name of the from_junction column (can be different for different generator types) + :type fj_col: str, default "from_junction" + :param tj_col: name of the to_junction column (can be different for different generator types) + :type tj_col: str, default "to_junction" + :param picker: Picker argument passed to the patch collection + :type picker: bool, default False + :param kwargs: Keyword arguments are passed to the patch function + :return: lc - line collection, pc - patch collection + """ + generators = get_index_array(generators, net[table_name].index) + generator_table = net[table_name].loc[generators] + + coords, generators_with_geo = coords_from_node_geodata( + generators, generator_table[fj_col].values, generator_table[tj_col].values, + junction_geodata if junction_geodata is not None else net["junction_geodata"], "heat_generator", + "Junction") + + if len(generators_with_geo) == 0: + return None + + colors = kwargs.pop("color", "r") # Default color for heat generators + linewidths = kwargs.pop("linewidths", 2.) + linewidths = kwargs.pop("linewidth", linewidths) + linewidths = kwargs.pop("lw", linewidths) + patch_edgecolor = kwargs.pop("patch_edgecolor", colors) + line_color = kwargs.pop("line_color", colors) + + infos = list(np.repeat([infofunc(i) for i in range(len(generators_with_geo))], 2)) \ + if infofunc is not None else [] + + pc, lc = _create_complex_branch_collection( + coords, heat_generator_patches, size, infos, picker=picker, linewidths=linewidths, + patch_edgecolor=patch_edgecolor, line_color=line_color, **kwargs) + + return pc, lc def create_pressure_control_collection(net, pcs=None, table_name='press_control', size=5., junction_geodata=None, diff --git a/src/pandapipes/plotting/patch_makers.py b/src/pandapipes/plotting/patch_makers.py index 80527f114..1da67d021 100644 --- a/src/pandapipes/plotting/patch_makers.py +++ b/src/pandapipes/plotting/patch_makers.py @@ -161,6 +161,55 @@ def pump_patches(coords, size, **kwargs): lines.append([p1, p1 + diff / 2 - vec_size]) lines.append([p2, p1 + diff / 2 + vec_size]) + + return lines, polys, {} + +def heat_generator_patches(coords, size, **kwargs): + polys, lines = list(), list() + edgecolor = kwargs.pop('patch_edgecolor') + colors = get_color_list(edgecolor, len(coords)) + lw = kwargs.get("linewidths", 2.) + for geodata, col in zip(coords, colors): + p1, p2 = np.array(geodata[0]), np.array(geodata[-1]) + diff = p2 - p1 + angle = np.arctan2(*diff) + vec_size = _rotate_dim2(np.array([0, size]), angle) + line1 = _rotate_dim2(np.array([0, size * np.sqrt(2)]), angle - np.pi / 4) + line2 = _rotate_dim2(np.array([0, size * np.sqrt(2)]), angle + np.pi / 4) + radius = size + + polys.append(Circle(p1 + diff / 2, radius=radius, edgecolor=col, facecolor='w', lw=lw)) + + lines.append([p1 + diff / 2 + vec_size, p1 + diff / 2 - vec_size + line1]) + lines.append([p1 + diff / 2 + vec_size, p1 + diff / 2 - vec_size + line2]) + + lines.append([p1, p1 + diff / 2 - vec_size]) + lines.append([p2, p1 + diff / 2 + vec_size]) + + # Heat exchanger + heat_exchanger_size = size * 0.5 # Size of heat exchanger + heat_exchanger_center = p1 + diff* 1 / 2 + np.array([-0.2 * radius, -0.1 * radius]) # Position of heat exchanger + + m = 3 * heat_exchanger_size / 4 + direc = diff / np.sqrt(diff[0] ** 2 + diff[1] ** 2) + normal = np.array([-direc[1], direc[0]]) + + path1 = (heat_exchanger_center + direc * m / 2) + normal * (heat_exchanger_size * 9 / 8) + path2 = heat_exchanger_center + direc * m / 2 + path3 = heat_exchanger_center + normal * heat_exchanger_size / 3 + path4 = heat_exchanger_center - direc * m / 2 + path5 = (heat_exchanger_center - direc * m / 2) + normal * (heat_exchanger_size * 9 / 8) + path = [path1, path2, path3, path4, path5] + + pa = Path(path) + polys.append(PathPatch(pa, fill=False, lw=lw, edgecolor=col)) + + # Rectangle + rect_width = size * 0.8 + rect_height = size * 0.5 + rect = Rectangle(heat_exchanger_center + np.array([-rect_width / 2, -rect_height*1 / 3]), + width=rect_width, fill=False,lw=lw, height=rect_height, edgecolor=col) + polys.append(rect) return lines, polys, {} diff --git a/src/pandapipes/plotting/simple_plot.py b/src/pandapipes/plotting/simple_plot.py index dc31e1ed7..e14dbb715 100644 --- a/src/pandapipes/plotting/simple_plot.py +++ b/src/pandapipes/plotting/simple_plot.py @@ -10,10 +10,11 @@ from pandapipes.component_models.circulation_pump_mass_component import CirculationPumpMass from pandapipes.component_models.circulation_pump_pressure_component import CirculationPumpPressure from pandapipes.component_models.pump_component import Pump +from pandapipes.component_models.heat_generator_component import HeatGenerator from pandapipes.plotting.collections import create_junction_collection, create_pipe_collection, \ create_valve_collection, create_source_collection, create_pressure_control_collection, \ create_heat_exchanger_collection, create_sink_collection, create_pump_collection, \ - create_compressor_collection, create_flow_control_collection, create_heat_consumer_collection + create_heat_generator_collection, create_compressor_collection, create_flow_control_collection, create_heat_consumer_collection from pandapipes.plotting.generic_geodata import create_generic_coordinates from pandapipes.plotting.plotting_toolbox import get_collection_sizes @@ -321,6 +322,16 @@ def create_simple_collections(net, respect_valves=False, respect_in_service=True color=pump_color, fj_col=fjc, tj_col=tjc) collections[pump_tbl] = pump_colls + for heat_generator_comp in [HeatGenerator]: + hg_tbl = heat_generator_comp.table_name() + if hg_tbl in net: + fjc, tjc = heat_generator_comp.from_to_node_cols() + idx = net[hg_tbl][net[hg_tbl].in_service].index if respect_in_service else net[hg_tbl].index + hg_colls = create_heat_generator_collection(net, idx, table_name=hg_tbl, + size=pump_size, linewidths=pipe_width, + color=pump_color, fj_col=fjc, tj_col=tjc) + collections[hg_tbl] = hg_colls + if ('flow_control' in net) and len(net['flow_control']): idx = net.flow_control[net.flow_control.in_service].index if respect_in_service \ else net.flow_control.index