Source code for thermosteam.equilibrium.plot_equilibrium

# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
# 
# This module is under the UIUC open-source license. See 
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
"""
from .._settings import settings
from ..utils import colors, style_axis
from chemicals.identifiers import to_searchable_format
from .bubble_point import BubblePoint
from .lle import LLE
from ..indexer import MaterialIndexer
import matplotlib.pyplot as plt
from math import floor
import thermosteam as tmo
import numpy as np
from numpy.linalg import lstsq
import flexsolve as flx
from scipy import interpolate
from scipy.ndimage.filters import gaussian_filter
from typing import Iterable

__all__ = (
    'plot_vle_binary_phase_envelope',
    'plot_vle_phase_envelope',
    'plot_lle_ternary_diagram',
    'ternary_composition_grid',
)


# %% Utilities

def ternary_composition_grid(N, grid=None): # pragma: no cover
    grid = grid or []
    for z in np.linspace(0, 1 - 1/N, N):
        xs = np.linspace(0, 1-z, N)
        ys = 1 - z - xs 
        zs = z * np.ones(N)
        mesh = np.array([xs, ys, zs])
        grid.append(mesh)
    mesh = np.array([0, 0, 1]).reshape([3, 1])
    grid.append(mesh)
    return np.hstack(grid).transpose()

def as_thermo(thermo, chemicals): # pragma: no cover
    try:
        thermo = settings.get_default_thermo(thermo)
    except:
        thermo = tmo.Thermo(chemicals)
    else:
        thermo = tmo.Thermo(chemicals,
                            Gamma=thermo.Gamma,
                            Phi=thermo.Phi,
                            PCF=thermo.PCF)
    return thermo

# %% Plot functions

[docs] def plot_vle_binary_phase_envelope(chemicals, T=None, P=None, vc=None, lc=None, thermo=None, yticks=None, line_styles=None, N=None, lle=False): # pragma: no cover """ Plot the binary phase envelope of two chemicals at a given temperature or pressure. Parameters ---------- chemicals : Iterable[Chemical or str] Chemicals in equilibrium. T : float|list[float], optional Temperature [K]. P : float|list[float], optional Pressure [Pa]. vc : str, optional Color of vapor line. lc : str, optional Color of liquid line. thermo : Thermo, optional Thermodynamic property package. Examples -------- >>> # from thermosteam import equilibrium as eq >>> # eq.plot_vle_binary_phase_envelope(['Ethanol', 'Water'], P=101325) .. figure:: ../images/water_ethanol_binary_phase_envelope.png """ if line_styles is None: line_styles = ['-', '-.', '--'] thermo = as_thermo(thermo, chemicals) chemical_a, chemical_b = chemicals = [thermo.as_chemical(i) for i in chemicals] BP = tmo.BubblePoint(chemicals, thermo) if N is None: N = 50 zs_a = np.linspace(0, 1, N) zs_b = 1 - zs_a zs = np.vstack([zs_a, zs_b]).transpose() if P: if not isinstance(P, Iterable): P = [P] Xs = P X_units = 'Pa' assert not T, "must pass either T or P, but not both" bpss = [[BP(z, P=i, lle=lle) for z in zs] for i in P] mss = [[bp.T for bp in bps] for bps in bpss] ylabel = 'Temperature [K]' elif T: if not isinstance(T, Iterable): T = [T] assert not P, "must pass either T or P, but not both" Xs = T X_units = 'K' bpss = [[BP(z, T=i, lle=lle) for z in zs] for i in T] mss = [[bp.P for bp in bps] for bps in bpss] ylabel = 'Pressure [Pa]' else: raise AssertionError("must pass either T or P") plt.figure() mss = np.array(mss) for X, ms, bps, ls in zip(Xs, mss, bpss, line_styles): ys_a = np.array([bp.y[0] for bp in bps]) ms_liq = ms.copy() # ms_gas = interpolate.interp1d(ys_a, ms, bounds_error=False, kind='slinear')(zs_a) # if P: # azeotrope = ms_liq > ms_gas # elif T: # azeotrope = ms_liq < ms_gas # ms_liq[0] = 0.5 * (ms_liq[0] + ms_gas[0]) # ms_liq[-1] = 0.5 * (ms_liq[-1] + ms_gas[-1]) # if azeotrope.any(): # index = np.where(azeotrope)[0] # left = index[0] # right = index[-1] # mid = int(np.median(index)) # azeotrope[left:right] = True # azeotrope[mid] = False # ms_gas[mid] = ms_liq[mid] # ms_gas = interpolate.interp1d(zs_a[~azeotrope], ms_gas[~azeotrope], bounds_error=False, kind='slinear')(zs_a) plt.plot(ys_a, ms, c=vc if vc is not None else colors.red.RGBn, label=f'vapor [{int(X)} {X_units}]', ls=ls) plt.plot(zs_a, ms_liq, c=lc if lc is not None else colors.blue.RGBn, label=f'liquid [{int(X)} {X_units}]', ls=ls) plt.xlim([0, 1]) plt.ylim([mss.min(), mss.max()]) if yticks is None: yticks, ytext = plt.yticks() plt.legend() plt.xlabel(f'{chemical_a} molar fraction') plt.ylabel(ylabel) style_axis(xticks=np.linspace(0, 1, 5), yticks=yticks)
[docs] def plot_vle_phase_envelope( IDs, zs, T_range=None, P_range=None, thermo=None, vc=None, lc=None, xticks=None, yticks=None, N=None, line_styles=None, labels=None): # pragma: no cover """ Plot the binary phase envelope of two chemicals at a given temperature or pressure. Parameters ---------- IDs : Iterable[str] IDs of chemicals in equilibrium. zs : Iterable[float] Molar fraction of chemicals in equilibrium. T_range : tuple[float, float] optional Temperature [K]. P_range : tuple[float, float] optional Pressure [atm]. thermo : Thermo|Iterable[Thermo], optional Thermodynamic property package(s). vc : str, optional Color of vapor line. lc : str, optional Color of liquid line. """ if line_styles is None: line_styles = ['-', '-.', '--'] if thermo is None: thermo = [tmo.settings.get_thermo()] elif isinstance(thermo, (tmo.Thermo, tmo.IdealThermo)): thermo = [thermo] if labels is None: N_thermo = len(thermo) if N_thermo == 1: labels = [''] else: labels = [str(i) for i in range(N_thermo)] BPs = [tmo.BubblePoint(t.chemicals[IDs], t) for t in thermo] DPs = [tmo.DewPoint(t.chemicals[IDs], t) for t in thermo] if N is None: N = 20 if P_range is not None: assert T_range is None, "must pass either T_range or P_range, but not both" xlim = P_range xs = np.linspace(*xlim, N) bpss = [ [BP(zs, P=P * 101325) for P in xs] for BP in BPs ] dpss = [ [DP(zs, P=P * 101325) for P in xs] for DP in DPs ] ylabel = 'Temperature [K]' xlabel = 'Pressure [atm]' variable = 'T' elif T_range: assert P_range is None, "must pass either T_range or P_range, but not both" xlim = T_range xs = np.linspace(*xlim, N) bpss = [ [BP(zs, T=T) for T in xs] for BP in BPs ] dpss = [ [DP(zs, T=T) for T in xs] for DP in DPs ] ylabel = 'Pressure [atm]' xlabel = 'Temperature [K]' variable = 'P' else: raise AssertionError("must pass either T or P") Lss = np.array([ [getattr(bp, variable) for bp in bps] for bps in bpss ]) Vss = np.array([ [getattr(dp, variable) for dp in dps] for dps in dpss ]) plt.figure() if vc is None: vc = colors.red.RGBn if lc is None: lc = colors.blue.RGBn for Vs, Ls, ls, label in zip(Vss, Lss, line_styles, labels): plt.plot(xs, Vs, c=vc, ls=ls, label=', '.join([label, 'vapor'])) plt.plot(xs, Ls, c=lc, ls=ls, label=', '.join([label, 'liquid'])) plt.xlim(xlim) plt.ylim([min(Lss.min(), Vss.min()), max(Lss.max(), Vss.max())]) plt.legend() plt.xlabel(xlabel) plt.ylabel(ylabel) style_axis(xticks=xticks, yticks=yticks)
[docs] def plot_lle_ternary_diagram( carrier, solvent, solute, T, P=101325, thermo=None, color=None, tie_line_points=None, tie_color=None, N_tie_lines=8, N_equilibrium_grids=8, method=None ): # pragma: no cover """ Plot the ternary phase diagram of chemicals in liquid-liquid equilibrium. Parameters ---------- carrier : Chemical solvent : Chemical solute : Chemical T : float, optional Temperature [K]. P : float, optional Pressure [Pa]. Defaults to 101325. thermo : Thermo, optional Thermodynamic property package. color : str, optional Color of equilibrium line. tie_line_points : 1d array(size=3), optional Additional composition points to create tie lines. tie_color : str, optional Color of tie lines. N_tie_lines : int, optional Number of tie lines. The default is 15. N_equilibrium_grids : int, optional Number of solute composition points to plot. The default is 15. Examples -------- >>> # from thermosteam import equilibrium as eq >>> # eq.plot_lle_ternary_diagram('Water', 'Ethanol', 'EthylAcetate', T=298.15) .. figure:: ../images/water_ethanol_ethyl_acetate_ternary_diagram.png """ import ternary chemicals = [carrier, solute, solvent] thermo = as_thermo(thermo, chemicals) chemicals = thermo.chemicals IDs = chemicals.IDs imol = MaterialIndexer(['l', 'L'], chemicals=chemicals) data = imol.data lle = LLE(imol, thermo=thermo, method=method) composition_grid = ternary_composition_grid(N_equilibrium_grids, tie_line_points) tie_lines = [] MW = chemicals.MW for zs in composition_grid: data[0] = zs data[1] = 0 lle(T, P) mass = data * MW lL = mass.sum(1, keepdims=True) if (abs(lL) > 1e-5).all(): tie_line = 100 * mass / lL tie_diff = np.abs(tie_line[0] - tie_line[1]) ** 0.75 if tie_diff.sum() > 0.2: tie_lines.append(tie_line) tie_points = np.vstack(tie_lines) tie_points = tie_points[np.argsort(tie_points[:, 0])] def f(x): shape = np.shape(x) X = np.ones([*shape, 5]) X[..., 0] = x2 = x * x X[..., 1] = np.sqrt(x) X[..., 2] = x X[..., 3] = x2 * x return X xs = tie_points[:, 0] ys = tie_points[:, 1] X = f(xs) result = lstsq(X, ys, rcond=None) coef = result[0] xs = np.linspace(0, 100, 500) ys = (f(xs) * coef).sum(axis=1) zs = 100 - xs - ys ys[ys < 0] = 0 zs[zs < 0] = 0 tie_points = np.zeros([len(xs), 3]) tie_points[:, 0] = xs tie_points[:, 1] = ys tie_points[:, 2] = zs tie_points /= tie_points.sum(axis=1, keepdims=True) / 100 fig, tax = ternary.figure(scale=100) label = "-".join(IDs) + " Phase Diagram" tax.boundary(linewidth=2.0) tax.gridlines(color=colors.grey_tint.RGBn, multiple=10) fontsize = 12 offset = 0.14 C, A, S = [to_searchable_format(i) for i in IDs] tax.right_corner_label(8*' ' + 'Solvent', fontsize=fontsize) tax.top_corner_label('Solute\n', fontsize=fontsize) tax.left_corner_label('Carrier' + 8*' ', fontsize=fontsize) tax.left_axis_label(f'{C} wt. %', fontsize=fontsize, offset=offset) tax.right_axis_label(f'{A} wt. %', fontsize=fontsize, offset=offset) tax.bottom_axis_label(f'{S} wt. %', fontsize=fontsize, offset=offset) if color is None: color = 'k' tax.plot(tie_points, c=colors.neutral_shade.RGBn, label=label) if tie_line_points is None: assert N_tie_lines, "must specify number of tie lines if no equilibrium points are given" xs, ys = zip(*tie_lines) new_xs = np.array(xs) new_ys = np.array(ys) # new_xs = [] # new_ys = [] # for left, right in zip(xs, ys): # x, *_ = left # y = (f(x) * coef).sum() # z = 100 - y - x # if y < 0 or z < 0: continue # left = np.array([x, y, z]) # left = 100 * left / left.sum() # x, *_ = right # y = (f(x) * coef).sum() # z = 100 - y - x # if y < 0 or z < 0: continue # right = np.array([x, y, z]) # right = 100 * right / right.sum() # new_xs.append(left) # new_ys.append(right) # new_xs = np.array(new_xs) # new_ys = np.array(new_ys) # index = np.argsort(new_xs[:, 0]) N_total_lines = len(new_xs) step = floor(N_total_lines / N_tie_lines) or 1 partition_index = np.arange(0, N_total_lines, step, dtype=int) # index = index[partition_index] new_xs = [new_xs[i] for i in partition_index] new_ys = [new_ys[i] for i in partition_index] tie_lines = list(zip(new_xs, new_ys)) else: assert N_tie_lines is None, "cannot specify number of tie lines if equilibrium points are given" N_tie_lines = len(tie_line_points) tie_lines = tie_lines[:N_tie_lines] if tie_color is None: tie_color = colors.grey_shade.RGBn for x, y in tie_lines: tax.line(x, y, ls='--', c=tie_color) tax.ticks(axis='lbr', multiple=10, linewidth=1, offset=0.025) tax.get_axes().axis('off') tax.clear_matplotlib_ticks()