
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# ============================================================
# БАЗОВЫЕ ФУНКЦИИ (из вашего скрипта)
# ============================================================
def y_left(x, a, b):
val = 1.0 — ((x + a)**2) / (a**2)
return b * np.sqrt(np.clip(val, 0.0, None))
def y_right(x, a, b, h1):
val = 1.0 — ((x — a — h1)**2) / (a**2)
return b * np.sqrt(np.clip(val, 0.0, None))
def build_signed_profile(a, b, h1, n=1800):
xmin = min(-a, h1)
xmax = max(0.0, a + h1)
x = np.linspace(xmin, xmax, n)
yL = np.full_like(x, np.nan, dtype=float)
yR = np.full_like(x, np.nan, dtype=float)
maskL = (x >= -a) & (x <= 0.0)
maskR = (x >= h1) & (x <= a + h1)
yL[maskL] = y_left(x[maskL], a, b)
yR[maskR] = y_right(x[maskR], a, b, h1)
y = np.full_like(x, np.nan, dtype=float)
if h1 >= 0:
onlyL = maskL & (~maskR)
onlyR = maskR & (~maskL)
y[onlyL] = yL[onlyL]
y[onlyR] = yR[onlyR]
gap = (x > 0.0) & (x < h1)
y[gap] = 0.0
y[np.isclose(x, 0.0)] = 0.0
if not np.isclose(h1, 0.0):
y[np.isclose(x, h1)] = 0.0
else:
both = maskL & maskR
onlyL = maskL & (~maskR)
onlyR = maskR & (~maskL)
y[onlyL] = yL[onlyL]
y[onlyR] = yR[onlyR]
y[both] = np.maximum(yL[both], yR[both])
return x, y, yL, yR, maskL, maskR
def compute_foci(a, b, h1, R, eps=1e-12):
if b > a + eps:
c = np.sqrt(b**2 — a**2)
foci_1d = [(-a, -c, ‘F1’), (-a, +c, ‘F2’),
(a+h1, -c, ‘F3’), (a+h1, +c, ‘F4’)]
foci_2d = foci_1d + [
(-a, 2*R+c, ‘F5’), (-a, 2*R-c, ‘F6’),
(a+h1, 2*R+c, ‘F7’), (a+h1, 2*R-c, ‘F8’)]
elif a > b + eps:
c = np.sqrt(a**2 — b**2)
foci_1d = [(-a-c, 0, ‘F1’), (-a+c, 0, ‘F2’),
(a+h1-c, 0, ‘F3’), (a+h1+c, 0, ‘F4’)]
foci_2d = foci_1d + [
(-a-c, 2*R, ‘F5’), (-a+c, 2*R, ‘F6’),
(a+h1-c, 2*R, ‘F7’), (a+h1+c, 2*R, ‘F8’)]
else:
c = 0.0
foci_1d = [(-a, 0, ‘F1=F2’), (a+h1, 0, ‘F3=F4’)]
foci_2d = foci_1d + [(-a, 2*R, ‘F5=F6’), (a+h1, 2*R, ‘F7=F8’)]
return c, foci_1d, foci_2d
def classify(K, eps=1e-12):
if K > 1+eps: return ‘Вертикальный’
elif K < 1-eps: return ‘Горизонтальный’
else: return ‘Псевдосфера’
# ============================================================
# 14 КАНОНИЧЕСКИХ ТИПОВ
# ============================================================
ALL_14 = [
# — Горизонтальные (K=0.5) —
{‘id’: 1, ‘K’: 0.5, ‘h1’: 0.0, ‘h’: 0.0, ‘name’: ‘Гориз. закрытый’},
{‘id’: 2, ‘K’: 0.5, ‘h1’: 0.0, ‘h’: 0.05, ‘name’: ‘Гориз. торц. окна’},
{‘id’: 3, ‘K’: 0.5, ‘h1’: 0.05, ‘h’: 0.0, ‘name’: ‘Гориз. экват. окно’},
{‘id’: 4, ‘K’: 0.5, ‘h1’: 0.05, ‘h’: 0.05, ‘name’: ‘Гориз. открытый’},
# — Псевдосферы (K=1.0) —
{‘id’: 5, ‘K’: 1.0, ‘h1’: 0.0, ‘h’: 0.0, ‘name’: ‘Псевдосфера закр.’},
{‘id’: 6, ‘K’: 1.0, ‘h1’: 0.0, ‘h’: 0.05, ‘name’: ‘Псевдосфера торц.’},
{‘id’: 7, ‘K’: 1.0, ‘h1’: 0.05, ‘h’: 0.0, ‘name’: ‘Псевдосфера экват.’},
{‘id’: 8, ‘K’: 1.0, ‘h1’: 0.05, ‘h’: 0.05, ‘name’: ‘Псевдосфера откр.’},
# — Вертикальные (K=1.5) —
{‘id’: 9, ‘K’: 1.5, ‘h1’: 0.0, ‘h’: 0.0, ‘name’: ‘Вертик. закрытый’},
{‘id’: 10, ‘K’: 1.5, ‘h1’: 0.0, ‘h’: 0.05, ‘name’: ‘Вертик. торц. окна’},
{‘id’: 11, ‘K’: 1.5, ‘h1’: 0.05, ‘h’: 0.0, ‘name’: ‘Вертик. экват. окно’},
{‘id’: 12, ‘K’: 1.5, ‘h1’: 0.05, ‘h’: 0.05, ‘name’: ‘Вертик. открытый’},
# — Трёхфокусные (K=0.6, h1=-0.4) —
{‘id’: 13, ‘K’: 0.6, ‘h1’: -0.4, ‘h’: 0.0, ‘name’: ‘Трёхфок. закрытый’},
{‘id’: 14, ‘K’: 0.6, ‘h1’: -0.4, ‘h’: 0.05, ‘name’: ‘Трёхфок. торц. окна’},
]
# ============================================================
# ГЕНЕРАЦИЯ: страницы по 7 типов, 3 столбца каждая
# ============================================================
def draw_page(configs, page_num):
n_rows = len(configs)
fig = plt.figure(figsize=(24, 5.0 * n_rows))
for row, cfg in enumerate(configs):
K, h1, h = cfg[‘K’], cfg[‘h1’], cfg[‘h’]
a, b, R = 1.0, K, K + h
tid = cfg[‘id’]
name = cfg[‘name’]
typ = classify(K)
x_pr, y_pr, yL, yR, mL, mR = build_signed_profile(a, b, h1, n=1500)
y_act = np.where(np.isnan(y_pr), np.nan, np.minimum(y_pr, R))
y_up = np.where(np.isnan(y_act), np.nan, 2.0*R — y_act)
d_pr = np.where(np.isnan(y_act), np.nan, R — y_act)
c_val, foci_1d, foci_2d = compute_foci(a, b, h1, R)
# ===== Столбец 1: Образующая =====
ax1 = fig.add_subplot(n_rows, 3, row*3 + 1)
# Полные родительские эллипсы
t_f = np.linspace(0, 2*np.pi, 400)
ax1.plot(-a + a*np.cos(t_f), b*np.sin(t_f), ‘—‘, color=’gray’,
alpha=0.3, lw=0.8)
ax1.plot((a+h1) + a*np.cos(t_f), b*np.sin(t_f), ‘—‘, color=’gray’,
alpha=0.3, lw=0.8)
# Ветви
vm_L = ~np.isnan(yL)
vm_R = ~np.isnan(yR)
if np.any(vm_L):
ax1.plot(x_pr[vm_L], yL[vm_L], ‘-‘, color=’steelblue’,
alpha=0.5, lw=1.5)
if np.any(vm_R):
ax1.plot(x_pr[vm_R], yR[vm_R], ‘-‘, color=’darkorange’,
alpha=0.5, lw=1.5)
# Итоговая
vm_y = ~np.isnan(y_pr)
ax1.plot(x_pr[vm_y], y_pr[vm_y], ‘-‘, color=’#2ca02c’, lw=2.5)
# y = R
ax1.axhline(R, color=’red’, ls=’:’, lw=1, alpha=0.6)
# Фокусы F1-F4
for fx, fy, fl in foci_1d:
ax1.plot(fx, fy, ‘D’, color=’red’, ms=5, zorder=10)
ax1.annotate(fl, (fx, fy), fontsize=5, color=’red’,
xytext=(3, 3), textcoords=’offset points’)
ax1.set_title(f’#{tid} {name}\nK={K} h₁={h1} h={h} R={R}\n’
f’Тип: {typ}’, fontsize=8, fontweight=’bold’)
ax1.set_xlabel(‘x’, fontsize=7)
ax1.set_ylabel(‘y’, fontsize=7)
ax1.set_aspect(‘equal’, adjustable=’box’)
ax1.grid(True, alpha=0.2)
ax1.tick_params(labelsize=6)
# ===== Столбец 2: 2D-сечение =====
ax2 = fig.add_subplot(n_rows, 3, row*3 + 2)
vm_a = ~np.isnan(y_act)
vm_u = ~np.isnan(y_up)
ax2.plot(x_pr[vm_a], y_act[vm_a], ‘-‘, color=’#2ca02c’, lw=2)
ax2.plot(x_pr[vm_u], y_up[vm_u], ‘-‘, color=’#2ca02c’, lw=2, alpha=0.7)
if np.any(vm_a & vm_u):
ax2.fill_between(x_pr[vm_a & vm_u], y_act[vm_a & vm_u],
y_up[vm_a & vm_u], color=’#2ca02c’, alpha=0.08)
ax2.axhline(R, color=’red’, ls=’:’, lw=1, alpha=0.6)
# Все фокусы F1-F8
for fx, fy, fl in foci_2d:
ax2.plot(fx, fy, ‘D’, color=’red’, ms=5, zorder=10)
ax2.annotate(fl, (fx, fy), fontsize=4, color=’red’,
xytext=(2, 2), textcoords=’offset points’)
ax2.set_title(f’2D-сечение ◆=фокусы F1–F8′, fontsize=8)
ax2.set_xlabel(‘x’, fontsize=7)
ax2.set_ylabel(‘y’, fontsize=7)
ax2.set_aspect(‘equal’, adjustable=’box’)
ax2.grid(True, alpha=0.2)
ax2.tick_params(labelsize=6)
# ===== Столбец 3: 3D =====
ax3 = fig.add_subplot(n_rows, 3, row*3 + 3, projection=’3d’)
x3, y3, _, _, _, _ = build_signed_profile(a, b, h1, n=400)
y3a = np.minimum(np.where(np.isnan(y3), R, y3), R)
d3 = R — y3a
theta = np.linspace(0, 2*np.pi, 80)
X3, TH = np.meshgrid(x3, theta)
D3 = np.tile(d3, (theta.size, 1))
Y3 = D3 * np.sin(TH)
Z3 = D3 * np.cos(TH)
ax3.plot_surface(X3, Y3, Z3, alpha=0.85, linewidth=0,
antialiased=True, cmap=’viridis’)
ax3.plot(x3, np.zeros_like(x3), d3, ‘r-‘, lw=1.5)
ax3.plot(x3, np.zeros_like(x3), -d3, ‘r-‘, lw=1.5)
x_rng = np.nanmax(x3) — np.nanmin(x3)
yz_mx = max(R, np.nanmax(d3), 0.01)
ax3.set_box_aspect((max(x_rng, 0.1), 2*yz_mx, 2*yz_mx))
ax3.view_init(elev=22, azim=-55)
ax3.set_title(f’3D d_max={np.nanmax(d3):.3f}’, fontsize=8)
ax3.set_xlabel(‘X’, fontsize=6)
ax3.set_ylabel(‘Y’, fontsize=6)
ax3.set_zlabel(‘Z’, fontsize=6)
ax3.tick_params(labelsize=5)
fig.suptitle(
f’Страница {page_num}. Каноническая геометрия 14 типов ‘
f’псевдоэллипсоидов 2-го порядка\n’
f’Столбец 1: Образующая + фокусы F1–F4 | ‘
f’Столбец 2: 2D-сечение + фокусы F1–F8 | ‘
f’Столбец 3: 3D-поверхность’,
fontsize=13, fontweight=’bold’, y=1.005)
fig.tight_layout(rect=[0, 0, 1, 0.99])
fname = f’fig01_14types_page{page_num}.png’
fig.savefig(fname, dpi=250, bbox_inches=’tight’)
print(f’Сохранено: {fname}’)
plt.show()
# ============================================================
# ГЕНЕРАЦИЯ ДВУХ СТРАНИЦ
# ============================================================
draw_page(ALL_14[:7], page_num=1) # Типы 1–7
draw_page(ALL_14[7:], page_num=2) # Типы 8–14