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

import numpy as np

import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D

# Настраиваем стиль

plt.style.use(‘seaborn-v0_8-whitegrid’)

plt.rcParams.update({‘font.size’: 11, ‘font.family’: ‘sans-serif’})

# ==============================================================================

# ПАРАМЕТРЫ ФИГУРЫ И ВРАЩЕНИЯ

# ==============================================================================

f = 2.0  # Фокусное расстояние парабол

R = 8.0  # Радиус смещения оси вращения ВВЕРХ

a = (R**2) / (4 * f)  # Вычисляем ширину a, при которой ветвь пересечет ось вращения

                     # (чтобы фигура замкнулась в «полюсах»)

# ==============================================================================

# ШАГ 1: БАЗОВЫЙ ПРОФИЛЬ (НАША ГАЛОЧКА)

# ==============================================================================

# Правая ветвь: y = sqrt(4fx)

x_right = np.linspace(0, a, 200)

y_right = np.sqrt(4 * f * x_right)

# Левая ветвь: y = sqrt(-4fx)

x_left = np.linspace(-a, 0, 200)

y_left = np.sqrt(-4 * f * x_left)

# Объединяем в один массив для вращения

x_full = np.concatenate((x_left, x_right))

y_full = np.concatenate((y_left, y_right))

# ==============================================================================

# ШАГ 2: РАДИУС ВРАЩЕНИЯ

# ==============================================================================

# Ось вращения поднята на R.

# Локальный радиус (расстояние от оси вращения до профиля галочки):

# r(x) = R — y_full

r_x = R — y_full

# ==============================================================================

# ВИЗУАЛИЗАЦИЯ: 2D СХЕМА И 3D ПОВЕРХНОСТЬ

# ==============================================================================

fig = plt.figure(figsize=(16, 7))

# —————————————————————————-

# ГРАФИК 1: 2D Схема Вращения

# —————————————————————————-

ax1 = fig.add_subplot(121)

# Линия фокусов (исходная ось)

ax1.axhline(0, color=’gray’, linestyle=’-.’, linewidth=1.5, label=’Линия фокусов (y=0)’)

# Новая ось вращения (сдвинутая вверх на R)

ax1.axhline(R, color=’red’, linestyle=’—‘, linewidth=2, label=f’Ось вращения (y={R})’)

# Исходная образующая («галочка»)

ax1.plot(x_full, y_full, ‘b-‘, linewidth=3, label=’Образующая (галочка)’)

# Зеркальное отражение профиля сверху (чтобы показать границы 3D фигуры в сечении)

ax1.plot(x_full, 2*R — y_full, ‘b:’, linewidth=2, alpha=0.5, label=’Зеркальный профиль’)

# Отмечаем фокусы и общую точку

ax1.plot(f, 0, ‘g*’, markersize=12, label=’Фокусы (F)’)

ax1.plot(-f, 0, ‘g*’, markersize=12)

ax1.plot(0, 0, ‘ko’, markersize=8, label=’Центральная точка’)

# Оформление 2D

ax1.set_title(‘Схема вращения (2D сечение)’, fontsize=14, fontweight=’bold’)

ax1.set_xlabel(‘Ось X’, fontweight=’bold’)

ax1.set_ylabel(‘Ось Y’, fontweight=’bold’)

ax1.axis(‘equal’)

ax1.legend(loc=’lower center’, fontsize=10, bbox_to_anchor=(0.5, -0.25), ncol=2)

ax1.grid(True, alpha=0.3)

# —————————————————————————-

# ГРАФИК 2: 3D Псевдоэллипсоид 2-го порядка

# —————————————————————————-

ax2 = fig.add_subplot(122, projection=’3d’)

# Создаем 3D сетку углов вращения

phi = np.linspace(0, 2 * np.pi, 150)

X_mesh, Phi_mesh = np.meshgrid(x_full, phi)

# Радиус для каждой точки X

R_mesh = R — np.concatenate((np.sqrt(-4 * f * x_left), np.sqrt(4 * f * x_right)))

