+ Coverage for vclibpy/components/heat_exchangers/__init__.py: + 100% +
+ ++ 2 statements + + + +
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +diff --git a/docs/main/coverage/.gitignore b/docs/main/coverage/.gitignore new file mode 100644 index 0000000..ccccf14 --- /dev/null +++ b/docs/main/coverage/.gitignore @@ -0,0 +1,2 @@ +# Created by coverage.py +* diff --git a/docs/main/coverage/badge.svg b/docs/main/coverage/badge.svg new file mode 100644 index 0000000..5d787db --- /dev/null +++ b/docs/main/coverage/badge.svg @@ -0,0 +1,21 @@ + + diff --git a/docs/main/coverage/coverage_html.js b/docs/main/coverage/coverage_html.js new file mode 100644 index 0000000..5934882 --- /dev/null +++ b/docs/main/coverage/coverage_html.js @@ -0,0 +1,624 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// General helpers +function debounce(callback, wait) { + let timeoutId = null; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback.apply(this, args); + }, wait); + }; +}; + +function checkVisible(element) { + const rect = element.getBoundingClientRect(); + const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); + const viewTop = 30; + return !(rect.bottom < viewTop || rect.top >= viewBottom); +} + +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + +// Helpers for table sorting +function getCellValue(row, column = 0) { + const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.childElementCount == 1) { + const child = cell.firstElementChild + if (child instanceof HTMLTimeElement && child.dateTime) { + return child.dateTime + } else if (child instanceof HTMLDataElement && child.value) { + return child.value + } + } + return cell.innerText || cell.textContent; +} + +function rowComparator(rowA, rowB, column = 0) { + let valueA = getCellValue(rowA, column); + let valueB = getCellValue(rowB, column); + if (!isNaN(valueA) && !isNaN(valueB)) { + return valueA - valueB + } + return valueA.localeCompare(valueB, undefined, {numeric: true}); +} + +function sortColumn(th) { + // Get the current sorting direction of the selected header, + // clear state on other headers and then set the new sorting direction + const currentSortOrder = th.getAttribute("aria-sort"); + [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); + if (currentSortOrder === "none") { + th.setAttribute("aria-sort", th.dataset.defaultSortOrder || "ascending"); + } else { + th.setAttribute("aria-sort", currentSortOrder === "ascending" ? "descending" : "ascending"); + } + + const column = [...th.parentElement.cells].indexOf(th) + + // Sort all rows and afterwards append them in order to move them in the DOM + Array.from(th.closest("table").querySelectorAll("tbody tr")) + .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (th.getAttribute("aria-sort") === "ascending" ? 1 : -1)) + .forEach(tr => tr.parentElement.appendChild(tr) ); +} + +// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + document.querySelectorAll("[data-shortcut]").forEach(element => { + document.addEventListener("keypress", event => { + if (event.target.tagName.toLowerCase() === "input") { + return; // ignore keypress from search filter + } + if (event.key === element.dataset.shortcut) { + element.click(); + } + }); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Cache elements. + const table = document.querySelector("table.index"); + const table_body_rows = table.querySelectorAll("tbody tr"); + const no_rows = document.getElementById("no_rows"); + + // Observe filter keyevents. + document.getElementById("filter").addEventListener("input", debounce(event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = new Array(table.rows[0].cells.length).fill(0); + // Accumulate the percentage as fraction + totals[totals.length - 1] = { "numer": 0, "denom": 0 }; // nosemgrep: eslint.detect-object-injection + + // Hide / show elements. + table_body_rows.forEach(row => { + if (!row.cells[0].textContent.includes(event.target.value)) { + // hide + row.classList.add("hidden"); + return; + } + + // show + row.classList.remove("hidden"); + totals[0]++; + + for (let column = 1; column < totals.length; column++) { + // Accumulate dynamic totals + cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (column === totals.length - 1) { + // Last column contains percentage + const [numer, denom] = cell.dataset.ratio.split(" "); + totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection + totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection + } else { + totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection + } + } + }); + + // Show placeholder if no rows will be displayed. + if (!totals[0]) { + // Show placeholder, hide table. + no_rows.style.display = "block"; + table.style.display = "none"; + return; + } + + // Hide placeholder, show table. + no_rows.style.display = null; + table.style.display = null; + + const footer = table.tFoot.rows[0]; + // Calculate new dynamic sum values based on visible rows. + for (let column = 1; column < totals.length; column++) { + // Get footer cell element. + const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection + + // Set value into dynamic footer cell element. + if (column === totals.length - 1) { + // Percentage column uses the numerator and denominator, + // and adapts to the number of decimal places. + const match = /\.([0-9]+)/.exec(cell.textContent); + const places = match ? match[1].length : 0; + const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection + cell.dataset.ratio = `${numer} ${denom}`; + // Check denom to prevent NaN if filtered files contain no statements + cell.textContent = denom + ? `${(numer * 100 / denom).toFixed(places)}%` + : `${(100).toFixed(places)}%`; + } else { + cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection + } + } + })); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + document.getElementById("filter").dispatchEvent(new Event("input")); +}; + +coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; + +// Loaded on index.html +coverage.index_ready = function () { + coverage.assign_shortkeys(); + coverage.wire_up_filter(); + document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( + th => th.addEventListener("click", e => sortColumn(e.target)) + ); + + // Look for a localStorage item containing previous sort settings: + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + + if (stored_list) { + const {column, direction} = JSON.parse(stored_list); + const th = document.querySelector("[data-sortable]").tHead.rows[0].cells[column]; // nosemgrep: eslint.detect-object-injection + th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); + th.click() + } + + // Watch for page unload events so we can save the final sort settings: + window.addEventListener("unload", function () { + const th = document.querySelector('[data-sortable] th[aria-sort="ascending"], [data-sortable] [aria-sort="descending"]'); + if (!th) { + return; + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + column: [...th.parentElement.cells].indexOf(th), + direction: th.getAttribute("aria-sort"), + })); + }); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); +}; + +// -- pyfile stuff -- + +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + +coverage.pyfile_ready = function () { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === "t") { + document.querySelector(frag).closest(".n").classList.add("highlight"); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } else { + coverage.set_sel(0); + } + + on_click(".button_toggle_run", coverage.toggle_lines); + on_click(".button_toggle_mis", coverage.toggle_lines); + on_click(".button_toggle_exc", coverage.toggle_lines); + on_click(".button_toggle_par", coverage.toggle_lines); + + on_click(".button_next_chunk", coverage.to_next_chunk_nicely); + on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); + on_click(".button_top_of_page", coverage.to_top); + on_click(".button_first_chunk", coverage.to_first_chunk); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection + } + + coverage.assign_shortkeys(); + coverage.init_scroll_markers(); + coverage.wire_up_sticky_header(); + + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + + // Rebuild scroll markers when the window height changes. + window.addEventListener("resize", coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (event) { + const btn = event.target.closest("button"); + const category = btn.value + const show = !btn.classList.contains("show_" + category); + coverage.set_line_visibilty(category, show); + coverage.build_scroll_markers(); + coverage.filters[category] = show; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (category, should_show) { + const cls = "show_" + category; + const btn = document.querySelector(".button_toggle_" + category); + if (btn) { + if (should_show) { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); + btn.classList.add(cls); + } + else { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); + btn.classList.remove(cls); + } + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return document.getElementById("t" + n)?.closest("p"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + const classes = line_elt?.className; + if (!classes) { + return null; + } + const match = classes.match(/\bshow_\w+\b/); + if (!match) { + return null; + } + return match[0]; +}; + +coverage.to_next_chunk = function () { + const c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + const c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 1 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + if (probe <= 0) { + return; + } + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + const begin = coverage.line_elt(coverage.sel_begin); + const end = coverage.line_elt(coverage.sel_end-1); + + return ( + (checkVisible(begin) ? 1 : 0) + + (checkVisible(end) ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the top line on the screen as selection. + + // This will select the top-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(0, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(1); + } else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the lowest line on the screen as selection. + + // This will select the bottom-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(coverage.lines_len); + } else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (!probe_line) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + // Highlight the lines in the chunk + document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); + for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { + coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); + } + + coverage.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + const element = coverage.line_elt(coverage.sel_begin); + coverage.scroll_window(element.offsetTop - 60); + } +}; + +coverage.scroll_window = function (to_pos) { + window.scroll({top: to_pos, behavior: "smooth"}); +}; + +coverage.init_scroll_markers = function () { + // Init some variables + coverage.lines_len = document.querySelectorAll("#source > p").length; + + // Build html + coverage.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + const temp_scroll_marker = document.getElementById("scroll_marker") + if (temp_scroll_marker) temp_scroll_marker.remove(); + // Don't build markers if the window has no scroll bar. + if (document.body.scrollHeight <= window.innerHeight) { + return; + } + + const marker_scale = window.innerHeight / document.body.scrollHeight; + const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); + + let previous_line = -99, last_mark, last_top; + + const scroll_marker = document.createElement("div"); + scroll_marker.id = "scroll_marker"; + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" + ).forEach(element => { + const line_top = Math.floor(element.offsetTop * marker_scale); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.style.height = `${line_top + line_height - last_top}px`; + } else { + // Add colored line in scroll_marker block. + last_mark = document.createElement("div"); + last_mark.id = `m${line_number}`; + last_mark.classList.add("marker"); + last_mark.style.height = `${line_height}px`; + last_mark.style.top = `${line_top}px`; + scroll_marker.append(last_mark); + last_top = line_top; + } + + previous_line = line_number; + }); + + // Append last to prevent layout calculation + document.body.append(scroll_marker); +}; + +coverage.wire_up_sticky_header = function () { + const header = document.querySelector("header"); + const header_bottom = ( + header.querySelector(".content h2").getBoundingClientRect().top - + header.getBoundingClientRect().top + ); + + function updateHeader() { + if (window.scrollY > header_bottom) { + header.classList.add("sticky"); + } else { + header.classList.remove("sticky"); + } + } + + window.addEventListener("scroll", updateHeader); + updateHeader(); +}; + +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + if (document.body.classList.contains("indexfile")) { + coverage.index_ready(); + } else { + coverage.pyfile_ready(); + } +}); diff --git a/docs/main/coverage/d_35fc0e049e81dfd6___init___py.html b/docs/main/coverage/d_35fc0e049e81dfd6___init___py.html new file mode 100644 index 0000000..df6682a --- /dev/null +++ b/docs/main/coverage/d_35fc0e049e81dfd6___init___py.html @@ -0,0 +1,99 @@ + + +
+ ++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from .heat_exchanger import HeatExchanger
+2from .moving_boundary_ntu import MovingBoundaryNTUCondenser, MovingBoundaryNTUEvaporator
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import logging
+ +3from vclibpy.components.heat_exchangers.ntu import BasicNTU
+4from vclibpy.media import ThermodynamicState
+ +6logger = logging.getLogger(__name__)
+ + +9class VaporInjectionEconomizerNTU(BasicNTU):
+10 """
+11 Economizer heat exchanger which is NTU based.
+12 Used only for vapor injection cycles, as
+13 calculations are purely based for two-phase
+14 and liquid estimations.
+ +16 See parent class for more arguments.
+ +18 Assumptions:
+ +20 - Default `flow_type` is counter_flow.
+21 - Default `ratio_outer_to_inner_area` is 1, as
+22 - pipes are nearly same diameter and length
+23 - Secondary heat transfer for alpha is disabled; gas,
+24 liquid and two-phase models are used for both sides.
+25 """
+ +27 def __init__(self, **kwargs):
+28 kwargs.pop("secondary_heat_transfer", None)
+29 kwargs.pop("secondary_medium", None)
+30 self._state_two_phase_outlet = None
+31 self._state_two_phase_inlet = None
+32 super().__init__(
+33 flow_type=kwargs.pop("flow_type", "counter"),
+34 secondary_heat_transfer="None",
+35 secondary_medium="None",
+36 ratio_outer_to_inner_area=kwargs.pop("ratio_outer_to_inner_area", 1),
+37 **kwargs)
+ +39 @property
+40 def state_two_phase_inlet(self) -> ThermodynamicState:
+41 return self._state_two_phase_inlet
+ +43 @state_two_phase_inlet.setter
+44 def state_two_phase_inlet(self, state_inlet: ThermodynamicState):
+45 self._state_two_phase_inlet = state_inlet
+ +47 @property
+48 def state_two_phase_outlet(self) -> ThermodynamicState:
+49 return self._state_two_phase_outlet
+ +51 @state_two_phase_outlet.setter
+52 def state_two_phase_outlet(self, state_outlet: ThermodynamicState):
+53 self._state_two_phase_outlet = state_outlet
+ +55 def calc(self, inputs, fs_state) -> (float, float):
+56 raise NotImplementedError("Could be moved from VaporInjectionEconomizer")
+ +58 def set_secondary_cp(self, cp: float):
+59 """Set primary m_flow_cp"""
+60 self._secondary_cp = cp
+ +62 def start_secondary_med_prop(self):
+63 self.med_prop_sec = self.med_prop
+ +65 def terminate_secondary_med_prop(self):
+66 pass # Not required as it is the central med_prop class
+ +68 def calc_alpha_secondary(self, transport_properties):
+69 raise NotImplementedError("Economizer does not use secondary heat transfer model.")
+ +71 def calc_transport_properties_secondary_medium(self, T, p=None):
+72 raise NotImplementedError("Economizer does not use this method")
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import abc
+ +3from typing import Tuple
+ +5from vclibpy import media
+6from vclibpy.datamodels import FlowsheetState, Inputs
+7from vclibpy.components.component import BaseComponent
+8from vclibpy.components.heat_exchangers.heat_transfer.heat_transfer import HeatTransfer, TwoPhaseHeatTransfer
+ + +11class HeatExchanger(BaseComponent, abc.ABC):
+12 """
+13 Class for a heat exchanger.
+ +15 Args:
+16 A (float):
+17 Area of HE in m^2 for NTU calculation
+18 secondary_medium (str):
+19 Name for secondary medium, e.g. `water` or `air`
+20 wall_heat_transfer (HeatTransfer):
+21 Model for heat transfer inside wall
+22 secondary_heat_transfer (HeatTransfer):
+23 Model for heat transfer from secondary medium to wall
+24 gas_heat_transfer (HeatTransfer):
+25 Model for heat transfer from refrigerant gas to wall
+26 liquid_heat_transfer (HeatTransfer):
+27 Model for heat transfer from refrigerant liquid to wall
+28 two_phase_heat_transfer (TwoPhaseHeatTransfer):
+29 Model for heat transfer from refrigerant two phase to wall
+30 """
+ +32 def __init__(
+33 self,
+34 A: float,
+35 wall_heat_transfer: HeatTransfer,
+36 secondary_heat_transfer: HeatTransfer,
+37 gas_heat_transfer: HeatTransfer,
+38 liquid_heat_transfer: HeatTransfer,
+39 two_phase_heat_transfer: TwoPhaseHeatTransfer,
+40 secondary_medium: str
+41 ):
+42 super().__init__()
+43 self.A = A
+44 self.secondary_medium = secondary_medium.lower()
+ +46 self._wall_heat_transfer = wall_heat_transfer
+47 self._secondary_heat_transfer = secondary_heat_transfer
+48 self._gas_heat_transfer = gas_heat_transfer
+49 self._liquid_heat_transfer = liquid_heat_transfer
+50 self._two_phase_heat_transfer = two_phase_heat_transfer
+ +52 self.med_prop_sec = None # Later start in start_secondary_med_prop
+53 self._m_flow_secondary = None
+54 self._secondary_cp = 0 # Allow initial calculation of _m_flow_secondary_cp if cp is not set
+55 self._m_flow_secondary_cp = 0
+ +57 def start_secondary_med_prop(self):
+58 """
+59 Set up the wrapper for the secondary medium's media properties.
+60 """
+61 # Set up the secondary_medium wrapper:
+62 med_prop_class, med_prop_kwargs = media.get_global_med_prop_and_kwargs()
+63 if self.secondary_medium == "air" and med_prop_class == media.RefProp:
+64 fluid_name = "AIR.PPF"
+65 else:
+66 fluid_name = self.secondary_medium
+67 if self.med_prop_sec is not None:
+68 if self.med_prop_sec.fluid_name == fluid_name:
+69 return
+70 self.med_prop_sec.terminate()
+71 self.med_prop_sec = med_prop_class(fluid_name=self.secondary_medium, **med_prop_kwargs)
+ +73 def terminate_secondary_med_prop(self):
+74 if self.med_prop_sec is not None:
+75 self.med_prop_sec.terminate()
+ +77 @abc.abstractmethod
+78 def calc(self, inputs: Inputs, fs_state: FlowsheetState) -> Tuple[float, float]:
+79 """
+80 Calculate the heat exchanger based on the given inputs.
+ +82 The flowsheet state can be used to save important variables
+83 during calculation for later analysis.
+ +85 Both return values are used to check if the heat transfer is valid or not.
+ +87 Args:
+88 inputs (Inputs): The inputs for the calculation.
+89 fs_state (FlowsheetState): The flowsheet state to save important variables.
+ +91 Returns:
+92 Tuple[float, float]:
+93 error: Error in percentage between the required and calculated heat flow rates.
+94 dT_min: Minimal temperature difference (can be negative).
+95 """
+96 raise NotImplementedError
+ +98 def calc_alpha_two_phase(self, state_q0, state_q1, inputs: Inputs, fs_state: FlowsheetState) -> float:
+99 """
+100 Calculate the two-phase heat transfer coefficient.
+ +102 Args:
+103 state_q0: State at vapor quality 0.
+104 state_q1: State at vapor quality 1.
+105 inputs (Inputs): The inputs for the calculation.
+106 fs_state (FlowsheetState): The flowsheet state to save important variables.
+ +108 Returns:
+109 float: The two-phase heat transfer coefficient.
+110 """
+111 return self._two_phase_heat_transfer.calc(
+112 state_q0=state_q0,
+113 state_q1=state_q1,
+114 inputs=inputs,
+115 fs_state=fs_state,
+116 m_flow=self.m_flow,
+117 med_prop=self.med_prop,
+118 state_inlet=self.state_inlet,
+119 state_outlet=self.state_outlet
+120 )
+ +122 def calc_alpha_liquid(self, transport_properties) -> float:
+123 """
+124 Calculate the liquid-phase heat transfer coefficient.
+ +126 Args:
+127 transport_properties: Transport properties for the liquid phase.
+ +129 Returns:
+130 float: The liquid-phase heat transfer coefficient.
+131 """
+132 return self._liquid_heat_transfer.calc(
+133 transport_properties=transport_properties,
+134 m_flow=self.m_flow
+135 )
+ +137 def calc_alpha_gas(self, transport_properties) -> float:
+138 """
+139 Calculate the gas-phase heat transfer coefficient.
+ +141 Args:
+142 transport_properties: Transport properties for the gas phase.
+ +144 Returns:
+145 float: The gas-phase heat transfer coefficient.
+146 """
+147 return self._gas_heat_transfer.calc(
+148 transport_properties=transport_properties,
+149 m_flow=self.m_flow
+150 )
+ +152 def calc_alpha_secondary(self, transport_properties) -> float:
+153 """
+154 Calculate the secondary-medium heat transfer coefficient.
+ +156 Args:
+157 transport_properties: Transport properties for the secondary medium.
+ +159 Returns:
+160 float: The secondary-medium heat transfer coefficient.
+161 """
+162 return self._secondary_heat_transfer.calc(
+163 transport_properties=transport_properties,
+164 m_flow=self.m_flow_secondary
+165 )
+ +167 def calc_wall_heat_transfer(self) -> float:
+168 """
+169 Calculate the heat transfer coefficient inside the wall.
+ +171 Returns:
+172 float: The wall heat transfer coefficient.
+173 """
+174 # Arguments are not required
+175 return self._wall_heat_transfer.calc(
+176 transport_properties=media.TransportProperties(),
+177 m_flow=0
+178 )
+ +180 @property
+181 def m_flow_secondary(self) -> float:
+182 return self._m_flow_secondary
+ +184 @m_flow_secondary.setter
+185 def m_flow_secondary(self, m_flow: float):
+186 self._m_flow_secondary = m_flow
+187 self._m_flow_secondary_cp = self._m_flow_secondary * self._secondary_cp
+ +189 @property
+190 def m_flow_secondary_cp(self):
+191 return self._m_flow_secondary_cp
+ +193 def calc_secondary_cp(self, T: float, p=None):
+194 """
+195 Calculate and set the heat capacity rate m_flow_cp of the secondary medium.
+ +197 Args:
+198 T (float): Temperature of the secondary medium.
+199 p (float, optional): Pressure of the secondary medium. Defaults to None.
+200 """
+201 self._secondary_cp = self.calc_transport_properties_secondary_medium(T=T, p=p).cp
+202 self._m_flow_secondary_cp = self.m_flow_secondary * self._secondary_cp
+ +204 def calc_secondary_Q_flow(self, Q_flow: float) -> float:
+205 return Q_flow
+ +207 def calc_Q_flow(self) -> float:
+208 """
+209 Calculate the total heat flow rate.
+ +211 Returns:
+212 float: The total heat flow rate.
+213 """
+214 return self.m_flow * abs(self.state_inlet.h - self.state_outlet.h)
+ +216 def calc_transport_properties_secondary_medium(self, T, p=None) -> media.TransportProperties:
+217 """
+218 Calculate the transport properties for the selected secondary_medium.
+ +220 Args:
+221 T (float): Temperature in K.
+222 p (float, optional): Pressure to use. Defaults to None.
+ +224 Returns:
+225 media.TransportProperties: The calculated transport properties.
+226 """
+227 if p is None:
+228 if self.secondary_medium == "water":
+229 p = 2e5 # 2 bar (default hydraulic pressure)
+230 elif self.secondary_medium == "air":
+231 p = 101325 # 1 atm
+232 else:
+233 raise NotImplementedError(
+234 "Default pressures for secondary_mediums aside from water and air are not supported yet."
+235 )
+236 # Calc state
+237 state = self.med_prop_sec.calc_state("PT", p, T)
+238 # Return properties
+239 return self.med_prop_sec.calc_transport_properties(state)
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import abc
+2import logging
+ +4import numpy as np
+5from vclibpy.datamodels import FlowsheetState, Inputs
+6from vclibpy.components.heat_exchangers.ntu import BasicNTU
+7from vclibpy.media import ThermodynamicState
+ +9logger = logging.getLogger(__name__)
+ + +12class MovingBoundaryNTU(BasicNTU, abc.ABC):
+13 """
+14 Moving boundary NTU based heat exchanger.
+ +16 See parent classe for arguments.
+17 """
+ +19 def separate_phases(self, state_max: ThermodynamicState, state_min: ThermodynamicState, p: float):
+20 """
+21 Separates a flow with possible phase changes into three parts:
+22 subcooling (sc), latent phase change (lat), and superheating (sh)
+23 at the given pressure.
+ +25 Args:
+26 state_max (ThermodynamicState): State with higher enthalpy.
+27 state_min (ThermodynamicState): State with lower enthalpy.
+28 p (float): Pressure of phase change.
+ +30 Returns:
+31 Tuple[float, float, float, ThermodynamicState, ThermodynamicState]:
+32 Q_sc: Heat for subcooling.
+33 Q_lat: Heat for latent phase change.
+34 Q_sh: Heat for superheating.
+35 state_q0: State at vapor quality 0 and the given pressure.
+36 state_q1: State at vapor quality 1 and the given pressure.
+37 """
+38 # Get relevant states:
+39 state_q0 = self.med_prop.calc_state("PQ", p, 0)
+40 state_q1 = self.med_prop.calc_state("PQ", p, 1)
+41 Q_sc = max(0.0,
+42 min((state_q0.h - state_min.h),
+43 (state_max.h - state_min.h))) * self.m_flow
+44 Q_lat = max(0.0,
+45 (min(state_max.h, state_q1.h) -
+46 max(state_min.h, state_q0.h))) * self.m_flow
+47 Q_sh = max(0.0,
+48 min((state_max.h - state_q1.h),
+49 (state_max.h - state_min.h))) * self.m_flow
+50 return Q_sc, Q_lat, Q_sh, state_q0, state_q1
+ +52 def iterate_area(self, dT_max, alpha_pri, alpha_sec, Q) -> float:
+53 """
+54 Iteratively calculates the required area for the heat exchange.
+ +56 Args:
+57 dT_max (float): Maximum temperature differential.
+58 alpha_pri (float): Heat transfer coefficient for the primary medium.
+59 alpha_sec (float): Heat transfer coefficient for the secondary medium.
+60 Q (float): Heat flow rate.
+ +62 Returns:
+63 float: Required area for heat exchange.
+64 """
+65 _accuracy = 1e-6 # square mm
+66 _step = 1.0
+67 R = self.calc_R()
+68 k = self.calc_k(alpha_pri, alpha_sec)
+69 m_flow_cp_min = self.calc_m_flow_cp_min()
+70 # First check if point is feasible at all
+71 if dT_max <= 0:
+72 return self.A
+73 eps_necessary = Q / (m_flow_cp_min * dT_max)
+ +75 # Special cases:
+76 # ---------------
+77 # eps is equal or higher than 1, an infinite amount of area would be necessary.
+78 if eps_necessary >= 1:
+79 return self.A
+80 # eps is lower or equal to zero: No Area required (Q<=0)
+81 if eps_necessary <= 0:
+82 return 0
+ +84 area = 0.0
+85 while True:
+86 NTU = self.calc_NTU(area, k, m_flow_cp_min)
+87 eps = self.calc_eps(R, NTU)
+88 if eps >= eps_necessary:
+89 if _step <= _accuracy:
+90 break
+91 else:
+92 # Go back
+93 area -= _step
+94 _step /= 10
+95 continue
+96 if _step < _accuracy and area > self.A:
+97 break
+98 area += _step
+ +100 return min(area, self.A)
+ + +103class MovingBoundaryNTUCondenser(MovingBoundaryNTU):
+104 """
+105 Condenser class which implements the actual `calc` method.
+ +107 Assumptions:
+108 - No phase changes in secondary medium
+109 - cp of secondary medium is constant over heat-exchanger
+ +111 See parent classes for arguments.
+112 """
+ +114 def calc(self, inputs: Inputs, fs_state: FlowsheetState) -> (float, float):
+115 """
+116 Calculate the heat exchanger with the NTU-Method based on the given inputs.
+ +118 The flowsheet state can be used to save important variables
+119 during calculation for later analysis.
+ +121 Both return values are used to check if the heat transfer is valid or not.
+ +123 Args:
+124 inputs (Inputs): The inputs for the calculation.
+125 fs_state (FlowsheetState): The flowsheet state to save important variables.
+ +127 Returns:
+128 Tuple[float, float]:
+129 error: Error in percentage between the required and calculated heat flow rates.
+130 dT_min: Minimal temperature difference (can be negative).
+131 """
+132 self.m_flow_secondary = inputs.m_flow_con # [kg/s]
+133 self.calc_secondary_cp(T=inputs.T_con_in)
+ +135 # First we separate the flow:
+136 Q_sc, Q_lat, Q_sh, state_q0, state_q1 = self.separate_phases(
+137 self.state_inlet,
+138 self.state_outlet,
+139 self.state_inlet.p
+140 )
+141 Q = Q_sc + Q_lat + Q_sh
+ +143 # Note: As Q_con_ntu has to converge to Q_con (m_ref*delta_h), we can safely
+144 # calculate the output temperature.
+ +146 T_mean = inputs.T_con_in + self.calc_secondary_Q_flow(Q) / (self.m_flow_secondary_cp * 2)
+147 tra_prop_med = self.calc_transport_properties_secondary_medium(T_mean)
+148 alpha_med_wall = self.calc_alpha_secondary(tra_prop_med)
+ +150 # Calculate secondary_medium side temperatures:
+151 # Assumption loss is the same correlation for each regime
+152 T_sc = inputs.T_con_in + self.calc_secondary_Q_flow(Q_sc) / self.m_flow_secondary_cp
+153 T_sh = T_sc + self.calc_secondary_Q_flow(Q_lat) / self.m_flow_secondary_cp
+154 T_out = T_sh + self.calc_secondary_Q_flow(Q_sh) / self.m_flow_secondary_cp
+ +156 # 1. Regime: Subcooling
+157 Q_sc_ntu, A_sc = 0, 0
+158 if Q_sc > 0 and (state_q0.T != self.state_outlet.T):
+159 self.set_primary_cp((state_q0.h - self.state_outlet.h) / (state_q0.T - self.state_outlet.T))
+160 # Get transport properties:
+161 tra_prop_ref_con = self.med_prop.calc_mean_transport_properties(state_q0, self.state_outlet)
+162 alpha_ref_wall = self.calc_alpha_liquid(tra_prop_ref_con)
+ +164 # Only use still available area:
+165 A_sc = self.iterate_area(dT_max=(state_q0.T - inputs.T_con_in),
+166 alpha_pri=alpha_ref_wall,
+167 alpha_sec=alpha_med_wall,
+168 Q=Q_sc)
+169 A_sc = min(self.A, A_sc)
+ +171 Q_sc_ntu, k_sc = self.calc_Q_ntu(dT_max=(state_q0.T - inputs.T_con_in),
+172 alpha_pri=alpha_ref_wall,
+173 alpha_sec=alpha_med_wall,
+174 A=A_sc)
+ +176 # 2. Regime: Latent heat exchange
+177 Q_lat_ntu, A_lat = 0, 0
+178 if Q_lat > 0:
+179 self.set_primary_cp(np.inf)
+180 # Get transport properties:
+181 alpha_ref_wall = self.calc_alpha_two_phase(
+182 state_q0=state_q0,
+183 state_q1=state_q1,
+184 fs_state=fs_state,
+185 inputs=inputs
+186 )
+ +188 A_lat = self.iterate_area(dT_max=(state_q1.T - T_sc),
+189 alpha_pri=alpha_ref_wall,
+190 alpha_sec=alpha_med_wall,
+191 Q=Q_lat)
+192 # Only use still available area:
+193 A_lat = min(self.A - A_sc, A_lat)
+ +195 Q_lat_ntu, k_lat = self.calc_Q_ntu(dT_max=(state_q1.T - T_sc),
+196 alpha_pri=alpha_ref_wall,
+197 alpha_sec=alpha_med_wall,
+198 A=A_lat)
+199 logger.debug(f"con_lat: pri: {round(alpha_ref_wall, 2)} sec: {round(alpha_med_wall, 2)}")
+ +201 # 3. Regime: Superheat heat exchange
+202 Q_sh_ntu, A_sh = 0, 0
+203 if Q_sh and (self.state_inlet.T != state_q1.T):
+204 self.set_primary_cp((self.state_inlet.h - state_q1.h) / (self.state_inlet.T - state_q1.T))
+205 # Get transport properties:
+206 tra_prop_ref_con = self.med_prop.calc_mean_transport_properties(self.state_inlet, state_q1)
+207 alpha_ref_wall = self.calc_alpha_gas(tra_prop_ref_con)
+ +209 # Only use still available area:
+210 A_sh = self.A - A_sc - A_lat
+ +212 Q_sh_ntu, k_sh = self.calc_Q_ntu(dT_max=(self.state_inlet.T - T_sh),
+213 alpha_pri=alpha_ref_wall,
+214 alpha_sec=alpha_med_wall,
+215 A=A_sh)
+216 logger.debug(f"con_sh: pri: {round(alpha_ref_wall, 2)} sec: {round(alpha_med_wall, 2)}")
+ +218 Q_ntu = Q_sh_ntu + Q_sc_ntu + Q_lat_ntu
+219 error = (Q_ntu / Q - 1) * 100
+220 # Get possible dT_min:
+221 dT_min_in = self.state_outlet.T - inputs.T_con_in
+222 dT_min_out = self.state_inlet.T - T_out
+223 dT_min_LatSH = state_q1.T - T_sh
+ +225 fs_state.set(name="A_con_sh", value=A_sh, unit="m2", description="Area for superheat heat exchange in condenser")
+226 fs_state.set(name="A_con_lat", value=A_lat, unit="m2", description="Area for latent heat exchange in condenser")
+227 fs_state.set(name="A_con_sc", value=A_sc, unit="m2", description="Area for subcooling heat exchange in condenser")
+ +229 return error, min(dT_min_in,
+230 dT_min_LatSH,
+231 dT_min_out)
+ + +234class MovingBoundaryNTUEvaporator(MovingBoundaryNTU):
+235 """
+236 Evaporator class which implements the actual `calc` method.
+ +238 Assumptions:
+239 - No phase changes in secondary medium
+240 - cp of secondary medium is constant over heat-exchanger
+ +242 See parent classes for arguments.
+243 """
+ +245 def calc(self, inputs: Inputs, fs_state: FlowsheetState) -> (float, float):
+246 """
+247 Calculate the heat exchanger with the NTU-Method based on the given inputs.
+ +249 The flowsheet state can be used to save important variables
+250 during calculation for later analysis.
+ +252 Both return values are used to check if the heat transfer is valid or not.
+ +254 Args:
+255 inputs (Inputs): The inputs for the calculation.
+256 fs_state (FlowsheetState): The flowsheet state to save important variables.
+ +258 Returns:
+259 Tuple[float, float]:
+260 error: Error in percentage between the required and calculated heat flow rates.
+261 dT_min: Minimal temperature difference (can be negative).
+262 """
+263 self.m_flow_secondary = inputs.m_flow_eva # [kg/s]
+264 self.calc_secondary_cp(T=inputs.T_eva_in)
+ +266 # First we separate the flow:
+267 Q_sc, Q_lat, Q_sh, state_q0, state_q1 = self.separate_phases(
+268 self.state_outlet,
+269 self.state_inlet,
+270 self.state_inlet.p
+271 )
+ +273 Q = Q_sc + Q_lat + Q_sh
+ +275 # Note: As Q_eva_ntu has to converge to Q_eva (m_ref*delta_h), we can safely
+276 # calculate the output temperature.
+277 T_mean = inputs.T_eva_in - Q / (self.m_flow_secondary_cp * 2)
+278 tra_prop_med = self.calc_transport_properties_secondary_medium(T_mean)
+279 alpha_med_wall = self.calc_alpha_secondary(tra_prop_med)
+ +281 # Calculate secondary_medium side temperatures:
+282 T_sh = inputs.T_eva_in - Q_sh / self.m_flow_secondary_cp
+283 T_sc = T_sh - Q_lat / self.m_flow_secondary_cp
+284 T_out = T_sc - Q_sc / self.m_flow_secondary_cp
+ +286 # 1. Regime: Superheating
+287 Q_sh_ntu, A_sh = 0, 0
+288 if Q_sh and (self.state_outlet.T != state_q1.T):
+289 self.set_primary_cp((self.state_outlet.h - state_q1.h) / (self.state_outlet.T - state_q1.T))
+290 # Get transport properties:
+291 tra_prop_ref_eva = self.med_prop.calc_mean_transport_properties(self.state_outlet, state_q1)
+292 alpha_ref_wall = self.calc_alpha_gas(tra_prop_ref_eva)
+ +294 if Q_lat > 0:
+295 A_sh = self.iterate_area(dT_max=(inputs.T_eva_in - state_q1.T),
+296 alpha_pri=alpha_ref_wall,
+297 alpha_sec=alpha_med_wall,
+298 Q=Q_sh)
+299 else:
+300 # if only sh is present --> full area:
+301 A_sh = self.A
+ +303 # Only use still available area
+304 A_sh = min(self.A, A_sh)
+ +306 Q_sh_ntu, k_sh = self.calc_Q_ntu(dT_max=(inputs.T_eva_in - state_q1.T),
+307 alpha_pri=alpha_ref_wall,
+308 alpha_sec=alpha_med_wall,
+309 A=A_sh)
+ +311 logger.debug(f"eva_sh: pri: {round(alpha_ref_wall, 2)} sec: {round(alpha_med_wall, 2)}")
+ +313 # 2. Regime: Latent heat exchange
+314 Q_lat_ntu, A_lat = 0, 0
+315 if Q_lat > 0:
+316 self.set_primary_cp(np.inf)
+ +318 alpha_ref_wall = self.calc_alpha_two_phase(
+319 state_q0=state_q0,
+320 state_q1=state_q1,
+321 fs_state=fs_state,
+322 inputs=inputs
+323 )
+ +325 if Q_sc > 0:
+326 A_lat = self.iterate_area(dT_max=(T_sh - self.state_inlet.T),
+327 alpha_pri=alpha_ref_wall,
+328 alpha_sec=alpha_med_wall,
+329 Q=Q_lat)
+330 else:
+331 A_lat = self.A - A_sh
+ +333 # Only use still available area:
+334 A_lat = min(self.A - A_sh, A_lat)
+335 Q_lat_ntu, k_lat = self.calc_Q_ntu(dT_max=(T_sh - self.state_inlet.T),
+336 alpha_pri=alpha_ref_wall,
+337 alpha_sec=alpha_med_wall,
+338 A=A_lat)
+339 logger.debug(f"eva_lat: pri: {round(alpha_ref_wall, 2)} sec: {round(alpha_med_wall, 2)}")
+ +341 # 3. Regime: Subcooling
+342 Q_sc_ntu, A_sc = 0, 0
+343 if Q_sc > 0 and (state_q0.T != self.state_inlet.T):
+344 self.set_primary_cp((state_q0.h - self.state_inlet.h) / (state_q0.T - self.state_inlet.T))
+345 # Get transport properties:
+346 tra_prop_ref_eva = self.med_prop.calc_mean_transport_properties(state_q0, self.state_inlet)
+347 alpha_ref_wall = self.calc_alpha_liquid(tra_prop_ref_eva)
+ +349 # Only use still available area:
+350 A_sc = self.A - A_sh - A_lat
+ +352 Q_sc_ntu, k_sc = self.calc_Q_ntu(dT_max=(T_sc - self.state_inlet.T),
+353 alpha_pri=alpha_ref_wall,
+354 alpha_sec=alpha_med_wall,
+355 A=A_sc)
+ +357 Q_ntu = Q_sh_ntu + Q_sc_ntu + Q_lat_ntu
+358 error = (Q_ntu / Q - 1) * 100
+359 # Get dT_min
+360 dT_min_in = inputs.T_eva_in - self.state_outlet.T
+361 dT_min_out = T_out - self.state_inlet.T
+ +363 fs_state.set(name="A_eva_sh", value=A_sh, unit="m2", description="Area for superheat heat exchange in evaporator")
+364 fs_state.set(name="A_eva_lat", value=A_lat, unit="m2", description="Area for latent heat exchange in evaporator")
+ +366 return error, min(dT_min_out, dT_min_in)
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import logging
+2import abc
+ +4import numpy as np
+5from vclibpy.components.heat_exchangers.heat_exchanger import HeatExchanger
+ + +8logger = logging.getLogger(__name__)
+ + +11class BasicNTU(HeatExchanger, abc.ABC):
+12 """
+13 Moving boundary NTU based heat exchanger.
+ +15 See parent class for more arguments.
+ +17 Args:
+18 flow_type (str):
+19 Counter, Cross or parallel flow
+20 ratio_outer_to_inner_area (float):
+21 The NTU method uses the overall heat transfer coefficient `k`
+22 and multiplies it with the outer area `A` (area of the secondary side).
+23 However, depending on the heat exchanger type, the areas may
+24 differ drastically. For instance in an air-to-water heat exchanger.
+25 The VDI-Atlas proposes the ratio of outer area to inner pipe area
+26 to account for this mismatch in sizes.
+27 The calculation follows the code in the function `calc_k`.
+28 Typical values are around 20-30.
+29 """
+ +31 def __init__(self, flow_type: str, ratio_outer_to_inner_area: float, **kwargs):
+32 """
+33 Initializes BasicNTU.
+ +35 Args:
+36 flow_type (str): Type of flow: Counter, Cross, or Parallel.
+37 ratio_outer_to_inner_area (float):
+38 The NTU method uses the overall heat transfer coefficient `k`
+39 and multiplies it with the overall area `A`.
+40 However, depending on the heat exchanger type, the areas may
+41 differ drastically. For instance in an air-to-water heat exchanger.
+42 The VDI-Atlas proposes the ratio of outer area to inner pipe area
+43 to account for this mismatch in sizes.
+44 The calculation follows the code in the function `calc_k`.
+45 **kwargs: Additional keyword arguments passed to the parent class.
+46 """
+47 super().__init__(**kwargs)
+48 self.ratio_outer_to_inner_area = ratio_outer_to_inner_area
+ +50 # Set primary cp:
+51 self._primary_cp = None
+ +53 # Type of HE:
+54 self.flow_type = flow_type.lower()
+55 if self.flow_type not in ["counter", "cross", "parallel"]:
+56 raise TypeError("Given flow_type is not supported")
+ +58 def set_primary_cp(self, cp: float):
+59 """
+60 Set the specific heat (cp) for the primary medium.
+ +62 Args:
+63 cp (float): Specific heat for the primary medium.
+64 """
+65 self._primary_cp = cp
+ +67 def calc_eps(self, R: float, NTU: float) -> float:
+68 """
+69 Calculate the effectiveness (eps) of the heat exchanger based on NTU.
+ +71 Source of implementation: EBC Lecture SimModelle.
+ +73 Args:
+74 R (float): Ratio of heat capacity rates (m_flow*cp) of the primary to secondary medium.
+75 NTU (float): Number of Transfer Units.
+ +77 Returns:
+78 float: Effectiveness (eps) of the heat exchanger.
+79 """
+80 if R in (0, 1):
+81 return NTU / (NTU + 1)
+82 if self.flow_type == "counter":
+83 return (1 - np.exp(-NTU * (1 - R))) / (1 - R * np.exp(-NTU * (1 - R)))
+84 if self.flow_type == "cross":
+85 if NTU == 0:
+86 return 0
+87 eta = NTU ** -0.22
+88 return 1 - np.exp((np.exp(- NTU * R * eta) - 1) / (R * eta))
+89 if self.flow_type == "parallel":
+90 return (1 - np.exp(-NTU * (1 + R))) / (1 + R)
+91 raise TypeError(f"Flow type {self.flow_type} not supported")
+ +93 def calc_R(self) -> float:
+94 """
+95 Calculate the R value, which is the ratio of heat capacity rates (m_flow*cp) of the primary to secondary medium.
+ +97 Returns:
+98 float: R value.
+99 """
+100 m_flow_pri_cp = self.m_flow * self._primary_cp
+101 if m_flow_pri_cp > self.m_flow_secondary_cp:
+102 return self.m_flow_secondary_cp / m_flow_pri_cp
+103 return m_flow_pri_cp / self.m_flow_secondary_cp
+ +105 def calc_k(self, alpha_pri: float, alpha_sec: float) -> float:
+106 """
+107 Calculate the overall heat transfer coefficient (k) of the heat exchanger.
+ +109 Args:
+110 alpha_pri (float): Heat transfer coefficient for the primary medium.
+111 alpha_sec (float): Heat transfer coefficient for the secondary medium.
+ +113 Returns:
+114 float: Overall heat transfer coefficient (k).
+115 """
+116 k_wall = self.calc_wall_heat_transfer()
+117 k = (1 / (
+118 (1 / alpha_pri) * self.ratio_outer_to_inner_area +
+119 (1 / k_wall) * self.ratio_outer_to_inner_area +
+120 (1 / alpha_sec)
+121 )
+122 )
+123 return k
+ +125 @staticmethod
+126 def calc_NTU(A: float, k: float, m_flow_cp: float) -> float:
+127 """
+128 Calculate the Number of Transfer Units (NTU) for the heat exchanger.
+ +130 Args:
+131 A (float): Area of the heat exchanger.
+132 k (float): Overall heat transfer coefficient.
+133 m_flow_cp (float): Minimal heat capacity rates (m_flow*cp) between primary and secondary side.
+ +135 Returns:
+136 float: Number of Transfer Units (NTU).
+137 """
+138 return k * A / m_flow_cp
+ +140 def calc_m_flow_cp_min(self) -> float:
+141 """
+142 Calculate the minimum value between the heat capacity rates (m_flow*cp) for the primary and secondary mediums.
+ +144 Returns:
+145 float: Minimum value.
+146 """
+147 return min(
+148 self.m_flow * self._primary_cp,
+149 self.m_flow_secondary_cp
+150 )
+ +152 def calc_Q_ntu(self, dT_max: float, alpha_pri: float, alpha_sec: float, A: float) -> (float, float):
+153 """
+154 Calculate the heat transfer and overall heat transfer coefficient for the heat exchanger based on NTU.
+ +156 Args:
+157 dT_max (float): Maximum temperature differential.
+158 alpha_pri (float): Heat transfer coefficient for the primary medium.
+159 alpha_sec (float): Heat transfer coefficient for the secondary medium.
+160 A (float): Area of the heat exchanger.
+ +162 Returns:
+163 Tuple[float, float]: Heat transfer and overall heat transfer coefficient.
+164 """
+165 R = self.calc_R()
+166 k = self.calc_k(alpha_pri, alpha_sec)
+167 m_flow_cp_min = self.calc_m_flow_cp_min()
+168 NTU = self.calc_NTU(A, k, m_flow_cp_min)
+169 eps = self.calc_eps(R, NTU)
+ +171 # Get the maximal allowed heat flow
+172 Q_max = m_flow_cp_min * dT_max
+173 return Q_max * eps, k
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from .expansion_valve import ExpansionValve
+2from .bernoulli import Bernoulli
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module with simple bernoulli expansion valve
+3"""
+4from vclibpy.components.expansion_valves import ExpansionValve
+ + +7class Bernoulli(ExpansionValve):
+8 """
+9 Simple Bernoulli model.
+ +11 Args:
+12 A (float): Cross-sectional area of the expansion valve.
+13 """
+ +15 def calc_m_flow_at_opening(self, opening):
+16 return opening * self.A * (2 * self.state_inlet.d * (self.state_inlet.p - self.state_outlet.p)) ** 0.5
+ +18 def calc_opening_at_m_flow(self, m_flow, **kwargs):
+19 return (
+20 m_flow /
+21 (self.A * (2 * self.state_inlet.d * (self.state_inlet.p - self.state_outlet.p)) ** 0.5)
+22 )
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module with classes for EV models.
+3They are not used for the calculation.
+4They may be used to see if the output is correct.
+5"""
+6import abc
+ +8from vclibpy.components.component import BaseComponent
+ + +11class ExpansionValve(BaseComponent, abc.ABC):
+12 """Base class for an expansion valve.
+ +14 Args:
+15 A (float): Cross-sectional area of the expansion valve.
+16 """
+ +18 def __init__(self, A):
+19 super().__init__()
+20 self.A = A # Cross-sectional area of the expansion valve
+ +22 @abc.abstractmethod
+23 def calc_m_flow_at_opening(self, opening) -> float:
+24 """
+25 Calculate the mass flow rate for the given opening.
+ +27 Args:
+28 opening (float): Opening of valve between 0 and 1
+ +30 Returns:
+31 float: Mass flow rate in kg/s
+32 """
+33 raise NotImplementedError
+ +35 @abc.abstractmethod
+36 def calc_opening_at_m_flow(self, m_flow, **kwargs) -> float:
+37 """
+38 Calculate the opening for the given mass flow rate
+ +40 Args:
+41 m_flow (float): Mass flow rate in kg/s
+42 **kwargs: Possible keyword arguments for child classes
+ +44 Returns:
+45 float: Opening
+46 """
+47 raise NotImplementedError
+ +49 def calc_outlet(self, p_outlet: float):
+50 """
+51 Calculate isenthalpic expansion valve.
+ +53 Args:
+54 p_outlet (float): Outlet pressure level
+55 """
+56 self.state_outlet = self.med_prop.calc_state("PH", p_outlet, self.state_inlet.h)
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Package for stationary vapor compression models and their analysis
+3"""
+ +5from vclibpy.datamodels import FlowsheetState, Inputs
+ +7__version__ = "0.1.0"
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module which contains datamodels which are used in this library.
+3"""
+ +5from dataclasses import dataclass
+6from typing import Dict, Any
+7from copy import deepcopy
+ + +10@dataclass
+11class Variable:
+12 """
+13 Class for a variable used in analysis.
+ +15 Args:
+16 name (str): The name of the variable.
+17 value (float): The numerical value of the variable.
+18 unit (str): The unit of measurement for the variable (optional).
+19 description (str): A description of the variable (optional).
+20 """
+21 name: str
+22 value: float
+23 unit: str = None
+24 description: str = None
+ + +27class VariableContainer:
+28 """
+29 Class which holds Variables to be used anywhere in the models.
+ +31 This class enables dynamic addition of Variables.
+32 """
+33 def __init__(self):
+34 self._variables: Dict[str, Variable] = {}
+ +36 def __str__(self):
+37 return f"{self.__class__.__name__}:\n" + "\n".join(
+38 [f"{var.name}={var.value} {var.unit} ({var.description})"
+39 for var in self._variables.values()]
+40 )
+ +42 def set(self, name: str, value: float, unit: str = None, description: str = None):
+43 """
+44 Add or update a Variable in the container.
+ +46 Args:
+47 name (str): The name of the variable.
+48 value (float): The numerical value of the variable.
+49 unit (str): The unit of measurement for the variable (optional).
+50 description (str): A description of the variable (optional).
+51 """
+52 if name in self._variables:
+53 self._variables[name].value = value
+54 else:
+55 self._variables[name] = Variable(
+56 name=name, value=value, unit=unit, description=description
+57 )
+ +59 def __getattr__(self, item):
+60 """
+61 Overwrite the dunder method to enable usage of e.g.
+62 fs_state.COP
+63 """
+64 if item in {'__getstate__', '__setstate__'}:
+65 return super().__getattr__(self, item)
+66 if item in self._variables:
+67 return self._variables.get(item).value
+68 raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{item}'")
+ +70 def get_variable_names(self) -> list:
+71 """
+72 Get the names of all variables in the container.
+ +74 Returns:
+75 list: A list of variable names.
+76 """
+77 return list(self._variables.keys())
+ +79 def get_variables(self):
+80 """
+81 Get all variables in the container.
+ +83 Returns:
+84 Dict[str, Variable]: A dictionary of variable names and Variable objects.
+85 """
+86 return self._variables
+ +88 def items(self):
+89 """
+90 Get items from the container.
+ +92 Returns:
+93 Dict[str, Variable]: A dictionary of variable names and Variable objects.
+94 """
+95 return self._variables.items()
+ +97 def get(self, name: str, default: Any = None):
+98 """
+99 Get the Variable with the specified name.
+ +101 Args:
+102 name (str): The name of the variable.
+103 default (Any): Default value to return if the variable is not found.
+ +105 Returns:
+106 Variable: The Variable object.
+ +108 """
+109 return self._variables.get(name, default)
+ +111 def copy(self):
+112 """
+113 Return a deepcopy of the instance as the variable dict is mutable.
+ +115 Returns:
+116 VariableContainer: A deepcopy of the VariableContainer instance.
+117 """
+118 return deepcopy(self)
+ +120 def convert_to_str_value_format(self, with_unit_and_description: bool) -> Dict[str, float]:
+121 """
+122 Returns a dictionary with a str: float format which is handy when storing results
+123 in files like .csv or .xlsx.
+ +125 Args:
+126 with_unit_and_description (bool): When False, only the name is in the string.
+ +128 Returns:
+129 dict: Containing all variables and values.
+130 """
+131 if with_unit_and_description:
+132 return {f"{k} in {v.unit} ({v.description})": v.value for k, v in self.items()}
+133 return {k: v.value for k, v in self.items()}
+ + +136class FlowsheetState(VariableContainer):
+137 """
+138 This class is used to define the unique states of the flowsheet
+139 in the heat pump.
+ +141 The class is dynamic in the sense that attributes may be
+142 added during calculation of new flowsheet. This enables
+143 the easy adding of custom values to analyze the whole flowsheet
+144 and not restrict to a certain naming convention.
+145 """
+ + +148class Inputs(VariableContainer):
+149 """
+150 Class defining inputs to calculate the FlowsheetState.
+ +152 While the inputs are pre-defined, you may add further ones
+153 using the `set` method.
+ +155 Args:
+156 n (float): Relative compressor speed between 0 and 1.
+157 T_eva_in (float): Secondary side evaporator inlet temperature.
+158 T_con_in (float): Secondary side condenser inlet temperature.
+159 m_flow_eva (float): Secondary side evaporator mass flow rate.
+160 m_flow_con (float): Secondary side condenser mass flow rate.
+161 dT_eva_superheating (float): Super-heating after evaporator.
+162 dT_con_subcooling (float): Subcooling after condenser.
+163 T_ambient (float): Ambient temperature of the machine.
+164 """
+ +166 def __init__(
+167 self,
+168 n: float = None,
+169 T_eva_in: float = None,
+170 T_con_in: float = None,
+171 m_flow_eva: float = None,
+172 m_flow_con: float = None,
+173 dT_eva_superheating: float = None,
+174 dT_con_subcooling: float = None,
+175 T_ambient: float = None
+176 ):
+177 """
+178 Initializes an Inputs object with parameters representing external conditions
+179 for the vapor compression cycle.
+ +181 Args:
+182 n (float): Relative compressor speed between 0 and 1 (unit: -).
+183 T_eva_in (float): Secondary side evaporator inlet temperature (unit: K).
+184 T_con_in (float): Secondary side condenser inlet temperature (unit: K).
+185 m_flow_eva (float): Secondary side evaporator mass flow rate (unit: kg/s).
+186 m_flow_con (float): Secondary side condenser mass flow rate (unit: kg/s).
+187 dT_eva_superheating (float): Super-heating after evaporator (unit: K).
+188 dT_con_subcooling (float): Subcooling after condenser (unit: K).
+189 T_ambient (float): Ambient temperature of the machine (unit: K).
+190 """
+191 super().__init__()
+192 self.set(
+193 name="n",
+194 value=n,
+195 unit="-",
+196 description="Relative compressor speed"
+197 )
+198 self.set(
+199 name="T_eva_in",
+200 value=T_eva_in,
+201 unit="K",
+202 description="Secondary side evaporator inlet temperature"
+203 )
+204 self.set(
+205 name="T_con_in",
+206 value=T_con_in,
+207 unit="K",
+208 description="Secondary side condenser inlet temperature"
+209 )
+210 self.set(
+211 name="m_flow_con",
+212 value=m_flow_con,
+213 unit="kg/s",
+214 description="Secondary side condenser mass flow rate"
+215 )
+216 self.set(
+217 name="m_flow_eva",
+218 value=m_flow_eva,
+219 unit="kg/s",
+220 description="Secondary side evaporator mass flow rate"
+221 )
+222 self.set(
+223 name="dT_eva_superheating",
+224 value=dT_eva_superheating,
+225 unit="K",
+226 description="Super-heating after evaporator"
+227 )
+228 self.set(
+229 name="dT_con_subcooling",
+230 value=dT_con_subcooling,
+231 unit="K",
+232 description="Subcooling after condenser"
+233 )
+234 if T_ambient is None:
+235 T_ambient = T_eva_in
+236 self.set(
+237 name="T_ambient",
+238 value=T_ambient,
+239 unit="K",
+240 description="Ambient temperature of machine"
+241 )
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from .heat_transfer import TwoPhaseHeatTransfer, HeatTransfer, calc_reynolds_pipe
+2from vclibpy.components.heat_exchangers.heat_transfer import constant
+3from vclibpy.components.heat_exchangers.heat_transfer import wall
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import abc
+2import logging
+ +4from .heat_transfer import HeatTransfer
+5from vclibpy.media import TransportProperties
+ + +8logger = logging.getLogger(__name__)
+ + +11class AirToWallTransfer(HeatTransfer, abc.ABC):
+12 """
+13 Heat transfer model for air to wall.
+ +15 Args:
+16 A_cross (float):
+17 Cross-section area in m2.
+18 characteristic_length (float):
+19 Length in m to calculate the similitude approach for the
+20 heat transfer from secondary_medium -> wall.
+21 """
+ +23 def __init__(self, A_cross: float, characteristic_length: float):
+24 self.A_cross = A_cross
+25 self.characteristic_length = characteristic_length
+ +27 def calc(self, transport_properties: TransportProperties, m_flow: float) -> float:
+28 """
+29 Heat transfer coefficient from air to the wall of the heat exchanger.
+30 The flow is assumed to be always laminar.
+ +32 Returns:
+33 float: Heat transfer coefficient in W/(m^2*K).
+34 """
+35 Re = self.calc_reynolds(dynamic_viscosity=transport_properties.dyn_vis, m_flow=m_flow)
+36 alpha_sec = self.calc_laminar_area_nusselt(Re, transport_properties.Pr, lambda_=transport_properties.lam)
+37 return alpha_sec
+ +39 @abc.abstractmethod
+40 def calc_reynolds(self, dynamic_viscosity: float, m_flow: float):
+41 """
+42 Calculate the reynolds number of the given flow rate.
+ +44 Args:
+45 dynamic_viscosity (float): Dynamic viscosity.
+46 m_flow (float): Mass flow rate.
+ +48 Returns:
+49 float: Reynolds number
+50 """
+51 raise NotImplementedError
+ +53 @abc.abstractmethod
+54 def calc_laminar_area_nusselt(self, Re, Pr, lambda_):
+55 """
+56 Calculate the Nusselt number for laminar heat transfer
+57 on an area (Used for Air->Wall in the evaporator).
+ +59 Args:
+60 Re (float): Reynolds number
+61 Pr (float): Prandlt number
+62 lambda_ (float): Lambda of air
+ +64 Returns:
+65 float: Nusselt number
+66 """
+67 raise NotImplementedError
+ + +70class WSUAirToWall(AirToWallTransfer):
+71 """
+72 Class to implement the heat transfer calculations
+73 based on the WSÜ-Script at the RWTH.
+74 """
+ +76 def calc_reynolds(self, dynamic_viscosity: float, m_flow: float) -> float:
+77 """
+78 Calculate the Reynolds number on air side.
+ +80 Args:
+81 dynamic_viscosity (float): Dynamic viscosity of air.
+82 m_flow (float): Mass flow rate of air.
+ +84 Returns:
+85 float: Reynolds number.
+86 """
+87 velocity_times_dens = m_flow / self.A_cross
+88 return (velocity_times_dens * self.characteristic_length) / dynamic_viscosity
+ +90 def calc_laminar_area_nusselt(self, Re, Pr, lambda_) -> float:
+91 """
+92 Calculate the Nusselt number for laminar heat transfer
+93 on an area (Used for Air->Wall in the evaporator).
+ +95 Args:
+96 Re (float): Reynolds number of air.
+97 Pr (float): Prandtl number of air.
+98 lambda_ (float): Lambda of air.
+ +100 Returns:
+101 float: Nusselt number of air.
+102 """
+103 const_fac = 0.664
+104 exp_rey = 0.5
+105 exp_pra = 1 / 3
+106 if Re > 2e5:
+107 raise ValueError(f"Given Re {Re} is outside of allowed bounds for WSÜ-Script")
+108 if Pr > 10 or Pr < 0.6:
+109 raise ValueError(f"Given Pr {Pr} is outside of allowed bounds for WSÜ-Script")
+110 Nu = const_fac * Re ** exp_rey * Pr ** exp_pra
+111 return Nu * lambda_ / self.characteristic_length
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module with constant heat transfer assumptions
+3"""
+4import abc
+ +6from vclibpy.media import TransportProperties
+ + +9class ConstantHeatTransfer(abc.ABC):
+10 """
+11 Constant heat transfer assumption
+ +13 Args:
+14 alpha (float):
+15 Constant heat transfer coefficient in W/(m2*K)
+16 """
+ +18 def __init__(self, alpha: float):
+19 self.alpha = alpha
+ +21 def calc(self, transport_properties: TransportProperties, m_flow: float) -> float:
+22 """
+23 Calculate constant heat transfer coefficient.
+ +25 Args:
+26 transport_properties (TransportProperties): Transport properties of the medium (not used).
+27 m_flow (float): Mass flow rate (not used).
+ +29 Returns:
+30 float: Constant heat transfer coefficient in W/(m2*K).
+ +32 """
+33 return self.alpha
+ + +36class ConstantTwoPhaseHeatTransfer(abc.ABC):
+37 """
+38 Constant heat transfer assumption.
+ +40 Args:
+41 alpha (float):
+42 Constant heat transfer coefficient in W/(m2*K).
+43 """
+ +45 def __init__(self, alpha: float):
+46 self.alpha = alpha
+ +48 def calc(self, **kwargs) -> float:
+49 """
+50 Calculate constant two-phase heat transfer coefficient.
+ +52 Args:
+53 **kwargs: Allows to set arguments for different heat transfer classes which are not used here.
+ +55 Returns:
+56 float: Constant two-phase heat transfer coefficient in W/(m2*K).
+ +58 """
+59 return self.alpha
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module with basic functions to calculate heat transfer coefficients.
+3"""
+4import abc
+ +6import numpy as np
+ +8from vclibpy.media import TransportProperties, ThermodynamicState, MedProp
+9from vclibpy.datamodels import FlowsheetState, Inputs
+ + +12class HeatTransfer(abc.ABC):
+13 """
+14 Base class to implement possible heat transfer models.
+ +16 Methods:
+17 calc(transport_properties: TransportProperties, m_flow: float) -> float:
+18 Abstract method to calculate heat transfer.
+ +20 """
+ +22 @abc.abstractmethod
+23 def calc(self, transport_properties: TransportProperties, m_flow: float) -> float:
+24 """
+25 Calculate heat transfer.
+ +27 Args:
+28 transport_properties (TransportProperties): Transport properties of the medium.
+29 m_flow (float): Mass flow rate.
+ +31 Returns:
+32 float: Calculated heat transfer coefficient.
+ +34 Raises:
+35 NotImplementedError: If the method is not implemented in the subclass.
+36 """
+37 raise NotImplementedError
+ + +40class TwoPhaseHeatTransfer(abc.ABC):
+41 """
+42 Base class to implement possible heat transfer models
+43 """
+ +45 @abc.abstractmethod
+46 def calc(
+47 self,
+48 state_q0: ThermodynamicState,
+49 state_q1: ThermodynamicState,
+50 state_inlet: ThermodynamicState,
+51 state_outlet: ThermodynamicState,
+52 med_prop: MedProp,
+53 inputs: Inputs,
+54 fs_state: FlowsheetState,
+55 m_flow: float
+56 ) -> float:
+57 """
+58 Calculate two-phase heat transfer.
+ +60 Args:
+61 state_q0 (ThermodynamicState): Thermodynamic state at the beginning of the two-phase region.
+62 state_q1 (ThermodynamicState): Thermodynamic state at the end of the two-phase region.
+63 state_inlet (ThermodynamicState): Inlet thermodynamic state.
+64 state_outlet (ThermodynamicState): Outlet thermodynamic state.
+65 med_prop (MedProp): Medium properties class.
+66 inputs (Inputs): Input parameters.
+67 fs_state (FlowsheetState): Flowsheet state.
+68 m_flow (float): Mass flow rate.
+ +70 Returns:
+71 float: Calculated two-phase heat transfer coefficient.
+ +73 Raises:
+74 NotImplementedError: If the method is not implemented in the subclass.
+75 """
+76 raise NotImplementedError
+ + +79def calc_reynolds_pipe(dynamic_viscosity: float, m_flow: float, characteristic_length: float) -> float:
+80 """
+81 Calculate Reynolds number for flow inside a pipe.
+ +83 Args:
+84 dynamic_viscosity (float): Dynamic viscosity of the fluid.
+85 m_flow (float): Mass flow rate.
+86 characteristic_length (float): Characteristic length (e.g., diameter) of the pipe.
+ +88 Returns:
+89 float: Reynolds number.
+ +91 """
+92 return 4 * m_flow / (np.pi * characteristic_length * dynamic_viscosity)
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module with models for pipe-to-wall heat transfer.
+3"""
+ +5from .heat_transfer import HeatTransfer, calc_reynolds_pipe
+6from vclibpy.media import TransportProperties
+ + +9class TurbulentFluidInPipeToWallTransfer(HeatTransfer):
+10 """
+11 Class to model turbulent heat exchange in a pipe.
+ +13 Args:
+14 method (str):
+15 Equation to calc the nusselt number of turbulent flow for
+16 a given Re and Pr number.
+17 Note: Just for one-phase heat transfer!!
+18 Implemented Options are:
+ +20 - Taler2016
+21 - Domanski1989_sp_smooth
+22 - Amalfi2016
+23 - ScriptWSÜ. For turbulent regimes, eta_by_eta_w is assumed to be one.
+ +25 Refer to the paper / documents or the function in this class for more
+26 info on numbers and assumptions
+27 characteristic_length (float):
+28 Length to calculate the similitude approach for the
+29 heat transfer from ref -> wall. For heat pumps this is
+30 always the Diameter of the HE in m
+31 """
+ +33 def __init__(self, method: str, characteristic_length: float):
+34 self.method = method
+35 self.characteristic_length = characteristic_length
+ +37 def calc(self, transport_properties: TransportProperties, m_flow: float) -> float:
+38 """
+39 Calculate heat transfer coefficient from refrigerant to the wall of the heat exchanger.
+ +41 The flow is assumed to be always turbulent and is based on a calibrated
+42 Nusselt correlation.
+ +44 Args:
+45 transport_properties (TransportProperties): Transport properties of the fluid.
+46 m_flow (float): Mass flow rate of the fluid.
+ +48 Returns:
+49 float: Heat transfer coefficient from refrigerant to HE in W/(m^2*K).
+50 """
+51 Re = calc_reynolds_pipe(
+52 characteristic_length=self.characteristic_length,
+53 dynamic_viscosity=transport_properties.dyn_vis,
+54 m_flow=m_flow
+55 )
+56 Nu = self.calc_turbulent_tube_nusselt(Re, transport_properties.Pr)
+57 return Nu * transport_properties.lam / self.characteristic_length
+ +59 def calc_turbulent_tube_nusselt(self, Re, Pr) -> float:
+60 """
+61 Calculate the Nuseelt number for turbulent heat transfer
+62 in a tube (used for ref/water->Wall in the evaporator and condernser).
+ +64 Args:
+65 Re (float): Reynolds number.
+66 Pr (float): Prandtl number.
+ +68 Returns:
+69 float: Nusselt number.
+70 """
+71 if self.method == "Taler2016":
+72 if Re < 3e3 or Re > 1e6:
+73 raise ValueError(f"Given Re {Re} is outside of allowed bounds for method {self.method}")
+74 if 0.1 <= Pr <= 1:
+75 return 0.02155 * Re ** 0.8018 * Pr ** 0.7095
+76 elif 1 < Pr <= 3:
+77 return 0.02155 * Re ** 0.8018 * Pr ** 0.7095
+78 elif 3 < Pr <= 1000:
+79 return 0.02155 * Re ** 0.8018 * Pr ** 0.7095
+80 else:
+81 raise ValueError(f"Given Pr {Pr} is outside of allowed bounds for method {self.method}")
+82 elif self.method == "ScriptWSÜ":
+83 if Re < 3000 or Re > 1e5:
+84 raise ValueError(f"Given Re {Re} is outside of allowed bounds for method {self.method}")
+85 eta_by_eta_w = 1
+86 return 0.027 * Re ** 0.8 ** Pr ** (1 / 3) * eta_by_eta_w ** 0.14
+87 elif self.method == "Amalfi2016":
+88 if Re <= 700:
+89 Nu = (0.0295 * Pr - 0.115) * Re ** 0.954
+90 else:
+91 Nu = (1.760 * Pr - 5.391) * Re ** 0.262
+92 if Nu < 0:
+93 raise ValueError(f"Given Pr {Pr} is outside of allowed bounds for method {self.method}")
+94 return Nu
+95 elif self.method == "Domanski1989_sp_smooth":
+96 # According to Domanski 1989 for singular phase and smooth surfaces
+97 return 0.023 * Re ** 0.8 * Pr ** 0.4
+98 else:
+99 raise TypeError(f"Method {self.method} not supported!")
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import logging
+2from dataclasses import dataclass
+ +4import numpy as np
+ +6from .air_to_wall import AirToWallTransfer
+ +8logger = logging.getLogger(__name__)
+ + +11@dataclass
+12class AirSourceHeatExchangerGeometry:
+13 """
+14 Geometry of a fin and tube heat exchanger with two rows of pipes in a shifted arrangement.
+ +16 Source: VDI-Wärmeatlas, Berechnungsblätter für den Wärmeübergang, 11. Auflage, S.1461
+ +18 As the source is in German, the names are kept in German as well.
+19 """
+20 t_l: float # Achsabstand der Rohre in Luftrichtung in m
+21 t_q: float # Achsabstand der Rohre quer zur Luftrichtung in m
+22 tiefe: float # Tiefe der Rippe gesamt
+23 d_a: float # Äußerer Rohrdurchmesser
+24 d_i: float # Innener Rohrdurchmesser
+25 lambda_R: float # Wärmeleitfähigkeit Material der Wand
+26 n_Rohre: int = 50
+27 n_Rippen: int = 500
+28 a: float = 1.95e-3
+29 dicke_rippe: float = 0.05e-3
+30 laenge: float = 1.040
+31 hoehe: float = 0.64
+ +33 @property
+34 def char_length(self) -> float:
+35 """Return the characteristic length d_a."""
+36 return self.d_a
+ +38 @property
+39 def A_Rippen(self) -> float:
+40 """Return the total surface area of the fins."""
+41 return 2 * self.n_Rippen * (self.tiefe * self.hoehe - self.n_Rohre * np.pi * 0.25 * self.d_a ** 2)
+ +43 @property
+44 def A(self) -> float:
+45 """Total air side heat transfer area."""
+46 return self.A_Rippen + self.A_FreieOberflaecheRohr
+ +48 @property
+49 def A_FreieOberflaecheRohr(self) -> float:
+50 """Free air side area of the tubes."""
+51 return self.n_Rippen * np.pi * self.d_a * self.a * self.n_Rohre
+ +53 @property
+54 def A_RohrInnen(self) -> float:
+55 """Total refrigerant heat transfer area."""
+56 return self.laenge * self.d_i * np.pi * self.n_Rohre
+ +58 @property
+59 def A_RohrUnberippt(self) -> float:
+60 """Total outside area of the tubes without fins"""
+61 return self.laenge * self.d_a * np.pi * self.n_Rohre
+ +63 @property
+64 def verjuengungsfaktor(self) -> float:
+65 """Reduction factor A_cross_free/A_cross_smallest"""
+66 return ((self.hoehe * self.laenge) /
+67 (self.hoehe * self.laenge -
+68 self.hoehe * self.n_Rippen * self.dicke_rippe -
+69 self.d_a * self.n_Rohre / 2 * (self.laenge - self.n_Rippen * self.dicke_rippe)))
+ +71 @property
+72 def phi(self) -> float:
+73 """Auxiliary variable for fin efficiency"""
+74 if self.t_l >= 0.5 * self.t_q:
+75 b_r = self.t_q
+76 else:
+77 b_r = 2 * self.t_l
+78 l_r = np.sqrt(self.t_l **2 + self.t_q **2 / 4)
+79 phi_strich = 1.27 * b_r / self.d_a * np.sqrt(l_r / b_r - 0.3)
+80 return (phi_strich - 1) * (1 + 0.35 * np.log(phi_strich))
+ +82 def eta_R(self, alpha_R) -> float:
+83 """
+84 Calculate fin efficiency.
+ +86 Args:
+87 alpha_R (float): Average heat transfer coefficient for tube and fin.
+ +89 Returns:
+90 float: Fin efficiency.
+91 """
+92 X = self.phi * self.d_a / 2 * np.sqrt(2 * alpha_R / (self.lambda_R * self.dicke_rippe))
+93 return 1 / X * (np.exp(X) - np.exp(-X)) / (np.exp(X) + np.exp(-X))
+ +95 def alpha_S(self, alpha_R) -> float:
+96 """
+97 Calculate apparent heat transfer coefficient.
+ +99 Args:
+100 alpha_R (float): Average heat transfer coefficient for tube and fin.
+ +102 Returns:
+103 float: Apparent heat transfer coefficient.
+104 """
+105 A_R_to_A = self.A_Rippen / self.A
+106 return alpha_R * (1 - (1 - self.eta_R(alpha_R)) * A_R_to_A)
+ + +109class VDIAtlasAirToWallTransfer(AirToWallTransfer):
+110 """
+111 VDI-Atlas based heat transfer estimation.
+112 Check `AirToWallTransfer` for further argument options.
+ +114 This class assumes two pipes with a shifted arrangement.
+ +116 Args:
+117 A_cross (float): Free cross-sectional area.
+118 characteristic_length (float): Characteristic length d_a.
+119 geometry_parameters (AirSourceHeatExchangerGeometry):
+120 Geometry parameters for heat exchanger according to VDI-Atlas
+121 """
+ +123 def __init__(self,
+124 A_cross: float, characteristic_length: float,
+125 geometry_parameters: AirSourceHeatExchangerGeometry):
+126 super().__init__(A_cross=A_cross, characteristic_length=characteristic_length)
+127 self.geometry_parameters = geometry_parameters
+ +129 def calc_reynolds(self, dynamic_viscosity: float, m_flow: float) -> float:
+130 """
+131 Calculate Reynolds number.
+ +133 Args:
+134 dynamic_viscosity (float): Dynamic viscosity of the fluid.
+135 m_flow (float): Mass flow rate.
+ +137 Returns:
+138 float: Reynolds number.
+139 """
+140 velocity_times_dens = m_flow / self.A_cross * self.geometry_parameters.verjuengungsfaktor
+141 return (velocity_times_dens * self.characteristic_length) / dynamic_viscosity
+ +143 def calc_laminar_area_nusselt(self, Re: float, Pr: float, lambda_: float) -> float:
+144 """
+145 Calculate apparent heat transfer coefficient based on Nusselt correlation.
+ +147 Args:
+148 Re (float): Reynolds number.
+149 Pr (float): Prandtl number.
+150 lambda_ (float): Thermal conductivity of air.
+ +152 Returns:
+153 float: Apparent heat transfer coefficient.
+154 """
+155 C = 0.33 # Versetzte Anordnung, zwei Rohre
+156 if Re < 1e3 or Re > 1e5:
+157 logger.debug("Given Re %s is outside of allowed bounds for VDI-Atlas", Re)
+158 A_div_A_0 = self.geometry_parameters.A / self.geometry_parameters.A_RohrUnberippt
+159 if A_div_A_0 < 5 or A_div_A_0 > 30:
+160 logger.debug("Given A/A0 is outside of bounds for method VDI-Atlas")
+161 Nu = (
+162 C * Re ** 0.6 *
+163 A_div_A_0 ** (-0.15) *
+164 Pr ** (1 / 3)
+165 )
+166 alpha_R = Nu * lambda_ / self.characteristic_length
+167 alpha_S = self.geometry_parameters.alpha_S(alpha_R=alpha_R)
+168 return alpha_S
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import logging
+ +3from .heat_transfer import HeatTransfer
+4from vclibpy.media import TransportProperties
+ + +7logger = logging.getLogger(__name__)
+ + +10class WallTransfer(HeatTransfer):
+11 """
+12 Simple wall heat transfer
+ +14 Args:
+15 lambda_ (float):
+16 Heat conductivity of wall material in W/Km
+17 thickness (float):
+18 Thickness of wall in m^2
+19 """
+20 def __init__(self, lambda_: float, thickness: float):
+21 self.thickness = thickness
+22 self.lambda_ = lambda_
+ +24 def calc(self, transport_properties: TransportProperties, m_flow: float) -> float:
+25 """
+26 Heat transfer coefficient inside wall
+ +28 Returns:
+29 float: Heat transfer coefficient in W/(m^2*K)
+30 """
+31 return self.lambda_ / self.thickness
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from .automation import full_factorial_map_generation, calc_multiple_states
+2from .sdf_ import save_to_sdf
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Functions to generate HP Maps automatically
+3"""
+4import logging
+5import pathlib
+6import os
+7from typing import List, Union
+8import multiprocessing
+9import numpy as np
+10import pandas as pd
+11from vclibpy.datamodels import FlowsheetState, Inputs
+12from vclibpy.flowsheets import BaseCycle
+13from vclibpy import utils
+ +15logger = logging.getLogger(__name__)
+ + +18def calc_multiple_states(
+19 save_path: pathlib.Path,
+20 heat_pump: BaseCycle,
+21 inputs: List[Inputs],
+22 **kwargs):
+23 """
+24 Function to calculate the flowsheet states for all given inputs.
+25 All results are stored as a .xlsx file in the given save-path
+ +27 Args:
+28 save_path (pathlib.Path): Location where to save the results as xlsx.
+29 heat_pump (BaseCycle): A valid flowsheet
+30 inputs (List[Inputs]): A list with all inputs to simulate
+31 **kwargs: Solver settings for the flowsheet
+32 """
+33 rel_infos = []
+34 for i, single_inputs in enumerate(inputs):
+35 fs_state = None
+36 logger.info(f"Running combination {i+1}/{len(inputs)}.")
+37 try:
+38 fs_state = heat_pump.calc_steady_state(inputs=single_inputs,
+39 **kwargs)
+40 except Exception as e:
+41 # Avoid loss of data if un-excepted errors occur.
+42 logger.error(f"An error occurred: {e}")
+43 if fs_state is None:
+44 fs_state = FlowsheetState()
+45 hp_state_dic = {
+46 **single_inputs.convert_to_str_value_format(with_unit_and_description=True),
+47 **fs_state.convert_to_str_value_format(with_unit_and_description=True)
+48 }
+49 rel_infos.append(hp_state_dic)
+50 df = pd.DataFrame(rel_infos)
+51 df.index.name = "State Number"
+52 df.to_excel(save_path.joinpath(f"{heat_pump}_{heat_pump.fluid}.xlsx"), sheet_name="HP_states", float_format="%.5f")
+ + +55def full_factorial_map_generation(
+56 heat_pump: BaseCycle,
+57 T_eva_in_ar: Union[list, np.ndarray],
+58 T_con_in_ar: Union[list, np.ndarray],
+59 n_ar: Union[list, np.ndarray],
+60 m_flow_con: float,
+61 m_flow_eva: float,
+62 save_path: Union[pathlib.Path, str],
+63 dT_eva_superheating=5,
+64 dT_con_subcooling=0,
+65 use_multiprocessing: bool = False,
+66 save_plots: bool = False,
+67 **kwargs
+68) -> (pathlib.Path, pathlib.Path):
+69 """
+70 Run a full-factorial simulation to create performance maps
+71 used in other simulation tools like Modelica or to analyze
+72 the off-design of the flowsheet.
+73 The results are stored and returned as .sdf and .csv files.
+74 Currently, only varying T_eva_in, T_con_in, and n is implemented.
+75 However, changing this to more dimensions or other variables
+76 is not much work. In this case, please raise an issue.
+ +78 Args:
+79 heat_pump (BaseCycle): The flowsheet to use
+80 T_eva_in_ar: Array with inputs for T_eva_in
+81 T_con_in_ar: Array with inputs for T_con_in
+82 n_ar: Array with inputs for n_ar
+83 m_flow_con: Condenser mass flow rate
+84 m_flow_eva: Evaporator mass flow rate
+85 save_path: Where to save all results.
+86 dT_eva_superheating: Evaporator superheating
+87 dT_con_subcooling: Condenser subcooling
+88 use_multiprocessing:
+89 True to use multiprocessing. May speed up the calculation. Default is False
+90 save_plots:
+91 True to save plots of each steady state point. Default is False
+92 **kwargs: Solver settings for the flowsheet
+ +94 Returns:
+95 tuple (pathlib.Path, pathlib.Path):
+96 Path to the created .sdf file and to the .csv file
+97 """
+98 if isinstance(save_path, str):
+99 save_path = pathlib.Path(save_path)
+100 if save_plots:
+101 kwargs["save_path_plots"] = pathlib.Path(save_path).joinpath(f"plots_{heat_pump.flowsheet_name}_{heat_pump.fluid}")
+102 os.makedirs(kwargs["save_path_plots"], exist_ok=True)
+ +104 list_mp_inputs = []
+105 list_inputs = []
+106 idx_for_access_later = []
+107 for i_T_eva_in, T_eva_in in enumerate(T_eva_in_ar):
+108 for i_n, n in enumerate(n_ar):
+109 for i_T_con_in, T_con_in in enumerate(T_con_in_ar):
+110 idx_for_access_later.append([i_n, i_T_con_in, i_T_eva_in])
+111 inputs = Inputs(n=n,
+112 T_eva_in=T_eva_in,
+113 T_con_in=T_con_in,
+114 m_flow_eva=m_flow_eva,
+115 m_flow_con=m_flow_con,
+116 dT_eva_superheating=dT_eva_superheating,
+117 dT_con_subcooling=dT_con_subcooling)
+118 list_mp_inputs.append([heat_pump, inputs, kwargs])
+119 list_inputs.append(inputs)
+120 fs_states = []
+121 i = 0
+122 if use_multiprocessing:
+123 pool = multiprocessing.Pool(processes=multiprocessing.cpu_count())
+124 for fs_state in pool.imap(_calc_single_hp_state, list_mp_inputs):
+125 fs_states.append(fs_state)
+126 i += 1
+127 logger.info(f"Ran {i} of {len(list_mp_inputs)} points")
+128 else:
+129 for inputs in list_inputs:
+130 fs_state = _calc_single_hp_state([heat_pump, inputs, kwargs])
+131 fs_states.append(fs_state)
+132 i += 1
+133 logger.info(f"Ran {i} of {len(list_mp_inputs)} points")
+ +135 # Save to sdf
+136 result_shape = (len(n_ar), len(T_con_in_ar), len(T_eva_in_ar))
+137 _dummy = np.zeros(result_shape) # Use a copy to avoid overwriting of values of any sort.
+138 _dummy[:] = np.nan
+139 # Get all possible values:
+140 all_variables = {}
+141 all_variables_info = {}
+142 variables_to_excel = []
+143 for fs_state, inputs in zip(fs_states, list_inputs):
+144 all_variables.update({var: _dummy.copy() for var in fs_state.get_variable_names()})
+145 all_variables_info.update({var: variable for var, variable in fs_state.get_variables().items()})
+146 variables_to_excel.append({
+147 **inputs.convert_to_str_value_format(with_unit_and_description=True),
+148 **fs_state.convert_to_str_value_format(with_unit_and_description=True),
+149 })
+ +151 # Save to excel
+152 save_path_sdf = save_path.joinpath(f"{heat_pump.flowsheet_name}_{heat_pump.fluid}.sdf")
+153 save_path_csv = save_path.joinpath(f"{heat_pump.flowsheet_name}_{heat_pump.fluid}.csv")
+154 pd.DataFrame(variables_to_excel).to_csv(
+155 save_path_csv
+156 )
+ +158 for fs_state, idx_triple in zip(fs_states, idx_for_access_later):
+159 i_n, i_T_con_in, i_T_eva_in = idx_triple
+160 for variable_name, variable in fs_state.get_variables().items():
+161 all_variables[variable_name][i_n][i_T_con_in][i_T_eva_in] = variable.value
+ +163 _nd_data = {}
+164 for variable, nd_data in all_variables.items():
+165 _nd_data.update({
+166 variable: {
+167 "data": nd_data,
+168 "unit": all_variables_info[variable].unit,
+169 "comment": all_variables_info[variable].description}
+170 })
+ +172 _scale_values = {
+173 "n": n_ar,
+174 "T_con_in": T_con_in_ar,
+175 "T_eva_in": T_eva_in_ar
+176 }
+177 inputs: Inputs = list_inputs[0]
+178 _parameters = {}
+179 for name, variable in inputs.items():
+180 if name not in _scale_values:
+181 _parameters[name] = {
+182 "data": variable.value,
+183 "unit": variable.unit,
+184 "comment": variable.description
+185 }
+186 _scales = {}
+187 for name, data in _scale_values.items():
+188 _scales[name] = {
+189 "data": data,
+190 "unit": inputs.get(name).unit,
+191 "comment": inputs.get(name).description
+192 }
+ +194 sdf_data = {
+195 heat_pump.flowsheet_name:
+196 {
+197 heat_pump.fluid: (_scales, _nd_data, _parameters)
+198 }
+199 }
+200 utils.save_to_sdf(data=sdf_data, save_path=save_path_sdf)
+ +202 # Terminate heat pump med-props:
+203 heat_pump.terminate()
+ +205 return save_path_sdf, save_path_csv
+ + +208def _calc_single_hp_state(data):
+209 """Helper function for a single state to enable multiprocessing"""
+210 heat_pump, inputs, kwargs = data
+211 fs_state = None
+212 try:
+213 fs_state = heat_pump.calc_steady_state(inputs=inputs,
+214 **kwargs)
+215 except Exception as e:
+216 logger.error(f"An error occurred for input: {inputs.__dict__}: {e}")
+217 if fs_state is None:
+218 fs_state = FlowsheetState()
+219 # Append the data to the dataframe
+220 return fs_state
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import time
+2import logging
+ +4from vclibpy import Inputs
+5from vclibpy.flowsheets import BaseCycle
+ +7logger = logging.getLogger(__name__)
+ + +10def nominal_hp_design(
+11 heat_pump: BaseCycle,
+12 inputs: Inputs,
+13 fluid: str,
+14 dT_con: float = None,
+15 dT_eva: float = None,
+16 **kwargs) -> dict:
+17 """
+18 Function to calculate the heat pump design
+19 at a given nominal point.
+20 Args:
+21 heat_pump (BaseCycle): A supported flowsheet
+22 inputs (Inputs):
+23 The input values at the nominal point.
+24 If the mass flow rates are not given, you
+25 can use dT_con and dT_eva to iteratively calculate
+26 the mass flow rates in order to achieve the required
+27 temperature differences at the nominal point.
+28 dT_con (float):
+29 Condenser temperature difference to calculate mass flow rate
+30 dT_eva (float):
+31 Evaporator temperature difference to calculate mass flow rate
+32 fluid (str): Fluid to be used.
+33 **kwargs:
+34 m_flow_eva_start: Guess start-value for iteration. Default 0.2
+35 m_flow_con_start: Guess start-value for iteration. Default 1
+36 accuracy: Minimal accuracy for mass flow rate iteration (in kg/s).
+37 Default 0.001 kg/s
+ +39 Returns:
+40 dict: A dictionary with all flowsheet states and inputs containing
+41 information about the nominal design.
+42 """
+43 t0 = time.time()
+44 # Define nominal values:
+45 m_flow_con_start = kwargs.get("m_flow_con_start", 0.2)
+46 m_flow_eva_start = kwargs.get("m_flow_eva_start", 1)
+47 accuracy = kwargs.get("accuracy", 0.001)
+48 calculate_m_flow = dT_eva is not None and dT_con is not None
+49 if calculate_m_flow:
+50 # Get nominal value:
+51 fs_state = heat_pump.calc_steady_state(fluid=fluid, inputs=inputs)
+52 if fs_state is None:
+53 raise ValueError("Given configuration is infeasible at nominal point.")
+ +55 else:
+56 # We have to iterate to match the m_flows to the Q_cons:
+57 m_flow_eva_next = m_flow_eva_start
+58 m_flow_con_next = m_flow_con_start
+59 while True:
+60 # Set values
+61 m_flow_eva = m_flow_eva_next
+62 m_flow_con = m_flow_con_next
+63 inputs.set("m_flow_con", m_flow_con)
+64 inputs.set("m_flow_eva", m_flow_eva)
+65 # Get nominal value:
+66 fs_state = heat_pump.calc_steady_state(fluid=fluid, inputs=inputs)
+67 if fs_state is None:
+68 raise ValueError("Given configuration is infeasible at nominal point.")
+69 cp_eva = heat_pump.evaporator._secondary_cp
+70 cp_con = heat_pump.condenser._secondary_cp
+71 m_flow_con_next = fs_state.get("Q_con") / (dT_con * cp_con)
+72 m_flow_eva_next = (fs_state.get("Q_con") * (1 - 1 / fs_state.get("COP"))) / (dT_eva * cp_eva)
+73 # Check convergence:
+74 if abs(m_flow_eva_next - m_flow_eva) < accuracy and abs(m_flow_con-m_flow_con_next) < accuracy:
+75 break
+ +77 nominal_design_info = {
+78 **inputs.convert_to_str_value_format(with_unit_and_description=False),
+79 **fs_state.convert_to_str_value_format(with_unit_and_description=False),
+80 "dT_con": dT_con,
+81 "dT_eva": dT_eva
+82 }
+83 logger.info("Auto-generation of nominal values took %s seconds", time.time()-t0)
+84 logger.info('Nominal values: %s', nominal_design_info)
+ +86 return nominal_design_info
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import pathlib
+2from typing import List
+ +4import numpy as np
+5import matplotlib.pyplot as plt
+6import sdf
+ + +9def plot_sdf_map(
+10 filepath_sdf: pathlib.Path,
+11 nd_data: str,
+12 first_dimension: str,
+13 second_dimension: str,
+14 fluids: List[str] = None,
+15 flowsheets: List[str] = None,
+16 violin_plot_variable: str = None,
+17 third_dimension: str = None
+18):
+19 """
+20 Generate and display visualizations based on data from an SDF (Structured Data File) dataset.
+21 This function generates various types of visualizations based on the provided parameters,
+22 including 3D scatter plots, 3D surface plots, and violin plots, and displays them using Matplotlib.
+ +24 Args:
+25 filepath_sdf (pathlib.Path):
+26 The path to the SDF dataset file.
+27 nd_data (str):
+28 The name of the primary data to be plotted.
+29 first_dimension (str):
+30 The name of the first dimension for the visualization.
+31 second_dimension (str):
+32 The name of the second dimension for the visualization.
+33 fluids (List[str], optional):
+34 List of specific fluids to include in the visualization.
+35 Default is None, which includes all fluids.
+36 flowsheets (List[str], optional):
+37 List of specific flowsheets to include in the visualization.
+38 Default is None, which includes all flowsheets.
+39 violin_plot_variable (str, optional):
+40 The variable to be used for creating violin plots.
+41 Default is None, which disables violin plots.
+42 third_dimension (str, optional):
+43 The name of the third dimension for 4D visualizations.
+44 Default is None, which disables 4D plotting.
+ +46 Raises:
+47 KeyError: If the specified data or dimensions are not found in the dataset.
+ +49 Examples:
+50 >>> FILEPATH_SDF = r"HeatPumpMaps.sdf"
+51 >>> plot_sdf_map(
+52 >>> filepath_sdf=FILEPATH_SDF,
+53 >>> nd_data="COP",
+54 >>> first_dimension="T_eva_in",
+55 >>> second_dimension="n",
+56 >>> fluids=["R410A"],
+57 >>> flowsheets=["OptiHorn"],
+58 >>> )
+ +60 """
+61 if fluids is None:
+62 fluids = []
+63 if flowsheets is None:
+64 flowsheets = []
+65 if "T_" in second_dimension:
+66 offset_sec = -273.15
+67 else:
+68 offset_sec = 0
+69 if "T_" in first_dimension:
+70 offset_pri = -273.15
+71 else:
+72 offset_pri = 0
+73 offset_thi = 0
+74 plot_4D = False
+75 if third_dimension is not None:
+76 plot_4D = True
+77 if "T_" in third_dimension:
+78 offset_thi = -273.15
+ +80 dataset = sdf.load(str(filepath_sdf))
+81 plot_violin = True
+82 if violin_plot_variable is None:
+83 plot_violin = False
+84 violin_plot_variable = ""
+85 if plot_violin:
+86 if flowsheets:
+87 n_rows = len(flowsheets)
+88 else:
+89 n_rows = len(dataset.groups)
+90 fig_v, ax_v = plt.subplots(nrows=n_rows, ncols=1, sharex=True,
+91 squeeze=False)
+92 fig_v.suptitle(violin_plot_variable)
+93 i_fs = 0
+94 nd_str_plot = nd_data
+95 fac = 1
+96 if nd_data == "dT_eva_min":
+97 nd_data = "T_1"
+98 sub_str = "T_eva_in"
+99 fac = - 1
+100 elif nd_data == "dT_con":
+101 nd_data = "T_3"
+102 sub_str = "T_con_in"
+103 elif nd_data == "dT_sh":
+104 nd_data = "T_1"
+105 sub_str = "T_4"
+106 else:
+107 sub_str = ""
+ +109 if nd_str_plot.startswith("T_"):
+110 offset_nd = -273.15
+111 else:
+112 offset_nd = 0
+ +114 for flowsheet in dataset.groups:
+115 violin_data = {}
+116 if flowsheet.name not in flowsheets and len(flowsheets) > 0:
+117 continue
+118 for fluid in flowsheet.groups:
+119 if fluid.name not in fluids and len(fluids) > 0:
+120 continue
+121 nd, fd, sd, sub_data, td = None, None, None, None, None
+122 _other_scale = {}
+123 for ds in fluid.datasets:
+124 if ds.name == nd_data:
+125 nd = ds
+126 elif ds.name == first_dimension:
+127 fd = ds.data
+128 elif ds.name == second_dimension:
+129 sd = ds.data
+130 if ds.name == sub_str:
+131 sub_data = ds.data
+132 if ds.name == violin_plot_variable:
+133 data = ds.data.flatten()
+134 violin_data[fluid.name] = data[~np.isnan(data)]
+135 if plot_4D and ds.name == third_dimension:
+136 td = ds.data
+ +138 if nd is None:
+139 raise KeyError("nd-String not found in dataset")
+ +141 if sub_data is None:
+142 sub_data = np.zeros(nd.data.shape)
+ +144 # Check other scales:
+145 for i, scale in enumerate(nd.scales):
+146 if scale.name not in [first_dimension, second_dimension]:
+147 _other_scale[i] = scale
+ +149 if fd is None or sd is None or not _other_scale or (plot_4D and td is None):
+150 raise KeyError("One of the given strings was not found in dataset")
+ +152 if plot_4D:
+153 fig = plt.figure()
+154 figtitle = f"{flowsheet.name}_{fluid.name}_{nd_str_plot}"
+155 fig.suptitle(figtitle)
+156 ax = fig.add_subplot(111, projection='3d')
+157 ax.set_xlabel(first_dimension)
+158 ax.set_ylabel(second_dimension)
+159 ax.set_zlabel(third_dimension)
+160 fourth_dim = (nd.data - sub_data) * fac + offset_nd
+161 # Scale values for better sizes of circles:
+162 bounds = [fourth_dim.min(), fourth_dim.max()]
+163 _max_circle_size = 30
+164 fourth_dim_scaled = (fourth_dim - bounds[0]) / (bounds[1] - bounds[0]) * _max_circle_size
+165 inp = [fd + offset_pri, sd + offset_sec, td + offset_thi]
+166 import itertools
+167 scattergrid = np.array([c for c in itertools.product(*inp)])
+168 ax.scatter(scattergrid[:, 0],
+169 scattergrid[:, 1],
+170 scattergrid[:, 2],
+171 c=fourth_dim_scaled,
+172 s=fourth_dim_scaled)
+173 else:
+174 for index, scale in _other_scale.items():
+175 for idx_data, value in enumerate(scale.data):
+176 if index==0:
+177 Z = nd.data[idx_data, :, :]
+178 if sub_str in ["T_4", ""]:
+179 sub_data_use = sub_data[idx_data, :, :]
+180 else:
+181 sub_data_use = sub_data
+182 elif index == 1:
+183 Z = nd.data[:, idx_data, :]
+184 if sub_str in ["T_4", ""]:
+185 sub_data_use = sub_data[:, idx_data, :]
+186 else:
+187 sub_data_use = sub_data
+188 else:
+189 Z = nd.data[:, :, idx_data]
+190 if sub_str in ["T_4", ""]:
+191 sub_data_use = sub_data[:, :, idx_data]
+192 else:
+193 sub_data_use = sub_data
+194 if not plot_violin:
+195 fig = plt.figure()
+196 figtitle = f"{flowsheet.name}_{fluid.name}_{nd_str_plot}_{scale.name}={round(value, 3)}"
+197 fig.suptitle(figtitle)
+198 ax = fig.add_subplot(111, projection='3d')
+199 ax.set_xlabel(first_dimension)
+200 ax.set_ylabel(second_dimension)
+201 X, Y = np.meshgrid(fd, sd)
+202 ax.plot_surface(X + offset_pri, Y + offset_sec, (Z - sub_data_use)*fac + offset_nd)
+ +204 if plot_violin:
+205 for key, value in violin_data.items():
+206 print(f"{violin_plot_variable}: {flowsheet.name}_{key}")
+207 print(f"Min: {np.min(value)}")
+208 print(f"Max: {np.max(value)}")
+209 print(f"Mean: {np.mean(value)}")
+210 print(f"Median: {np.median(value)}\n")
+211 ax_v[i_fs][0].violinplot(
+212 list(violin_data.values()),
+213 showextrema=True,
+214 showmeans=True,
+215 showmedians=True
+216 )
+217 set_axis_style(ax_v[i_fs][0], list(violin_data.keys()))
+218 ax_v[i_fs][0].set_ylabel(flowsheet.name.replace("Flowsheet", ""))
+219 i_fs += 1
+220 plt.show()
+ + +223def set_axis_style(ax, labels):
+224 """
+225 From: https://matplotlib.org/3.1.1/gallery/statistics/customized_violin.html#sphx-glr-gallery-statistics-customized-violin-py
+226 """
+227 ax.get_xaxis().set_tick_params(direction='out')
+228 ax.xaxis.set_ticks_position('bottom')
+229 ax.set_xticks(np.arange(1, len(labels) + 1))
+230 ax.set_xticklabels(labels)
+231 ax.set_xlim(0.25, len(labels) + 0.75)
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import itertools
+2import pathlib
+ +4import pandas as pd
+5import sdf
+ + +8def save_to_sdf(data: dict, save_path: pathlib.Path):
+9 """
+10 Save given input dictionary to a sdf file in the given save_path
+ +12 Args:
+13 data (dict):
+14 A dictionary with the following structure:
+15 Keys: Flowsheet_name
+16 Values: A dictionary with the following structure:
+ +18 - Keys: Fluids
+19 - Values: Data dictionaries with the following structure:
+20 A tuple with three values, in that order:
+ +22 - scales: Of the given nd_data, e.g. T_Amb, n
+23 - nd_data: More-dimensional data, e.g. COP
+24 - parameters: Scalar values, like m_flow_con or similar
+ +26 save_path (pathlib.Path): Where to store the data
+27 flowsheet_name (str): Name of the flowsheet. This is the top level group
+28 :return:
+29 """
+30 if isinstance(save_path, str):
+31 save_path = pathlib.Path(save_path)
+32 _all_groups = []
+ +34 for flowsheet_name, fluid_dict in data.items():
+35 _all_fluids = []
+36 for fluid, fluid_data in fluid_dict.items():
+37 # First write scales
+38 _scales = []
+39 for scale_name, scale_values in fluid_data[0].items():
+40 _scales.append(sdf.Dataset(scale_name,
+41 data=scale_values["data"],
+42 unit=scale_values["unit"],
+43 is_scale=True,
+44 display_name=scale_name,
+45 comment=scale_values.get("comment", "")))
+46 # Now the ND-Data:
+47 _nd_data = []
+48 for data_name, data_values in fluid_data[1].items():
+49 _nd_data.append(sdf.Dataset(data_name,
+50 data=data_values["data"],
+51 unit=data_values["unit"],
+52 scales=_scales,
+53 comment=data_values.get("comment", "")))
+54 # Now the constant parameters
+55 _paras = []
+56 for para_name, para_value in fluid_data[2].items():
+57 _paras.append(sdf.Dataset(para_name,
+58 data=para_value["data"],
+59 unit=para_value["unit"],
+60 comment=para_value.get("comment", "")))
+ +62 # Save everything
+63 fluid_group = sdf.Group(fluid, comment="Values for fluid", datasets=_scales + _nd_data + _paras)
+64 _all_fluids.append(fluid_group)
+ +66 flowsheet_group = sdf.Group(flowsheet_name,
+67 comment="Multiple fluids for the flowsheet",
+68 groups=_all_fluids)
+69 _all_groups.append(flowsheet_group)
+ +71 parent_group = sdf.Group("/", comment="Generated with VCLibPy", groups=_all_groups)
+72 sdf.save(save_path, group=parent_group)
+73 return save_path
+ + +76def merge_sdfs(filepaths, save_path):
+77 """
+78 Merge given files and return a merged file.
+79 Be careful if both files contain the same combination.
+80 Then, the latter element of the list will overwrite the first one.
+ +82 Args:
+83 filepaths (list): List with paths to the files
+84 save_path (str): Save path for the new file
+85 """
+86 _all_flowsheets = {}
+87 # Merge to structure
+88 for fpath in filepaths:
+89 dataset = sdf.load(fpath)
+90 for flowsheet in dataset.groups:
+91 fluid_data = {fluid.name: fluid for fluid in flowsheet.groups}
+92 if flowsheet.name not in _all_flowsheets:
+93 _all_flowsheets.update({flowsheet.name: fluid_data})
+94 else:
+95 _all_flowsheets[flowsheet.name].update(fluid_data)
+ +97 # Write structure
+98 _all_groups = []
+99 for flowsheet_name, fluid_dict in _all_flowsheets.items():
+100 _all_fluids = []
+101 for fluid, data in fluid_dict.items():
+102 _all_fluids.append(data)
+103 flowsheet_group = sdf.Group(flowsheet_name,
+104 comment="Multiple fluids for the flowsheet",
+105 groups=_all_fluids)
+106 _all_groups.append(flowsheet_group)
+ +108 parent_group = sdf.Group("/", comment="Generated with python script", groups=_all_groups)
+109 sdf.save(save_path, group=parent_group)
+110 return save_path
+ + +113def sdf_to_csv(filepath: pathlib.Path, save_path: pathlib.Path):
+114 """
+115 Convert a given .sdf file to multiple excel files,
+116 for each combination of flowsheet and refrigerant one file.
+ +118 Args:
+119 filepath (pathlib.Path): sdf file
+120 save_path (pathlib.Path): Directory where to store the csv files.
+121 """
+122 dataset = sdf.load(str(filepath))
+123 for flowsheet in dataset.groups:
+124 for fluid in flowsheet.groups:
+125 dfs = []
+126 for data in fluid.datasets:
+127 if _is_nd(data):
+128 dfs.append(_unpack_nd_data(data))
+129 df = pd.concat(dfs, axis=1)
+130 df = df.loc[:, ~df.columns.duplicated()]
+131 df.to_csv(save_path.joinpath(f"{flowsheet.name}_{fluid.name}.csv"))
+ + +134def _get_name(data):
+135 return f"{data.name} in {data.unit} ({data.comment})"
+ + +138def _is_nd(data):
+139 if data.scales == [None]:
+140 return False
+141 return True
+ + +144def _unpack_nd_data(data):
+145 column_name = _get_name(data)
+146 scale_names = [_get_name(scale) for scale in data.scales]
+147 scales_with_idx = [[(idx, value) for idx, value in enumerate(scale.data)] for scale in data.scales]
+148 all_data = []
+149 for scales in itertools.product(*scales_with_idx):
+150 indexer = tuple([scale[0] for scale in scales])
+151 values = [scale[1] for scale in scales]
+152 all_data.append({
+153 column_name: data.data[indexer],
+154 **{name: value for name, value in zip(scale_names, values)}
+155 })
+156 return pd.DataFrame(all_data)
+ + +159if __name__ == '__main__':
+160 sdf_to_csv(
+161 filepath=pathlib.Path(r"D:\00_temp\calibration_jmo\Optihorst_3D_vclibpy.sdf"),
+162 save_path=pathlib.Path(r"D:\04_git\vclibpy\tests\regression_data")
+163 )
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from typing import List
+2import csv
+3import pandas as pd
+ +5from vclibpy.components.compressors import TenCoefficientCompressor
+6from vclibpy import media, Inputs
+ +8try:
+9 from sklearn.linear_model import LinearRegression
+10 from sklearn.preprocessing import PolynomialFeatures
+11 from xlsxwriter.workbook import Workbook
+12except ImportError as err:
+13 raise ImportError("You have to install xlsxwriter and "
+14 "sklearn to use this regression tool")
+ + +17def create_regression_data(
+18 variables: List[str],
+19 T_con: List[float], T_eva: List[float], n: List[float],
+20 T_sh: float, T_sc: float, n_max: int, V_h: float, fluid: str, datasheet: str,
+21 capacity_definition: str, assumed_eta_mech: int,
+22 folder_path: str):
+23 """
+24 Performs multidimensional linear regression to create compressor learning data.
+ +26 Args:
+27 variables: (List[str]):
+28 Variable names to create regressions for.
+29 Options are: eta_s, lambda_h, and eta_mech
+30 T_con (List[float]): Condensing temperatures in K
+31 T_eva (List[float]): Evaporation temperatures in K
+32 n (List[float]): Compressor speeds from 0 to 1
+33 T_sh (float): Superheat temperature.
+34 T_sc (float): Subcooling temperature.
+35 n_max (int): Maximum compressor speed.
+36 V_h (float): Compressor volume.
+37 fluid (str): Type of refrigerant.
+38 datasheet (str): Path to the modified datasheet.
+39 capacity_definition (str): Definition of compressor capacity (e.g., "cooling").
+40 assumed_eta_mech (int): Assumed mechanical efficiency.
+41 folder_path (str): Path to the folder where the newly created table will be saved.
+ +43 Returns:
+44 List[float]: A list containing the regression parameters [P0, P1, ..., P9].
+ +46 Raises:
+47 ValueError: If any specified variable column is not present in the DataFrame.
+ +49 Example:
+50 >>> create_regression_data(11.1, 8.3, 120, 20.9e-6, "PROPANE", "path/to/datasheet.xlsx",
+51 ... "cooling", 1, 6, 5, 5, 5, 303.15, 243.15, "path/to/save", 3)
+52 [intercept, P1, P2, P3, P4, P5, P6, P7, P8, P9]
+53 """
+54 # create RefProp, fluid & compressor instance
+55 med_prop = media.CoolProp(fluid)
+ +57 compressor = TenCoefficientCompressor(
+58 N_max=n_max, V_h=V_h, T_sc=T_sc, T_sh=T_sh,
+59 capacity_definition=capacity_definition,
+60 assumed_eta_mech=assumed_eta_mech,
+61 datasheet=datasheet
+62 )
+ +64 compressor.med_prop = med_prop
+65 keywords = {
+66 "eta_s": "Isentropic Efficiency(-)",
+67 "lambda_h": "Volumentric Efficiency(-)",
+68 "eta_mech": "Mechanical Efficiency(-)"
+69 }
+ +71 tuples_for_cols = [("", "n")]
+72 for _variable in variables:
+73 for _n in n:
+74 tuples_for_cols.append((keywords[_variable], compressor.get_n_absolute(_n)))
+75 # tuples_for_cols:
+76 # eta_s eta_s eta_s lambda_h lambda_h lambda_h eta_mech ...
+77 # n 30 60 90 30 60 90 30 ...
+78 cols = pd.MultiIndex.from_tuples(tuples_for_cols)
+79 final_df = pd.DataFrame(
+80 data={cols[0]: ["P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10"]},
+81 columns=cols
+82 )
+83 # final_df: column names are tuples (tuples_for_cols).
+84 # First column is filled with P1, P2, ...
+ +86 # for-loop for multiple types(eta_s, eta_mech, etc)
+87 for m, _variable in enumerate(variables):
+88 for k, _n in enumerate(n): # for-loop for multiple rotation speeds
+89 T_eva_list = []
+90 T_con_list = []
+91 result_list = []
+92 # for-loop for multiple evaporating temperatures
+93 for i in range(len(T_eva)):
+94 # for-loop for multiple condensing temperatures
+95 for j in range(len(T_con)):
+96 if T_eva[i] < T_con[j]:
+97 p1 = med_prop.calc_state("TQ", T_eva[i], 1).p
+98 state_1 = med_prop.calc_state("PT", p1, (T_eva[i] + T_sh))
+99 compressor.state_inlet = state_1
+100 p2 = med_prop.calc_state("TQ", T_con[j], 1).p
+101 # The enthalpy and entropy of the outlet
+102 # state do not matter, only the pressure:
+103 # TODO: Enable calculation of get_lambda_h etc. with p2 only
+104 state_2 = med_prop.calc_state("PS", p2, state_1.s)
+105 compressor.state_outlet = state_2
+106 T_eva_list.append(T_eva[i])
+107 T_con_list.append(T_con[j])
+108 inputs = Inputs(n=_n)
+ +110 if _variable == "eta_s":
+111 result_list.append(compressor.get_eta_isentropic(
+112 p_outlet=p2, inputs=inputs)
+113 )
+114 elif _variable == "lambda_h":
+115 result_list.append(compressor.get_lambda_h(inputs=inputs))
+116 elif _variable == "eta_mech":
+117 result_list.append(compressor.get_eta_mech(inputs=inputs))
+ +119 df = pd.DataFrame(
+120 data={"T_eva": T_eva_list,
+121 "T_con": T_con_list,
+122 _variable: result_list}
+123 )
+ +125 final_df[cols[m * len(n) + k + 1]] = create_regression_parameters(df, _variable)
+ +127 # dataframes with a double column header can't be saved as a
+128 # .xlsx yet, if index = False (NotImplementedError).
+129 # .xlsx format is necessary, because TenCoefficientCompressor.get_parameter()
+130 # expects a .xlsx as an input
+131 # --> workaround: save the dataframe as a .csv, read it again and save it as a .xlsx
+132 # TODO: Revert once this feature is in pandas.
+133 final_df.to_csv(folder_path + r"\regressions.csv", index=False)
+ +135 workbook = Workbook(folder_path + r"\regressions.xlsx")
+136 worksheet = workbook.add_worksheet()
+137 with open(folder_path + r"\regressions.csv", 'rt', encoding='utf8') as f:
+138 reader = csv.reader(f)
+139 for r, row in enumerate(reader):
+140 for c, col in enumerate(row):
+141 worksheet.write(r, c, col)
+142 workbook.close()
+ + +145def create_regression_parameters(df: pd.DataFrame, variable: str):
+146 """
+147 Performs multidimensional linear regression to calculate
+148 ten coefficient regression parameters.
+ +150 Args:
+151 df (pd.DataFrame): The input DataFrame containing the necessary columns.
+152 variable (str): The column name for the dependent variable.
+ +154 Returns:
+155 List[float]: A list containing the ten regression parameters.
+ +157 Raises:
+158 ValueError: If the specified variable column is not present in the DataFrame.
+ +160 Example:
+161 >>> df = pd.DataFrame({'T_eva': [1, 2, 3], 'T_con': [4, 5, 6], 'X': [7, 8, 9]})
+162 >>> create_regression_parameters(df, 'X')
+163 [intercept, P1, P2, P3, P4, P5, P6, P7, P8, P9]
+164 """
+165 # extract the columns x, y und z
+166 x = df['T_eva'].values
+167 y = df['T_con'].values
+168 z = df[variable].values
+ +170 # define the features (x, y, x^2, xy, y^2, x^3, x^2y, xy^2, y^3)
+171 features = PolynomialFeatures(degree=3, include_bias=False)
+172 X = features.fit_transform(pd.concat([pd.DataFrame(x), pd.DataFrame(y)], axis=1))
+ +174 # Execute the multidimensional linear regression
+175 model = LinearRegression().fit(X, z)
+ +177 output = [model.intercept_, model.coef_[0], model.coef_[1], model.coef_[2],
+178 model.coef_[3], model.coef_[4],
+179 model.coef_[5], model.coef_[6], model.coef_[7], model.coef_[8]]
+180 # output = P1-P10
+181 return output
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from .states import ThermodynamicState, TransportProperties
+2from .media import get_two_phase_limits, MedProp
+3from .cool_prop import CoolProp
+4from .ref_prop import RefProp
+ + +7__all__ = ['ThermodynamicState',
+8 'TransportProperties',
+9 'MedProp',
+10 'CoolProp',
+11 'RefProp']
+ +13USED_MED_PROP = (CoolProp, {})
+ + +16def set_global_media_properties(med_prop_class: object, **kwargs):
+17 """
+18 Set the globally used MedProp class.
+ +20 Args:
+21 med_prop_class (object):
+22 Available MedProp children class.
+23 kwargs (dict):
+24 Additional settings for the MedProp class,
+25 e.g. {"use_high_level_api": True} for CoolProp.
+26 """
+27 global USED_MED_PROP
+28 USED_MED_PROP = (med_prop_class, kwargs)
+ + +31def get_global_med_prop_and_kwargs():
+32 """
+33 Get the global MedProp class used
+34 for all calculations.
+35 Returns:
+36 MedProp: The class
+37 """
+38 global USED_MED_PROP
+39 return USED_MED_PROP[0], USED_MED_PROP[1]
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module with cool prop wrapper.
+3"""
+4import logging
+ +6import CoolProp.CoolProp as CoolPropInternal
+ +8from vclibpy.media.media import MedProp
+9from vclibpy.media.states import ThermodynamicState, TransportProperties
+ +11logger = logging.getLogger(__name__)
+ + +14class CoolProp(MedProp):
+15 """
+16 Class using the open-source CoolProp package
+17 to access the properties.
+ +19 Args:
+20 use_high_level_api (bool):
+21 True to use the high-level api, which is much slower,
+22 but you can use all modes in calc_state.
+23 Default is False.
+24 """
+ +26 _mode_map = {
+27 "PT": (CoolPropInternal.PT_INPUTS, True),
+28 "TQ": (CoolPropInternal.QT_INPUTS, False),
+29 "PS": (CoolPropInternal.PSmass_INPUTS, True),
+30 "PH": (CoolPropInternal.HmassP_INPUTS, False),
+31 "PQ": (CoolPropInternal.PQ_INPUTS, True)
+32 }
+ +34 def __init__(self, fluid_name, use_high_level_api: bool = False):
+35 super().__init__(fluid_name=fluid_name)
+36 # Set molar mass and trigger a possible fluid-name error
+37 # if the fluid is not supported.
+38 self._helmholtz_equation_of_state = CoolPropInternal.AbstractState("HEOS", self.fluid_name)
+39 self.M = self._helmholtz_equation_of_state.molar_mass()
+40 self.use_high_level_api = use_high_level_api
+ +42 def calc_state(self, mode: str, var1: float, var2: float):
+43 super().calc_state(mode=mode, var1=var1, var2=var2)
+ +45 if self.use_high_level_api:
+46 _var1_str, _var2_str = mode[0], mode[1]
+47 # CoolProp returns Pa
+48 p = CoolPropInternal.PropsSI('P', _var1_str, var1, _var2_str, var2, self.fluid_name)
+49 # CoolProp returns kg/m^3
+50 d = CoolPropInternal.PropsSI('D', _var1_str, var1, _var2_str, var2, self.fluid_name)
+51 # CoolProp returns K
+52 T = CoolPropInternal.PropsSI('T', _var1_str, var1, _var2_str, var2, self.fluid_name)
+53 # CoolProp returns J/kg
+54 u = CoolPropInternal.PropsSI('U', _var1_str, var1, _var2_str, var2, self.fluid_name)
+55 # CoolProp returns J/kg
+56 h = CoolPropInternal.PropsSI('H', _var1_str, var1, _var2_str, var2, self.fluid_name)
+57 # CoolProp returns J/kg/K
+58 s = CoolPropInternal.PropsSI('S', _var1_str, var1, _var2_str, var2, self.fluid_name)
+59 # CoolProp returns mol/mol
+60 q = CoolPropInternal.PropsSI('Q', _var1_str, var1, _var2_str, var2, self.fluid_name)
+61 # Return new state
+62 return ThermodynamicState(p=p, T=T, u=u, h=h, s=s, d=d, q=q)
+ +64 self._update_coolprop_heos(mode=mode, var1=var1, var2=var2)
+65 # Return new state
+66 return ThermodynamicState(
+67 p=self._helmholtz_equation_of_state.p(),
+68 T=self._helmholtz_equation_of_state.T(),
+69 u=self._helmholtz_equation_of_state.umass(),
+70 h=self._helmholtz_equation_of_state.hmass(),
+71 s=self._helmholtz_equation_of_state.smass(),
+72 d=self._helmholtz_equation_of_state.rhomass(),
+73 q=self._helmholtz_equation_of_state.Q()
+74 )
+ +76 def calc_transport_properties(self, state: ThermodynamicState):
+77 if 0 <= state.q <= 1:
+78 mode = "PQ"
+79 var1, var2 = state.p, state.q
+80 else:
+81 # Get using p and T
+82 mode = "PT"
+83 var1, var2 = state.p, state.T
+ +85 if self.use_high_level_api:
+86 args = [mode[0], var1, mode[1], var2, self.fluid_name]
+87 # CoolProp returns -
+88 pr = CoolPropInternal.PropsSI('PRANDTL', *args)
+89 # CoolProp returns J/kg/K
+90 cp = CoolPropInternal.PropsSI('C', *args)
+91 # CoolProp returns J/kg/K
+92 cv = CoolPropInternal.PropsSI('CVMASS', *args)
+93 # CoolProp returns W/m/K
+94 lam = CoolPropInternal.PropsSI('CONDUCTIVITY', *args)
+95 # CoolProp returns Pa*s
+96 dyn_vis = CoolPropInternal.PropsSI('VISCOSITY', *args)
+97 # Internal calculation as kinematic vis is ration of dyn_vis to density
+98 # In m^2/s
+99 kin_vis = dyn_vis / state.d
+ +101 # Create transport properties instance
+102 return TransportProperties(lam=lam, dyn_vis=dyn_vis, kin_vis=kin_vis,
+103 pr=pr, cp=cp, cv=cv, state=state)
+104 # Low-level API
+105 self._update_coolprop_heos(mode=mode, var1=var1, var2=var2)
+106 # Create transport properties instance
+107 return TransportProperties(
+108 lam=self._helmholtz_equation_of_state.conductivity(),
+109 dyn_vis=self._helmholtz_equation_of_state.viscosity(),
+110 kin_vis=self._helmholtz_equation_of_state.viscosity() / state.d,
+111 pr=self._helmholtz_equation_of_state.Prandtl(),
+112 cp=self._helmholtz_equation_of_state.cpmass(),
+113 cv=self._helmholtz_equation_of_state.cvmass(),
+114 state=state
+115 )
+ +117 def _update_coolprop_heos(self, mode, var1, var2):
+118 if mode not in self._mode_map:
+119 raise KeyError(
+120 f"Given mode '{mode}' is currently not supported with the "
+121 f"faster low-level API to cool-prop. "
+122 f"Either use the high-level API or raise an issue. "
+123 f"Supported modes: {', '.join(self._mode_map.keys())}"
+124 )
+125 i_input, not_reverse_variables = self._mode_map[mode]
+126 if not_reverse_variables:
+127 self._helmholtz_equation_of_state.update(i_input, var1, var2)
+128 else:
+129 self._helmholtz_equation_of_state.update(i_input, var2, var1)
+ +131 def get_critical_point(self):
+132 Tc = CoolPropInternal.PropsSI("TCRIT", self.fluid_name)
+133 pc = CoolPropInternal.PropsSI("PCRIT", self.fluid_name)
+134 dc = CoolPropInternal.PropsSI("RHOCRIT", self.fluid_name)
+135 return Tc, pc, dc
+ +137 def get_molar_mass(self):
+138 return self.M
+ + +141if __name__ == '__main__':
+142 CoolProp("Propan")
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""Module with wrappers to access and handle media property databases.
+ +3This module provides interfaces to load media properties using various wrappers and
+4handle calculations related to media properties.
+ +6Classes:
+7 MedProp: Base class for all media property interfaces.
+ +9Functions:
+10 get_two_phase_limits: Return the states of the boundaries of the two-phase section for a given fluid.
+ +12"""
+13import abc
+14import logging
+15import warnings
+16from typing import List
+17import numpy as np
+ +19from vclibpy.media import ThermodynamicState, TransportProperties
+ + +22logger = logging.getLogger(__name__)
+ + +25class MedProp(abc.ABC):
+26 """Base class for all media property interfaces.
+ +28 This class serves as the base for defining interfaces to access and compute media properties.
+ +30 Methods:
+31 calc_state: Calculate the thermodynamic state based on mode and state variables.
+32 calc_transport_properties: Calculate transport properties for a given state.
+33 get_critical_point: Retrieve critical point information.
+34 get_molar_mass: Retrieve molar mass information.
+35 get_two_phase_limits: Retrieve the two-phase limits for plotting.
+36 calc_mean_transport_properties: Calculate the average transport properties for given states.
+37 """
+38 _fluid_mapper = {}
+ +40 def __init__(self, fluid_name):
+41 """Initialize the MedProp class instance.
+ +43 Args:
+44 fluid_name (str): The name of the fluid.
+45 """
+46 # Check if better internal names exist (e.g. air is modelled as air.ppf)
+47 self.fluid_name = self._fluid_mapper.get(fluid_name, fluid_name)
+48 self._two_phase_limits: dict = None
+ +50 def calc_state(self, mode: str, var1: float, var2: float):
+51 """Calculate the thermodynamic state based on the specified mode and state variables.
+ +53 This function calculates the thermodynamic state based on the chosen mode and provided state variables.
+54 The input state variables need to be in SI units.
+ +56 Notes:
+57 - PT does not work when the state might fall within the two-phase region.
+58 - Only functions for density are implemented. In cases where you know the specific volume, use the density
+59 functions with the inverse value.
+60 - Quality (q) may have values outside the 'physical' scope:
+61 - q = -998: Subcooled liquid
+62 - q = 998: Superheated vapor
+63 - q = 999: Supercritical state
+ +65 Possible modes include:
+66 - "PD": Pressure, Density
+67 - "PH": Pressure, Enthalpy
+68 - "PQ": Pressure, Quality
+69 - "PS": Pressure, Entropy
+70 - "PT": Pressure, Temperature
+71 - "PU": Pressure, Internal Energy
+72 - "TD": Temperature, Density
+73 - "TH": Temperature, Enthalpy
+74 - "TQ": Temperature, Quality
+75 - "TS": Temperature, Entropy
+76 - "TU": Temperature, Internal Energy
+77 - "DH": Density, Enthalpy
+78 - "DS": Density, Entropy
+79 - "DU": Density, Internal Energy
+ +81 Args:
+82 mode (str): Defines the given input state variables (see possible modes above).
+83 var1 (float): Value of the first state variable (as specified in the mode) in SI units.
+84 var2 (float): Value of the second state variable (as specified in the mode) in SI units.
+ +86 Returns:
+87 ThermodynamicState: A ThermodynamicState instance with state variables.
+ +89 Raises:
+90 AssertionError: If the given mode is not within the available options.
+91 """
+92 available_options = ['PD', 'PH', 'PQ', 'PS', 'PT',
+93 'PU', 'TD', 'TH', 'TQ', 'TS',
+94 'TU', 'DH', 'DS', 'DU', ]
+95 assert mode in available_options, f'Given mode {mode} is not in available options'
+ +97 def terminate(self):
+98 """
+99 Terminate the class.
+100 Default behaviour does nothing.
+101 """
+102 pass
+ +104 @abc.abstractmethod
+105 def calc_transport_properties(self, state: ThermodynamicState):
+106 """Calculate the transport properties for the given state.
+ +108 Args:
+109 state (ThermodynamicState): The current thermodynamic state.
+ +111 Returns:
+112 TransportProperties: An instance of TransportProperties.
+113 """
+114 pass
+ +116 @abc.abstractmethod
+117 def get_critical_point(self):
+118 """Retrieve critical point information for the fluid.
+ +120 Returns:
+121 Tuple[float, float, float]: A tuple containing critical point information
+122 (Temperature Tc [K], Pressure pc [Pa], Density dc [kg/m^3]).
+123 """
+124 pass
+ +126 @abc.abstractmethod
+127 def get_molar_mass(self):
+128 """Retrieve the molar mass of the current fluid.
+ +130 Returns:
+131 float: The molar mass M of the current fluid in kg/mol.
+132 """
+133 pass
+ +135 def get_two_phase_limits(self, quantity: str, p_min: int = 100000, p_step: int = 5000):
+136 """
+137 Retrieve the two-phase limits for plotting a specified quantity.
+ +139 This method returns the two-phase limits for a specified quantity (T, h, s, or p) in an array used for
+140 plotting purposes. It calculates the limits within the pressure range from p_min and quality (q) 0 to the
+141 critical pressure (pc), and then from the critical pressure to the pressure p_min and quality 1.
+ +143 Args:
+144 quantity (str): The specified quantity (T, h, s, or p).
+145 p_min (int, optional): The minimum pressure value to start iteration. Default is 100000 Pa.
+146 p_step (int, optional): The step size for pressure variation. Default is 5000 Pa.
+ +148 Returns:
+149 numpy.ndarray: An array containing the two-phase limits for the specified quantity.
+ +151 Raises:
+152 ValueError: If the given quantity is not supported (T, h, s, or p).
+153 """
+154 if self._two_phase_limits is not None:
+155 # Check existing two-phase limits
+156 p_min_old = self._two_phase_limits['p'][0]
+157 p_step_old = self._two_phase_limits['p'][1] - p_min_old
+158 if not np.isclose(p_min_old, p_min, 0, 10) or not np.isclose(p_step_old, p_step, 0, 10):
+159 warnings.warn(f"Overwriting previously calculated two-phase limits with "
+160 f"p_min={p_min_old} and p_step={p_step_old}. This might take a few seconds.\n"
+161 f"The quantity might not match with the previously calculated quantities.")
+162 self._two_phase_limits = None
+ +164 if self._two_phase_limits is None:
+165 # Calculate new two-phase limits for plotting
+166 _two_phase_limits = get_two_phase_limits(self, p_step=p_step, p_min=p_min)
+167 self._two_phase_limits = {
+168 "T": np.array([state.T for state in _two_phase_limits]),
+169 "h": np.array([state.h for state in _two_phase_limits]),
+170 "s": np.array([state.s for state in _two_phase_limits]),
+171 "p": np.array([state.p for state in _two_phase_limits]),
+172 }
+ +174 if quantity not in self._two_phase_limits:
+175 raise ValueError("The given quantity is not supported. T, h, s, or p are supported.")
+176 return self._two_phase_limits[quantity]
+ +178 def calc_mean_transport_properties(self, state_in, state_out):
+179 """
+180 Calculate the average transport properties for the given states.
+ +182 Args:
+183 state_in (ThermodynamicState): First state
+184 state_out (ThermodynamicState): Second state
+ +186 Returns:
+187 TransportProperties: Average transport properties
+ +189 Notes:
+190 The TransportProperties does not contain a state, as an average
+191 state is not possible to calculate.
+192 """
+193 tr_pr_in = self.calc_transport_properties(state_in)
+194 tr_pr_out = self.calc_transport_properties(state_out)
+ +196 return TransportProperties(
+197 lam=0.5 * (tr_pr_in.lam + tr_pr_out.lam),
+198 dyn_vis=0.5 * (tr_pr_in.dyn_vis + tr_pr_out.dyn_vis),
+199 kin_vis=0.5 * (tr_pr_in.kin_vis + tr_pr_out.kin_vis),
+200 pr=0.5 * (tr_pr_in.Pr + tr_pr_out.Pr),
+201 cp=0.5 * (tr_pr_in.cp + tr_pr_out.cp),
+202 cv=0.5 * (tr_pr_in.cv + tr_pr_out.cv),
+203 state=None)
+ + +206def get_two_phase_limits(med_prop: MedProp, p_step: int = 1000, p_min: int = int(1e3)) -> List[ThermodynamicState]:
+207 """
+208 Return the states representing the boundaries of the two-phase section for the given fluid.
+ +210 This function is primarily used for visualizing the two-phase section and validating the accuracy of calculations.
+ +212 Args:
+213 med_prop (MedProp): An instance of a valid MedProp-Class.
+214 p_step (int): The step size for pressure variation in Pa. Default is 1000 Pa.
+215 p_min (int): The minimum pressure in Pa from where to start calculation. Default is 1000 Pa.
+ +217 Returns:
+218 List[ThermodynamicState]: A list of ThermodynamicState instances representing the two-phase limits.
+ +220 Notes:
+221 The two-phase limits are computed by iterating over a range of pressures from the minimum pressure up to the
+222 critical point pressure (exclusive) with a specified step size. States at quality 0 (saturated liquid)
+223 and quality 1 (saturated vapor) are appended to form the two-phase boundary. The list is reversed to
+224 maintain the correct order for visualization purposes.
+225 """
+226 _, _p_max, _ = med_prop.get_critical_point()
+227 q0, q1 = [], []
+228 for _p in range(p_min, int(_p_max), p_step):
+229 try:
+230 q0.append(med_prop.calc_state("PQ", _p, 0))
+231 q1.append(med_prop.calc_state("PQ", _p, 1))
+232 except ValueError as err:
+233 logger.info("Could not calculate two-phase limits for p=%s: %s",
+234 _p, err)
+235 # Reverse list for correct order
+236 return q0 + q1[::-1]
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1# -*- coding: utf-8 -*-
+2"""
+3Created on 21.04.2020
+ +5@author: Christoph Hoeges, Fabian Wuellhorst, Jona Brach
+ +7To test:
+8- Transport properties are not fully tested (Status: 11.06.2020)
+9- Error raising is not implemented at all refprop calls
+10 - Additional change might be that not all errors and warning are excluded when 'use_error_check' is set to false but
+11 only warnings or errors?
+12"""
+13import logging
+14import os
+15import warnings
+16import shutil
+17import atexit
+ +19from ctREFPROP.ctREFPROP import REFPROPFunctionLibrary
+ +21from vclibpy.media import ThermodynamicState, TransportProperties, MedProp
+ + +24logger = logging.getLogger(__name__)
+ + +27class RefProp(MedProp):
+28 """
+29 Class to connect to refProp package.
+ +31 Args:
+32 :param string fluid_name:
+33 Fluid name for RefProp use
+34 :param list or None z:
+35 Fluid composition. Should only be used, when a self-design mixture shall be used. Further information
+36 see notes.
+37 When you want to use a self-design mixture to as follows:
+38 - Fluid-name needs to contain all component names within mixture: "R32.FLD|R125.FLD"
+39 - z needs to be a list with molar fractions: [0.697, 0.303]
+40 - Used example would be similar to R410A
+ +42 :param string dll_path:
+43 Specifier for the dll path used for RefProp,
+44 e.g. dll_path='C:\\path_to_dll\\RefProp64.dll'.
+45 If None, the `ref_prop_path` and function `get_dll_path` are
+46 used to determine the dll path
+47 :param boolean use_error_check:
+48 Specifier whether errors and warnings shall be checked when calling RefProp or not
+49 :param boolean use_warnings:
+50 Specifier whether warnings shall be used
+51 :param str ref_prop_path:
+52 Path to RefProp package. Default is the ENV variable `RPPREFIX`.
+53 :param bool copy_dll:
+54 If True (not the default), a copy of the dll is created to enable simultaneous use of
+55 multiple fluids in multiprocessing.
+56 :param str copy_dll_directory:
+57 If `copy_dll` is True, the DLL is copied to this directory.
+58 If None (default), the current working directory is used.
+ +60 Note:
+61 - You need to install package ctREFPROP Package
+62 https://github.com/usnistgov/REFPROP-wrappers/tree/master/wrappers/python
+63 - In case you use a self-defined mixture: Entropy reference state might deviate from GUI!!
+ +65 Functionality:
+66 - It does not work to have multiple instances simultaneously. When calculating values the last fluid name
+67 somehow will be used even though instance includes "new" name. Need to be fixed at some point.
+ + +70 How to use RefProp-Wrapper:
+71 ---------------------------
+72 1.) Create RefProp instance: rp = RefProp("R32")
+73 2.) In case you want to calculate fluid properties (state variables) for a specific state: Use calc_state() function
+74 Multiple inputs can be given (but you need to now two in order to define the values). For further
+75 information see function header.
+76 3.) Further get-Functions implemented
+77 - get_gwp(): Global warming potential
+78 - get_odp(): Ozone depletion potential
+79 - get_safety(): Safety class
+80 - get_mass_fraction(): Mass fractions of pure substances in fluid
+81 - get_molar_mass(): Molar mass of fluid
+82 - get_mol_fraction(): Mol fractions of pure substances in fluid
+83 - get_comp_names(): Pure substances names in fluid
+84 - get_longname(): Long name of fluid within RefProp
+85 - get_critical_point(): Crit. Temperature and Pressure
+86 - get_version(): Version of wrapper and of RefProp dll
+ + +89 Version notes:
+90 --------------
+91 0.1.0 (21.04.2020, Christoph Hoeges):
+92 First implementation
+93 - Contains multiple functions to call RefProp instance
+94 - Can calculate state, crit, GWP, ODP, Safety class, molar mass etc. of fluid
+95 - Mixtures might work but it wasn't fully testet - R455A e.g. still deviates slightly
+ +97 0.1.1 (25.04.2020, Christoph Hoeges):
+98 Multiple changes, added functionality. Commands are still the same though
+99 - Self designed mixtures are possible now as well (see instructions in init
+100 - Added composition input to __init__ function
+101 - Added modes in calc_state function and removed bug in PH calculation
+102 - Additional protected functions due to different input
+103 - Adjusted _call_refprop function (z is not mass frac but mol fraction)
+104 - Added documentation / instruction
+ +106 0.1.2 (08.05.2020, Christoph Hoeges):
+107 Multiple adjustments
+108 - Added function to call ABFLSHdll function in refprop (high level function)
+109 - Changed function calls in calc_state function to ABFLSH and adjusted conversion
+110 - Change init-function so user can choose which dll shall be used
+111 - Add function to get version of this current wrapper as well as RefProp-dll version
+ +113 0.1.3 (12.05.2020, Christoph Hoeges):
+114 Multiple changes
+115 - Added function to get all files in MIXTURE and FLUIDS directory to check available fluids
+116 - Added error function in order to return errors when RefProp-Functions are called.
+117 NOTE: Not all instances where refprop is called are checked for errors. Currently, it is only used in
+118 init and calc_state
+ +120 0.1.4 (19.05.2020, Christoph Hoeges):
+121 Multiple changes:
+122 - Debugged fluid properties calculation for predefined mixtures
+123 - Fixed option of self-defined mixtures
+ +125 0.1.5 (22.05.2020, Christoph Hoeges):
+126 Include transport properties calculation into wrapper
+ +128 0.1.6 (10.06.2020, Fabian Wuellhorst):
+129 Add option to use a custom dll path. This necessary to use multiple instances with possible
+130 different fluids at the same time.
+131 """
+132 # General information
+133 #
+134 __version__ = "0.1.6"
+135 __author__ = "Christoph Hoeges"
+ +137 _fluid_mapper = {'air': 'air.ppf'}
+ +139 def __init__(self,
+140 fluid_name,
+141 z=None,
+142 dll_path: str = None,
+143 use_error_check: bool = True,
+144 use_warnings: bool = True,
+145 ref_prop_path: str = None,
+146 copy_dll: bool = True,
+147 copy_dll_directory: str = None
+148 ):
+149 if ref_prop_path is None:
+150 # Get environment variable for path to dll
+151 ref_prop_path = os.environ["RPPREFIX"]
+152 if dll_path is None:
+153 path_to_dll = self.get_dll_path(ref_prop_path)
+154 else:
+155 path_to_dll = dll_path
+ +157 if copy_dll:
+158 if copy_dll_directory is None:
+159 copy_dll_directory = os.getcwd()
+160 try:
+161 self._delete_dll_path = os.path.join(
+162 copy_dll_directory,
+163 f"med_prop_{fluid_name}_REFPRP64.dll"
+164 )
+165 shutil.copyfile(path_to_dll, self._delete_dll_path)
+166 atexit.register(self.terminate)
+167 path_to_dll = self._delete_dll_path
+168 except (PermissionError, FileExistsError) as err:
+169 logger.error("Can't copy file to new path: %s", err)
+170 else:
+171 self._delete_dll_path = None
+172 logger.info("Using dll: %s", path_to_dll)
+ +174 super().__init__(fluid_name=fluid_name)
+ +176 self._flag_check_errors = use_error_check
+177 self._flag_warnings = use_warnings
+178 # Set path to RefProp package
+179 self._ref_prop_path = ref_prop_path
+180 self.rp = REFPROPFunctionLibrary(path_to_dll)
+181 self.rp.SETPATHdll(ref_prop_path)
+182 self.molar_base_si = self.rp.GETENUMdll(0, "MOLAR BASE SI").iEnum
+183 # Set fluid name
+184 self.fluid_name = fluid_name
+185 # Get mass and mol fraction and number of components
+186 self._get_comp_frac(z)
+187 # Get component names
+188 self._comp_names = self._get_comp_names()
+189 # Mixture flag
+190 if self._n_comp > 1:
+191 self._mix_flag = True
+192 else:
+193 self._mix_flag = False
+ +195 # Setup
+196 self._setup_rp()
+197 # Calculate molar mass in kg/mol
+198 # self.M = self._call_refprop_allprop("M").Output[0]
+199 self.M = self._call_refprop(inp_name="", out_name="M").Output[0] # kg/mol
+ +201 self._nbp = None
+ +203 def terminate(self):
+204 if self._delete_dll_path is not None:
+205 self._delete_dll()
+ +207 def _delete_dll(self):
+208 try:
+209 # Taken from here: https://stackoverflow.com/questions/21770419/free-the-opened-ctypes-library-in-python
+210 import _ctypes
+211 import sys
+212 _handle = self.rp.dll._handle
+213 if sys.platform.startswith('win'):
+214 _ctypes.FreeLibrary(_handle)
+215 else:
+216 _ctypes.dlclose(_handle)
+217 os.remove(self._delete_dll_path)
+218 self._delete_dll_path = None
+219 except (FileNotFoundError, PermissionError) as err:
+220 logger.error(
+221 "Could not automatically delete the copied RefProp dll at %s. "
+222 "Delete it yourself! Error message: %s", self._delete_dll_path, err
+223 )
+ +225 def _call_refprop_abflsh(self,
+226 inp_name,
+227 value_a,
+228 value_b,
+229 i_flag=1):
+230 """ Call RefProp via ABFLSHdll method
+231 You can define multiple inputs but only "specific ones" where no input values are needed for
+232 e.g. M, Tcrit, pcrit
+ +234 Parameters:
+235 -----------
+236 :param string inp_name:
+237 Input commands: "PQ"
+238 :param float value_a:
+239 Value of parameter b defined in inp_name. In case of None 0 will be used.
+240 :param float value_b:
+241 Value of parameter b defined in inp_name. In case of None 0 will be used.
+242 :param int i_flag:
+243 Flag
+244 Return:
+245 -------
+246 :return ABFLSHdlloutput tmp:
+247 Returns ABFLSH output
+ +249 """
+250 # TODO
+251 tmp = self.rp.ABFLSHdll(inp_name, value_a, value_b, self._mol_frac, i_flag)
+ +253 return tmp
+ +255 def _call_refprop_allprop(self,
+256 out_name,
+257 T_val=None,
+258 d_val=None,
+259 i_mass=0,
+260 i_flag=1):
+261 """ Call RefProp via ALLPROPSdll-method
+ +263 Parameters:
+264 -----------
+265 :param string out_name:
+266 Variables you want to calculate. Multiple outputs are possible:
+267 - Single: "M"
+268 - Multiple: "M,TC,PC"
+269 :param float T_val:
+270 Temperature in current state in K.
+271 Note: In case you want to get general fluid parameters such as M, Tcrit, .. Stick to default value!
+272 :param float d_val:
+273 Density in current state (unit depending on i_mass flag - either mol/m^3 or kg/m^3)
+274 Note: In case you want to get general fluid parameters such as M, Tcrit, .. Stick to default value!
+275 :param int i_mass:
+276 Specifies which units the inputs are given in.
+277 - 0: Molar based
+278 - 1: Mass based
+279 Note: In current version (10.0.0.72) Ian Bell says in multiple Git-Issues that you should stick to molar
+280 base!
+281 :param int i_flag:
+282 In current version (10.0.0.72) i_flag is used to define whether a string containing the units is written in
+283 'hUnits'.
+284 - 0: Deactivated (increases the calculation speed)
+285 - 1: activated
+286 Return:
+287 -------
+288 :return ALLPROPSdlloutput result:
+289 List with values for parameters
+ +291 """
+292 # Check values of T and d
+293 if T_val is None:
+294 T_val = 0
+295 if d_val is None:
+296 d_val = 0
+297 # Define fraction used depending on i_mass flag
+298 if i_mass == 0:
+299 frac = self._mol_frac
+300 elif i_mass == 1:
+301 frac = self._mass_frac
+302 else:
+303 raise ValueError("Chosen i_mass flag '{}' is not possible in ALLPROPSdll function!".format(i_mass))
+ +305 # Call RefProp
+306 res = self.rp.ALLPROPSdll(out_name, self.molar_base_si, i_mass, i_flag, T_val, d_val, frac)
+ +308 return res
+ +310 def _call_refprop(self,
+311 inp_name,
+312 out_name,
+313 value_a=None,
+314 value_b=None,
+315 i_mass=0,
+316 i_flag=1,
+317 frac=None,
+318 fluid=None):
+319 """ Call general refProp function and calculate values
+ +321 Parameters:
+322 -----------
+323 :param string fluid:
+324 Fluid name - in case default value None is used, stored fluid name will be used for command
+325 :param string inp_name:
+326 Input parameter specification
+327 :param string out_name:
+328 Output string name
+329 :param float value_a:
+330 Value of parameter b defined in inp_name. In case of None 0 will be used.
+331 :param float value_b:
+332 Value of parameter b defined in inp_name. In case of None 0 will be used.
+333 :param integer i_flag:
+334 Defines further settings (see documentation)
+335 :param int i_mass:
+336 Specifies which units the inputs are given in. # TODO: WRONG! iMass determines composition, iUnits determines properties (except q)
+337 - 0: Molar based
+338 - 1: Mass based
+339 Note: In current version (10.0.0.72) Ian Bell says in multiple Git-Issues that you should stick to molar
+340 base!
+341 :param list frac:
+342 List with either mol or mass fraction of pure substances in current fluid
+343 (in case of single pure substance: [1]).
+ +345 Return:
+346 -------
+347 :return REFPROPdlloutput output:
+348 Command of refprop
+349 """
+350 # Check inputs
+351 if value_a is None:
+352 value_a = 0
+353 if value_b is None:
+354 value_b = 0
+ +356 if fluid is None:
+357 if self._predefined:
+358 fluid = self.fluid_name # TODO: in general not necessary, decreases performance
+359 else:
+360 fluid = ""
+361 if frac is None:
+362 if self._predefined:
+363 frac = ""
+364 else:
+365 if i_mass == 0:
+366 frac = self._mol_frac
+367 elif i_mass == 1:
+368 frac = self._mass_frac
+369 else:
+370 raise ValueError("Variable i_mass has invalid input '{}'".format(i_mass))
+ +372 # Call refprop function
+373 tmp = self.rp.REFPROPdll(fluid,
+374 inp_name,
+375 out_name,
+376 self.molar_base_si,
+377 i_mass,
+378 i_flag,
+379 value_a,
+380 value_b,
+381 frac)
+ +383 return tmp
+ +385 def _check_error(self,
+386 err_num,
+387 err_msg,
+388 func_name=""):
+389 """ Check error code and raise error in case it is critical
+ +391 Parameters:
+392 -----------
+393 :param integer err_num:
+394 Error return code
+395 :param string err_msg:
+396 Error message given in RefProp call
+397 :param string func_name:
+398 Name of function error needs to be checked in
+399 """
+400 # All fine in case error number is 0
+401 # Throw warning in case error number different than 0 and smaller than 100 is given
+402 # Throw error in case error number higher than 100 is given
+403 if err_num:
+404 if err_num < 100:
+405 if self._flag_warnings:
+406 warnings.warn("[WARNING] Error number {} was given in function '{}'. No critical error but "
+407 "something went wrong maybe. \n Error message is: '{}'".format(str(err_num),
+408 func_name, err_msg))
+409 else:
+410 if self._flag_check_errors:
+411 raise TypeError("[ERROR] When calling RefProp in function '{}' error number {} was "
+412 "returned. \n Error message is: '{}'".format(func_name, str(err_num), err_msg))
+ +414 def _get_comp_names(self):
+415 """ Get component names. In case current fluid is mixture, only component names will be returned.
+416 In case fluid is a pure substance, substance name is returned.
+ +418 Return:
+419 -------
+420 :return list comp_names:
+421 List with pure substances in current refrigerant
+422 """
+423 comp_names = []
+424 if self._predefined:
+425 # Note: While trying it was possible to get fluid name as well, therefore n_comp+1 is used.
+426 if self._n_comp > 1:
+427 for i in range(1, self._n_comp + 3):
+428 test = self.rp.NAMEdll(i)
+429 tmp = test.hn80.replace(".FLD", "")
+430 if not tmp == "":
+431 comp_names.append(tmp)
+432 else:
+433 for i in range(self._n_comp + 3):
+434 tmp = self.rp.NAMEdll(i).hnam
+435 if not tmp == "":
+436 comp_names.append(tmp)
+ +438 else:
+439 # Self-defined
+440 tmp_str = self.fluid_name.split("|")
+441 for i in tmp_str:
+442 i = i.replace(".FLD", "")
+443 i = i.replace(".MIX", "")
+444 if len(i) < 2:
+445 continue
+446 else:
+447 comp_names.append(i)
+ +449 # Replace Carbon Dioxide for CO2
+450 for i, tmp in enumerate(comp_names):
+451 if "Carbon dio" in tmp:
+452 comp_names[i] = "CO2"
+ +454 return comp_names
+ +456 def _get_comp_frac(self, z):
+457 """ Get mass/mol fraction and number of components of current fluid
+ +459 Parameters:
+460 -----------
+461 :param list z:
+462 Contains predefined molar fractions in case one is given. Otherwise, z will be None
+463 """
+464 # Check if predefined or not
+465 # Predefined
+466 if z is None:
+467 self._predefined = True
+468 # Pure substance
+469 elif len(z) == 1:
+470 self._predefined = True
+471 # Self-designed mixture
+472 else:
+473 self._predefined = False
+ +475 # In case predefined mixture or pure substance is used
+476 if self._predefined:
+477 # Dummy function to get values for z in order to specify number of components
+478 tmp_mol = self.rp.REFPROPdll(self.fluid_name, "PQ", "H", self.molar_base_si, 0, 0, 101325, 0, [1])
+479 tmp_mass = self.rp.REFPROPdll(self.fluid_name, "PQ", "H", self.molar_base_si, 1, 0, 101325, 0, [])
+480 # Check for errors
+481 self._check_error(tmp_mol.ierr, tmp_mol.herr, self._get_comp_frac.__name__)
+482 self._check_error(tmp_mass.ierr, tmp_mass.herr, self._get_comp_frac.__name__)
+483 # Mass and molar fractions of components
+484 self._mol_frac = [zi for zi in tmp_mol.z if zi > 0]
+485 self._mass_frac = [zi for zi in tmp_mass.z if zi > 0]
+486 # Get number of components
+487 self._n_comp = len(self._mol_frac)
+488 # Check, whether error occurred
+489 if self._n_comp < 1:
+490 # It might be possible that z value bugs when calling RefProp. In case of a pure substance this does not
+491 # matter so an additional filter is included
+492 if len(self._mass_frac) == 1:
+493 self._n_comp = 1
+494 self._mol_frac = [1]
+495 else:
+496 raise ValueError("Number of components for current fluid '{}' is less than "
+497 "one!".format(self.fluid_name))
+ +499 # Get mol fraction
+500 # self._mol_frac = self._transform_to_molfraction(self._mass_frac)
+501 else:
+502 # Mol fraction
+503 self._mol_frac = z
+504 self._mass_frac = []
+505 # Get number of components
+506 self._n_comp = len(self._mol_frac)
+ +508 def _setup_rp(self):
+509 """ Setup for RefProp """
+510 # Errors can occur in case REFPROP is initalizated multiple time with same fluid - thus a pre setup is used here
+511 self.rp.SETUPdll(1, "N2", "HMX.BNC", "DEF")
+512 # In case of pure substance
+513 if self._n_comp == 1:
+514 self.rp.SETUPdll(self._n_comp, self.fluid_name, "HMX.BNC", "DEF")
+515 # In case of mixtures
+516 else:
+517 # Check if mixture is predefined
+518 if self._predefined:
+519 # Pre defined mixture - different baseline operating point is used
+520 mode = 2
+521 mixture = "|".join([f+".FLD" for f in self._comp_names])
+522 n_comp = self._n_comp
+523 else:
+524 # Self defined mixture
+525 mode = 1
+526 n_comp = self._n_comp
+527 # Define mixtures name
+528 # TODO: Ending is not necessary to create mixtures....
+529 mixture = "|".join([f+".FLD" for f in self._comp_names])
+ +531 # Setup for mixture
+532 setup = self.rp.SETUPdll(n_comp, mixture, 'HMX.BNC', 'DEF')
+533 setref = self.rp.SETREFdll("DEF", mode, self._mol_frac, 0, 0, 0, 0)
+534 # z = self._mol_frac
+535 # Get mass fraction
+536 self._mass_frac = self._transform_to_massfraction(self._mol_frac)
+ +538 # Check whether mixing rules are available
+539 if setup.ierr == 117:
+540 if self._flag_check_errors:
+541 raise ValueError(
+542 "[MIXING ERROR] Mixing rules for mixture '{}' do not exist!".format(self._comp_names))
+543 else:
+544 print(
+545 "[MIXING ERROR] Mixing rules for mixture '{}' do not exist!".format(self._comp_names))
+546 elif setup.ierr == -117:
+547 if self._flag_warnings:
+548 warnings.warn(
+549 "[MIXING ERROR] Mixing rules for mixture '{}' are estimated!".format(self._comp_names))
+550 else:
+551 print(
+552 "[MIXING WARNING] Mixing rules for mixture '{}' are estimated!".format(self._comp_names))
+ +554 def _transform_to_massfraction(self,
+555 mol_frac):
+556 """ Transforms mol fraction to mass fraction
+ +558 Parameters:
+559 -----------
+560 :param list mol_frac:
+561 List containing floats for mol fraction
+ +563 Return:
+564 -------
+565 :return list mass_frac:
+566 List containing floats for mass fraction
+567 """
+568 tmp = self.rp.XMASSdll(mol_frac)
+569 mass_frac = [yi for yi in tmp.xkg if yi > 0]
+570 return mass_frac
+ +572 def _transform_to_molfraction(self,
+573 mass_frac):
+574 """ Transforms mass fraction to mol fraction
+ +576 Parameters:
+577 -----------
+578 :param list mass_frac:
+579 List containing floats for mass fraction
+ +581 Return:
+582 -------
+583 :return list frac:
+584 List containing floats for mol fraction
+585 """
+586 tmp = self.rp.XMOLEdll(mass_frac)
+587 mol_frac = [xi for xi in tmp.xmol if xi > 0]
+588 return mol_frac
+ +590 def calc_state(self, mode: str, var1: float, var2: float, kr=1):
+591 """ Calculate state. Depending on mode, different function will be chosen. Input state variables need to be in
+592 SI units!
+ +594 Notes:
+595 ------
+596 1.) PT does not work when state might be within the two-phase region!
+597 2.) Only functions for density are implemented. In case you know the specific volume instead use density
+598 functions with inverse value!
+599 3.) Quality can have values outside of 'physical' scope:
+600 q = -998: Subcooled liquid
+601 q = 998: Superheated vapor
+602 q = 999: Supercritical state
+ +604 Possible modes are currently:
+605 - "PD": Pressure, Density
+606 - "PH": Pressure, Enthalpy
+607 - "PQ": Pressure, Quality
+608 - "PS": Pressure, Entropy
+609 - "PT": Pressure, Temperature
+610 - "PU": Pressure, Internal Energy
+611 - "TD": Temperature, Density
+612 - "TH": Temperature, Enthalpy
+613 - "TQ": Temperature, Quality
+614 - "TS": Temperature, Entropy
+615 - "TU": Temperature, Internal Energy
+616 - "DH": Density, Enthalpy
+617 - "DS": Density, Entropy
+618 - "DU": Density, Internal Energy
+619 - "HS": Enthalpy, Entropy
+ +621 Parameters:
+622 -----------
+623 :param string mode:
+624 Defines which input state variables are given (see possible modes above)
+625 :param float var1:
+626 Value of state variable 1 (first one in name) - use SI units!
+627 :param float var2:
+628 Value of state variable 2 (second one in name) - use SI units!
+629 :param int kr:
+630 phase flag (kr=1: lower density, kr=2: higher density)
+631 relevant for "TH", "TS", "TU"
+ +633 Return:
+634 -------
+635 :return ThermodynamicState state:
+636 Thermodynamic state with state variables
+637 """
+638 # Multiplier for pressure since kPa is used in RefProp
+639 p_multi = 1e-3
+640 # Multiplier for energy
+641 e_multi = self.M
+642 # Multiplier for density
+643 d_multi = 1 / self.M / 1000
+ +645 # Init all parameters
+646 p = None
+647 d = None
+648 T = None
+649 u = None
+650 h = None
+651 s = None
+652 q = None
+ +654 # Check modi
+ +656 # Pressure and density
+657 if mode == "PD":
+658 p = var1
+659 d = var2
+660 var1 = var1 * p_multi
+661 var2 = var2 * d_multi
+662 tmp = self.rp.PDFLSHdll(var1, var2, self._mol_frac)
+663 # Pressure and enthalpy
+664 elif mode == "PH":
+665 p = var1
+666 var1 = var1 * p_multi
+667 h = var2
+668 var2 = var2 * e_multi
+669 tmp = self.rp.PHFLSHdll(var1, var2, self._mol_frac)
+670 # Pressure and quality
+671 elif mode == "PQ":
+672 p = var1
+673 var1 = var1 * p_multi
+674 q = var2
+675 # In case current fluid is mixture you need to transform Q to molar base for RefProp-function
+676 if self._mix_flag:
+677 var2 = self._call_refprop("PQMASS", "QMOLE", p, q, i_mass=1).Output[0]
+678 tmp = self.rp.PQFLSHdll(var1, var2, self._mol_frac, 0)
+679 # Pressure and entropy
+680 elif mode == "PS":
+681 p = var1
+682 var1 = var1 * p_multi
+683 s = var2
+684 var2 = var2 * e_multi
+685 tmp = self.rp.PSFLSHdll(var1, var2, self._mol_frac)
+686 # Pressure and Temperature
+687 elif mode == "PT":
+688 p = var1
+689 var1 = var1 * p_multi
+690 T = var2
+691 tmp = self.rp.TPFLSHdll(var2, var1, self._mol_frac)
+692 # Pressure and internal energy
+693 elif mode == "PU":
+694 p = var1
+695 var1 = var1 * p_multi
+696 u = var2
+697 var2 = var2 * e_multi
+698 # mode = "PE"
+699 tmp = self.rp.PEFLSHdll(var1, var2, self._mol_frac)
+700 # Temperature and density
+701 elif mode == "TD":
+702 T = var1
+703 d = var2
+704 var2 = var2 * d_multi
+705 tmp = self.rp.TDFLSHdll(var1, var2, self._mol_frac)
+706 # Temperature and enthalpy
+707 elif mode == "TH":
+708 T = var1
+709 h = var2
+710 var2 = var2 * e_multi
+711 tmp = self.rp.THFLSHdll(T, var2, self._mol_frac, kr)
+712 # Temperature and quality
+713 elif mode == "TQ":
+714 T = var1
+715 q = var2
+716 # In case current fluid is mixture you need to transform Q to molar base for RefProp-function
+717 if self._mix_flag:
+718 var2 = self._call_refprop("TQMASS", "QMOLE", T, q, i_mass=1).Output[0]
+719 tmp = self.rp.TQFLSHdll(T, var2, self._mol_frac, 1)
+720 # Temperature and entropy
+721 elif mode == "TS":
+722 T = var1
+723 s = var2
+724 var2 = var2 * e_multi
+725 tmp = self.rp.TSFLSHdll(T, var2, self._mol_frac, kr)
+726 # Temperature and internal energy
+727 elif mode == "TU":
+728 T = var1
+729 u = var2
+730 var2 = var2 * e_multi
+731 # mode = "TE"
+732 tmp = self.rp.TEFLSHdll(T, var2, self._mol_frac, kr)
+733 # Density and enthalpy
+734 elif mode == "DH":
+735 d = var1
+736 var1 = var1 * d_multi
+737 h = var2
+738 var2 = var2 * e_multi
+739 tmp = self.rp.DHFLSHdll(var1, var2, self._mol_frac)
+740 # Density and entropy
+741 elif mode == "DS":
+742 d = var1
+743 var1 = var1 * d_multi
+744 s = var2
+745 var2 = var2 * e_multi
+746 tmp = self.rp.DSFLSHdll(var1, var2, self._mol_frac)
+747 # Density and inner energy
+748 elif mode == "DU":
+749 d = var1
+750 var1 = var1 * d_multi
+751 u = var2
+752 var2 = var2 * e_multi
+753 # mode = "DE"
+754 tmp = self.rp.DEFLSHdll(var1, var2, self._mol_frac)
+755 elif mode == "HS":
+756 h = var1
+757 var1 = var1 * e_multi
+758 s = var2
+759 var2 = var2 * e_multi
+760 tmp = self.rp.HSFLSHdll(var1, var2, self._mol_frac)
+761 else:
+762 raise ValueError("Chosen mode is not available in refprop calc_state function!")
+ +764 # Check for errors
+765 self._check_error(tmp.ierr, tmp.herr, self.calc_state.__name__)
+ +767 # Get all state variables
+768 if p is None:
+769 p = tmp.P / p_multi
+770 if T is None:
+771 T = tmp.T
+772 if u is None:
+773 u = tmp.e / e_multi
+774 if h is None:
+775 h = tmp.h / e_multi
+776 if s is None:
+777 s = tmp.s / e_multi
+778 if d is None:
+779 d = tmp.D / d_multi
+780 if q is None:
+781 if self._mix_flag:
+782 # Transform current q (molar) to mass based quality
+783 tmp2 = self._call_refprop("PH", "QMASS", p, h * e_multi, i_mass=1)
+784 if tmp2.Output[0] < 0:
+785 q_mass = tmp2.ierr
+786 else:
+787 q_mass = tmp2.Output[0]
+788 q = q_mass
+789 else:
+790 q = tmp.q
+ +792 # # In case not in two phase region reset q to -1
+793 # if q > 1 or q < 0:
+794 # q = -1
+ +796 # Define state
+797 state = ThermodynamicState(p=p, T=T, u=u, h=h, s=s, d=d, q=q)
+798 return state
+ +800 def calc_satliq_state(self, s):
+801 """s in kJ/kgK"""
+802 s = s * self.M * 1000 # kJ/kgK -> J/molK
+803 tmp = self.rp.SATSdll(s=s, z="", kph=1)
+804 self._check_error(tmp.ierr, tmp.herr, self.calc_satliq_state.__name__)
+805 if tmp.k1 != 1:
+806 raise TypeError
+807 p = tmp.P1 * 1000 # kPa -> Pa
+808 d = tmp.D1 * self.M * 1000 # mol/l -> kg/mol
+809 return self.calc_state("PD", p, d)
+ +811 def calc_transport_properties(self, state: ThermodynamicState):
+812 """ Calculate transport properties of RefProp fluid at given state
+ +814 Parameters:
+815 -----------
+816 :param ThermodynamicState state:
+817 Current thermodynamic state
+818 Return:
+819 -------
+820 :return TransportProperties props:
+821 Instance of TransportProperties
+822 """
+823 # Get properties
+824 tmp = self._call_refprop_allprop("PRANDTL,VIS,TCX,KV,CV,CP,BETA,STN,ACF", state.T, state.d / self.M, i_mass=0)
+825 # Create transport properties instance
+826 props = TransportProperties(lam=tmp.Output[2],
+827 dyn_vis=tmp.Output[1],
+828 kin_vis=tmp.Output[3],
+829 pr=tmp.Output[0],
+830 cp=tmp.Output[5] / self.M,
+831 cv=tmp.Output[4] / self.M,
+832 beta=tmp.Output[6],
+833 sur_ten=tmp.Output[7],
+834 ace_fac=tmp.Output[8],
+835 state=state)
+836 # Return props
+837 return props
+ +839 def get_available_substances(self,
+840 mode="all",
+841 include_ending=False,
+842 save_txt=False):
+843 """ Get all available RefProp fluids (mixtures and / or pure substances depending on mode)
+ +845 Parameters:
+846 -----------
+847 :param string mode:
+848 Mode defining which kind of fluids you want to have: 'pure': pure substances, 'mix': mixtures, 'all': all
+849 :param boolean include_ending:
+850 Defines, whether file ending shall be returned as well or not
+851 :param boolean save_txt:
+852 Defines, whether a text file with names shall be created in current working directory
+853 Return:
+854 -------
+855 :return list names:
+856 String list containing names of available fluids (depending on defined mode)
+857 """
+858 # Possible endings
+859 _endings = ["MIX", "FLD", "PPF"]
+ +861 # Folders where fluid data is located
+862 folders = [r"FLUIDS", r"MIXTURES"]
+ +864 # Define paths by mode
+865 if mode == "pure":
+866 paths = [os.path.join(self._ref_prop_path, folders[0])]
+867 elif mode == "mix":
+868 paths = [os.path.join(self._ref_prop_path, folders[1])]
+869 elif mode == "all":
+870 paths = [os.path.join(self._ref_prop_path, folders[0]),
+871 os.path.join(self._ref_prop_path, folders[1])]
+872 else:
+873 raise ValueError("Chosen mode '{}' is not possible!".format(mode))
+ +875 # Get files in folders, remove ending and append to names
+876 names = []
+877 for p in paths:
+878 # Get all files in directory
+879 files = [f for f in os.listdir(p) if os.path.isfile(os.path.join(p, f))]
+880 # Remove ending
+881 if include_ending:
+882 tmp = [path for path in files if path.split(".")[1] in _endings]
+883 else:
+884 tmp = [path.split(".")[0] for path in files if path.split(".")[1] in _endings]
+885 # Append names to names list
+886 names.extend(tmp)
+ +888 # In case names shall be stored
+889 if save_txt:
+890 with open("available_fluids.txt", "w") as output:
+891 for i in names:
+892 output.write("{} \n".format(i))
+ +894 if not names:
+895 raise ValueError("No fluids are in current RefProp directory. Check path '{}'!".format(self._ref_prop_path))
+ +897 # Return names
+898 return names
+ +900 def get_comp_names(self):
+901 """ Get name of components within current fluid"
+ +903 Return:
+904 -------
+905 :return list comp_names:
+906 String names of components within current fluid
+907 """
+908 return self._comp_names
+ +910 def get_nbp(self):
+911 """ Get normal boiling point (T @ q=0 and 1 bar)
+ +913 Return:
+914 -------
+915 :return float nbp:
+916 Normal boiling point of refrigerant in °C
+917 """
+918 if not self._nbp:
+919 self._nbp = self.calc_state("PQ", 1e5, 0.0).T - 273.15
+ +921 return self._nbp
+ +923 def get_molar_composition(self, state: ThermodynamicState, z_molar=None):
+924 """ Get composition on molar base. Liquid phase, vapor phase and total.
+ +926 :param ThermodynamicState state: the state whose compositions will be returned
+927 :param list z_molar: molar composition of fluid. In case of None, default value _mol_frac is used
+928 :return:
+929 - list x:
+930 composition of liquid phase
+931 - list y:
+932 composition of vapor phase
+933 - list z:
+934 composition in total
+935 """
+936 if z_molar is None:
+937 z = self._mol_frac
+938 M = self.M
+939 else:
+940 z = z_molar
+941 M = self.rp.REFPROPdll(hFld="",
+942 hIn="",
+943 hOut="M",
+944 iUnits=self.molar_base_si,
+945 iMass=0,
+946 iFlag=1,
+947 a=0,
+948 b=0,
+949 z=z_molar).Output[0]
+950 num_components = len(z)
+ +952 tmp = self.rp.TDFLSHdll(T=state.T,
+953 D=state.d / M / 1000,
+954 z=z)
+955 # TDFLSHdll is deprecated, use the following in future:
+956 # tmp = self.rp.ABFLSHdll(ab="TD",
+957 # a=state.T,
+958 # b=state.d / self.M / 1000,
+959 # z=self._mol_frac,
+960 # iFlag=0) # molar units
+ +962 x = list(tmp.x[:num_components])
+963 y = list(tmp.y[:num_components])
+ +965 return x, y, z
+ +967 def get_critical_point(self):
+968 """ Get T and p of critical point
+ +970 Return:
+971 -------
+972 :return float Tc:
+973 Temperature at critical point in K
+974 :return float pc:
+975 Pressure at critical point in Pa
+976 :return float dc:
+977 Density at critical point in kg/m^3
+978 """
+979 mode = 2
+980 if mode == 1:
+981 tmp = self._call_refprop("CRIT", "T,P,D", i_mass=1)
+982 Tc = tmp.Output[0]
+983 pc = tmp.Output[1]
+984 dc = tmp.Output[2] * self.M
+985 else:
+986 res = self._call_refprop_allprop("TC,PC,DC")
+987 Tc = res.Output[0]
+988 pc = res.Output[1]
+989 dc = res.Output[2] * self.M
+990 return Tc, pc, dc
+ +992 #
+993 #
+994 def get_def_limits(self):
+995 """ Get limits of current ref prop fluid
+996 Limits contain Tmin, Tmax, Dmax and Pmax (Temperatures, density, pressure)
+ +998 :return dict limits:
+999 Dictionary with definition limits in RefProp. Contains min/max temperature, max density, max pressure.
+1000 """
+1001 tmp = self._call_refprop_allprop("TMIN,TMAX,DMAX,PMAX")
+1002 limits = {"Tmin": tmp.Output[0],
+1003 "Tmax": tmp.Output[1],
+1004 "Dmax": tmp.Output[2] * self.M,
+1005 "Pmax": tmp.Output[3]}
+1006 return limits
+ +1008 @staticmethod
+1009 def get_dll_path(ref_prop_path: str):
+1010 """
+1011 Return the location of the dll
+ +1013 Return:
+1014 :return: string path_to_dll:
+1015 Path of a valid dll
+1016 """
+1017 path_to_dll = os.path.join(ref_prop_path, r"REFPRP64.DLL")
+ +1019 # Check if dll actually exists:
+1020 if not os.path.exists(path_to_dll):
+1021 raise FileNotFoundError("Selected dll not found automatically. "
+1022 "Please alter the local attribute or search for yourself.")
+ +1024 return path_to_dll
+ +1026 def get_fluid_name(self):
+1027 """ Get fluid name.
+ +1029 Return:
+1030 -------
+1031 :return string fluid_name:
+1032 Fluid name
+1033 """
+1034 return self.fluid_name
+ +1036 def get_gwp(self):
+1037 """ Get gwp of current fluid from refProp
+ +1039 Return:
+1040 -------
+1041 :return float gwp:
+1042 Global warming potential of fluid. In case calculation failed, None will be returned.
+1043 """
+1044 # Call refProp
+1045 tmp = self._call_refprop("",
+1046 "GWP",
+1047 i_mass=1)
+1048 # Calculate gwp
+1049 gwp = round(sum(max(tmp.Output[i], 0) * self._mass_frac[i] for i in range(self._n_comp)), 2)
+1050 # In case GWP cannot be calculated
+1051 if gwp < 0:
+1052 gwp = 0
+1053 return gwp
+ +1055 def get_longname(self):
+1056 """ Get longname of fluid
+ +1058 Return:
+1059 -------
+1060 :return string longname:
+1061 Name of current fluid in refprop - provides mass fractions and components as well (in case of mixture)
+1062 """
+1063 longname = self._call_refprop("", "LONGNAME(0)").hUnits
+1064 return longname
+ +1066 def get_mass_fraction(self, use_round=True):
+1067 """ Get mass fractions of pure substances in current fluid
+ +1069 Parameters:
+1070 :param boolean use_round:
+1071 Flag to define, whether the exact values or rounded values (by the fourth number) shall be used
+1072 Return:
+1073 -------
+1074 :return list mass_frac:
+1075 List of component mass fractions
+1076 """
+1077 if use_round:
+1078 mass_frac = [round(i, 4) for i in self._mass_frac]
+1079 else:
+1080 mass_frac = self._mass_frac
+1081 return mass_frac
+ +1083 def get_molar_mass(self):
+1084 """ Get molar mass of current fluid
+ +1086 Return:
+1087 -------
+1088 :return float M:
+1089 Molar mass of current fluid in kg/mol
+1090 """
+1091 return self.M
+ +1093 def get_mol_fraction(self, use_round=True):
+1094 """ Get mol fractions of pure substances in current fluid
+ +1096 Parameters:
+1097 :param boolean use_round:
+1098 Flag to define, whether the exact values or rounded values (by the fourth number) shall be used
+1099 Return:
+1100 -------
+1101 :return list frac:
+1102 List of component mol fractions
+1103 """
+1104 if use_round:
+1105 mol_frac = [round(i, 4) for i in self._mol_frac]
+1106 else:
+1107 mol_frac = self._mol_frac
+1108 return mol_frac
+ +1110 def get_odp(self):
+1111 """ Calculate ozone depletion potential
+1112 In case of mixtures: Maximum value of pure substances will be used
+ +1114 Return:
+1115 -------
+1116 :return float odp:
+1117 ODP of fluid. In case calculation failed, None will be returned.
+1118 """
+1119 # Call refProp
+1120 tmp = self._call_refprop("",
+1121 "ODP",
+1122 i_mass=1)
+1123 # Calculate odp
+1124 odp = max(max(tmp.Output), 0)
+1125 # In case some error occured
+1126 if odp < 0:
+1127 odp = 0
+1128 return odp
+ +1130 def get_safety(self):
+1131 """ Calculate safety class of refrigerant
+ +1133 Return:
+1134 -------
+1135 :return string safety:
+1136 Safety class of fluid.
+1137 """
+1138 # Call refProp
+1139 tmp = self._call_refprop("",
+1140 "SAFETY",
+1141 i_mass=1)
+1142 # Get safety class
+1143 safety = tmp.hUnits
+1144 # Return safety
+1145 return safety
+ +1147 def get_sat_vap_pressure(self, T_sat):
+1148 """ Get pressure of saturated vapor for defined temperature
+ +1150 Note:
+1151 - works for vapor, liquid and solid
+1152 - returns equilibrium pressure at defined line (q=1)
+ +1154 Parameters:
+1155 :param float T_sat:
+1156 Temperature in K
+1157 Return:
+1158 :return float p_sat:
+1159 Vapor pressure in Pa
+1160 """
+1161 trip = self.get_triple_point()
+1162 if trip[0] <= T_sat:
+1163 p_sat = self.calc_state("TQ", T_sat, 1.0).p
+1164 else:
+1165 tmp = self.rp.REFPROPdll("", "TSUBL", "P", self.molar_base_si, 0, 0, T_sat, 0.0, self._mol_frac)
+1166 p_sat = tmp.Output[0]
+1167 return p_sat
+ +1169 def get_triple_point(self):
+1170 """ Get temperature and pressure at triple point of current fluid
+ +1172 Note: Works fine for pure substances, mixtures might not work properly
+ +1174 Return:
+1175 :return float T_tpl:
+1176 Temperature at triple point in K
+1177 :return float p_tpl:
+1178 Pressure at triple point in Pa
+1179 """
+1180 # Call Refprop
+1181 tmp = self._call_refprop("TRIP", "T;P")
+1182 return tmp.Output[0], tmp.Output[1]
+ +1184 def get_version(self):
+1185 """ Get version of wrapper and used RefProp dll
+ +1187 Return:
+1188 :return string wrapper_version:
+1189 Refprop wrapper version
+1190 :return string refprop_version:
+1191 Version of used RefProp dll
+1192 """
+1193 return self.__version__, self.rp.RPVersion()
+ +1195 def is_mixture(self):
+1196 """ Find out if fluid is mixture or not.
+1197 In case current fluid is mixture, true is returned.
+1198 In case current fluid is pure substance, false is returned.
+ +1200 Return:
+1201 -------
+1202 :return boolean _mix_flag:
+1203 Boolean for mixture (True), pure substance (False)
+1204 """
+1205 return self._mix_flag
+ +1207 def set_error_flag(self, flag):
+1208 """ Set error flag
+ +1210 Parameters:
+1211 :param boolean flag:
+1212 New error flag
+1213 Return:
+1214 :return int err:
+1215 Notifier for error code - in case everything went fine, 0 is returned
+1216 """
+1217 self._flag_check_errors = flag
+1218 return 0
+ +1220 def set_warning_flag(self, flag):
+1221 """ Set warning flag
+ +1223 Parameters:
+1224 :param boolean flag:
+1225 New warning flag
+1226 Return:
+1227 :return int err:
+1228 Notifier for error code - in case everything went fine, 0 is returned
+1229 """
+1230 self._flag_warnings = flag
+1231 return 0
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module containing classes for thermodynamic state and transport properties.
+3"""
+4from vclibpy.datamodels import VariableContainer
+ + +7__all__ = [
+8 'ThermodynamicState',
+9 'TransportProperties',
+10]
+ + +13class ThermodynamicState:
+14 """
+15 Represents a thermodynamic state within a cycle.
+ +17 Notes:
+18 Does not necessarily need to have all state variables defined!
+ +20 Args:
+21 p (float): Pressure at the state in Pa.
+22 T (float): Temperature at the state in K.
+23 u (float): Inner energy at the state in J/kg.
+24 h (float): Enthalpy at the state in J/kg.
+25 s (float): Entropy at the state in J/(kg * K).
+26 v (float): Specific volume at the state in m^3/kg.
+27 q (float): Quality at the state (between 0 and 1).
+28 d (float): Density at the state in kg/m^3.
+ +30 Methods:
+31 __init__: Initializes the state class.
+32 __str__: Provides a string representation of the state.
+33 get_pretty_print: Formats the state with names, units, and descriptions.
+34 """
+ +36 def __init__(self,
+37 p=None,
+38 T=None,
+39 u=None,
+40 h=None,
+41 s=None,
+42 v=None,
+43 q=None,
+44 d=None):
+45 """
+46 Initializes a thermodynamic state.
+ +48 Args:
+49 p (float): Pressure at the state in Pa.
+50 T (float): Temperature at the state in K.
+51 u (float): Inner energy at the state in J/kg.
+52 h (float): Enthalpy at the state in J/kg.
+53 s (float): Entropy at the state in J/(kg * K).
+54 v (float): Specific volume at the state in m^3/kg.
+55 q (float): Quality at the state (between 0 and 1).
+56 d (float): Density at the state in kg/m^3.
+ +58 Notes:
+59 If only v or d is provided, the other attribute will be calculated. If both are given and they are similar,
+60 an error will be raised.
+61 """
+62 self.p = p
+63 self.T = T
+64 self.u = u
+65 self.h = h
+66 self.s = s
+67 self.v = v
+68 self.q = q
+69 self.d = d
+70 # Define density
+71 if v and d:
+72 if not round(1/v, 4) == round(d, 4):
+73 raise ValueError("At current state d and v do not match", d, v)
+74 elif v:
+75 self.d = 1/v
+76 elif d:
+77 self.v = 1/d
+ +79 def __str__(self):
+80 """
+81 Returns a string representation of the state.
+82 """
+83 return ";".join([f"{k}={v}" for k, v in self.__dict__.items()])
+ +85 def get_pretty_print(self):
+86 """
+87 Provides a formatted representation of the state with names, units, and descriptions.
+88 """
+89 _container = VariableContainer()
+90 _container.__class__.__name__ = self.__class__.__name__
+91 _container.set(name="p", value=self.p, unit="Pa", description="Pressure")
+92 _container.set(name="T", value=self.T, unit="K", description="Temperature")
+93 _container.set(name="u", value=self.u, unit="J/kg", description="Inner energy")
+94 _container.set(name="h", value=self.h, unit="J/kg", description="Enthalpy")
+95 _container.set(name="s", value=self.s, unit="J/(kg*K)", description="Entropy")
+96 _container.set(name="v", value=self.v, unit="m^3/kg", description="Specific volume")
+97 _container.set(name="q", value=self.q, unit="-", description="Quality")
+98 _container.set(name="d", value=self.d, unit="kg/m^3", description="Density")
+99 return str(_container)
+ + +102class TransportProperties:
+103 """
+104 Represents transport properties at a specific thermodynamic state.
+ +106 Args:
+107 lam (float): Thermal conductivity in W/(m*K).
+108 dyn_vis (float): Dynamic viscosity in Pa*s.
+109 kin_vis (float): Kinematic viscosity in m^2/s.
+110 Pr (float): Prandtl number.
+111 cp (float): Isobaric specific heat capacity in J/(kg*K).
+112 cv (float): Isochoric specific heat capacity in J/(kg*K).
+113 beta (float): Thermal expansion coefficient in 1/K.
+114 sur_ten (float): Surface tension in N/m.
+115 ace_fac (float): Acentric factor.
+116 state (ThermodynamicState): The state the transport properties belong to.
+ +118 Methods:
+119 __init__: Initializes the transport properties class.
+120 __str__: Provides a string representation of the transport properties.
+121 get_pretty_print: Formats the properties with names, units, and descriptions.
+122 """
+ +124 def __init__(self,
+125 lam=None,
+126 dyn_vis=None,
+127 kin_vis=None,
+128 pr=None,
+129 cp=None,
+130 cv=None,
+131 beta=None,
+132 sur_ten=None,
+133 ace_fac=None,
+134 state=None):
+135 """
+136 Initializes transport properties.
+ +138 Args:
+139 lam (float): Thermal conductivity in W/(m*K).
+140 dyn_vis (float): Dynamic viscosity in Pa*s.
+141 kin_vis (float): Kinematic viscosity in m^2/s.
+142 pr (float): Prandtl number.
+143 cp (float): Isobaric specific heat capacity in J/(kg*K).
+144 cv (float): Isochoric specific heat capacity in J/(kg*K).
+145 beta (float): Thermal expansion coefficient in 1/K.
+146 sur_ten (float): Surface tension in N/m.
+147 ace_fac (float): Acentric factor.
+148 state (ThermodynamicState): The state the transport properties belong to.
+149 """
+150 self.lam = lam
+151 self.dyn_vis = dyn_vis
+152 self.kin_vis = kin_vis
+153 self.Pr = pr
+154 self.cp = cp
+155 self.cv = cv
+156 self.beta = beta
+157 self.sur_ten = sur_ten
+158 self.ace_fac = ace_fac
+159 self.state = state
+ +161 def __str__(self):
+162 """
+163 Returns a string representation of the transport properties.
+164 """
+165 return ";".join([f"{k}={v}" for k, v in self.__dict__.items()])
+ +167 def get_pretty_print(self):
+168 """
+169 Provides a formatted representation of the properties with names, units, and descriptions.
+170 """
+171 _container = VariableContainer()
+172 _container.__class__.__name__ = self.__class__.__name__
+173 _container.set(name="lam", value=self.lam, unit="W/(m*K)", description="Thermal conductivity")
+174 _container.set(name="dyn_vis", value=self.dyn_vis, unit="Pa*s", description="Dynamic viscosity")
+175 _container.set(name="kin_vis", value=self.kin_vis, unit="m^2/s", description="Kinematic viscosity")
+176 _container.set(name="pr", value=self.Pr, unit="-", description="Prandtl number")
+177 _container.set(name="cp", value=self.cp, unit="J/(kg*K)", description="Isobaric specific heat capacity")
+178 _container.set(name="cv", value=self.cv, unit="J/(kg*K)", description="Isochoric specific heat capacity")
+179 _container.set(name="beta", value=self.beta, unit="1/K", description="Thermal expansion coefficient")
+180 _container.set(name="sur_ten", value=self.sur_ten, unit="N/m", description="Surface tension")
+181 _container.set(name="ace_fac", value=self.ace_fac, unit="-", description="Acentric factor")
+182 return str(_container)
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from .compressor import Compressor
+2from .rotary import RotaryCompressor
+3from .ten_coefficient import TenCoefficientCompressor, DataSheetCompressor
+4from .constant_effectivness import ConstantEffectivenessCompressor
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module for different compressor models
+3"""
+ +5from vclibpy.components.component import BaseComponent
+6from vclibpy.datamodels import Inputs, FlowsheetState
+ + +9class Compressor(BaseComponent):
+10 """
+11 Base compressor class to be extended for specific compressor models.
+ +13 Args:
+14 N_max (float): Maximal rotations per second of the compressor.
+15 V_h (float): Volume of the compressor in m^3.
+ +17 Methods:
+18 get_lambda_h(inputs: Inputs) -> float:
+19 Get the volumetric efficiency.
+ +21 get_eta_isentropic(p_outlet: float, inputs: Inputs) -> float:
+22 Get the isentropic efficiency.
+ +24 get_eta_mech(inputs: Inputs) -> float:
+25 Get the mechanical efficiency.
+ +27 get_p_outlet() -> float:
+28 Get the outlet pressure.
+ +30 get_n_absolute(n: float) -> float:
+31 Return the absolute compressor frequency based on the relative speed.
+ +33 calc_state_outlet(p_outlet: float, inputs: Inputs, fs_state: FlowsheetState):
+34 Calculate the outlet state based on the high pressure level and provided inputs.
+ +36 calc_m_flow(inputs: Inputs, fs_state: FlowsheetState) -> float:
+37 Calculate the refrigerant mass flow rate.
+ +39 calc_electrical_power(inputs: Inputs, fs_state: FlowsheetState) -> float:
+40 Calculate the electrical power consumed by the compressor based on an adiabatic energy balance.
+41 """
+ +43 def __init__(self, N_max: float, V_h: float):
+44 """
+45 Initialize the compressor.
+ +47 Args:
+48 N_max (float): Maximal rotations per second of the compressor.
+49 V_h (float): Volume of the compressor in m^3.
+50 """
+51 super().__init__()
+52 self.N_max = N_max
+53 self.V_h = V_h
+ +55 def get_lambda_h(self, inputs: Inputs) -> float:
+56 """
+57 Get the volumetric efficiency.
+ +59 Args:
+60 inputs (Inputs): Inputs for the calculation.
+ +62 Returns:
+63 float: Volumetric efficiency.
+64 """
+65 raise NotImplementedError("Re-implement this function to use it")
+ +67 def get_eta_isentropic(self, p_outlet: float, inputs: Inputs) -> float:
+68 """
+69 Get the isentropic efficiency.
+ +71 Args:
+72 p_outlet (float): High pressure value.
+73 inputs (Inputs): Inputs for the calculation.
+ +75 Returns:
+76 float: Isentropic efficiency.
+77 """
+78 raise NotImplementedError("Re-implement this function to use it")
+ +80 def get_eta_mech(self, inputs: Inputs) -> float:
+81 """
+82 Get the mechanical efficiency including motor and inverter efficiencies.
+ +84 Args:
+85 inputs (Inputs): Inputs for the calculation.
+ +87 Returns:
+88 float: Mechanical efficiency including motor and inverter efficiencies.
+89 """
+90 raise NotImplementedError("Re-implement this function to use it")
+ +92 def get_p_outlet(self) -> float:
+93 """
+94 Get the outlet pressure.
+ +96 Returns:
+97 float: Outlet pressure.
+98 """
+99 assert self.state_outlet is not None, "You have to calculate the outlet state first."
+100 return self.state_outlet.p
+ +102 def get_n_absolute(self, n: float) -> float:
+103 """
+104 Return given relative n as absolute rounds/sec based on self.N_max.
+ +106 Args:
+107 n (float): Relative compressor speed between 0 and 1.
+ +109 Returns:
+110 float: Absolute compressor frequency in rounds/sec.
+111 """
+112 return self.N_max * n
+ +114 def calc_state_outlet(self, p_outlet: float, inputs: Inputs, fs_state: FlowsheetState):
+115 """
+116 Calculate the output state based on the high pressure level and the provided inputs.
+117 The state is automatically set as the outlet state of this component.
+ +119 Args:
+120 p_outlet (float): High pressure value.
+121 inputs (Inputs): Inputs for calculation.
+122 fs_state (FlowsheetState): Flowsheet state.
+123 """
+124 state_outlet_isentropic = self.med_prop.calc_state("PS", p_outlet, self.state_inlet.s)
+125 eta_is = self.get_eta_isentropic(p_outlet=p_outlet, inputs=inputs)
+126 h_outlet = (
+127 self.state_inlet.h + (state_outlet_isentropic.h - self.state_inlet.h) /
+128 eta_is
+129 )
+130 fs_state.set(name="eta_is", value=eta_is, unit="%", description="Isentropic efficiency")
+131 self.state_outlet = self.med_prop.calc_state("PH", p_outlet, h_outlet)
+ +133 def calc_m_flow(self, inputs: Inputs, fs_state: FlowsheetState) -> float:
+134 """
+135 Calculate the refrigerant mass flow rate.
+ +137 Args:
+138 inputs (Inputs): Inputs for the calculation.
+139 fs_state (FlowsheetState): Flowsheet state.
+ +141 Returns:
+142 float: Refrigerant mass flow rate.
+143 """
+144 lambda_h = self.get_lambda_h(inputs=inputs)
+145 V_flow_ref = (
+146 lambda_h *
+147 self.V_h *
+148 self.get_n_absolute(inputs.n)
+149 )
+150 self.m_flow = self.state_inlet.d * V_flow_ref
+151 fs_state.set(name="lambda_h", value=lambda_h, unit="%", description="Volumetric efficiency")
+152 fs_state.set(name="V_flow_ref", value=V_flow_ref, unit="m3/s", description="Refrigerant volume flow rate")
+153 fs_state.set(name="m_flow_ref", value=self.m_flow, unit="kg/s", description="Refrigerant mass flow rate")
+154 return self.m_flow
+ +156 def calc_electrical_power(self, inputs: Inputs, fs_state: FlowsheetState) -> float:
+157 """
+158 Calculate the electrical power consumed by the compressor based on an adiabatic energy balance.
+ +160 Args:
+161 inputs (Inputs): Inputs for the calculation.
+162 fs_state (FlowsheetState): Flowsheet state.
+ +164 Returns:
+165 float: Electrical power consumed.
+166 """
+167 # Heat flow in the compressor
+168 P_t = self.m_flow * (self.state_outlet.h - self.state_inlet.h)
+169 # Electrical power consumed
+170 eta_mech = self.get_eta_mech(inputs=inputs)
+171 P_el = P_t / eta_mech
+172 fs_state.set(name="eta_mech", value=eta_mech, unit="-", description="Mechanical efficiency")
+173 return P_el
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from vclibpy.components.compressors.compressor import Compressor
+2from vclibpy.datamodels import Inputs
+ + +5class ConstantEffectivenessCompressor(Compressor):
+6 """
+7 Compressor model with constant efficiencies.
+ +9 Inherits from the Compressor class, which defines the basic properties and behavior of a compressor in a vapor
+10 compression cycle.
+ +12 Parameters:
+13 N_max (float): Maximal rotations per second of the compressor.
+14 V_h (float): Volume of the compressor in m^3.
+15 eta_isentropic (float): Constant isentropic efficiency of the compressor.
+16 eta_mech (float): Constant mechanical efficiency of the compressor.
+17 lambda_h (float): Constant volumetric efficiency.
+ +19 Args:
+20 N_max (float): Maximal rotations per second of the compressor.
+21 V_h (float): Volume of the compressor in m^3.
+22 eta_isentropic (float): Constant isentropic efficiency of the compressor.
+23 eta_inverter (float): Constant inverter efficiency of the compressor.
+24 eta_motor (float): Constant motor efficiency of the compressor.
+25 eta_mech (float): Constant mechanical efficiency of the compressor including motor and inverter efficiencies.
+26 lambda_h (float): Constant volumetric efficiency.
+ +28 Methods:
+29 get_lambda_h(inputs: Inputs) -> float:
+30 Returns the constant volumetric efficiency of the compressor.
+ +32 get_eta_isentropic(p_outlet: float, inputs: Inputs) -> float:
+33 Returns the constant isentropic efficiency of the compressor.
+ +35 get_eta_mech(inputs: Inputs) -> float:
+36 Returns the constant mechanical efficiency including motor and inverter efficiencies.
+ +38 """
+ +40 def __init__(self,
+41 N_max: float, V_h: float,
+42 eta_isentropic: float,
+43 eta_mech: float,
+44 lambda_h: float):
+45 """
+46 Initialize the ConstantEffectivenessCompressor.
+ +48 Args:
+49 N_max (float): Maximal rotations per second of the compressor.
+50 V_h (float): Volume of the compressor in m^3.
+51 eta_isentropic (float): Constant isentropic efficiency of the compressor.
+52 eta_inverter (float): Constant inverter efficiency of the compressor.
+53 eta_motor (float): Constant motor efficiency of the compressor.
+54 eta_mech (float): Constant mechanical efficiency of the compressor.
+55 lambda_h (float): Constant volumetric efficiency.
+56 """
+57 super().__init__(N_max=N_max, V_h=V_h)
+58 self.eta_isentropic = eta_isentropic
+59 self.eta_mech = eta_mech
+60 self.lambda_h = lambda_h
+ +62 def get_lambda_h(self, inputs: Inputs) -> float:
+63 """
+64 Returns the constant volumetric efficiency of the compressor.
+ +66 Args:
+67 inputs (Inputs): Input parameters for the calculation.
+ +69 Returns:
+70 float: Constant volumetric efficiency.
+71 """
+72 return self.lambda_h
+ +74 def get_eta_isentropic(self, p_outlet: float, inputs: Inputs) -> float:
+75 """
+76 Returns the constant isentropic efficiency of the compressor.
+ +78 Args:
+79 p_outlet (float): High pressure value.
+80 inputs (Inputs): Input parameters for the calculation.
+ +82 Returns:
+83 float: Constant isentropic efficiency.
+84 """
+85 return self.eta_isentropic
+ +87 def get_eta_mech(self, inputs: Inputs) -> float:
+88 """
+89 Returns the product of the constant mechanical, motor, and inverter efficiencies
+90 as the effective mechanical efficiency of the compressor.
+ +92 Args:
+93 inputs (Inputs): Input parameters for the calculation.
+ +95 Returns:
+96 float: Effective mechanical efficiency.
+97 """
+98 return self.eta_mech
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from vclibpy.components.compressors.compressor import Compressor
+2from vclibpy.datamodels import Inputs
+ + +5class RotaryCompressor(Compressor):
+6 """
+7 Compressor model based on the thesis of Mirko Engelpracht.
+ +9 This compressor is characterized by using regressions provided by Mirko Engelpracht for a family of rotary
+10 compressors. The coefficients used in the regressions are sourced from his Master's thesis.
+ +12 Parameters:
+13 N_max (float): Maximal rotations per second of the compressor.
+14 V_h (float): Volume of the compressor in m^3.
+ +16 Methods:
+17 get_lambda_h(inputs: Inputs) -> float:
+18 Returns the volumetric efficiency based on the regressions of Mirko Engelpracht.
+ +20 get_eta_isentropic(p_outlet: float, inputs: Inputs) -> float:
+21 Returns the isentropic efficiency based on the regressions of Mirko Engelpracht.
+ +23 get_eta_mech(inputs: Inputs) -> float:
+24 Returns the mechanical efficiency based on the regressions of Mirko Engelpracht.
+ +26 """
+ +28 def get_lambda_h(self, inputs: Inputs) -> float:
+29 """
+30 Returns the volumetric efficiency based on the regressions of Mirko Engelpracht.
+ +32 Args:
+33 inputs (Inputs): Input parameters for the calculation.
+ +35 Returns:
+36 float: Volumetric efficiency.
+37 """
+38 p_outlet = self.get_p_outlet()
+39 # If not constant value is given, eta_is is calculated based on the regression of Mirko Engelpracht
+40 n = self.get_n_absolute(inputs.n)
+41 T_1 = self.state_inlet.T
+ +43 a_1 = 0.80179
+44 a_2 = -0.05210
+45 sigma_pi = 1.63495
+46 pi_ave = 4.54069
+47 a_3 = 3.21616e-4
+48 sigma_T_1 = 8.43797
+49 T_1_ave = 263.86428
+50 a_4 = -0.00494
+51 a_5 = 0.04981
+52 sigma_n = 20.81378
+53 n_ave = 64.41071
+54 a_6 = -0.02190
+ +56 pi = p_outlet / self.state_inlet.p
+57 return (
+58 a_1 +
+59 a_2 * (pi - pi_ave) / sigma_pi +
+60 a_3 * (T_1 - T_1_ave) / sigma_T_1 * (pi - pi_ave) / sigma_pi +
+61 a_4 * (T_1 - T_1_ave) / sigma_T_1 +
+62 a_5 * (n - n_ave) / sigma_n +
+63 a_6 * ((n - n_ave) / sigma_n) ** 2
+64 )
+ +66 def get_eta_isentropic(self, p_outlet: float, inputs: Inputs) -> float:
+67 """
+68 Returns the isentropic efficiency based on the regressions of Mirko Engelpracht.
+ +70 Args:
+71 p_outlet (float): High pressure value.
+72 inputs (Inputs): Input parameters for the calculation.
+ +74 Returns:
+75 float: Isentropic efficiency.
+76 """
+77 # If not constant value is given, eta_is is calculated based on the regression of Mirko Engelpracht
+78 n = self.get_n_absolute(inputs.n)
+ +80 a_1 = 0.5816
+81 a_2 = 0.002604
+82 a_3 = -1.515e-7
+83 a_4 = -0.00473
+84 pi = p_outlet / self.state_inlet.p
+85 eta = (
+86 a_1 +
+87 a_2 * n +
+88 a_3 * n ** 3 +
+89 a_4 * pi ** 2
+90 )
+91 if eta <= 0:
+92 raise ValueError("Efficiency is lower or equal to 0")
+93 return eta
+ +95 def get_eta_mech(self, inputs: Inputs) -> float:
+96 """
+97 Returns the mechanical efficiency based on the regressions of Mirko Engelpracht.
+ +99 Args:
+100 inputs (Inputs): Input parameters for the calculation.
+ +102 Returns:
+103 float: Mechanical efficiency.
+104 """
+105 p_outlet = self.get_p_outlet()
+106 n = self.get_n_absolute(inputs.n)
+107 # If not constant value is given, eta_is is calculated based on the regression of Mirko Engelpracht
+108 a_00 = 0.2199
+109 a_10 = -0.0193
+110 a_01 = 0.02503
+111 a_11 = 8.817e-5
+112 a_20 = -0.001345
+113 a_02 = -0.0003382
+114 a_21 = 1.584e-5
+115 a_12 = -1.083e-6
+116 a_22 = -5.321e-8
+117 a_03 = 1.976e-6
+118 a_13 = 4.053e-9
+119 a_04 = -4.292e-9
+120 pi = p_outlet / self.state_inlet.p
+121 return (
+122 a_00 +
+123 a_10 * pi + a_01 * n + a_11 * pi * n +
+124 a_20 * pi ** 2 + a_02 * n ** 2 + a_21 * pi ** 2 * n + a_12 * pi * n ** 2 + a_22 * pi ** 2 * n ** 2 +
+125 a_03 * n ** 3 + a_13 * pi * n ** 3 +
+126 a_04 * n ** 4
+127 )
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import warnings
+2from abc import ABC
+3import numpy as np
+4import pandas as pd
+ +6from vclibpy.components.compressors.compressor import Compressor
+7from vclibpy.datamodels import Inputs
+ + +10def calc_ten_coefficients(T_eva, T_con, coef_list):
+11 """
+12 Calculate the result using the ten-coefficient method.
+ +14 Args:
+15 T_eva (float): Evaporator temperature in Celsius.
+16 T_con (float): Condenser temperature in Celsius.
+17 coef_list (list): List of coefficients.
+ +19 Returns:
+20 float: Result of the calculation.
+21 """
+22 # Formula for the ten-coefficient method according to the datasheet
+23 z = coef_list[0] + coef_list[1] * T_eva + coef_list[2] * T_con + coef_list[3] * T_eva ** 2 + \
+24 coef_list[4] * T_eva * T_con + coef_list[5] * T_con ** 2 + coef_list[6] * T_eva ** 3 + \
+25 coef_list[7] * T_eva ** 2 * T_con + coef_list[8] * T_con ** 2 * T_eva + coef_list[9] * T_con ** 3
+26 return z
+ + +29class BaseTenCoefficientCompressor(Compressor, ABC):
+30 """
+31 Base class for compressors using the ten-coefficient method.
+ +33 Used table has to be in this format
+34 (order of the columns is not important).
+35 The values must be the same as in the example tabel.
+36 The column names can be different but must
+37 then be matched with argument parameter_names.
+38 (All typed in numbers are fictional placeholders)
+ +40 Capacity(W) Input Power(W) Flow Rate(kg/h) Capacity(W) ... Flow Rate(kg/h)
+41 n n1 n1 n1 n2 ... n_last
+42 P1 42 12 243 32 ... 412
+43 ... ... ... ... ... ... ...
+44 P10 10 23 21 41 ... 2434
+ +46 Args:
+47 N_max (float): Maximal rotations per second of the compressor.
+48 V_h (float): Volume of the compressor in m^3.
+49 datasheet (str): Path of the datasheet file.
+50 **kwargs:
+51 parameter_names (dict, optional):
+52 Dictionary to match internal parameter names (keys) to the names used in the table values.
+53 Default
+54 {
+55 "m_flow": "Flow Rate(kg/h)",
+56 "capacity": "Capacity(W)",
+57 "input_power": "Input Power(W)",
+58 "eta_s": "Isentropic Efficiency(-)",
+59 "lambda_h": "Volumentric Efficiency(-)",
+60 "eta_mech": "Mechanical Efficiency(-)"
+61 }
+62 sheet_name (str, optional): Name of the sheet in the datasheet. Defaults to None.
+63 """
+ +65 def __init__(self, N_max, V_h, datasheet, **kwargs):
+66 """
+67 Initialize the BaseTenCoefficientCompressor.
+ +69 Args:
+70 N_max (float): Maximal rotations per second of the compressor.
+71 V_h (float): Volume of the compressor in m^3.
+72 datasheet (str): Path of the datasheet file.
+73 parameter_names (dict, optional): Dictionary of parameter names. Defaults to None.
+74 sheet_name (str, optional): Name of the sheet in the datasheet. Defaults to None.
+75 """
+ +77 super().__init__(N_max, V_h)
+78 sheet_name = kwargs.get('sheet_name', None)
+79 self.md = pd.read_excel(datasheet, sheet_name=sheet_name)
+80 parameter_names = kwargs.get('parameter_names', None)
+81 if parameter_names is None:
+82 self.parameter_names = {
+83 "m_flow": "Flow Rate(kg/h)",
+84 "capacity": "Capacity(W)",
+85 "input_power": "Input Power(W)",
+86 "eta_s": "Isentropic Efficiency(-)",
+87 "lambda_h": "Volumentric Efficiency(-)",
+88 "eta_mech": "Mechanical Efficiency(-)"
+89 }
+90 else:
+91 self.parameter_names = parameter_names
+ +93 def get_parameter(self, T_eva, T_con, n, type_):
+94 """
+95 Get a parameter based on temperatures, rotations, and parameter type from the datasheet.
+ +97 Args:
+98 T_eva (float): Evaporator temperature in Celsius.
+99 T_con (float): Condenser temperature in Celsius.
+100 n (float): Rotations per second.
+101 type_ (str): Parameter type in parameter_names.
+ +103 Returns:
+104 float: Interpolated parameter value.
+105 """
+106 param_list = []
+107 n_list = []
+ +109 sampling_points = sum(
+110 self.parameter_names[type_] in s for s in list(self.md.columns.values)) # counts number of sampling points
+ +112 for i in range(sampling_points):
+113 if i == 0:
+114 coefficients = self.md[self.parameter_names[type_]].tolist()
+115 else:
+116 coefficients = self.md[str(self.parameter_names[type_] + "." + str(i))].tolist()
+117 n_list.append(coefficients.pop(0))
+118 param_list.append(calc_ten_coefficients(T_eva, T_con, coefficients))
+ +120 return np.interp(self.get_n_absolute(n), n_list, param_list) # linear interpolation
+ + +123class TenCoefficientCompressor(BaseTenCoefficientCompressor):
+124 """
+125 Compressor based on the ten coefficient method.
+ +127 Used table has to be in this format
+128 (order of the columns is not important).
+129 The values must be the same as in the example tabel.
+130 The column names can be different but must
+131 then be matched with the keyword argument parameter_names.
+132 (All typed in numbers are fictional placeholders)
+ +134 Capacity(W) Input Power(W) Flow Rate(kg/h) Capacity(W) ... Flow Rate(kg/h)
+135 n n1 n1 n1 n2 ... n_last
+136 P1 42 12 243 32 ... 412
+137 ... ... ... ... ... ... ...
+138 P10 10 23 21 41 ... 2434
+ +140 T_sh and T_sc have to be set according to the data sheet of your compressor. capacity_definition defines the
+141 parameter "capacity" in the datasheet. If capacity is the specific cooling capacity (h1-h4), set it on "cooling".
+142 If capacity is the specific heating capacity (h2-h3), set it on "heating".
+143 In the case of cooling capacity, the mechanical efficiency of the compressor has to be assumed (assumed_eta_mech)
+144 as h2 can't be calculated otherwise. Summary:
+145 - For the case heating capacity: h2 = h3 + capacity / m_flow
+146 - For the case cooling capacity: h2 = h3 + (capacity + p_el * assumed_eta_mech) / m_flow
+ +148 Args:
+149 N_max (float): Maximal rotations per second of the compressor.
+150 V_h (float): Volume of the compressor in m^3.
+151 T_sc (float): Subcooling according to datasheet in K.
+152 T_sh (float): Superheating according to datasheet in K.
+153 capacity_definition (str): Definition of "capacity" in the datasheet. "cooling" or "heating".
+154 assumed_eta_mech (float): Assumed mechanical efficiency of the compressor (only needed if cooling).
+155 datasheet (str): Path of the modified datasheet.
+156 **kwargs:
+157 parameter_names (dict, optional):
+158 Dictionary to match internal parameter names (keys) to the names used in the table values.
+159 Default
+160 {
+161 "m_flow": "Flow Rate(kg/h)",
+162 "capacity": "Capacity(W)",
+163 "input_power": "Input Power(W)"
+164 }
+165 sheet_name (str, optional): Name of the sheet in the datasheet. Defaults to None.
+166 """
+ +168 def __init__(self, N_max, V_h, T_sc, T_sh, capacity_definition, assumed_eta_mech, datasheet, **kwargs):
+169 super().__init__(N_max=N_max, V_h=V_h, datasheet=datasheet, **kwargs)
+170 self.T_sc = T_sc
+171 self.T_sh = T_sh
+172 if capacity_definition not in ["cooling", "heating"]:
+173 raise ValueError("capacity_definition has to be either 'heating' or 'cooling'")
+174 self._capacity_definition = capacity_definition
+175 self.assumed_eta_mech = assumed_eta_mech
+176 self.datasheet = datasheet
+ +178 def get_lambda_h(self, inputs: Inputs):
+179 """
+180 Get the volumetric efficiency.
+ +182 Args:
+183 inputs (Inputs): Input parameters.
+ +185 Returns:
+186 float: Volumetric efficiency.
+187 """
+188 p_outlet = self.get_p_outlet()
+ +190 n_abs = self.get_n_absolute(inputs.n)
+191 T_eva = self.med_prop.calc_state("PQ", self.state_inlet.p, 1).T - 273.15 # [°C]
+192 T_con = self.med_prop.calc_state("PQ", p_outlet, 0).T - 273.15 # [°C]
+ +194 if round((self.state_inlet.T - T_eva - 273.15), 2) != round(self.T_sh, 2):
+195 warnings.warn("The superheating of the given state is not "
+196 "equal to the superheating of the datasheet. "
+197 "State1.T_sh= " + str(round((self.state_inlet.T - T_eva - 273.15), 2)) +
+198 ". Datasheet.T_sh = " + str(self.T_sh))
+199 # The datasheet has a given superheating temperature which can
+200 # vary from the superheating of the real state 1
+201 # which is given by the user.
+202 # Thus a new self.state_inlet_datasheet has to
+203 # be defined for all further calculations
+204 state_inlet_datasheet = self.med_prop.calc_state("PT", self.state_inlet.p, T_eva + 273.15 + self.T_sh)
+ +206 m_flow = self.get_parameter(T_eva, T_con, inputs.n, "m_flow") / 3600 # [kg/s]
+ +208 lambda_h = m_flow / (n_abs * state_inlet_datasheet.d * self.V_h)
+209 return lambda_h
+ +211 def get_eta_isentropic(self, p_outlet: float, inputs: Inputs):
+212 """
+213 Get the isentropic efficiency.
+ +215 Args:
+216 p_outlet (float): Outlet pressure in Pa.
+217 inputs (Inputs): Input parameters.
+ +219 Returns:
+220 float: Isentropic efficiency.
+221 """
+222 T_con, state_inlet_datasheet, m_flow, capacity, p_el = self._calculate_values(
+223 p_2=p_outlet, inputs=inputs
+224 )
+ +226 h3 = self.med_prop.calc_state("PT", p_outlet, T_con + 273.15 - self.T_sc).h # [J/kg]
+227 h2s = self.med_prop.calc_state("PS", p_outlet, state_inlet_datasheet.s).h # [J/kg]
+ +229 if self._capacity_definition == "heating":
+230 h2 = h3 + capacity / m_flow # [J/kg]
+231 else:
+232 h2 = h3 + (capacity + p_el * self.assumed_eta_mech) / m_flow # [J/kg]
+ +234 if h2s > h2:
+235 raise ValueError("The calculated eta_s is above 1. You probably chose the wrong capacity_definition")
+ +237 eta_s = (h2s - state_inlet_datasheet.h) / (h2 - state_inlet_datasheet.h)
+238 return eta_s
+ +240 def get_eta_mech(self, inputs: Inputs):
+241 """
+242 Get the mechanical efficiency.
+ +244 Args:
+245 inputs (Inputs): Input parameters.
+ +247 Returns:
+248 float: Mechanical efficiency.
+249 """
+250 p_outlet = self.get_p_outlet()
+ +252 if self._capacity_definition == "cooling":
+253 return self.assumed_eta_mech
+254 # Else heating
+255 T_con, state_inlet_datasheet, m_flow, capacity, p_el = self._calculate_values(
+256 p_2=p_outlet, inputs=inputs
+257 )
+ +259 h3 = self.med_prop.calc_state("PT", p_outlet, T_con + 273.15 - self.T_sc).h # [J/kg]
+260 h2 = h3 + capacity / m_flow # [J/kg]
+ +262 eta_mech = p_el / (m_flow * (h2 - state_inlet_datasheet.h))
+263 return eta_mech
+ +265 def _calculate_values(self, p_2: float, inputs: Inputs):
+266 """
+267 Calculate intermediate values for efficiency calculations.
+ +269 Args:
+270 p_2 (float): Outlet pressure in Pa.
+271 inputs (Inputs): Input parameters.
+ +273 Returns:
+274 Tuple[float, State, float, float, float]: Intermediate values.
+275 """
+276 T_eva = self.med_prop.calc_state("PQ", self.state_inlet.p, 1).T - 273.15 # [°C]
+277 T_con = self.med_prop.calc_state("PQ", p_2, 0).T - 273.15 # [°C]
+ +279 state_inlet_datasheet = self.med_prop.calc_state("PT", self.state_inlet.p, T_eva + 273.15 + self.T_sh)
+ +281 m_flow = self.get_parameter(T_eva, T_con, inputs.n, "m_flow") / 3600 # [kg/s]
+282 capacity = self.get_parameter(T_eva, T_con, inputs.n, "capacity") # [W]
+283 p_el = self.get_parameter(T_eva, T_con, inputs.n, "input_power") # [W]
+284 return T_con, state_inlet_datasheet, m_flow, capacity, p_el
+ + +287class DataSheetCompressor(BaseTenCoefficientCompressor):
+288 """
+289 Compressor based on the ten coefficient method.
+ +291 Used table has to be in this format
+292 (order of the columns is not important).
+293 The values must be the same as in the example tabel.
+294 The column names can be different but must
+295 then be matched with the keyword argument parameter_names.
+296 (All typed in numbers are fictional placeholders)
+ +298 Isentropic Volumetric Mechanical Isentropic Mechanical
+299 Efficiency(-) Efficiency(-) Efficiency(-) Efficiency(-) ... Efficiency(-)
+300 n n1 n1 n1 n2 ... n_last
+301 P1 42 12 243 32 ... 412
+302 ... ... ... ... ... ... ...
+303 P10 10 23 21 41 ... 2434
+ +305 Args:
+306 N_max (float): Maximal rotations per second of the compressor.
+307 V_h (float): Volume of the compressor in m^3.
+308 datasheet (str): Path of the datasheet file.
+309 **kwargs:
+310 parameter_names (dict, optional):
+311 Dictionary to match internal parameter names (keys) to the names used in the table values.
+312 Default
+313 {
+314 "eta_s": "Isentropic Efficiency(-)",
+315 "lambda_h": "Volumetric Efficiency(-)",
+316 "eta_mech": "Mechanical Efficiency(-)"
+317 }
+318 sheet_name (str, optional): Name of the sheet in the datasheet. Defaults to None.
+319 """
+ +321 def __init__(self, N_max, V_h, datasheet, **kwargs):
+322 super().__init__(N_max=N_max, V_h=V_h, datasheet=datasheet, **kwargs)
+ +324 def get_lambda_h(self, inputs: Inputs):
+325 """
+326 Get the volumetric efficiency.
+ +328 Args:
+329 inputs (Inputs): Input parameters.
+ +331 Returns:
+332 float: Volumetric efficiency.
+333 """
+334 p_outlet = self.get_p_outlet()
+335 T_eva = self.med_prop.calc_state("PQ", self.state_inlet.p, 0).T
+336 T_con = self.med_prop.calc_state("PQ", p_outlet, 0).T
+337 return self.get_parameter(T_eva, T_con, inputs.n, "lambda_h")
+ +339 def get_eta_isentropic(self, p_outlet: float, inputs: Inputs):
+340 """
+341 Get the isentropic efficiency.
+ +343 Args:
+344 p_outlet (float): Outlet pressure in Pa.
+345 inputs (Inputs): Input parameters.
+ +347 Returns:
+348 float: Isentropic efficiency.
+349 """
+350 T_eva = self.med_prop.calc_state("PQ", self.state_inlet.p, 0).T
+351 T_con = self.med_prop.calc_state("PQ", p_outlet, 0).T
+352 return self.get_parameter(T_eva, T_con, inputs.n, "eta_s")
+ +354 def get_eta_mech(self, inputs: Inputs):
+355 """
+356 Get the mechanical efficiency.
+ +358 Args:
+359 inputs (Inputs): Input parameters.
+ +361 Returns:
+362 float: Mechanical efficiency.
+363 """
+364 p_outlet = self.get_p_outlet()
+365 T_eva = self.med_prop.calc_state("PQ", self.state_inlet.p, 0).T
+366 T_con = self.med_prop.calc_state("PQ", p_outlet, 0).T
+367 return self.get_parameter(T_eva, T_con, inputs.n, "eta_mech")
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from .base import BaseCycle
+2from .standard import StandardCycle
+3from .vapor_injection_economizer import VaporInjectionEconomizer
+4from .vapor_injection_phase_separator import VaporInjectionPhaseSeparator
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import logging
+2from typing import List
+3import numpy as np
+ +5from abc import abstractmethod
+6import matplotlib.pyplot as plt
+7from vclibpy import media, Inputs
+8from vclibpy.datamodels import FlowsheetState
+9from vclibpy.components.heat_exchangers import HeatExchanger
+10from vclibpy.components.component import BaseComponent
+ +12logger = logging.getLogger(__name__)
+ + +15class BaseCycle:
+16 """
+17 Base class for a heat pump. More complex systems may inherit from this class
+18 All HP have a compressor, two HE and a source and sink.
+19 Therefore, the parameters defined here are general parameters.
+ +21 Args:
+22 fluid (str): Name of the fluid
+23 evaporator (HeatExchanger): Instance of a heat exchanger used for the evaporator
+24 condenser (HeatExchanger): Instance of a heat exchanger used for the condenser
+25 """
+ +27 flowsheet_name: str = "BaseCLass of all HP classes - not to use for map generation"
+ +29 def __init__(
+30 self,
+31 fluid: str,
+32 evaporator: HeatExchanger,
+33 condenser: HeatExchanger
+34 ):
+35 self.fluid: str = fluid
+36 self.evaporator = evaporator
+37 self.condenser = condenser
+38 # Instantiate dummy values
+39 self.med_prop = None
+40 self._p_min = 10000 # So that p>0 at all times
+41 self._p_max = None # Is set by med-prop
+ +43 def __str__(self):
+44 return self.flowsheet_name
+ +46 def setup_new_fluid(self, fluid):
+47 # Only do so if new fluid is given
+48 if self.med_prop is not None:
+49 if self.med_prop.fluid_name == fluid:
+50 return
+51 self.med_prop.terminate()
+ +53 # Else create new instance of MedProp
+54 med_prop_class, med_prop_kwargs = media.get_global_med_prop_and_kwargs()
+55 self.med_prop = med_prop_class(fluid_name=fluid, **med_prop_kwargs)
+ +57 # Write the instance to the components
+58 for component in self.get_all_components():
+59 component.med_prop = self.med_prop
+60 component.start_secondary_med_prop()
+ +62 # Get max and min pressure
+63 _, self._p_max, _ = self.med_prop.get_critical_point()
+64 self.fluid = fluid
+ +66 def terminate(self):
+67 self.med_prop.terminate()
+68 for component in self.get_all_components():
+69 component.terminate_secondary_med_prop()
+ +71 def get_all_components(self) -> List[BaseComponent]:
+72 return [self.condenser, self.evaporator]
+ +74 def calc_steady_state(self, inputs: Inputs, fluid: str = None, **kwargs):
+75 """
+76 Calculate the steady-state performance of a vapor compression cycle
+77 based on given inputs and assumptions.
+ +79 This function ensures consistent assumptions across different cycles.
+80 It calculates the performance of the heat pump under
+81 specific conditions while adhering to several general assumptions.
+ +83 General Assumptions:
+84 ---------------------
+85 - Isenthalpic expansion valves:
+86 The enthalpy at the inlet equals the enthalpy at the outlet.
+87 - No heat losses in any component:
+88 The heat input to the condenser equals the heat
+89 output of the evaporator plus the power input.
+90 - Input to the evaporator is always in the two-phase region.
+91 - Output of the evaporator and output of the condenser maintain
+92 a constant overheating or subcooling (can be set in Inputs).
+ +94 Args:
+95 inputs (Inputs):
+96 An instance of the Inputs class containing the
+97 necessary parameters to calculate the flowsheet state.
+98 fluid (str):
+99 The fluid to be used in the calculations.
+100 Required only if 'fluid' is not specified during the object's initialization.
+ +102 Keyword Arguments:
+103 min_iteration_step (int):
+104 The minimum step size for iterations (default: 1).
+105 save_path_plots (str or None):
+106 The path to save plots (default: None).
+107 If None, no plots are created.
+108 show_iteration (bool):
+109 Whether to display iteration progress (default: False).
+110 T_max (float):
+111 Maximum temperature allowed (default: 273.15 + 150).
+112 use_quick_solver (bool):
+113 Whether to use a quick solver (default: True).
+114 max_err_ntu (float):
+115 Maximum allowable error for the heat exchanger in percent (default: 0.5).
+116 max_err_dT_min (float):
+117 Maximum allowable error for minimum temperature difference in K (default: 0.1).
+118 max_num_iterations (int or None):
+119 Maximum number of iterations allowed (default: None).
+ +121 Returns:
+122 fs_state (FlowsheetState):
+123 An instance of the FlowsheetState class representing
+124 the calculated state of the vapor compression cycle.
+125 """
+126 # Settings
+127 min_iteration_step = kwargs.pop("min_iteration_step", 1)
+128 save_path_plots = kwargs.get("save_path_plots", None)
+129 input_name = ";".join([k + "=" + str(np.round(v.value, 3)).replace(".", "_")
+130 for k, v in inputs.get_variables().items()])
+131 show_iteration = kwargs.get("show_iteration", False)
+132 use_quick_solver = kwargs.pop("use_quick_solver", True)
+133 err_ntu = kwargs.pop("max_err_ntu", 0.5)
+134 err_dT_min = kwargs.pop("max_err_dT_min", 0.1)
+135 max_num_iterations = kwargs.pop("max_num_iterations", 1e5)
+136 p_1_history = []
+137 p_2_history = []
+ +139 if use_quick_solver:
+140 step_p1 = kwargs.get("step_max", 10000)
+141 step_p2 = kwargs.get("step_max", 10000)
+142 else:
+143 step_p1 = min_iteration_step
+144 step_p2 = min_iteration_step
+ +146 # Setup fluid:
+147 if fluid is None:
+148 fluid = self.fluid
+149 self.setup_new_fluid(fluid)
+ +151 # First: Iterate with given conditions to get the 4 states and the mass flow rate:
+152 T_1_start = inputs.T_eva_in - inputs.dT_eva_superheating
+153 T_3_start = inputs.T_con_in + inputs.dT_con_subcooling
+154 p_1_start = self.med_prop.calc_state("TQ", T_1_start, 1).p
+155 p_2_start = self.med_prop.calc_state("TQ", T_3_start, 0).p
+156 p_1_next = p_1_start
+157 p_2_next = p_2_start
+ +159 fs_state = FlowsheetState() # Always log what is happening in the whole flowsheet
+160 fs_state.set(name="Q_con", value=1, unit="W", description="Condenser heat flow rate")
+161 fs_state.set(name="COP", value=0, unit="-", description="Coefficient of performance")
+ +163 if show_iteration:
+164 fig_iterations, ax_iterations = plt.subplots(2)
+ +166 num_iterations = 0
+ +168 while True:
+169 if isinstance(max_num_iterations, (int, float)):
+170 if num_iterations > max_num_iterations:
+171 logger.warning("Maximum number of iterations %s exceeded. Stopping.",
+172 max_num_iterations)
+173 return
+ +175 if (num_iterations + 1) % (0.1 * max_num_iterations) == 0:
+176 logger.info("Info: %s percent of max_num_iterations %s used",
+177 100 * (num_iterations + 1) / max_num_iterations, max_num_iterations)
+ +179 p_1 = p_1_next
+180 p_2 = p_2_next
+181 p_1_history.append(p_1)
+182 p_2_history.append(p_2)
+183 if show_iteration:
+184 ax_iterations[0].cla()
+185 ax_iterations[1].cla()
+186 ax_iterations[0].scatter(list(range(len(p_1_history))), p_1_history)
+187 ax_iterations[1].scatter(list(range(len(p_2_history))), p_2_history)
+188 plt.draw()
+189 plt.pause(1e-5)
+ +191 # Increase counter
+192 num_iterations += 1
+193 # Check critical pressures:
+194 if p_2 >= self._p_max:
+195 if step_p2 == min_iteration_step:
+196 logger.error("Pressure too high. Configuration is infeasible.")
+197 return
+198 p_2_next = p_2 - step_p2
+199 step_p2 /= 10
+200 continue
+201 if p_1 <= self._p_min:
+202 if p_1_next == min_iteration_step:
+203 logger.error("Pressure too low. Configuration is infeasible.")
+204 return
+205 p_1_next = p_1 + step_p1
+206 step_p1 /= 10
+207 continue
+ +209 # Calculate the states based on the given flowsheet
+210 try:
+211 self.calc_states(p_1, p_2, inputs=inputs, fs_state=fs_state)
+212 except ValueError as err:
+213 logger.error("An error occurred while calculating states. "
+214 "Can't guess next pressures, thus, exiting: %s", err)
+215 return
+216 if save_path_plots is not None and num_iterations == 1 and show_iteration:
+217 self.plot_cycle(save_path=save_path_plots.joinpath(f"{input_name}_initialization.png"), inputs=inputs)
+ +219 # Check heat exchangers:
+220 error_eva, dT_min_eva = self.evaporator.calc(inputs=inputs, fs_state=fs_state)
+221 if not isinstance(error_eva, float):
+222 print(error_eva)
+223 if error_eva < 0:
+224 p_1_next = p_1 - step_p1
+225 continue
+226 else:
+227 if step_p1 > min_iteration_step:
+228 p_1_next = p_1 + step_p1
+229 step_p1 /= 10
+230 continue
+231 elif error_eva > err_ntu and dT_min_eva > err_dT_min:
+232 step_p1 = 1000
+233 p_1_next = p_1 + step_p1
+234 continue
+ +236 error_con, dT_min_con = self.condenser.calc(inputs=inputs, fs_state=fs_state)
+237 if error_con < 0:
+238 p_2_next = p_2 + step_p2
+239 continue
+240 else:
+241 if step_p2 > min_iteration_step:
+242 p_2_next = p_2 - step_p2
+243 step_p2 /= 10
+244 continue
+245 elif error_con > err_ntu and dT_min_con > err_dT_min:
+246 p_2_next = p_2 - step_p2
+247 step_p2 = 1000
+248 continue
+ +250 # If still here, and the values are equal, we may break.
+251 if p_1 == p_1_next and p_2 == p_2_next:
+252 # Check if solution was too far away. If so, jump back
+253 # And decrease the iteration step by factor 10.
+254 if step_p2 > min_iteration_step:
+255 p_2_next = p_2 - step_p2
+256 step_p2 /= 10
+257 continue
+258 if step_p1 > min_iteration_step:
+259 p_1_next = p_1 + step_p1
+260 step_p1 /= 10
+261 continue
+262 logger.info("Breaking: Converged")
+263 break
+ +265 # Check if values are not converging at all:
+266 p_1_unique = set(p_1_history[-10:])
+267 p_2_unique = set(p_2_history[-10:])
+268 if len(p_1_unique) == 2 and len(p_2_unique) == 2 \
+269 and step_p1 == min_iteration_step and step_p2 == min_iteration_step:
+270 logger.critical("Breaking: not converging at all")
+271 break
+ +273 if show_iteration:
+274 plt.close(fig_iterations)
+ +276 # Calculate the heat flow rates for the selected states.
+277 Q_con = self.condenser.calc_Q_flow()
+278 Q_con_outer = self.condenser.calc_secondary_Q_flow(Q_con)
+279 Q_eva = self.evaporator.calc_Q_flow()
+280 Q_eva_outer = self.evaporator.calc_secondary_Q_flow(Q_eva)
+281 self.evaporator.calc(inputs=inputs, fs_state=fs_state)
+282 self.condenser.calc(inputs=inputs, fs_state=fs_state)
+283 P_el = self.calc_electrical_power(fs_state=fs_state, inputs=inputs)
+284 T_con_out = inputs.T_con_in + Q_con_outer / self.condenser.m_flow_secondary_cp
+ +286 # COP based on P_el and Q_con:
+287 COP_inner = Q_con / P_el
+288 COP_outer = Q_con_outer / P_el
+289 # Calculate carnot quality as a measure of reliability of model:
+290 COP_carnot = (T_con_out / (T_con_out - inputs.T_eva_in))
+291 carnot_quality = COP_inner / COP_carnot
+292 # Calc return temperature:
+293 fs_state.set(
+294 name="P_el", value=P_el, unit="W",
+295 description="Power consumption"
+296 )
+297 fs_state.set(
+298 name="carnot_quality", value=carnot_quality,
+299 unit="-", description="Carnot Quality"
+300 )
+301 fs_state.set(
+302 name="Q_con", value=Q_con, unit="W",
+303 description="Condenser refrigerant heat flow rate"
+304 )
+305 # COP based on P_el and Q_con:
+306 fs_state.set(
+307 name="Q_con_outer", value=Q_con_outer, unit="W",
+308 description="Secondary medium condenser heat flow rate"
+309 )
+310 fs_state.set(
+311 name="Q_eva_outer", value=Q_eva_outer, unit="W",
+312 description="Secondary medium evaporator heat flow rate"
+313 )
+314 fs_state.set(
+315 name="COP", value=COP_inner,
+316 unit="-", description="Coefficient of Performance"
+317 )
+318 fs_state.set(
+319 name="COP_outer", value=COP_outer,
+320 unit="-", description="Outer COP, including heat losses"
+321 )
+ +323 if save_path_plots is not None:
+324 self.plot_cycle(save_path=save_path_plots.joinpath(f"{input_name}_final_result.png"), inputs=inputs)
+ +326 return fs_state
+ +328 @abstractmethod
+329 def get_states_in_order_for_plotting(self):
+330 """
+331 Function to return all thermodynamic states of cycle
+332 in the correct order for plotting.
+333 Include phase change states to see if your simulation
+334 runs plausible cycles.
+ +336 Returns:
+337 - List with tuples, first entry being the state and second the mass flow rate
+338 """
+339 return []
+ +341 def set_evaporator_outlet_based_on_superheating(self, p_eva: float, inputs: Inputs):
+342 """
+343 Calculate the outlet state of the evaporator based on
+344 the required degree of superheating.
+ +346 Args:
+347 p_eva (float): Evaporation pressure
+348 inputs (Inputs): Inputs with superheating level
+349 """
+350 T_1 = self.med_prop.calc_state("PQ", p_eva, 1).T + inputs.dT_eva_superheating
+351 if inputs.dT_eva_superheating > 0:
+352 self.evaporator.state_outlet = self.med_prop.calc_state("PT", p_eva, T_1)
+353 else:
+354 self.evaporator.state_outlet = self.med_prop.calc_state("PQ", p_eva, 1)
+ +356 def set_condenser_outlet_based_on_subcooling(self, p_con: float, inputs: Inputs):
+357 """
+358 Calculate the outlet state of the evaporator based on
+359 the required degree of superheating.
+ +361 Args:
+362 p_con (float): Condensing pressure
+363 inputs (Inputs): Inputs with superheating level
+364 """
+365 T_3 = self.med_prop.calc_state("PQ", p_con, 0).T - inputs.dT_con_subcooling
+366 if inputs.dT_con_subcooling > 0:
+367 self.condenser.state_outlet = self.med_prop.calc_state("PT", p_con, T_3)
+368 else:
+369 self.condenser.state_outlet = self.med_prop.calc_state("PQ", p_con, 0)
+ +371 def plot_cycle(self, save_path: bool, inputs: Inputs, states: list = None):
+372 """Function to plot the resulting flowsheet of the steady state config."""
+373 if states is None:
+374 states = self.get_states_in_order_for_plotting()
+375 states.append(states[0]) # Plot full cycle
+376 # Unpack state var:
+377 h_T = np.array([state.h for state in states]) / 1000
+378 T = [state.T - 273.15 for state in states]
+379 p = np.array([state.p for state in states])
+380 h_p = h_T
+ +382 fig, ax = plt.subplots(2, 1, sharex=True)
+383 ax[0].set_ylabel("$T$ in °C")
+384 ax[1].set_xlabel("$h$ in kJ/kgK")
+385 # Two phase limits
+386 ax[0].plot(
+387 self.med_prop.get_two_phase_limits("h") / 1000,
+388 self.med_prop.get_two_phase_limits("T") - 273.15, color="black"
+389 )
+ +391 ax[0].plot(h_T, T, color="r", marker="s")
+392 self._plot_secondary_heat_flow_rates(ax=ax[0], inputs=inputs)
+393 ax[1].plot(h_p, np.log(p), marker="s", color="r")
+394 # Two phase limits
+395 ax[1].plot(
+396 self.med_prop.get_two_phase_limits("h") / 1000,
+397 np.log(self.med_prop.get_two_phase_limits("p")),
+398 color="black"
+399 )
+400 plt.plot()
+401 ax[1].set_ylabel("$log(p)$")
+402 ax[1].set_ylim([np.min(np.log(p)) * 0.9, np.max(np.log(p)) * 1.1])
+403 ax[0].set_ylim([np.min(T) - 5, np.max(T) + 5])
+404 ax[1].set_xlim([np.min(h_T) * 0.9, np.max(h_T) * 1.1])
+405 ax[0].set_xlim([np.min(h_T) * 0.9, np.max(h_T) * 1.1])
+406 fig.tight_layout()
+407 fig.savefig(save_path)
+408 plt.close(fig)
+ +410 def _plot_secondary_heat_flow_rates(self, ax, inputs):
+411 Q_con = self.condenser.calc_Q_flow()
+412 Q_eva = self.evaporator.calc_Q_flow()
+ +414 delta_H_con = np.array([
+415 self.condenser.state_outlet.h * self.condenser.m_flow,
+416 self.condenser.state_outlet.h * self.condenser.m_flow + Q_con
+417 ]) / self.condenser.m_flow
+418 delta_H_eva = np.array([
+419 self.evaporator.state_outlet.h * self.evaporator.m_flow,
+420 self.evaporator.state_outlet.h * self.evaporator.m_flow - Q_eva
+421 ]) / self.evaporator.m_flow
+422 self.condenser.m_flow_secondary = inputs.m_flow_con
+423 self.condenser.calc_secondary_cp(T=inputs.T_con_in)
+424 self.evaporator.m_flow_secondary = inputs.m_flow_eva
+425 self.evaporator.calc_secondary_cp(T=inputs.T_eva_in)
+426 ax.plot(delta_H_con / 1000, [
+427 inputs.T_con_in - 273.15,
+428 inputs.T_con_in + Q_con / self.condenser.m_flow_secondary_cp - 273.15
+429 ], color="b")
+430 ax.plot(delta_H_eva / 1000, [
+431 inputs.T_eva_in - 273.15,
+432 inputs.T_eva_in - Q_eva / self.evaporator.m_flow_secondary_cp - 273.15
+433 ], color="b")
+ +435 @abstractmethod
+436 def calc_electrical_power(self, inputs: Inputs, fs_state: FlowsheetState):
+437 """Function to calc the electrical power consumption based on the flowsheet used"""
+438 raise NotImplementedError
+ +440 @abstractmethod
+441 def calc_states(self, p_1, p_2, inputs: Inputs, fs_state: FlowsheetState):
+442 """
+443 Function to calculate the states and mass flow rates of the flowsheet
+444 and set these into each component based on the given pressure levels p_1 and p_2.
+ +446 Args:
+447 p_1 (float):
+448 Lower pressure level. If no pressure losses are assumed,
+449 this equals the evaporation pressure and the compressor inlet pressure.
+450 p_2 (float):
+451 Higher pressure level. If no pressure losses are assumed,
+452 this equals the condensing pressure and the compressor outlet pressure.
+453 inputs (Inputs): Inputs of calculation.
+454 fs_state (FlowsheetState): Flowsheet state to save important variables.
+455 """
+456 raise NotImplementedError
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from vclibpy.flowsheets import BaseCycle
+2from vclibpy.datamodels import FlowsheetState, Inputs
+3from vclibpy.components.compressors import Compressor
+4from vclibpy.components.expansion_valves import ExpansionValve
+ + +7class StandardCycle(BaseCycle):
+8 """
+9 Class for a standard cycle with four components.
+ +11 For the standard cycle, we have 4 possible states:
+ +13 1. Before compressor, after evaporator
+14 2. Before condenser, after compressor
+15 3. Before EV, after condenser
+16 4. Before Evaporator, after EV
+17 """
+ +19 flowsheet_name = "Standard"
+ +21 def __init__(
+22 self,
+23 compressor: Compressor,
+24 expansion_valve: ExpansionValve,
+25 **kwargs
+26 ):
+27 super().__init__(**kwargs)
+28 self.compressor = compressor
+29 self.expansion_valve = expansion_valve
+ +31 def get_all_components(self):
+32 return super().get_all_components() + [
+33 self.compressor,
+34 self.expansion_valve
+35 ]
+ +37 def get_states_in_order_for_plotting(self):
+38 return [
+39 self.evaporator.state_inlet,
+40 self.med_prop.calc_state("PQ", self.evaporator.state_inlet.p, 1),
+41 self.evaporator.state_outlet,
+42 self.compressor.state_inlet,
+43 self.compressor.state_outlet,
+44 self.condenser.state_inlet,
+45 self.med_prop.calc_state("PQ", self.condenser.state_inlet.p, 1),
+46 self.med_prop.calc_state("PQ", self.condenser.state_inlet.p, 0),
+47 self.condenser.state_outlet,
+48 self.expansion_valve.state_inlet,
+49 self.expansion_valve.state_outlet,
+50 ]
+ +52 def calc_states(self, p_1, p_2, inputs: Inputs, fs_state: FlowsheetState):
+53 self.set_condenser_outlet_based_on_subcooling(p_con=p_2, inputs=inputs)
+54 self.expansion_valve.state_inlet = self.condenser.state_outlet
+55 self.expansion_valve.calc_outlet(p_outlet=p_1)
+56 self.evaporator.state_inlet = self.expansion_valve.state_outlet
+57 self.set_evaporator_outlet_based_on_superheating(p_eva=p_1, inputs=inputs)
+58 self.compressor.state_inlet = self.evaporator.state_outlet
+59 self.compressor.calc_state_outlet(p_outlet=p_2, inputs=inputs, fs_state=fs_state)
+60 self.condenser.state_inlet = self.compressor.state_outlet
+ +62 # Mass flow rate:
+63 self.compressor.calc_m_flow(inputs=inputs, fs_state=fs_state)
+64 self.condenser.m_flow = self.compressor.m_flow
+65 self.evaporator.m_flow = self.compressor.m_flow
+66 self.expansion_valve.m_flow = self.compressor.m_flow
+67 fs_state.set(
+68 name="y_EV", value=self.expansion_valve.calc_opening_at_m_flow(m_flow=self.expansion_valve.m_flow),
+69 unit="-", description="Expansion valve opening"
+70 )
+71 fs_state.set(
+72 name="T_1", value=self.evaporator.state_outlet.T,
+73 unit="K", description="Refrigerant temperature at evaporator outlet"
+74 )
+75 fs_state.set(
+76 name="T_2", value=self.compressor.state_outlet.T,
+77 unit="K", description="Compressor outlet temperature"
+78 )
+79 fs_state.set(
+80 name="T_3", value=self.condenser.state_outlet.T, unit="K",
+81 description="Refrigerant temperature at condenser outlet"
+82 )
+83 fs_state.set(
+84 name="T_4", value=self.evaporator.state_inlet.T,
+85 unit="K", description="Refrigerant temperature at evaporator inlet"
+86 )
+87 fs_state.set(name="p_con", value=p_2, unit="Pa", description="Condensation pressure")
+88 fs_state.set(name="p_eva", value=p_1, unit="Pa", description="Evaporation pressure")
+ +90 def calc_electrical_power(self, inputs: Inputs, fs_state: FlowsheetState):
+91 """Based on simple energy balance - Adiabatic"""
+92 return self.compressor.calc_electrical_power(inputs=inputs, fs_state=fs_state)
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import numpy as np
+ +3from vclibpy.flowsheets.vapor_injection import BaseVaporInjection
+4from vclibpy.components.heat_exchangers.economizer import VaporInjectionEconomizerNTU
+ + +7class VaporInjectionEconomizer(BaseVaporInjection):
+8 """
+9 Cycle with vapor injection using an economizer.
+ +11 For this cycle, we have 9 relevant states:
+ +13 - 1: Before compressor, after evaporator
+14 - 2: Before condenser, after compressor
+15 - 3: Before ihx, after condenser
+16 - 4: Before Evaporator, after ihx
+17 - 5_ihx: Before ihx, after condenser and EV
+18 - 6_ihx: Before Mixing with 1_VI, after ihx
+19 - 7_ihx: Before second EV, after ihx
+20 - 1_VI: Before Mixing with 6_ihx, After first stage
+21 - 1_VI_mixed. Before second stage of compressor, after mixing with 6_ihx
+ +23 Additional Assumptions:
+24 -----------------------
+25 - No heat losses in ihx
+26 - No pressure loss in ihx
+27 - No losses for splitting of streams
+28 - Isenthalpic second EV
+ +30 Notes
+31 -----
+32 See parent docstring for info on further assumptions and parameters.
+33 """
+ +35 flowsheet_name = "VaporInjectionEconomizer"
+ +37 def __init__(self, economizer: VaporInjectionEconomizerNTU, **kwargs):
+38 self.economizer = economizer
+39 super().__init__(**kwargs)
+ +41 def get_all_components(self):
+42 return super().get_all_components() + [
+43 self.economizer
+44 ]
+ +46 def calc_injection(self):
+47 """
+48 This calculation assumes that the heat transfer
+49 of the higher temperature liquid is always in the subcooling
+50 region, while the vapor injection is always a two-phase heat
+51 transfer.
+52 In reality, you would need to achieve a certain degree of superheat.
+53 Thus, a moving boundary approach would be more fitting. For simplicity,
+54 we assume no superheat.
+ +56 This function iterates the amount of vapor injected.
+57 The iteration starts with close to no injection and increases
+58 the amount of injection as long as enough hotter sub-cooled liquid is
+59 present to fully vaporize the injected part.
+60 """
+61 self.economizer.state_inlet = self.condenser.state_outlet
+62 self.economizer.state_two_phase_inlet = self.high_pressure_valve.state_outlet
+63 self.economizer.state_two_phase_outlet = self.med_prop.calc_state(
+64 "PQ", self.high_pressure_valve.state_outlet.p, 1
+65 )
+66 m_flow_evaporator = self.evaporator.m_flow
+ +68 dh_ihe_goal = (
+69 self.economizer.state_two_phase_outlet.h -
+70 self.economizer.state_two_phase_inlet.h
+71 )
+ +73 # Get transport properties:
+74 tra_properties_liquid = self.med_prop.calc_transport_properties(
+75 self.economizer.state_inlet
+76 )
+77 alpha_liquid = self.economizer.calc_alpha_liquid(tra_properties_liquid)
+78 tra_properties_two_phase = self.med_prop.calc_mean_transport_properties(
+79 self.economizer.state_two_phase_inlet,
+80 self.economizer.state_two_phase_outlet
+81 )
+82 alpha_two_phase = self.economizer.calc_alpha_liquid(tra_properties_two_phase)
+ +84 # Set cp based on transport properties
+85 dT_secondary = (
+86 self.economizer.state_two_phase_outlet.T -
+87 self.economizer.state_two_phase_inlet.T
+88 )
+89 if dT_secondary == 0:
+90 cp_4 = np.inf
+91 else:
+92 cp_4 = dh_ihe_goal / dT_secondary
+93 self.economizer.set_secondary_cp(cp=cp_4)
+94 self.economizer.set_primary_cp(cp=tra_properties_liquid.cp)
+ +96 # We have to iterate to ensure the correct fraction of mass is
+97 # used to ensure state5 has q=1
+98 _x_vi_step = 0.1
+99 _min_step_x_vi = 0.0001
+ +101 x_vi_next = _min_step_x_vi # Don't start with zero!
+102 while True:
+103 x_vi = x_vi_next
+104 x_eva = 1 - x_vi
+105 m_flow_vapor_injection = x_vi * m_flow_evaporator
+106 Q_flow_goal = dh_ihe_goal * m_flow_vapor_injection
+ +108 self.economizer.m_flow = x_eva * m_flow_evaporator
+109 self.economizer.m_flow_secondary = m_flow_vapor_injection
+ +111 # This dT_max is always valid, as the primary inlet is cooled
+112 # and the secondary inlet (the vapor) is either heated
+113 # or isothermal for pure fluids
+114 Q_flow, k = self.economizer.calc_Q_ntu(
+115 dT_max=(
+116 self.economizer.state_inlet.T -
+117 self.economizer.state_two_phase_inlet.T
+118 ),
+119 alpha_pri=alpha_liquid,
+120 alpha_sec=alpha_two_phase,
+121 A=self.economizer.A
+122 )
+123 if Q_flow > Q_flow_goal:
+124 if _x_vi_step <= _min_step_x_vi:
+125 break
+126 # We can increase x_vi_next further, as more heat can be extracted
+127 x_vi_next = x_vi + _x_vi_step
+128 else:
+129 x_vi_next = x_vi - _x_vi_step * 0.9
+130 _x_vi_step /= 10
+ +132 # Solve Energy Balance
+133 h_7 = self.economizer.state_inlet.h - dh_ihe_goal * self.economizer.m_flow
+134 state_7_ihx = self.med_prop.calc_state("PH", self.economizer.state_inlet.p, h_7)
+135 self.economizer.state_outlet = state_7_ihx
+136 return x_vi, self.economizer.state_two_phase_outlet.h, state_7_ihx
+ +138 def get_states_in_order_for_plotting(self):
+139 return super().get_states_in_order_for_plotting() + [
+140 self.economizer.state_two_phase_inlet,
+141 self.economizer.state_two_phase_outlet,
+142 self.high_pressure_compressor.state_inlet,
+143 # Go back to the condenser outlet
+144 self.economizer.state_two_phase_outlet,
+145 self.economizer.state_two_phase_inlet,
+146 self.high_pressure_valve.state_outlet,
+147 self.high_pressure_valve.state_inlet,
+148 self.condenser.state_outlet,
+149 self.economizer.state_inlet,
+150 self.economizer.state_outlet
+151 ]
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import logging
+ +3from vclibpy.flowsheets.vapor_injection import BaseVaporInjection
+4from vclibpy.components.phase_separator import PhaseSeparator
+ + +7logger = logging.getLogger(__name__)
+ + +10class VaporInjectionPhaseSeparator(BaseVaporInjection):
+11 """
+12 Cycle with vapor injection using an adiabatic ideal phase seperator.
+ +14 For this cycle, we have 9 relevant states:
+ +16 - 1: Before compressor, after evaporator
+17 - 2: Before condenser, after compressor
+18 - 3: Before PS, after condenser
+19 - 4: Before Evaporator, after PS
+20 - 5_vips: Before PS, after first EV
+21 - 6_vips: Before Mixing with 1_VI, after PS
+22 - 7_vips: Before second EV, after PS
+23 - 1_VI: Before Mixing with 6_vips, After first stage
+24 - 1_VI_mixed. Before second stage of compressor, after mixing with 6_vips
+ +26 Additional Assumptions:
+27 -----------------------
+28 - Ideal mixing in compressor of state 5 and state 4
+ +30 Notes
+31 -----
+32 See parent docstring for info on further assumptions and parameters.
+33 """
+ +35 flowsheet_name = "VaporInjectionPhaseSeparator"
+ +37 def __init__(self, **kwargs):
+38 self.phase_separator = PhaseSeparator()
+39 super().__init__(**kwargs)
+ +41 def get_all_components(self):
+42 return super().get_all_components() + [
+43 self.phase_separator
+44 ]
+ +46 def calc_injection(self):
+47 # Phase separator
+48 self.phase_separator.state_inlet = self.high_pressure_valve.state_outlet
+49 x_vapor_injection = self.phase_separator.state_inlet.q
+50 h_vapor_injection = self.phase_separator.state_outlet_vapor.h
+51 return x_vapor_injection, h_vapor_injection, self.phase_separator.state_outlet_liquid
+ +53 def get_states_in_order_for_plotting(self):
+54 return super().get_states_in_order_for_plotting() + [
+55 self.phase_separator.state_inlet,
+56 self.phase_separator.state_outlet_vapor,
+57 self.high_pressure_compressor.state_inlet,
+58 # Go back to separator for clear lines
+59 self.phase_separator.state_outlet_vapor,
+60 self.phase_separator.state_outlet_liquid
+61 ]
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1import abc
+2import logging
+3from copy import deepcopy
+4import numpy as np
+ +6from vclibpy.flowsheets import BaseCycle
+7from vclibpy.datamodels import Inputs, FlowsheetState
+8from vclibpy.components.compressors import Compressor
+9from vclibpy.components.expansion_valves import ExpansionValve
+10from vclibpy.media import ThermodynamicState
+ +12logger = logging.getLogger(__name__)
+ + +15class BaseVaporInjection(BaseCycle, abc.ABC):
+16 """
+17 Partial cycle with vapor injection, using
+18 two separated compressors and expansion valves.
+ +20 Notes
+21 -----
+22 See parent docstring for info on further assumptions and parameters.
+23 """
+ +25 flowsheet_name = "VaporInjectionPhaseSeparator"
+ +27 def __init__(
+28 self,
+29 high_pressure_compressor: Compressor,
+30 low_pressure_compressor: Compressor,
+31 high_pressure_valve: ExpansionValve,
+32 low_pressure_valve: ExpansionValve,
+33 **kwargs):
+34 super().__init__(**kwargs)
+35 self.high_pressure_compressor = high_pressure_compressor
+36 self.low_pressure_compressor = low_pressure_compressor
+37 self.high_pressure_valve = high_pressure_valve
+38 self.low_pressure_valve = low_pressure_valve
+39 # Avoid nasty bugs for setting states
+40 if id(high_pressure_compressor) == id(low_pressure_compressor):
+41 self.high_pressure_compressor = deepcopy(low_pressure_compressor)
+42 if id(low_pressure_valve) == id(high_pressure_valve):
+43 self.high_pressure_valve = deepcopy(low_pressure_valve)
+ +45 def get_all_components(self):
+46 return super().get_all_components() + [
+47 self.high_pressure_compressor,
+48 self.low_pressure_compressor,
+49 self.high_pressure_valve,
+50 self.low_pressure_valve,
+51 ]
+ +53 def calc_states(self, p_1, p_2, inputs: Inputs, fs_state: FlowsheetState):
+54 k_vapor_injection = inputs.get("k_vapor_injection", default=1)
+55 # Default according to Xu, 2019
+ +57 p_vapor_injection = k_vapor_injection * np.sqrt(p_1 * p_2)
+ +59 # Condenser outlet
+60 self.set_condenser_outlet_based_on_subcooling(p_con=p_2, inputs=inputs)
+61 # High pressure EV
+62 self.high_pressure_valve.state_inlet = self.condenser.state_outlet
+63 self.high_pressure_valve.calc_outlet(p_outlet=p_vapor_injection)
+ +65 # Calculate low compressor stage to already have access to the mass flow rates.
+66 self.set_evaporator_outlet_based_on_superheating(p_eva=p_1, inputs=inputs)
+67 self.low_pressure_compressor.state_inlet = self.evaporator.state_outlet
+68 self.low_pressure_compressor.calc_state_outlet(
+69 p_outlet=p_vapor_injection, inputs=inputs, fs_state=fs_state
+70 )
+71 m_flow_low = self.low_pressure_compressor.calc_m_flow(inputs=inputs, fs_state=fs_state)
+72 self.evaporator.m_flow = self.low_pressure_compressor.m_flow
+ +74 # Injection component:
+75 x_vapor_injection, h_vapor_injection, state_low_ev_inlet = self.calc_injection()
+ +77 # Low pressure EV
+78 self.low_pressure_valve.state_inlet = state_low_ev_inlet
+79 self.low_pressure_valve.calc_outlet(p_outlet=p_1)
+80 # Evaporator
+81 self.evaporator.state_inlet = self.low_pressure_valve.state_outlet
+ +83 # Ideal Mixing of state_5 and state_1_VI:
+84 h_1_VI_mixed = (
+85 (1-x_vapor_injection) * self.low_pressure_compressor.state_outlet.h +
+86 x_vapor_injection * h_vapor_injection
+87 )
+88 self.high_pressure_compressor.state_inlet = self.med_prop.calc_state(
+89 "PH", p_vapor_injection, h_1_VI_mixed
+90 )
+91 self.high_pressure_compressor.calc_state_outlet(
+92 p_outlet=p_2, inputs=inputs, fs_state=fs_state
+93 )
+ +95 # Check m_flow of both compressor stages to check if
+96 # there would be an asymmetry of how much refrigerant is transported
+97 m_flow_high = self.high_pressure_compressor.calc_m_flow(
+98 inputs=inputs, fs_state=fs_state
+99 )
+100 m_flow_low_should = m_flow_high * (1-x_vapor_injection)
+101 percent_deviation = (m_flow_low - m_flow_low_should) / m_flow_low_should * 100
+102 logger.debug("Deviation of mass flow rates is %s percent", percent_deviation)
+ +104 # Set states
+105 self.condenser.m_flow = self.high_pressure_compressor.m_flow
+106 self.condenser.state_inlet = self.high_pressure_compressor.state_outlet
+ +108 fs_state.set(
+109 name="T_1", value=self.evaporator.state_outlet.T,
+110 unit="K", description="Refrigerant temperature at evaporator outlet"
+111 )
+112 fs_state.set(
+113 name="T_2", value=self.high_pressure_compressor.state_outlet.T,
+114 unit="K", description="Compressor outlet temperature"
+115 )
+116 fs_state.set(
+117 name="T_3", value=self.condenser.state_outlet.T,
+118 unit="K", description="Refrigerant temperature at condenser outlet"
+119 )
+120 fs_state.set(
+121 name="T_4", value=self.evaporator.state_inlet.T,
+122 unit="K", description="Refrigerant temperature at evaporator inlet"
+123 )
+124 fs_state.set(
+125 name="p_con", value=p_2,
+126 unit="Pa", description="Condensation pressure"
+127 )
+128 fs_state.set(
+129 name="p_eva", value=p_1,
+130 unit="Pa", description="Evaporation pressure"
+131 )
+ +133 def calc_injection(self) -> (float, float, ThermodynamicState):
+134 """
+135 Calculate the injection component, e.g. phase separator
+136 or heat exchanger.
+137 In this function, child classes must set inlets
+138 and calculate outlets of additional components.
+ +140 Returns:
+141 float: Portion of vapor injected (x)
+142 float: Enthalpy of vapor injected
+143 ThermodynamicState: Inlet state of low pressure expansion valve
+144 """
+145 raise NotImplementedError
+ +147 def calc_electrical_power(self, inputs: Inputs, fs_state: FlowsheetState):
+148 P_el_low = self.low_pressure_compressor.calc_electrical_power(
+149 inputs=inputs, fs_state=fs_state
+150 )
+151 P_el_high = self.high_pressure_compressor.calc_electrical_power(
+152 inputs=inputs, fs_state=fs_state
+153 )
+154 fs_state.set(
+155 name="P_el_low",
+156 value=P_el_low,
+157 unit="W",
+158 description="Electrical power consumption of low stage compressor"
+159 )
+160 fs_state.set(
+161 name="P_el_high",
+162 value=P_el_high,
+163 unit="W",
+164 description="Electrical power consumption of high stage compressor"
+165 )
+166 return P_el_low + P_el_high
+ +168 def get_states_in_order_for_plotting(self):
+169 """
+170 List with all relevant states of two-stage cycle
+171 except the intermediate component, e.g. phase separator
+172 or heat exchanger.
+173 """
+174 return [
+175 self.low_pressure_valve.state_inlet,
+176 self.low_pressure_valve.state_outlet,
+177 self.evaporator.state_inlet,
+178 self.med_prop.calc_state("PQ", self.evaporator.state_inlet.p, 1),
+179 self.evaporator.state_outlet,
+180 self.low_pressure_compressor.state_inlet,
+181 self.low_pressure_compressor.state_outlet,
+182 self.high_pressure_compressor.state_inlet,
+183 self.high_pressure_compressor.state_outlet,
+184 self.condenser.state_inlet,
+185 self.med_prop.calc_state("PQ", self.condenser.state_inlet.p, 1),
+186 self.med_prop.calc_state("PQ", self.condenser.state_inlet.p, 0),
+187 self.condenser.state_outlet,
+188 self.high_pressure_valve.state_inlet,
+189 self.high_pressure_valve.state_outlet,
+190 ]
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ ++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1from abc import ABC
+2from vclibpy.media import ThermodynamicState, MedProp
+ + +5class BaseComponent(ABC):
+6 """
+7 Abstract base class for defining interfaces of components in the vapor compression cycle.
+ +9 Methods:
+10 start_secondary_med_prop():
+11 To use multiprocessing, MedProp can't start in the main thread, as the object can't be pickled.
+12 This function starts possible secondary MedProp classes, for instance in heat exchangers.
+13 The default component does not have to override this function.
+ +15 Properties:
+16 state_inlet (ThermodynamicState):
+17 Property for accessing and setting the inlet state of the component.
+18 state_outlet (ThermodynamicState):
+19 Property for accessing and setting the outlet state of the component.
+20 m_flow (float):
+21 Property for accessing and setting the mass flow rate through the component.
+22 med_prop (MedProp):
+23 Property for accessing and setting the property wrapper for the working fluid.
+24 """
+ +26 def __init__(self):
+27 """
+28 Initialize the BaseComponent.
+29 """
+30 self._state_inlet: ThermodynamicState = None
+31 self._state_outlet: ThermodynamicState = None
+32 self._m_flow: float = None
+33 self._med_prop: MedProp = None
+ +35 def start_secondary_med_prop(self):
+36 """
+37 Start secondary MedProp classes for multiprocessing.
+ +39 To use multiprocessing, MedProp can't start in the main thread, as the object can't be pickled.
+40 This function starts possible secondary MedProp classes, for instance in heat exchangers.
+41 The default component does not have to override this function.
+42 """
+43 pass
+ +45 def terminate_secondary_med_prop(self):
+46 """
+47 To use multi-processing, MedProp can't start
+48 in the main thread, as the object can't be pickled.
+ +50 This function terminates possible secondary med-prop
+51 classes, for instance in heat exchangers.
+52 The default component does not have to override
+53 this function.
+54 """
+55 pass
+ +57 @property
+58 def state_inlet(self) -> ThermodynamicState:
+59 """
+60 Get or set the inlet state of the component.
+ +62 Returns:
+63 ThermodynamicState: Inlet state of the component.
+64 """
+65 return self._state_inlet
+ +67 @state_inlet.setter
+68 def state_inlet(self, state_inlet: ThermodynamicState):
+69 """
+70 Set the inlet state of the component.
+ +72 Args:
+73 state_inlet (ThermodynamicState): Inlet state to set.
+74 """
+75 self._state_inlet = state_inlet
+ +77 @property
+78 def state_outlet(self) -> ThermodynamicState:
+79 """
+80 Get or set the outlet state of the component.
+ +82 Returns:
+83 ThermodynamicState: Outlet state of the component.
+84 """
+85 return self._state_outlet
+ +87 @state_outlet.setter
+88 def state_outlet(self, state_outlet: ThermodynamicState):
+89 """
+90 Set the outlet state of the component.
+ +92 Args:
+93 state_outlet (ThermodynamicState): Outlet state to set.
+94 """
+95 self._state_outlet = state_outlet
+ +97 @property
+98 def m_flow(self) -> float:
+99 """
+100 Get or set the mass flow rate through the component.
+ +102 Returns:
+103 float: Mass flow rate through the component.
+104 """
+105 return self._m_flow
+ +107 @m_flow.setter
+108 def m_flow(self, m_flow: float):
+109 """
+110 Set the mass flow rate through the component.
+ +112 Args:
+113 m_flow (float): Mass flow rate to set.
+114 """
+115 self._m_flow = m_flow
+ +117 @property
+118 def med_prop(self) -> MedProp:
+119 """
+120 Get or set the property wrapper for the working fluid.
+ +122 Returns:
+123 MedProp: Property wrapper for the working fluid.
+124 """
+125 return self._med_prop
+ +127 @med_prop.setter
+128 def med_prop(self, med_prop: MedProp):
+129 """
+130 Set the property wrapper for the working fluid.
+ +132 Args:
+133 med_prop (MedProp): Property wrapper to set.
+134 """
+135 self._med_prop = med_prop
++ « prev + ^ index + » next + + coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
+ +1"""
+2Module with a simple phase separator model.
+3"""
+ +5from vclibpy.media import ThermodynamicState
+6from vclibpy.components.component import BaseComponent
+ + +9class PhaseSeparator(BaseComponent):
+10 """
+11 A simple phase separator model.
+12 """
+ +14 def __init__(self):
+15 super().__init__()
+16 self._state_outlet_liquid: ThermodynamicState = None
+17 self._state_outlet_vapor: ThermodynamicState = None
+ +19 @BaseComponent.state_inlet.setter
+20 def state_inlet(self, state_inlet: ThermodynamicState):
+21 """
+22 Set the state of the inlet and calculate the outlet states for liquid and vapor phases.
+ +24 Args:
+25 state_inlet (ThermodynamicState): Inlet state.
+26 """
+27 self._state_inlet = state_inlet
+28 self.state_outlet_vapor = self.med_prop.calc_state("PQ", self.state_inlet.p, 1)
+29 self.state_outlet_liquid = self.med_prop.calc_state("PQ", self.state_inlet.p, 0)
+ +31 @BaseComponent.state_outlet.setter
+32 def state_outlet(self, state: ThermodynamicState):
+33 """
+34 This outlet is disabled for this component.
+ +36 Args:
+37 state (ThermodynamicState): Outlet state.
+38 """
+39 raise NotImplementedError("This outlet is disabled for this component")
+ +41 @property
+42 def state_outlet_vapor(self) -> ThermodynamicState:
+43 """
+44 Getter for the outlet state of the vapor phase.
+ +46 Returns:
+47 ThermodynamicState: Outlet state for the vapor phase.
+48 """
+49 return self._state_outlet_vapor
+ +51 @state_outlet_vapor.setter
+52 def state_outlet_vapor(self, state: ThermodynamicState):
+53 """
+54 Setter for the outlet state of the vapor phase.
+ +56 Args:
+57 state (ThermodynamicState): Outlet state for the vapor phase.
+58 """
+59 self._state_outlet_vapor = state
+ +61 @property
+62 def state_outlet_liquid(self) -> ThermodynamicState:
+63 """
+64 Getter for the outlet state of the liquid phase.
+ +66 Returns:
+67 ThermodynamicState: Outlet state for the liquid phase.
+68 """
+69 return self._state_outlet_liquid
+ +71 @state_outlet_liquid.setter
+72 def state_outlet_liquid(self, state: ThermodynamicState):
+73 """
+74 Setter for the outlet state of the liquid phase.
+ +76 Args:
+77 state (ThermodynamicState): Outlet state for the liquid phase.
+78 """
+79 self._state_outlet_liquid = state
++ coverage.py v7.3.2, + created at 2023-12-06 11:19 +0000 +
++ No items found using the specified filter. +
+