Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
320 changes: 320 additions & 0 deletions francis_runner_gui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
"""Francis Turbine Runner Design GUI (Bovet Method, 1963).

Python port of the MATLAB tool at https://github.com/.../francis-runner-design
with an interactive Tkinter + Matplotlib interface.

Run:
python francis_runner_gui.py
"""

from __future__ import annotations

import math
import tkinter as tk
from tkinter import ttk, messagebox

import numpy as np
import matplotlib

matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg,
NavigationToolbar2Tk,
)
from matplotlib.figure import Figure


# ---------- Bovet method core (ported from MATLAB) ----------

G = 9.81
V2EO = 0.27


def bovet_curve(x, l):
"""Bovet shape function: y = 3.08*(1-x/l)^1.5 * (x/l)^0.5."""
xn = np.asarray(x) / l
xn = np.clip(xn, 0.0, 1.0)
return 3.08 * (1.0 - xn) ** 1.5 * xn**0.5


def r1e_poly(no):
return (
-125.6 * no**5
+ 299.4 * no**4
- 278.0 * no**3
+ 126.3 * no**2
- 28.52 * no
+ 3.674
)


def r2i_poly(no):
return 9.133 * no**4 - 16.61 * no**3 + 11.34 * no**2 - 3.707 * no + 0.974


def meridional_dims(Vo, H, nr, N):
no = (nr * (Vo / math.pi) ** 0.5) / ((2 * G * H) ** 0.75)
bo = 0.8 * (2 - no) * no
roi = 0.7 + 0.16 / (no + 0.08)
ymi = roi
rli = 0.493 / no ** (2 / 3)
roe = 0.493 / no ** (2 / 3) if no < 0.275 else 1.255 - 0.3 * no
li = 3.2 + 3.2 * (2 - no) * no
le = 2.4 - 1.9 * (2 - no) * no
x2e = 0.5
y2e = roe - 1
y2ebyyme = float(bovet_curve(x2e, le))
yme = y2e / y2ebyyme
rme = roe - yme

R2e = ((Vo / math.pi) / (V2EO * nr)) ** (1 / 3)
return dict(
no=no,
R2e=R2e,
Bo=bo * R2e,
Roi=roi * R2e,
Ymi=ymi * R2e,
Rli=rli * R2e,
Roe=roe * R2e,
Li=li * R2e,
Le=le * R2e,
X2e=x2e * R2e,
Y2e=y2e * R2e,
Yme=yme * R2e,
Rme=rme * R2e,
)


def _line_curve_intersections(y_const, curve_x, curve_y):
"""Find x where a horizontal-in-(y,x) sweep meets the curve.

In the MATLAB code the meridional plane is plotted as plot(y, x),
so axes are swapped. We mirror that: given y_const (radius), find
points (y_const, x) on the curve sampled at (curve_y, curve_x).
"""
pts = []
for i in range(len(curve_x) - 1):
y0, y1 = curve_y[i], curve_y[i + 1]
if (y0 - y_const) * (y1 - y_const) <= 0 and y1 != y0:
t = (y_const - y0) / (y1 - y0)
x = curve_x[i] + t * (curve_x[i + 1] - curve_x[i])
pts.append((y_const, x))
return pts


def build_meridional(Vo, H, nrpm, N, n=80):
nr = 2 * math.pi * nrpm / 60.0
d = meridional_dims(Vo, H, nr, N)
no = d["no"]
if not (0.1 <= no <= 0.8):
raise ValueError(
f"无量纲比转速 no={no:.3f} 超出 Bovet 法适用范围 [0.1, 0.8]"
)

Li, Le = d["Li"], d["Le"]
Bo, Roi, Roe = d["Bo"], d["Roi"], d["Roe"]
Ymi, Yme = d["Ymi"], d["Yme"]
R2e = d["R2e"]

# Hub curve
x1_raw = np.linspace(0, Li / 4, n)
y1_raw = Ymi * bovet_curve(x1_raw, Li)
x1 = -x1_raw + (Le + Bo)
y1 = -y1_raw + Roi

# Shroud curve
x2_raw = np.linspace(0, Le, n)
y2_raw = Yme * bovet_curve(x2_raw, Le)
x2 = -x2_raw + Le
y2 = -y2_raw + Roe

# Streamlines
streams = []
for i in range(1, N + 1):
xi = x1 + (x2 - x1) * i / (N + 1)
yi = y1 + (y2 - y1) * i / (N + 1)
streams.append((xi, yi))

# LE / TE intersections — in (y=radius, x=axial)
def first(pts):
return pts[0] if pts else None

def max_x(pts):
return max(pts, key=lambda p: p[1]) if pts else None

def min_x(pts):
return min(pts, key=lambda p: p[1]) if pts else None

LE1 = first(_line_curve_intersections(d["Rli"], x1, y1))
LE2 = max_x(_line_curve_intersections(R2e * r1e_poly(no), x2, y2))
TE1 = first(_line_curve_intersections(R2e * r2i_poly(no), x1, y1))
TE2 = min_x(_line_curve_intersections(R2e, x2, y2))

le_curve = te_curve = None
if None not in (LE1, LE2):
le_curve = _parabola(LE1, LE2, n)
if None not in (TE1, TE2):
te_curve = _parabola(TE1, TE2, n)

return dict(
dims=d,
hub=(x1, y1),
shroud=(x2, y2),
streams=streams,
LE1=LE1, LE2=LE2, TE1=TE1, TE2=TE2,
le_curve=le_curve, te_curve=te_curve,
)