# Переход в декартовы координаты 3D

# X остается X, а Y и Z формируют круги радиуса R_mesh

Y_mesh = R_mesh * np.cos(Phi_mesh)

Z_mesh = R_mesh * np.sin(Phi_mesh)

# Построение 3D поверхности

surf = ax2.plot_surface(X_mesh, Y_mesh, Z_mesh,

                        cmap=’coolwarm’, alpha=0.9,

                        edgecolor=’none’, antialiased=True)

# Ось вращения в 3D

ax2.plot([x_full[0], x_full[-1]], [0, 0], [0, 0], color=’red’, linestyle=’—‘, linewidth=3, label=’Ось вращения’)

# Настройка 3D осей

ax2.set_title(‘3D Псевдоэллипсоид 2-го порядка’, fontsize=14, fontweight=’bold’)

ax2.set_xlabel(‘X (вдоль оси)’)

ax2.set_ylabel(‘Y’)

ax2.set_zlabel(‘Z’)

# Выравнивание масштабов в 3D (чтобы фигура не искажалась)

max_range = np.array([X_mesh.max()-X_mesh.min(), Y_mesh.max()-Y_mesh.min(), Z_mesh.max()-Z_mesh.min()]).max() / 2.0

mid_x = (X_mesh.max()+X_mesh.min()) * 0.5

mid_y = (Y_mesh.max()+Y_mesh.min()) * 0.5

mid_z = (Z_mesh.max()+Z_mesh.min()) * 0.5

ax2.set_xlim(mid_x — max_range, mid_x + max_range)

ax2.set_ylim(mid_y — max_range, mid_y + max_range)

ax2.set_zlim(mid_z — max_range, mid_z + max_range)

# Убираем лишние панели для красоты

ax2.xaxis.pane.fill = False

ax2.yaxis.pane.fill = False

ax2.zaxis.pane.fill = False

ax2.legend(loc=’upper right’)

plt.tight_layout()

plt.show()

import numpy as np

import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D

# Настраиваем стиль

plt.style.use(‘seaborn-v0_8-darkgrid’)

plt.rcParams.update({‘font.size’: 11, ‘font.family’: ‘sans-serif’})

# ==============================================================================

# ПАРАМЕТРЫ ФИГУРЫ И ВРАЩЕНИЯ

# ==============================================================================

f = 2.0  # Фокусное расстояние парабол

R = 8.0  # Радиус смещения оси вращения

a = (R**2) / (4 * f)  # Ширина резонатора до смыкания на полюсах

# ==============================================================================

# ПРОФИЛЬ ПОВЕРХНОСТИ

# ==============================================================================

# Правая и левая ветви

x_right = np.linspace(0, a, 100)

x_left = np.linspace(-a, 0, 100)

x_full = np.concatenate((x_left, x_right))

# Формируем галочку

y_full = np.concatenate((np.sqrt(-4 * f * x_left), np.sqrt(4 * f * x_right)))

# Радиус поверхности (расстояние от оси вращения до стенки)

r_surf = R — y_full

# ==============================================================================

# 3D МОДЕЛИРОВАНИЕ

# ==============================================================================

fig = plt.figure(figsize=(14, 10))

ax = fig.add_subplot(111, projection=’3d’)

# 1. Сетка для поверхности

phi = np.linspace(0, 2 * np.pi, 100)

X_mesh, Phi_mesh = np.meshgrid(x_full, phi)

# 2. Декартовы координаты поверхности

Y_mesh = (R — np.interp(X_mesh, x_full, y_full)) * np.cos(Phi_mesh)

Z_mesh = (R — np.interp(X_mesh, x_full, y_full)) * np.sin(Phi_mesh)

# Построение поверхности (полупрозрачная, чтобы видеть сквозь нее)

surf = ax.plot_surface(X_mesh, Y_mesh, Z_mesh,

                       color=’lightblue’, alpha=0.4,

                       edgecolor=’royalblue’, linewidth=0.2, antialiased=True)

