# -*- coding: utf-8 -*-
##############################################################################
# Institute for the Design of Advanced Energy Systems Process Systems
# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018-2019, by the
# software owners: The Regents of the University of California, through
# Lawrence Berkeley National Laboratory, National Technology & Engineering
# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia
# University Research Corporation, et al. All rights reserved.
#
# Please see the files COPYRIGHT.txt and LICENSE.txt for full copyright and
# license information, respectively. Both files are also available online
# at the URL "https://github.com/IDAES/idaes-pse".
##############################################################################
from enum import Enum
from bokeh.models import Arrow, OpenHead, NormalHead
from bokeh.models import Label, HoverTool, CircleCross, X
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from statistics import median
import random
[docs]class HENStreamType(Enum):
"""Enum type defining hot and cold streams
"""
hot = 1
cold = 2
hot_utility = 3
cold_utility = 4
[docs]def turn_off_grid_and_axes_ticks(plot):
"""Turn off axis ticks and grid lines on a bokeh figure object.
Args:
plot: bokeh.plotting.plotting.figure instance.
Returns:
modified bokeh.plotting.plotting.figure instance.
Raises:
None
"""
plot.xaxis.major_tick_line_color = None
plot.xaxis.minor_tick_line_color = None
plot.yaxis.major_tick_line_color = None
plot.yaxis.minor_tick_line_color = None
plot.xaxis.visible = None
plot.yaxis.visible = None
plot.xgrid.grid_line_color = None
plot.ygrid.grid_line_color = None
return plot
[docs]def get_color_dictionary(set_to_color):
"""Given a set, return a dictionary of the form:
.. code-block:: python
{'set_member': valid_bokeh_color}
..
Args:
set_to_color: set of unique elements, e.g: [1,2,3] or ["1", "2", "3"]
Returns:
Dictionary of the form:
.. code-block:: python
{'set_member': valid_bokeh_color}
Raises:
None
"""
color_dict = {}
for stg in set_to_color:
def r(): return random.randint(0, 255)
color_dict[stg] = '#%02X%02X%02X' % (r(), r(), r())
return color_dict
[docs]def add_module_markers_to_heat_exchanger_plot(plot,
x, y, modules, line_color,
fill_color,
mark_modules_with_tooltips):
"""Plot module markers as tooltips to a heat exchanger network diagram.
Args:
plot : bokeh.plotting.plotting.figure instance.
x : x-axis coordinate of module marker tooltip.
y : y-axis coordinate of module marker tooltip.
modules : dict containing modules.
line_color : color of border of the module marker.
fill_color : color inside the module marker.
mark_modules_with_tooltips : whether to add tooltips to plot or not (currently not utilized).
Returns:
bokeh.plotting.plotting.figure instance with module markers added.
Raises:
None
"""
module_text_template = '{0} x {1} m^2, '
module_text = ''.join([module_text_template.format(
modules[m], m) for m in modules])[0:-2]
source = ColumnDataSource(data=dict(x=[x],
y=[y],
modules=['{0}'.format(
str(module_text))]))
marker_module = X(x=x, y=y,
line_color=line_color,
fill_color=fill_color,
size=5)
glyph_marker = plot.add_glyph(
source_or_glyph=source, glyph=marker_module)
hover_modules = HoverTool(renderers=[glyph_marker],
tooltips=[('Modules', '@modules')])
plot.add_tools(hover_modules)
return plot
[docs]def is_hot_or_cold_utility(exchanger):
"""Return if an exchanger is a hot or a cold utility by checking if it has the key `utility_type`.
Args:
exchanger: dict representing the exchanger.
Returns:
True if `utility_type` in the `exchanger` dict passed.
Raises:
None
"""
return 'utility_type' in exchanger
[docs]def get_stream_y_values(exchangers, hot_streams, cold_streams, y_stream_step=1):
"""Return a dict containing the layout of the heat exchanger diagram including any stage splits.
Args:
exchangers: List of exchangers where each exchanger is a dict of the form:
.. code-block:: python
{'hot': 'H2', 'cold': 'C1', 'Q': 1400, 'A': 159, 'annual_cost': 28358,
'stg': 2}
where hot is the hot stream name, cold is the cold stream name, A is the area (in m^2), annual_cost
is the annual cost in $, Q is the amount of heat transferred from one stream
to another in a given exchanger and stg is the stage the exchanger belongs to.
Additionally a 'utility_type' can specify if we draw the cold stream as water (:class:`idaes.vis.plot_utils.HENStreamType.cold_utility`)
or the hot stream as steam (:class:`idaes.vis.plot_utils.HENStreamType.hot_utility`).
Additionally, the exchanger could have the key 'modules', like this:
.. code-block:: python
{'hot': 'H1', 'cold': 'C1', 'Q': 667, 'A': 50, 'annual_cost': 10979, 'stg': 3,
'modules': {10: 1, 20: 2}}
hot_streams: List of dicts representing hot streams where each item is a dict of the form:
.. code-block:: python
{'name':'H1', 'temps': [443, 435, 355, 333], 'type': HENStreamType.hot}
cold_streams: List of dicts representing cold streams where each item is a dict of the form:
.. code-block:: python
{'name':'H1', 'temps': [443, 435, 355, 333], 'type': HENStreamType.hot}
y_stream_step: how many units on the HEN diagram to leave between each stream (or sub-stream) and the one above it. Defaults to 1.
Returns:
Tuple containing 3 dictionaries to be used when plotting the HEN:
* stream_y_values_dict : a dict of each stream name as key and value being a dict of the form
.. code-block:: python
{'default_y_value': 2, 'split_y_values': [1,3]}.
..
This indicates what the default y value of this stream will be on the diagram and what values we'll
use when it splits.
* hot_split_streams : list of tuples of the form (a,b) where a is a hot stream name and b is the max.
times it will split over all the stages.
* cold_split_streams : list of tuples of the form (a,b) where a is a cold stream name and b is the max.
times it will split over all the stages.
Raises:
None
"""
STREAM_NAME_INDEX = 0
STREAM_SPLIT_COUNT = 3
stages = set([exchanger['stg'] for exchanger in exchangers])
stream_y_values = {}
split_streams = []
stream_names = [list(stream.keys())[0] for stream in hot_streams] + \
[list(stream.keys())[0] for stream in cold_streams]
for name in stream_names:
stream_y_values[name] = {
'default_y_value': -1, 'split_y_values': []}
for stage in stages:
hot_stage_streams = [exchanger['hot']
for exchanger in exchangers if exchanger['stg'] == stage
and not is_hot_or_cold_utility(exchanger)]
cold_stage_streams = [exchanger['cold']
for exchanger in exchangers if exchanger['stg'] == stage
and not is_hot_or_cold_utility(exchanger)]
hot_streams_to_split = set(
[(stream, HENStreamType.hot, stage, hot_stage_streams.count(stream)) for stream in hot_stage_streams if hot_stage_streams.count(stream) > 1])
cold_streams_to_split = set(
[(stream, HENStreamType.cold, stage, cold_stage_streams.count(stream)) for stream in cold_stage_streams if cold_stage_streams.count(stream) > 1])
split_streams.extend(list(hot_streams_to_split))
split_streams.extend(list(cold_streams_to_split))
split_stream_names = [split_stream_record[STREAM_NAME_INDEX]
for split_stream_record in split_streams]
cold_split_streams = [
split_stream_record for split_stream_record in split_streams if split_stream_record[1] == HENStreamType.cold]
hot_split_streams = [
split_stream_record for split_stream_record in split_streams if split_stream_record[1] == HENStreamType.hot]
max_y = 0
for stream in cold_streams:
stream_name = list(stream.keys())[0]
if stream_name in split_stream_names:
max_split_count = max([split_stream_record[STREAM_SPLIT_COUNT]
for split_stream_record in cold_split_streams])
stream_split_y_values = []
for i in range(max_y, max_y+max_split_count):
stream_split_y_values.append(i)
stream_y_values[stream_name]['split_y_values'] = sorted(
stream_split_y_values)
if len(stream_split_y_values) % 2 != 0:
stream_y_values[stream_name]['default_y_value'] = median(
stream_split_y_values[1:])
else:
stream_y_values[stream_name]['default_y_value'] = median(
stream_split_y_values)
max_y = max(stream_split_y_values) + y_stream_step
else:
stream_y_values[stream_name]['default_y_value'] = max_y
max_y += y_stream_step
for stream in hot_streams:
stream_name = list(stream.keys())[0]
if stream_name in split_stream_names:
max_split_count = max([split_stream_record[STREAM_SPLIT_COUNT]
for split_stream_record in hot_split_streams])
stream_split_y_values = []
for i in range(max_y, max_y+max_split_count):
stream_split_y_values.append(i)
stream_y_values[stream_name]['split_y_values'] = sorted(
stream_split_y_values)
if len(stream_split_y_values) % 2 != 0:
stream_y_values[stream_name]['default_y_value'] = median(
stream_split_y_values[1:])
else:
stream_y_values[stream_name]['default_y_value'] = median(
stream_split_y_values)
max_y = max(stream_split_y_values) + y_stream_step
else:
stream_y_values[stream_name]['default_y_value'] = max_y
max_y += y_stream_step
stream_y_values = sorted(stream_y_values.items(
), key=lambda k_v: k_v[1]['default_y_value'])
stream_y_values_dict = {}
for item in stream_y_values:
stream_y_values_dict[item[0]] = item[1]
return stream_y_values_dict, hot_split_streams, cold_split_streams
[docs]def plot_line_segment(plot, x_start, x_end, y_start, y_end, color="white", legend=None):
"""Plot a line segment on a bokeh figure.
Args:
plot: bokeh.plotting.plotting.figure instance.
x_start: x-axis coordinate of 1st point in line.
x_end: x-axis coordinate of 2nd point in line.
y_start: y-axis coordinate of 1st point in line.
y_end: y-axis coordinate of 2nd point in line.
color: color of line (defaults to white).
legend: what legend to associate with (defaults to None).
Returns:
modified bokeh.plotting.plotting.figure instance with line added.
Raises:
None
"""
plot.line([x_start, x_end], [y_start, y_end],
line_width=2,
color=color,
legend=legend)
return plot
[docs]def plot_stream_arrow(plot, line_color,
stream_arrow_temp,
temp_label_font_size,
x_start, x_end, y_start, y_end,
stream_name=None):
"""Plot a stream arrow for the heat exchanger network diagram.
Args:
plot: bokeh.plotting.plotting.figure instance.
line_color: color of arrow (defaults to white).
stream_arrow_temp: Tempreature of the stream to be plotted.
temp_label_font_size: font-size of the temperature label to be added.
x_start: x-axis coordinate of arrow base.
x_end: x-axis coordinate of arrow head.
y_start: y-axis coordinate of arrow base.
y_end: y-axis coordinate of arrow head.
stream_name: Name of the stream to add as a label to arrow (defaults to None).
Returns:
modified bokeh.plotting.plotting.figure instance with stream arrow added.
Raises:
None
"""
plot.add_layout(Arrow(line_color=line_color,
end=OpenHead(line_color=line_color,
line_width=2, size=10),
x_start=x_start, y_start=y_start,
x_end=x_end, y_end=y_end))
temp_end_label = Label(
x=x_end, y=y_end,
x_offset=-20, y_offset=8,
text_font_size=temp_label_font_size,
text=str(stream_arrow_temp))
plot.add_layout(temp_end_label)
if stream_name:
stream_name_label = Label(
x=x_end, y=y_end,
x_offset=-10, y_offset=-12,
text_font_size=temp_label_font_size,
text=stream_name)
plot.add_layout(stream_name_label)
return plot
[docs]def add_exchanger_labels(plot, x, y_start, y_end, label_font_size, exchanger,
module_marker_line_color,
module_marker_fill_color,
mark_modules_with_tooltips):
"""Plot exchanger labels for an exchanger (for Q and A) on a heat exchanger network diagram and add module markers (if needed).
Args:
plot: bokeh.plotting.plotting.figure instance.
label_font_size: font-size for labels.
x: x-axis coordinate of exchanger (exchangers are vertical lines so we just need 1 x-value)
y_start: y-axis coordinate of exchanger start.
y_end: y-axis coordinate of exchanger end.
exchanger: exchanger dictionary of the form:
.. code-block:: python
{'hot': 'H2', 'cold': 'C1', 'Q': 1400, 'A': 159, 'annual_cost': 28358,
'stg': 2}
module_marker_line_color : color of border of the module marker.
module_marker_fill_color : color inside the module marker.
mark_modules_with_tooltips : whether to add tooltips to plot or not (currently not utilized).
Returns:
modified bokeh.plotting.plotting.figure instance with labels added.
Raises:
None
"""
label_q = Label(
x=x, y=y_start,
x_offset=-10,
text_font_size=label_font_size,
text='{0} kW'.format(exchanger['Q']))
plot.add_layout(label_q)
label_a = Label(x=x, y=y_end,
y_offset=-20, x_offset=-20,
text_font_size=label_font_size,
text='{0} m^2'.format(exchanger['A']))
plot.add_layout(label_a)
# Mark modules:
if 'modules' in exchanger:
plot = add_module_markers_to_heat_exchanger_plot(plot=plot,
x=x,
y=y_end,
modules=exchanger['modules'],
line_color=module_marker_line_color,
fill_color=module_marker_fill_color,
mark_modules_with_tooltips=mark_modules_with_tooltips)
return plot