def _parabola(P1, P2, n):
"""y = a*(x - P2_x)^2 + P2_y passing through P1 (x is radius here)."""
y1, x1 = P1 # (radius, axial)
y2, x2 = P2
if (y1 - y2) == 0:
xs = np.linspace(y1, y2, n)
return xs, np.linspace(x1, x2, n)
a = (x1 - x2) / ((y1 - y2) ** 2)
xs = np.linspace(y1, y2, n)
ys = a * (xs - y2) ** 2 + x2
return xs, ys


# ---------- GUI ----------

class FrancisGUI:
def __init__(self, root):
self.root = root
root.title("Francis 转轮设计 — Bovet 法")
root.geometry("1100x720")

left = ttk.Frame(root, padding=12)
left.pack(side=tk.LEFT, fill=tk.Y)

ttk.Label(left, text="输入参数", font=("", 13, "bold")).pack(anchor=tk.W, pady=(0, 8))

self.vars = {}
for key, label, default in [
("Vo", "流量 Q (m³/s)", "0.225"),
("H", "水头 H (m)", "12"),
("nrpm", "转速 (rpm)", "750"),
("N", "流线数 N", "3"),
]:
row = ttk.Frame(left)
row.pack(fill=tk.X, pady=4)
ttk.Label(row, text=label, width=18).pack(side=tk.LEFT)
v = tk.StringVar(value=default)
ttk.Entry(row, textvariable=v, width=12).pack(side=tk.LEFT)
self.vars[key] = v

ttk.Button(left, text="生成 / 刷新", command=self.regenerate).pack(
fill=tk.X, pady=(12, 4)
)
ttk.Button(left, text="保存图片…", command=self.save_fig).pack(fill=tk.X, pady=4)

ttk.Separator(left).pack(fill=tk.X, pady=10)
ttk.Label(left, text="计算结果", font=("", 12, "bold")).pack(anchor=tk.W)

self.result = tk.Text(left, width=36, height=22, font=("Courier", 10))
self.result.pack(fill=tk.BOTH, expand=True, pady=(6, 0))

# Right: plot
right = ttk.Frame(root)
right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

self.fig = Figure(figsize=(7, 6), dpi=100)
self.ax = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, master=right)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
NavigationToolbar2Tk(self.canvas, right)

self.regenerate()

def _read_inputs(self):
return (
float(self.vars["Vo"].get()),
float(self.vars["H"].get()),
float(self.vars["nrpm"].get()),
int(self.vars["N"].get()),
)

def regenerate(self):
try:
Vo, H, nrpm, N = self._read_inputs()
data = build_meridional(Vo, H, nrpm, N)
except Exception as e:
messagebox.showerror("错误", str(e))
return

d = data["dims"]
self.result.delete("1.0", tk.END)
lines = [
f"无量纲比转速 no = {d['no']:.4f}",
f"出口外径 R2e = {d['R2e']*1000:.2f} mm",
f"进口高度 Bo = {d['Bo']*1000:.2f} mm",
f"轮毂半径 Roi = {d['Roi']*1000:.2f} mm",
f"外环半径 Roe = {d['Roe']*1000:.2f} mm",
f"轴向长 Li/Le = {d['Li']*1000:.1f} / {d['Le']*1000:.1f} mm",
f"LE 半径 Rli = {d['Rli']*1000:.2f} mm",
"",
"LE1 (hub): " + _fmt(data["LE1"]),
"LE2 (shroud): " + _fmt(data["LE2"]),
"TE1 (hub): " + _fmt(data["TE1"]),
"TE2 (shroud): " + _fmt(data["TE2"]),
]
self.result.insert(tk.END, "\n".join(lines))

ax = self.ax
ax.clear()
ax.set_title(f"子午面 (no = {d['no']:.3f})")
ax.set_xlabel("径向 r (m)")
ax.set_ylabel("轴向 z (m)")
ax.grid(True, alpha=0.3)

x1, y1 = data["hub"]
x2, y2 = data["shroud"]
ax.plot(y1, x1, "b-", lw=2, label="轮毂 hub")
ax.plot(y2, x2, "r-", lw=2, label="外环 shroud")
for i, (xi, yi) in enumerate(data["streams"]):
ax.plot(yi, xi, "g--", lw=1, label="流线" if i == 0 else None)

if data["le_curve"]:
xs, ys = data["le_curve"]
ax.plot(xs, ys, "m-", lw=1.5, label="前缘 LE")
if data["te_curve"]:
xs, ys = data["te_curve"]
ax.plot(xs, ys, "c-", lw=1.5, label="尾缘 TE")

for name in ("LE1", "LE2", "TE1", "TE2"):
p = data[name]
if p:
ax.plot(p[0], p[1], "k*", ms=10)
ax.annotate(name, (p[0], p[1]), textcoords="offset points", xytext=(6, 6))

ax.set_aspect("equal", adjustable="datalim")
ax.legend(loc="best", fontsize=9)
self.canvas.draw()

def save_fig(self):
from tkinter import filedialog

path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG", "*.png"), ("PDF", "*.pdf"), ("SVG", "*.svg")],
)
if path:
self.fig.savefig(path, dpi=200, bbox_inches="tight")
messagebox.showinfo("已保存", path)


def _fmt(p):
return f"r={p[0]*1000:7.2f} mm, z={p[1]*1000:7.2f} mm" if p else "未找到"


def main():
root = tk.Tk()
FrancisGUI(root)
root.mainloop()


if __name__ == "__main__":
main()