# 3. ПОСТРОЕНИЕ ВНЕШНИХ ФОКУСНЫХ КОЛЕЦ

# Левое кольцо (x = -f), радиус = R

Y_ring = R * np.cos(phi)

Z_ring = R * np.sin(phi)

X_ring_left = np.full_like(phi, -f)

# Правое кольцо (x = +f), радиус = R

X_ring_right = np.full_like(phi, f)

# Рисуем кольца (толстые, яркие)

ax.plot(X_ring_left, Y_ring, Z_ring, color=’red’, linewidth=4, label=’Левое фокусное кольцо (внешнее)’)

ax.plot(X_ring_right, Y_ring, Z_ring, color=’darkorange’, linewidth=4, label=’Правое фокусное кольцо (внешнее)’)

# Для наглядности соединим центры колец с самими кольцами тонкими лучами (как спицы)

for angle in np.linspace(0, 2*np.pi, 8):

    ax.plot([-f, -f], [0, R*np.cos(angle)], [0, R*np.sin(angle)], color=’red’, linestyle=’:’, alpha=0.5)

    ax.plot([f, f], [0, R*np.cos(angle)], [0, R*np.sin(angle)], color=’darkorange’, linestyle=’:’, alpha=0.5)

# 4. Ось вращения

ax.plot([-a*1.2, a*1.2], [0, 0], [0, 0], color=’black’, linestyle=’-.’, linewidth=2, label=’Центральная ось вращения’)

# Настройка вида

ax.set_title(‘3D Псевдоэллипсоид с Внешними Фокусными Кольцами’, fontsize=16, fontweight=’bold’, pad=20)

ax.set_xlabel(‘Ось X (продольная)’, fontweight=’bold’)

ax.set_ylabel(‘Ось Y’, fontweight=’bold’)

ax.set_zlabel(‘Ось Z’, fontweight=’bold’)

# Выравнивание масштабов 3D

max_range = np.array([X_mesh.max()-X_mesh.min(), Y_mesh.max()-Y_mesh.min(), Z_mesh.max()-Z_mesh.min()]).max() / 2.0

mid_x = 0

mid_y = 0

mid_z = 0

ax.set_xlim(mid_x — max_range, mid_x + max_range)

ax.set_ylim(mid_y — max_range, mid_y + max_range)

ax.set_zlim(mid_z — max_range, mid_z + max_range)

# Убираем серый фон панелей для «чистого» космического вида

ax.xaxis.pane.fill = False

ax.yaxis.pane.fill = False

ax.zaxis.pane.fill = False

ax.legend(loc=’upper right’, fontsize=11, framealpha=0.9)

# Угол обзора (чуть сбоку и сверху, чтобы хорошо видеть кольца)

ax.view_init(elev=20, azim=45)

plt.tight_layout()

plt.show()

Параметры фигуры:

f = 2.0

R = 8.0

a = 8.0

Длина = 16.0

Максимальный радиус = 8.0

Максимальный диаметр = 16.0

import numpy as np

import matplotlib.pyplot as plt

# =========================

# Параметры псевдоэллипсоида ГВИ

# =========================

f = 2.0

R = 8.0

# Вычисляем границу по X

a = R**2 / (4 * f)

# =========================

# 1. Образующая в плоскости XY

# y(x) = sqrt(4 f |x|) = 2*sqrt(f*|x|)

# =========================

x = np.linspace(-a, a, 1000)

y = 2 * np.sqrt(f * np.abs(x))

# Проверка размеров

length = 2 * a

max_radius = R

max_diameter = 2 * R

print(«Параметры фигуры:»)

print(f»f = {f}»)

print(f»R = {R}»)

print(f»a = {a}»)

print(f»Длина = {length}»)

print(f»Максимальный радиус = {max_radius}»)

print(f»Максимальный диаметр = {max_diameter}»)

# =========================

# 2. Построение 2D-сечения

# =========================

plt.figure(figsize=(10, 6))

plt.plot(x, y, label=r’$y(x)=2\sqrt{f|x|}$’, linewidth=2)

