Files
least_action/least_action.py
T

245 lines
7.9 KiB
Python

from __future__ import annotations
import argparse
import random
from pathlib import Path
from typing import Dict, List
from PIL import Image, ImageDraw, ImageFont
import yaml
def load_input(input_path: Path) -> Dict[str, float]:
with input_path.open("r", encoding="utf-8") as file:
data = yaml.safe_load(file) or {}
defaults = {
"m": 1.0,
"xa": 0.0,
"ta": 0.0,
"xb": 20.0,
"tb": 10.0,
"N": 100,
"xmax": 1.0,
"seed": None,
}
defaults.update(data)
return defaults
def build_straight_path(xa: float, xb: float, n_points: int) -> List[float]:
if n_points < 2:
raise ValueError("N must be at least 2.")
return [
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]:
if len(straight_path) < 2:
raise ValueError("N must be at least 2.")
path = [straight_path[0]]
for base_x in straight_path[1:-1]:
path.append(base_x + random.uniform(-1.0, 1.0) * xmax)
path.append(straight_path[-1])
return path
def kinetic_action(path: List[float], mass: float, dt: float) -> float:
action = 0.0
for left, right in zip(path[:-1], path[1:]):
velocity = (right - left) / dt
kinetic_energy = 0.5 * mass * velocity * velocity
action += kinetic_energy * dt
return action
def build_time_points(ta: float, tb: float, n_points: int) -> List[float]:
if n_points < 2:
raise ValueError("N must be at least 2.")
dt = (tb - ta) / (n_points - 1)
return [ta + step * dt for step in range(n_points)]
def draw_trajectory_image(
image_path: Path,
time_points: List[float],
random_path: List[float],
straight_path: List[float],
) -> None:
width = 1200
height = 800
margin_left = 110
margin_right = 60
margin_top = 80
margin_bottom = 110
image = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(image)
try:
title_font = ImageFont.truetype("DejaVuSans.ttf", 34)
label_font = ImageFont.truetype("DejaVuSans.ttf", 24)
tick_font = ImageFont.truetype("DejaVuSans.ttf", 20)
legend_font = ImageFont.truetype("DejaVuSans.ttf", 24)
except OSError:
title_font = ImageFont.load_default()
label_font = ImageFont.load_default()
tick_font = ImageFont.load_default()
legend_font = ImageFont.load_default()
all_x = random_path + straight_path
t_min = min(time_points)
t_max = max(time_points)
x_min = min(all_x)
x_max = max(all_x)
if t_min == t_max:
t_max = t_min + 1.0
if x_min == x_max:
x_max = x_min + 1.0
x_padding = max((x_max - x_min) * 0.08, 1.0)
x_min -= x_padding
x_max += x_padding
plot_left = margin_left
plot_right = width - margin_right
plot_top = margin_top
plot_bottom = height - margin_bottom
def map_point(time_value: float, position_value: float) -> tuple[float, float]:
x_ratio = (time_value - t_min) / (t_max - t_min)
y_ratio = (position_value - x_min) / (x_max - x_min)
px = plot_left + x_ratio * (plot_right - plot_left)
py = plot_bottom - y_ratio * (plot_bottom - plot_top)
return px, py
draw.rectangle([plot_left, plot_top, plot_right, plot_bottom], outline=(0, 0, 0), width=2)
tick_count = 5
for tick in range(tick_count + 1):
x_value = t_min + (t_max - t_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_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([(plot_left, tick_y), (plot_right, tick_y)], fill=(225, 225, 225), width=1)
draw.line([(tick_x, plot_bottom), (tick_x, plot_bottom + 8)], fill=(0, 0, 0), width=2)
draw.line([(plot_left - 8, tick_y), (plot_left, tick_y)], fill=(0, 0, 0), width=2)
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)]
draw.line(straight_points, fill=(40, 110, 220), width=4)
draw.line(random_points, fill=(220, 70, 70), width=3)
for px, py in random_points:
draw.ellipse([(px - 3, py - 3), (px + 3, py + 3)], fill=(220, 70, 70))
for px, py in (straight_points[0], straight_points[-1]):
draw.ellipse([(px - 5, py - 5), (px + 5, py + 5)], fill=(40, 110, 220))
draw.text(
(margin_left, 20),
"Least Action Trajectory Comparison",
fill=(0, 0, 0),
font=title_font,
)
draw.text((width // 2 - 45, height - 50), "time t", fill=(0, 0, 0), font=label_font)
draw.text((20, 20), "position x", fill=(0, 0, 0), font=label_font)
legend_x = width - 290
legend_y = 28
draw.line([(legend_x, legend_y + 10), (legend_x + 40, legend_y + 10)], fill=(40, 110, 220), width=4)
draw.text((legend_x + 50, legend_y - 6), "straight path", fill=(0, 0, 0), font=legend_font)
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)
image.save(image_path, format="JPEG", quality=95)
def solve_case(input_path: Path, output_path: Path) -> str:
params = load_input(input_path)
mass = float(params["m"])
xa = float(params["xa"])
ta = float(params["ta"])
xb = float(params["xb"])
tb = float(params["tb"])
n_points = int(params["N"])
xmax = float(params["xmax"])
seed = params.get("seed")
if n_points < 2:
raise ValueError("N must be at least 2.")
if tb <= ta:
raise ValueError("tb must be greater than ta.")
if seed is not None:
random.seed(seed)
dt = (tb - ta) / (n_points - 1)
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)
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)
ratio = random_action / straight_action if straight_action != 0 else float("inf")
image_path = output_path.parent / "trajectory.jpg"
result = "\n".join(
[
"Least Action Case 01: Free Particle",
f"input_file: {input_path}",
f"trajectory_image: {image_path}",
f"m: {mass}",
f"xa: {xa}",
f"ta: {ta}",
f"xb: {xb}",
f"tb: {tb}",
f"N: {n_points}",
f"xmax: {xmax}",
f"seed: {seed}",
f"dt: {dt}",
f"random_action: {random_action:.10f}",
f"straight_action: {straight_action:.10f}",
f"random_over_straight: {ratio:.10f}",
]
)
output_path.parent.mkdir(parents=True, exist_ok=True)
draw_trajectory_image(
image_path=image_path,
time_points=time_points,
random_path=random_path,
straight_path=straight_path,
)
output_path.write_text(result + "\n", encoding="utf-8")
return result
def main() -> None:
parser = argparse.ArgumentParser(description="Compute action for a free particle path.")
parser.add_argument("input", type=Path, help="YAML input file path.")
parser.add_argument("output", type=Path, help="Output result file path.")
args = parser.parse_args()
solve_case(input_path=args.input, output_path=args.output)
if __name__ == "__main__":
main()