modified: examples/case01/input/input.txt

modified:   least_action.py
This commit is contained in:
2026-06-03 16:36:23 +08:00
parent af6c816dcf
commit f5c5e350c9
2 changed files with 573 additions and 86 deletions
+1 -1
View File
@@ -4,5 +4,5 @@ ta: 0
xb: 20 xb: 20
tb: 10 tb: 10
N: 100 N: 100
xmax: 1.0 xmax: 1.0 0.5 0.1 0.01 0.001
seed: 42 seed: 42
+558 -71
View File
@@ -1,15 +1,16 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import html
import random import random
from pathlib import Path from pathlib import Path
from typing import Dict, List from typing import Dict, List, Sequence
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import yaml import yaml
def load_input(input_path: Path) -> Dict[str, float]: def load_input(input_path: Path) -> Dict[str, object]:
with input_path.open("r", encoding="utf-8") as file: with input_path.open("r", encoding="utf-8") as file:
data = yaml.safe_load(file) or {} data = yaml.safe_load(file) or {}
@@ -27,28 +28,53 @@ def load_input(input_path: Path) -> Dict[str, float]:
return defaults return defaults
def parse_xmax_values(raw_value: object) -> List[float]:
if isinstance(raw_value, (int, float)):
return [float(raw_value)]
if isinstance(raw_value, str):
normalized = raw_value.replace(",", " ")
values = [float(part) for part in normalized.split() if part]
if values:
return values
if isinstance(raw_value, list):
values = [float(item) for item in raw_value]
if values:
return values
raise ValueError("xmax must be a number, a YAML list, or a space-separated string of numbers.")
def build_straight_path(xa: float, xb: float, n_points: int) -> List[float]: def build_straight_path(xa: float, xb: float, n_points: int) -> List[float]:
if n_points < 2: if n_points < 2:
raise ValueError("N must be at least 2.") raise ValueError("N must be at least 2.")
return [ return [xa + (xb - xa) * step / (n_points - 1) for step in range(n_points)]
xa + (xb - xa) * step / (n_points - 1)
for step in range(n_points)
]
def build_random_path(straight_path: List[float], xmax: float) -> List[float]: def build_noise_profile(n_points: int, seed: object) -> List[float]:
if len(straight_path) < 2: if n_points < 2:
raise ValueError("N must be at least 2.") raise ValueError("N must be at least 2.")
rng = random.Random(seed)
return [rng.uniform(-1.0, 1.0) for _ in range(n_points - 2)]
def build_random_path(straight_path: Sequence[float], xmax: float, noise_profile: Sequence[float]) -> List[float]:
if len(straight_path) < 2:
raise ValueError("N must be at least 2.")
if len(noise_profile) != len(straight_path) - 2:
raise ValueError("Noise profile length must match the number of interior points.")
path = [straight_path[0]] path = [straight_path[0]]
for base_x in straight_path[1:-1]: for base_x, noise in zip(straight_path[1:-1], noise_profile):
path.append(base_x + random.uniform(-1.0, 1.0) * xmax) path.append(base_x + noise * xmax)
path.append(straight_path[-1]) path.append(straight_path[-1])
return path return path
def kinetic_action(path: List[float], mass: float, dt: float) -> float: def kinetic_action(path: Sequence[float], mass: float, dt: float) -> float:
action = 0.0 action = 0.0
for left, right in zip(path[:-1], path[1:]): for left, right in zip(path[:-1], path[1:]):
velocity = (right - left) / dt velocity = (right - left) / dt
@@ -65,38 +91,98 @@ def build_time_points(ta: float, tb: float, n_points: int) -> List[float]:
return [ta + step * dt for step in range(n_points)] return [ta + step * dt for step in range(n_points)]
def draw_circle(draw: ImageDraw.ImageDraw, center: tuple[float, float], size: int, color: tuple[int, int, int]) -> None:
px, py = center
draw.ellipse([(px - size, py - size), (px + size, py + size)], fill=color, outline=color)
def draw_square(draw: ImageDraw.ImageDraw, center: tuple[float, float], size: int, color: tuple[int, int, int]) -> None:
px, py = center
draw.rectangle([(px - size, py - size), (px + size, py + size)], fill=color, outline=color)
def draw_triangle(draw: ImageDraw.ImageDraw, center: tuple[float, float], size: int, color: tuple[int, int, int]) -> None:
px, py = center
points = [(px, py - size), (px - size, py + size), (px + size, py + size)]
draw.polygon(points, fill=color, outline=color)
def draw_diamond(draw: ImageDraw.ImageDraw, center: tuple[float, float], size: int, color: tuple[int, int, int]) -> None:
px, py = center
points = [(px, py - size), (px - size, py), (px, py + size), (px + size, py)]
draw.polygon(points, fill=color, outline=color)
def draw_cross(draw: ImageDraw.ImageDraw, center: tuple[float, float], size: int, color: tuple[int, int, int]) -> None:
px, py = center
draw.line([(px - size, py - size), (px + size, py + size)], fill=color, width=2)
draw.line([(px - size, py + size), (px + size, py - size)], fill=color, width=2)
def draw_plus(draw: ImageDraw.ImageDraw, center: tuple[float, float], size: int, color: tuple[int, int, int]) -> None:
px, py = center
draw.line([(px - size, py), (px + size, py)], fill=color, width=2)
draw.line([(px, py - size), (px, py + size)], fill=color, width=2)
def draw_marker(
draw: ImageDraw.ImageDraw,
center: tuple[float, float],
marker_name: str,
size: int,
color: tuple[int, int, int],
) -> None:
if marker_name == "circle":
draw_circle(draw, center, size, color)
elif marker_name == "square":
draw_square(draw, center, size, color)
elif marker_name == "triangle":
draw_triangle(draw, center, size, color)
elif marker_name == "diamond":
draw_diamond(draw, center, size, color)
elif marker_name == "cross":
draw_cross(draw, center, size, color)
else:
draw_plus(draw, center, size, color)
def draw_trajectory_image( def draw_trajectory_image(
image_path: Path, image_path: Path,
time_points: List[float], time_points: Sequence[float],
random_path: List[float], straight_path: Sequence[float],
straight_path: List[float], random_cases: Sequence[Dict[str, object]],
) -> None: ) -> None:
width = 1200 width = 1800
height = 800 height = 1200
margin_left = 110 margin_left = 150
margin_right = 60 margin_right = 460
margin_top = 80 margin_top = 110
margin_bottom = 110 margin_bottom = 150
image = Image.new("RGB", (width, height), "white") image = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
try: try:
title_font = ImageFont.truetype("DejaVuSans.ttf", 34) title_font = ImageFont.truetype("DejaVuSans.ttf", 50)
label_font = ImageFont.truetype("DejaVuSans.ttf", 24) label_font = ImageFont.truetype("DejaVuSans.ttf", 34)
tick_font = ImageFont.truetype("DejaVuSans.ttf", 20) tick_font = ImageFont.truetype("DejaVuSans.ttf", 28)
legend_font = ImageFont.truetype("DejaVuSans.ttf", 24) legend_title_font = ImageFont.truetype("DejaVuSans.ttf", 34)
legend_font = ImageFont.truetype("DejaVuSans.ttf", 28)
except OSError: except OSError:
title_font = ImageFont.load_default() title_font = ImageFont.load_default()
label_font = ImageFont.load_default() label_font = ImageFont.load_default()
tick_font = ImageFont.load_default() tick_font = ImageFont.load_default()
legend_title_font = ImageFont.load_default()
legend_font = ImageFont.load_default() legend_font = ImageFont.load_default()
all_x = random_path + straight_path all_positions = list(straight_path)
for case in random_cases:
all_positions.extend(case["path"])
t_min = min(time_points) t_min = min(time_points)
t_max = max(time_points) t_max = max(time_points)
x_min = min(all_x) x_min = min(all_positions)
x_max = max(all_x) x_max = max(all_positions)
if t_min == t_max: if t_min == t_max:
t_max = t_min + 1.0 t_max = t_min + 1.0
@@ -125,50 +211,421 @@ def draw_trajectory_image(
for tick in range(tick_count + 1): for tick in range(tick_count + 1):
x_value = t_min + (t_max - t_min) * tick / tick_count x_value = t_min + (t_max - t_min) * tick / tick_count
y_value = x_min + (x_max - x_min) * tick / tick_count y_value = x_min + (x_max - x_min) * tick / tick_count
tick_x = plot_left + (plot_right - plot_left) * tick / tick_count tick_x = plot_left + (plot_right - plot_left) * tick / tick_count
tick_y = plot_bottom - (plot_bottom - plot_top) * tick / tick_count tick_y = plot_bottom - (plot_bottom - plot_top) * tick / tick_count
draw.line([(tick_x, plot_top), (tick_x, plot_bottom)], fill=(225, 225, 225), width=1) draw.line([(tick_x, plot_top), (tick_x, plot_bottom)], fill=(225, 225, 225), width=1)
draw.line([(plot_left, tick_y), (plot_right, tick_y)], fill=(225, 225, 225), width=1) draw.line([(plot_left, tick_y), (plot_right, tick_y)], fill=(225, 225, 225), width=1)
draw.line([(tick_x, plot_bottom), (tick_x, plot_bottom + 12)], fill=(0, 0, 0), width=3)
draw.line([(plot_left - 12, tick_y), (plot_left, tick_y)], fill=(0, 0, 0), width=3)
draw.line([(tick_x, plot_bottom), (tick_x, plot_bottom + 8)], fill=(0, 0, 0), width=2) draw.text((tick_x - 28, plot_bottom + 24), f"{x_value:.1f}", fill=(0, 0, 0), font=tick_font)
draw.line([(plot_left - 8, tick_y), (plot_left, tick_y)], fill=(0, 0, 0), width=2) draw.text((28, tick_y - 14), f"{y_value:.1f}", fill=(0, 0, 0), font=tick_font)
draw.text((tick_x - 20, plot_bottom + 15), f"{x_value:.1f}", fill=(0, 0, 0), font=tick_font)
draw.text((20, tick_y - 10), f"{y_value:.1f}", fill=(0, 0, 0), font=tick_font)
random_points = [map_point(t, x) for t, x in zip(time_points, random_path)]
straight_points = [map_point(t, x) for t, x in zip(time_points, straight_path)] straight_points = [map_point(t, x) for t, x in zip(time_points, straight_path)]
draw.line(straight_points, fill=(35, 95, 210), width=6)
for point in (straight_points[0], straight_points[-1]):
draw_circle(draw, point, 8, (35, 95, 210))
draw.line(straight_points, fill=(40, 110, 220), width=4) marker_step = max(1, len(time_points) // 12)
draw.line(random_points, fill=(220, 70, 70), width=3) for case in random_cases:
points = [map_point(t, x) for t, x in zip(time_points, case["path"])]
draw.line(points, fill=case["color"], width=4)
for point in points[::marker_step]:
draw_marker(draw, point, case["marker"], 6, case["color"])
for px, py in random_points: draw.text((margin_left, 28), "Least Action Trajectory Comparison", fill=(0, 0, 0), font=title_font)
draw.ellipse([(px - 3, py - 3), (px + 3, py + 3)], fill=(220, 70, 70)) draw.text((width // 2 - 70, height - 72), "time t", fill=(0, 0, 0), font=label_font)
draw.text((24, 28), "position x", fill=(0, 0, 0), font=label_font)
for px, py in (straight_points[0], straight_points[-1]): legend_left = plot_right + 24
draw.ellipse([(px - 5, py - 5), (px + 5, py + 5)], fill=(40, 110, 220)) legend_top = margin_top
legend_bottom = plot_bottom
draw.text( draw.rectangle(
(margin_left, 20), [legend_left - 10, legend_top, width - 30, legend_bottom],
"Least Action Trajectory Comparison", fill=(250, 248, 242),
fill=(0, 0, 0), outline=(210, 205, 195),
font=title_font, width=2,
) )
draw.text((width // 2 - 45, height - 50), "time t", fill=(0, 0, 0), font=label_font) draw.text((legend_left, legend_top + 22), "Legend", fill=(0, 0, 0), font=legend_title_font)
draw.text((20, 20), "position x", fill=(0, 0, 0), font=label_font)
legend_x = width - 290 item_y = legend_top + 102
legend_y = 28 draw.line([(legend_left, item_y + 14), (legend_left + 66, item_y + 14)], fill=(35, 95, 210), width=6)
draw.line([(legend_x, legend_y + 10), (legend_x + 40, legend_y + 10)], fill=(40, 110, 220), width=4) draw.text((legend_left + 86, item_y - 4), "straight path", fill=(0, 0, 0), font=legend_font)
draw.text((legend_x + 50, legend_y - 6), "straight path", fill=(0, 0, 0), font=legend_font) item_y += 64
draw.line([(legend_x, legend_y + 40), (legend_x + 40, legend_y + 40)], fill=(220, 70, 70), width=3)
draw.text((legend_x + 50, legend_y + 24), "random path", fill=(0, 0, 0), font=legend_font) for case in random_cases:
draw.line([(legend_left, item_y + 14), (legend_left + 66, item_y + 14)], fill=case["color"], width=4)
draw_marker(draw, (legend_left + 33, item_y + 14), case["marker"], 6, case["color"])
label = f"xmax = {case['xmax']:.6g}"
draw.text((legend_left + 86, item_y - 4), label, fill=(0, 0, 0), font=legend_font)
item_y += 60
image.save(image_path, format="JPEG", quality=95) image.save(image_path, format="JPEG", quality=95)
def render_stats_cards(case_count: int, straight_action: float, min_xmax: float, best_ratio: float) -> str:
return "\n".join(
[
'<div class="stats">',
' <div class="stat"><span class="stat-label">Straight Action</span>'
f'<span class="stat-value">{straight_action:.4f}</span></div>',
' <div class="stat"><span class="stat-label">Random Cases</span>'
f'<span class="stat-value">{case_count}</span></div>',
' <div class="stat"><span class="stat-label">Smallest xmax</span>'
f'<span class="stat-value">{min_xmax:.6g}</span></div>',
' <div class="stat"><span class="stat-label">Best Ratio</span>'
f'<span class="stat-value">{best_ratio:.4f}</span></div>',
'</div>',
]
)
def build_summary_rows(straight_action: float, random_cases: Sequence[Dict[str, object]]) -> str:
rows: List[str] = []
for case in random_cases:
rows.append(
"<tr>"
f"<td>{case['xmax']:.6g}</td>"
f"<td>{case['random_action']:.6f}</td>"
f"<td>{straight_action:.6f}</td>"
f"<td>{case['ratio']:.6f}</td>"
f"<td>{case['difference']:.6f}</td>"
"</tr>"
)
return "\n".join(rows)
def build_observation(random_cases: Sequence[Dict[str, object]]) -> str:
first_case = random_cases[0]
last_case = random_cases[-1]
return (
f"在这组同源扰动下,当 xmax 从 {first_case['xmax']:.6g} 逐步减小到 {last_case['xmax']:.6g} 时,"
f"随机路径作用量从 {first_case['random_action']:.4f} 收敛到 {last_case['random_action']:.4f}"
f"并逐渐靠近匀速直线轨迹的作用量。"
)
def write_index_html(
output_dir: Path,
mass: float,
n_points: int,
seed: object,
straight_action: float,
random_cases: Sequence[Dict[str, object]],
) -> None:
min_xmax = min(case["xmax"] for case in random_cases)
best_ratio = min(case["ratio"] for case in random_cases)
stats_cards = render_stats_cards(len(random_cases), straight_action, min_xmax, best_ratio)
summary_rows = build_summary_rows(straight_action, random_cases)
observation = html.escape(build_observation(random_cases))
html_text = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Least Action Case 01</title>
<style>
:root {{
--bg: #f4f1ea;
--panel: #fffdf8;
--ink: #1c1b1a;
--muted: #5e5a54;
--line: #d8d1c4;
--accent: #245eb8;
--accent-2: #c44747;
}}
* {{
box-sizing: border-box;
}}
body {{
margin: 0;
font-family: "Georgia", "Noto Serif SC", serif;
background:
radial-gradient(circle at top right, rgba(36, 94, 184, 0.10), transparent 24rem),
radial-gradient(circle at left 20%, rgba(196, 71, 71, 0.08), transparent 20rem),
var(--bg);
color: var(--ink);
}}
main {{
width: min(1180px, calc(100vw - 32px));
margin: 32px auto 56px;
}}
.hero,
.panel {{
background: var(--panel);
border: 1px solid var(--line);
border-radius: 24px;
box-shadow: 0 16px 48px rgba(45, 39, 29, 0.08);
}}
.hero {{
padding: 28px 30px;
margin-bottom: 24px;
}}
.panel {{
padding: 24px;
margin-bottom: 24px;
}}
.eyebrow {{
margin: 0 0 8px;
color: var(--muted);
font-size: 14px;
letter-spacing: 0.08em;
text-transform: uppercase;
}}
h1, h2 {{
margin: 0;
font-weight: 700;
}}
h1 {{
font-size: clamp(30px, 4vw, 44px);
line-height: 1.1;
}}
h2 {{
font-size: 28px;
margin-bottom: 16px;
}}
.subtitle,
.note,
.observation {{
color: var(--muted);
line-height: 1.7;
font-size: 17px;
}}
.stats {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
margin-top: 26px;
}}
.stat {{
padding: 16px 18px;
background: #faf7ef;
border: 1px solid var(--line);
border-radius: 18px;
}}
.stat-label {{
display: block;
color: var(--muted);
font-size: 13px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.05em;
}}
.stat-value {{
font-size: 24px;
font-weight: 700;
}}
.trajectory-image {{
width: 100%;
display: block;
border-radius: 18px;
border: 1px solid var(--line);
background: white;
}}
table {{
width: 100%;
border-collapse: collapse;
overflow: hidden;
border-radius: 18px;
border: 1px solid var(--line);
background: white;
}}
th, td {{
padding: 14px 16px;
border-bottom: 1px solid var(--line);
text-align: left;
}}
th {{
background: #f7f1e7;
}}
.formula-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 18px;
}}
.formula-card {{
padding: 18px 20px;
border: 1px solid var(--line);
border-radius: 18px;
background: #fffdfa;
}}
.formula-card h3 {{
margin: 0 0 12px;
font-size: 22px;
}}
.formula-card p {{
margin: 0 0 12px;
color: var(--muted);
line-height: 1.7;
}}
.formula-block {{
margin: 14px 0;
padding: 14px 16px;
border-radius: 14px;
background: #faf7ef;
overflow-x: auto;
}}
.formula-block.blue {{
border-left: 4px solid var(--accent);
}}
.formula-block.red {{
border-left: 4px solid var(--accent-2);
}}
code {{
font-family: "Consolas", "SFMono-Regular", monospace;
font-size: 0.95em;
}}
</style>
<script>
window.MathJax = {{
tex: {{
inlineMath: [['\\\\(', '\\\\)']],
displayMath: [['\\\\[', '\\\\]']],
packages: {{'[+]': ['ams', 'textmacros']}}
}},
options: {{
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
}}
}};
</script>
<script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
</head>
<body>
<main>
<section class="hero">
<p class="eyebrow">Least Action / Case 01</p>
<h1>自由粒子最小作用量收敛示例</h1>
<p class="subtitle">
页面使用同一组基础扰动,分别按不同的 <code>xmax</code> 缩放随机路径,并比较它们的离散作用量如何逐步收敛到匀速直线轨迹的作用量。
</p>
{stats_cards}
<p class="note">
参数:<code>m={mass:.6g}</code>
<code>N={n_points}</code>
<code>seed={seed}</code>
<code>xmax={", ".join(f"{case['xmax']:.6g}" for case in random_cases)}</code>
</p>
</section>
<section class="panel">
<h2>轨迹对比图</h2>
<img class="trajectory-image" src="trajectory.jpg" alt="不同 xmax 下的随机轨迹与匀速直线轨迹对比图" />
</section>
<section class="panel">
<h2>收敛汇总</h2>
<p class="observation">{observation}</p>
<table>
<thead>
<tr>
<th>xmax</th>
<th>random action</th>
<th>straight action</th>
<th>random / straight</th>
<th>difference</th>
</tr>
</thead>
<tbody>
{summary_rows}
</tbody>
</table>
</section>
<section class="panel">
<h2>作用量公式</h2>
<div class="formula-grid">
<article class="formula-card">
<h3>匀速直线轨迹</h3>
<p>先按起点和终点做等时间间隔的线性插值,再基于离散速度计算作用量。</p>
<div class="formula-block blue">
\\[
x_i = x_a + \\frac{{i}}{{N-1}}(x_b - x_a), \\qquad i = 0,1,\\dots,N-1
\\]
</div>
<div class="formula-block blue">
\\[
\\Delta t = \\frac{{t_b - t_a}}{{N-1}}, \\qquad
v_i = \\frac{{x_{{i+1}} - x_i}}{{\\Delta t}}
\\]
</div>
<div class="formula-block blue">
\\[
S_{{\\mathrm{{straight}}}} = \\sum_{{i=0}}^{{N-2}}\\frac{{1}}{{2}}m v_i^2 \\Delta t
\\]
</div>
</article>
<article class="formula-card">
<h3>随机扰动轨迹</h3>
<p>以直线路径为基线,在每个中间采样点加入均匀分布扰动,并保持起点终点不变。</p>
<div class="formula-block red">
\\[
x_i^{{(\\mathrm{{rand}})}} = x_i + \\epsilon_i, \\qquad
\\epsilon_i \\sim U(-x_{{\\max}}, x_{{\\max}})
\\]
</div>
<div class="formula-block red">
\\[
x_0^{{(\\mathrm{{rand}})}} = x_a, \\qquad
x_{{N-1}}^{{(\\mathrm{{rand}})}} = x_b
\\]
</div>
<div class="formula-block red">
\\[
v_i^{{(\\mathrm{{rand}})}} =
\\frac{{x_{{i+1}}^{{(\\mathrm{{rand}})}} - x_i^{{(\\mathrm{{rand}})}}}}{{\\Delta t}}
\\]
</div>
<div class="formula-block red">
\\[
S_{{\\mathrm{{rand}}}} =
\\sum_{{i=0}}^{{N-2}}\\frac{{1}}{{2}}m\\left(v_i^{{(\\mathrm{{rand}})}}\\right)^2 \\Delta t
\\]
</div>
</article>
</div>
<p class="note">
页面中的公式由 MathJax 在浏览器中渲染。若本地打开后暂时没有看到公式样式,请确认浏览器能够访问
<code>cdn.jsdelivr.net</code>。
</p>
</section>
</main>
</body>
</html>
"""
(output_dir / "index.html").write_text(html_text, encoding="utf-8")
def solve_case(input_path: Path, output_path: Path) -> str: def solve_case(input_path: Path, output_path: Path) -> str:
params = load_input(input_path) params = load_input(input_path)
@@ -178,7 +635,7 @@ def solve_case(input_path: Path, output_path: Path) -> str:
xb = float(params["xb"]) xb = float(params["xb"])
tb = float(params["tb"]) tb = float(params["tb"])
n_points = int(params["N"]) n_points = int(params["N"])
xmax = float(params["xmax"]) xmax_values = parse_xmax_values(params["xmax"])
seed = params.get("seed") seed = params.get("seed")
if n_points < 2: if n_points < 2:
@@ -186,22 +643,42 @@ def solve_case(input_path: Path, output_path: Path) -> str:
if tb <= ta: if tb <= ta:
raise ValueError("tb must be greater than ta.") raise ValueError("tb must be greater than ta.")
if seed is not None:
random.seed(seed)
dt = (tb - ta) / (n_points - 1) dt = (tb - ta) / (n_points - 1)
time_points = build_time_points(ta=ta, tb=tb, n_points=n_points) time_points = build_time_points(ta=ta, tb=tb, n_points=n_points)
straight_path = build_straight_path(xa=xa, xb=xb, n_points=n_points) straight_path = build_straight_path(xa=xa, xb=xb, n_points=n_points)
random_path = build_random_path(straight_path=straight_path, xmax=xmax)
random_action = kinetic_action(path=random_path, mass=mass, dt=dt)
straight_action = kinetic_action(path=straight_path, mass=mass, dt=dt) straight_action = kinetic_action(path=straight_path, mass=mass, dt=dt)
noise_profile = build_noise_profile(n_points=n_points, seed=seed)
palette = [
(220, 70, 70),
(28, 150, 110),
(220, 145, 40),
(125, 85, 220),
(40, 135, 205),
(180, 95, 145),
]
markers = ["circle", "square", "triangle", "diamond", "cross", "plus"]
random_cases: List[Dict[str, object]] = []
for index, xmax in enumerate(xmax_values):
random_path = build_random_path(straight_path=straight_path, xmax=xmax, noise_profile=noise_profile)
random_action = kinetic_action(path=random_path, mass=mass, dt=dt)
ratio = random_action / straight_action if straight_action != 0 else float("inf") ratio = random_action / straight_action if straight_action != 0 else float("inf")
random_cases.append(
{
"xmax": xmax,
"path": random_path,
"random_action": random_action,
"ratio": ratio,
"difference": random_action - straight_action,
"color": palette[index % len(palette)],
"marker": markers[index % len(markers)],
}
)
image_path = output_path.parent / "trajectory.jpg" image_path = output_path.parent / "trajectory.jpg"
result = "\n".join( result_lines = [
[
"Least Action Case 01: Free Particle", "Least Action Case 01: Free Particle",
f"input_file: {input_path}", f"input_file: {input_path}",
f"trajectory_image: {image_path}", f"trajectory_image: {image_path}",
@@ -211,24 +688,34 @@ def solve_case(input_path: Path, output_path: Path) -> str:
f"xb: {xb}", f"xb: {xb}",
f"tb: {tb}", f"tb: {tb}",
f"N: {n_points}", f"N: {n_points}",
f"xmax: {xmax}", f"xmax_values: {', '.join(f'{value:.6g}' for value in xmax_values)}",
f"seed: {seed}", f"seed: {seed}",
f"dt: {dt}", f"dt: {dt}",
f"random_action: {random_action:.10f}",
f"straight_action: {straight_action:.10f}", f"straight_action: {straight_action:.10f}",
f"random_over_straight: {ratio:.10f}",
] ]
) for case in random_cases:
result_lines.append(f"xmax={case['xmax']:.6g} random_action={case['random_action']:.10f}")
result_lines.append(f"xmax={case['xmax']:.6g} random_over_straight={case['ratio']:.10f}")
output_path.parent.mkdir(parents=True, exist_ok=True) output_path.parent.mkdir(parents=True, exist_ok=True)
draw_trajectory_image( draw_trajectory_image(
image_path=image_path, image_path=image_path,
time_points=time_points, time_points=time_points,
random_path=random_path,
straight_path=straight_path, straight_path=straight_path,
random_cases=random_cases,
) )
output_path.write_text(result + "\n", encoding="utf-8") write_index_html(
return result output_dir=output_path.parent,
mass=mass,
n_points=n_points,
seed=seed,
straight_action=straight_action,
random_cases=random_cases,
)
result_text = "\n".join(result_lines)
output_path.write_text(result_text + "\n", encoding="utf-8")
return result_text
def main() -> None: def main() -> None: