Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Water Tower Component #107

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions pandapipes/component_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
from pandapipes.component_models.pump_component import *
from pandapipes.component_models.circulation_pump_mass_component import *
from pandapipes.component_models.circulation_pump_pressure_component import *
from pandapipes.component_models.water_tower_component import *
140 changes: 140 additions & 0 deletions pandapipes/component_models/water_tower_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Copyright (c) 2020 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.abstract_models import NodeElementComponent
from pandapipes.constants import GRAVITATION_CONSTANT, P_CONVERSION
from pandapipes.idx_branch import FROM_NODE, TO_NODE, LOAD_VEC_NODES
from pandapipes.idx_node import PINIT, LOAD, NODE_TYPE, P, EXT_GRID_OCCURENCE, HEIGHT
from pandapipes.internals_toolbox import _sum_by_group
from pandapipes.pipeflow_setup import get_lookup

try:
import pplog as logging
except ImportError:
import logging

logger = logging.getLogger(__name__)


class WaterTower(NodeElementComponent):
"""

Copy link
Collaborator

@jkisse jkisse Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some information on the pictured model and important limitations and assumptions?
It seems to me like an infinite diameter is assumed (no change in water column height on withdraw or feed-in), is that correct?
How does it differ from an ext_grid, apart from the different formulations of pressure, and to which extend does it overlap?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I will do so.
There are not many differences compared to a ext_grid. The pressure is determined by the height of the water column. It is assumed that the water tower capacity is unlimited and the height of the water column remains constant, correct!
I agree that we could think of making a new parent component for ext_grid and water_tower. But for now I would leave it like this!

"""

@classmethod
def table_name(cls):
return "water_tower"

@classmethod
def sign(cls):
return 1.

@classmethod
def create_pit_node_entries(cls, net, node_pit, node_name):
"""
Function which creates pit node entries.

:param net: The pandapipes network
:type net: pandapipesNet
:param node_pit:
:type node_pit:
:return: No Output.
"""
water_towers = net[cls.table_name()]
density = net.fluid.get_density(water_towers.t_k.values)
junction_idx_lookups = get_lookup(net, "node", "index")[node_name]
junction = cls.get_connected_junction(net)
index_wt = junction_idx_lookups[junction]
press = density * (water_towers.height_m.values - node_pit[index_wt, HEIGHT]) * \
GRAVITATION_CONSTANT / P_CONVERSION * water_towers.in_service.values
Copy link
Collaborator

@jkisse jkisse Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimonRubenDrauz What exactly is the definition of height_m? My guess was the height of the water column in the tower. However, if the junction's height is subtracted, this could lead to negative height, couldn't it?
I'd find it confusing if height_m was in "meters above sea level" but I do not know the conventions.
As a suggestion, maybe it should be renamed to tower_height_m. A scaling factor could be introduced to derive water_column_height_m which then serves as input for the static pressure (rhogwater_column_height_m)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually should be the height above sea level. But I agree. It really seems confusing. It is a convention in SINCAL. But I would change it in our case taking only the height of the water_column.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the adaption! However, thinking about it twice, maybe this leads to a problem regarding communicating vessels. (see my latest comment.) This could be the reason why Sincal uses absolute height.

juncts_p, press_sum, number = _sum_by_group(junction.values, press,
np.ones_like(press, dtype=np.int32))
index_p = junction_idx_lookups[juncts_p]
node_pit[index_p, PINIT] = press_sum / number
node_pit[index_p, NODE_TYPE] = P
node_pit[index_p, EXT_GRID_OCCURENCE] += number

net["_lookups"]["water_tower"] = \
np.array(list(set(np.concatenate([net["_lookups"]["water_tower"], index_p])))) if \
"water_tower" in net['_lookups'] else index_p
return water_towers, press

@classmethod
def extract_results(cls, net, options, node_name):
"""
Function that extracts certain results.

:param net: The pandapipes network
:type net: pandapipesNet
:param options:
:type options:
:return: No Output.
"""
water_towers = net[cls.table_name()]

if len(water_towers) == 0:
return

res_table = super().extract_results(net, options, node_name)

branch_pit = net['_pit']['branch']
node_pit = net["_pit"]["node"]

junction = cls.get_connected_junction(net)
index_juncts = junction.values
junct_uni = np.array(list(set(index_juncts)))
index_nodes = get_lookup(net, "node", "index")[node_name][junct_uni]
eg_from_branches = np.isin(branch_pit[:, FROM_NODE], index_nodes)
eg_to_branches = np.isin(branch_pit[:, TO_NODE], index_nodes)
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]
mass_flow_to = branch_pit[eg_to_branches, LOAD_VEC_NODES]
press = node_pit[index_nodes, PINIT]
loads = node_pit[index_nodes, LOAD]
counts = node_pit[index_nodes, EXT_GRID_OCCURENCE]
all_index_nodes = np.concatenate([from_nodes, to_nodes, index_nodes])
all_mass_flows = np.concatenate([-mass_flow_from, mass_flow_to, -loads])
nodes, sum_mass_flows = _sum_by_group(all_index_nodes, all_mass_flows)

# 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[:] = np.repeat(cls.sign() * sum_mass_flows / counts,
counts.astype(int))
res_table["p_bar"].values[:] = press
return res_table, water_towers, index_nodes, node_pit, branch_pit

@classmethod
def get_connected_junction(cls, net):
junction = net[cls.table_name()].junction
return junction

@classmethod
def get_component_input(cls):
"""

:return:
"""