plt.axhline(R, linestyle=’—‘, linewidth=1.5, label=’Ось вращения: y = R’)

plt.axvline(-a, linestyle=’:’, linewidth=1)

plt.axvline(a, linestyle=’:’, linewidth=1)

# Фокусы двумерного сечения

plt.scatter([f, -f], [0, 0], s=60, label=’Фокусы’)

plt.text(f, 0.3, ‘F1′, ha=’center’)

plt.text(-f, 0.3, ‘F2′, ha=’center’)

plt.title(‘Образующая псевдоэллипсоида ГВИ в плоскости XY’)

plt.xlabel(‘X’)

plt.ylabel(‘Y’)

plt.axis(‘equal’)

plt.grid(True)

plt.legend()

plt.tight_layout()

# =========================

# 3. Построение 3D-поверхности вращения

# Вращение вокруг прямой y = R, параллельной оси X

# =========================

theta = np.linspace(0, 2 * np.pi, 180)

X, Theta = np.meshgrid(x, theta)

Y_curve = 2 * np.sqrt(f * np.abs(X))

rho = R — Y_curve   # расстояние от образующей до оси вращения

# Параметризация поверхности

Y = R + rho * np.cos(Theta)

Z = rho * np.sin(Theta)

fig = plt.figure(figsize=(12, 9))

ax = fig.add_subplot(111, projection=’3d’)

ax.plot_surface(X, Y, Z, rstride=2, cstride=2, edgecolor=’none’, alpha=0.9)

# Ось вращения

ax.plot(x, np.full_like(x, R), np.zeros_like(x), linestyle=’—‘, linewidth=2)

# Фокальные окружности при x = ±f, радиус R

phi = np.linspace(0, 2 * np.pi, 300)

# Окружность при x = +f

x_f1 = np.full_like(phi, f)

y_f1 = R + R * np.cos(phi)

z_f1 = R * np.sin(phi)

ax.plot(x_f1, y_f1, z_f1, linewidth=2)

# Окружность при x = -f

x_f2 = np.full_like(phi, -f)

y_f2 = R + R * np.cos(phi)

z_f2 = R * np.sin(phi)

ax.plot(x_f2, y_f2, z_f2, linewidth=2)

ax.set_title(‘3D-поверхность псевдоэллипсоида ГВИ’)

ax.set_xlabel(‘X’)

ax.set_ylabel(‘Y’)

ax.set_zlabel(‘Z’)

# Для более наглядной геометрии

ax.set_box_aspect([length, 2 * R, 2 * R])

plt.tight_layout()

plt.show()

import numpy as np

import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D

# Настройка стиля

plt.style.use(‘seaborn-v0_8-darkgrid’)

plt.rcParams.update({‘font.size’: 11})

# ============================================================================

# ВХОДНЫЕ ПАРАМЕТРЫ

# ============================================================================

f = 2.0  # Фокусное расстояние парабол

R = 8.0  # Высота куполов от экватора

a = (R**2) / (4 * f)  # Экваториальный радиус (a = 8.0)

# ============================================================================

# МАТЕМАТИКА ФОРМЫ

# ============================================================================

z_vals = np.linspace(-R, R, 400)

# Радиус сечения r(z) для горизонтального псевдопараболоида

r_vals = (R — np.abs(z_vals))**2 / (4 * f)

# ============================================================================

# ВИЗУАЛИЗАЦИЯ

# ============================================================================

fig = plt.figure(figsize=(18, 6))

# —————————————————————————-

# 1. ОБРАЗУЮЩАЯ (Одна ветвь)

# —————————————————————————-

ax1 = fig.add_subplot(131)

ax1.plot(r_vals, z_vals, ‘b-‘, linewidth=3, label=’Образующая: r(z)’)

ax1.axvline(0, color=’red’, linestyle=’—‘, linewidth=2, label=’Ось вращения (Z)’)

# Геометрические фокусы парабол образующей

ax1.plot(f, R, ‘g*’, markersize=14, label=’Верхний фокус’)

