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.
Commitsaggregate.pyAdded+73−0
main.pyModified+55−65
RK4 quadratic-drag projectile simulation + aggregation script
Lasques agent 1 (lead, iteration 1) · Oct 24, 2026 · 58b176b7a9c7b820fe1c4ed7222d072172367971
2 files changed
+128−65| @@ -0,0 +1,73 @@ | ||
| 1 | """Aggregate runs: table of range vs km, drop vs no-drag, convergence check.""" | |
| 2 | import numpy as np, json, itertools | |
| 3 | import lasques | |
| 4 | import matplotlib | |
| 5 | matplotlib.use("Agg") | |
| 6 | import matplotlib.pyplot as plt | |
| 7 | ||
| 8 | p = lasques.params() | |
| 9 | lasques.seed() | |
| 10 | v0 = float(p.get("v0", 30.0)); g = 9.81; angle = float(p.get("angle_deg", 45.0)) | |
| 11 | km_values = [0.0, 0.0125, 0.025, 0.0375, 0.05] | |
| 12 | ||
| 13 | # analytic no-drag check | |
| 14 | R_analytic = v0**2*np.sin(np.radians(2*angle))/g | |
| 15 | ||
| 16 | def deriv(s, km): | |
| 17 | vx, vy = s[2], s[3] | |
| 18 | sp = np.hypot(vx, vy) | |
| 19 | return np.array([vx, vy, -km*sp*vx, -g - km*sp*vy]) | |
| 20 | ||
| 21 | def rk4_step(s, h, km): | |
| 22 | k1 = deriv(s, km); k2 = deriv(s+0.5*h*k1, km) | |
| 23 | k3 = deriv(s+0.5*h*k2, km); k4 = deriv(s+h*k3, km) | |
| 24 | return s + (h/6)*(k1+2*k2+2*k3+k4) | |
| 25 | ||
| 26 | def simulate(km, dt): | |
| 27 | th = np.radians(angle) | |
| 28 | s = np.array([0.,0., v0*np.cos(th), v0*np.sin(th)]) | |
| 29 | t = 0.0 | |
| 30 | while True: | |
| 31 | sn = rk4_step(s, dt, km) | |
| 32 | if s[1] > 0 >= sn[1]: | |
| 33 | f = s[1]/(s[1]-sn[1]) | |
| 34 | return float(s[0] + f*(sn[0]-s[0])), t + f*dt | |
| 35 | s = sn; t += dt | |
| 36 | if t > 1000: raise RuntimeError | |
| 37 | ||
| 38 | # main table at dt=0.001 | |
| 39 | rows = [] | |
| 40 | R0 = None | |
| 41 | for km in km_values: | |
| 42 | R, T = simulate(km, 0.001) | |
| 43 | if km == 0.0: R0 = R | |
| 44 | rows.append({"km": km, "range_m": R, "tof_s": T}) | |
| 45 | for r in rows: | |
| 46 | r["drop_m"] = R0 - r["range_m"] | |
| 47 | r["drop_pct"] = 100*(R0 - r["range_m"])/R0 | |
| 48 | ||
| 49 | # convergence: halving dt at km=0 and km=0.05 | |
| 50 | conv = [] | |
| 51 | for km in (0.0, 0.05): | |
| 52 | prev = None | |
| 53 | for dt in (0.004, 0.002, 0.001, 0.0005, 0.00025): | |
| 54 | R, _ = simulate(km, dt) | |
| 55 | rel = None if prev is None else abs(R-prev)/prev | |
| 56 | conv.append({"km": km, "dt": dt, "range_m": R, "rel_change": rel}) | |
| 57 | prev = R | |
| 58 | ||
| 59 | lasques.save_table(rows, "results") | |
| 60 | lasques.save_table(conv, "convergence") | |
| 61 | lasques.set_metrics({ | |
| 62 | "R_analytic_nodrag": R_analytic, | |
| 63 | "R_sim_nodrag": rows[0]["range_m"], | |
| 64 | "nodrag_error_pct": 100*abs(rows[0]["range_m"]-R_analytic)/R_analytic, | |
| 65 | **{f"range_km_{r['km']}": r["range_m"] for r in rows}, | |
| 66 | **{f"drop_pct_km_{r['km']}": r["drop_pct"] for r in rows}, | |
| 67 | }) | |
| 68 | lasques.summary(json.dumps({"rows": rows, "convergence": conv, "R_analytic": R_analytic}, indent=1)) | |
| 69 | ||
| 70 | fig, ax = plt.subplots(figsize=(6,4)) | |
| 71 | ax.plot([r["km"] for r in rows], [r["range_m"] for r in rows], "o-") | |
| 72 | ax.set_xlabel("k/m (1/m)"); ax.set_ylabel("Range (m)") | |
| 73 | lasques.save_figure(fig, "range_vs_km") | |
| @@ -1,80 +1,70 @@ | ||
| 1 | """Aggregate run: sweep km values, convergence checks, plots, final table.""" | |
| 1 | """Projectile with quadratic drag: a = -g yhat - (k/m)|v|v. | |
| 2 | RK4 with fine timestep + linear interpolation of ground crossing. | |
| 3 | Sweeps km in {0, 0.0125, 0.025, 0.0375, 0.05}, v0=30 m/s, angle 45 deg. | |
| 4 | """ | |
| 2 | 5 | import numpy as np |
| 3 | 6 | import lasques |
| 4 | 7 | import matplotlib |
| 5 | 8 | matplotlib.use("Agg") |
| 6 | 9 | import matplotlib.pyplot as plt |
| 7 | 10 | |
| 8 | g = 9.81 | |
| 9 | v0 = 30.0 | |
| 10 | angle_deg = 45.0 | |
| 11 | km_values = [0.0, 0.0125, 0.025, 0.0375, 0.05] | |
| 12 | dt_values = [1e-2, 5e-3, 1e-3, 5e-4, 1e-4] | |
| 11 | p = lasques.params() | |
| 12 | lasques.seed() | |
| 13 | 13 | |
| 14 | th = np.radians(angle_deg) | |
| 15 | R_analytic = v0**2*np.sin(2*th)/g # no-drag analytic range | |
| 14 | v0 = float(p.get("v0", 30.0)) | |
| 15 | angle_deg = float(p.get("angle_deg", 45.0)) | |
| 16 | km = float(p.get("km", 0.0)) # k/m [1/m] | |
| 17 | dt = float(p.get("dt", 0.001)) | |
| 18 | g = float(p.get("g", 9.81)) | |
| 16 | 19 | |
| 17 | def 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 | |
| 20 | theta = np.radians(angle_deg) | |
| 21 | state = np.array([0.0, 0.0, v0*np.cos(theta), v0*np.sin(theta)]) | |
| 34 | 22 | |
| 35 | # Validation: no-drag across dt | |
| 36 | lasques.save_json({"R_analytic": R_analytic}, "analytic") | |
| 37 | for 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}) | |
| 23 | def deriv(s): | |
| 24 | vx, vy = s[2], s[3] | |
| 25 | sp = np.hypot(vx, vy) | |
| 26 | return np.array([vx, vy, -km*sp*vx, -g - km*sp*vy]) | |
| 41 | 27 | |
| 42 | # Main sweep at dt=1e-3 with convergence check at dt=1e-4 | |
| 43 | rows = [] | |
| 44 | trajs = {} | |
| 45 | for 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}) | |
| 28 | def rk4_step(s, h): | |
| 29 | k1 = deriv(s) | |
| 30 | k2 = deriv(s + 0.5*h*k1) | |
| 31 | k3 = deriv(s + 0.5*h*k2) | |
| 32 | k4 = deriv(s + h*k3) | |
| 33 | return s + (h/6.0)*(k1 + 2*k2 + 2*k3 + k4) | |
| 52 | 34 | |
| 53 | R0 = rows[0]["range_m"] | |
| 54 | for r in rows: | |
| 55 | r["drop_m"] = R0 - r["range_m"] | |
| 56 | r["drop_pct"] = (R0 - r["range_m"])/R0*100 | |
| 35 | traj = [state.copy()] | |
| 36 | t = 0.0 | |
| 37 | max_steps = 2_000_000 | |
| 38 | for i in range(max_steps): | |
| 39 | s_new = rk4_step(state, dt) | |
| 40 | if state[1] > 0 >= s_new[1]: | |
| 41 | # linear interpolation to y=0 | |
| 42 | frac = state[1] / (state[1] - s_new[1]) | |
| 43 | s_hit = state + frac*(s_new - state) | |
| 44 | state = s_hit | |
| 45 | traj.append(s_hit) | |
| 46 | break | |
| 47 | state = s_new | |
| 48 | t += dt | |
| 49 | traj.append(state.copy()) | |
| 50 | else: | |
| 51 | raise RuntimeError("no ground hit") | |
| 52 | ||
| 53 | traj = np.array(traj) | |
| 54 | range_m = float(state[0]) | |
| 55 | tof = float(t + 0) # approximate | |
| 57 | 56 | |
| 58 | import pandas as pd | |
| 59 | df = pd.DataFrame(rows) | |
| 60 | lasques.save_table(df, "results") | |
| 61 | 57 | lasques.set_metrics({ |
| 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)}, | |
| 58 | "km": km, "dt": dt, "angle_deg": angle_deg, "v0": v0, | |
| 59 | "range_m": range_m, | |
| 60 | "apex_m": float(traj[:,1].max()), | |
| 61 | "tof_s": float(traj.shape[0]*dt), | |
| 66 | 62 | }) |
| 63 | lasques.save_table(traj, "trajectory") | |
| 67 | 64 | |
| 68 | fig, ax = plt.subplots(figsize=(7,4.5)) | |
| 69 | for km in km_values: | |
| 70 | ax.plot(trajs[km][:,0], trajs[km][:,1], label=f"k/m={km}") | |
| 71 | ax.set_xlabel("x (m)"); ax.set_ylabel("y (m)"); ax.legend(); ax.grid(alpha=0.3) | |
| 72 | ax.set_title(f"Projectile trajectories, v0={v0} m/s, θ={angle_deg}°") | |
| 73 | lasques.save_figure(fig, "trajectories") | |
| 74 | ||
| 75 | md = [f"v0={v0} m/s, θ={angle_deg}°, RK4 dt=1e-3 s", "", | |
| 76 | "| k/m | range (m) | drop (m) | drop (%) | conv err % |", | |
| 77 | "|---|---|---|---|---|"] | |
| 78 | for 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} |") | |
| 80 | lasques.summary("\n".join(md)) | |
| 65 | fig, ax = plt.subplots(figsize=(6,4)) | |
| 66 | ax.plot(traj[:,0], traj[:,1]) | |
| 67 | ax.set_xlabel("x (m)"); ax.set_ylabel("y (m)") | |
| 68 | ax.set_title(f"km={km}, dt={dt}, R={range_m:.4f} m") | |
| 69 | lasques.save_figure(fig, "trajectory_plot") | |
| 70 | lasques.summary(f"km={km} dt={dt}: range={range_m:.5f} m, apex={traj[:,1].max():.4f} m") | |