Source code for tools.UI_Tables.plot

#!/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/>.'''


###############################################################################
# Plot functionality for plugin:
#   - PlotMEoS: Plot widget to show meos plot data, add context menu options
#   - Plot2D: Dialog for select a special 2D plot
#   - Plot3D: Dialog for define a 3D plot
#   - EditPlot: Dialog to edit plot
#   - AddLine: Dialog to add new isoline to plot
#   - EditAxis: Dialog to configure axes plot properties
#   - AxisWidget: Dialog to configure axes plot properties
#   - calcIsoline: Isoline calculation procedure
#   - get_points: Get point number to plot lines from Preferences
#   - getLineFormat: get matplotlib line format from preferences
#   - plotIsoline: plot isoline procedure
#   - plot2D3D: general procedure for plotting 2D and 3D
#   - _getunitTransform: Return the axis unit transform function to map data
#     to configurated unit
###############################################################################


from functools import partial
import gzip
import json
from math import log10, atan, pi
import os

from numpy import concatenate, linspace, logspace, transpose, log, nan, array
from matplotlib import colormaps as cm
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT
from matplotlib.font_manager import FontProperties
from tools.qt import QtCore, QtGui, QtWidgets, __qt__, translate

from lib import meos, unidades, plot, config
from lib.thermo import ThermoAdvanced
from lib.utilities import formatLine
from UI.widgets import (Entrada_con_unidades, createAction, LineStyleCombo,
                        MarkerCombo, ColorSelector, InputFont, ClickableLabel)

from .library import calcPoint, getLimit, getClassFluid, getMethod
from .chooseFluid import Dialog_InfoFluid