ax1.plot(f, -R, ‘g*’, markersize=14, label=’Нижний фокус’)

ax1.set_title(‘1. Образующая (Параболы)’, fontsize=14, fontweight=’bold’)

ax1.set_xlabel(‘Радиус r (Ось X)’)

ax1.set_ylabel(‘Высота (Ось Z)’)

ax1.set_xlim(-1, a*1.2)

ax1.set_ylim(-R*1.2, R*1.2)

ax1.legend(loc=’lower right’)

ax1.set_aspect(‘equal’)

# —————————————————————————-

# 2. ПОЛНОЕ 2D СЕЧЕНИЕ

# —————————————————————————-

ax2 = fig.add_subplot(132)

ax2.plot(r_vals, z_vals, ‘b-‘, linewidth=2.5)

ax2.plot(-r_vals, z_vals, ‘b-‘, linewidth=2.5)

ax2.axvline(0, color=’red’, linestyle=’—‘, linewidth=1.5, alpha=0.5)

# Экваториальный внутренний аттрактор

ax2.plot(a, 0, ‘yX’, markersize=12, label=’Внутренний острый экватор’)

ax2.plot(-a, 0, ‘yX’, markersize=12)

# ВНЕШНИЕ ФОКУСЫ (4 точки в 2D сечении)

ax2.plot(f, R, ‘g*’, markersize=12, label=’Внешние фокусы’)

ax2.plot(-f, R, ‘g*’, markersize=12)

ax2.plot(f, -R, ‘g*’, markersize=12)

ax2.plot(-f, -R, ‘g*’, markersize=12)

ax2.set_title(‘2. Полное 2D Сечение’, fontsize=14, fontweight=’bold’)

ax2.set_xlabel(‘Ось X’)

ax2.set_ylabel(‘Ось Z’)

ax2.set_xlim(-a*1.2, a*1.2)

ax2.set_ylim(-R*1.2, R*1.2)

ax2.legend(loc=’lower center’, fontsize=9, bbox_to_anchor=(0.5, -0.2), ncol=2)

ax2.set_aspect(‘equal’)

# —————————————————————————-

# 3. 3D МОДЕЛЬ (С ФОКАЛЬНЫМИ КОЛЬЦАМИ)

# —————————————————————————-

ax3 = fig.add_subplot(133, projection=’3d’)

theta = np.linspace(0, 2*np.pi, 100)

Z_mesh, Theta_mesh = np.meshgrid(z_vals, theta)

R_mesh = (R — np.abs(Z_mesh))**2 / (4 * f)

X_mesh = R_mesh * np.cos(Theta_mesh)

Y_mesh = R_mesh * np.sin(Theta_mesh)

# Поверхность диска

surf = ax3.plot_surface(X_mesh, Y_mesh, Z_mesh, cmap=’plasma’, alpha=0.7, edgecolor=’none’)

# ВНУТРЕННИЙ АТТРАКТОР (Экваториальная кромка)

X_eq = a * np.cos(theta); Y_eq = a * np.sin(theta); Z_eq = np.zeros_like(theta)

ax3.plot(X_eq, Y_eq, Z_eq, color=’yellow’, linewidth=3, label=’Внутренний экватор (Аттрактор)’)

# ВНЕШНИЕ ФОКАЛЬНЫЕ КОЛЬЦА («НИМБЫ»)

# Верхнее кольцо (z = R, радиус = f)

X_halo_top = f * np.cos(theta); Y_halo_top = f * np.sin(theta); Z_halo_top = np.full_like(theta, R)

ax3.plot(X_halo_top, Y_halo_top, Z_halo_top, color=’lime’, linewidth=4, label=’Верхнее фокальное кольцо (Внешнее)’)

# Нижнее кольцо (z = -R, радиус = f)

Z_halo_bot = np.full_like(theta, -R)

ax3.plot(X_halo_top, Y_halo_top, Z_halo_bot, color=’lime’, linewidth=4, label=’Нижнее фокальное кольцо (Внешнее)’)

# Центральная ось

