Псевдоэллипсоид 2-го порядка на питоне

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