[docs] class PlotMEoS(QtWidgets.QWidget): """Plot widget to show meos plot data, add context menu options""" icon = os.path.join(config.IMAGE_PATH, "button", "plot.png") mouseMove = QtCore.pyqtSignal(QtCore.QPointF)
[docs] def __init__(self, dim, toolbar=False, filename="", parent=None): """constructor Input: dim: dimension of plot, | 2 | 3 toolbar: boolean to add the matplotlib toolbar filename: filename for data """ super().__init__(parent) self.setWindowIcon(QtGui.QIcon(QtGui.QPixmap(self.icon))) self.setMouseTracking(True) self.parent = parent self.dim = dim self.filename = filename self.notes = [] layout = QtWidgets.QVBoxLayout(self) self.plot = plot.PlotWidget(dim=dim, parent=self) self.plot.lx = self.plot.ax.axhline(c="#888888", ls=":") # horiz line self.plot.ly = self.plot.ax.axvline(c="#888888", ls=":") # vert line self.plot.lx.set_visible(False) self.plot.ly.set_visible(False) layout.addWidget(self.plot) self.toolbar = NavigationToolbar2QT(self.plot, self.plot) self.toolbar.setVisible(toolbar) layout.addWidget(self.toolbar) self.editAxesAction = createAction( self.tr("Edit &Axis"), icon=os.path.join("button", "editor.png"), slot=self.editAxis, parent=self) self.editAction = createAction( self.tr("Edit &Plot"), slot=self.edit, icon=os.path.join("button", "fit.png"), parent=self) self.editMarginAction = createAction( self.tr("Edit &Margins"), slot=self.toolbar.configure_subplots, parent=self) self.saveAction = createAction( self.tr("&Save Plot"), slot=self.toolbar.save_figure, icon=os.path.join("button", "fileSave.png"), parent=self) self.toolbarVisibleAction = createAction( self.tr("Toggle &Toolbar"), slot=self.toolbar.setVisible, checkable=True, parent=self) self.gridToggleAction = createAction( self.tr("Toggle &Grid"), slot=self.grid, checkable=True, parent=self) grid = config.Preferences.getboolean("MEOS", "grid") self.gridToggleAction.setChecked(grid) # Widgets to show in the statusbar of mainwindow self.statusWidget = [] self.statusPosition = QtWidgets.QLabel() self.statusPosition.setFrameShape(QtWidgets.QFrame.Shape.WinPanel) self.statusPosition.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) self.statusWidget.append(self.statusPosition) self.statusThermo = ClickableLabel() self.statusThermo.setFrameShape(QtWidgets.QFrame.Shape.WinPanel) self.statusThermo.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) self.statusWidget.append(self.statusThermo) self.statusThermo.clicked.connect(self.showFluid) if dim == 2: self.plot.fig.canvas.mpl_connect('button_press_event', self.click) self.plot.fig.canvas.mpl_connect( 'motion_notify_event', self.updatePosition)
[docs] def showFluid(self): """Show dialog with properties of selected fluid""" method = self.config["method"] index = self.config["fluid"] fluid = getClassFluid(method, index) dlg = Dialog_InfoFluid(fluid.__class__) dlg.exec()
[docs] def mouseMoveEvent(self, event): """Mouse move event over the plot""" QtWidgets.QWidget.mouseMoveEvent(self, event) if __qt__ == 5: self.mouseMove.emit(event.globalPos()) else: self.mouseMove.emit(event.globalPosition())
[docs] def closeEvent(self, event): """Force save status of window at exit""" self.parent.dirty[self.parent.idTab] = True self.parent.saveControl()
[docs] def contextMenuEvent(self, event): """Create context menu""" menuTable = QtWidgets.QMenu(self.tr("Tabulated data")) menuTable.setIcon( QtGui.QIcon(os.environ["pychemqt"]+"/images/button/table")) for linea in self.plot.ax.lines: action = createAction( linea.get_label(), slot=partial(self.table, linea), parent=self) menuTable.addAction(action) menu = QtWidgets.QMenu() menu.addAction(self.editAxesAction) menu.addAction(self.editAction) menu.addAction(self.editMarginAction) menu.addSeparator() menu.addAction(self.saveAction) menu.addAction(menuTable.menuAction()) menu.addSeparator() menu.addAction(self.toolbarVisibleAction) menu.addAction(self.gridToggleAction) menu.exec(event.globalPos()) if self.plot.ax._gridOn: self.gridToggleAction.setChecked(True)
[docs] def grid(self, boolean): """Set grid visibility of plot""" self.plot.ax.grid(boolean) self.plot.ax._gridOn = boolean self.plot.draw()
[docs] def edit(self): """Show edit plot dialog""" dialog = EditPlot(self, self.parent) dialog.exec()
[docs] def editAxis(self): """Show edit axes dialog""" dialog = EditAxis(self.plot, self.parent) dialog.exec()
[docs] def table(self, obj): """Export plot data to table Input: obj: object (Line2D instance) to show data """ xtxt = meos.propiedades[meos.keys.index(self.x)] ytxt = meos.propiedades[meos.keys.index(self.y)] xunit = meos.units[meos.keys.index(self.x)] yunit = meos.units[meos.keys.index(self.y)] HHeader = [xtxt+os.linesep+xunit.text(), ytxt+os.linesep+yunit.text()] units = [xunit, yunit] if self.dim == 3: ztxt = meos.propiedades[meos.keys.index(self.z)] zunit = meos.units[meos.keys.index(self.z)] HHeader.append(ztxt+os.linesep+zunit.text()) units.append(zunit) data = obj._verts3d else: data = obj.get_data(orig=True) # Don't import at top level to avoid recursion import from .table import TablaMEoS tabla = TablaMEoS(self.dim, horizontalHeader=HHeader, units=units, stretch=False, readOnly=True, parent=self.parent) method = getMethod() projectConfig = self.parent.currentConfig index = projectConfig.getint("MEoS", "fluid") tabla.Point = getClassFluid(method, index) tabla.setData(list(map(list, transpose(data)))) tabla.verticalHeader().setContextMenuPolicy( QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.parent.centralWidget().currentWidget().addSubWindow(tabla) title = self.tr("Table from") + " " + obj.get_label() tabla.setWindowTitle(title) wdg = self.parent.centralWidget().currentWidget().subWindowList()[-1] wdg.setWindowIcon(QtGui.QIcon(QtGui.QPixmap(tabla.icon))) self.parent.dirty[self.parent.idTab] = True self.parent.saveControl() tabla.show()
[docs] def _getData(self): """Get data from file""" filenameHard = os.path.join(os.environ["pychemqt"], "dat", "mEoS", self.filename+".gz") filenameSoft = os.path.join(config.conf_dir, self.filename) if os.path.isfile(filenameSoft): print(filenameSoft) with open(filenameSoft, "rb") as archivo: data = json.load(archivo) elif os.path.isfile(filenameHard): print(filenameHard) with gzip.GzipFile(filenameHard, 'rb') as archivo: data = json.load(archivo) self._saveData(data) else: data = None return data
[docs] def _saveData(self, data): """Save changes in data to file""" with open(os.path.join(config.conf_dir, self.filename), 'w') as file: json.dump(data, file)
[docs] def click(self, event): """Update input and graph annotate when mouse click over chart""" # Accept only left click if event.button != 1: return units = {"x": unidades.Dimensionless, "T": unidades.Temperature, "P": unidades.Pressure, "h": unidades.Enthalpy, "u": unidades.Enthalpy, "s": unidades.SpecificHeat, "v": unidades.SpecificVolume, "rho": unidades.Density} if self.x in units and self.y in units: x = units[self.x](event.xdata, "conf") y = units[self.y](event.ydata, "conf") method = self.config["method"] index = self.config["fluid"] fluid = getClassFluid(method, index) kwargs = {self.x: x, self.y: y} fluido = calcPoint(fluid, self.config, **kwargs) Tmin, Tmax, Pmin, Pmax = getLimit(fluid, self.config) if fluido and fluido.status and \ Tmin <= fluido.T <= Tmax and \ 0 < fluido.P.kPa <= Pmax: self.plot.lx.set_ydata([event.ydata]) self.plot.ly.set_xdata([event.xdata]) self.plot.lx.set_visible(True) self.plot.ly.set_visible(True) self.showPointData(fluido) else: self.plot.lx.set_visible(False) self.plot.ly.set_visible(False) self.clearPointData()
[docs] def showPointData(self, state): self.clearPointData() yi = 0.98 for key in ("T", "P", "x", "v", "rho", "h", "s", "u"): self.notes.append(self.plot.ax.annotate( "%s: %s" % (key, getattr(state, key).str), (0.01, yi), xycoords='axes fraction', size="small", va="center")) yi -= 0.025 self.plot.draw()
[docs] def clearPointData(self): while self.notes: anotation = self.notes.pop() anotation.remove() self.plot.draw()
[docs] def updatePosition(self, point): try: x = point.xdata y = point.ydata except AttributeError: x = None if x is None: self.statusPosition.setText("-, -") else: txt = "%s=%4g, %s=%4g" % (self.x, x, self.y, y) self.statusPosition.setText(txt)
[docs] def writeToJSON(self, data): """Write instance parameter to file""" data["filename"] = self.filename data["windowTitle"] = self.windowTitle() data["x"] = self.x data["y"] = self.y data["z"] = self.z # TODO: Add support for save font properties # Title format title = {} title["txt"] = self.plot.ax.get_title() title["color"] = self.plot.ax.title.get_color() title["family"] = self.plot.ax.title.get_fontfamily() title["style"] = self.plot.ax.title.get_style() title["weight"] = self.plot.ax.title.get_weight() title["stretch"] = self.plot.ax.title.get_stretch() title["size"] = self.plot.ax.title.get_size() data["title"] = title # xlabel format xlabel = {} xlabel["txt"] = self.plot.ax.get_xlabel() xlabel["color"] = self.plot.ax.xaxis.get_label().get_color() xlabel["family"] = self.plot.ax.xaxis.get_label().get_fontfamily() xlabel["style"] = self.plot.ax.xaxis.get_label().get_style() xlabel["weight"] = self.plot.ax.xaxis.get_label().get_weight() xlabel["stretch"] = self.plot.ax.xaxis.get_label().get_stretch() xlabel["size"] = self.plot.ax.xaxis.get_label().get_size() data["xlabel"] = xlabel # ylable format ylabel = {} ylabel["txt"] = self.plot.ax.get_ylabel() ylabel["color"] = self.plot.ax.yaxis.get_label().get_color() ylabel["family"] = self.plot.ax.yaxis.get_label().get_fontfamily() ylabel["style"] = self.plot.ax.yaxis.get_label().get_style() ylabel["weight"] = self.plot.ax.yaxis.get_label().get_weight() ylabel["stretch"] = self.plot.ax.yaxis.get_label().get_stretch() ylabel["size"] = self.plot.ax.yaxis.get_label().get_size() data["ylabel"] = ylabel # zlable format zlabel = {} if self.z: zlabel["txt"] = self.plot.ax.get_zlabel() zlabel["color"] = self.plot.ax.zaxis.get_label().get_color() zlabel["family"] = self.plot.ax.zaxis.get_label().get_fontfamily() zlabel["style"] = self.plot.ax.zaxis.get_label().get_style() zlabel["weight"] = self.plot.ax.zaxis.get_label().get_weight() zlabel["stretch"] = self.plot.ax.zaxis.get_label().get_stretch() zlabel["size"] = self.plot.ax.zaxis.get_label().get_size() data["zlabel"] = zlabel data["grid"] = self.plot.ax._gridOn data["xscale"] = self.plot.ax.get_xscale() data["yscale"] = self.plot.ax.get_yscale() xmin, xmax = self.plot.ax.get_xlim() data["xmin"] = xmin data["xmax"] = xmax ymin, ymax = self.plot.ax.get_ylim() data["ymin"] = ymin data["ymax"] = ymax if self.z: zmin, zmax = self.plot.ax.get_zlim() data["zmin"] = zmin data["zmax"] = zmax data["zscale"] = self.plot.ax.get_zscale() data["azim"] = self.plot.ax.azim data["elev"] = self.plot.ax.elev data["roll"] = self.plot.ax.roll # Save mesh data if exist mesh = {} if self.plot.ax.collections: mesh["data"] = self.plot.ax.data3D mesh["type"] = self.plot.ax.meshtype if mesh["type"]: # Wireframe specific kw color = self.plot.ax.collections[0].get_color().tolist() mesh["color"] = color mesh["lw"] = self.plot.ax.collections[0].get_lw()[0] mesh["ls"] = self.plot.ax.collections[0].get_ls()[0] else: # Surface specific kw mesh["colormap"] = self.plot.ax.collections[0].cmap.name mesh["alpha"] = self.plot.ax.collections[0].get_alpha() data["mesh"] = mesh else: data["zmin"] = None data["zmax"] = None data["marginleft"] = self.plot.fig.subplotpars.left data["marginbottom"] = self.plot.fig.subplotpars.bottom data["marginright"] = self.plot.fig.subplotpars.right data["margintop"] = self.plot.fig.subplotpars.top # Config data["method"] = self.config["method"] data["fluid"] = self.config["fluid"] data["eq"] = self.config["eq"] data["visco"] = self.config["visco"] data["thermal"] = self.config["thermal"] if self.config["method"] == "meos": data["external_dependences"] = "" elif self.config["method"] == "coolprop": data["external_dependences"] = "CoolProp" else: data["external_dependences"] = "refprop" # data lines = {} for line in self.plot.ax.lines[2:]: dat = {} if self.z: x, y, z = line.get_data_3d() dat["x"] = x.tolist() dat["y"] = y.tolist() dat["z"] = z.tolist() else: dat["x"] = line.get_xdata().tolist() dat["y"] = line.get_ydata().tolist() dat["label"] = line.get_label() # line style dat["lw"] = line.get_lw() dat["ls"] = line.get_ls() dat["marker"] = line.get_marker() dat["color"] = line.get_color() dat["ms"] = line.get_ms() dat["mfc"] = line.get_mfc() dat["mew"] = line.get_mew() dat["mec"] = line.get_mec() dat["visible"] = line.get_visible() dat["antialiased"] = line.get_antialiased() # line text # saturation and melting line dont define it at plot creation try: text = {} text["visible"] = line.text.get_visible() text["txt"] = line.text.get_text() text["rot"] = line.text.get_rotation() text["pos"] = line.text.pos text["family"] = line.text.get_fontfamily() text["style"] = line.text.get_style() text["weight"] = line.text.get_weight() text["stretch"] = line.text.get_stretch() text["size"] = line.text.get_size() text["va"] = line.text.get_va() except AttributeError: text = {"visible": False, "txt": "", "pos": 50, "rot": 0, "family": "sans-serif", "style": "normal", "weight": "normal", "stretch": "normal", "size": "small", "va": "center"} dat["annotation"] = text lines[line._label] = dat data["lines"] = lines
[docs] @classmethod def readFromJSON(cls, data, parent): """Read window data from file""" filename = data["filename"] title = data["windowTitle"] x = data["x"] y = data["y"] z = data["z"] if z: dim = 3 else: dim = 2 grafico = PlotMEoS(dim=dim, parent=parent, filename=filename) grafico.x = x grafico.y = y grafico.z = z grafico.setWindowTitle(title) title = data["title"]["txt"] if title: grafico.plot.ax.set_title(title) grafico.plot.ax.title.set_color(data["title"]["color"]) grafico.plot.ax.title.set_family(data["title"]["family"]) grafico.plot.ax.title.set_style(data["title"]["style"]) grafico.plot.ax.title.set_weight(data["title"]["weight"]) grafico.plot.ax.title.set_stretch(data["title"]["stretch"]) grafico.plot.ax.title.set_size(data["title"]["size"]) xlabel = data["xlabel"]["txt"] if xlabel: grafico.plot.ax.set_xlabel(xlabel) label = grafico.plot.ax.xaxis.get_label() label.set_color(data["xlabel"]["color"]) label.set_family(data["xlabel"]["family"]) label.set_style(data["xlabel"]["style"]) label.set_weight(data["xlabel"]["weight"]) label.set_stretch(data["xlabel"]["stretch"]) label.set_size(data["xlabel"]["size"]) ylabel = data["ylabel"]["txt"] if ylabel: grafico.plot.ax.set_ylabel(ylabel) label = grafico.plot.ax.yaxis.get_label() label.set_color(data["ylabel"]["color"]) label.set_family(data["ylabel"]["family"]) label.set_style(data["ylabel"]["style"]) label.set_weight(data["ylabel"]["weight"]) label.set_stretch(data["ylabel"]["stretch"]) label.set_size(data["ylabel"]["size"]) if z: zlabel = data["zlabel"]["txt"] if zlabel: grafico.plot.ax.set_zlabel(zlabel) label = grafico.plot.ax.zaxis.get_label() label.set_color(data["zlabel"]["color"]) label.set_family(data["zlabel"]["family"]) label.set_style(data["zlabel"]["style"]) label.set_weight(data["zlabel"]["weight"]) label.set_stretch(data["zlabel"]["stretch"]) label.set_size(data["zlabel"]["size"]) grafico.plot.ax._gridOn = data["grid"] grafico.plot.ax.grid(data["grid"]) grafico.plot.ax.set_xlim(data["xmin"], data["xmax"]) grafico.plot.ax.set_ylim(data["ymin"], data["ymax"]) if z: grafico.plot.ax.set_zlim(data["zmin"], data["zmax"]) kw = {k: data[k] for k in ("azim", "elev", "roll")} grafico.plot.ax.view_init(**kw) if data["mesh"]: xi, yi, zi = data["mesh"]["data"] kw = {} meshtype = data["mesh"]["type"] if meshtype: # Wireframe specific kw kw["color"] = data["mesh"]["color"] kw["lw"] = data["mesh"]["lw"] kw["ls"] = tuple(data["mesh"]["ls"]) grafico.plot.ax.plot_wireframe(xi, yi, array(zi), **kw) else: # Surface specific kw kw["cmap"] = cm.get(data["mesh"]["colormap"]) kw["alpha"] = data["mesh"]["alpha"] grafico.plot.ax.plot_surface(xi, yi, array(zi), **kw) grafico.plot.ax.data3D = (xi, yi, zi) grafico.plot.ax.meshtype = meshtype for label, line in data["lines"].items(): xdata = line["x"] ydata = line["y"] fmt = {} fmt["label"] = label fmt["lw"] = line["lw"] fmt["ls"] = line["ls"] fmt["marker"] = line["marker"] fmt["color"] = line["color"] fmt["ms"] = line["ms"] fmt["mfc"] = line["mfc"] fmt["mew"] = line["mew"] fmt["mec"] = line["mec"] if z: zdata = line["z"] ln, = grafico.plot.ax.plot(xdata, ydata, zdata, **fmt) else: ln, = grafico.plot.ax.plot(xdata, ydata, **fmt) ln.set_visible(line["visible"]) ln.set_antialiased(line["antialiased"]) if dim == 3: continue txt = line["annotation"]["txt"] rot = line["annotation"]["rot"] pos = line["annotation"]["pos"] i = int(len(x)*pos/100) kw = {} kw["ha"] = "center" kw["rotation"] = rot kw["rotation_mode"] = "anchor" for key in ("va", "visible", "family", "style", "weight", "stretch", "size"): kw[key] = line["annotation"][key] if i >= len(x): i = len(x)-1 if txt: text = grafico.plot.ax.text(x[i], y[i], txt, **kw) # We creating a link between line and its annotation text ln.text = text # We save position value in % unit to avoid index find ln.text.pos = pos if dim == 2: grafico.plot.ax.set_xscale(data["xscale"]) grafico.plot.ax.set_yscale(data["yscale"]) # Load margins kw = {} kw["left"] = data["marginleft"] kw["bottom"] = data["marginbottom"] kw["right"] = data["marginright"] kw["top"] = data["margintop"] grafico.plot.fig.subplots_adjust(**kw) # Load config conf = {} conf["method"] = data["method"] conf["fluid"] = data["fluid"] conf["eq"] = data["eq"] conf["visco"] = data["visco"] conf["thermal"] = data["thermal"] grafico.config = conf grafico.changeStatusThermo(conf) return grafico
[docs] def changeStatusThermo(self, conf): fluid = getClassFluid(conf["method"], conf["fluid"]) txt = "%s (%s)" % (fluid.name, conf["method"]) self.statusThermo.setText(txt)
[docs] class Plot2D(QtWidgets.QDialog): """Dialog for select a special 2D plot"""
[docs] def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle(self.tr("Setup 2D Plot")) layout = QtWidgets.QVBoxLayout(self) group_Ejex = QtWidgets.QGroupBox(self.tr("Axis X")) layout.addWidget(group_Ejex) layout_GroupX = QtWidgets.QVBoxLayout(group_Ejex) self.ejeX = QtWidgets.QComboBox() layout_GroupX.addWidget(self.ejeX) self.Xscale = QtWidgets.QCheckBox(self.tr("Logarithmic scale")) layout_GroupX.addWidget(self.Xscale) for prop in ThermoAdvanced.propertiesName(): self.ejeX.addItem(prop) group_Ejey = QtWidgets.QGroupBox(self.tr("Axis Y")) layout.addWidget(group_Ejey) layout_GroupY = QtWidgets.QVBoxLayout(group_Ejey) self.ejeY = QtWidgets.QComboBox() layout_GroupY.addWidget(self.ejeY) self.Yscale = QtWidgets.QCheckBox(self.tr("Logarithmic scale")) layout_GroupY.addWidget(self.Yscale) self.buttonBox = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) layout.addWidget(self.buttonBox) self.ejeXChanged(0) self.ejeX.currentIndexChanged.connect(self.ejeXChanged)
[docs] def ejeXChanged(self, index): """Fill variables available in ejeY, all except the active in ejeX""" # Save current status to restore current = self.ejeY.currentIndex() if current == -1: current = 0 # Refill ejeY combo self.ejeY.clear() props = ThermoAdvanced.propertiesName() del props[index] for prop in props: self.ejeY.addItem(prop) # Restore inicial state if index == 0 and current == 0: self.ejeY.setCurrentIndex(0) elif index <= current: self.ejeY.setCurrentIndex(current) else: self.ejeY.setCurrentIndex(current+1)
[docs] class Plot3D(QtWidgets.QDialog): """Dialog for configure a 3D plot"""
[docs] def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle(self.tr("Setup 3D Plot")) layout = QtWidgets.QGridLayout(self) layout.addWidget(QtWidgets.QLabel(self.tr("Axis X")), 1, 1) self.ejeX = QtWidgets.QComboBox() for prop in ThermoAdvanced.propertiesName(): self.ejeX.addItem(prop) layout.addWidget(self.ejeX, 1, 2) layout.addWidget(QtWidgets.QLabel(self.tr("Axis Y")), 2, 1) self.ejeY = QtWidgets.QComboBox() layout.addWidget(self.ejeY, 2, 2) layout.addWidget(QtWidgets.QLabel(self.tr("Axis Z")), 3, 1) self.ejeZ = QtWidgets.QComboBox() layout.addWidget(self.ejeZ, 3, 2) layout.addItem(QtWidgets.QSpacerItem( 10, 10, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed), 4, 3) self.checkMesh = QtWidgets.QCheckBox(self.tr("Draw 3D mesh")) layout.addWidget(self.checkMesh, 5, 1, 1, 2) layout.addWidget(QtWidgets.QLabel(self.tr("Mesh type")), 6, 1) self.typeMesh = QtWidgets.QComboBox() self.typeMesh.addItem("Surface") self.typeMesh.addItem("Wireframe") layout.addWidget(self.typeMesh, 6, 2) layout.addItem(QtWidgets.QSpacerItem( 10, 10, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding), 7, 3) self.buttonBox = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) layout.addWidget(self.buttonBox, 10, 1, 1, 3) # Initialize widget with global preferences values self.checkMesh.setChecked( config.Preferences.getboolean("MEOS", "3DMesh")) self.typeMesh.setEnabled( config.Preferences.getboolean("MEOS", "3DMesh")) self.typeMesh.setCurrentIndex( config.Preferences.getint("MEOS", "3Dtype")) self.checkMesh.stateChanged.connect(self.typeMesh.setEnabled) self.ejeX.currentIndexChanged.connect(self.ejeXChanged) self.ejeY.currentIndexChanged.connect(self.ejeYChanged) self.ejeXChanged(0)
[docs] def ejeXChanged(self, index): """Fill variables available in ejeY, all except the active in ejeX""" # Save current status to restore current = self.ejeY.currentIndex() if current == -1: current = 0 # Refill ejeY combo self.ejeY.clear() props = ThermoAdvanced.propertiesName() del props[index] for prop in props: self.ejeY.addItem(prop) # Restore inicial state if index == 0 and current == 0: self.ejeY.setCurrentIndex(0) elif index <= current: self.ejeY.setCurrentIndex(current) else: self.ejeY.setCurrentIndex(current+1)
[docs] def ejeYChanged(self, indY): """Fill variables available in ejeZ, all except the actives in other""" # Save current status to restore current = self.ejeZ.currentIndex() if current == -1: current = 0 # Refill ejeY combo self.ejeZ.clear() prop2 = ThermoAdvanced.propertiesName()[:] indX = self.ejeX.currentIndex() del prop2[indX] del prop2[indY] for prop in prop2: self.ejeZ.addItem(prop) # Restore inicial state if indX == 0 and indY == 0 and current == 0: self.ejeZ.setCurrentIndex(0) elif indY <= current or indX <= current: self.ejeZ.setCurrentIndex(current) else: self.ejeZ.setCurrentIndex(current+1)
[docs] class EditPlot(QtWidgets.QDialog): """Dialog to edit plot. This dialog let user change plot properties"""
[docs] def __init__(self, plotMEoS, parent=None): super().__init__(parent) self.setWindowTitle(self.tr("Edit Plot")) layout = QtWidgets.QGridLayout(self) self.plotMEoS = plotMEoS self.fig = plotMEoS.plot self.parent = parent self.semaforo = QtCore.QSemaphore(1) self.lista = QtWidgets.QListWidget() layout.addWidget(self.lista, 0, 1, 1, 3) lytTitle = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(self.tr("Label")) lytTitle.addWidget(label) self.label = QtWidgets.QLineEdit() lytTitle.addWidget(self.label) layout.addLayout(lytTitle, 1, 1, 1, 3) layout.addWidget(QtWidgets.QLabel(self.tr("Line Width")), 2, 1) layout.addWidget(QtWidgets.QLabel(self.tr("Line Style")), 2, 2) layout.addWidget(QtWidgets.QLabel(self.tr("Color")), 2, 3) self.Grosor = QtWidgets.QDoubleSpinBox() self.Grosor.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) self.Grosor.setRange(0.1, 5) self.Grosor.setDecimals(1) self.Grosor.setSingleStep(0.1) layout.addWidget(self.Grosor, 3, 1) self.Linea = LineStyleCombo() layout.addWidget(self.Linea, 3, 2) self.ColorButton = ColorSelector() layout.addWidget(self.ColorButton, 3, 3) layout.addWidget(QtWidgets.QLabel(self.tr("Marker")), 4, 1) layout.addWidget(QtWidgets.QLabel(self.tr("Marker Size")), 4, 2) layout.addWidget(QtWidgets.QLabel(self.tr("Marker Color")), 4, 3) self.Marca = MarkerCombo() layout.addWidget(self.Marca, 5, 1) self.markerSize = QtWidgets.QDoubleSpinBox() self.markerSize.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) self.markerSize.setDecimals(1) self.markerSize.setSingleStep(0.1) layout.addWidget(self.markerSize, 5, 2) self.markerfacecolor = ColorSelector() layout.addWidget(self.markerfacecolor, 5, 3) layout.addWidget(QtWidgets.QLabel(self.tr("Marker edge")), 7, 1) layout.addWidget(QtWidgets.QLabel(self.tr("Width")), 6, 2) layout.addWidget(QtWidgets.QLabel(self.tr("Color")), 6, 3) self.markerEdgeSize = QtWidgets.QDoubleSpinBox() self.markerEdgeSize.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) self.markerEdgeSize.setDecimals(1) self.markerEdgeSize.setSingleStep(0.1) layout.addWidget(self.markerEdgeSize, 7, 2) self.markeredgecolor = ColorSelector() layout.addWidget(self.markeredgecolor, 7, 3) grpAnnotate = QtWidgets.QGroupBox(self.tr("Annotation")) layout.addWidget(grpAnnotate, 8, 1, 1, 3) lytAnnotation = QtWidgets.QGridLayout(grpAnnotate) self.annotationVisible = QtWidgets.QCheckBox(self.tr("Visible")) lytAnnotation.addWidget(self.annotationVisible, 1, 1, 1, 3) lytTitle = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(self.tr("Label")) lytTitle.addWidget(label) self.annotationLabel = InputFont() lytTitle.addWidget(self.annotationLabel) lytAnnotation.addLayout(lytTitle, 2, 1, 1, 3) lytPosition = QtWidgets.QHBoxLayout() lytPosition.addWidget(QtWidgets.QLabel(self.tr("Location"))) self.labelAnnotationPos = Entrada_con_unidades( int, value=50, width=40, frame=False, readOnly=True, suffix="%", showNull=True) self.labelAnnotationPos.setFixedWidth(40) lytPosition.addWidget(self.labelAnnotationPos) self.annotationPos = QtWidgets.QSlider( QtCore.Qt.Orientation.Horizontal) self.annotationPos.setRange(0, 100) self.annotationPos.setValue(50) self.annotationPos.valueChanged.connect( partial(self._updateLabel, self.labelAnnotationPos)) lytPosition.addWidget(self.annotationPos) lytAnnotation.addLayout(lytPosition, 3, 1, 1, 3) lytAngle = QtWidgets.QHBoxLayout() lytAngle.addWidget(QtWidgets.QLabel(self.tr("Rotation"))) self.labelAnnotationRot = Entrada_con_unidades( int, value=50, width=40, frame=False, readOnly=True, suffix="º", showNull=True) self.labelAnnotationRot.setFixedWidth(40) lytAngle.addWidget(self.labelAnnotationRot) self.annotationRot = QtWidgets.QSlider( QtCore.Qt.Orientation.Horizontal) self.annotationRot.setRange(0, 360) self.annotationRot.setValue(0) self.annotationRot.valueChanged.connect( partial(self._updateLabel, self.labelAnnotationRot)) lytAngle.addWidget(self.annotationRot) lytAnnotation.addLayout(lytAngle, 4, 1, 1, 3) lytVA = QtWidgets.QHBoxLayout() lytVA.addWidget(QtWidgets.QLabel(self.tr("Aligment"))) self.annotationVA = QtWidgets.QComboBox() alignment = [ self.tr("Center"), self.tr("Top"), self.tr("Bottom"), self.tr("Baseline"), self.tr("Center baseline")] for alig in alignment: self.annotationVA.addItem(alig) lytVA.addWidget(self.annotationVA) lytVA.addItem(QtWidgets.QSpacerItem( 10, 10, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)) lytAnnotation.addLayout(lytVA, 5, 1, 1, 3) self.annotationVisible.stateChanged.connect( self.annotationLabel.setEnabled) self.annotationVisible.stateChanged.connect( self.annotationPos.setEnabled) self.annotationVisible.stateChanged.connect( self.annotationRot.setEnabled) self.visible = QtWidgets.QCheckBox(self.tr("Visible")) layout.addWidget(self.visible, 13, 1, 1, 3) self.antialiases = QtWidgets.QCheckBox(self.tr("Antialiases")) layout.addWidget(self.antialiases, 14, 1, 1, 3) layoutButton = QtWidgets.QHBoxLayout() layout.addLayout(layoutButton, 15, 1, 1, 3) self.botonAdd = QtWidgets.QPushButton(QtGui.QIcon(QtGui.QPixmap( os.environ["pychemqt"] + "/images/button/add.png")), "") self.botonAdd.clicked.connect(self.add) layoutButton.addWidget(self.botonAdd) self.botonRemove = QtWidgets.QPushButton(QtGui.QIcon(QtGui.QPixmap( os.environ["pychemqt"] + "/images/button/remove.png")), "") self.botonRemove.clicked.connect(self.remove) layoutButton.addWidget(self.botonRemove) self.buttonBox = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Close) self.buttonBox.rejected.connect(self.close) layoutButton.addWidget(self.buttonBox) for linea in self.fig.ax.lines[2:]: self.lista.addItem(linea._label) self.lista.currentRowChanged.connect(self.update) self.lista.setCurrentRow(0) self.label.textChanged.connect(partial(self.changeValue, "label")) self.Grosor.valueChanged.connect(partial(self.changeValue, "lw")) self.Linea.valueChanged.connect(partial(self.changeValue, "ls")) self.Linea.currentIndexChanged.connect(self.ColorButton.setEnabled) self.ColorButton.valueChanged.connect( partial(self.changeValue, "color")) self.Marca.valueChanged.connect(partial(self.changeValue, "marker")) self.Marca.currentIndexChanged.connect(self.markerSize.setEnabled) self.Marca.currentIndexChanged.connect(self.markerfacecolor.setEnabled) self.Marca.currentIndexChanged.connect(self.markerEdgeSize.setEnabled) self.Marca.currentIndexChanged.connect(self.markeredgecolor.setEnabled) self.markerSize.valueChanged.connect(partial(self.changeValue, "ms")) self.markerfacecolor.valueChanged.connect( partial(self.changeValue, "mfc")) self.markerEdgeSize.valueChanged.connect( partial(self.changeValue, "mew")) self.markeredgecolor.valueChanged.connect( partial(self.changeValue, "mec")) self.visible.toggled.connect(partial(self.changeValue, "visible")) self.antialiases.toggled.connect( partial(self.changeValue, "antialiases")) self.annotationVisible.toggled.connect( partial(self.changeValue, "textVisible")) self.annotationLabel.textChanged.connect( partial(self.changeValue, "textLabel")) self.annotationLabel.colorChanged.connect( partial(self.changeValue, "textcolor")) self.annotationLabel.fontChanged.connect( partial(self.changeValue, "textfont")) self.annotationPos.valueChanged.connect( partial(self.changeValue, "textPos")) self.annotationRot.valueChanged.connect( partial(self.changeValue, "textRot")) self.annotationVA.currentIndexChanged.connect( partial(self.changeValue, "textVA"))
[docs] @staticmethod def _updateLabel(label, value): label.setValue(value)
[docs] def update(self, i): """Fill format widget with value of selected line""" if self.semaforo.available() > 0: self.semaforo.acquire(1) line = self.fig.ax.lines[i+2] self.label.setText(line.get_label()) self.Grosor.setValue(line.get_lw()) self.Linea.setCurrentValue(line.get_ls()) self.ColorButton.setColor(line.get_color()) self.Marca.setCurrentValue(line.get_marker()) self.markerSize.setValue(line.get_ms()) self.markerfacecolor.setColor(line.get_mfc()) self.markerEdgeSize.setValue(line.get_mew()) self.markeredgecolor.setColor(line.get_mec()) self.visible.setChecked(line.get_visible()) self.antialiases.setChecked(line.get_antialiased()) try: self.annotationVisible.setChecked(line.text.get_visible()) self.annotationLabel.setText(line.text.get_text()) self.annotationPos.setValue(line.text.pos) self.annotationRot.setValue(line.text.get_rotation()) va = ["center", "top", "bottom", "baseline", "center_baseline"] self.annotationVA.setCurrentIndex(va.index(line.text.get_va())) except AttributeError: self.annotationVisible.setChecked(False) self.semaforo.release(1)
[docs] def changeValue(self, key, value): """Update plot data""" if self.semaforo.available() > 0: self.semaforo.acquire(1) linea = self.fig.ax.lines[self.lista.currentRow()+2] func = {"label": linea.set_label, "lw": linea.set_lw, "ls": linea.set_ls, "marker": linea.set_marker, "color": linea.set_color, "ms": linea.set_ms, "mfc": linea.set_mfc, "mew": linea.set_mew, "mec": linea.set_mec, "visible": linea.set_visible, "antialiases": linea.set_antialiased, "textVisible": linea.text.set_visible, "textLabel": linea.text.set_text, "textcolor": linea.text.set_color, "textfont": linea.text.set_fontproperties, "textPos": linea.text.set_position, "textRot": linea.text.set_rotation, "textVA": linea.text.set_va} if key == "textPos": linea.text.pos = value xi = linea.get_xdata() yi = linea.get_ydata() i = int(len(xi)*value/100) if i >= len(xi): i = len(yi)-1 value = xi[i], yi[i] elif key == "textVA": va = ["center", "top", "bottom", "baseline", "center_baseline"] value = va[value] elif key == "textfont": value = convertFont(value) elif key in ("ls", "marker", "color", "mfc", "mec"): value = str(value) func[key](value) if key == "label": self.lista.currentItem().setText(value) else: self.fig.draw() self.parent.dirty[self.parent.idTab] = True self.parent.saveControl() self.semaforo.release(1)
[docs] def add(self): """Add a isoline to plot""" dialog = AddLine() if dialog.exec(): method = getMethod() projectConfig = self.parent.currentConfig points = get_points(config.Preferences) self.parent.progressBar.setVisible(True) index = projectConfig.getint("MEoS", "fluid") fluid = getClassFluid(method, index) prop = dialog.tipo.currentIndex() value = dialog.input[prop].value Tmin, Tmax, Pmin, Pmax = getLimit(fluid, projectConfig) Pmax = Pmax*1000 T = concatenate([ linspace(Tmin, 0.9*fluid.Tc, points), linspace(0.9*fluid.Tc, 0.99*fluid.Tc, points), linspace(0.99*fluid.Tc, fluid.Tc, points), linspace(fluid.Tc, 1.01*fluid.Tc, points), linspace(1.01*fluid.Tc, 1.1*fluid.Tc, points), linspace(1.1*fluid.Tc, Tmax, points)]).tolist() P = concatenate([ logspace(log10(Pmin), log10(0.9*fluid.Pc), points), linspace(0.9*fluid.Pc, 0.99*fluid.Pc, points), linspace(0.99*fluid.Pc, fluid.Pc, points), linspace(fluid.Pc, 1.01*fluid.Pc, points), linspace(1.01*fluid.Pc, 1.1*fluid.Pc, points), logspace(log10(1.1*fluid.Pc), log10(Pmax), points)]).tolist() for i in range(5, 0, -1): del T[points*i] del P[points*i] if prop == 0: # Calcualte isotherm line self.parent.statusBar().showMessage( self.tr("Adding isotherm line...")) fluidos = calcIsoline( fluid, self.parent.currentConfig, "P", "T", P, value, 0, 0, 100, 1, self.parent.progressBar) var = "T" name = "Isotherm" unit = unidades.Temperature elif prop == 1: # Calculate isobar line self.parent.statusBar().showMessage( self.tr("Adding isobar line...")) fluidos = calcIsoline( fluid, self.parent.currentConfig, "T", "P", T, value, 0, 0, 100, 1, self.parent.progressBar) var = "P" name = "Isobar" unit = unidades.Pressure elif prop == 2: # Calculate isoenthalpic line self.parent.statusBar().showMessage( self.tr("Adding isoenthalpic line...")) fluidos = calcIsoline( fluid, self.parent.currentConfig, "P", "h", P, value, 0, 0, 100, 1, self.parent.progressBar) var = "h" name = "Isoenthalpic" unit = unidades.Enthalpy elif prop == 3: # Calculate isoentropic line self.parent.statusBar().showMessage( self.tr("Adding isoentropic line...")) fluidos = calcIsoline( fluid, self.parent.currentConfig, "T", "s", T, value, 0, 0, 100, 1, self.parent.progressBar) var = "s" name = "Isoentropic" unit = unidades.SpecificHeat elif prop == 4: # Calculate isochor line self.parent.statusBar().showMessage( self.tr("Adding isochor line...")) fluidos = calcIsoline( fluid, self.parent.currentConfig, "T", "v", T, value, 0, 0, 100, 1, self.parent.progressBar) var = "v" name = "Isochor" unit = unidades.SpecificVolume elif prop == 5: # Calculate isodensity line self.parent.statusBar().showMessage( self.tr("Adding isodensity line...")) fluidos = calcIsoline( fluid, self.parent.currentConfig, "T", "rho", T, value, 0, 0, 100, 1, self.parent.progressBar) var = "rho" name = "Isochor" unit = unidades.Density elif prop == 6: # Calculate isoquality line self.parent.statusBar().showMessage( self.tr("Adding isoquality line...")) T = T[:3*points-2] fluidos = calcIsoline( fluid, self.parent.currentConfig, "T", "x", T, value, 0, 0, 100, 1, self.parent.progressBar) var = "x" name = "Isoquality" unit = unidades.Dimensionless line = {value: {}} for x in ThermoAdvanced.propertiesKey(): dat_propiedad = [] for fluido in fluidos: num = getattr(fluido, x) if isinstance(num, str): dat_propiedad.append(num) elif x in ("f", "fi"): dat_propiedad.append(num[0]) elif num is not None: dat_propiedad.append(num._data) else: dat_propiedad.append(None) line[value][x] = dat_propiedad style = getLineFormat(config.Preferences, name) functionx = _getunitTransform(self.plotMEoS.x) functiony = _getunitTransform(self.plotMEoS.y) functionz = _getunitTransform(self.plotMEoS.z) transform = (functionx, functiony, functionz) ax = self.plotMEoS.x, self.plotMEoS.y, self.plotMEoS.z plotIsoline(line, ax, var, unit, self.plotMEoS, transform, **style) self.plotMEoS.plot.draw() self.parent.progressBar.setVisible(False) self.parent.dirty[self.parent.idTab] = True self.parent.saveControl() self.lista.addItem(self.fig.ax.lines[-1].get_label()) self.lista.setCurrentRow(self.lista.count()-1) # Save new line to file data = self.plotMEoS._getData() if var not in data: data[var] = {} data[var][value] = line[value] self.plotMEoS._saveData(data)
[docs] def remove(self): """Remove a line from plot""" self.parent.statusBar().showMessage(self.tr("Deleting line...")) QtWidgets.QApplication.processEvents() # Remove data from file data = self.plotMEoS._getData() txt = self.lista.currentItem().text().split() var = txt[0] units = {"T": unidades.Temperature, "P": unidades.Pressure, "v": unidades.SpecificVolume, "rho": unidades.Density, "h": unidades.Enthalpy, "s": unidades.SpecificHeat, "x": unidades.Dimensionless} if var in units: unit = units[var] for key in data[var]: string = unit(key).str if string[1:] == " ".join(txt[2:]): del data[var][key] self.plotMEoS._saveData(data) break # Remove line to plot and update list element index = self.lista.currentRow() del self.fig.ax.lines[index+2] if index == 0: self.lista.setCurrentRow(1) else: self.lista.setCurrentRow(index-1) self.lista.takeItem(index) self.fig.draw() self.parent.statusBar().clearMessage() self.parent.dirty[self.parent.idTab] = True self.parent.saveControl()
[docs] class AddLine(QtWidgets.QDialog): """Dialog to add new isoline to plot""" lineas = [(translate("UI_Tables", "Isotherm"), unidades.Temperature, None), (translate("UI_Tables", "Isobar"), unidades.Pressure, None), (translate("UI_Tables", "Isoenthalpic"), unidades.Enthalpy, None), (translate("UI_Tables", "Isoentropic"), unidades.SpecificHeat, "SpecificEntropy"), (translate("UI_Tables", "Isochor"), unidades.SpecificVolume, None), (translate("UI_Tables", "Isodensity"), unidades.Density, None), (translate("UI_Tables", "Isoquality"), float, None)]
[docs] def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle(self.tr("Add Line to Plot")) layout = QtWidgets.QGridLayout(self) self.tipo = QtWidgets.QComboBox() layout.addWidget(self.tipo, 1, 1, 1, 2) layout.addWidget(QtWidgets.QLabel(self.tr("Value")), 2, 1) self.input = [] for title, unidad, magnitud in self.lineas: self.input.append(Entrada_con_unidades(unidad, magnitud)) layout.addWidget(self.input[-1], 2, 2) self.tipo.addItem(title) self.buttonBox = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) layout.addWidget(self.buttonBox, 10, 1, 1, 2) self.isolineaChanged(0) self.tipo.currentIndexChanged.connect(self.isolineaChanged)
[docs] def isolineaChanged(self, key): """Let show only the active inputs""" for i in self.input: i.setVisible(False) self.input[key].setVisible(True)
[docs] class EditAxis(QtWidgets.QDialog): """Dialog to configure axes plot properties, label, margins, scales"""
[docs] def __init__(self, fig=None, parent=None): super().__init__(parent) self.setWindowTitle(self.tr("Edit Axis")) layout = QtWidgets.QGridLayout(self) self.fig = fig lytTitle = QtWidgets.QHBoxLayout() lb = QtWidgets.QLabel(self.tr("Title")) lb.setSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Maximum) lytTitle.addWidget(lb) self.title = InputFont() lytTitle.addWidget(self.title) layout.addLayout(lytTitle, 1, 1, 1, self.fig.dim) self.axisX = AxisWidget("x", self) layout.addWidget(self.axisX, 2, 1) self.axisY = AxisWidget("y", self) layout.addWidget(self.axisY, 2, 2) if self.fig.dim == 3: self.axisZ = AxisWidget("z", self) layout.addWidget(self.axisZ, 2, 3) self.axisX.scale.setEnabled(False) self.axisY.scale.setEnabled(False) self.axisZ.scale.setEnabled(False) self.gridCheckbox = QtWidgets.QCheckBox(self.tr("Show Grid")) layout.addWidget(self.gridCheckbox, 3, 1, 1, self.fig.dim) layout.addItem(QtWidgets.QSpacerItem( 10, 10, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding), 5, 1, 1, self.fig.dim) self.buttonBox = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Close) self.buttonBox.rejected.connect(self.reject) layout.addWidget(self.buttonBox, 10, 1, 1, self.fig.dim) if fig: self.populate() self.title.textChanged.connect(partial(self.update, "title")) self.title.colorChanged.connect(partial(self.update, "titlecolor")) self.title.fontChanged.connect(partial(self.update, "titlefont")) self.axisX.label.textChanged.connect(partial(self.update, "xlabel")) self.axisX.label.colorChanged.connect( partial(self.update, "xlabelcolor")) self.axisX.label.fontChanged.connect( partial(self.update, "xlabelfont")) self.axisY.label.textChanged.connect(partial(self.update, "ylabel")) self.axisY.label.colorChanged.connect( partial(self.update, "ylabelcolor")) self.axisY.label.fontChanged.connect( partial(self.update, "ylabelfont")) self.gridCheckbox.toggled.connect(partial(self.update, "grid")) self.axisX.scale.toggled.connect(partial(self.update, "xscale")) self.axisY.scale.toggled.connect(partial(self.update, "yscale")) self.axisX.min.valueChanged.connect(partial(self.update, "xmin")) self.axisY.min.valueChanged.connect(partial(self.update, "ymin")) self.axisX.max.valueChanged.connect(partial(self.update, "xmax")) self.axisY.max.valueChanged.connect(partial(self.update, "ymax")) if self.fig.dim == 3: self.axisZ.label.textChanged.connect( partial(self.update, "zlabel")) self.axisZ.label.colorChanged.connect( partial(self.update, "zlabelcolor")) self.axisZ.label.fontChanged.connect( partial(self.update, "zlabelfont")) self.axisZ.min.valueChanged.connect(partial(self.update, "zmin")) self.axisZ.max.valueChanged.connect(partial(self.update, "zmax"))
[docs] def populate(self): """Fill widget with plot parameters""" self.title.setText(self.fig.ax.get_title()) self.title.setColor(QtGui.QColor(self.fig.ax.title.get_color())) self.axisX.label.setText(self.fig.ax.get_xlabel()) xcolor = self.fig.ax.xaxis.get_label().get_color() self.axisX.label.setColor(QtGui.QColor(xcolor)) self.axisY.label.setText(self.fig.ax.get_ylabel()) ycolor = self.fig.ax.yaxis.get_label().get_color() self.axisY.label.setColor(QtGui.QColor(ycolor)) self.gridCheckbox.setChecked(self.fig.ax._gridOn) self.axisX.scale.setChecked(self.fig.ax.get_xscale() == "log") self.axisY.scale.setChecked(self.fig.ax.get_yscale() == "log") xmin, xmax = self.fig.ax.get_xlim() self.axisX.min.setValue(xmin) self.axisX.max.setValue(xmax) ymin, ymax = self.fig.ax.get_ylim() self.axisY.min.setValue(ymin) self.axisY.max.setValue(ymax) if self.fig.dim == 3: self.axisZ.label.setText(self.fig.ax.get_zlabel()) zcolor = self.fig.ax.zaxis.get_label().get_color() self.axisZ.label.setColor(QtGui.QColor(zcolor)) zmin, zmax = self.fig.ax.get_zlim() self.axisZ.min.setValue(zmin) self.axisZ.max.setValue(zmax)
[docs] def update(self, key, value): """Update plot Input: key: plot parameter key to update value: new value for key """ f = {"xlabel": self.fig.ax.set_xlabel, "xlabelcolor": self.fig.ax.xaxis.get_label().set_color, "xlabelfont": self.fig.ax.xaxis.get_label().set_fontproperties, "ylabel": self.fig.ax.set_ylabel, "ylabelcolor": self.fig.ax.yaxis.get_label().set_color, "ylabelfont": self.fig.ax.yaxis.get_label().set_fontproperties, "title": self.fig.ax.set_title, "titlecolor": self.fig.ax.title.set_color, "titlefont": self.fig.ax.title.set_fontproperties, "xscale": self.fig.ax.set_xscale, "yscale": self.fig.ax.set_yscale, "grid": self.fig.ax.grid} if self.fig.dim == 3: f["zlabel"] = self.fig.ax.set_zlabel f["zlabelcolor"] = self.fig.ax.zaxis.get_label().set_color f["zlabelfont"] = self.fig.ax.zaxis.get_label().set_fontproperties if key in ("xscale", "yscale"): if value: value = "log" else: value = "linear" if key == "grid": self.fig.ax._gridOn = value if key in ("titlecolor", "xlabelcolor", "ylabelcolor"): value = str(value) if key in ("titlefont", "xlabelfont", "ylabelfont"): value = convertFont(value) if key in ("xmin", "xmax"): xmin = self.axisX.min.value xmax = self.axisX.max.value self.fig.ax.set_xlim(xmin, xmax) elif key in ("ymin", "ymax"): ymin = self.axisY.min.value ymax = self.axisY.max.value self.fig.ax.set_ylim(ymin, ymax) elif key in ("zmin", "zmax"): ymin = self.axisZ.min.value ymax = self.axisZ.max.value self.fig.ax.set_zlim(ymin, ymax) else: f[key](value) self.parent().dirty[self.parent().idTab] = True self.parent().saveControl() self.fig.draw()
[docs] def convertFont(qfont): """Convert qt QFont class properties to FontProperties to use in matplotlib Parameters ---------- qfont : QFont QFont with properties to extract Returns ------- font : FontProperties FontProperties instance to use in any matplotlib text instance """ family = str(qfont.family()) # Matplotlib use 0-1000 scale, Qt only 0-100 scale weight = 10*qfont.weight() if qfont.style() == 0: style = "normal" elif qfont.style() == 1: style = "italic" elif qfont.style() == 2: style = "oblique" else: style = None # print(family, style, qfont.stretch(), weight, qfont.pointSize()) font = FontProperties(family, style, None, qfont.stretch(), weight, qfont.pointSize()) return font
[docs] class AxisWidget(QtWidgets.QGroupBox): """Dialog to configure axes plot properties"""
[docs] def __init__(self, name, parent=None): title = name+" "+self.tr("Axis") super().__init__(title, parent) lyt = QtWidgets.QGridLayout(self) lyt.addWidget(QtWidgets.QLabel(self.tr("Label")), 1, 1) self.label = InputFont() lyt.addWidget(self.label, 1, 2) self.scale = QtWidgets.QCheckBox(self.tr("Logarithmic scale")) lyt.addWidget(self.scale, 2, 1, 1, 2) lyt.addWidget(QtWidgets.QLabel(self.tr("from")), 3, 1) self.min = Entrada_con_unidades(float, min=float("-inf")) lyt.addWidget(self.min, 3, 2) lyt.addWidget(QtWidgets.QLabel(self.tr("to")), 4, 1) self.max = Entrada_con_unidades(float, min=float("-inf")) lyt.addWidget(self.max, 4, 2)
[docs] def calcIsoline(f, conf, var, fix, vvar, vfix, ini, step, end, total, bar): """Procedure to calculate isoline. In isotherm and isobar add to calculate point the saturated states in two-phases region""" fluidos = [] fail = 0 N_points = get_points(config.Preferences) fase = None rhoo = 0 To = 0 for Ti in vvar: kwargs = {var: Ti, fix: vfix, "rho0": rhoo, "T0": To} print(kwargs) fluido = calcPoint(f, conf, **kwargs) avance = ini + end*step/total + \ end/total*(len(fluidos)+fail)/(len(vvar)+N_points) bar.setValue(avance) QtWidgets.QApplication.processEvents() if fluido and fluido.status and (fluido.rho != rhoo or fluido.T != To): fluidos.append(fluido) # Save values of last point as initial guess for next calculation if var not in ("T", "P") or fix not in ("T", "P"): rhoo = fluido.rho To = fluido.T if var in ("T", "P") and fix in ("T", "P"): if fase is None: fase = fluido.x if fase == fluido.x: continue print("Calculating two phase additional point") if fluido.P < f.Pc and fluido.T < f.Tc: if fase != fluido.x and fase <= 0: xi = linspace(0, 1, N_points) elif fase != fluido.x and fase >= 1: xi = linspace(1, 0, N_points) for x in xi: print({fix: vfix, "x": x}) fluido_x = calcPoint(f, conf, **{fix: vfix, "x": x}) fluidos.insert(-1, fluido_x) avance = ini + end*step/total + end/total * \ (len(fluidos)+fail)/(len(vvar)+N_points) bar.setValue(avance) fase = fluido.x else: fail += 1 return fluidos
[docs] def calcMesh(f, conf, Ti, Pi): """Calculate mesh data for a 3D plot""" fluids = [] for T in Ti: fluids_i = [] for P in Pi: kwargs = {"T": T, "P": P} print(kwargs) QtWidgets.QApplication.processEvents() fluid = calcPoint(f, conf, **kwargs) if fluid and fluid.status: fluids_i.append(fluid) else: fluids_i.append(None) fluids.append(fluids_i) return fluids
[docs] def get_points(Preferences): """Get point number to plot lines from Preferences""" definition = Preferences.getint("MEOS", "definition") if definition == 1: points = 10 elif definition == 2: points = 25 elif definition == 3: points = 50 elif definition == 4: points = 100 else: points = 5 return points
[docs] def getLineFormat(Preferences, name): """get matplotlib line format from preferences Preferences: configparser instance with pycheqmt preferences name: name of isoline""" fmt = formatLine(Preferences, "MEOS", name) # Anotation if name != "saturation": fmt["annotate"] = Preferences.getboolean("MEOS", name+"label") fmt["pos"] = Preferences.getint("MEOS", name+"position") fmt["unit"] = Preferences.getboolean("MEOS", name+"units") fmt["variable"] = Preferences.getboolean("MEOS", name+"variable") return fmt
[docs] def plotIsoline(data, axis, title, unidad, grafico, transform, **fmt): """Procedure to plot any isoline Input: data: section of property isoline of matrix data axis: array with keys of three axis, z None in 2D plot title: key of isoline type unidad: unidades subclass with isoline unit grafico: PlotMEoS instance to plot data transform: unit transform function for use configurated units in plots fmt: any matplotlib plot kwargs """ x, y, z = axis fx, fy, fz = transform xscale = grafico.plot.ax.get_xscale() yscale = grafico.plot.ax.get_yscale() annotate = fmt.pop("annotate") pos = fmt.pop("pos") unit = fmt.pop("unit") variable = fmt.pop("variable") for key in sorted(data.keys()): xi = list(map(fx, data[key][x])) yi = list(map(fy, data[key][y])) label = f"{title} ={unidad(key).str}" if z: zi = list(map(fz, data[key][z])) line, = grafico.plot.ax.plot(xi, yi, zi, label=label, **fmt) else: line, = grafico.plot.ax.plot(xi, yi, label=label, **fmt) # Add annotate for isolines if not z: if variable and unit: txt = label elif variable: txt = f"{title} ={unidad(key).config()}" elif unit: txt = unidad(key).str else: txt = unidad(key).config() xmin, xmax = grafico.plot.ax.get_xlim() ymin, ymax = grafico.plot.ax.get_ylim() i = int(len(xi)*pos/100) if i >= len(xi): i = len(yi)-2 if pos > 50: j = i-1 else: j = i+1 if xscale == "log": f_x = (log(xi[i])-log(xi[j]))/(log(xmax)-log(xmin)) else: f_x = (xi[i]-xi[j])/(xmax-xmin) if yscale == "log": f_y = (log(yi[i])-log(yi[j]))/(log(ymax)-log(ymin)) else: f_y = (yi[i]-yi[j])/(ymax-ymin) rot = atan(f_y/f_x)*360/2/pi kw = {} kw["ha"] = "center" kw["va"] = "center_baseline" kw["rotation_mode"] = "anchor" kw["rotation"] = rot kw["size"] = "small" text = grafico.plot.ax.text(xi[i], yi[i], txt, **kw) line.text = text line.text.pos = pos if not annotate: text.set_visible(False)
[docs] def plot2D3D(grafico, data, Preferences, x, y, z=None, mesh=False, typemesh=0): """Plot procedure Parameters: grafico: plot data: data to plot Preferences: ConfigParser instance from mainwindow preferencesChanged x: Key for x axis y: Key for y axis z: Key for z axis Optional for 3D plot """ functionx = _getunitTransform(x) functiony = _getunitTransform(y) functionz = _getunitTransform(z) transform = (functionx, functiony, functionz) # Plot saturation lines fmt = getLineFormat(Preferences, "saturation") if x == "P" and y == "T": satLines = (translate("Saturation Line"),) else: satLines = [ translate("Liquid Saturation Line"), translate("Vapor Saturation Line")] for fase, label in enumerate(satLines): xsat = list(map(functionx, data["saturation_{fase}"][x])) ysat = list(map(functiony, data["saturation_{fase}"][y])) if z: zsat = list(map(functionz, data[f"saturation_{fase}"][z])) grafico.plot.ax.plot(xsat, ysat, zsat, label=label, **fmt) else: grafico.plot.ax.plot(xsat, ysat, label=label, **fmt) # Plot melting and sublimation lines if "melting" in data: label = translate("Melting Line") xmel = list(map(functionx, data["melting"][x])) ymel = list(map(functiony, data["melting"][y])) if z: zmel = list(map(functionz, data["melting"][z])) grafico.plot.ax.plot(xmel, ymel, zmel, label=label, **fmt) else: grafico.plot.ax.plot(xmel, ymel, label=label, **fmt) if "sublimation" in data: xsub = list(map(functionx, data["sublimation"][x])) ysub = list(map(functiony, data["sublimation"][y])) label = translate("Sublimation Line") if z: zmel = list(map(functionz, data["melting"][z])) grafico.plot.ax.plot(xmel, ymel, zmel, label=label, **fmt) else: grafico.plot.ax.plot(xsub, ysub, label=label, **fmt) # Plot quality isolines if x not in ["P", "T"] or y not in ["P", "T"] or z: fmt = getLineFormat(Preferences, "Isoquality") plotIsoline(data["x"], (x, y, z), "x", unidades.Dimensionless, grafico, transform, **fmt) # Plot isotherm lines if x != "T" and y != "T" or z: fmt = getLineFormat(Preferences, "Isotherm") plotIsoline(data["T"], (x, y, z), "T", unidades.Temperature, grafico, transform, **fmt) # Plot isobar lines if x != "P" and y != "P" or z: fmt = getLineFormat(Preferences, "Isobar") plotIsoline(data["P"], (x, y, z), "P", unidades.Pressure, grafico, transform, **fmt) # Plot isochor lines if x not in ["rho", "v"] and y not in ["rho", "v"] or z: fmt = getLineFormat(Preferences, "Isochor") plotIsoline(data["v"], (x, y, z), "v", unidades.SpecificVolume, grafico, transform, **fmt) # Plot isodensity lines if "rho" in data: plotIsoline(data["rho"], (x, y, z), "rho", unidades.Density, grafico, transform, **fmt) # Plot isoenthalpic lines if x != "h" and y != "h" or z: fmt = getLineFormat(Preferences, "Isoenthalpic") plotIsoline(data["h"], (x, y, z), "h", unidades.Enthalpy, grafico, transform, **fmt) # Plot isoentropic lines if x != "s" and y != "s" or z: fmt = getLineFormat(Preferences, "Isoentropic") plotIsoline(data["s"], (x, y, z), "s", unidades.SpecificHeat, grafico, transform, **fmt) # Plot 3D mesh if mesh: xdata = data["mesh"][x] ydata = data["mesh"][y] zdata = data["mesh"][z] fx, fy, fz = transform xi = array(list(map(lambda w: list(map(fx, w)), xdata))) yi = array(list(map(lambda w: list(map(fy, w)), ydata))) zi = array(list(map(lambda w: list(map(fz, w)), zdata))) kw = {} if typemesh: kw["color"] = Preferences.get("MEOS", "3Dcolor") kw["alpha"] = Preferences.getint("MEOS", "3Dalpha")/255 kw["lw"] = Preferences.getfloat("MEOS", "3Dlinewidth") kw["ls"] = Preferences.get("MEOS", "3Dlinestyle") grafico.plot.ax.plot_wireframe(xi, yi, zi, **kw) else: kw["cmap"] = Preferences.get("MEOS", "3Dcolormap") kw["alpha"] = Preferences.getint("MEOS", "3Dalphasurface")/255 grafico.plot.ax.plot_surface(xi, yi, zi, **kw) grafico.plot.ax.data3D = (xi.tolist(), yi.tolist(), zi.tolist()) grafico.plot.ax.meshtype = typemesh
[docs] def _getunitTransform(eje): """Return the axis unit transform function to map data to configurated unit Parameters: eje: list with axis property keys """ if not eje: return None if eje == "T": index = config.getMainWindowConfig().getint("Units", "Temperature") func = [float, unidades.K2C, unidades.K2R, unidades.K2F, unidades.K2Re] return func[index] unit = meos.units[meos.keys.index(eje)] factor = unit(1.).config() def f(val): if val is not None and not isinstance(val, str): return val*factor return nan return f
if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) # conf = config.getMainWindowConfig() # SteamTables = AddPoint(conf) # SteamTables=AddLine(None) # SteamTables = Dialog(conf) SteamTables = Plot3D() SteamTables.show() sys.exit(app.exec())