Source code for plots.moody

#!/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:: moody.rst


API Reference
-------------

The module include all related moody chart functionality
    * :class:`Moody`: Chart dialog
    * :func:`calculate`: Calculate procedure
    * :class:`CalculateDialog`: Dialog to calculate a specified point \
and its configuration
    * :class:`Config`: Moody chart configuration
'''


import json
import os

from numpy import logspace
from numpy.lib.scimath import log10
from matplotlib.patches import ConnectionPatch

from lib.config import conf_dir, Preferences
from lib.friction import f_list, eD
from lib.utilities import formatLine, representacion
from tools.qt import QtWidgets, translate
from UI.widgets import Entrada_con_unidades, GridConfig, LineConfig

from plots.ui import Chart


Re_laminar = [600, 2400]
Re_turbulent = logspace(log10(2400), 8, 50)
Re_fully = logspace(log10(4000), 8, 50)


[docs] def calculate(config): """Calculate procedure, the data are saved to file to fast load again""" fanning = config.getboolean("Moody", "fanning") method = config.getint("Moody", "method") ed = map(float, config.get("Moody", "eD").split(",")) F = f_list[method] dat = {} dat["fanning"] = fanning dat["method"] = method # laminar if fanning: dat["laminar"] = [16./R for R in Re_laminar] x = 4 else: dat["laminar"] = [64./R for R in Re_laminar] x = 1 # turbulent turb = {} for e in ed: turb[e] = [F(Rei, e)/x for Rei in Re_turbulent] dat["turbulent"] = turb # Line to define the fully desarrolled turbulent flux dat["fully"] = [(1/(1.14-2*log10(3500/R)))**2/x for R in Re_fully] # Save to file with open(conf_dir+"moody.dat", "w", encoding="utf-8") as file: json.dump(dat, file, indent=4)
[docs] class Config(QtWidgets.QWidget): """Moody chart configuration""" TITLE = translate("Moody", "Moody chart") TITLECONFIG = translate("Moody", "Moody diagram 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.metodos = QtWidgets.QComboBox() for f in f_list: line = f.__doc__.split("\n")[0] year = line.split(" ")[-1] name = line.split(" ")[-3] doc = name + " " + year self.metodos.addItem(doc) layout.addWidget(self.metodos, 1, 2) layout.addItem(QtWidgets.QSpacerItem( 10, 10, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed), 1, 3) self.fanning = QtWidgets.QCheckBox( self.tr("Calculate fanning friction factor")) layout.addWidget(self.fanning, 2, 1, 1, 3) layout.addWidget(QtWidgets.QLabel("ε/d:"), 3, 1) self.ed = QtWidgets.QLineEdit() layout.addWidget(self.ed, 3, 2, 1, 2) self.lineconfig = LineConfig( "line", self.tr("Relative roughtness style line")) layout.addWidget(self.lineconfig, 4, 1, 1, 3) self.cruxconfig = LineConfig("crux", self.tr("Crux style line")) layout.addWidget(self.cruxconfig, 5, 1, 1, 3) self.gridconfig = GridConfig( "grid", self.tr("Grid style line")) layout.addWidget(self.gridconfig, 6, 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("Moody"): self.metodos.setCurrentIndex(config.getint("Moody", 'method')) self.fanning.setChecked(config.getboolean("Moody", 'fanning')) self.ed.setText(config.get("Moody", "ed")) self.lineconfig.setConfig(config, "Moody") self.cruxconfig.setConfig(config, "Moody") self.gridconfig.setConfig(config, "Moody")
[docs] def value(self, config): """Update ConfigParser instance with the config""" if not config.has_section("Moody"): config.add_section("Moody") config.set("Moody", "method", str(self.metodos.currentIndex())) config.set("Moody", "fanning", str(self.fanning.isChecked())) config.set("Moody", "ed", self.ed.text()) config = self.lineconfig.value(config, "Moody") config = self.cruxconfig.value(config, "Moody") config = self.gridconfig.value(config, "Moody") return config
[docs] class Moody(Chart): """Moody chart dialog""" title = translate("Moody", "Moody Diagram") widgetConfig = Config locLogo = (0.3, 0.15, 0.1, 0.1) note = None
[docs] def config(self): """Initialization action in plot don't neccesary to update in plot""" txt = self.tr("Relative roughness") + ", " + r"$r=\frac{\epsilon}{D}$" self.plt.fig.text(0.97, 0.5, txt, rotation=90, size='10', va="center", ha="center") self.plt.fig.subplots_adjust( left=0.1, right=0.9, bottom=0.12, top=0.98) self.note = None
[docs] def click(self, event): """Update input and graph annotate when mouse click over chart""" Re = event.xdata f = event.ydata # Exit if click event if out of axis if Re is None: return ed = None method = Preferences.getint("Moody", "method") fanning = Preferences.getboolean("Moody", "fanning") F = f_list[method] if Re < 2400: if fanning: f = 16/Re else: f = 64/Re else: f_min = F(Re, 0) if f < f_min: Re = f = 0 else: ed = eD(Re, f) self.createCrux(Re, f, ed)
[docs] @staticmethod def _txt(Re, f, ed): if ed is None: txt = f"Re: {Re:0.4g}\nf: {f:0.4g}" else: txt = f"Re: {Re:0.4g}\nε/D: {ed:0.4g}\nf: {f:0.4g}" return txt
[docs] def createCrux(self, Re, f, ed): """Create a crux in selected point of plot and show data at bottom right corner""" if f and Re: txt = self._txt(Re, f, ed) self.plt.lx.set_ydata([f]) self.plt.ly.set_xdata([Re]) if self.note: self.note.remove() self.note = None if f and Re: self.note = self.plt.fig.text(0.92, 0.10, txt, size="8", va="top") self.plt.draw()
[docs] def plot(self): """Plot the Moody chart using the indicate method """ fanning = Preferences.getboolean("Moody", "fanning") method = Preferences.getint("Moody", "method") if fanning: x = 4 else: x = 1 self.plt.ax.set_autoscale_on(False) self.plt.ax.clear() grid = Preferences.getboolean("Moody", "grid") kw = formatLine(Preferences, "Moody", "grid") del kw["marker"] if grid: self.plt.ax.grid(grid, **kw) else: self.plt.ax.grid(grid) self.plt.ax.set_xlim(600, 1e8) self.plt.ax.set_ylim(0.008/x, 0.11/x) self.plt.ax.set_xscale("log") self.plt.ax.set_yscale("log") kw = formatLine(Preferences, "Moody", "crux") self.plt.lx = self.plt.ax.axhline(**kw) # the horiz line self.plt.ly = self.plt.ax.axvline(**kw) # the vert line xlabel = self.tr("Reynolds number") + ", " + r"$Re=\frac{V\rho D}{\mu}$" self.plt.ax.set_xlabel(xlabel, ha='center', size='10') if fanning: ylabel = self.tr("Fanning Friction factor") formula = r"$f_f=\frac{2hDg}{LV^2}$" else: ylabel = self.tr("Darcy Friction factor") formula = r"$f_d=\frac{2hDg}{LV^2}$" self.plt.ax.set_ylabel(ylabel+", " + formula, size='10') xticks = [7e2, 8e2, 9e2, 1e3, 2e3, 3e3, 4e3, 5e3, 6e3, 7e3, 8e3, 9e3, 1e4, 2e4, 3e4, 4e4, 5e4, 6e4, 7e4, 8e4, 9e4, 1e5, 2e5, 3e5, 4e5, 5e5, 6e5, 7e5, 8e5, 9e5, 1e6, 2e6, 3e6, 4e6, 5e6, 6e6, 7e6, 8e6, 9e6, 1e7, 2e7, 3e7, 4e7, 5e7, 6e7, 7e7, 8e7, 9e7] self.plt.ax.set_xticks(xticks, minor=False) self.plt.ax.set_yticks([], minor=True) if fanning: yticks = [2e-3, 2.5e-3, 3e-3, 3.5e-3, 4e-3, 5e-3, 6e-3, 7e-3, 8e-3, 9e-3, 1e-2, 1.1e-2, 1.2e-2, 1.3e-2, 1.4e-2, 1.5e-2, 1.6e-2, 1.7e-2, 1.8e-2, 1.9e-2, 2e-2, 2.1e-2, 2.2e-2, 2.3e-2, 2.4e-2, 2.5e-2] ytickslabel = [2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, r"$10^{-2}$", "", 1.2, "", 1.4, "", 1.6, "", 1.8, "", 2, "", "", "", "", 2.5] else: yticks = [9e-3, 1e-2, 1.1e-2, 1.2e-2, 1.3e-2, 1.4e-2, 1.5e-2, 1.6e-2, 1.7e-2, 1.8e-2, 1.9e-2, 2e-2, 2.1e-2, 2.2e-2, 2.3e-2, 2.4e-2, 2.5e-2, 2.6e-2, 2.7e-2, 2.8e-2, 2.9e-2, 3e-2, 3.2e-2, 3.4e-2, 3.6e-2, 3.8e-2, 4e-2, 4.2e-2, 4.4e-2, 4.6e-2, 4.8e-2, 5e-2, 5.2e-2, 5.4e-2, 5.6e-2, 5.8e-2, 6e-2, 6.2e-2, 6.4e-2, 6.6e-2, 6.8e-2, 7e-2, 7.5e-2, 8e-2, 8.5e-2, 9e-2, 9.5e-2, 1e-1] ytickslabel = [9, r"$10^{-2}$", "", 1.2, "", 1.4, "", 1.6, "", 1.8, "", 2, "", "", "", "", 2.5, "", "", "", "", 3, "", "", "", "", 4, "", "", "", "", 5, "", "", "", "", 6, "", "", "", "", 7, "", 8, "", 9, "", r"$10^{-1}$"] self.plt.ax.set_yticks(yticks) self.plt.ax.set_yticklabels(ytickslabel) if not os.path.isfile(conf_dir+"moody.dat"): calculate(Preferences) load = False with open(conf_dir+"moody.dat", "r", encoding="utf-8") as file: try: dat = json.load(file) except ValueError: calculate(Preferences) load = True if dat["fanning"] != fanning or dat["method"] != method: calculate(Preferences) load = True # Reload file if it's created in last with statement if load: with open(conf_dir+"moody.dat", "r", encoding="utf-8") as file: dat = json.load(file) # Plot data kw = formatLine(Preferences, "Moody", "line") self.plt.ax.plot(Re_laminar, dat["laminar"], **kw) for ed, f in dat["turbulent"].items(): self.plt.ax.plot(Re_turbulent, f, **kw) title = " " + representacion(ed, tol=4.5) if f[-1] > 0.008/x: self.plt.ax.text(1e8, f[-1], title, size="x-small", ha='left', va='center') self.plt.ax.plot(Re_fully, dat["fully"], "k", lw=0.5, ls=":") # Add explicative legend # Laminar zone self.plt.ax.add_artist( ConnectionPatch((600, 0.01/x), (2400, 0.01/x), "data", "data", arrowstyle="<|-|>", mutation_scale=15, fc="w")) txt = self.tr("Laminar flux") self.plt.ax.text(1200, 0.0095/x, txt, size="small", va="top", ha="center", backgroundcolor="#ffffff") # Critic zone self.plt.ax.add_artist( ConnectionPatch((2400, 0.01/x), (4000, 0.01/x), "data", "data", arrowstyle="<|-|>", mutation_scale=15, fc="w")) txt = self.tr("Critic\nzone") self.plt.ax.text(3200, 0.0095/x, txt, size="small", va="top", ha="center", backgroundcolor="#ffffff") # Transition zone self.plt.ax.add_artist( ConnectionPatch((4000, 0.095/x), (40000, 0.095/x), "data", "data", arrowstyle="<|-|>", mutation_scale=15, fc="w")) txt = self.tr("Transition Zone") self.plt.ax.text(11000, 0.098/x, txt, size="small", va="bottom", ha="center", backgroundcolor="#ffffff") # Turbulent zone self.plt.ax.add_artist( ConnectionPatch((40000, 0.095/x), (9.9e7, 0.095/x), "data", "data", arrowstyle="<|-|>", mutation_scale=15, fc="w")) txt = self.tr("Turbulent flux fully developed") self.plt.ax.text(1e6, 0.098/x, txt, size="small", va="bottom", ha="center", backgroundcolor="#ffffff") # Smooth tubes self.plt.ax.add_artist( ConnectionPatch((1e6, 0.0095/x), (1.5e6, 0.011/x), "data", "data", arrowstyle="<|-|>", mutation_scale=15, fc="w")) txt = self.tr("Smooth tubes") self.plt.ax.text(1e6, 0.009/x, txt, size="small", va="top", ha="center", backgroundcolor="#ffffff") # Laminar equation if fanning: txt = r"$f=\frac{16}{Re}$" else: txt = r"$f=\frac{64}{Re}$" self.plt.ax.text(1.4e3, 0.042/x, txt, size="12", va="top", ha="center", rotation=-66) # TODO: Calculate the angle dynamically self.plt.draw()
[docs] def calculate(self): dlg = CalculateDialog() if dlg.exec(): Re = dlg.Re.value f = dlg.f.value ed = dlg.eD.value self.createCrux(Re, f, ed)
[docs] class CalculateDialog(QtWidgets.QDialog): """Dialog to calculate a specified point"""
[docs] def __init__(self, parent=None): super().__init__(parent) title = self.tr("Calculate friction factor") self.setWindowTitle(title) layout = QtWidgets.QGridLayout(self) label = QtWidgets.QLabel(self.tr("Method:")) layout.addWidget(label, 1, 0) self.metodos = QtWidgets.QComboBox() for f in f_list: line = f.__doc__.split("\n")[0] year = line.split(" ")[-1] name = line.split(" ")[-3] doc = name + " " + year self.metodos.addItem(doc) self.metodos.currentIndexChanged.connect(self.calculate) layout.addWidget(self.metodos, 1, 1, 1, 2) self.fanning = QtWidgets.QCheckBox( self.tr("Calculate fanning friction factor")) self.fanning.toggled.connect(self.calculate) layout.addWidget(self.fanning, 2, 0, 1, 3) layout.addWidget(QtWidgets.QLabel("Re"), 3, 1) self.Re = Entrada_con_unidades(float, tolerancia=4) self.Re.valueChanged.connect(self.calculate) layout.addWidget(self.Re, 3, 2) layout.addWidget(QtWidgets.QLabel("e/D"), 4, 1) self.eD = Entrada_con_unidades(float) self.eD.valueChanged.connect(self.calculate) layout.addWidget(self.eD, 4, 2) layout.addWidget(QtWidgets.QLabel("f"), 5, 1) self.f = Entrada_con_unidades(float, readOnly=True, decimales=8) layout.addWidget(self.f, 5, 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.metodos.currentIndex() F = f_list[index] Re = self.Re.value ed = self.eD.value if Re and ed is not None: f = F(Re, ed) if self.fanning.isChecked(): f /= 4 self.f.setValue(f)
if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) Dialog = Moody() Dialog.show() sys.exit(app.exec())