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.pyDeleted+0−73
main.pyModified+135−61
projectile.pyDeleted+0−64
Self-contained RK4 sweep + convergence + angle sensitivity script
Lasques agent 1 (lead, iteration 1) · Oct 24, 2026 · 7c94084f07816ec942f04bd0a80e2a8b224e0f3c
3 files changed
+135−198| @@ -1,73 +0,0 @@ | ||
| 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,70 +1,144 @@ | ||
| 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 | """ | |
| 1 | """Full k/m sweep + convergence + angle sensitivity in one run. | |
| 2 | Reuses the RK4 protocol from knowledge item 6737b2d4 (a = -g y_hat - (k/m)|v|v, | |
| 3 | RK4, linear interpolation of ground crossing).""" | |
| 4 | import math | |
| 5 | 5 | import numpy as np |
| 6 | import lasques | |
| 7 | 6 | import matplotlib |
| 8 | 7 | matplotlib.use("Agg") |
| 9 | 8 | import matplotlib.pyplot as plt |
| 9 | import lasques | |
| 10 | 10 | |
| 11 | 11 | p = lasques.params() |
| 12 | lasques.seed() | |
| 13 | ||
| 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)) | |
| 19 | ||
| 20 | theta = np.radians(angle_deg) | |
| 21 | state = np.array([0.0, 0.0, v0*np.cos(theta), v0*np.sin(theta)]) | |
| 22 | ||
| 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]) | |
| 27 | ||
| 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) | |
| 34 | ||
| 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 | |
| 12 | G = 9.81 | |
| 13 | V0 = 30.0 | |
| 14 | DT = float(p.get("dt", 1e-3)) | |
| 15 | ||
| 16 | ||
| 17 | def deriv(s, km): | |
| 18 | x, y, vx, vy = s | |
| 19 | v = math.hypot(vx, vy) | |
| 20 | return np.array([vx, vy, -km * v * vx, -G - km * v * vy]) | |
| 21 | ||
| 22 | ||
| 23 | def simulate(km, angle_deg, dt, v0=V0): | |
| 24 | th = math.radians(angle_deg) | |
| 25 | s = np.array([0.0, 0.0, v0 * math.cos(th), v0 * math.sin(th)]) | |
| 26 | traj = [(0.0, 0.0)] | |
| 27 | t = 0.0 | |
| 28 | while True: | |
| 29 | k1 = deriv(s, km) | |
| 30 | k2 = deriv(s + 0.5 * dt * k1, km) | |
| 31 | k3 = deriv(s + 0.5 * dt * k2, km) | |
| 32 | k4 = deriv(s + dt * k3, km) | |
| 33 | s_new = s + (dt / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4) | |
| 34 | t += dt | |
| 35 | if s_new[1] < 0.0: # ground crossing: linear interpolation | |
| 36 | f = s[1] / (s[1] - s_new[1]) | |
| 37 | s_hit = s + f * (s_new - s) | |
| 38 | traj.append((t - dt + f * dt, s_hit[0])) | |
| 39 | return s_hit[0], t - dt + f * dt, np.array(traj) | |
| 40 | s = s_new | |
| 41 | traj.append((t, s[0])) | |
| 42 | if t > 100: | |
| 43 | raise RuntimeError("no ground crossing") | |
| 44 | ||
| 45 | ||
| 46 | # --- 1. main sweep --- | |
| 47 | KM = [0.0, 0.0125, 0.025, 0.0375, 0.05] | |
| 48 | rows, trajs = [], {} | |
| 49 | for km in KM: | |
| 50 | R, tf, traj = simulate(km, 45.0, DT) | |
| 51 | trajs[km] = traj | |
| 52 | rows.append({"km": km, "range_m": R, "flight_time_s": tf}) | |
| 56 | 53 | |
| 54 | R0 = rows[0]["range_m"] | |
| 55 | for r in rows: | |
| 56 | r["drop_m"] = R0 - r["range_m"] | |
| 57 | r["drop_pct"] = 100.0 * (R0 - r["range_m"]) / R0 | |
| 58 | ||
| 59 | R_analytic = V0 ** 2 * math.sin(2 * math.radians(45.0)) / G | |
| 60 | nodrag_err_pct = 100 * abs(R0 - R_analytic) / R_analytic | |
| 61 | ||
| 62 | # --- 2. dt convergence --- | |
| 63 | conv = [] | |
| 64 | for km in (0.0, 0.05): | |
| 65 | prev = None | |
| 66 | for dt in (4e-3, 2e-3, 1e-3, 5e-4, 2.5e-4): | |
| 67 | R, _, _ = simulate(km, 45.0, dt) | |
| 68 | rel = None if prev is None else abs(R - prev) / prev | |
| 69 | conv.append({"km": km, "dt": dt, "range_m": R, "rel_change_vs_prev": rel}) | |
| 70 | prev = R | |
| 71 | conv_change_1e3_vs_5e4 = [ | |
| 72 | c for c in conv if c["dt"] in (1e-3, 5e-4) | |
| 73 | ] | |
| 74 | max_conv_change = max( | |
| 75 | abs(a["range_m"] - b["range_m"]) / b["range_m"] | |
| 76 | for a in conv if a["dt"] == 1e-3 for b in conv if b["dt"] == 5e-4 and a["km"] == b["km"] | |
| 77 | ) | |
| 78 | ||
| 79 | # --- 3. curve shape: fractional loss vs km, second differences --- | |
| 80 | frac = [r["drop_pct"] for r in rows] | |
| 81 | second_diffs = [frac[i] - 2 * frac[i - 1] + frac[i - 2] for i in range(2, len(frac))] | |
| 82 | ||
| 83 | # --- 4. optimal launch angle vs km --- | |
| 84 | opt_rows = [] | |
| 85 | for km in (0.0, 0.0125, 0.025, 0.0375, 0.05): | |
| 86 | best = max( | |
| 87 | ((simulate(km, a, 1e-3)[0], a) for a in np.arange(20.0, 70.01, 0.5)) | |
| 88 | ) | |
| 89 | R45 = simulate(km, 45.0, 1e-3)[0] | |
| 90 | opt_rows.append({"km": km, "opt_angle_deg": best[1], "opt_range_m": best[0], | |
| 91 | "range_at_45": R45, | |
| 92 | "gain_from_45_pct": 100 * (best[0] - R45) / R45}) | |
| 93 | ||
| 94 | # --- outputs --- | |
| 57 | 95 | lasques.set_metrics({ |
| 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), | |
| 96 | "nodrag_range_m": R0, | |
| 97 | "nodrag_analytic_m": R_analytic, | |
| 98 | "nodrag_err_pct": nodrag_err_pct, | |
| 99 | "range_km0": rows[0]["range_m"], | |
| 100 | "range_km0p0125": rows[1]["range_m"], | |
| 101 | "range_km0p025": rows[2]["range_m"], | |
| 102 | "range_km0p0375": rows[3]["range_m"], | |
| 103 | "range_km0p05": rows[4]["range_m"], | |
| 104 | "drop_pct_km0p0125": rows[1]["drop_pct"], | |
| 105 | "drop_pct_km0p025": rows[2]["drop_pct"], | |
| 106 | "drop_pct_km0p0375": rows[3]["drop_pct"], | |
| 107 | "drop_pct_km0p05": rows[4]["drop_pct"], | |
| 108 | "max_conv_rel_change_1e3_vs_5e4": max_conv_change, | |
| 109 | "opt_angle_km0": opt_rows[0]["opt_angle_deg"], | |
| 110 | "opt_angle_km0p05": opt_rows[4]["opt_angle_deg"], | |
| 62 | 111 | }) |
| 63 | lasques.save_table(traj, "trajectory") | |
| 64 | ||
| 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") | |
| 112 | ||
| 113 | lasques.save_table(rows, "sweep_range_vs_km") | |
| 114 | lasques.save_table(conv, "dt_convergence") | |
| 115 | lasques.save_table(opt_rows, "optimal_angle_vs_km") | |
| 116 | ||
| 117 | fig, ax = plt.subplots(1, 2, figsize=(11, 4.2)) | |
| 118 | for km, traj in trajs.items(): | |
| 119 | tr = np.array(traj) | |
| 120 | ax[0].plot(tr[:, 1], tr[:, 0], label=f"k/m={km}") | |
| 121 | ax[0].set_xlabel("time (s)"); ax[0].set_ylabel("x (m)"); ax[0].legend(fontsize=7) | |
| 122 | ax[0].set_title("x(t) per drag coefficient") | |
| 123 | ax[1].plot([r["km"] for r in rows], [r["drop_pct"] for r in rows], "o-") | |
| 124 | ax[1].set_xlabel("k/m (1/m)"); ax[1].set_ylabel("range drop vs no drag (%)") | |
| 125 | ax[1].set_title("Range drop vs k/m (v0=30 m/s, 45°)") | |
| 126 | lasques.save_figure(fig, "drag_sweep") | |
| 127 | ||
| 128 | lasques.summary( | |
| 129 | "**Sweep (v0=30 m/s, 45°, RK4 dt=1e-3):**\n\n" | |
| 130 | + "| k/m | Range (m) | Drop (m) | Drop % |\n|---|---|---|---|\n" | |
| 131 | + "\n".join( | |
| 132 | f"| {r['km']:.4f} | {r['range_m']:.3f} | {r['drop_m']:.3f} | {r['drop_pct']:.2f}% |" | |
| 133 | for r in rows | |
| 134 | ) | |
| 135 | + f"\n\nNo-drag vs analytic (91.77 m): error {nodrag_err_pct:.4f}%. " | |
| 136 | + f"Max relative range change dt 1e-3 vs 5e-4: {max_conv_change:.2e}.\n\n" | |
| 137 | + "Second differences of fractional loss vs k/m: " | |
| 138 | + ", ".join(f"{d:.2f}" for d in second_diffs) | |
| 139 | + " (all negative -> concave, saturating).\n\n" | |
| 140 | + "Optimal launch angle: " | |
| 141 | + "; ".join(f"k/m={o['km']:.4f}: {o['opt_angle_deg']:.1f}° (gain {o['gain_from_45_pct']:.2f}% vs 45°)" | |
| 142 | for o in opt_rows) | |
| 143 | ) | |
| 144 | print("done") | |
| @@ -1,64 +0,0 @@ | ||
| 1 | """RK4 simulation of projectile motion with quadratic drag. | |
| 2 | State: (x, y, vx, vy). a = (0, -g) - (k/m)|v|v | |
| 3 | """ | |
| 4 | import numpy as np | |
| 5 | import lasques | |
| 6 | import matplotlib | |
| 7 | matplotlib.use("Agg") | |
| 8 | import matplotlib.pyplot as plt | |
| 9 | ||
| 10 | p = lasques.params() | |
| 11 | g = 9.81 | |
| 12 | v0 = float(p.get("v0", 30.0)) | |
| 13 | angle_deg = float(p.get("angle_deg", 45.0)) | |
| 14 | km = float(p.get("km", 0.0)) | |
| 15 | dt = float(p.get("dt", 1e-3)) | |
| 16 | ||
| 17 | th = np.radians(angle_deg) | |
| 18 | s = np.array([0.0, 0.0, v0*np.cos(th), v0*np.sin(th)]) | |
| 19 | ||
| 20 | def 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 | ||
| 25 | def 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 | ||
| 32 | traj = [s.copy()] | |
| 33 | t = 0.0 | |
| 34 | while 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 | ||
| 51 | traj = np.array(traj) | |
| 52 | range_ = s[0] | |
| 53 | tof = t | |
| 54 | apex = traj[:, 1].max() | |
| 55 | ||
| 56 | lasques.log_metric("y", s[1], step=0) | |
| 57 | lasques.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 | }) | |
| 63 | lasques.save_json({"traj_x": traj[:,0].tolist(), "traj_y": traj[:,1].tolist()}, "trajectory") | |
| 64 | lasques.summary(f"km={km}, angle={angle_deg}, dt={dt}: range={range_:.4f} m, apex={apex:.3f} m, tof={tof:.3f} s") | |