import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display, clear_output, HTML
Для Jupyter/Colab: графики выводятся прямо в ноутбуке
try:
get_ipython().run_line_magic(«matplotlib», «inline»)
except Exception:
pass
============================================================
ИНТЕРАКТИВНАЯ ФОРМА:
сверху 2D и 3D, снизу ползунки a, b, R, R1, R2
============================================================
————————————————————
Стиль тонких 2D-линий
————————————————————
LW_WALL_2D = 0.75
LW_TORUS_2D = 0.85
LW_EDGE_2D = 0.60
LW_AUX_2D = 0.40
LW_ARROW_2D = 0.90
FOCUS_MARKER_SIZE = 3.0
FOCUS_FONT_SIZE = 8
def r_horn(x, a, b, R):
«»»
Гиперболическая образующая:
rho(x) = R — b * sqrt((x/a)^2 — 1)
Область:
|x| >= a
"""
x = np.asarray(x, dtype=float)
val = (x / a) ** 2 - 1.0
val = np.maximum(val, 0.0)
return R - b * np.sqrt(val)
def half_torus_outer_rho(x, a, R):
«»»
Наружное полутороидальное закрытие.
В 2D это две наружные полуокружности:
верхняя: rho = R + sqrt(a^2 - x^2)
нижняя: rho = -R - sqrt(a^2 - x^2)
при |x| <= a.
"""
x = np.asarray(x, dtype=float)
val = a ** 2 - x ** 2
val = np.maximum(val, 0.0)
return R + np.sqrt(val)
def plot_hyperbola_foci(ax, a, b, R):
«»»
Фокусы образующих гипербол.
Верхняя гипербола:
(x/a)^2 — ((rho — R)/b)^2 = 1
Нижняя гипербола:
(x/a)^2 — ((rho + R)/b)^2 = 1
«»»
c = np.sqrt(a ** 2 + b ** 2)
foci = [
(-c, R, "F1+"),
( c, R, "F2+"),
(-c, -R, "F1-"),
( c, -R, "F2-"),
]
for xf, yf, label in foci:
ax.plot(xf, yf, "mo", ms=FOCUS_MARKER_SIZE, zorder=10)
dy = 0.18 * max(1.0, R / 10.0) if yf > 0 else -0.18 * max(1.0, R / 10.0)
va = "bottom" if yf > 0 else "top"
ax.text(
xf,
yf + dy,
label,
color="m",
fontsize=FOCUS_FONT_SIZE,
ha="center",
va=va,
fontweight="bold",
zorder=11,
)
def compute_geometry(a, b, R, R1, R2):
«»»
R1 — доля от R: R_in = R * R1.
R2 — полная доля толщины выходного окна: dR_total = R * R2.
Выходное окно симметрично относительно линии фокусов rho=R:
dR_half = dR_total / 2
"""
R_in = R * R1
dR_total = R * R2
dR_half_raw = 0.5 * dR_total
# Полутороид имеет малый радиус a.
# Если половина выходного окна больше a, построение не должно падать.
# Для визуализации ограничиваем dR_half чуть меньше a.
clipped = False
if dR_half_raw > 0.999 * a:
dR_half = 0.999 * a
clipped = True
else:
dR_half = dR_half_raw
L = a * np.sqrt(1.0 + (R / b) ** 2)
show_inlet = R_in > 0.0
show_outlet = dR_total > 0.0 and dR_half > 0.0
if show_inlet:
x_edge_l = -a * np.sqrt(1.0 + ((R - R_in) / b) ** 2)
else:
x_edge_l = -L
if show_outlet:
rho_horn_cut = R - dR_half
rho_torus_cut = R + dR_half
# r_horn(x_horn_out) = R - dR_half
x_horn_out = a * np.sqrt(1.0 + (dR_half / b) ** 2)
# R + sqrt(a^2 - x_torus_out^2) = R + dR_half
x_torus_out = np.sqrt(max(a ** 2 - dR_half ** 2, 0.0))
else:
rho_horn_cut = R
rho_torus_cut = R
x_horn_out = a
x_torus_out = a
return {
"R_in": R_in,
"dR_total": dR_total,
"dR_half_raw": dR_half_raw,
"dR_half": dR_half,
"clipped": clipped,
"L": L,
"show_inlet": show_inlet,
"show_outlet": show_outlet,
"x_edge_l": x_edge_l,
"rho_horn_cut": rho_horn_cut,
"rho_torus_cut": rho_torus_cut,
"x_horn_out": x_horn_out,
"x_torus_out": x_torus_out,
}
def draw_2d(ax, a, b, R, R1, R2, g):
R_in = g[«R_in»]
dR_total = g[«dR_total»]
dR_half = g[«dR_half»]
L = g[«L»]
show_inlet = g[«show_inlet»]
show_outlet = g[«show_outlet»]
x_edge_l = g[«x_edge_l»]
rho_horn_cut = g[«rho_horn_cut»]
x_horn_out = g[«x_horn_out»]
x_torus_out = g[«x_torus_out»]
# Левая гиперболическая воронка
x_left = np.linspace(-L, -a, 900)
rho_left = r_horn(x_left, a, b, R)
if show_inlet:
mask_left = rho_left >= R_in - 1e-12
else:
mask_left = np.ones_like(x_left, dtype=bool)
ax.plot(
x_left[mask_left],
rho_left[mask_left],
color="black",
lw=LW_WALL_2D,
label="гиперболическая стенка",
)
ax.plot(
x_left[mask_left],
-rho_left[mask_left],
color="black",
lw=LW_WALL_2D,
)
# Входное окно
if show_inlet:
ax.plot(
[x_edge_l, x_edge_l],
[-R_in, R_in],
color="royalblue",
lw=LW_EDGE_2D,
)
ax.annotate(
"",
xy=(x_edge_l - 0.02 * max(1, a), 0.55 * R_in),
xytext=(x_edge_l - 0.22 * L, 0.55 * R_in),
arrowprops=dict(
arrowstyle="->",
color="royalblue",
lw=LW_ARROW_2D,
),
)
ax.annotate(
"",
xy=(x_edge_l - 0.02 * max(1, a), -0.55 * R_in),
xytext=(x_edge_l - 0.22 * L, -0.55 * R_in),
arrowprops=dict(
arrowstyle="->",
color="royalblue",
lw=LW_ARROW_2D,
),
)
# Правая гиперболическая воронка
x_right = np.linspace(a, L, 900)
rho_right = r_horn(x_right, a, b, R)
if show_outlet:
mask_right = rho_right <= rho_horn_cut + 1e-12
else:
mask_right = np.ones_like(x_right, dtype=bool)
ax.plot(x_right[mask_right], rho_right[mask_right], color="black", lw=LW_WALL_2D)
ax.plot(x_right[mask_right], -rho_right[mask_right], color="black", lw=LW_WALL_2D)
# Линии фокусов и фокусы
ax.plot([-L, L], [R, R], color="purple", ls="--", lw=LW_AUX_2D, alpha=0.35)
ax.plot([-L, L], [-R, -R], color="purple", ls="--", lw=LW_AUX_2D, alpha=0.35)
plot_hyperbola_foci(ax, a, b, R)
# Наружное полутороидальное закрытие
x_tor = np.linspace(-a, a, 900)
rho_tor = half_torus_outer_rho(x_tor, a, R)
if show_outlet:
# Выходное окно справа симметрично относительно rho=R:
# часть полутороида около правого соединения не рисуется.
mask_torus = x_tor <= x_torus_out + 1e-12
else:
mask_torus = np.ones_like(x_tor, dtype=bool)
ax.plot(
x_tor[mask_torus],
rho_tor[mask_torus],
color="darkgreen",
lw=LW_TORUS_2D,
label="наружный полутороид",
)
ax.plot(
x_tor[mask_torus],
-rho_tor[mask_torus],
color="darkgreen",
lw=LW_TORUS_2D,
)
# Размер 2a
ax.axvline(-a, color="blue", lw=LW_AUX_2D, ls=":", alpha=0.35)
ax.axvline(a, color="blue", lw=LW_AUX_2D, ls=":", alpha=0.35)
ax.annotate(
"",
xy=(a, R + 0.18 * max(1, a)),
xytext=(-a, R + 0.18 * max(1, a)),
arrowprops=dict(arrowstyle="<->", color="gray", lw=LW_ARROW_2D),
)
ax.text(
0,
R + 0.02 * max(1, a),
"2a",
ha="center",
color="gray",
fontsize=9,
fontweight="bold",
)
# Выходное окно справа — пустота
if show_outlet:
ax.annotate(
"",
xy=(x_horn_out + 0.25 * max(1, a), R + dR_half),
xytext=(x_horn_out + 0.25 * max(1, a), R - dR_half),
arrowprops=dict(
arrowstyle="<->",
color="dimgray",
lw=LW_ARROW_2D,
),
)
ax.text(
x_horn_out + 0.35 * max(1, a),
R,
f"R*R2={dR_total:.3g}",
color="dimgray",
fontsize=8,
va="center",
fontweight="bold",
)
ax.annotate(
"",
xy=(L + 0.30 * L, R),
xytext=(x_horn_out + 0.04 * L, R),
arrowprops=dict(
arrowstyle="->",
color="dimgray",
lw=LW_ARROW_2D,
),
)
ax.annotate(
"",
xy=(L + 0.30 * L, -R),
xytext=(x_horn_out + 0.04 * L, -R),
arrowprops=dict(
arrowstyle="->",
color="dimgray",
lw=LW_ARROW_2D,
),
)
# Заливка внутренней области
ax.fill_between(
x_left[mask_left],
rho_left[mask_left],
-rho_left[mask_left],
color="lightsteelblue",
alpha=0.08,
)
ax.fill_between(
x_right[mask_right],
rho_right[mask_right],
-rho_right[mask_right],
color="lightsteelblue",
alpha=0.08,
)
x_mid = np.linspace(-a, a, 300)
ax.fill_between(x_mid, R, -R, color="lightsteelblue", alpha=0.04)
ax.set_xlabel("x")
ax.set_ylabel("rho")
ax.set_title(
f"2D: a={a:.2f}, b={b:.2f}, R={R:.2f}, R1={R1:.2f}, R2={R2:.2f}",
fontsize=10,
)
ax.set_aspect("equal", adjustable="box")
ax.grid(True, alpha=0.22)
ax.set_xlim(-L * 1.25 - 0.2 * L, L * 1.35)
ax.set_ylim(-(R + a) * 1.35, (R + a) * 1.35)
def draw_3d(ax3d, a, b, R, R1, R2, g):
R_in = g[«R_in»]
L = g[«L»]
show_inlet = g[«show_inlet»]
show_outlet = g[«show_outlet»]
x_edge_l = g[«x_edge_l»]
rho_horn_cut = g[«rho_horn_cut»]
x_horn_out = g[«x_horn_out»]
x_torus_out = g[«x_torus_out»]
rho_torus_cut = g[«rho_torus_cut»]
phi = np.linspace(0, 1.65 * np.pi, 90)
phi_full = np.linspace(0, 2 * np.pi, 240)
# Левая воронка
x_lh = np.linspace(-L, -a, 120)
Xl, PHIl = np.meshgrid(x_lh, phi)
RHOl = r_horn(Xl, a, b, R)
if show_inlet:
RHOl_draw = np.where(RHOl >= R_in - 1e-12, RHOl, np.nan)
else:
RHOl_draw = RHOl
ax3d.plot_surface(
Xl,
RHOl_draw * np.cos(PHIl),
RHOl_draw * np.sin(PHIl),
color="lightsteelblue",
alpha=0.58,
edgecolor="gray",
linewidth=0.035,
rstride=2,
cstride=2,
)
# Кромка входного окна
if show_inlet:
ax3d.plot(
np.full_like(phi_full, x_edge_l),
R_in * np.cos(phi_full),
R_in * np.sin(phi_full),
color="royalblue",
lw=1.0,
)
for ph in np.linspace(0, 2 * np.pi, 10, endpoint=False):
y0 = 0.75 * R_in * np.cos(ph)
z0 = 0.75 * R_in * np.sin(ph)
ax3d.quiver(
x_edge_l - L * 0.20,
y0,
z0,
L * 0.16,
0,
0,
color="royalblue",
lw=0.8,
arrow_length_ratio=0.22,
alpha=0.85,
)
# Правая воронка
x_rh = np.linspace(a, L, 120)
Xr, PHIr = np.meshgrid(x_rh, phi)
RHOr = r_horn(Xr, a, b, R)
if show_outlet:
RHOr_draw = np.where(RHOr <= rho_horn_cut + 1e-12, RHOr, np.nan)
else:
RHOr_draw = RHOr
ax3d.plot_surface(
Xr,
RHOr_draw * np.cos(PHIr),
RHOr_draw * np.sin(PHIr),
color="lightsteelblue",
alpha=0.58,
edgecolor="gray",
linewidth=0.035,
rstride=2,
cstride=2,
)
# Наружный полутороид
theta = np.linspace(0, np.pi, 95)
Theta, PhiT = np.meshgrid(theta, phi)
Xtor = a * np.cos(Theta)
RHOtor = R + a * np.sin(Theta)
if show_outlet:
RHOtor_draw = np.where(Xtor <= x_torus_out + 1e-12, RHOtor, np.nan)
else:
RHOtor_draw = RHOtor
ax3d.plot_surface(
Xtor,
RHOtor_draw * np.cos(PhiT),
RHOtor_draw * np.sin(PhiT),
color="mediumseagreen",
alpha=0.62,
edgecolor="darkgreen",
linewidth=0.035,
rstride=2,
cstride=2,
)
# 3D-выходное окно отмечено красным цветом
if show_outlet:
# Красная кромка со стороны гиперболической воронки
ax3d.plot(
np.full_like(phi_full, x_horn_out),
rho_horn_cut * np.cos(phi_full),
rho_horn_cut * np.sin(phi_full),
color="red",
lw=2.0,
)
# Красная кромка со стороны полутороида
ax3d.plot(
np.full_like(phi_full, x_torus_out),
rho_torus_cut * np.cos(phi_full),
rho_torus_cut * np.sin(phi_full),
color="red",
lw=2.0,
)
# Красные стрелки осевого вывода
for ph in np.linspace(0, 2 * np.pi, 14, endpoint=False):
y0 = R * np.cos(ph)
z0 = R * np.sin(ph)
ax3d.quiver(
x_horn_out,
y0,
z0,
L * 0.45,
0,
0,
color="red",
lw=1.0,
arrow_length_ratio=0.18,
alpha=0.90,
)
ax3d.set_xlabel("x")
ax3d.set_ylabel("y")
ax3d.set_zlabel("z")
ax3d.set_title("3D: выходное окно отмечено красным", fontsize=10)
ax3d.view_init(elev=14, azim=36)
mx = (R + a) * 1.08
ax3d.set_xlim(-L * 1.25, L * 1.30)
ax3d.set_ylim(-mx, mx)
ax3d.set_zlim(-mx, mx)
try:
ax3d.set_box_aspect((2 * L * 1.275, 2 * mx, 2 * mx))
except Exception:
pass
============================================================
WIDGETS
============================================================
a_slider = widgets.FloatSlider(
value=1.0,
min=0.2,
max=5.0,
step=0.1,
description=»a»,
continuous_update=False,
readout_format=».2f»,
)
b_slider = widgets.FloatSlider(
value=1.0,
min=0.2,
max=5.0,
step=0.1,
description=»b»,
continuous_update=False,
readout_format=».2f»,
)
R_slider = widgets.FloatSlider(
value=10.0,
min=2.0,
max=30.0,
step=0.5,
description=»R»,
continuous_update=False,
readout_format=».2f»,
)
R1_slider = widgets.FloatSlider(
value=0.1,
min=0.0,
max=1.0,
step=0.01,
description=»R1″,
continuous_update=False,
readout_format=».2f»,
)
R2_slider = widgets.FloatSlider(
value=0.01,
min=0.0,
max=0.5,
step=0.01,
description=»R2″,
continuous_update=False,
readout_format=».2f»,
)
out = widgets.Output()
def redraw(change=None):
a = float(a_slider.value)
b = float(b_slider.value)
R = float(R_slider.value)
R1 = float(R1_slider.value)
R2 = float(R2_slider.value)
g = compute_geometry(a, b, R, R1, R2)
with out:
clear_output(wait=True)
if g["clipped"]:
display(HTML(
f"<b style='color:#b00000'>Предупреждение:</b> "
f"для выбранных параметров половина выходного окна R·R2/2 = "
f"{g['dR_half_raw']:.3f} больше малого радиуса полутороида a = {a:.3f}. "
f"Для отображения временно использовано R·R2/2 = {g['dR_half']:.3f}. "
f"Уменьшите R2 или увеличьте a."
))
fig = plt.figure(figsize=(15, 6.4))
ax2d = fig.add_subplot(1, 2, 1)
ax3d = fig.add_subplot(1, 2, 2, projection="3d")
draw_2d(ax2d, a, b, R, R1, R2, g)
draw_3d(ax3d, a, b, R, R1, R2, g)
plt.tight_layout()
display(fig)
plt.close(fig)
for slider in [a_slider, b_slider, R_slider, R1_slider, R2_slider]:
slider.observe(redraw, names=»value»)
controls = widgets.GridBox(
children=[a_slider, b_slider, R_slider, R1_slider, R2_slider],
layout=widgets.Layout(
grid_template_columns=»repeat(5, 220px)»,
grid_gap=»8px 10px»,
align_items=»center»,
),
)
display(HTML(
«
Псевдогиперболоид: интерактивная форма
«
«
Сверху отображаются 2D и 3D. Снизу меняйте параметры » «a, b, R, R1, R2. » «R1 — доля радиуса входного окна, R2 — полная доля толщины выходного окна.»
))
display(widgets.VBox([out, controls]))
redraw()