# -*- coding: utf-8
"""Module for tespy network class.
The network is the container for every TESPy simulation. The network class
automatically creates the system of equations describing topology and
parametrization of a specific model and solves it.
This file is part of project TESPy (github.com/oemof/tespy). It's copyrighted
by the contributors recorded in the version control history of the file,
available from its original location tespy/networks/networks.py
SPDX-License-Identifier: MIT
"""
import ast
import json
import logging
import os
from collections import OrderedDict
from time import time
import numpy as np
import pandas as pd
from numpy.linalg import norm
from tabulate import tabulate
from tespy import connections as con
from tespy.tools import fluid_properties as fp
from tespy.tools import helpers as hlp
from tespy.tools.data_containers import ComponentCharacteristicMaps as dc_cm
from tespy.tools.data_containers import ComponentCharacteristics as dc_cc
from tespy.tools.data_containers import ComponentProperties as dc_cp
from tespy.tools.data_containers import DataContainerSimple as dc_simple
from tespy.tools.data_containers import GroupedComponentCharacteristics as dc_gcc
from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp
from tespy.tools.global_vars import err
from tespy.tools.global_vars import fluid_property_data as fpd
# Only require cupy if Cuda shall be used
try:
import cupy as cu
except ModuleNotFoundError:
cu = None
[docs]class Network:
r"""
Class component is the base class of all TESPy components.
Parameters
----------
fluids : list
A list of all fluids within the network container.
memorise_fluid_properties : boolean
Activate or deactivate fluid property value memorization. Default
state is activated (:code:`True`).
h_range : list
List with minimum and maximum values for enthalpy value range.
h_unit : str
Specify the unit for enthalpy: 'J / kg', 'kJ / kg', 'MJ / kg'.
iterinfo : boolean
Print convergence progress to console.
m_range : list
List with minimum and maximum values for mass flow value range.
m_unit : str
Specify the unit for mass flow: 'kg / s', 't / h'.
p_range : list
List with minimum and maximum values for pressure value range.
p_unit : str
Specify the unit for pressure: 'Pa', 'psi', 'bar', 'MPa'.
s_unit : str
Specify the unit for specific entropy: 'J / kgK', 'kJ / kgK',
'MJ / kgK'.
T_unit : str
Specify the unit for temperature: 'K', 'C', 'F', 'R'.
v_unit : str
Specify the unit for volumetric flow: 'm3 / s', 'm3 / h', 'l / s',
'l / h'.
vol_unit : str
Specify the unit for specific volume: 'm3 / kg', 'l / kg'.
x_unit : str
Specify the unit for steam mass fraction: '-', '%'.
Note
----
Unit specification is optional: If not specified the SI unit (first
element in above lists) will be applied!
Range specification is optional, too. The value range is used to stabilize
the newton algorithm. For more information see the "getting started"
section in the online-documentation.
Example
-------
Basic example for a setting up a tespy.networks.network.Network object. Specifying
the fluids is mandatory! Unit systems, fluid property range and iterinfo
are optional.
Standard value for iterinfo is :code:`True`. This will print out
convergence progress to the console. You can stop the printouts by setting
this property to :code:`False`.
>>> from tespy.networks import Network
>>> fluid_list = ['water', 'air', 'R134a']
>>> mynetwork = Network(fluids=fluid_list, p_unit='bar', T_unit='C')
>>> mynetwork.set_attr(p_range=[1, 10])
>>> type(mynetwork)
<class 'tespy.networks.network.Network'>
>>> mynetwork.set_attr(iterinfo=False)
>>> mynetwork.iterinfo
False
>>> mynetwork.set_attr(iterinfo=True)
>>> mynetwork.iterinfo
True
A simple network consisting of a source, a pipe and a sink. This example
shows how the printout parameter can be used. We specify
:code:`printout=False` for both connections, the pipe as well as the heat
bus. Therefore the :code:`.print_results()` method should not print any
results.
>>> from tespy.networks import Network
>>> from tespy.components import Source, Sink, Pipe
>>> from tespy.connections import Connection, Bus
>>> nw = Network(['CH4'], T_unit='C', p_unit='bar', v_unit='m3 / s')
>>> so = Source('source')
>>> si = Sink('sink')
>>> p = Pipe('pipe', Q=0, pr=0.95, printout=False)
>>> a = Connection(so, 'out1', p, 'in1')
>>> b = Connection(p, 'out1', si, 'in1')
>>> nw.add_conns(a, b)
>>> a.set_attr(fluid={'CH4': 1}, T=30, p=10, m=10, printout=False)
>>> b.set_attr(printout=False)
>>> b = Bus('heat bus')
>>> b.add_comps({'comp': p})
>>> nw.add_busses(b)
>>> b.set_attr(printout=False)
>>> nw.set_attr(iterinfo=False)
>>> nw.solve('design')
>>> nw.print_results()
"""
def __init__(self, fluids, memorise_fluid_properties=True, **kwargs):
# fluid list and constants
if isinstance(fluids, list):
self.fluids = sorted(fluids)
else:
msg = ('Please provide a list containing the network\'s fluids on '
'creation.')
logging.error(msg)
raise TypeError(msg)
self.set_defaults()
self.set_fluid_back_ends(memorise_fluid_properties)
self.set_attr(**kwargs)
[docs] def set_defaults(self):
"""Set default network properties."""
# connection dataframe
self.conns = pd.DataFrame(
columns=['object', 'source', 'source_id', 'target', 'target_id'],
dtype='object'
)
# component dataframe
self.comps = pd.DataFrame(dtype='object')
# user defined function dictionary for fast access
self.user_defined_eq = {}
# bus dictionary
self.busses = OrderedDict()
# results and specification dictionary
self.results = {}
self.specifications = {}
# in case of a design calculation after an offdesign calculation
self.redesign = False
self.checked = False
self.design_path = None
self.iterinfo = True
msg = 'Default unit specifications:\n'
for prop, data in fpd.items():
# standard unit set
self.__dict__.update({prop + '_unit': data['SI_unit']})
msg += data['text'] + ': ' + data['SI_unit'] + '\n'
# don't need the last newline
logging.debug(msg[:-1])
# generic value range
self.m_range_SI = np.array([-1e12, 1e12])
self.p_range_SI = np.array([2e2, 300e5])
self.h_range_SI = np.array([1e3, 7e6])
for prop in ['m', 'p', 'h']:
limits = self.get_attr(prop + '_range_SI')
msg = (
'Default ' + fpd[prop]['text'] + ' limits\n'
'min: ' + str(limits[0]) + ' ' +
self.get_attr(prop + '_unit') + '\n'
'max: ' + str(limits[1]) + ' ' + self.get_attr(prop + '_unit'))
logging.debug(msg)
[docs] def set_fluid_back_ends(self, memorise_fluid_properties):
"""Set the fluid back ends."""
# this must be ordered as the fluid property memorisation calls
# the mass fractions of the different fluids as keys in a given order.
self.fluids_backends = OrderedDict()
msg = 'Network fluids are: '
i = 0
for f in self.fluids:
try:
data = f.split('::')
backend = data[0]
fluid = data[1]
except IndexError:
backend = 'HEOS'
fluid = f
self.fluids_backends[fluid] = backend
self.fluids[i] = fluid
msg += fluid + ', '
i += 1
msg = msg[:-2] + '.'
logging.debug(msg)
# initialise fluid property memorisation function for this network
fp.Memorise.add_fluids(self.fluids_backends, memorise_fluid_properties)
# set up results dataframe for connections
cols = (
['m', 'p', 'h', 'T', 'v', 'vol', 's', 'x', 'Td_bp']
+ self.fluids)
self.results['Connection'] = pd.DataFrame(
columns=cols, dtype='float64')
# include column for fluid balance in specs dataframe
self.specifications['Connection'] = pd.DataFrame(
columns=cols + ['balance'], dtype='bool')
self.specifications['Ref'] = pd.DataFrame(
columns=cols, dtype='bool')
self.specifications['lookup'] = {
'properties': 'prop_specifications',
'chars': 'char_specifications',
'variables': 'var_specifications',
'groups': 'group_specifications'
}
[docs] def set_attr(self, **kwargs):
r"""
Set, resets or unsets attributes of a network.
Parameters
----------
h_range : list
List with minimum and maximum values for enthalpy value range.
h_unit : str
Specify the unit for enthalpy: 'J / kg', 'kJ / kg', 'MJ / kg'.
iterinfo : boolean
Print convergence progress to console.
m_range : list
List with minimum and maximum values for mass flow value range.
m_unit : str
Specify the unit for mass flow: 'kg / s', 't / h'.
p_range : list
List with minimum and maximum values for pressure value range.
p_unit : str
Specify the unit for pressure: 'Pa', 'psi', 'bar', 'MPa'.
s_unit : str
Specify the unit for specific entropy: 'J / kgK', 'kJ / kgK',
'MJ / kgK'.
T_unit : str
Specify the unit for temperature: 'K', 'C', 'F', 'R'.
v_unit : str
Specify the unit for volumetric flow: 'm3 / s', 'm3 / h', 'l / s',
'l / h'.
vol_unit : str
Specify the unit for specific volume: 'm3 / kg', 'l / kg'.
"""
# unit sets
for prop in fpd.keys():
unit = prop + '_unit'
if unit in kwargs:
if kwargs[unit] in fpd[prop]['units']:
self.__dict__.update({unit: kwargs[unit]})
msg = (
'Setting ' + fpd[prop]['text'] +
' unit: ' + kwargs[unit] + '.')
logging.debug(msg)
else:
keys = ', '.join(fpd[prop]['units'].keys())
msg = (
'Allowed units for ' +
fpd[prop]['text'] + ' are: ' + keys)
logging.error(msg)
raise ValueError(msg)
for prop in ['m', 'p', 'h']:
if prop + '_range' in kwargs:
if isinstance(kwargs[prop + '_range'], list):
self.__dict__.update(
{prop + '_range_SI': hlp.convert_to_SI(
prop, np.array(kwargs[prop + '_range']),
self.get_attr(prop + '_unit'))})
else:
msg = (
'Specify the value range as list: [' + prop +
'_min, ' + prop + '_max]')
logging.error(msg)
raise TypeError(msg)
limits = self.get_attr(prop + '_range_SI')
msg = (
'Setting ' + fpd[prop]['text'] +
' limits\nmin: ' + str(limits[0]) + ' ' +
self.get_attr(prop + '_unit') + '\n'
'max: ' + str(limits[1]) + ' ' +
self.get_attr(prop + '_unit'))
logging.debug(msg)
# update non SI value ranges
for prop in ['m', 'p', 'h']:
self.__dict__.update({
prop + '_range': hlp.convert_from_SI(
prop, self.get_attr(prop + '_range_SI'),
self.get_attr(prop + '_unit')
)
})
self.iterinfo = kwargs.get('iterinfo', self.iterinfo)
if not isinstance(self.iterinfo, bool):
msg = ('Network parameter iterinfo must be True or False!')
logging.error(msg)
raise TypeError(msg)
[docs] def get_attr(self, key):
r"""
Get the value of a network attribute.
Parameters
----------
key : str
The attribute you want to retrieve.
Returns
-------
out :
Specified attribute.
"""
if key in self.__dict__:
return self.__dict__[key]
else:
msg = 'Network has no attribute \"' + str(key) + '\".'
logging.error(msg)
raise KeyError(msg)
[docs] def add_subsys(self, *args):
r"""
Add one or more subsystems to the network.
Parameters
----------
c : tespy.components.subsystem.Subsystem
The subsystem to be added to the network, subsystem objects si
:code:`network.add_subsys(s1, s2, s3, ...)`.
"""
for subsys in args:
for c in subsys.conns.values():
self.add_conns(c)
[docs] def get_conn(self, label):
r"""
Get Connection via label.
Parameters
----------
label : str
Label of the Connection object.
Returns
-------
c : tespy.connections.connection.Connection
Connection object with specified label, None if no Connection of
the network has this label.
"""
try:
return self.conns.loc[label, 'object']
except KeyError:
logging.warning('Connection with label ' + label + ' not found.')
return None
[docs] def get_comp(self, label):
r"""
Get Component via label.
Parameters
----------
label : str
Label of the Component object.
Returns
-------
c : tespy.components.component.Component
Component object with specified label, None if no Component of
the network has this label.
"""
try:
return self.comps.loc[label, 'object']
except KeyError:
logging.warning('Component with label ' + label + ' not found.')
return None
[docs] def add_conns(self, *args):
r"""
Add one or more connections to the network.
Parameters
----------
c : tespy.connections.connection.Connection
The connection to be added to the network, connections objects ci
:code:`add_conns(c1, c2, c3, ...)`.
"""
for c in args:
if not isinstance(c, con.Connection):
msg = ('Must provide tespy.connections.connection.Connection '
'objects as parameters.')
logging.error(msg)
raise TypeError(msg)
elif c.label in self.conns.index:
msg = (
'There is already a connection with the label ' +
c.label + '. The connection labels must be unique!')
logging.error(msg)
raise ValueError(msg)
c.good_starting_values = False
self.conns.loc[c.label] = [
c, c.source, c.source_id, c.target, c.target_id]
self.results['Connection'].loc[c.label] = np.nan
msg = 'Added connection ' + c.label + ' to network.'
logging.debug(msg)
# set status "checked" to false, if connection is added to network.
self.checked = False
self._add_comps(*args)
[docs] def del_conns(self, *args):
"""
Remove one or more connections from the network.
Parameters
----------
c : tespy.connections.connection.Connection
The connection to be removed from the network, connections objects
ci :code:`del_conns(c1, c2, c3, ...)`.
"""
for c in args:
self.conns.drop(c.label, inplace=True)
self.results['Connection'].drop(c.label, inplace=True)
msg = ('Deleted connection ' + c.label + ' from network.')
logging.debug(msg)
# set status "checked" to false, if connection is deleted from network.
self.checked = False
[docs] def check_conns(self):
r"""Check connections for multiple usage of inlets or outlets."""
dub = self.conns.loc[
self.conns.duplicated(['source', 'source_id']) == True] # noqa: E712
for c in dub['object']:
targets = ''
for conns in self.conns[
(self.conns['source'] == c.source) &
(self.conns['source_id'] == c.source_id)]['object']:
targets += conns.target.label + ' (' + conns.target_id + '); '
msg = (
'The source ' + c.source.label + ' (' + c.source_id +
') is attached '
'to more than one target: ' + targets[:-2] + '. '
'Please check your network.')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
dub = self.conns.loc[
self.conns.duplicated(['target', 'target_id']) == True] # noqa: E712
for c in dub['object']:
sources = ''
for conns in self.conns[
(self.conns['target'] == c.target) &
(self.conns['target_id'] == c.target_id)]['object']:
sources += conns.source.label + ' (' + conns.source_id + '); '
msg = (
'The target ' + c.target.label + ' (' + c.target_id +
') is attached to more than one source: ' + sources[:-2] + '. '
'Please check your network.')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
def _add_comps(self, *args):
r"""
Add to network's component DataFrame from added connections.
Parameters
----------
c : tespy.connections.connection.Connection
The connections, which have been added to the network. The
components are extracted from these information.
"""
# get unique components in new connections
comps = list(set([cp for c in args for cp in [c.source, c.target]]))
# add to the dataframe of components
for comp in comps:
if comp.label in self.comps.index:
if self.comps.loc[comp.label, 'object'] == comp:
continue
else:
comp_type = comp.__class__.__name__
other_obj = self.comps.loc[comp.label, "object"]
other_comp_type = other_obj.__class__.__name__
msg = (
f"The component with the label {comp.label} of type "
f"{comp_type} cannot be added to the network as a "
f"different component of type {other_comp_type} with "
"the same label has already been added. All "
"components must have unique values!"
)
raise hlp.TESPyNetworkError(msg)
comp_type = comp.__class__.__name__
self.comps.loc[comp.label, 'comp_type'] = comp_type
self.comps.loc[comp.label, 'object'] = comp
[docs] def add_ude(self, *args):
r"""
Add a user defined function to the network.
Parameters
----------
c : tespy.tools.helpers.UserDefinedEquation
The objects to be added to the network, UserDefinedEquation objects
ci :code:`del_conns(c1, c2, c3, ...)`.
"""
for c in args:
if not isinstance(c, hlp.UserDefinedEquation):
msg = ('Must provide tespy.connections.connection.Connection '
'objects as parameters.')
logging.error(msg)
raise TypeError(msg)
elif c.label in self.user_defined_eq:
msg = (
'There is already a UserDefinedEquation with the label ' +
c.label + '. The UserDefinedEquation labels must be '
'unique within a network')
logging.error(msg)
raise ValueError(msg)
self.user_defined_eq[c.label] = c
msg = 'Added UserDefinedEquation ' + c.label + ' to network.'
logging.debug(msg)
[docs] def del_ude(self, *args):
"""
Remove a user defined function from the network.
Parameters
----------
c : tespy.tools.helpers.UserDefinedEquation
The objects to be added deleted from the network,
UserDefinedEquation objects ci :code:`del_conns(c1, c2, c3, ...)`.
"""
for c in args:
del self.user_defined_eq[c.label]
msg = 'Deleted UserDefinedEquation ' + c.label + ' from network.'
logging.debug(msg)
[docs] def add_busses(self, *args):
r"""
Add one or more busses to the network.
Parameters
----------
b : tespy.connections.bus.Bus
The bus to be added to the network, bus objects bi
:code:`add_busses(b1, b2, b3, ...)`.
"""
for b in args:
if self.check_busses(b):
self.busses[b.label] = b
msg = 'Added bus ' + b.label + ' to network.'
logging.debug(msg)
self.results[b.label] = pd.DataFrame(
columns=[
'component value', 'bus value', 'efficiency',
'design value'],
dtype='float64')
[docs] def del_busses(self, *args):
r"""
Remove one or more busses from the network.
Parameters
----------
b : tespy.connections.bus.Bus
The bus to be removed from the network, bus objects bi
:code:`add_busses(b1, b2, b3, ...)`.
"""
for b in args:
if b in self.busses.values():
del self.busses[b.label]
msg = 'Deleted bus ' + b.label + ' from network.'
logging.debug(msg)
del self.results[b.label]
[docs] def check_busses(self, b):
r"""
Checksthe busses to be added for type, duplicates and identical labels.
Parameters
----------
b : tespy.connections.bus.Bus
The bus to be checked.
"""
if isinstance(b, con.Bus):
if len(self.busses) > 0:
if b in self.busses.values():
msg = ('Network contains the bus ' + b.label + ' (' +
str(b) + ') already.')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
elif b.label in self.busses:
msg = ('Network already has a bus with the name ' +
b.label + '.')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
else:
return True
else:
return True
else:
msg = 'Only objects of type bus are allowed in *args.'
logging.error(msg)
raise TypeError(msg)
[docs] def check_network(self):
r"""Check if components are connected properly within the network."""
if len(self.conns) == 0:
msg = (
'No connections have been added to the network, please make '
'sure to add your connections with the .add_conns() method.'
)
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
if len(self.fluids) == 0:
msg = (
'Network has no fluids, please specify a list with fluids on '
'network creation.'
)
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
self.check_conns()
self.init_components()
# count number of incoming and outgoing connections and compare to
# expected values
for comp in self.comps['object']:
num_o = (self.conns[['source', 'target']] == comp).sum().source
num_i = (self.conns[['source', 'target']] == comp).sum().target
if num_o != comp.num_o:
msg = (
comp.label + ' is missing ' + str(comp.num_o - num_o) + ' '
'outgoing connections. Make sure all outlets are connected'
' and all connections have been added to the network.')
logging.error(msg)
# raise an error in case network check is unsuccesful
raise hlp.TESPyNetworkError(msg)
elif num_i != comp.num_i:
msg = (
comp.label + ' is missing ' + str(comp.num_i - num_i) + ' '
'incoming connections. Make sure all inlets are connected '
'and all connections have been added to the network.')
logging.error(msg)
# raise an error in case network check is unsuccesful
raise hlp.TESPyNetworkError(msg)
# network checked
self.checked = True
msg = 'Networkcheck successful.'
logging.info(msg)
[docs] def init_components(self):
r"""Set up necessary component information."""
for comp in self.comps["object"]:
# get incoming and outgoing connections of a component
sources = self.conns[self.conns['source'] == comp]
sources = sources['source_id'].sort_values().index.tolist()
targets = self.conns[self.conns['target'] == comp]
targets = targets['target_id'].sort_values().index.tolist()
# save the incoming and outgoing as well as the number of
# connections as component attribute
comp.inl = self.conns.loc[targets, 'object'].tolist()
comp.outl = self.conns.loc[sources, 'object'].tolist()
comp.num_i = len(comp.inlets())
comp.num_o = len(comp.outlets())
# save the connection locations to the components
comp.conn_loc = []
for c in comp.inl + comp.outl:
comp.conn_loc += [self.conns.index.get_loc(c.label)]
# set up restults and specification dataframes
comp_type = comp.__class__.__name__
if comp_type not in self.results:
cols = [col for col, data in comp.variables.items()
if isinstance(data, dc_cp)]
self.results[comp_type] = pd.DataFrame(
columns=cols, dtype='float64')
if comp_type not in self.specifications:
cols, groups, chars = [], [], []
for col, data in comp.variables.items():
if isinstance(data, dc_cp):
cols += [col]
elif isinstance(data, dc_gcp) or isinstance(data, dc_gcc):
groups += [col]
elif isinstance(data, dc_cc) or isinstance(data, dc_cm):
chars += [col]
self.specifications[comp_type] = {
'groups': pd.DataFrame(columns=groups, dtype='bool'),
'chars': pd.DataFrame(columns=chars, dtype='object'),
'variables': pd.DataFrame(columns=cols, dtype='bool'),
'properties': pd.DataFrame(columns=cols, dtype='bool')
}
[docs] def initialise(self):
r"""
Initilialise the network depending on calclation mode.
Design
- Generic fluid composition and fluid property initialisation.
- Starting values from initialisation path if provided.
Offdesign
- Check offdesign path specification.
- Set component and connection design point properties.
- Switch from design/offdesign parameter specification.
"""
# keep track of the number of bus, component and connection equations
# as well as number of component variables
self.num_bus_eq = 0
self.num_comp_eq = 0
self.num_conn_eq = 0
self.num_comp_vars = 0
self.init_set_properties()
if self.mode == 'offdesign':
self.redesign = True
if self.design_path is None:
# must provide design_path
msg = ('Please provide "design_path" for every offdesign '
'calculation.')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
# load design case
if self.new_design:
self.init_offdesign_params()
self.init_offdesign()
else:
# reset any preceding offdesign calculation
self.init_design()
# generic fluid initialisation
# for offdesign cases good starting values should be available
self.init_fluids()
# generic fluid property initialisation
self.init_properties()
msg = 'Network initialised.'
logging.info(msg)
[docs] def init_set_properties(self):
"""Specification of SI values for user set values."""
# fluid property values
for c in self.conns['object']:
# set all specifications to False
self.specifications['Connection'].loc[c.label] = False
if not self.init_previous:
c.good_starting_values = False
c.conn_loc = self.conns.index.get_loc(c.label)
for key in ['m', 'p', 'h', 'T', 'x', 'v', 'Td_bp', 'vol', 's']:
# read unit specifications
if key == 'Td_bp':
c.get_attr(key).unit = self.get_attr('T_unit')
else:
c.get_attr(key).unit = self.get_attr(key + '_unit')
# set SI value
if c.get_attr(key).val_set:
c.get_attr(key).val_SI = hlp.convert_to_SI(
key, c.get_attr(key).val, c.get_attr(key).unit)
if c.get_attr(key).ref_set:
if key == 'T':
c.get_attr(key).ref.delta_SI = hlp.convert_to_SI(
'Td_bp', c.get_attr(key).ref.delta,
c.get_attr(key).unit)
else:
c.get_attr(key).ref.delta_SI = hlp.convert_to_SI(
key, c.get_attr(key).ref.delta,
c.get_attr(key).unit)
# fluid vector specification
tmp = c.fluid.val
for fluid in tmp.keys():
if fluid not in self.fluids:
msg = ('Your connection ' + c.label + ' holds a fluid, '
'that is not part of the networks\'s fluids (' +
fluid + ').')
raise hlp.TESPyNetworkError(msg)
tmp0 = c.fluid.val0
tmp_set = c.fluid.val_set
c.fluid.val = OrderedDict()
c.fluid.val0 = OrderedDict()
c.fluid.val_set = OrderedDict()
# if the number of fluids is one the mass fraction is 1 for every
# connection
if len(self.fluids) == 1:
c.fluid.val[self.fluids[0]] = 1
c.fluid.val0[self.fluids[0]] = 1
if self.fluids[0] in tmp_set:
c.fluid.val_set[self.fluids[0]] = tmp_set[self.fluids[0]]
else:
c.fluid.val_set[self.fluids[0]] = False
# jump to next connection
continue
for fluid in self.fluids:
# take over values from temporary dicts
if fluid in tmp and fluid in tmp_set:
c.fluid.val[fluid] = tmp[fluid]
c.fluid.val0[fluid] = tmp[fluid]
c.fluid.val_set[fluid] = tmp_set[fluid]
# take over starting values
elif fluid in tmp0:
if fluid not in tmp_set:
c.fluid.val[fluid] = tmp0[fluid]
c.fluid.val0[fluid] = tmp0[fluid]
c.fluid.val_set[fluid] = False
# if fluid not in keys
else:
c.fluid.val[fluid] = 0
c.fluid.val0[fluid] = 0
c.fluid.val_set[fluid] = False
msg = (
'Updated fluid property SI values and fluid mass fraction for '
'user specified connection parameters.')
logging.debug(msg)
[docs] def init_design(self):
r"""
Initialise a design calculation.
Offdesign parameters are unset, design parameters are set. If
:code:`local_offdesign` is :code:`True` for connections or components,
the design point information are read from the .csv-files in the
respective :code:`design_path`. In this case, the design values are
unset, the offdesign values set.
"""
# connections
for c in self.conns['object']:
# read design point information of connections with
# local_offdesign activated from their respective design path
if c.local_offdesign:
if c.design_path is None:
msg = (
'The parameter local_offdesign is True for the '
'connection ' + c.label + ', an individual '
'design_path must be specified in this case!')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
# unset design parameters
for var in c.design:
c.get_attr(var).val_set = False
# set offdesign parameters
for var in c.offdesign:
c.get_attr(var).val_set = True
# read design point information
df = self.init_read_connections(c.design_path)
msg = (
'Reading individual design point information for '
'connection ' + c.label + ' from path ' + c.design_path +
'connections.')
logging.debug(msg)
# write data to connections
self.init_conn_design_params(c, df)
else:
# unset all design values
c.m.design = np.nan
c.p.design = np.nan
c.h.design = np.nan
c.fluid.design = OrderedDict()
c.new_design = True
# switch connections to design mode
if self.redesign:
for var in c.design:
c.get_attr(var).val_set = True
for var in c.offdesign:
c.get_attr(var).val_set = False
# unset design values for busses, count bus equations and
# reindex bus dictionary
for b in self.busses.values():
self.busses[b.label] = b
self.num_bus_eq += b.P.is_set * 1
for cp in b.comps.index:
b.comps.loc[cp, 'P_ref'] = np.nan
series = pd.Series(dtype='float64')
for cp in self.comps['object']:
# read design point information of components with
# local_offdesign activated from their respective design path
if cp.local_offdesign:
if cp.design_path is not None:
# get type of component (class name)
c = cp.__class__.__name__
# read design point information
path = hlp.modify_path_os(
cp.design_path + '/components/' + c + '.csv')
df = pd.read_csv(
path, sep=';', decimal='.', converters={
'busses': ast.literal_eval,
'bus_P_ref': ast.literal_eval})
df.set_index('label', inplace=True)
# write data
self.init_comp_design_params(cp, df.loc[cp.label])
# unset design parameters
for var in cp.design:
cp.get_attr(var).is_set = False
# set offdesign parameters
switched = False
msg = 'Set component attributes '
for var in cp.offdesign:
# set variables provided in .offdesign attribute
data = cp.get_attr(var)
data.is_set = True
# take nominal values from design point
if isinstance(data, dc_cp):
cp.get_attr(var).val = cp.get_attr(var).design
switched = True
msg += var + ', '
if switched:
msg = (msg[:-2] + ' to design value at component ' +
cp.label + '.')
logging.debug(msg)
cp.new_design = False
else:
# switch connections to design mode
if self.redesign:
for var in cp.design:
cp.get_attr(var).is_set = True
for var in cp.offdesign:
cp.get_attr(var).is_set = False
cp.set_parameters(self.mode, series)
# component initialisation
cp.comp_init(self)
ct = cp.__class__.__name__
for spec in self.specifications[ct].keys():
if len(cp.get_attr(self.specifications['lookup'][spec])) > 0:
self.specifications[ct][spec].loc[cp.label] = (
cp.get_attr(self.specifications['lookup'][spec]))
# count number of component equations and variables
self.num_comp_vars += cp.num_vars
self.num_comp_eq += cp.num_eq
[docs] def init_offdesign_params(self):
r"""
Read design point information from specified :code:`design_path`.
If a :code:`design_path` has been specified individually for components
or connections, the data will be read from the specified individual
path instead.
Note
----
The methods
:py:meth:`tespy.networks.network.Network.init_comp_design_params`
(components) and the
:py:meth:`tespy.networks.network.Network.init_conn_design_params`
(connections) handle the parameter specification.
"""
# components without any parameters
not_required = [
'source', 'sink', 'node', 'merge', 'splitter', 'separator', 'drum',
'subsystem_interface', 'droplet_separator']
# fetch all components, reindex with label
df_comps = self.comps.copy()
df_comps = df_comps[~df_comps['comp_type'].isin(not_required)]
# iter through unique types of components (class names)
for c in df_comps['comp_type'].unique():
path = hlp.modify_path_os(
self.design_path + '/components/' + c + '.csv')
msg = (
'Reading design point information for components of type '
+ c + ' from path ' + path + '.')
logging.debug(msg)
# read data
df = pd.read_csv(
path, sep=';', decimal='.', converters={
'busses': ast.literal_eval,
'bus_P_ref': ast.literal_eval})
df.set_index('label', inplace=True)
# iter through all components of this type and set data
for c_label in df.index:
comp = df_comps.loc[c_label, 'object']
# read data of components with individual design_path
if comp.design_path is not None:
path_c = hlp.modify_path_os(
comp.design_path + '/components/' + c + '.csv')
df_c = pd.read_csv(
path_c, sep=';', decimal='.', converters={
'busses': ast.literal_eval,
'bus_P_ref': ast.literal_eval})
df_c.set_index('label', inplace=True)
data = df_c.loc[comp.label]
else:
data = df.loc[comp.label]
# write data to components
self.init_comp_design_params(comp, data)
msg = 'Done reading design point information for components.'
logging.debug(msg)
# read connection design point information
df = self.init_read_connections(self.design_path)
msg = (
'Reading design point information for connections from path ' +
self.design_path + '/connections.csv.')
logging.debug(msg)
# iter through connections
for c in self.conns['object']:
# read data of connections with individual design_path
if c.design_path is not None:
df_c = self.init_read_connections(c.design_path)
msg = (
'Reading individual design point information for '
'connection ' + c.label + ' from path ' + c.design_path +
'/connections.csv.')
logging.debug(msg)
# write data
self.init_conn_design_params(c, df_c)
else:
# write data
self.init_conn_design_params(c, df)
msg = 'Done reading design point information for connections.'
logging.debug(msg)
[docs] def init_comp_design_params(self, component, data):
r"""
Write design point information to components.
Parameters
----------
component : tespy.components.component.Component
Write design point information to this component.
data : pandas.core.series.Series, pandas.core.frame.DataFrame
Design point information.
"""
# write component design data
component.set_parameters(self.mode, data)
# write design values to busses
i = 0
for b in data.busses:
bus = self.busses[b].comps
bus.loc[component, 'P_ref'] = data['bus_P_ref'][i]
i += 1
[docs] def init_conn_design_params(self, c, df):
r"""
Write design point information to connections.
Parameters
----------
c : tespy.connections.connection.Connection
Write design point information to this connection.
df : pandas.core.frame.DataFrame
Dataframe containing design point information.
"""
# match connection (source, source_id, target, target_id) on
# connection objects of design file
conn = df.loc[
df['source'].isin([c.source.label]) &
df['target'].isin([c.target.label]) &
df['source_id'].isin([c.source_id]) &
df['target_id'].isin([c.target_id])]
try:
# read connection information
conn_id = conn.index[0]
for var in ['m', 'p', 'h', 'v', 'x', 'T', 'Td_bp']:
c.get_attr(var).design = hlp.convert_to_SI(
var, df.loc[conn_id, var], df.loc[conn_id, var + '_unit'])
c.vol.design = c.v.design / c.m.design
for fluid in self.fluids:
c.fluid.design[fluid] = df.loc[conn_id, fluid]
except IndexError:
# no matches in the connections of the network and the design files
msg = (
'Could not find connection ' + c.label + ' in design case. '
'Please, make sure no connections have been modified or '
'components have been relabeled for your offdesign '
'calculation.')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
[docs] def init_offdesign(self):
r"""
Switch components and connections from design to offdesign mode.
Note
----
**components**
All parameters stated in the component's attribute :code:`cp.design`
will be unset and all parameters stated in the component's attribute
:code:`cp.offdesign` will be set instead.
Additionally, all component parameters specified as variables are
unset and the values from design point are set.
**connections**
All parameters given in the connection's attribute :code:`c.design`
will be unset and all parameters stated in the connections's attribute
:code:`cp.offdesign` will be set instead. This does also affect
referenced values!
"""
for c in self.conns['object']:
if not c.local_design:
# switch connections to offdesign mode
for var in c.design:
c.get_attr(var).val_set = False
c.get_attr(var).ref_set = False
for var in c.offdesign:
c.get_attr(var).val_set = True
c.get_attr(var).val_SI = c.get_attr(var).design
c.new_design = False
msg = 'Switched connections from design to offdesign.'
logging.debug(msg)
for cp in self.comps['object']:
if not cp.local_design:
# unset variables provided in .design attribute
for var in cp.design:
cp.get_attr(var).is_set = False
switched = False
msg = 'Set component attributes '
for var in cp.offdesign:
# set variables provided in .offdesign attribute
data = cp.get_attr(var)
data.is_set = True
# take nominal values from design point
if isinstance(data, dc_cp):
cp.get_attr(var).val = cp.get_attr(var).design
switched = True
msg += var + ', '
if switched:
msg = (msg[:-2] + ' to design value at component ' +
cp.label + '.')
logging.debug(msg)
# start component initialisation
cp.comp_init(self)
ct = cp.__class__.__name__
for spec in self.specifications[ct].keys():
if len(cp.get_attr(self.specifications['lookup'][spec])) > 0:
self.specifications[ct][spec].loc[cp.label] = (
cp.get_attr(self.specifications['lookup'][spec]))
cp.new_design = False
self.num_comp_vars += cp.num_vars
self.num_comp_eq += cp.num_eq
msg = 'Switched components from design to offdesign.'
logging.debug(msg)
# count bus equations and reindex bus dictionary
for b in self.busses.values():
self.busses[b.label] = b
self.num_bus_eq += b.P.is_set * 1
[docs] def init_fluids(self):
r"""
Initialise the fluid vector on every connection of the network.
- Create fluid vector for every component as dict,
index: nw.fluids,
values: 0 if not set by user.
- Create fluid_set vector with same logic,
index: nw.fluids,
values: False if not set by user.
- If there are any combustion chambers in the network, calculate fluid
vector starting from there.
- Propagate fluid vector in direction of sources and targets.
"""
# stop fluid propagation for single fluid networks
if len(self.fluids) == 1:
return
# fluid propagation from set values
for c in self.conns['object']:
if any(c.fluid.val_set.values()):
c.target.propagate_fluid_to_target(c, c.target)
c.source.propagate_fluid_to_source(c, c.source)
if any(c.fluid.val0.values()):
c.target.propagate_fluid_to_target(c, c.target)
c.source.propagate_fluid_to_source(c, c.source)
# fluid starting value generation for components
for cp in self.comps['object']:
cp.initialise_fluids()
msg = 'Fluid initialisation done.'
logging.debug(msg)
[docs] def init_properties(self):
"""
Initialise the fluid properties on every connection of the network.
- Set generic starting values for mass flow, enthalpy and pressure if
not user specified, read from :code:`ìnit_path` or available from
previous calculation.
- For generic starting values precalculate enthalpy value at points of
given temperature, vapor mass fraction, temperature difference to
boiling point or fluid state.
"""
if self.init_path is not None:
df = self.init_read_connections(self.init_path)
# improved starting values for referenced connections,
# specified vapour content values, temperature values as well as
# subccooling/overheating and state specification
for c in self.conns['object']:
if self.init_path is not None:
conn = df.loc[
df['source'].isin([c.source.label]) &
df['target'].isin([c.target.label]) &
df['source_id'].isin([c.source_id]) &
df['target_id'].isin([c.target_id])]
try:
conn_id = conn.index[0]
# overwrite SI-values with values from init_file,
# except user specified values
for prop in ['m', 'p', 'h']:
data = c.get_attr(prop)
data.val0 = df.loc[conn_id, prop]
data.unit = df.loc[conn_id, prop + '_unit']
for fluid in self.fluids:
if not c.fluid.val_set[fluid]:
c.fluid.val[fluid] = df.loc[conn_id, fluid]
c.fluid.val0[fluid] = c.fluid.val[fluid]
c.good_starting_values = True
except IndexError:
msg = (
'Could not find connection ' + c.label + ' in '
'connections.csv of init_path ' + self.init_path + '.')
logging.debug(msg)
if sum(c.fluid.val.values()) == 0:
msg = (
'The starting value for the fluid composition of the '
'connection ' + c.label + ' is empty. This might lead to '
'issues in the initialisation and solving process as '
'fluid property functions can not be called. Make sure '
'you specified a fluid composition in all parts of the '
'network.')
logging.warning(msg)
for key in ['m', 'p', 'h']:
if not c.good_starting_values:
self.init_val0(c, key)
if not c.get_attr(key).val_set:
c.get_attr(key).val_SI = hlp.convert_to_SI(
key, c.get_attr(key).val0, c.get_attr(key).unit)
self.init_count_connections_parameters(c)
for c in self.conns['object']:
if not c.good_starting_values:
for key in ['m', 'p', 'h', 'T']:
if (c.get_attr(key).ref_set and
not c.get_attr(key).val_set):
c.get_attr(key).val_SI = (
c.get_attr(key).ref.obj.get_attr(key).val_SI *
c.get_attr(key).ref.factor +
c.get_attr(key).ref.delta_SI)
self.init_precalc_properties(c)
# starting values for specified subcooling/overheating
# and state specification. These should be recalculated even with
# good starting values, for example, when one exchanges enthalpy
# with boiling point temperature difference.
if ((c.Td_bp.val_set or c.state.is_set) and
not c.h.val_set):
if ((c.Td_bp.val_SI > 0 and c.Td_bp.val_set) or
(c.state.val == 'g' and c.state.is_set)):
h = fp.h_mix_pQ(c.get_flow(), 1)
if c.h.val_SI < h:
c.h.val_SI = h * 1.001
elif ((c.Td_bp.val_SI < 0 and c.Td_bp.val_set) or
(c.state.val == 'l' and c.state.is_set)):
h = fp.h_mix_pQ(c.get_flow(), 0)
if c.h.val_SI > h:
c.h.val_SI = h * 0.999
msg = 'Generic fluid property specification complete.'
logging.debug(msg)
[docs] def init_count_connections_parameters(self, c):
"""
Count the number of parameters set on a connection.
Parameters
----------
c : tespy.connections.connection.Connection
Connection count parameters of.
"""
# variables 0 to 9: fluid properties
vars = self.specifications['Connection'].columns[:9]
row = [c.get_attr(var).val_set for var in vars]
self.num_conn_eq += row.count(True)
# write information to specifaction dataframe
self.specifications['Connection'].loc[c.label, vars] = row
row = [c.get_attr(var).ref_set for var in vars]
self.num_conn_eq += row.count(True)
# write refrenced value information to specifaction dataframe
self.specifications['Ref'].loc[c.label, vars] = row
# variables 9 to last but one: fluid mass fractions
fluids = self.specifications['Connection'].columns[9:-1]
row = [c.fluid.val_set[fluid] for fluid in fluids]
self.num_conn_eq += row.count(True)
self.specifications['Connection'].loc[c.label, fluids] = row
# last one: fluid balance specification
self.num_conn_eq += c.fluid.balance * 1
self.specifications['Connection'].loc[
c.label, 'balance'] = c.fluid.balance
[docs] def init_precalc_properties(self, c):
"""
Precalculate enthalpy values for connections.
Precalculation is performed only if temperature, vapor mass fraction,
temperature difference to boiling point or phase is specified.
Parameters
----------
c : tespy.connections.connection.Connection
Connection to precalculate values for.
"""
# starting values for specified vapour content or temperature
if c.x.val_set and not c.h.val_set:
try:
c.h.val_SI = fp.h_mix_pQ(c.get_flow(), c.x.val_SI)
except ValueError:
pass
if c.T.val_set and not c.h.val_set:
try:
c.h.val_SI = fp.h_mix_pT(c.get_flow(), c.T.val_SI)
except ValueError:
pass
[docs] def init_val0(self, c, key):
r"""
Set starting values for fluid properties.
The component classes provide generic starting values for their inlets
and outlets.
Parameters
----------
c : tespy.connections.connection.Connection
Connection to initialise.
"""
if np.isnan(c.get_attr(key).val0):
# starting value for mass flow is random between 1 and 2 kg/s
# (should be generated based on some hash maybe?)
if key == 'm':
c.get_attr(key).val0 = np.random.random() + 1
# generic starting values for pressure and enthalpy
else:
# retrieve starting values from component information
val_s = c.source.initialise_source(c, key)
val_t = c.target.initialise_target(c, key)
if val_s == 0 and val_t == 0:
if key == 'p':
c.get_attr(key).val0 = 1e5
elif key == 'h':
c.get_attr(key).val0 = 1e6
elif val_s == 0:
c.get_attr(key).val0 = val_t
elif val_t == 0:
c.get_attr(key).val0 = val_s
else:
c.get_attr(key).val0 = (val_s + val_t) / 2
# change value according to specified unit system
c.get_attr(key).val0 = hlp.convert_from_SI(
key, c.get_attr(key).val0, self.get_attr(key + '_unit'))
[docs] @staticmethod
def init_read_connections(base_path):
r"""
Read connection information from base_path.
Parameters
----------
base_path : str
Path to network information.
"""
path = hlp.modify_path_os(base_path + '/connections.csv')
df = pd.read_csv(path, index_col=0, delimiter=';', decimal='.')
return df
[docs] def solve(self, mode, init_path=None, design_path=None,
max_iter=50, min_iter=4, init_only=False, init_previous=True,
use_cuda=False, always_all_equations=True):
r"""
Solve the network.
- Check network consistency.
- Initialise calculation and preprocessing.
- Perform actual calculation.
- Postprocessing.
Parameters
----------
mode : str
Choose from 'design' and 'offdesign'.
init_path : str
Path to the folder, where your network was saved to, e.g.
saving to :code:`nw.save('myplant/tests')` would require loading
from :code:`init_path='myplant/tests'`.
design_path : str
Path to the folder, where your network's design case was saved to,
e.g. saving to :code:`nw.save('myplant/tests')` would require
loading from :code:`design_path='myplant/tests'`.
max_iter : int
Maximum number of iterations before calculation stops, default: 50.
min_iter : int
Minimum number of iterations before calculation stops, default: 4.
init_only : boolean
Perform initialisation only, default: :code:`False`.
init_previous : boolean
Initialise the calculation with values from the previous
calculation, default: :code:`True`.
use_cuda : boolean
Use cuda instead of numpy for matrix inversion, default:
:code:`False`.
always_all_equations : boolean
Calculate all equations in every iteration. Disabling this flag,
will increase calculation speed, especially for mixtures, default:
:code:`True`.
Note
----
For more information on the solution process have a look at the online
documentation at tespy.readthedocs.io in the section "TESPy modules".
"""
self.new_design = False
if self.design_path == design_path and design_path is not None:
for c in self.conns['object']:
if c.new_design:
self.new_design = True
break
if not self.new_design:
for cp in self.comps['object']:
if cp.new_design:
self.new_design = True
break
else:
self.new_design = True
self.init_path = init_path
self.design_path = design_path
self.max_iter = max_iter
self.min_iter = min_iter
self.init_previous = init_previous
self.iter = 0
self.use_cuda = use_cuda
self.always_all_equations = always_all_equations
if self.use_cuda and cu is None:
msg = ('Specifying use_cuda=True requires cupy to be installed on '
'your machine. Numpy will be used instead.')
logging.warning(msg)
self.use_cuda = False
if mode != 'offdesign' and mode != 'design':
msg = 'Mode must be "design" or "offdesign".'
logging.error(msg)
raise ValueError(msg)
else:
self.mode = mode
msg = (
'Solver properties: mode=' + self.mode + ', init_path=' +
str(self.init_path) + ', design_path=' + str(self.design_path) +
', max_iter=' + str(max_iter) + ', min_iter=' + str(min_iter) +
', init_only=' + str(init_only))
logging.debug(msg)
if not self.checked:
self.check_network()
msg = (
'Network properties: '
'number of components=' + str(len(self.comps)) +
', number of connections=' + str(len(self.conns.index)) +
', number of busses=' + str(len(self.busses)))
logging.debug(msg)
self.initialise()
if init_only:
return
msg = 'Starting solver.'
logging.info(msg)
self.solve_determination()
self.solve_loop()
if self.lin_dep:
msg = (
'Singularity in jacobian matrix, calculation aborted! Make '
'sure your network does not have any linear dependencies in '
'the parametrisation. Other reasons might be\n-> given '
'temperature with given pressure in two phase region, try '
'setting enthalpy instead or provide accurate starting value '
'for pressure.\n-> given logarithmic temperature differences '
'or kA-values for heat exchangers, \n-> support better '
'starting values.\n-> bad starting value for fuel mass flow '
'of combustion chamber, provide small (near to zero, but not '
'zero) starting value.')
logging.error(msg)
return
self.postprocessing()
fp.Memorise.del_memory(self.fluids)
if not self.progress:
msg = (
'The solver does not seem to make any progress, aborting '
'calculation. Residual value is '
'{:.2e}'.format(norm(self.residual)) + '. This frequently '
'happens, if the solver pushes the fluid properties out of '
'their feasible range.')
logging.warning(msg)
return
msg = 'Calculation complete.'
logging.info(msg)
[docs] def solve_loop(self):
r"""Loop of the newton algorithm."""
# parameter definitions
self.res = np.array([])
self.residual = np.zeros([self.num_vars])
self.increment = np.ones([self.num_vars])
self.jacobian = np.zeros((self.num_vars, self.num_vars))
self.start_time = time()
self.progress = True
if self.iterinfo:
self.print_iterinfo_head()
for self.iter in range(self.max_iter):
self.increment_filter = np.absolute(self.increment) < err ** 2
self.solve_control()
self.res = np.append(self.res, norm(self.residual))
if self.iterinfo:
self.print_iterinfo_body()
if ((self.iter >= self.min_iter and self.res[-1] < err ** 0.5) or
self.lin_dep):
break
if self.iter > 40:
if (all(self.res[(self.iter - 3):] >= self.res[-3] * 0.95) and
self.res[-1] >= self.res[-2] * 0.95):
self.progress = False
break
self.end_time = time()
self.print_iterinfo_tail()
if self.iter == self.max_iter - 1:
msg = ('Reached maximum iteration count (' + str(self.max_iter) +
'), calculation stopped. Residual value is '
'{:.2e}'.format(norm(self.residual)))
logging.warning(msg)
[docs] def solve_determination(self):
r"""Check, if the number of supplied parameters is sufficient."""
# number of variables per connection
self.num_conn_vars = len(self.fluids) + 3
# number of user defined functions
self.num_ude_eq = len(self.user_defined_eq)
for func in self.user_defined_eq.values():
# remap connection objects
func.conns = [
self.conns.loc[c.label, 'object'] for c in func.conns]
# remap jacobian
func.jacobian = {
c: np.zeros(self.num_conn_vars)
for c in func.conns}
# total number of variables
self.num_vars = (
self.num_conn_vars * len(self.conns.index) + self.num_comp_vars)
msg = 'Number of connection equations: ' + str(self.num_conn_eq) + '.'
logging.debug(msg)
msg = 'Number of bus equations: ' + str(self.num_bus_eq) + '.'
logging.debug(msg)
msg = 'Number of component equations: ' + str(self.num_comp_eq) + '.'
logging.debug(msg)
msg = 'Number of user defined equations: ' + str(self.num_ude_eq) + '.'
logging.debug(msg)
msg = 'Total number of variables: ' + str(self.num_vars) + '.'
logging.debug(msg)
msg = 'Number of component variables: ' + str(self.num_comp_vars) + '.'
logging.debug(msg)
msg = ('Number of connection variables: ' +
str(self.num_conn_vars * len(self.conns.index)) + '.')
logging.debug(msg)
n = (
self.num_comp_eq + self.num_conn_eq +
self.num_bus_eq + self.num_ude_eq)
if n > self.num_vars:
msg = ('You have provided too many parameters: ' +
str(self.num_vars) + ' required, ' + str(n) +
' supplied. Aborting calculation!')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
elif n < self.num_vars:
msg = ('You have not provided enough parameters: '
+ str(self.num_vars) + ' required, ' + str(n) +
' supplied. Aborting calculation!')
logging.error(msg)
raise hlp.TESPyNetworkError(msg)
[docs] def print_iterinfo_head(self):
"""Print head of convergence progress."""
if self.num_comp_vars == 0:
# iterinfo printout without any custom variables
msg = (
'iter\t| residual | massflow | pressure | enthalpy | fluid\n')
msg += '-' * 8 + '+----------' * 4 + '+' + '-' * 9
else:
# iterinfo printout with custom variables in network
msg = ('iter\t| residual | massflow | pressure | enthalpy | '
'fluid | custom\n')
msg += '-' * 8 + '+----------' * 5 + '+' + '-' * 9
print(msg)
[docs] def print_iterinfo_body(self):
"""Print convergence progress."""
vec = self.increment[0:-(self.num_comp_vars + 1)]
msg = (str(self.iter + 1))
if not self.lin_dep and not np.isnan(norm(self.residual)):
msg += '\t| ' + '{:.2e}'.format(norm(self.residual))
msg += ' | ' + '{:.2e}'.format(norm(vec[0::self.num_conn_vars]))
msg += ' | ' + '{:.2e}'.format(norm(vec[1::self.num_conn_vars]))
msg += ' | ' + '{:.2e}'.format(norm(vec[2::self.num_conn_vars]))
ls = []
for f in range(len(self.fluids)):
ls += vec[3 + f::self.num_conn_vars].tolist()
msg += ' | ' + '{:.2e}'.format(norm(ls))
if self.num_comp_vars > 0:
msg += ' | ' + '{:.2e}'.format(norm(
self.increment[-self.num_comp_vars:]))
else:
if np.isnan(norm(self.residual)):
msg += '\t| nan'
else:
msg += '\t| ' + '{:.2e}'.format(norm(self.residual))
msg += ' | nan' * 4
if self.num_comp_vars > 0:
msg += ' | nan'
print(msg)
[docs] def print_iterinfo_tail(self):
"""Print tail of convergence progress."""
msg = (
'Total iterations: ' + str(self.iter + 1) + ', Calculation '
'time: ' + str(round(self.end_time - self.start_time, 1)) +
' s, Iterations per second: ')
ips = 'inf'
if self.end_time != self.start_time:
ips = str(round(
(self.iter + 1) / (self.end_time - self.start_time), 2))
msg += ips
logging.debug(msg)
if self.iterinfo:
if self.num_comp_vars == 0:
print('-' * 8 + '+----------' * 4 + '+' + '-' * 9)
else:
print('-' * 8 + '+----------' * 5 + '+' + '-' * 9)
print(msg)
[docs] def matrix_inversion(self):
"""Invert matrix of derivatives and caluclate increment."""
self.lin_dep = True
try:
# Let the matrix inversion be computed by the GPU if use_cuda in
# global_vars.py is true.
if self.use_cuda:
self.increment = cu.asnumpy(cu.dot(
cu.linalg.inv(cu.asarray(self.jacobian)),
-cu.asarray(self.residual)))
else:
self.increment = np.linalg.inv(
self.jacobian).dot(-self.residual)
self.lin_dep = False
except np.linalg.linalg.LinAlgError:
self.increment = self.residual * 0
[docs] def solve_control(self):
r"""
Control iteration step of the newton algorithm.
- Calculate the residual value for each equation
- Calculate the jacobian matrix
- Calculate new values for variables
- Restrict fluid properties to value ranges
- Check component parameters for consistency
"""
self.solve_components()
self.solve_busses()
self.solve_connections()
self.solve_user_defined_eq()
self.matrix_inversion()
# check for linear dependency
if self.lin_dep:
return
# add the increment
i = 0
for c in self.conns['object']:
# mass flow, pressure and enthalpy
if not c.m.val_set:
c.m.val_SI += self.increment[i * (self.num_conn_vars)]
if not c.p.val_set:
# this prevents negative pressures
relax = max(1, -self.increment[i * (self.num_conn_vars) + 1] /
(0.5 * c.p.val_SI))
c.p.val_SI += self.increment[
i * (self.num_conn_vars) + 1] / relax
if not c.h.val_set:
c.h.val_SI += self.increment[i * (self.num_conn_vars) + 2]
# fluid vector (only if number of fluids is greater than 1)
if len(self.fluids) > 1:
j = 0
for fluid in self.fluids:
# add increment
if not c.fluid.val_set[fluid]:
c.fluid.val[fluid] += (
self.increment[
i * (self.num_conn_vars) + 3 + j])
# keep mass fractions within [0, 1]
if c.fluid.val[fluid] < err:
c.fluid.val[fluid] = 0
elif c.fluid.val[fluid] > 1 - err:
c.fluid.val[fluid] = 1
j += 1
# check the fluid properties for physical ranges
self.solve_check_props(c)
i += 1
# increment for the custom variables
if self.num_comp_vars > 0:
sum_c_var = 0
for cp in self.comps['object']:
for var in cp.vars.keys():
pos = var.var_pos
# add increment
var.val += self.increment[
self.num_conn_vars * len(self.conns) + sum_c_var + pos]
# keep value within specified value range
if var.val < var.min_val:
var.val = var.min_val
elif var.val > var.max_val:
var.val = var.max_val
sum_c_var += cp.num_vars
# second property check for first three iterations without an init_file
if self.iter < 3:
for cp in self.comps['object']:
cp.convergence_check()
for c in self.conns['object']:
self.solve_check_props(c)
[docs] def property_range_message(self, c, prop):
r"""
Return debugging message for fluid property range adjustments.
Parameters
----------
c : tespy.connections.connection.Connection
Connection to check fluid properties.
prop : str
Fluid property.
Returns
-------
msg : str
Debugging message.
"""
msg = (
fpd[prop]['text'][0].upper() + fpd[prop]['text'][1:] +
' out of fluid property range at connection ' + c.label +
' adjusting value to ' + str(c.get_attr(prop).val_SI) +
' ' + fpd[prop]['SI_unit'] + '.')
return msg
[docs] def solve_check_props(self, c):
r"""
Check for invalid fluid property values.
Parameters
----------
c : tespy.connections.connection.Connection
Connection to check fluid properties.
"""
fl = hlp.single_fluid(c.fluid.val)
if fl is not None:
# pressure
if c.p.val_SI < fp.Memorise.value_range[fl][0] and not c.p.val_set:
c.p.val_SI = fp.Memorise.value_range[fl][0]
logging.debug(self.property_range_message(c, 'p'))
elif (c.p.val_SI > fp.Memorise.value_range[fl][1] and
not c.p.val_set):
c.p.val_SI = fp.Memorise.value_range[fl][1]
logging.debug(self.property_range_message(c, 'p'))
# enthalpy
try:
hmin = fp.h_pT(
c.p.val_SI, fp.Memorise.value_range[fl][2] * 1.001, fl)
except ValueError:
f = 1.05
hmin = fp.h_pT(
c.p.val_SI, fp.Memorise.value_range[fl][2] * f, fl)
T = fp.Memorise.value_range[fl][3]
while True:
try:
hmax = fp.h_pT(c.p.val_SI, T, fl)
break
except ValueError as e:
T *= 0.99
if T < fp.Memorise.value_range[fl][2]:
raise ValueError(e)
if c.h.val_SI < hmin and not c.h.val_set:
if hmin < 0:
c.h.val_SI = hmin * 0.9999
else:
c.h.val_SI = hmin * 1.0001
logging.debug(self.property_range_message(c, 'h'))
elif c.h.val_SI > hmax and not c.h.val_set:
c.h.val_SI = hmax * 0.9999
logging.debug(self.property_range_message(c, 'h'))
if ((c.Td_bp.val_set or c.state.is_set) and
not c.h.val_set and self.iter < 3):
if (c.Td_bp.val_SI > 0 or
(c.state.val == 'g' and c.state.is_set)):
h = fp.h_mix_pQ(c.get_flow(), 1)
if c.h.val_SI < h:
c.h.val_SI = h * 1.01
logging.debug(self.property_range_message(c, 'h'))
elif (c.Td_bp.val_SI < 0 or
(c.state.val == 'l' and c.state.is_set)):
h = fp.h_mix_pQ(c.get_flow(), 0)
if c.h.val_SI > h:
c.h.val_SI = h * 0.99
logging.debug(self.property_range_message(c, 'h'))
elif self.iter < 4 and not c.good_starting_values:
# pressure
if c.p.val_SI <= self.p_range_SI[0] and not c.p.val_set:
c.p.val_SI = self.p_range_SI[0]
logging.debug(self.property_range_message(c, 'p'))
elif c.p.val_SI >= self.p_range_SI[1] and not c.p.val_set:
c.p.val_SI = self.p_range_SI[1]
logging.debug(self.property_range_message(c, 'p'))
# enthalpy
if c.h.val_SI < self.h_range_SI[0] and not c.h.val_set:
c.h.val_SI = self.h_range_SI[0]
logging.debug(self.property_range_message(c, 'h'))
elif c.h.val_SI > self.h_range_SI[1] and not c.h.val_set:
c.h.val_SI = self.h_range_SI[1]
logging.debug(self.property_range_message(c, 'h'))
# temperature
if c.T.val_set and not c.h.val_set:
self.solve_check_temperature(c)
# mass flow
if c.m.val_SI <= self.m_range_SI[0] and not c.m.val_set:
c.m.val_SI = self.m_range_SI[0]
logging.debug(self.property_range_message(c, 'm'))
elif c.m.val_SI >= self.m_range_SI[1] and not c.m.val_set:
c.m.val_SI = self.m_range_SI[1]
logging.debug(self.property_range_message(c, 'm'))
[docs] def solve_check_temperature(self, c):
r"""
Check if temperature is within user specified limits.
Parameters
----------
c : tespy.connections.connection.Connection
Connection to check fluid properties.
"""
flow = c.get_flow()
Tmin = max(
[fp.Memorise.value_range[f][2] for
f in flow[3].keys() if flow[3][f] > err]
) + 100
Tmax = min(
[fp.Memorise.value_range[f][3] for
f in flow[3].keys() if flow[3][f] > err]
) - 100
hmin = fp.h_mix_pT(flow, Tmin)
hmax = fp.h_mix_pT(flow, Tmax)
if c.h.val_SI < hmin:
c.h.val_SI = hmin
logging.debug(self.property_range_message(c, 'h'))
if c.h.val_SI > hmax:
c.h.val_SI = hmax
logging.debug(self.property_range_message(c, 'h'))
[docs] def solve_components(self):
r"""
Calculate the residual and derivatives of component equations.
- Iterate through components in network to get residuals and
derivatives.
- Place residual values in residual value vector of the network.
- Place partial derivatives in jacobian matrix of the network.
"""
# fetch component equation residuals and component partial derivatives
sum_eq = 0
sum_c_var = 0
for cp in self.comps['object']:
indices = []
for c in cp.conn_loc:
start = c * self.num_conn_vars
end = (c + 1) * self.num_conn_vars
indices += [np.arange(start, end)]
cp.solve(self.increment_filter[np.array(indices)])
self.residual[sum_eq:sum_eq + cp.num_eq] = cp.residual
deriv = cp.jacobian
if deriv is not None:
i = 0
# place derivatives in jacobian matrix
for loc in cp.conn_loc:
coll_s = loc * self.num_conn_vars
coll_e = (loc + 1) * self.num_conn_vars
self.jacobian[
sum_eq:sum_eq + cp.num_eq, coll_s:coll_e] = deriv[:, i]
i += 1
# derivatives for custom variables
for j in range(cp.num_vars):
coll = self.num_vars - self.num_comp_vars + sum_c_var
self.jacobian[sum_eq:sum_eq + cp.num_eq, coll] = (
deriv[:, i + j, :1].transpose()[0])
sum_c_var += 1
sum_eq += cp.num_eq
cp.it += 1
[docs] def solve_user_defined_eq(self):
"""
Calculate the residual and jacobian of user defined equations.
- Iterate through user defined functions and calculate residual value
and corresponding jacobian.
- Place residual values in residual value vector of the network.
- Place partial derivatives regarding connection parameters in jacobian
matrix of the network.
"""
row = self.num_comp_eq + self.num_conn_eq + self.num_bus_eq
for ude in self.user_defined_eq.values():
self.residual[row] = ude.func(ude)
jacobian = ude.deriv(ude)
for c, derivative in jacobian.items():
col = c.conn_loc * self.num_conn_vars
self.jacobian[row, col:col + self.num_conn_vars] = derivative
row += 1
[docs] def solve_connections(self):
r"""
Calculate the residual and derivatives of connection equations.
- Iterate through connections in network to get residuals and
derivatives.
- Place residual values in residual value vector of the network.
- Place partial derivatives in jacobian matrix of the network.
Note
----
**Equations**
**mass flow, pressure and enthalpy**
.. math::
val = 0
**temperatures**
.. math::
val = T_{j} - T \left( p_{j}, h_{j}, fluid_{j} \right)
**volumetric flow**
.. math::
val = \dot{V}_{j} - v \left( p_{j}, h_{j} \right) \cdot \dot{m}_j
**superheating or subcooling** *Works with pure fluids only!*
.. math::
val = T_{j} - td_{bp} - T_{bp}\left( p_{j}, fluid_{j} \right)
\text{td: temperature difference, bp: boiling point}
**vapour mass fraction** *Works with pure fluids only!*
.. math::
val = h_{j} - h \left( p_{j}, x_{j}, fluid_{j} \right)
**Referenced values**
**mass flow, pressure and enthalpy**
.. math::
val = x_{j} - x_{j,ref} \cdot a + b
**temperatures**
.. math::
val = T \left( p_{j}, h_{j}, fluid_{j} \right) -
T \left( p_{j}, h_{j}, fluid_{j} \right) \cdot a + b
**Derivatives**
**mass flow, pressure and enthalpy**
.. math::
J\left(\frac{\partial f_{i}}{\partial m_{j}}\right) = 1\\
\text{for equation i, connection j}\\
\text{pressure and enthalpy analogously}
**temperatures**
.. math::
J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) =
-\frac{\partial T_{j}}{\partial p_{j}}\\
J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) =
-\frac{\partial T_{j}}{\partial h_{j}}\\
J\left(\frac{\partial f_{i}}{\partial fluid_{j,k}}\right) =
- \frac{\partial T_{j}}{\partial fluid_{j,k}}
\forall k \in \text{fluid components}\\
\text{for equation i, connection j}
**volumetric flow**
.. math::
J\left(\frac{\partial f_{i}}{\partial m_{j}}\right) =
-v \left( p_{j}, h_{j} \right)\\
J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) =
-\frac{\partial v_{j}}{\partial p_{j}} \cdot \dot{m}_j\\
J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) =
-\frac{\partial v_{j}}{\partial h_{j}} \cdot \dot{m}_j\\
\forall k \in \text{fluid components}\\
\text{for equation i, connection j}
**superheating or subcooling** *Works with pure fluids only!*
.. math::
J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) =
\frac{\partial T \left( p_{j}, h_{j}, fluid_{j} \right)}
{\partial p_{j}} -
\frac{\partial T_{bp} \left( p_{j}, fluid_{j} \right)}
{\partial p_{j}} \\
J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) =
\frac{\partial T \left( p_{j}, h_{j}, fluid_{j} \right)}
{\partial h_{j}}\\
\text{for equation i, connection j}\\
\text{td: temperature difference, bp: boiling point}
**vapour mass fraction** *Works with pure fluids only!*
.. math::
J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) =
-\frac{\partial h \left( p_{j}, x_{j}, fluid_{j} \right)}
{\partial p_{j}}\\
J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) = 1\\
\text{for equation i, connection j, x: vapour mass fraction}
**Referenced values**
**mass flow, pressure and enthalpy**
.. math::
J\left(\frac{\partial f_{i}}{\partial m_{j}}\right) = 1\\
J\left(\frac{\partial f_{i}}{\partial m_{j,ref}}\right) = - a\\
\text{for equation i, connection j}\\
\text{pressure and enthalpy analogously}
**temperatures**
.. math::
J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) =
\frac{dT_{j}}{dp_{j}}\\
J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) =
\frac{dT_{j}}{dh_{j}}\\
J\left(\frac{\partial f_{i}}{\partial fluid_{j,k}}\right) =
\frac{dT_{j}}{dfluid_{j,k}}
\; , \forall k \in \text{fluid components}\\
J\left(\frac{\partial f_{i}}{\partial p_{j,ref}}\right) =
\frac{dT_{j,ref}}{dp_{j,ref}} \cdot a \\
J\left(\frac{\partial f_{i}}{\partial h_{j,ref}}\right) =
\frac{dT_{j,ref}}{dh_{j,ref}} \cdot a \\
J\left(\frac{\partial f_{i}}{\partial fluid_{j,k,ref}}\right) =
\frac{dT_{j}}{dfluid_{j,k,ref}} \cdot a
\; , \forall k \in \text{fluid components}\\
\text{for equation i, connection j}
"""
k = self.num_comp_eq
primary_vars = {'m': 0, 'p': 1, 'h': 2}
for c in self.conns['object']:
flow = c.get_flow()
col = c.conn_loc * self.num_conn_vars
# referenced mass flow, pressure or enthalpy
for var, pos in primary_vars.items():
if c.get_attr(var).ref_set:
ref = c.get_attr(var).ref
ref_col = ref.obj.conn_loc * self.num_conn_vars
self.residual[k] = (
c.get_attr(var).val_SI - (
ref.obj.get_attr(var).val_SI * ref.factor +
ref.delta_SI))
self.jacobian[k, col + pos] = 1
self.jacobian[k, ref_col + pos] = -c.get_attr(var).ref.factor
k += 1
# temperature
if c.T.val_set:
self.residual[k] = fp.T_mix_ph(
flow, T0=c.T.val_SI) - c.T.val_SI
self.jacobian[k, col + 1] = (
fp.dT_mix_dph(flow, T0=c.T.val_SI))
self.jacobian[k, col + 2] = (
fp.dT_mix_pdh(flow, T0=c.T.val_SI))
if len(self.fluids) != 1:
col_s = c.conn_loc * self.num_conn_vars + 3
col_e = (c.conn_loc + 1) * self.num_conn_vars
if not all(self.increment_filter[col_s:col_e]):
self.jacobian[k, col_s:col_e] = fp.dT_mix_ph_dfluid(
flow, T0=c.T.val_SI)
k += 1
# referenced temperature
if c.T.ref_set:
ref = c.T.ref
flow_ref = ref.obj.get_flow()
ref_col = ref.obj.conn_loc * self.num_conn_vars
self.residual[k] = fp.T_mix_ph(flow, T0=c.T.val_SI) - (
fp.T_mix_ph(flow_ref, T0=ref.obj.T.val_SI) *
ref.factor + ref.delta_SI)
self.jacobian[k, col + 1] = (
fp.dT_mix_dph(flow, T0=c.T.val_SI))
self.jacobian[k, col + 2] = (
fp.dT_mix_pdh(flow, T0=c.T.val_SI))
self.jacobian[k, ref_col + 1] = -(
fp.dT_mix_dph(flow_ref, T0=ref.obj.T.val_SI) * ref.factor)
self.jacobian[k, ref_col + 2] = -(
fp.dT_mix_pdh(flow_ref, T0=ref.obj.T.val_SI) * ref.factor)
# dT / dFluid
if len(self.fluids) != 1:
col_s = c.conn_loc * self.num_conn_vars + 3
col_e = (c.conn_loc + 1) * self.num_conn_vars
ref_col_s = ref.obj.conn_loc * self.num_conn_vars + 3
ref_col_e = (ref.obj.conn_loc + 1) * self.num_conn_vars
if not all(self.increment_filter[col_s:col_e]):
self.jacobian[k, col_s:col_e] = (
fp.dT_mix_ph_dfluid(flow, T0=c.T.val_SI))
if not all(self.increment_filter[ref_col_s:ref_col_e]):
self.jacobian[k, ref_col_s:ref_col_e] = -np.array([
fp.dT_mix_ph_dfluid(
flow_ref, T0=ref.obj.T.val_SI)])
k += 1
# saturated steam fraction
if c.x.val_set:
if (np.absolute(self.residual[k]) > err ** 2 or
self.iter % 2 == 0 or self.always_all_equations):
self.residual[k] = c.h.val_SI - (
fp.h_mix_pQ(flow, c.x.val_SI))
if not self.increment_filter[col + 1]:
self.jacobian[k, col + 1] = -(
fp.dh_mix_dpQ(flow, c.x.val_SI))
self.jacobian[k, col + 2] = 1
k += 1
# volumetric flow
if c.v.val_set:
if (np.absolute(self.residual[k]) > err ** 2 or
self.iter % 2 == 0 or self.always_all_equations):
self.residual[k] = (
fp.v_mix_ph(flow, T0=c.T.val_SI) * c.m.val_SI -
c.v.val_SI)
self.jacobian[k, col] = fp.v_mix_ph(flow, T0=c.T.val_SI)
self.jacobian[k, col + 1] = (
fp.dv_mix_dph(flow, T0=c.T.val_SI) * c.m.val_SI)
self.jacobian[k, col + 2] = (
fp.dv_mix_pdh(flow, T0=c.T.val_SI) * c.m.val_SI)
k += 1
# referenced volumetric flow
if c.v.ref_set:
ref = c.v.ref
flow_ref = ref.obj.get_flow()
ref_col = ref.obj.conn_loc * self.num_conn_vars
v = fp.v_mix_ph(flow, T0=c.T.val_SI)
v_ref = fp.v_mix_ph(flow_ref, T0=ref.obj.T.val_SI)
self.residual[k] = (
(v * c.m.val_SI)
- ((v_ref * ref.obj.m.val_SI) * ref.factor + ref.delta_SI)
)
self.jacobian[k, col] = v
self.jacobian[k, col + 1] = (
fp.dv_mix_dph(flow, T0=c.T.val_SI) * c.m.val_SI
)
self.jacobian[k, col + 2] = (
fp.dv_mix_pdh(flow, T0=c.T.val_SI) * c.m.val_SI
)
self.jacobian[k, ref_col] = -v_ref * ref.factor
self.jacobian[k, ref_col + 1] = -(
fp.dv_mix_dph(flow_ref, T0=ref.obj.T.val_SI)
* ref.factor * ref.obj.m.val_SI
)
self.jacobian[k, ref_col + 2] = -(
fp.dv_mix_pdh(flow_ref, T0=ref.obj.T.val_SI)
* ref.factor * ref.obj.m.val_SI
)
k += 1
# temperature difference to boiling point
if c.Td_bp.val_set:
if (np.absolute(self.residual[k]) > err ** 2 or
self.iter % 2 == 0 or self.always_all_equations):
self.residual[k] = (
fp.T_mix_ph(flow, T0=c.T.val_SI) - c.Td_bp.val_SI -
fp.T_bp_p(flow))
if not self.increment_filter[col + 1]:
self.jacobian[k, col + 1] = (
fp.dT_mix_dph(flow, T0=c.T.val_SI) - fp.dT_bp_dp(flow))
if not self.increment_filter[col + 2]:
self.jacobian[k, col + 2] = fp.dT_mix_pdh(
flow, T0=c.T.val_SI)
k += 1
# fluid composition balance
if c.fluid.balance:
j = 0
res = 1
for f in self.fluids:
res -= c.fluid.val[f]
self.jacobian[k, c.conn_loc + 3 + j] = -1
j += 1
self.residual[k] = res
k += 1
# equations and derivatives for specified primary variables are static
if self.iter == 0:
for c in self.conns['object']:
col = c.conn_loc * self.num_conn_vars
# specified mass flow, pressure and enthalpy
for var, pos in primary_vars.items():
if c.get_attr(var).val_set:
self.residual[k] = 0
self.jacobian[k, col + pos] = 1
k += 1
j = 0
# specified fluid mass fraction
for f in self.fluids:
if c.fluid.val_set[f]:
self.jacobian[k, col + 3 + j] = 1
k += 1
j += 1
[docs] def solve_busses(self):
r"""
Calculate the equations and the partial derivatives for the busses.
- Iterate through busses in network to get residuals and derivatives.
- Place residual values in residual value vector of the network.
- Place partial derivatives in jacobian matrix of the network.
"""
row = self.num_comp_eq + self.num_conn_eq
for bus in self.busses.values():
if bus.P.is_set:
P_res = 0
for cp in bus.comps.index:
P_res -= cp.calc_bus_value(bus)
deriv = -cp.bus_deriv(bus)
j = 0
for loc in cp.conn_loc:
# start collumn index
coll_s = loc * self.num_conn_vars
# end collumn index
coll_e = (loc + 1) * self.num_conn_vars
self.jacobian[row, coll_s:coll_e] = deriv[:, j]
j += 1
self.residual[row] = bus.P.val + P_res
row += 1
[docs] def postprocessing(self):
r"""Calculate connection, bus and component parameters."""
self.process_connections()
self.process_components()
self.process_busses()
msg = 'Postprocessing complete.'
logging.info(msg)
[docs] def process_connections(self):
"""Process the Connection results."""
for c in self.conns['object']:
flow = c.get_flow()
c.good_starting_values = True
c.T.val_SI = fp.T_mix_ph(flow, T0=c.T.val_SI)
fluid = hlp.single_fluid(c.fluid.val)
if (fluid is None and
abs(
fp.h_mix_pT(flow, c.T.val_SI) - c.h.val_SI
) > err ** .5):
c.T.val_SI = np.nan
c.vol.val_SI = np.nan
c.v.val_SI = np.nan
c.s.val_SI = np.nan
msg = (
'Could not find a feasible value for mixture temperature '
'at connection ' + c.label + '. The values for '
'temperature, specific volume, volumetric flow and '
'entropy are set to nan.')
logging.error(msg)
else:
c.vol.val_SI = fp.v_mix_ph(flow, T0=c.T.val_SI)
c.v.val_SI = c.vol.val_SI * c.m.val_SI
c.s.val_SI = fp.s_mix_ph(flow, T0=c.T.val_SI)
if fluid is not None:
if not c.x.val_set:
c.x.val_SI = fp.Q_ph(c.p.val_SI, c.h.val_SI, fluid)
if not c.Td_bp.val_set:
c.Td_bp.val_SI = np.nan
for prop in fpd.keys():
c.get_attr(prop).val = hlp.convert_from_SI(
prop, c.get_attr(prop).val_SI, c.get_attr(prop).unit)
c.m.val0 = c.m.val
c.p.val0 = c.p.val
c.h.val0 = c.h.val
c.fluid.val0 = c.fluid.val.copy()
self.results['Connection'].loc[c.label] = (
[c.m.val, c.p.val, c.h.val, c.T.val, c.v.val, c.vol.val,
c.s.val, c.x.val, c.Td_bp.val] +
[f for f in c.fluid.val.values()])
[docs] def process_components(self):
"""Process the component results."""
# components
for cp in self.comps['object']:
cp.calc_parameters()
cp.check_parameter_bounds()
key = cp.__class__.__name__
for param in self.results[key].columns:
p = cp.get_attr(param)
if (p.func is not None or (p.func is None and p.is_set) or
p.is_result):
self.results[key].loc[cp.label, param] = p.val
else:
self.results[key].loc[cp.label, param] = np.nan
[docs] def process_busses(self):
"""Process the bus results."""
# busses
for b in self.busses.values():
for cp in b.comps.index:
# get components bus func value
bus_val = cp.calc_bus_value(b)
eff = cp.calc_bus_efficiency(b)
cmp_val = cp.bus_func(b.comps.loc[cp])
b.comps.loc[cp, 'char'].get_domain_errors(
cp.calc_bus_expr(b), cp.label)
# save as reference value
if self.mode == 'design':
if b.comps.loc[cp, 'base'] == 'component':
design_value = cmp_val
else:
design_value = bus_val
b.comps.loc[cp, 'P_ref'] = design_value
else:
design_value = b.comps.loc[cp, 'P_ref']
self.results[b.label].loc[cp.label] = (
[cmp_val, bus_val, eff, design_value])
b.P.val = self.results[b.label]['bus value'].sum()
# %% printing and plotting
[docs] def print_results(self, colored=True, colors={}):
r"""Print the calculations results to prompt."""
# Define colors for highlighting values in result table
coloring = {
'end': '\033[0m',
'set': '\033[94m',
'err': '\033[31m',
'var': '\033[32m'
}
coloring.update(colors)
if not hasattr(self, 'results'):
msg = (
'It is not possible to print the results of a network, that '
'has never been solved successfully. Results DataFrames are '
'only available after a full simulation run is performed.')
raise hlp.TESPyNetworkError(msg)
for cp in self.comps['comp_type'].unique():
df = self.results[cp].copy()
# are there any parameters to print?
if df.size > 0:
cols = df.columns
if len(cols) > 0:
for col in cols:
df[col] = df.apply(
self.print_components, axis=1,
args=(col, colored, coloring))
df.dropna(how='all', inplace=True)
if len(df) > 0:
# printout with tabulate
print('##### RESULTS (' + cp + ') #####')
print(
tabulate(
df, headers='keys', tablefmt='psql',
floatfmt='.2e'
)
)
# connection properties
df = self.results['Connection'].loc[:, ['m', 'p', 'h', 'T']]
for c in df.index:
if not self.get_conn(c).printout:
df.drop([c], axis=0, inplace=True)
elif colored:
conn = self.get_conn(c)
for col in df.columns:
if conn.get_attr(col).val_set:
df.loc[c, col] = (
coloring['set'] + str(conn.get_attr(col).val) +
coloring['end'])
if len(df) > 0:
print('##### RESULTS (Connection) #####')
print(
tabulate(df, headers='keys', tablefmt='psql', floatfmt='.3e')
)
for b in self.busses.values():
if b.printout:
df = self.results[b.label].loc[
:, ['component value', 'bus value', 'efficiency']]
df.loc['total'] = df.sum()
df.loc['total', 'efficiency'] = np.nan
if colored and b.P.is_set:
df.loc['total', 'bus value'] = (
coloring['set'] + str(df.loc['total', 'bus value']) +
coloring['end'])
print('##### RESULTS (Bus: ' + b.label + ') #####')
print(
tabulate(
df, headers='keys', tablefmt='psql',
floatfmt='.3e'
)
)
[docs] def print_components(self, c, *args):
"""
Get the print values for the component data.
Parameters
----------
c : pandas.core.series.Series
Series containing the component data.
param : str
Component parameter to print.
colored : booloean
Color the printout.
coloring : dict
Coloring information for colored printout.
Returns
----------
value : str
String representation of the value to print.
"""
param, colored, coloring = args
comp = self.get_comp(c.name)
if comp.printout:
# select parameter from results DataFrame
val = c[param]
if not colored:
return str(val)
# else part
if (val < comp.get_attr(param).min_val - err or
val > comp.get_attr(param).max_val + err):
return coloring['err'] + ' ' + str(val) + ' ' + coloring['end']
if comp.get_attr(args[0]).is_var:
return coloring['var'] + ' ' + str(val) + ' ' + coloring['end']
if comp.get_attr(args[0]).is_set:
return coloring['set'] + ' ' + str(val) + ' ' + coloring['end']
return str(val)
else:
return np.nan
# %% saving
[docs] def save(self, path, **kwargs):
r"""
Save the results to results files.
Parameters
----------
filename : str
Path for the results.
Note
----
Results will be saved to path. The results contain:
- network.json (network information)
- connections.csv (connection information)
- folder components containing .csv files for busses and
characteristics as well as .csv files for all types of components
within your network.
"""
if path[-1] != '/' and path[-1] != '\\':
path += '/'
path = hlp.modify_path_os(path)
logging.debug('Saving network to path ' + path + '.')
# creat path, if non existent
if not os.path.exists(path):
os.makedirs(path)
# create path for component folder if non existent
path_comps = hlp.modify_path_os(path + 'components/')
if not os.path.exists(path_comps):
os.makedirs(path_comps)
# save all network information
self.save_network(path + 'network.json')
self.save_connections(path + 'connections.csv')
self.save_components(path_comps)
self.save_busses(path_comps + 'bus.csv')
self.save_characteristics(path_comps)
[docs] def save_network(self, fn):
r"""
Save basic network configuration.
Parameters
----------
fn : str
Path/filename for the network configuration file.
"""
data = {}
data['m_unit'] = self.m_unit
data['m_range'] = list(self.m_range)
data['p_unit'] = self.p_unit
data['p_range'] = list(self.p_range)
data['h_unit'] = self.h_unit
data['h_range'] = list(self.h_range)
data['T_unit'] = self.T_unit
data['x_unit'] = self.x_unit
data['v_unit'] = self.v_unit
data['s_unit'] = self.s_unit
data['fluids'] = self.fluids_backends
with open(fn, 'w') as f:
f.write(json.dumps(data, indent=4))
logging.debug('Network information saved to ' + fn + '.')
[docs] def save_connections(self, fn):
r"""
Save the connection properties.
- Uses connections object id as row identifier and saves
- connections source and target as well as
- properties with references and
- fluid vector (including user specification if structure is True).
- Connections source and target are identified by its labels.
Parameters
----------
fn : str
Path/filename for the file.
"""
f = Network.get_props
df = self.conns.copy()
df.set_index('object', inplace=True)
# connection id
df['id'] = df.apply(Network.get_id, axis=1)
cols = df.columns.tolist()
df = df[cols[-1:] + cols[:-1]]
# general connection parameters
# source
df['source'] = df.apply(f, axis=1, args=('source', 'label'))
# target
df['target'] = df.apply(f, axis=1, args=('target', 'label'))
# design and offdesign properties
cols = ['label', 'design', 'offdesign', 'design_path', 'local_design',
'local_offdesign', 'label']
for key in cols:
df[key] = df.apply(f, axis=1, args=(key,))
# fluid properties
cols = ['m', 'p', 'h', 'T', 'x', 'v', 'Td_bp']
for key in cols:
# values and units
df[key] = df.apply(f, axis=1, args=(key, 'val'))
df[key + '_unit'] = df.apply(f, axis=1, args=(key, 'unit'))
df[key + '0'] = df.apply(f, axis=1, args=(key, 'val0'))
df[key + '_set'] = df.apply(f, axis=1, args=(key, 'val_set'))
df[key + '_ref'] = df.apply(
f, axis=1, args=(key, 'ref', 'obj',)).astype(str)
df[key + '_ref'] = df[key + '_ref'].str.extract(
r' at (.*?)>', expand=False)
df[key + '_ref_f'] = df.apply(
f, axis=1, args=(key, 'ref', 'factor',))
df[key + '_ref_d'] = df.apply(
f, axis=1, args=(key, 'ref', 'delta',))
df[key + '_ref_set'] = df.apply(f, axis=1, args=(key, 'ref_set',))
# state property
key = 'state'
df[key] = df.apply(f, axis=1, args=(key, 'val'))
df[key + '_set'] = df.apply(f, axis=1, args=(key, 'is_set'))
# fluid composition
for val in self.fluids:
# fluid mass fraction
df[val] = df.apply(f, axis=1, args=('fluid', 'val', val))
# fluid mass fraction parametrisation
df[val + '0'] = df.apply(f, axis=1, args=('fluid', 'val0', val))
df[val + '_set'] = df.apply(
f, axis=1, args=('fluid', 'val_set', val))
# fluid balance
df['balance'] = df.apply(f, axis=1, args=('fluid', 'balance'))
df.to_csv(fn, sep=';', decimal='.', index=False, na_rep='nan')
logging.debug('Connection information saved to ' + fn + '.')
[docs] def save_components(self, path):
r"""
Save the component properties.
- Uses components labels as row identifier.
- Writes:
- component's incomming and outgoing connections (object id) and
- component's parametrisation.
Parameters
----------
path : str
Path/filename for the file.
"""
busses = self.busses.values()
# create / overwrite csv file
df_comps = self.comps.copy()
df_comps.set_index('object', inplace=True)
# busses
df_comps['busses'] = df_comps.apply(
Network.get_busses, axis=1, args=(busses,))
for var in ['param', 'P_ref', 'char', 'base']:
df_comps['bus_' + var] = df_comps.apply(
Network.get_bus_data, axis=1, args=(busses, var))
pd.options.mode.chained_assignment = None
f = Network.get_props
for c in df_comps['comp_type'].unique():
df = df_comps[df_comps['comp_type'] == c]
# basic information
cols = ['label', 'design', 'offdesign', 'design_path',
'local_design', 'local_offdesign']
for col in cols:
df[col] = df.apply(f, axis=1, args=(col,))
# attributes
for col, data in df.index[0].variables.items():
# component characteristics container
if isinstance(data, dc_cc) or isinstance(data, dc_cm):
df[col] = df.apply(
f, axis=1, args=(col, 'char_func')).astype(str)
df[col] = df[col].str.extract(r' at (.*?)>', expand=False)
df[col + '_set'] = df.apply(
f, axis=1, args=(col, 'is_set'))
df[col + '_param'] = df.apply(
f, axis=1, args=(col, 'param'))
# component property container
elif isinstance(data, dc_cp):
df[col] = df.apply(f, axis=1, args=(col, 'val'))
df[col + '_set'] = df.apply(
f, axis=1, args=(col, 'is_set'))
df[col + '_var'] = df.apply(
f, axis=1, args=(col, 'is_var'))
# component property container
elif isinstance(data, dc_simple):
df[col] = df.apply(f, axis=1, args=(col, 'val'))
df[col + '_set'] = df.apply(
f, axis=1, args=(col, 'is_set'))
# component property container
elif isinstance(data, dc_gcp):
df[col] = df.apply(f, axis=1, args=(col, 'method'))
df.set_index('label', inplace=True)
fn = path + c + '.csv'
df.to_csv(fn, sep=';', decimal='.', index=True, na_rep='nan')
logging.debug(
'Component information (' + c + ') saved to ' + fn + '.')
[docs] def save_busses(self, fn):
r"""
Save the bus properties.
Parameters
----------
fn : str
Path/filename for the file.
"""
if len(self.busses) > 0:
df = pd.DataFrame(
{'id': self.busses.values()}, index=self.busses.values(),
dtype='object')
df['label'] = df.apply(Network.get_props, axis=1, args=('label',))
df['P'] = df.apply(Network.get_props, axis=1, args=('P', 'val'))
df['P_set'] = df.apply(Network.get_props, axis=1,
args=('P', 'is_set'))
df.drop('id', axis=1, inplace=True)
df.set_index('label', inplace=True)
df.to_csv(fn, sep=';', decimal='.', index=True, na_rep='nan')
logging.debug('Bus information saved to ' + fn + '.')
[docs] def save_characteristics(self, path):
r"""
Save the characteristics.
Parameters
----------
fn : str
Path/filename for the file.
"""
# characteristic lines in components
char_lines = []
char_maps = []
for c in self.comps['object']:
for col, data in c.variables.items():
if isinstance(data, dc_cc):
char_lines += [data.char_func]
elif isinstance(data, dc_cm):
char_maps += [data.char_func]
# characteristic lines in busses
for bus in self.busses.values():
for c in bus.comps.index:
ch = bus.comps.loc[c, 'char']
if ch not in char_lines:
char_lines += [ch]
# characteristic line export
if len(char_lines) > 0:
# get id and data
df = pd.DataFrame(
{'id': char_lines}, index=char_lines, dtype='object')
df['id'] = df.apply(Network.get_id, axis=1)
df['type'] = df.apply(Network.get_class_base, axis=1)
cols = ['x', 'y', 'extrapolate']
for val in cols:
df[val] = df.apply(Network.get_props, axis=1, args=(val,))
# write to char.csv
fn = path + 'char_line.csv'
df.to_csv(fn, sep=';', decimal='.', index=False, na_rep='nan')
logging.debug(
'Characteristic line information saved to ' + fn + '.')
if len(char_maps) > 0:
# get id and data
df = pd.DataFrame(
{'id': char_maps}, index=char_maps, dtype='object')
df['id'] = df.apply(Network.get_id, axis=1)
df['type'] = df.apply(Network.get_class_base, axis=1)
cols = ['x', 'y', 'z']
for val in cols:
df[val] = df.apply(Network.get_props, axis=1, args=(val,))
# write to char_map.csv
fn = path + 'char_map.csv'
df.to_csv(fn, sep=';', decimal='.', index=False, na_rep='nan')
logging.debug(
'Characteristic map information saved to ' + fn + '.')
[docs] @staticmethod
def get_id(c):
"""Return the id of the python object."""
return str(c.name)[str(c.name).find(' at ') + 4:-1]
[docs] @staticmethod
def get_class_base(c):
"""Return the class name."""
return c.name.__class__.__name__
[docs] @staticmethod
def get_props(c, *args):
"""Return properties."""
if hasattr(c.name, args[0]):
if (not isinstance(c.name.get_attr(args[0]), int) and
not isinstance(c.name.get_attr(args[0]), str) and
not isinstance(c.name.get_attr(args[0]), float) and
not isinstance(c.name.get_attr(args[0]), list) and
not isinstance(c.name.get_attr(args[0]), np.ndarray) and
not isinstance(c.name.get_attr(args[0]), con.Connection)):
if len(args) == 1:
return c.name.get_attr(args[0])
elif args[0] == 'fluid' and args[1] != 'balance':
return c.name.fluid.get_attr(args[1])[args[2]]
elif args[1] == 'ref':
obj = c.name.get_attr(args[0]).get_attr(args[1])
if obj is not None:
return obj.get_attr(args[2])
else:
return np.nan
else:
return c.name.get_attr(args[0]).get_attr(args[1])
elif isinstance(c.name.get_attr(args[0]), np.ndarray):
if len(c.name.get_attr(args[0]).shape) > 1:
return tuple(c.name.get_attr(args[0]).tolist())
else:
return c.name.get_attr(args[0]).tolist()
else:
return c.name.get_attr(args[0])
[docs] @staticmethod
def get_busses(c, *args):
"""Return the list of busses a component is integrated in."""
busses = []
for bus in args[0]:
if c.name in bus.comps.index:
busses += [bus.label]
return busses
[docs] @staticmethod
def get_bus_data(c, *args):
"""Return bus information of a component."""
items = []
if args[1] == 'char':
for bus in args[0]:
if c.name in bus.comps.index:
val = bus.comps.loc[c.name, args[1]]
items += [str(val)[str(val).find(' at ') + 4:-1]]
else:
for bus in args[0]:
if c.name in bus.comps.index:
items += [bus.comps.loc[c.name, args[1]]]
return items