#!/usr/bin/python3
# -*- coding: utf-8 -*-
'''Pychemqt, Chemical Engineering Process simulator
Copyright (C) 2009-2025, Juan José Gómez Romera <jjgomera@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
.. include:: standing.rst
API Reference
-------------
The module include all related moody chart functionality
* :class:`Standing_Katz`: Chart dialog
* :func:`calculate`: Calculate procedure
* :class:`CalculateDialog`: Dialog to calculate a specified point and \
its configuration
* :class:`Config`: Standing-Katz chart configuration
'''
import json
import os
import re
from numpy import arange
from scipy.optimize import fsolve
from lib.config import conf_dir, Preferences
from lib.crude import Z_list
from lib.plot import PlotWidget
from lib.utilities import formatLine
from tools.qt import QtCore, QtGui, QtWidgets, translate
from UI.widgets import Entrada_con_unidades, GridConfig, LineConfig
from plots.ui import Chart
[docs]
def calculate(config, dat=None):
"""Plot calculate procedure
Parameters
----------
config : Configparser
pychemqt configparser configuration instance
dat : dict
dict with other method data
Returns
-------
dat : dict
dict with method data
Notes
-----
This procedure is called when a new method of calculation of Z is
necessary. Add the new data to the input dat with the other Method
calculated yet
"""
method = config.getint("Standing_Katz", "method")
Tr = map(float, config.get("Standing_Katz", "Tr").split(","))
Z = Z_list[method]
Pr = arange(1e-3, 15, 0.01)
if dat is None:
dat = {}
lines = {}
for t in Tr:
pr = []
z = []
for p in Pr:
try:
z.append(Z(t, p))
pr.append(p)
except NotImplementedError:
pass
except ValueError:
pass
# Only save the lines with data
if pr:
lines[t] = {"Pr": pr, "Z": z}
dat[method] = lines
# Save to file
with open(conf_dir+"standing_katz.dat", "w", encoding="utf-8") as file:
json.dump(dat, file, indent=4)
[docs]
class Config(QtWidgets.QWidget):
"""Standing-Katz chart configuration"""
TITLE = translate("Standing_Katz", "Standing-Katz chart")
TITLECONFIG = translate("Standing_Katz", "Standing-Katz chart configuration")
[docs]
def __init__(self, config=None, parent=None):
super().__init__(parent)
layout = QtWidgets.QGridLayout(self)
layout.addWidget(QtWidgets.QLabel(self.tr("Method:")), 1, 1)
self.method = QtWidgets.QComboBox()
for Z in Z_list:
name = Z.__name__[2:].replace("_", "-")
year = re.search(r"((\d+))", Z.__doc__).group(0)
doc = f"{name} ({year})"
self.method.addItem(doc)
layout.addWidget(self.method, 1, 2)
layout.addItem(QtWidgets.QSpacerItem(
10, 10, QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Fixed), 1, 3)
layout.addWidget(QtWidgets.QLabel("Tr:"), 4, 1)
self.Tr = QtWidgets.QLineEdit()
layout.addWidget(self.Tr, 4, 2, 1, 2)
self.lineconfig = LineConfig(
"line", self.tr("Reduced temperature style line"))
layout.addWidget(self.lineconfig, 5, 1, 1, 3)
self.cruxconfig = LineConfig("crux", self.tr("Crux style line"))
layout.addWidget(self.cruxconfig, 6, 1, 1, 3)
self.gridconfig = GridConfig("grid", self.tr("Grid style line"))
layout.addWidget(self.gridconfig, 7, 1, 1, 3)
layout.addItem(QtWidgets.QSpacerItem(
10, 0, QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding), 10, 1, 1, 3)
if config and config.has_section("Standing_Katz"):
self.method.setCurrentIndex(config.getint(
"Standing_Katz", "method"))
self.Tr.setText(config.get("Standing_Katz", "Tr"))
self.lineconfig.setConfig(config, "Standing_Katz")
self.cruxconfig.setConfig(config, "Standing_Katz")
self.gridconfig.setConfig(config, "Standing_Katz")
[docs]
def value(self, config):
"""Update ConfigParser instance with the config"""
if not config.has_section("Standing_Katz"):
config.add_section("Standing_Katz")
config.set("Standing_Katz", "method", str(self.method.currentIndex()))
config.set("Standing_Katz", "Tr", self.Tr.text())
config = self.lineconfig.value(config, "Standing_Katz")
config = self.cruxconfig.value(config, "Standing_Katz")
config = self.gridconfig.value(config, "Standing_Katz")
return config
[docs]
class Standing_Katz(Chart):
"""Standing-Katz chart dialog"""
title = translate("Standing_Katz", "Standing and Katz compressivitity factors "
"chart for natural gas")
widgetConfig = Config
locLogo = (0.8, 0.12, 0.1, 0.1)
note = None
[docs]
def customUI(self):
"""Define custom UI element"""
self.butonPNG.clicked.disconnect()
self.butonPNG.clicked.connect(self.savePNG)
self.plt2 = PlotWidget(parent=self, dpi=90)
self.plt2.fig.canvas.mpl_connect('button_press_event', self.click)
self.plotWidget.layout().addWidget(self.plt2, 1, 1)
self.setMask()
self.set_logo(self.plt2)
[docs]
def savePNG(self):
"""Save chart image to png file"""
fmt = "Portable Network Graphics (*.png)"
fname, ext = QtWidgets.QFileDialog.getSaveFileName(
self, self.tr("Save chart to file"), "./", fmt)
if fname and ext == fmt:
if fname.split(".")[-1] != "png":
fname += ".png"
pix = self.plotWidget.grab()
pix.save(fname, "png")
[docs]
def setMask(self):
"""Mask both plot to show only the region useful"""
w = self.plt.width()
h = self.plt.height()
pol = QtGui.QPolygon()
pol.append(QtCore.QPoint(int(0.05*w), int(0.05*h)))
pol.append(QtCore.QPoint(int(w), int(0.05*h)))
pol.append(QtCore.QPoint(int(w), int(0.23*h)))
pol.append(QtCore.QPoint(int(0.9*w), int(0.228*h)))
pol.append(QtCore.QPoint(int(0.3*w), int(0.698*h)))
pol.append(QtCore.QPoint(int(0.125*w), int(0.698*h)))
pol.append(QtCore.QPoint(int(0.125*w), int(0.7*h)))
pol.append(QtCore.QPoint(int(0.05*w), int(0.7*h)))
reg = QtGui.QRegion(pol)
self.plt.setMask(reg)
pol = QtGui.QPolygon()
pol.append(QtCore.QPoint(int(0.05*w), int(0.7*h)))
pol.append(QtCore.QPoint(int(0.3*w), int(0.7*h)))
pol.append(QtCore.QPoint(int(0.90*w), int(0.23*h)))
pol.append(QtCore.QPoint(int(0.90*w), int(0.228*h)))
pol.append(QtCore.QPoint(int(w), int(0.228*h)))
pol.append(QtCore.QPoint(int(w), int(h)))
pol.append(QtCore.QPoint(int(0.05*w), int(h)))
reg = QtGui.QRegion(pol)
self.plt2.setMask(reg)
x = (0, 1.8, 8)
y = (0.276, 0.276, 0.95)
self.plt.ax.plot(x, y, "black", lw=0.5)
x = (7, 8.8, 15)
y = (1.17, 1.17, 1.84)
self.plt2.ax.plot(x, y, "black", lw=0.5)
self.plt.draw()
self.plt2.draw()
[docs]
def paintEvent(self, event):
"""Do redraw in each change of window size or position"""
self.setMask()
Chart.paintEvent(self, event)
[docs]
def click(self, event):
"""Update input and graph annotate when mouse click over chart"""
Pr = event.xdata
Z = event.ydata
# Exit if click event if out of axis
if Pr is None:
self.clearCrux()
return
method = Preferences.getint("Standing_Katz", "method")
f_Z = Z_list[method]
def f(Tr):
return f_Z(Tr, Pr) - Z
Tr = 0
for to in (1, 2, 3):
try:
rinput = fsolve(f, to, full_output=True)
if rinput[2] == 1:
Tr = rinput[0][0]
break
except ValueError:
continue
self.createCrux(Tr, Pr, Z)
[docs]
def plot(self):
"""Plot the Standing-Katz chart using the indicate method """
method = Preferences.get("Standing_Katz", "method")
self.plt.ax.clear()
self.plt.ax.set_xlim(0, 8)
self.plt.ax.set_ylim(0, 1.1)
self.plt.ax.set_xlabel(r"$P_r=\frac{P}{P_c}$", ha='center', size='14')
self.plt.ax.set_ylabel(r"$Z=\frac{PV}{nRT}$", va="bottom", size='14')
self.plt2.ax.clear()
self.plt2.ax.set_xlim(7, 15)
self.plt2.ax.set_ylim(0.9, 2)
self.plt2.ax.set_xlabel(r"$P_r=\frac{P}{P_c}$", ha='center', size='14')
grid = Preferences.getboolean("Standing_Katz", "grid")
kw = formatLine(Preferences, "Standing_Katz", "grid")
del kw["marker"]
if grid:
self.plt.ax.grid(grid, **kw)
self.plt2.ax.grid(grid, **kw)
else:
self.plt.ax.grid(grid)
self.plt2.ax.grid(grid)
if not os.path.isfile(conf_dir+"standing_katz.dat"):
calculate(Preferences)
load = False
with open(conf_dir+"standing_katz.dat", "r", encoding="utf-8") as file:
try:
dat = json.load(file)
except ValueError:
calculate(Preferences)
load = True
if method not in dat:
calculate(Preferences, dat)
load = True
# Reload file if it's created in last with statement
if load:
fname = conf_dir+"standing_katz.dat"
with open(fname, "r", encoding="utf-8") as file:
dat = json.load(file)
# Define Crux
kw = formatLine(Preferences, "Standing_Katz", "crux")
self.plt.lx = self.plt.ax.axhline(**kw) # the horiz line
self.plt.ly = self.plt.ax.axvline(**kw) # the vert line
self.plt.lx.set_visible(False)
self.plt.ly.set_visible(False)
self.plt2.lx = self.plt2.ax.axhline(**kw) # the horiz line
self.plt2.ly = self.plt2.ax.axvline(**kw) # the vert line
self.plt2.lx.set_visible(False)
self.plt2.ly.set_visible(False)
self.plt.ax.xaxis.tick_top()
self.plt2.ax.yaxis.tick_right()
self.note = None
# Plot data
kw = formatLine(Preferences, "Standing_Katz", "line")
pzmin = 4
for Tr in sorted(dat[str(method)].keys()):
line = dat[str(method)][Tr]
self.plt.ax.plot(line["Pr"], line["Z"], **kw)
self.plt2.ax.plot(line["Pr"], line["Z"], **kw)
# Add Tr legend
# Position as possible at minimum position of line
zmin = min(line["Z"])
if zmin < 1:
pzmin = line["Pr"][line["Z"].index(zmin)]
else:
if 4 not in line["Pr"]:
line["Pr"].append(pzmin)
line["Pr"].sort()
zmin = line["Z"][line["Pr"].index(pzmin)]
self.plt.ax.text(pzmin, zmin, str(Tr),
size="x-small", ha='left', va='bottom')
self.plt2.ax.text(line["Pr"][-1], line["Z"][-1], str(Tr),
size="x-small", ha='right', va='bottom')
# Add explicative legend of isoline
self.plt.ax.text(3, 1.07, r"$T_r$", size="12",
ha='left', va='center')
self.plt.draw()
[docs]
def calculate(self):
dlg = CalculateDialog()
if dlg.exec():
Tr = dlg.Tr.value
Pr = dlg.Pr.value
Z = dlg.Z.value
self.createCrux(Tr, Pr, Z)
[docs]
def clearCrux(self):
"""Delete crux and note text"""
if self.note:
self.note.remove()
self.note = None
self.plt.lx.set_visible(False)
self.plt.ly.set_visible(False)
self.plt2.lx.set_visible(False)
self.plt2.ly.set_visible(False)
self.plt.draw()
self.plt2.draw()
[docs]
def createCrux(self, Tr, Pr, Z):
"""Create a crux in selected point of plot and show data at bottom
right corner"""
self.clearCrux()
if Pr < 8:
self.plt.lx.set_visible(True)
self.plt.ly.set_visible(True)
self.plt.ly.set_xdata([Pr])
self.plt.lx.set_ydata([Z])
if Pr > 7:
self.plt2.lx.set_visible(True)
self.plt2.ly.set_visible(True)
self.plt2.ly.set_xdata([Pr])
self.plt2.lx.set_ydata([Z])
if Tr:
txt = f"$T_r$: {Tr:0.4g}\n$P_r$: {Pr:0.4g}\nZ: {Z:0.4g}"
else:
Tr = self.tr("Not converged")
txt = f"$T_r$: {Tr}\n$P_r$: {Pr:0.4g}\nZ: {Z:0.4g}"
self.note = self.plt2.fig.text(0.92, 0.05, txt, size="8", ha="right")
self.plt.draw()
self.plt2.draw()
[docs]
class CalculateDialog(QtWidgets.QDialog):
"""Dialog to calculate a specified point"""
[docs]
def __init__(self, parent=None):
super().__init__(parent)
title = self.tr("Calculate compressibility factor of natural gas")
self.setWindowTitle(title)
layout = QtWidgets.QGridLayout(self)
label = QtWidgets.QLabel(self.tr("Method:"))
layout.addWidget(label, 1, 0)
self.method = QtWidgets.QComboBox()
for Z in Z_list:
name = Z.__name__[2:].replace("_", "-")
year = re.search(r"((\d+))", Z.__doc__).group(0)
doc = f"{name} ({year})"
self.method.addItem(doc)
self.method.currentIndexChanged.connect(self.calculate)
layout.addWidget(self.method, 1, 1, 1, 2)
layout.addWidget(QtWidgets.QLabel("Tr"), 2, 1)
self.Tr = Entrada_con_unidades(float, tolerancia=4)
self.Tr.valueChanged.connect(self.calculate)
layout.addWidget(self.Tr, 2, 2)
layout.addWidget(QtWidgets.QLabel("Pr"), 3, 1)
self.Pr = Entrada_con_unidades(float)
self.Pr.valueChanged.connect(self.calculate)
layout.addWidget(self.Pr, 3, 2)
layout.addWidget(QtWidgets.QLabel("Z"), 4, 1)
self.Z = Entrada_con_unidades(float, readOnly=True, decimales=8)
layout.addWidget(self.Z, 4, 2)
layout.addItem(QtWidgets.QSpacerItem(
10, 10, QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding), 9, 1, 1, 3)
self.buttonBox = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok
| QtWidgets.QDialogButtonBox.StandardButton.Close)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 10, 1, 1, 3)
[docs]
def calculate(self):
"""Calculate point procedure"""
index = self.method.currentIndex()
Z = Z_list[index]
Tr = self.Tr.value
Pr = self.Pr.value
if Pr and Tr is not None:
z = Z(Tr, Pr)
self.Z.setValue(z)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = Standing_Katz()
Dialog.show()
sys.exit(app.exec())