ax3.plot([0,0], [0,0], [-R*1.2, R*1.2], ‘r—‘, linewidth=2, alpha=0.5)

ax3.set_title(‘3. Горизонтальный Псевдопараболоид’, fontsize=14, fontweight=’bold’)

ax3.set_xlabel(‘X’)

ax3.set_ylabel(‘Y’)

ax3.set_zlabel(‘Z’)

max_range = a * 1.1

ax3.set_xlim(-max_range, max_range)

ax3.set_ylim(-max_range, max_range)

ax3.set_zlim(-max_range, max_range)

ax3.xaxis.pane.fill = False; ax3.yaxis.pane.fill = False; ax3.zaxis.pane.fill = False

ax3.legend(loc=’lower center’, fontsize=9, bbox_to_anchor=(0.5, -0.15))

plt.tight_layout()

plt.show()

МОНТЕ КАРТО ПЕРЕСЧЁТ

  • 200 лучей на каждую точку сетки,
  • 50 отражений на луч,
  • 2 режима возбуждения:
    • центральный источник,
    • равномерное объёмное возбуждение,
  • метрика удержания:
    вероятность p_run_ge5, то есть шанс получить не менее 5 подряд отражений в асимптотической клиновой зоне аттрактора.

#!/usr/bin/env python3

# -*- coding: utf-8 -*-

from __future__ import annotations

import math, os

from typing import Iterable

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from matplotlib.ticker import PercentFormatter

OUT_DIR = «mc_corrected_pseudoparaboloids_v3»

os.makedirs(OUT_DIR, exist_ok=True)

try:

    plt.style.use(«seaborn-v0_8-whitegrid»)

except Exception:

    plt.style.use(«default»)

plt.rcParams.update({«font.size»:11, «figure.figsize»:(10,7), «axes.grid»:True, «grid.alpha»:0.3})

R_BASE=10.0

U0=0.20

N_RAYS=200

N_REFL=50

BURN_IN=8

CENTER_BALL_FRAC=0.03

K_GRID=[0.05,0.10,0.125,0.1667,0.25,0.50]

TARGET_ETAS=[0.95,0.90,0.80,0.70,0.60,0.50]

def a_from(R,f): return R*R/(4.0*f)

def F_vertical(p,R,f):

    x,y,z=p

    return math.hypot(y,z) — (R — math.sqrt(max(0.0,4.0*f*abs(x))))

def F_horizontal(p,R,f):

    x,y,z=p

    return math.hypot(x,y) — ((R-abs(z))**2)/(4.0*f)

def normal_vertical(p,R,f,eps=1e-9):

    x,y,z=p

    rho=max(math.hypot(y,z),eps); ax=max(abs(x),eps)

    grad=np.array([(math.sqrt(f/ax))*(1 if x>=0 else -1), y/rho, z/rho], float)

    return grad/np.linalg.norm(grad)

def normal_horizontal(p,R,f,eps=1e-9):

    x,y,z=p

    rho=max(math.hypot(x,y),eps)

    dz=0.0 if abs(z)<eps else ((R-abs(z))/(2.0*f))*(1 if z>=0 else -1)

    grad=np.array([x/rho,y/rho,dz], float)

    return grad/np.linalg.norm(grad)

def rand_dir(rng):

    v=rng.normal(size=3); return v/np.linalg.norm(v)

def sample_center_ball(R,rng,frac=CENTER_BALL_FRAC):

    r=frac*R*(rng.uniform()**(1/3)); u=rng.uniform(-1,1); phi=rng.uniform(0,2*np.pi)

    s=math.sqrt(max(0.0,1-u*u))

    return np.array([r*s*math.cos(phi), r*s*math.sin(phi), r*u], float)

def sample_uniform_vertical(R,f,rng):

    a=a_from(R,f); x=rng.uniform(-a,a)

    rv=max(R-math.sqrt(max(0.0,4.0*f*abs(x))),0.0)

    rho=rv*math.sqrt(rng.uniform()); phi=rng.uniform(0,2*np.pi)

    return np.array([x, rho*math.cos(phi), rho*math.sin(phi)], float)

