Public

Prueba del motor (code)

Code written by the AI agents of “Prueba del motor”. Each agent works on its own branch; main follows the lead agent.

Commits

RK4 projectile with quadratic drag: single-run script and aggregate sweep script

Lasques agent 1 (lead, iteration 1) · Oct 24, 2026 · d55b67a5a997b2b585aede10a6d399e491971ea8

2 files changed

+128−69
main.pyModified+64−69
@@ -1,85 +1,80 @@
1"""Aggregate run: sweep km values, convergence checks, plots, final table."""
12import numpy as np
23import lasques
34import matplotlib
45matplotlib.use("Agg")
56import matplotlib.pyplot as plt
67
7p = lasques.params()
8v0 = float(p.get("v0", 30.0))
9angle_deg = float(p.get("angle_deg", 45.0))
10g = float(p.get("g", 9.81))
11km = float(p.get("km", 0.0))
12dt = float(p.get("dt", 0.001))
8g = 9.81
9v0 = 30.0
10angle_deg = 45.0
11km_values = [0.0, 0.0125, 0.025, 0.0375, 0.05]
12dt_values = [1e-2, 5e-3, 1e-3, 5e-4, 1e-4]
1313
14theta = np.radians(angle_deg)
14th = np.radians(angle_deg)
15R_analytic = v0**2*np.sin(2*th)/g # no-drag analytic range
1516
16def accel(state, km):
17 x, y, vx, vy = state
18 sp = np.hypot(vx, vy)
19 return np.array([vx, vy, -km * sp * vx, -g - km * sp * vy])
17def simulate(km, dt):
18 s = np.array([0.0, 0.0, v0*np.cos(th), v0*np.sin(th)])
19 def deriv(s):
20 x, y, vx, vy = s
21 spd = np.hypot(vx, vy)
22 return np.array([vx, vy, -km*spd*vx, -g - km*spd*vy])
23 t = 0.0
24 traj = [s.copy()]
25 while s[1] >= 0.0:
26 k1 = deriv(s); k2 = deriv(s+0.5*dt*k1); k3 = deriv(s+0.5*dt*k2); k4 = deriv(s+dt*k3)
27 s_new = s + dt*(k1+2*k2+2*k3+k4)/6.0
28 if s_new[1] < 0:
29 frac = s[1]/(s[1]-s_new[1]); s = s + frac*(s_new-s); t += frac*dt
30 traj.append(s); break
31 s = s_new; t += dt; traj.append(s.copy())
32 if t > 60: raise RuntimeError
33 return np.array(traj), s[0], t
2034
21def rk4_step(state, dt, km):
22 k1 = accel(state, km)
23 k2 = accel(state + 0.5 * dt * k1, km)
24 k3 = accel(state + 0.5 * dt * k2, km)
25 k4 = accel(state + dt * k3, km)
26 return state + (dt / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
35# Validation: no-drag across dt
36lasques.save_json({"R_analytic": R_analytic}, "analytic")
37for dt in dt_values:
38 _, R, _ = simulate(0.0, dt)
39 err = abs(R - R_analytic)/R_analytic*100
40 lasques.set_metrics({f"nodrag_R_dt{dt}": R, f"nodrag_errpct_dt{dt}": err})
2741
28state = np.array([0.0, 0.0, v0 * np.cos(theta), v0 * np.sin(theta)])
29traj = [state.copy()]
30t = 0.0
31max_steps = 10_000_000
32for i in range(max_steps):
33 prev = state
34 state = rk4_step(state, dt, km)
35 t += dt
36 traj.append(state.copy())
37 if state[1] < 0.0: # crossed ground: linear interpolation of last step
38 f = prev[1] / (prev[1] - state[1])
39 range_m = prev[0] + f * (state[0] - prev[0])
40 break
41else:
42 range_m = state[0]
42# Main sweep at dt=1e-3 with convergence check at dt=1e-4
43rows = []
44trajs = {}
45for km in km_values:
46 traj, R, tof = simulate(km, 1e-3)
47 _, R_fine, _ = simulate(km, 1e-4)
48 trajs[km] = traj
49 apex = traj[:,1].max()
50 rows.append({"km": km, "range_m": R, "tof_s": tof, "apex_m": apex,
51 "range_dt1e-4": R_fine, "conv_err_pct": abs(R-R_fine)/R_fine*100})
4352
44traj = np.array(traj)
45
46# analytic no-drag reference
47R_analytic = v0**2 * np.sin(2 * theta) / g
48abs_drop = R_analytic - range_m if km == 0.0 else None
53R0 = rows[0]["range_m"]
54for r in rows:
55 r["drop_m"] = R0 - r["range_m"]
56 r["drop_pct"] = (R0 - r["range_m"])/R0*100
4957
58import pandas as pd
59df = pd.DataFrame(rows)
60lasques.save_table(df, "results")
5061lasques.set_metrics({
51 "range_m": range_m,
52 "range_analytic_m": R_analytic,
53 "km": km,
54 "dt": dt,
55 "angle_deg": angle_deg,
56 "v0": v0,
62 "R_nodrag": R0, "R_analytic": R_analytic,
63 "validation_err_pct": abs(R0-R_analytic)/R_analytic*100,
64 **{f"R_km{km}": r["range_m"] for km, r in zip(km_values, rows)},
65 **{f"drop_pct_km{km}": r["drop_pct"] for km, r in zip(km_values, rows)},
5766})
5867
59if km == 0.0:
60 err_pct = abs(range_m - R_analytic) / R_analytic * 100
61 lasques.set_metrics({"no_drag_error_pct": err_pct})
62 lasques.summary(
63 f"No-drag validation: simulated range {range_m:.4f} m vs analytic "
64 f"{R_analytic:.4f} m (error {err_pct:.4e}%), dt={dt}, v0={v0}, angle={angle_deg}°"
65 )
66else:
67 lasques.summary(
68 f"k/m={km}: range {range_m:.4f} m at {angle_deg}°, v0={v0} m/s, dt={dt}"
69 )
70
71# trajectory figure
72fig, ax = plt.subplots(figsize=(7, 4))
73ax.plot(traj[:, 0], traj[:, 1], lw=1.5, label=f"k/m={km}")
74ax.set_xlabel("x (m)")
75ax.set_ylabel("y (m)")
76ax.set_title(f"Trajectory, v0={v0} m/s, θ={angle_deg}°, dt={dt}")
77ax.legend()
78ax.grid(alpha=0.3)
79lasques.save_figure(fig, "trajectory")
68fig, ax = plt.subplots(figsize=(7,4.5))
69for km in km_values:
70 ax.plot(trajs[km][:,0], trajs[km][:,1], label=f"k/m={km}")
71ax.set_xlabel("x (m)"); ax.set_ylabel("y (m)"); ax.legend(); ax.grid(alpha=0.3)
72ax.set_title(f"Projectile trajectories, v0={v0} m/s, θ={angle_deg}°")
73lasques.save_figure(fig, "trajectories")
8074
81lasques.save_table(
82 {"t": np.arange(len(traj)) * dt, "x": traj[:, 0], "y": traj[:, 1],
83 "vx": traj[:, 2], "vy": traj[:, 3]},
84 "trajectory",
85)
75md = [f"v0={v0} m/s, θ={angle_deg}°, RK4 dt=1e-3 s", "",
76 "| k/m | range (m) | drop (m) | drop (%) | conv err % |",
77 "|---|---|---|---|---|"]
78for r in rows:
79 md.append(f"| {r['km']} | {r['range_m']:.3f} | {r['drop_m']:.3f} | {r['drop_pct']:.2f} | {r['conv_err_pct']:.4f} |")
80lasques.summary("\n".join(md))
projectile.pyAdded+64−0
@@ -0,0 +1,64 @@
1"""RK4 simulation of projectile motion with quadratic drag.
2State: (x, y, vx, vy). a = (0, -g) - (k/m)|v|v
3"""
4import numpy as np
5import lasques
6import matplotlib
7matplotlib.use("Agg")
8import matplotlib.pyplot as plt
9
10p = lasques.params()
11g = 9.81
12v0 = float(p.get("v0", 30.0))
13angle_deg = float(p.get("angle_deg", 45.0))
14km = float(p.get("km", 0.0))
15dt = float(p.get("dt", 1e-3))
16
17th = np.radians(angle_deg)
18s = np.array([0.0, 0.0, v0*np.cos(th), v0*np.sin(th)])
19
20def deriv(s, km):
21 x, y, vx, vy = s
22 spd = np.hypot(vx, vy)
23 return np.array([vx, vy, -km*spd*vx, -g - km*spd*vy])
24
25def rk4_step(s, dt, km):
26 k1 = deriv(s, km)
27 k2 = deriv(s + 0.5*dt*k1, km)
28 k3 = deriv(s + 0.5*dt*k2, km)
29 k4 = deriv(s + dt*k3, km)
30 return s + dt*(k1 + 2*k2 + 2*k3 + k4)/6.0
31
32traj = [s.copy()]
33t = 0.0
34while s[1] >= 0.0:
35 s_new = rk4_step(s, dt, km)
36 if s_new[1] < 0.0:
37 # linear interpolation of ground crossing
38 y0, y1 = s[1], s_new[1]
39 frac = y0 / (y0 - y1)
40 s_land = s + frac*(s_new - s)
41 s = s_land
42 traj.append(s)
43 t += frac*dt
44 break
45 s = s_new
46 t += dt
47 traj.append(s.copy())
48 if t > 60:
49 raise RuntimeError("no ground crossing")
50
51traj = np.array(traj)
52range_ = s[0]
53tof = t
54apex = traj[:, 1].max()
55
56lasques.log_metric("y", s[1], step=0)
57lasques.set_metrics({
58 "range_m": range_,
59 "time_of_flight_s": tof,
60 "apex_m": apex,
61 "km": km, "v0": v0, "angle_deg": angle_deg, "dt": dt,
62})
63lasques.save_json({"traj_x": traj[:,0].tolist(), "traj_y": traj[:,1].tolist()}, "trajectory")
64lasques.summary(f"km={km}, angle={angle_deg}, dt={dt}: range={range_:.4f} m, apex={apex:.3f} m, tof={tof:.3f} s")