return [("name", dtype(object)),
("junction", "u4"),
("height_m", "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", "p_bar"], True
61 changes: 56 additions & 5 deletions pandapipes/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

import numpy as np
import pandas as pd
from pandapower.auxiliary import get_free_id, _preserve_dtypes

from pandapipes.component_models import Junction, Sink, Source, Pump, Pipe, ExtGrid, \
HeatExchanger, Valve, CirculationPumpPressure, CirculationPumpMass, WaterTower
from pandapipes.component_models.auxiliaries.component_toolbox import add_new_component
from pandapipes.pandapipes_net import pandapipesNet, get_default_pandapipes_structure
from pandapipes.properties import call_lib, add_fluid_to_net
from pandapower.auxiliary import get_free_id, _preserve_dtypes
from pandapipes.properties.fluids import Fluid
from pandapipes.std_types.std_type import PumpStdType, add_basic_std_types, add_pump_std_type, \
load_std_type
from pandapipes.std_types.std_type_toolbox import regression_function
from pandapipes.component_models import Junction, Sink, Source, Pump, Pipe, ExtGrid, \
HeatExchanger, Valve, CirculationPumpPressure, CirculationPumpMass

try:
import pplog as logging
Expand Down Expand Up @@ -303,6 +304,56 @@ def create_ext_grid(net, junction, p_bar, t_k, name=None, in_service=True, index
return index


def create_water_tower(net, junction, height_m, t_k=293.15, name=None, in_service=True, index=None,
type='water_tower', **kwargs):
"""

:param net: The net that the water tower should be connected to
:type net: pandapipesNet
:param junction: The junction to which the water tower is connected to
:type junction: int
:param h_m: The height of the water tower
:type h_m: float
:param t_k: The fixed temperature of the water in the water tower
:type t_k: float, default 293.15
:param name: A name tag for this water tower
:type name: str, default None
:param in_service: True for in service, False for out of service
:type in_service: bool, default True
:param index: Force a specified ID if it is available. If None, the index is set one higher than the \
highest already existing index is selected.
:return: index - The unique ID of the created element
:rtype: int

:Example: create_water_tower(net, junction1, h_m=3, name="Grid reservoir")

"""
add_new_component(net, WaterTower)

if junction not in net["junction"].index.values:
raise UserWarning("Cannot attach to junction %s, junction does not exist" % junction)

if index is not None and index in net["water_tower"].index:
raise UserWarning("An water tower with index %s already exists" % index)

if index is None:
index = get_free_id(net["water_tower"])

# store dtypes
dtypes = net.water_tower.dtypes

cols = ["name", "junction", "height_m", "t_k", "in_service", "type"]
vals = [name, junction, height_m, t_k, bool(in_service), type]
all_values = {col: val for col, val in zip(cols, vals)}
all_values.update(**kwargs)
for col, val in all_values.items():
net.water_tower.at[index, col] = val

# and preserve dtypes
_preserve_dtypes(net.water_tower, dtypes)
return index


def create_heat_exchanger(net, from_junction, to_junction, diameter_m, qext_w, loss_coefficient=0,
name=None, index=None, in_service=True, type="heat_exchanger", **kwargs):
"""
Expand Down Expand Up @@ -823,8 +874,8 @@ def create_circ_pump_const_pressure(net, from_junction, to_junction, p_bar, plif
for b in [from_junction, to_junction]:
if b not in net["junction"].index.values:
raise UserWarning(
"CirculationPumpPressure %s tries to attach to non-existing junction %s"
% (name, b))
"CirculationPumpPressure %s tries to attach to non-existing junction %s"
% (name, b))

if index is None:
index = get_free_id(net["circ_pump_pressure"])
Expand Down
45 changes: 45 additions & 0 deletions pandapipes/test/api/test_components/test_water_tower.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (c) 2020 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 os

import numpy as np
import pandas as pd

import pandapipes as pp
from pandapipes.test.pipeflow_internals import internals_data_path


def test_reservoir():
"""

:rtype:
"""
net = pp.create_empty_network(fluid="water")

junction1 = pp.create_junction(net, pn_bar=1.0, tfluid_k=293.15, name="Connection to Water Tower")
junction2 = pp.create_junction(net, pn_bar=1.0, tfluid_k=293.15, name="Junction 2")

pp.create_water_tower(net, junction1, height_m=30, name="Water Tower")

pp.create_pipe_from_parameters(net, from_junction=junction1, to_junction=junction2, length_km=10, diameter_m=0.075,
name="Pipe 1")

pp.create_sink(net, junction=junction2, mdot_kg_per_s=0.545, name="Sink 1")

pp.pipeflow(net, stop_condition="tol", iter=3, friction_model="nikuradse",
mode="hydraulics", transient=False, nonlinear_method="automatic",
tol_p=1e-4,
tol_v=1e-4)

data = pd.read_csv(os.path.join(internals_data_path, "test_water_tower.csv"), sep=';')

res_junction = net.res_junction.p_bar.values
res_pipe = net.res_pipe.v_mean_m_per_s.values

p_diff = np.abs(1 - res_junction / data['p'].dropna().values)
v_diff = np.abs(1 - res_pipe / data['v'].dropna().values)

assert np.all(p_diff < 0.01)
assert np.all(v_diff < 0.01)
3 changes: 3 additions & 0 deletions pandapipes/test/pipeflow_internals/data/test_water_tower.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
v;p
0.123588;2.937630
;2.442159