def sample_uniform_horizontal(R,f,rng):

    z=rng.uniform(-R,R); rv=((R-abs(z))**2)/(4.0*f)

    rho=rv*math.sqrt(rng.uniform()); phi=rng.uniform(0,2*np.pi)

    return np.array([rho*math.cos(phi), rho*math.sin(phi), z], float)

def hit_boundary(F,p,d,step,tmax,max_steps=30000):

    t=step

    for _ in range(max_steps):

        if F(p+t*d)>=0:

            lo,hi=t-step,t

            for __ in range(28):

                mid=0.5*(lo+hi)

                if F(p+mid*d)<=0: lo=mid

                else: hi=mid

            return 0.5*(lo+hi)

        t+=step

        if t>tmax: return None

    return None

def trace_metric(kind,R,f,regime,seed):

    rng=np.random.default_rng(seed)

    a=a_from(R,f)

    step=max(R,a)/200.0

    tmax=2.5*max(R,a)

    max_runs=[]; refl_fracs=[]

    for _ in range(N_RAYS):

        if regime==’center’:

            p=sample_center_ball(R,rng)

        else:

            p=sample_uniform_vertical(R,f,rng) if kind==’vertical’ else sample_uniform_horizontal(R,f,rng)

        d=rand_dir(rng)

        if kind==’vertical’:

            F=lambda q: F_vertical(q,R,f); normal=lambda q: normal_vertical(q,R,f)

        else:

            F=lambda q: F_horizontal(q,R,f); normal=lambda q: normal_horizontal(q,R,f)

        flags=[]

        for j in range(N_REFL):

            t=hit_boundary(F,p,d,step,tmax)

            if t is None: break

            pt=p+t*d

            if j>=BURN_IN:

                if kind==’vertical’:

                    s=a-abs(pt[0]); active=(s/a)<=U0

                else:

                    rho=math.hypot(pt[0],pt[1]); delta=a-rho; active=(delta/a)<=U0

                flags.append(active)

            n=normal(pt); d=d-2*np.dot(d,n)*n; d=d/np.linalg.norm(d); p=pt-1e-7*n

        if flags:

            refl_fracs.append(sum(flags)/len(flags))

            cur=mr=0

            for fl in flags:

                if fl: cur+=1; mr=max(mr,cur)

                else: cur=0

            max_runs.append(mr)

    max_runs=np.array(max_runs,float); refl_fracs=np.array(refl_fracs,float)

    return {‘refl_frac’:float(np.mean(refl_fracs)), ‘mean_max_run’:float(np.mean(max_runs)), ‘p_run_ge5’:float(np.mean(max_runs>=5)), ‘p_run_ge8’:float(np.mean(max_runs>=8))}

def fit_power(Ks, etas):

    best=None

    for C in np.logspace(-2,4,400):

        for p in np.linspace(0.5,4.5,401):

            pred=1.0/(1.0 + C*(Ks**p))

            err=np.mean((pred-etas)**2)

            if best is None or err<best[0]:

                best=(err,C,p)

    return best

def K_from_eta(eta,C,p): return ((1.0/eta — 1.0)/C)**(1.0/p)

def save_table_png(df, title, filename):

    fig_h=0.8+0.55*len(df)

    fig,ax=plt.subplots(figsize=(12,fig_h)); ax.axis(‘off’)

    table=ax.table(cellText=df.values,colLabels=df.columns,cellLoc=’center’,loc=’center’)

    table.auto_set_font_size(False); table.set_fontsize(10); table.scale(1.0,1.45)

    ax.set_title(title,pad=16)

    plt.tight_layout(); plt.savefig(os.path.join(OUT_DIR,filename), dpi=180, bbox_inches=’tight’); plt.close()

def main():

    rows=[]

    seedbase=0

    for regime in [‘center’,’uniform’]:

        for K in K_GRID:

            for kind in [‘vertical’,’horizontal’]:

                seedbase+=1

                f=K*R_BASE

                m=trace_metric(kind,R_BASE,f,regime,1000+seedbase)

                rows.append({‘regime’:regime,’kind’:kind,’K’:K,’f’:f,’a’:a_from(R_BASE,f),**m})

                print(regime,kind,K,m)

    df=pd.DataFrame(rows)

    df.to_csv(os.path.join(OUT_DIR,’mc_raw_long.csv’), index=False, encoding=’utf-8-sig’)

    pivot=df.pivot_table(index=[‘regime’,’K’], columns=’kind’, values=’p_run_ge5′).reset_index()

    pivot.to_csv(os.path.join(OUT_DIR,’mc_raw_pivot.csv’), index=False, encoding=’utf-8-sig’)

    fits={}

    for regime in [‘center’,’uniform’]:

        d=pivot[pivot.regime==regime]

        Ks=d[‘K’].to_numpy()

        for kind in [‘vertical’,’horizontal’]:

            fits[(regime,kind)] = fit_power(Ks, d[kind].to_numpy())

    with open(os.path.join(OUT_DIR,’fit_summary.txt’),’w’,encoding=’utf-8′) as f:

        for regime in [‘center’,’uniform’]:

            for kind in [‘vertical’,’horizontal’]:

                _,C,p=fits[(regime,kind)]

                f.write(f'{regime} {kind}: eta = 1 / (1 + {C:.4f} * K^{p:.4f})\\n’)

    tables=[]

    for regime in [‘center’,’uniform’]:

        _,Cv,pv=fits[(regime,’vertical’)]

        _,Ch,ph=fits[(regime,’horizontal’)]

        rows=[]

        for eta in TARGET_ETAS:

            Kv=K_from_eta(eta,Cv,pv); Kh=K_from_eta(eta,Ch,ph)

            rows.append({

                ‘eta_target’:eta,

                ‘eta_percent’:f'{eta*100:.0f}%’,

                ‘K_vertical’:Kv,

                ‘f_vertical’:R_BASE*Kv,

                ‘a_vertical’:a_from(R_BASE,R_BASE*Kv),

                ‘alpha_vertical_deg’:math.degrees(math.atan(2*Kv)),

                ‘K_horizontal’:Kh,

                ‘f_horizontal’:R_BASE*Kh,

                ‘a_horizontal’:a_from(R_BASE,R_BASE*Kh),

                ‘alpha_horizontal_deg’:math.degrees(math.atan(2*Kh)),

            })

        t=pd.DataFrame(rows).round(4)

        t.to_csv(os.path.join(OUT_DIR,f’mc_refined_{regime}_table.csv’), index=False, encoding=’utf-8-sig’)

        save_table_png(t, f’Monte Carlo refined table — {regime}’, f’mc_refined_{regime}_table.png’)

        tables.append((regime,t))

    Ks = np.array(K_GRID)

    for regime in [‘center’,’uniform’]:

        d=pivot[pivot.regime==regime]

        Kplot=np.logspace(np.log10(Ks.min()*0.8), np.log10(Ks.max()*1.2), 400)

        plt.figure(figsize=(10,6))

        for kind, marker in [(‘vertical’,’o’),(‘horizontal’,’s’)]:

            plt.semilogx(d[‘K’].to_numpy(), d[kind].to_numpy(), marker, label=f'{kind} raw’)

            _,C,p=fits[(regime,kind)]

            plt.semilogx(Kplot, 1/(1+C*Kplot**p), label=f'{kind} fit’)

        plt.gca().yaxis.set_major_formatter(PercentFormatter(1.0))

        plt.ylim(0,1.0); plt.xlabel(‘K = f/R’); plt.ylabel(‘η_MC (p_run_ge5)’)

        plt.title(f’Monte Carlo fit — {regime}’)

        plt.legend(); plt.grid(True,alpha=0.3); plt.tight_layout()

        plt.savefig(os.path.join(OUT_DIR,f’mc_fit_{regime}.png’), dpi=180, bbox_inches=’tight’); plt.close()

if __name__ == ‘__main__’:

    main()