diff --git a/.gitignore b/.gitignore index 28cd8a1..8270e66 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,9 @@ CLAUDE.md AGENTS.md *.tmp + +.venv +*.env + +__pycache__/ +debug_runs/ diff --git a/assets/data/calibration/flightscope.csv b/assets/data/calibration/flightscope.csv index a261135..8409db0 100644 --- a/assets/data/calibration/flightscope.csv +++ b/assets/data/calibration/flightscope.csv @@ -1,24 +1,24 @@ shot_name,filename,speed_mph,vla_deg,hla_deg,total_spin_rpm,spin_axis_deg,backspin_rpm,sidespin_rpm,carry_yd,total_yd,rollout_yd,apex_ft -5iron,5iron.json,102.10,17.40,1.50,5391.0,12.30,5266.0,1151.0,138.1,142.2,0.0,59.6 -approach_mid_iron_test_shot,approach_mid_iron_test_shot.json,93.90,22.90,-2.60,5375.0,2.00,5375.0,0.0,125.8,128.5,0.0,69.3 -approach_test_shot,approach_test_shot.json,81.05,30.50,1.35,10489.8,5.74,10454.8,699.3,92.2,91.5,0.0,72.2 -bump_and_run,bump_and_run.json,58.27,15.57,-0.85,1850.1,2.31,1700.4,75.7,43.8,62,0.0,10.1 +5iron,5iron.json,102.10,17.40,1.50,5391.0,12.30,5266.0,1151.0,0.0,0.0,0.0,0.0 +approach_mid_iron_test_shot,approach_mid_iron_test_shot.json,93.90,22.90,-2.60,5375.0,2.00,5375.0,0.0,0.0,0.0,0.0,0.0 +approach_test_shot,approach_test_shot.json,81.05,30.50,1.35,10489.8,5.74,10454.8,699.3,0.0,0.0,0.0,0.0 +bump_and_run,bump_and_run.json,58.27,15.57,-0.85,1850.1,2.31,1700.4,75.7,0.0,0.0,0.0,0.0 bump_and_run_slow,bump_and_run_slow.json,30.27,15.57,-0.85,1850.1,2.31,1700.4,75.7,0.0,0.0,0.0,0.0 -bump_test_shot,bump_test_shot.json,78.27,5.57,-0.85,1850.1,2.31,1700.4,75.7,39,74,0.0,3.1 -checked_test_shot,checked_test_shot.json,75.05,38.50,1.35,10700.8,5.74,10500.8,699.3,77.6,77.5,0.0,79.5 +bump_test_shot,bump_test_shot.json,78.27,5.57,-0.85,1850.1,2.31,1700.4,75.7,0.0,0.0,0.0,0.0 +checked_test_shot,checked_test_shot.json,75.05,38.50,1.35,10700.8,5.74,10500.8,699.3,0.0,0.0,0.0,0.0 chip_test_shot,chip_test_shot.json,24.68,17.94,20.35,3203.9,0.00,3203.9,0.0,0.0,0.0,0.0,0.0 -drive_test_shot,drive_test_shot.json,150.00,12.50,1.50,2335.0,-9.90,2300.0,-400.0,244,256,0.0,80.6 -driver1,driver1.json,124.00,9.40,-9.50,2322.0,-24.80,2107.9,-973.2,158,184,0.0,30.2 -driver2,driver2.json,124.60,13.20,-6.80,3994.0,-6.10,3971.3,-423.5,184.8,192.3,0.0,65.8 -driver3,driver3.json,119.10,16.00,-8.90,4935.0,-9.40,4868.9,-806.3,174.1,178.5,0.0,78.5 -driver4,driver4.json,119.00,15.50,-10.20,4454.0,-7.80,4412.0,-608.1,175.6,180.9,0.0,73.8 -flop_test_shot,flop_test_shot.json,68.05,45.00,0.45,12000.0,0.84,12000.0,140.3,61.8,60.3,0.0,76.8 -p_wedge_shot_1,p_wedge_shot_1.json,82.28,26.46,-3.15,4946.5,6.74,4912.3,580.3,104.5,107.3,0.0,60.5 -topped_test_shot,topped_test_shot.json,91.78,5.00,-0.89,2195.3,16.20,2108.2,612.3,56.2,95.3,0.0,4.3 -wedge_shot_1,wedge_shot_1.json,48.66,28.96,-1.27,5683.7,11.12,5577.0,1096.3,42.6,46.9,0.0,21.3 -wedge_shot_2,wedge_shot_2.json,51.83,37.03,2.37,5652.0,9.08,5581.2,891.7,49.1,50.1,0.0,35.3 -wedge_test_shot,wedge_test_shot.json,66.40,23.20,-1.40,6449.0,7.10,6399.0,793.0,70.6,75.8,0.0,31.2 -wedge_test_shot2,wedge_test_shot2.json,54.70,26.80,1.60,4976.0,5.70,4951.0,494.0,52.2,57.7,0.0,24.4 -wood1,wood1.json,124.20,6.70,-8.10,4528.0,4.80,4512.0,378.0,165.5,181.90,0.0,32.80 -wood2,wood2.json,118.80,14.50,-3.30,3026.0,11.20,2968.0,586.0,175.6,186.10,0.0,59.3 -wood_low_test_shot,wood_low_test_shot.json,114.46,6.95,-0.63,1932.5,-1.42,1931.9,-47.8,122.6,158.9,0.0,15.7 +drive_test_shot,drive_test_shot.json,150.00,12.50,1.50,2335.0,-9.90,2300.0,-400.0,0.0,0.0,0.0,0.0 +driver1,driver1.json,124.00,9.40,-9.50,2322.0,-24.80,2107.9,-973.2,0.0,0.0,0.0,0.0 +driver2,driver2.json,124.60,13.20,-6.80,3994.0,-6.10,3971.3,-423.5,0.0,0.0,0.0,0.0 +driver3,driver3.json,119.10,16.00,-8.90,4935.0,-9.40,4868.9,-806.3,0.0,0.0,0.0,0.0 +driver4,driver4.json,119.00,15.50,-10.20,4454.0,-7.80,4412.0,-608.1,0.0,0.0,0.0,0.0 +flop_test_shot,flop_test_shot.json,68.05,45.00,0.45,12000.0,0.84,12000.0,140.3,0.0,0.0,0.0,0.0 +p_wedge_shot_1,p_wedge_shot_1.json,82.28,26.46,-3.15,4946.5,6.74,4912.3,580.3,0.0,0.0,0.0,0.0 +topped_test_shot,topped_test_shot.json,91.78,5.00,-0.89,2195.3,16.20,2108.2,612.3,0.0,0.0,0.0,0.0 +wedge_shot_1,wedge_shot_1.json,48.66,28.96,-1.27,5683.7,11.12,5577.0,1096.3,0.0,0.0,0.0,0.0 +wedge_shot_2,wedge_shot_2.json,51.83,37.03,2.37,5652.0,9.08,5581.2,891.7,0.0,0.0,0.0,0.0 +wedge_test_shot,wedge_test_shot.json,66.40,23.20,-1.40,6449.0,7.10,6399.0,793.0,0.0,0.0,0.0,0.0 +wedge_test_shot2,wedge_test_shot2.json,54.70,26.80,1.60,4976.0,5.70,4951.0,494.0,0.0,0.0,0.0,0.0 +wood1,wood1.json,124.20,6.70,-8.10,4528.0,4.80,4512.0,378.0,0.0,0.0,0.0,0.0 +wood2,wood2.json,118.80,14.50,-3.30,3026.0,11.20,2968.0,586.0,0.0,0.0,0.0,0.0 +wood_low_test_shot,wood_low_test_shot.json,114.46,6.95,-0.63,1932.5,-1.42,1931.9,-47.8,0.0,0.0,0.0,0.0 diff --git a/assets/data/calibration/physics.csv b/assets/data/calibration/physics.csv index 0d1af6e..10aa6e2 100644 --- a/assets/data/calibration/physics.csv +++ b/assets/data/calibration/physics.csv @@ -1,22 +1,22 @@ shot_name,filename,speed_mph,vla_deg,hla_deg,total_spin_rpm,spin_axis_deg,backspin_rpm,sidespin_rpm,carry_yd,total_yd,rollout_yd,apex_ft,hang_time_s,landing_speed_mps,landing_angle_deg,initial_re,initial_spin_ratio,initial_cd,initial_cl,peak_cl,carry_only_yd 5iron,5iron.json,102.10,17.40,1.50,5391.0,12.30,5266.0,1151.0,138.5,143.5,5.0,51.5,4.35,24.52,36.93,124951.4,0.263855,0.223365,0.210272,0.220626,138.5 approach_mid_iron_test_shot,approach_mid_iron_test_shot.json,93.90,22.90,-2.60,5375.0,2.00,5375.0,0.0,125.3,133.7,8.4,62.0,4.58,23.03,45.86,114916.1,0.286081,0.227772,0.214457,0.268000,125.3 -approach_test_shot,approach_test_shot.json,81.05,30.50,1.35,10489.8,5.74,10454.8,699.3,94.6,95.7,1.1,65.6,4.49,21.02,50.22,99195.9,0.646073,0.273237,0.261523,0.311337,94.6 +approach_test_shot,approach_test_shot.json,81.05,30.50,1.35,10489.8,5.74,10454.8,699.3,100.9,102.3,1.4,68.5,4.64,21.73,48.71,99195.9,0.646073,0.244740,0.261523,0.311335,100.9 bump_and_run,bump_and_run.json,58.27,15.57,-0.85,1850.1,2.31,1700.4,75.7,34.7,34.5,-0.2,8.0,1.43,20.83,18.52,71314.6,0.145977,0.336605,0.121604,0.121604,34.7 bump_and_run_slow,bump_and_run_slow.json,30.27,15.57,-0.85,1850.1,2.31,1700.4,75.7,10.0,27.1,17.0,2.2,0.73,12.54,16.52,37047.8,0.280996,0.484456,0.001800,0.001800,10.0 bump_test_shot,bump_test_shot.json,78.27,5.57,-0.85,1850.1,2.31,1700.4,75.7,36.9,86.8,49.9,3.0,1.05,29.99,6.63,95790.9,0.108677,0.227686,0.175278,0.175278,36.9 -checked_test_shot,checked_test_shot.json,75.05,38.50,1.35,10700.8,5.74,10500.8,699.3,77.3,79.1,1.7,71.1,4.44,20.65,56.18,91853.0,0.700778,0.317143,0.256092,0.311339,77.3 +checked_test_shot,checked_test_shot.json,75.05,38.50,1.35,10700.8,5.74,10500.8,699.3,78.5,80.4,1.8,71.9,4.48,20.82,55.82,91853.0,0.700778,0.306261,0.256092,0.311338,78.5 chip_test_shot,chip_test_shot.json,24.68,17.94,20.35,3203.9,-0.00,3203.9,0.0,7.7,16.9,9.2,1.9,0.69,10.50,18.85,30200.0,0.648877,0.414952,0.000085,0.000085,7.7 drive_test_shot,drive_test_shot.json,150.00,12.50,1.50,2335.0,-9.90,2300.0,-400.0,245.0,256.8,11.8,80.0,5.76,30.70,33.64,183572.1,0.077783,0.207210,0.148598,0.153033,245.0 driver1,driver1.json,124.00,9.40,-9.50,2322.0,-24.80,2107.9,-973.2,158.3,158.3,-0.0,29.5,3.60,32.70,18.37,151752.9,0.093576,0.202282,0.160723,0.161429,158.3 driver2,driver2.json,124.60,13.20,-6.80,3994.0,-6.10,3971.3,-423.5,191.4,196.8,5.4,60.5,5.05,27.77,31.85,152487.2,0.160192,0.215946,0.185912,0.187444,191.4 driver3,driver3.json,119.10,16.00,-8.90,4935.0,-9.40,4868.9,-806.3,177.3,185.2,7.8,69.5,5.15,26.16,39.43,145756.2,0.207095,0.225115,0.198532,0.202081,177.3 driver4,driver4.json,119.00,15.50,-10.20,4454.0,-7.80,4412.0,-608.1,179.3,187.6,8.2,66.1,5.08,26.65,36.96,145633.8,0.187047,0.218968,0.193166,0.196002,179.3 -flop_test_shot,flop_test_shot.json,68.05,45.00,0.45,12000.0,0.84,12000.0,140.3,62.7,63.1,0.4,69.2,4.27,19.75,59.65,83286.3,0.881310,0.370633,0.244725,0.288000,62.7 +flop_test_shot,flop_test_shot.json,68.05,45.00,0.45,12000.0,0.84,12000.0,140.3,63.1,63.5,0.4,69.5,4.28,19.83,59.47,83286.3,0.881310,0.364456,0.244725,0.288000,63.1 p_wedge_shot_1,p_wedge_shot_1.json,82.28,26.46,-3.15,4946.5,6.74,4912.3,580.3,100.1,109.2,9.1,54.8,4.15,21.54,47.03,100689.8,0.300469,0.248684,0.217245,0.268000,100.1 topped_test_shot,topped_test_shot.json,91.78,5.00,-0.89,2195.3,16.20,2108.2,612.3,55.3,109.1,53.8,4.2,1.38,33.49,6.56,112323.8,0.119540,0.202761,0.188703,0.188703,55.3 -wedge_shot_1,wedge_shot_1.json,48.66,28.96,-1.27,5683.7,11.12,5577.0,1096.3,39.2,50.4,11.1,19.5,2.31,16.58,36.29,59555.9,0.583711,0.409129,0.287990,0.287990,39.2 -wedge_shot_2,wedge_shot_2.json,51.83,37.03,2.37,5652.0,9.08,5581.2,891.7,45.1,56.0,10.9,32.4,2.92,17.23,46.77,63425.0,0.545045,0.378213,0.288228,0.288228,45.1 +wedge_shot_1,wedge_shot_1.json,48.66,28.96,-1.27,5683.7,11.12,5577.0,1096.3,39.8,51.2,11.4,19.6,2.33,16.72,36.00,59555.9,0.583711,0.397554,0.287990,0.287990,39.8 +wedge_shot_2,wedge_shot_2.json,51.83,37.03,2.37,5652.0,9.08,5581.2,891.7,45.8,57.0,11.2,32.6,2.93,17.35,46.30,63425.0,0.545045,0.373927,0.288228,0.288228,45.8 wedge_test_shot,wedge_test_shot.json,66.40,23.20,-1.40,6449.0,7.10,6399.0,793.0,71.2,87.3,16.1,29.5,3.13,19.90,33.51,81261.2,0.485321,0.271064,0.274986,0.311338,71.2 wedge_test_shot2,wedge_test_shot2.json,54.70,26.80,1.60,4976.0,5.70,4951.0,494.0,50.0,65.4,15.4,23.3,2.63,17.77,35.55,66942.6,0.454603,0.348572,0.308404,0.311339,50.0 wood1,wood1.json,124.20,6.70,-8.10,4528.0,4.80,4512.0,378.0,167.9,167.6,-0.3,29.5,4.00,29.55,19.71,151997.7,0.182197,0.221541,0.206752,0.207171,167.9 diff --git a/assets/data/calibration/shot_diff_analysis.csv b/assets/data/calibration/shot_diff_analysis.csv index 2a1311d..66af12e 100644 --- a/assets/data/calibration/shot_diff_analysis.csv +++ b/assets/data/calibration/shot_diff_analysis.csv @@ -1,24 +1,28 @@ shot_name,speed_mph,vla_deg,hla_deg,total_spin_rpm,spin_axis_deg,physics_carry_yd,flightscope_carry_yd,diff_carry_yd,physics_total_yd,flightscope_total_yd,diff_total_yd,rollout_physics_yd,rollout_flightscope_yd,diff_rollout_yd,physics_apex_ft,flightscope_apex_ft,diff_apex_ft,status -5iron,102.1,17.4,1.5,5391,12.3,138.5,138.1,0.4,143.5,142.2,1.3,5.0,4.1,0.9,51.5,59.6,-8.1,pass -approach_mid_iron_test_shot,93.9,22.9,-2.6,5375,2.0,125.3,125.8,-0.5,133.7,128.5,5.2,8.4,2.7,5.7,62.0,69.3,-7.3,moderate -approach_test_shot,81.0,30.5,1.4,10490,5.7,94.6,92.2,2.4,95.7,91.5,4.2,1.1,-0.7,1.8,65.6,72.2,-6.6,pass -bump_and_run,58.3,15.6,-0.8,1850,2.3,34.7,43.8,-9.1,34.5,62.0,-27.5,-0.2,18.2,-18.4,8.0,10.1,-2.1,severe -bump_and_run_slow,30.3,15.6,-0.8,1850,2.3,10.0,,,27.1,,,17.1,,,2.2,,, -bump_test_shot,78.3,5.6,-0.8,1850,2.3,36.9,39.0,-2.1,86.8,74.0,12.8,49.9,35.0,14.9,3.0,3.1,-0.1,severe -checked_test_shot,75.0,38.5,1.4,10701,5.7,77.3,77.6,-0.3,79.1,77.5,1.6,1.8,-0.1,1.9,71.1,79.5,-8.4,pass -chip_test_shot,24.7,17.9,20.4,3204,-0.0,7.7,,,16.9,,,9.2,,,1.9,,, -drive_test_shot,150.0,12.5,1.5,2335,-9.9,245.0,244.0,1.0,256.8,256.0,0.8,11.8,12.0,-0.2,80.0,80.6,-0.6,pass -driver1,124.0,9.4,-9.5,2322,-24.8,158.3,158.0,0.3,158.3,184.0,-25.7,0.0,26.0,-26.0,29.5,30.2,-0.7,severe -driver2,124.6,13.2,-6.8,3994,-6.1,191.4,184.8,6.6,196.8,192.3,4.5,5.4,7.5,-2.1,60.5,65.8,-5.3,moderate -driver3,119.1,16.0,-8.9,4935,-9.4,177.3,174.1,3.2,185.2,178.5,6.7,7.9,4.4,3.5,69.5,78.5,-9.0,moderate -driver4,119.0,15.5,-10.2,4454,-7.8,179.3,175.6,3.7,187.6,180.9,6.7,8.3,5.3,3.0,66.1,73.8,-7.7,moderate -flop_test_shot,68.0,45.0,0.5,12000,0.8,62.7,61.8,0.9,63.1,60.3,2.8,0.4,-1.5,1.9,69.2,76.8,-7.6,pass -p_wedge_shot_1,82.3,26.5,-3.1,4946,6.7,100.1,104.5,-4.4,109.2,107.3,1.9,9.1,2.8,6.3,54.8,60.5,-5.7,moderate -topped_test_shot,91.8,5.0,-0.9,2195,16.2,55.3,56.2,-0.9,109.1,95.3,13.8,53.8,39.1,14.7,4.2,4.3,-0.1,severe -wedge_shot_1,48.7,29.0,-1.3,5684,11.1,39.2,42.6,-3.4,50.4,46.9,3.5,11.2,4.3,6.9,19.5,21.3,-1.8,moderate -wedge_shot_2,51.8,37.0,2.4,5652,9.1,45.1,49.1,-4.0,56.0,50.1,5.9,10.9,1.0,9.9,32.4,35.3,-2.9,moderate -wedge_test_shot,66.4,23.2,-1.4,6449,7.1,71.2,70.6,0.6,87.3,75.8,11.5,16.1,5.2,10.9,29.5,31.2,-1.7,severe -wedge_test_shot2,54.7,26.8,1.6,4976,5.7,50.0,52.2,-2.2,65.4,57.7,7.7,15.4,5.5,9.9,23.3,24.4,-1.1,moderate -wood1,124.2,6.7,-8.1,4528,4.8,167.9,165.5,2.4,167.6,181.9,-14.3,-0.3,16.4,-16.7,29.5,32.8,-3.3,severe -wood2,118.8,14.5,-3.3,3026,11.2,179.6,175.6,4.0,185.6,186.1,-0.5,6.0,10.5,-4.5,57.0,59.3,-2.3,moderate -wood_low_test_shot,114.5,7.0,-0.6,1932,-1.4,124.4,122.6,1.8,181.8,158.9,22.9,57.4,36.3,21.1,16.1,15.7,0.4,severe +shot_1,18.8,35.3,-3.4,3729,-0.0,7.2,,,11.3,,,4.1,,,3.9,,, +shot_10,89.7,25.5,-3.1,6446,6.2,122.1,115.1,7.0,130.2,116.4,13.8,8.1,1.3,6.8,66.9,72.0,-5.1,severe +shot_11,78.5,29.6,4.3,7350,10.7,101.1,93.0,8.1,106.0,93.6,12.4,4.9,0.6,4.3,63.7,64.0,-0.3,severe +shot_12,90.8,24.5,-3.9,7261,4.9,128.5,115.4,13.1,134.8,116.4,18.4,6.3,1.0,5.3,68.7,71.5,-2.8,severe +shot_13,95.2,19.6,-1.1,5610,12.2,126.4,125.3,1.1,131.4,128.7,2.7,5.0,3.4,1.6,51.1,58.4,-7.3,pass +shot_14,87.1,22.4,-2.0,5660,11.4,111.1,110.9,0.2,115.4,113.7,1.7,4.3,2.8,1.5,49.7,55.9,-6.2,pass +shot_15,88.1,14.2,-0.3,4985,19.4,95.7,101.3,-5.6,95.6,113.1,-17.5,-0.1,11.8,-11.9,24.2,28.9,-4.7,severe +shot_16,94.1,19.1,-5.9,5291,5.0,122.3,124.3,-2.0,127.1,128.1,-1.0,4.8,3.8,1.0,47.9,55.3,-7.4,pass +shot_17,122.9,14.4,-1.3,4119,6.7,189.3,184.0,5.3,197.2,190.3,6.9,7.9,6.3,1.6,65.3,71.8,-6.5,moderate +shot_18,105.7,20.5,-1.7,5667,8.2,150.6,146.8,3.8,162.2,149.4,12.8,11.6,2.6,9.0,71.8,80.4,-8.6,severe +shot_19,128.6,12.5,-8.4,2514,5.1,197.8,194.5,3.3,204.3,208.7,-4.4,6.5,14.2,-7.7,56.4,57.3,-0.9,moderate +shot_2,18.6,33.3,-2.1,3688,-0.0,6.8,,,11.2,,,4.4,,,3.4,,, +shot_20,120.9,15.0,-3.0,4245,9.6,184.1,179.4,4.7,191.9,185.3,6.6,7.8,5.9,1.9,65.6,72.3,-6.7,moderate +shot_21,127.3,7.2,-11.5,2321,-19.5,160.7,154.9,5.8,215.3,186.5,28.8,54.6,31.6,23.0,24.9,23.1,1.8,severe +shot_22,123.3,14.0,-6.1,3104,10.0,190.5,185.4,5.1,198.1,195.5,2.6,7.6,10.1,-2.5,60.8,63.0,-2.2,moderate +shot_23,125.5,8.6,-14.3,3062,-23.9,165.4,161.9,3.5,165.4,185.0,-19.6,0.0,23.1,-23.1,30.7,31.0,-0.3,severe +shot_24,121.5,14.5,-11.2,3173,-6.8,187.8,183.0,4.8,195.8,192.8,3.0,8.0,9.8,-1.8,62.0,64.6,-2.6,moderate +shot_25,127.2,5.0,-12.6,2671,-16.2,149.1,,,204.0,,,54.9,,,17.0,,, +shot_26,117.0,18.4,-2.0,5035,9.7,172.7,170.7,2.0,185.3,174.2,11.1,12.6,3.5,9.1,79.1,88.1,-9.0,severe +shot_27,113.0,17.6,-2.3,4292,12.2,165.7,164.5,1.2,175.2,169.6,5.6,9.5,5.1,4.4,66.8,73.6,-6.8,moderate +shot_3,30.7,32.9,-2.8,4070,-0.0,17.5,,,26.5,,,9.0,,,8.9,,, +shot_4,41.6,35.2,-1.0,5229,5.2,30.7,,,40.1,,,9.4,,,18.7,,, +shot_5,34.0,35.1,-1.2,4487,-0.0,21.4,,,30.6,,,9.2,,,12.2,,, +shot_6,34.8,36.4,-0.6,4515,0.9,22.4,,,30.9,,,8.5,,,13.4,,, +shot_7,41.4,18.0,-7.7,3110,-0.0,22.0,,,40.2,,,18.2,,,5.9,,, +shot_8,43.1,12.6,7.9,2821,18.9,16.6,,,37.9,,,21.3,,,3.0,,, +shot_9,94.4,20.1,2.4,4020,16.3,118.7,126.5,-7.8,125.8,132.5,-6.7,7.1,6.0,1.1,49.0,54.8,-5.8,severe diff --git a/assets/data/shot_session_2/diagnostic_report.txt b/assets/data/shot_session_2/diagnostic_report.txt new file mode 100644 index 0000000..4bd6eba --- /dev/null +++ b/assets/data/shot_session_2/diagnostic_report.txt @@ -0,0 +1,136 @@ +====================================================================== +CALIBRATION DIAGNOSTIC REPORT +====================================================================== + + Pass: 3 + Moderate: 6 + Severe: 9 + No reference: 9 + +---------------------------------------------------------------------- +SEVERE SHOTS (|total_diff| > 10 yd or |carry_diff| > 7 yd) +---------------------------------------------------------------------- + + shot_10: + Status: SEVERE + Pattern: CARRY_AND_ROLLOUT_LONG — Both carry and rollout overshoot + Regimes: high-spin-wedge + Diffs: carry=+7.0, total=+13.8, rollout=+6.8, apex=-5.1ft + + shot_11: + Status: SEVERE + Pattern: CARRY_AND_ROLLOUT_LONG — Both carry and rollout overshoot + Regimes: high-spin-wedge + Diffs: carry=+8.1, total=+12.4, rollout=+4.3, apex=-0.3ft + + shot_12: + Status: SEVERE + Pattern: CARRY_AND_ROLLOUT_LONG — Both carry and rollout overshoot + Regimes: high-spin-wedge, mid-iron + Diffs: carry=+13.1, total=+18.4, rollout=+5.3, apex=-2.8ft + + shot_15: + Status: SEVERE + Pattern: CARRY_AND_ROLLOUT_SHORT — Both carry and rollout undershoot + Regimes: none + Diffs: carry=-5.6, total=-17.5, rollout=-11.9, apex=-4.7ft + + shot_18: + Status: SEVERE + Pattern: CARRY_AND_ROLLOUT_LONG — Both carry and rollout overshoot + Regimes: high-spin-wedge, mid-iron + Diffs: carry=+3.8, total=+12.8, rollout=+9.0, apex=-8.6ft + + shot_21: + Status: SEVERE + Pattern: CARRY_AND_ROLLOUT_LONG — Both carry and rollout overshoot + Regimes: low-launch, driver-wood + Diffs: carry=+5.8, total=+28.8, rollout=+23.0, apex=+1.8ft + + shot_23: + Status: SEVERE + Pattern: CARRY_TOO_LONG — Physics carry exceeds reference + Regimes: low-launch, driver-wood + Diffs: carry=+3.5, total=-19.6, rollout=-23.1, apex=-0.3ft + Suggested knobs: + -> Flight.ClMaxBase: decrease by 0.005 (range 0.22-0.32) + -> Flight.CdMin: increase by 0.005 (range 0.18-0.28) + -> DragScaleMultiplier: increase by 0.02 (range 0.85-1.15) + -> LiftScaleMultiplier: decrease by 0.02 (range 0.85-1.15) + + shot_26: + Status: SEVERE + Pattern: ROLLOUT_TOO_LONG — Carry close, total overshoots — rollout is too long + Regimes: high-spin-wedge + Diffs: carry=+2.0, total=+11.1, rollout=+9.1, apex=-9.0ft + Suggested knobs: + -> Bounce.FlightSpinFactorMin: decrease by 0.03 (range 0.25-0.55) + -> Bounce.FlightSpinFactorDivisor: decrease by 500.0 (range 4000.0-12000.0) + -> Rollout.HighSpinMultiplierMax: increase by 0.1 (range 1.5-3.5) + -> Bounce.RolloutHighSpinRetention: decrease by 0.03 (range 0.55-0.85) + + shot_9: + Status: SEVERE + Pattern: CARRY_AND_ROLLOUT_SHORT — Both carry and rollout undershoot + Regimes: mid-iron + Diffs: carry=-7.8, total=-6.7, rollout=+1.1, apex=-5.8ft +---------------------------------------------------------------------- +MODERATE SHOTS (5-10 yd total or 3-7 yd carry) +---------------------------------------------------------------------- + + shot_17: + Status: MODERATE + Pattern: CARRY_AND_ROLLOUT_LONG — Both carry and rollout overshoot + Regimes: driver-wood + Diffs: carry=+5.3, total=+6.9, rollout=+1.6, apex=-6.5ft + + shot_19: + Status: MODERATE + Pattern: CARRY_TOO_LONG — Physics carry exceeds reference + Regimes: driver-wood + Diffs: carry=+3.3, total=-4.4, rollout=-7.7, apex=-0.9ft + Suggested knobs: + -> Flight.ClMaxBase: decrease by 0.005 (range 0.22-0.32) + -> Flight.CdMin: increase by 0.005 (range 0.18-0.28) + -> DragScaleMultiplier: increase by 0.02 (range 0.85-1.15) + -> LiftScaleMultiplier: decrease by 0.02 (range 0.85-1.15) + + shot_20: + Status: MODERATE + Pattern: CARRY_AND_ROLLOUT_LONG — Both carry and rollout overshoot + Regimes: driver-wood + Diffs: carry=+4.7, total=+6.6, rollout=+1.9, apex=-6.7ft + + shot_22: + Status: MODERATE + Pattern: CARRY_TOO_LONG — Physics carry exceeds reference + Regimes: driver-wood + Diffs: carry=+5.1, total=+2.6, rollout=-2.5, apex=-2.2ft + Suggested knobs: + -> Flight.ClMaxBase: decrease by 0.005 (range 0.22-0.32) + -> Flight.CdMin: increase by 0.005 (range 0.18-0.28) + -> DragScaleMultiplier: increase by 0.02 (range 0.85-1.15) + -> LiftScaleMultiplier: decrease by 0.02 (range 0.85-1.15) + + shot_24: + Status: MODERATE + Pattern: CARRY_TOO_LONG — Physics carry exceeds reference + Regimes: driver-wood + Diffs: carry=+4.8, total=+3.0, rollout=-1.8, apex=-2.6ft + Suggested knobs: + -> Flight.ClMaxBase: decrease by 0.005 (range 0.22-0.32) + -> Flight.CdMin: increase by 0.005 (range 0.18-0.28) + -> DragScaleMultiplier: increase by 0.02 (range 0.85-1.15) + -> LiftScaleMultiplier: decrease by 0.02 (range 0.85-1.15) + + shot_27: + Status: MODERATE + Pattern: ROLLOUT_TOO_LONG — Carry close, total overshoots — rollout is too long + Regimes: driver-wood + Diffs: carry=+1.2, total=+5.6, rollout=+4.4, apex=-6.8ft + Suggested knobs: + -> Bounce.FlightTangentialRetentionBase: decrease by 0.03 (range 0.4-0.75) + -> Bounce.CorBaseA: decrease by 0.02 (range 0.3-0.6) + -> Rollout.LowSpinMultiplierMax: increase by 0.05 (range 0.8-1.5) + -> Rollout.LowSpinThreshold: decrease by 100.0 (range 1000.0-2500.0) + -> KineticFrictionMultiplier: increase by 0.05 (range 0.7-1.3) diff --git a/assets/data/shot_session_2/flightscope.csv b/assets/data/shot_session_2/flightscope.csv new file mode 100644 index 0000000..e77bfd4 --- /dev/null +++ b/assets/data/shot_session_2/flightscope.csv @@ -0,0 +1,28 @@ +shot_name,filename,speed_mph,vla_deg,hla_deg,total_spin_rpm,spin_axis_deg,backspin_rpm,sidespin_rpm,carry_yd,total_yd,rollout_yd,apex_ft +shot_1,shot_1.json,18.82,35.31,-3.37,3729.2,0.00,3729.2,0.0,0.0,0.0,0.0,0.0 +shot_10,shot_10.json,89.70,25.51,-3.13,6446.1,6.16,6408.9,691.6,115.1,116.4,1.3,72.0 +shot_11,shot_11.json,78.51,29.59,4.31,7349.8,10.69,7222.3,1363.2,93.0,93.6,0.6,64.0 +shot_12,shot_12.json,90.80,24.49,-3.88,7260.6,4.91,7233.9,621.5,115.4,116.4,1.0,71.5 +shot_13,shot_13.json,95.22,19.62,-1.09,5609.6,12.20,5482.8,1185.8,125.3,128.7,3.4,58.4 +shot_14,shot_14.json,87.13,22.37,-2.00,5660.3,11.37,5549.3,1115.6,110.9,113.7,2.8,55.9 +shot_15,shot_15.json,88.14,14.21,-0.32,4985.2,19.37,4703.0,1653.5,101.3,113.1,11.8,28.9 +shot_16,shot_16.json,94.07,19.14,-5.93,5291.1,4.96,5271.3,457.7,124.3,128.1,3.8,55.3 +shot_17,shot_17.json,122.92,14.40,-1.33,4119.1,6.69,4091.0,480.2,184.0,190.3,6.3,71.8 +shot_18,shot_18.json,105.67,20.50,-1.72,5666.8,8.18,5609.2,806.3,146.8,149.4,2.6,80.4 +shot_19,shot_19.json,128.59,12.54,-8.40,2513.7,5.13,2503.6,224.6,194.5,208.7,14.2,57.3 +shot_2,shot_2.json,18.59,33.32,-2.15,3688.3,0.00,3688.3,0.0,0.0,0.0,0.0,0.0 +shot_20,shot_20.json,120.88,15.02,-2.96,4245.1,9.62,4185.4,709.5,179.4,185.3,5.9,72.3 +shot_21,shot_21.json,127.35,7.24,-11.46,2320.9,-19.47,2188.2,-773.4,154.9,186.5,31.6,23.1 +shot_22,shot_22.json,123.29,14.01,-6.11,3104.1,9.97,3057.3,537.3,185.4,195.5,10.1,63.0 +shot_23,shot_23.json,125.52,8.64,-14.33,3062.5,-23.87,2800.5,-1239.4,161.9,185.0,23.1,31.0 +shot_24,shot_24.json,121.48,14.53,-11.23,3172.9,-6.80,3150.6,-375.5,183.0,192.8,9.8,64.6 +shot_25,shot_25.json,127.22,4.99,-12.57,2671.0,-16.25,2564.3,-747.4,0.0,0.0,0.0,0.0 +shot_26,shot_26.json,116.97,18.42,-2.02,5035.4,9.70,4963.4,848.2,170.7,174.2,3.5,88.1 +shot_27,shot_27.json,112.96,17.59,-2.26,4291.5,12.21,4194.4,907.8,164.5,169.6,5.1,73.6 +shot_3,shot_3.json,30.72,32.94,-2.80,4070.1,0.00,4070.1,0.0,0.0,0.0,0.0,0.0 +shot_4,shot_4.json,41.63,35.25,-0.96,5228.9,5.22,5207.3,475.4,0.0,0.0,0.0,0.0 +shot_5,shot_5.json,34.04,35.10,-1.20,4487.3,0.00,4487.3,0.0,0.0,0.0,0.0,0.0 +shot_6,shot_6.json,34.78,36.39,-0.62,4515.3,0.93,4514.7,73.1,0.0,0.0,0.0,0.0 +shot_7,shot_7.json,41.38,18.02,-7.65,3110.5,0.00,3110.5,0.0,0.0,0.0,0.0,0.0 +shot_8,shot_8.json,43.09,12.55,7.93,2821.4,18.87,2669.8,912.3,0.0,0.0,0.0,0.0 +shot_9,shot_9.json,94.41,20.07,2.40,4020.4,16.33,3858.1,1130.8,126.5,132.5,6.0,54.8 diff --git a/assets/data/shot_session_2/flightscope_reference.json b/assets/data/shot_session_2/flightscope_reference.json new file mode 100644 index 0000000..edd11f3 --- /dev/null +++ b/assets/data/shot_session_2/flightscope_reference.json @@ -0,0 +1,254 @@ +{ + "shot_10": { + "filename": "shot_10.json", + "speed_mph": 89.70254516601562, + "vla_deg": 25.51108741760254, + "hla_deg": -3.133885383605957, + "total_spin_rpm": 6446.09033203125, + "spin_axis_deg": 6.158735752105713, + "carry_yd": 115.1, + "roll_yd": 1.3, + "total_yd": 116.4, + "lateral_yd": 11.1, + "time_s": 5.1, + "apex_ft": 72.0 + }, + "shot_11": { + "filename": "shot_11.json", + "speed_mph": 78.50968170166016, + "vla_deg": 29.592864990234375, + "hla_deg": 4.308452129364014, + "total_spin_rpm": 7349.791015625, + "spin_axis_deg": 10.688864707946777, + "carry_yd": 93.0, + "roll_yd": 0.7, + "total_yd": 93.6, + "lateral_yd": 13.0, + "time_s": 4.6, + "apex_ft": 64.0 + }, + "shot_12": { + "filename": "shot_12.json", + "speed_mph": 90.79634857177734, + "vla_deg": 24.492616653442383, + "hla_deg": -3.880093574523926, + "total_spin_rpm": 7260.55419921875, + "spin_axis_deg": 4.910156726837158, + "carry_yd": 115.4, + "roll_yd": 0.9, + "total_yd": 116.4, + "lateral_yd": 11.9, + "time_s": 5.1, + "apex_ft": 71.5 + }, + "shot_13": { + "filename": "shot_13.json", + "speed_mph": 95.22373962402344, + "vla_deg": 19.6151180267334, + "hla_deg": -1.0942004919052124, + "total_spin_rpm": 5609.5908203125, + "spin_axis_deg": 12.203797340393066, + "carry_yd": 125.3, + "roll_yd": 3.3, + "total_yd": 128.7, + "lateral_yd": 12.3, + "time_s": 4.8, + "apex_ft": 58.4 + }, + "shot_14": { + "filename": "shot_14.json", + "speed_mph": 87.1322021484375, + "vla_deg": 22.372407913208008, + "hla_deg": -2.002088785171509, + "total_spin_rpm": 5660.34130859375, + "spin_axis_deg": 11.367351531982422, + "carry_yd": 110.9, + "roll_yd": 2.8, + "total_yd": 113.7, + "lateral_yd": 11.4, + "time_s": 4.5, + "apex_ft": 55.9 + }, + "shot_15": { + "filename": "shot_15.json", + "speed_mph": 88.13645935058594, + "vla_deg": 14.205162048339844, + "hla_deg": -0.31607601046562195, + "total_spin_rpm": 4985.18603515625, + "spin_axis_deg": 19.371000289916992, + "carry_yd": 101.3, + "roll_yd": 11.8, + "total_yd": 113.1, + "lateral_yd": 9.9, + "time_s": 3.5, + "apex_ft": 28.9 + }, + "shot_16": { + "filename": "shot_16.json", + "speed_mph": 94.06647491455078, + "vla_deg": 19.13742446899414, + "hla_deg": -5.926681041717529, + "total_spin_rpm": 5291.1259765625, + "spin_axis_deg": 4.962690830230713, + "carry_yd": 124.3, + "roll_yd": 3.9, + "total_yd": 128.1, + "lateral_yd": 16.6, + "time_s": 4.7, + "apex_ft": 55.3 + }, + "shot_17": { + "filename": "shot_17.json", + "speed_mph": 122.9229507446289, + "vla_deg": 14.39944839477539, + "hla_deg": -1.334268569946289, + "total_spin_rpm": 4119.08837890625, + "spin_axis_deg": 6.694624900817871, + "carry_yd": 184.0, + "roll_yd": 6.4, + "total_yd": 190.3, + "lateral_yd": 13.0, + "time_s": 5.6, + "apex_ft": 71.8 + }, + "shot_18": { + "filename": "shot_18.json", + "speed_mph": 105.67237854003906, + "vla_deg": 20.504825592041016, + "hla_deg": -1.715098261833191, + "total_spin_rpm": 5666.81201171875, + "spin_axis_deg": 8.180413246154785, + "carry_yd": 146.8, + "roll_yd": 2.5, + "total_yd": 149.4, + "lateral_yd": 13.5, + "time_s": 5.6, + "apex_ft": 80.4 + }, + "shot_19": { + "filename": "shot_19.json", + "speed_mph": 128.5924530029297, + "vla_deg": 12.543039321899414, + "hla_deg": -8.401840209960938, + "total_spin_rpm": 2513.6943359375, + "spin_axis_deg": 5.126776695251465, + "carry_yd": 194.5, + "roll_yd": 14.2, + "total_yd": 208.7, + "lateral_yd": 34.0, + "time_s": 5.1, + "apex_ft": 57.3 + }, + "shot_20": { + "filename": "shot_20.json", + "speed_mph": 120.87996673583984, + "vla_deg": 15.021929740905762, + "hla_deg": -2.9594407081604004, + "total_spin_rpm": 4245.142578125, + "spin_axis_deg": 9.621642112731934, + "carry_yd": 179.4, + "roll_yd": 5.9, + "total_yd": 185.3, + "lateral_yd": 21.7, + "time_s": 5.6, + "apex_ft": 72.3 + }, + "shot_21": { + "filename": "shot_21.json", + "speed_mph": 127.35234832763672, + "vla_deg": 7.244511604309082, + "hla_deg": -11.457404136657715, + "total_spin_rpm": 2320.867919921875, + "spin_axis_deg": -19.465559005737305, + "carry_yd": 154.9, + "roll_yd": 31.7, + "total_yd": 186.5, + "lateral_yd": 42.8, + "time_s": 3.5, + "apex_ft": 23.1 + }, + "shot_22": { + "filename": "shot_22.json", + "speed_mph": 123.28929138183594, + "vla_deg": 14.01224136352539, + "hla_deg": -6.107107639312744, + "total_spin_rpm": 3104.138916015625, + "spin_axis_deg": 9.967370986938477, + "carry_yd": 185.4, + "roll_yd": 10.1, + "total_yd": 195.5, + "lateral_yd": 31.0, + "time_s": 5.3, + "apex_ft": 63.0 + }, + "shot_23": { + "filename": "shot_23.json", + "speed_mph": 125.51544189453125, + "vla_deg": 8.638062477111816, + "hla_deg": -14.332796096801758, + "total_spin_rpm": 3062.542724609375, + "spin_axis_deg": -23.872516632080078, + "carry_yd": 161.9, + "roll_yd": 23.2, + "total_yd": 185.0, + "lateral_yd": 57.9, + "time_s": 4.0, + "apex_ft": 31.0 + }, + "shot_24": { + "filename": "shot_24.json", + "speed_mph": 121.48118591308594, + "vla_deg": 14.52869987487793, + "hla_deg": -11.230563163757324, + "total_spin_rpm": 3172.8828125, + "spin_axis_deg": -6.796906471252441, + "carry_yd": 183.0, + "roll_yd": 9.8, + "total_yd": 192.8, + "lateral_yd": 43.1, + "time_s": 5.3, + "apex_ft": 64.6 + }, + "shot_26": { + "filename": "shot_26.json", + "speed_mph": 116.96517944335938, + "vla_deg": 18.422693252563477, + "hla_deg": -2.0174665451049805, + "total_spin_rpm": 5035.37939453125, + "spin_axis_deg": 9.697626113891602, + "carry_yd": 170.7, + "roll_yd": 3.5, + "total_yd": 174.2, + "lateral_yd": 19.0, + "time_s": 5.9, + "apex_ft": 88.1 + }, + "shot_27": { + "filename": "shot_27.json", + "speed_mph": 112.96182250976562, + "vla_deg": 17.587596893310547, + "hla_deg": -2.2626442909240723, + "total_spin_rpm": 4291.4892578125, + "spin_axis_deg": 12.212044715881348, + "carry_yd": 164.5, + "roll_yd": 5.2, + "total_yd": 169.6, + "lateral_yd": 20.5, + "time_s": 5.5, + "apex_ft": 73.6 + }, + "shot_9": { + "filename": "shot_9.json", + "speed_mph": 94.4128646850586, + "vla_deg": 20.070960998535156, + "hla_deg": 2.4021687507629395, + "total_spin_rpm": 4020.436767578125, + "spin_axis_deg": 16.33491325378418, + "carry_yd": 126.5, + "roll_yd": 6.1, + "total_yd": 132.5, + "lateral_yd": 16.6, + "time_s": 4.5, + "apex_ft": 54.8 + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/physics.csv b/assets/data/shot_session_2/physics.csv new file mode 100644 index 0000000..5c4fa18 --- /dev/null +++ b/assets/data/shot_session_2/physics.csv @@ -0,0 +1,28 @@ +shot_name,filename,speed_mph,vla_deg,hla_deg,total_spin_rpm,spin_axis_deg,backspin_rpm,sidespin_rpm,carry_yd,total_yd,rollout_yd,apex_ft,hang_time_s,landing_speed_mps,landing_angle_deg,initial_re,initial_spin_ratio,initial_cd,initial_cl,peak_cl,carry_only_yd +shot_1,shot_1.json,18.82,35.31,-3.37,3729.2,-0.00,3729.2,-0.0,7.2,11.3,4.2,3.9,0.98,8.03,36.61,23028.8,0.990462,0.480871,0.000000,0.000000,7.2 +shot_10,shot_10.json,89.70,25.51,-3.13,6446.1,6.16,6408.9,691.6,122.1,130.2,8.1,66.9,4.79,22.52,47.02,109779.2,0.359143,0.233072,0.227765,0.283872,122.1 +shot_11,shot_11.json,78.51,29.59,4.31,7349.8,10.69,7222.3,1363.2,101.1,106.0,4.9,63.7,4.55,21.52,46.91,96081.2,0.467873,0.236811,0.274776,0.311338,101.1 +shot_12,shot_12.json,90.80,24.49,-3.88,7260.6,4.91,7233.9,621.5,128.5,134.8,6.4,68.7,4.97,22.72,45.50,111117.8,0.399648,0.231466,0.243274,0.303858,128.5 +shot_13,shot_13.json,95.22,19.62,-1.09,5609.6,12.20,5482.8,1185.8,126.4,131.4,5.1,51.1,4.29,23.36,39.00,116536.1,0.294416,0.226563,0.216073,0.268000,126.4 +shot_14,shot_14.json,87.13,22.37,-2.00,5660.3,11.37,5549.3,1115.6,111.1,115.4,4.2,49.7,4.14,22.22,40.64,106633.6,0.324668,0.237516,0.221768,0.268122,111.1 +shot_15,shot_15.json,88.14,14.21,-0.32,4985.2,19.37,4703.0,1653.5,95.7,95.6,-0.1,24.2,3.00,24.58,23.58,107862.6,0.282684,0.235665,0.213801,0.225544,95.7 +shot_16,shot_16.json,94.07,19.14,-5.93,5291.1,4.96,5271.3,457.7,122.3,127.1,4.8,47.9,4.12,23.41,37.94,115119.9,0.281118,0.227609,0.213500,0.257648,122.3 +shot_17,shot_17.json,122.92,14.40,-1.33,4119.1,6.69,4091.0,480.2,189.3,197.2,8.0,65.3,5.15,27.33,34.41,150434.8,0.167473,0.216547,0.187650,0.189686,189.3 +shot_18,shot_18.json,105.67,20.50,-1.72,5666.8,8.18,5609.2,806.3,150.6,162.2,11.6,71.8,5.02,24.45,46.03,129323.3,0.268011,0.223441,0.211029,0.251604,150.6 +shot_19,shot_19.json,128.59,12.54,-8.40,2513.7,5.13,2503.6,224.6,197.8,204.3,6.5,56.4,4.88,29.73,28.77,157373.2,0.097695,0.206058,0.163440,0.165203,197.8 +shot_2,shot_2.json,18.59,33.32,-2.15,3688.3,-0.00,3688.3,-0.0,6.8,11.2,4.4,3.4,0.93,7.96,34.51,22755.1,0.991373,0.460310,0.000000,0.000000,6.8 +shot_20,shot_20.json,120.88,15.02,-2.96,4245.1,9.62,4185.4,709.5,184.1,191.9,7.8,65.6,5.11,27.05,35.54,147934.6,0.175515,0.217146,0.189766,0.192311,184.1 +shot_21,shot_21.json,127.35,7.24,-11.46,2320.9,-19.47,2188.2,-773.4,160.7,215.3,54.6,24.9,3.53,33.40,16.13,155855.5,0.091079,0.204241,0.169690,0.170459,160.7 +shot_22,shot_22.json,123.29,14.01,-6.11,3104.1,9.97,3057.3,537.3,190.5,198.1,7.6,60.8,4.99,28.41,31.53,150883.1,0.125832,0.207300,0.177010,0.178251,190.5 +shot_23,shot_23.json,125.52,8.64,-14.33,3062.5,-23.87,2800.5,-1239.4,165.4,165.4,-0.0,30.7,3.81,31.77,19.02,153607.5,0.121944,0.208148,0.178451,0.179126,165.4 +shot_24,shot_24.json,121.48,14.53,-11.23,3172.9,-6.80,3150.6,-375.5,187.8,195.8,8.0,62.0,5.01,28.07,32.27,148670.3,0.130533,0.206931,0.178517,0.179700,187.8 +shot_25,shot_25.json,127.22,4.99,-12.57,2671.0,-16.25,2564.3,-747.4,149.1,204.0,54.8,17.0,3.18,34.48,12.29,155698.6,0.104925,0.206299,0.181290,0.181830,149.1 +shot_26,shot_26.json,116.97,18.42,-2.02,5035.4,9.70,4963.4,848.2,172.7,185.3,12.6,79.1,5.30,25.94,44.77,143143.6,0.215155,0.226139,0.200479,0.205025,172.7 +shot_27,shot_27.json,112.96,17.59,-2.26,4291.5,12.21,4194.4,907.8,165.7,175.2,9.5,66.8,4.93,26.17,39.14,138244.2,0.189868,0.215786,0.193971,0.196999,165.7 +shot_3,shot_3.json,30.72,32.94,-2.80,4070.1,-0.00,4070.1,-0.0,17.5,26.5,9.1,8.9,1.49,12.31,35.60,37591.2,0.662227,0.451423,0.091740,0.091740,17.5 +shot_4,shot_4.json,41.63,35.25,-0.96,5228.9,5.22,5207.3,475.4,30.7,40.1,9.4,18.7,2.18,15.13,41.30,50942.2,0.627805,0.500030,0.286567,0.286567,30.7 +shot_5,shot_5.json,34.04,35.10,-1.20,4487.3,-0.00,4487.3,-0.0,21.4,30.6,9.2,12.2,1.74,13.22,38.70,41663.5,0.658746,0.493685,0.177386,0.177386,21.4 +shot_6,shot_6.json,34.78,36.39,-0.62,4515.3,0.93,4514.7,73.1,22.4,30.9,8.5,13.4,1.83,13.39,40.49,42558.8,0.648916,0.511661,0.196137,0.196137,22.4 +shot_7,shot_7.json,41.38,18.02,-7.65,3110.5,-0.00,3110.5,-0.0,22.0,40.2,18.2,5.9,1.27,15.51,21.10,50646.4,0.375640,0.519397,0.241716,0.254335,22.0 +shot_8,shot_8.json,43.09,12.55,7.93,2821.4,18.87,2669.8,912.3,16.6,37.9,21.3,3.0,0.88,16.61,14.18,52729.6,0.327265,0.522545,0.124722,0.124722,16.6 +shot_9,shot_9.json,94.41,20.07,2.40,4020.4,16.33,3858.1,1130.8,118.7,125.8,7.1,49.0,3.96,24.26,38.91,115543.8,0.212823,0.223713,0.199927,0.203125,118.7 diff --git a/assets/data/shot_session_2/shot_1.json b/assets/data/shot_session_2/shot_1.json new file mode 100644 index 0000000..bb1489c --- /dev/null +++ b/assets/data/shot_session_2/shot_1.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 18.817201614379883, + "SpinAxis": -0, + "TotalSpin": 3729.2099609375, + "BackSpin": 3729.2099609375, + "SideSpin": -0, + "HLA": -3.373659372329712, + "VLA": 35.30873489379883, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_10.json b/assets/data/shot_session_2/shot_10.json new file mode 100644 index 0000000..26bc0f5 --- /dev/null +++ b/assets/data/shot_session_2/shot_10.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 89.70254516601562, + "SpinAxis": 6.158735752105713, + "TotalSpin": 6446.09033203125, + "BackSpin": 6408.88671875, + "SideSpin": 691.55810546875, + "HLA": -3.133885383605957, + "VLA": 25.51108741760254, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_11.json b/assets/data/shot_session_2/shot_11.json new file mode 100644 index 0000000..05c1415 --- /dev/null +++ b/assets/data/shot_session_2/shot_11.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 78.50968170166016, + "SpinAxis": 10.688864707946777, + "TotalSpin": 7349.791015625, + "BackSpin": 7222.263671875, + "SideSpin": 1363.207275390625, + "HLA": 4.308452129364014, + "VLA": 29.592864990234375, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_12.json b/assets/data/shot_session_2/shot_12.json new file mode 100644 index 0000000..4baeb62 --- /dev/null +++ b/assets/data/shot_session_2/shot_12.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 90.79634857177734, + "SpinAxis": 4.910156726837158, + "TotalSpin": 7260.55419921875, + "BackSpin": 7233.9091796875, + "SideSpin": 621.45654296875, + "HLA": -3.880093574523926, + "VLA": 24.492616653442383, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_13.json b/assets/data/shot_session_2/shot_13.json new file mode 100644 index 0000000..ee32caf --- /dev/null +++ b/assets/data/shot_session_2/shot_13.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 95.22373962402344, + "SpinAxis": 12.203797340393066, + "TotalSpin": 5609.5908203125, + "BackSpin": 5482.82470703125, + "SideSpin": 1185.80908203125, + "HLA": -1.0942004919052124, + "VLA": 19.6151180267334, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_14.json b/assets/data/shot_session_2/shot_14.json new file mode 100644 index 0000000..f28083a --- /dev/null +++ b/assets/data/shot_session_2/shot_14.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 87.1322021484375, + "SpinAxis": 11.367351531982422, + "TotalSpin": 5660.34130859375, + "BackSpin": 5549.30615234375, + "SideSpin": 1115.6461181640625, + "HLA": -2.002088785171509, + "VLA": 22.372407913208008, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_15.json b/assets/data/shot_session_2/shot_15.json new file mode 100644 index 0000000..4ac1ff4 --- /dev/null +++ b/assets/data/shot_session_2/shot_15.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 88.13645935058594, + "SpinAxis": 19.371000289916992, + "TotalSpin": 4985.18603515625, + "BackSpin": 4702.97802734375, + "SideSpin": 1653.5048828125, + "HLA": -0.31607601046562195, + "VLA": 14.205162048339844, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_16.json b/assets/data/shot_session_2/shot_16.json new file mode 100644 index 0000000..9097e76 --- /dev/null +++ b/assets/data/shot_session_2/shot_16.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 94.06647491455078, + "SpinAxis": 4.962690830230713, + "TotalSpin": 5291.1259765625, + "BackSpin": 5271.291015625, + "SideSpin": 457.7196044921875, + "HLA": -5.926681041717529, + "VLA": 19.13742446899414, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_17.json b/assets/data/shot_session_2/shot_17.json new file mode 100644 index 0000000..41b2207 --- /dev/null +++ b/assets/data/shot_session_2/shot_17.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 122.9229507446289, + "SpinAxis": 6.694624900817871, + "TotalSpin": 4119.08837890625, + "BackSpin": 4091.002685546875, + "SideSpin": 480.19329833984375, + "HLA": -1.334268569946289, + "VLA": 14.39944839477539, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_18.json b/assets/data/shot_session_2/shot_18.json new file mode 100644 index 0000000..3e4a1b2 --- /dev/null +++ b/assets/data/shot_session_2/shot_18.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 105.67237854003906, + "SpinAxis": 8.180413246154785, + "TotalSpin": 5666.81201171875, + "BackSpin": 5609.15185546875, + "SideSpin": 806.3339233398438, + "HLA": -1.715098261833191, + "VLA": 20.504825592041016, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_19.json b/assets/data/shot_session_2/shot_19.json new file mode 100644 index 0000000..74bce23 --- /dev/null +++ b/assets/data/shot_session_2/shot_19.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 128.5924530029297, + "SpinAxis": 5.126776695251465, + "TotalSpin": 2513.6943359375, + "BackSpin": 2503.63818359375, + "SideSpin": 224.6231689453125, + "HLA": -8.401840209960938, + "VLA": 12.543039321899414, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_2.json b/assets/data/shot_session_2/shot_2.json new file mode 100644 index 0000000..36ab3e2 --- /dev/null +++ b/assets/data/shot_session_2/shot_2.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 18.593608856201172, + "SpinAxis": -0, + "TotalSpin": 3688.287109375, + "BackSpin": 3688.287109375, + "SideSpin": -0, + "HLA": -2.1475605964660645, + "VLA": 33.32229232788086, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_20.json b/assets/data/shot_session_2/shot_20.json new file mode 100644 index 0000000..a44bd4d --- /dev/null +++ b/assets/data/shot_session_2/shot_20.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 120.87996673583984, + "SpinAxis": 9.621642112731934, + "TotalSpin": 4245.142578125, + "BackSpin": 4185.42626953125, + "SideSpin": 709.5380859375, + "HLA": -2.9594407081604004, + "VLA": 15.021929740905762, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_21.json b/assets/data/shot_session_2/shot_21.json new file mode 100644 index 0000000..29a7380 --- /dev/null +++ b/assets/data/shot_session_2/shot_21.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 127.35234832763672, + "SpinAxis": -19.465559005737305, + "TotalSpin": 2320.867919921875, + "BackSpin": 2188.211669921875, + "SideSpin": -773.4064331054688, + "HLA": -11.457404136657715, + "VLA": 7.244511604309082, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_22.json b/assets/data/shot_session_2/shot_22.json new file mode 100644 index 0000000..b6efc8a --- /dev/null +++ b/assets/data/shot_session_2/shot_22.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 123.28929138183594, + "SpinAxis": 9.967370986938477, + "TotalSpin": 3104.138916015625, + "BackSpin": 3057.28662109375, + "SideSpin": 537.2870483398438, + "HLA": -6.107107639312744, + "VLA": 14.01224136352539, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_23.json b/assets/data/shot_session_2/shot_23.json new file mode 100644 index 0000000..480dc85 --- /dev/null +++ b/assets/data/shot_session_2/shot_23.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 125.51544189453125, + "SpinAxis": -23.872516632080078, + "TotalSpin": 3062.542724609375, + "BackSpin": 2800.53662109375, + "SideSpin": -1239.420166015625, + "HLA": -14.332796096801758, + "VLA": 8.638062477111816, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_24.json b/assets/data/shot_session_2/shot_24.json new file mode 100644 index 0000000..0bde5bc --- /dev/null +++ b/assets/data/shot_session_2/shot_24.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 121.48118591308594, + "SpinAxis": -6.796906471252441, + "TotalSpin": 3172.8828125, + "BackSpin": 3150.58349609375, + "SideSpin": -375.5118103027344, + "HLA": -11.230563163757324, + "VLA": 14.52869987487793, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_25.json b/assets/data/shot_session_2/shot_25.json new file mode 100644 index 0000000..be7ae49 --- /dev/null +++ b/assets/data/shot_session_2/shot_25.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 127.22408294677734, + "SpinAxis": -16.2506103515625, + "TotalSpin": 2670.99267578125, + "BackSpin": 2564.278076171875, + "SideSpin": -747.4485473632812, + "HLA": -12.57325267791748, + "VLA": 4.986321449279785, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_26.json b/assets/data/shot_session_2/shot_26.json new file mode 100644 index 0000000..455a381 --- /dev/null +++ b/assets/data/shot_session_2/shot_26.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 116.96517944335938, + "SpinAxis": 9.697626113891602, + "TotalSpin": 5035.37939453125, + "BackSpin": 4963.42626953125, + "SideSpin": 848.2023315429688, + "HLA": -2.0174665451049805, + "VLA": 18.422693252563477, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_27.json b/assets/data/shot_session_2/shot_27.json new file mode 100644 index 0000000..37d9966 --- /dev/null +++ b/assets/data/shot_session_2/shot_27.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 112.96182250976562, + "SpinAxis": 12.212044715881348, + "TotalSpin": 4291.4892578125, + "BackSpin": 4194.37890625, + "SideSpin": 907.7798461914062, + "HLA": -2.2626442909240723, + "VLA": 17.587596893310547, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_3.json b/assets/data/shot_session_2/shot_3.json new file mode 100644 index 0000000..2f418f6 --- /dev/null +++ b/assets/data/shot_session_2/shot_3.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 30.71647834777832, + "SpinAxis": -0, + "TotalSpin": 4070.076416015625, + "BackSpin": 4070.076416015625, + "SideSpin": -0, + "HLA": -2.799088716506958, + "VLA": 32.94072341918945, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_4.json b/assets/data/shot_session_2/shot_4.json new file mode 100644 index 0000000..e2fc7f3 --- /dev/null +++ b/assets/data/shot_session_2/shot_4.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 41.62580490112305, + "SpinAxis": 5.216817855834961, + "TotalSpin": 5228.9169921875, + "BackSpin": 5207.25732421875, + "SideSpin": 475.438720703125, + "HLA": -0.955000102519989, + "VLA": 35.24593734741211, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_5.json b/assets/data/shot_session_2/shot_5.json new file mode 100644 index 0000000..b06eeb5 --- /dev/null +++ b/assets/data/shot_session_2/shot_5.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 34.04402542114258, + "SpinAxis": -0, + "TotalSpin": 4487.27978515625, + "BackSpin": 4487.27978515625, + "SideSpin": -0, + "HLA": -1.1950721740722656, + "VLA": 35.09980773925781, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_6.json b/assets/data/shot_session_2/shot_6.json new file mode 100644 index 0000000..6b36723 --- /dev/null +++ b/assets/data/shot_session_2/shot_6.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 34.77558517456055, + "SpinAxis": 0.9274818897247314, + "TotalSpin": 4515.3017578125, + "BackSpin": 4514.7099609375, + "SideSpin": 73.08876037597656, + "HLA": -0.6180511116981506, + "VLA": 36.38626480102539, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_7.json b/assets/data/shot_session_2/shot_7.json new file mode 100644 index 0000000..b3b41ae --- /dev/null +++ b/assets/data/shot_session_2/shot_7.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 41.384071350097656, + "SpinAxis": -0, + "TotalSpin": 3110.4951171875, + "BackSpin": 3110.4951171875, + "SideSpin": -0, + "HLA": -7.650838375091553, + "VLA": 18.022966384887695, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_8.json b/assets/data/shot_session_2/shot_8.json new file mode 100644 index 0000000..de1e7b9 --- /dev/null +++ b/assets/data/shot_session_2/shot_8.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 43.08625793457031, + "SpinAxis": 18.866695404052734, + "TotalSpin": 2821.38671875, + "BackSpin": 2669.803466796875, + "SideSpin": 912.3445434570312, + "HLA": 7.925503253936768, + "VLA": 12.551077842712402, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_9.json b/assets/data/shot_session_2/shot_9.json new file mode 100644 index 0000000..f6a83ba --- /dev/null +++ b/assets/data/shot_session_2/shot_9.json @@ -0,0 +1,15 @@ +{ + "BallData": { + "Speed": 94.4128646850586, + "SpinAxis": 16.33491325378418, + "TotalSpin": 4020.436767578125, + "BackSpin": 3858.148193359375, + "SideSpin": 1130.75390625, + "HLA": 2.4021687507629395, + "VLA": 20.070960998535156, + "CarryDistance": 0 + }, + "ShotDataOptions": { + "ContainsBallData": true + } +} \ No newline at end of file diff --git a/assets/data/shot_session_2/shot_diff_analysis.csv b/assets/data/shot_session_2/shot_diff_analysis.csv new file mode 100644 index 0000000..66af12e --- /dev/null +++ b/assets/data/shot_session_2/shot_diff_analysis.csv @@ -0,0 +1,28 @@ +shot_name,speed_mph,vla_deg,hla_deg,total_spin_rpm,spin_axis_deg,physics_carry_yd,flightscope_carry_yd,diff_carry_yd,physics_total_yd,flightscope_total_yd,diff_total_yd,rollout_physics_yd,rollout_flightscope_yd,diff_rollout_yd,physics_apex_ft,flightscope_apex_ft,diff_apex_ft,status +shot_1,18.8,35.3,-3.4,3729,-0.0,7.2,,,11.3,,,4.1,,,3.9,,, +shot_10,89.7,25.5,-3.1,6446,6.2,122.1,115.1,7.0,130.2,116.4,13.8,8.1,1.3,6.8,66.9,72.0,-5.1,severe +shot_11,78.5,29.6,4.3,7350,10.7,101.1,93.0,8.1,106.0,93.6,12.4,4.9,0.6,4.3,63.7,64.0,-0.3,severe +shot_12,90.8,24.5,-3.9,7261,4.9,128.5,115.4,13.1,134.8,116.4,18.4,6.3,1.0,5.3,68.7,71.5,-2.8,severe +shot_13,95.2,19.6,-1.1,5610,12.2,126.4,125.3,1.1,131.4,128.7,2.7,5.0,3.4,1.6,51.1,58.4,-7.3,pass +shot_14,87.1,22.4,-2.0,5660,11.4,111.1,110.9,0.2,115.4,113.7,1.7,4.3,2.8,1.5,49.7,55.9,-6.2,pass +shot_15,88.1,14.2,-0.3,4985,19.4,95.7,101.3,-5.6,95.6,113.1,-17.5,-0.1,11.8,-11.9,24.2,28.9,-4.7,severe +shot_16,94.1,19.1,-5.9,5291,5.0,122.3,124.3,-2.0,127.1,128.1,-1.0,4.8,3.8,1.0,47.9,55.3,-7.4,pass +shot_17,122.9,14.4,-1.3,4119,6.7,189.3,184.0,5.3,197.2,190.3,6.9,7.9,6.3,1.6,65.3,71.8,-6.5,moderate +shot_18,105.7,20.5,-1.7,5667,8.2,150.6,146.8,3.8,162.2,149.4,12.8,11.6,2.6,9.0,71.8,80.4,-8.6,severe +shot_19,128.6,12.5,-8.4,2514,5.1,197.8,194.5,3.3,204.3,208.7,-4.4,6.5,14.2,-7.7,56.4,57.3,-0.9,moderate +shot_2,18.6,33.3,-2.1,3688,-0.0,6.8,,,11.2,,,4.4,,,3.4,,, +shot_20,120.9,15.0,-3.0,4245,9.6,184.1,179.4,4.7,191.9,185.3,6.6,7.8,5.9,1.9,65.6,72.3,-6.7,moderate +shot_21,127.3,7.2,-11.5,2321,-19.5,160.7,154.9,5.8,215.3,186.5,28.8,54.6,31.6,23.0,24.9,23.1,1.8,severe +shot_22,123.3,14.0,-6.1,3104,10.0,190.5,185.4,5.1,198.1,195.5,2.6,7.6,10.1,-2.5,60.8,63.0,-2.2,moderate +shot_23,125.5,8.6,-14.3,3062,-23.9,165.4,161.9,3.5,165.4,185.0,-19.6,0.0,23.1,-23.1,30.7,31.0,-0.3,severe +shot_24,121.5,14.5,-11.2,3173,-6.8,187.8,183.0,4.8,195.8,192.8,3.0,8.0,9.8,-1.8,62.0,64.6,-2.6,moderate +shot_25,127.2,5.0,-12.6,2671,-16.2,149.1,,,204.0,,,54.9,,,17.0,,, +shot_26,117.0,18.4,-2.0,5035,9.7,172.7,170.7,2.0,185.3,174.2,11.1,12.6,3.5,9.1,79.1,88.1,-9.0,severe +shot_27,113.0,17.6,-2.3,4292,12.2,165.7,164.5,1.2,175.2,169.6,5.6,9.5,5.1,4.4,66.8,73.6,-6.8,moderate +shot_3,30.7,32.9,-2.8,4070,-0.0,17.5,,,26.5,,,9.0,,,8.9,,, +shot_4,41.6,35.2,-1.0,5229,5.2,30.7,,,40.1,,,9.4,,,18.7,,, +shot_5,34.0,35.1,-1.2,4487,-0.0,21.4,,,30.6,,,9.2,,,12.2,,, +shot_6,34.8,36.4,-0.6,4515,0.9,22.4,,,30.9,,,8.5,,,13.4,,, +shot_7,41.4,18.0,-7.7,3110,-0.0,22.0,,,40.2,,,18.2,,,5.9,,, +shot_8,43.1,12.6,7.9,2821,18.9,16.6,,,37.9,,,21.3,,,3.0,,, +shot_9,94.4,20.1,2.4,4020,16.3,118.7,126.5,-7.8,125.8,132.5,-6.7,7.1,6.0,1.1,49.0,54.8,-5.8,severe diff --git a/game/ShotRecordingService.cs b/game/ShotRecordingService.cs new file mode 100644 index 0000000..af865c9 --- /dev/null +++ b/game/ShotRecordingService.cs @@ -0,0 +1,132 @@ +using System.IO; +using System.Text.Json; +using Godot; +using Godot.Collections; + +public partial class ShotRecordingService : Node +{ + private GlobalSettings _globalSettings; + private Setting _recordingEnabledSetting; + private Setting _recordingPathSetting; + + private string _currentSessionPath = string.Empty; + private int _shotCounter; + private bool _isRecording; + + public bool IsRecording => _isRecording; + public string CurrentSessionName => _isRecording ? Path.GetFileName(_currentSessionPath) : string.Empty; + public int ShotCount => _shotCounter; + + public override void _Ready() + { + _globalSettings = GetNodeOrNull("/root/GlobalSettings"); + + if (_globalSettings?.AppSettings == null) + return; + + _recordingEnabledSetting = _globalSettings.AppSettings.ShotRecordingEnabled; + _recordingPathSetting = _globalSettings.AppSettings.ShotRecordingPath; + + // Always start disabled regardless of persisted value + _recordingEnabledSetting.SetValue(false); + + _recordingEnabledSetting.SettingChanged += OnRecordingEnabledChanged; + } + + public override void _ExitTree() + { + if (_recordingEnabledSetting != null) + _recordingEnabledSetting.SettingChanged -= OnRecordingEnabledChanged; + } + + public void RecordShot(Dictionary ballData) + { + if (!_isRecording || ballData == null) + return; + + if (string.IsNullOrWhiteSpace(_currentSessionPath) || !Directory.Exists(_currentSessionPath)) + return; + + _shotCounter++; + + var shotJson = BuildShotJson(ballData); + string filePath = Path.Combine(_currentSessionPath, $"shot_{_shotCounter}.json"); + + try + { + File.WriteAllText(filePath, shotJson); + PhysicsLogger.Info($"ShotRecordingService: recorded shot {_shotCounter} to {filePath}"); + } + catch (IOException ex) + { + PhysicsLogger.Error($"ShotRecordingService: failed to write {filePath}: {ex.Message}"); + } + } + + private void OnRecordingEnabledChanged(Variant value) + { + bool enabled = (bool)value; + if (enabled) + StartNewSession(); + else + _isRecording = false; + } + + private void StartNewSession() + { + string basePath = _recordingPathSetting?.Value.ToString() ?? string.Empty; + if (string.IsNullOrWhiteSpace(basePath) || !Directory.Exists(basePath)) + { + PhysicsLogger.Error("ShotRecordingService: recording path is empty or does not exist."); + _isRecording = false; + return; + } + + int nextSession = 1; + while (Directory.Exists(Path.Combine(basePath, $"shot_session_{nextSession}"))) + nextSession++; + + _currentSessionPath = Path.Combine(basePath, $"shot_session_{nextSession}"); + Directory.CreateDirectory(_currentSessionPath); + _shotCounter = 0; + _isRecording = true; + + PhysicsLogger.Info($"ShotRecordingService: started session at {_currentSessionPath}"); + } + + private static string BuildShotJson(Dictionary ballData) + { + var shot = new System.Collections.Generic.Dictionary + { + ["BallData"] = ConvertGodotDict(ballData), + ["ShotDataOptions"] = new System.Collections.Generic.Dictionary + { + ["ContainsBallData"] = true + } + }; + + return JsonSerializer.Serialize(shot, new JsonSerializerOptions + { + WriteIndented = true + }); + } + + private static System.Collections.Generic.Dictionary ConvertGodotDict(Dictionary dict) + { + var result = new System.Collections.Generic.Dictionary(); + foreach (var key in dict.Keys) + { + Variant val = dict[key]; + result[key.ToString()] = val.VariantType switch + { + Variant.Type.Int => (long)val, + Variant.Type.Float => (double)val, + Variant.Type.Bool => (bool)val, + Variant.Type.String => val.ToString(), + _ => val.ToString() + }; + } + + return result; + } +} diff --git a/game/ShotRecordingService.cs.uid b/game/ShotRecordingService.cs.uid new file mode 100644 index 0000000..9f9d097 --- /dev/null +++ b/game/ShotRecordingService.cs.uid @@ -0,0 +1 @@ +uid://dcgumpg5yw6yn diff --git a/game/hole/HoleSceneControllerBase.cs b/game/hole/HoleSceneControllerBase.cs index 6110ced..3e972b3 100644 --- a/game/hole/HoleSceneControllerBase.cs +++ b/game/hole/HoleSceneControllerBase.cs @@ -56,6 +56,7 @@ public abstract partial class HoleSceneControllerBase : Node3D private AudioStreamPlayer3D _audioBackgroundBirds; private AudioStreamPlayer3D _audioGolfBallLanding; private TcpServer _tcpServer; + private ShotRecordingService _shotRecordingService; private GameSettings _gameSettings; private AppSettings _appSettings; private Setting _cameraOrbitDistanceSetting; @@ -211,6 +212,8 @@ private void InitializeCoreStage() if (_tcpServer != null) _tcpServer.HitBall += OnTcpClientHitBall; + _shotRecordingService = GetNodeOrNull("/root/ShotRecordingService"); + var globalSettings = GetNodeOrNull("/root/GlobalSettings"); if (globalSettings == null) { @@ -518,6 +521,7 @@ private void LaunchShot(Dictionary data, bool useTcpTracker, bool logPayload) UpdateBallDisplay(); PlayDriverHitAudio(); IncrementStrokeCount(); + _shotRecordingService?.RecordShot(data); if (useTcpTracker) _shotTracker.OnTcpClientHitBall(data); diff --git a/project.godot b/project.godot index 87451b3..5296f20 100644 --- a/project.godot +++ b/project.godot @@ -27,6 +27,7 @@ config/icon="uid://brlg0cmgh2ld8" PhantomCameraManager="*uid://duq6jhf6unyis" GlobalSettings="*res://utils/Settings/GlobalSettings.cs" TcpServerService="*res://tcp/TcpServer.cs" +ShotRecordingService="*res://game/ShotRecordingService.cs" GameProgressStore="*res://game/save/GameProgressStore.cs" CourseLoadService="*res://game/loading/CourseLoadService.cs" diff --git a/tools/shot_calibration/README.md b/tools/shot_calibration/README.md index f38b9d0..066a58b 100644 --- a/tools/shot_calibration/README.md +++ b/tools/shot_calibration/README.md @@ -4,6 +4,7 @@ Tools for comparing OpenFairway physics output against FlightScope reference dat ## Table of Contents +- [Prerequisites](#prerequisites) - [Overview](#overview) - [Directory Layout](#directory-layout) - [Shot Data Source](#shot-data-source) @@ -27,6 +28,11 @@ Tools for comparing OpenFairway physics output against FlightScope reference dat - [Quick Start (Automated)](#quick-start-automated) - [Manual Step-by-Step](#manual-step-by-step) - [Iterative Tuning Loop](#iterative-tuning-loop) +- [Session Calibration](#session-calibration) + - [Session Directory Layout](#session-directory-layout) + - [Session Quick Start](#session-quick-start) + - [Session Tool Usage](#session-tool-usage) + - [FlightScope Scraper Workaround](#flightscope-scraper-workaround) - [Diagnostic Report](#diagnostic-report) - [Status Thresholds](#status-thresholds) - [Error Patterns](#error-patterns) @@ -35,6 +41,27 @@ Tools for comparing OpenFairway physics output against FlightScope reference dat - [Iteration History](#iteration-history) - [Output Columns](#output-columns) +## Prerequisites + +The Python calibration tools require a virtual environment with the project dependencies. + +```bash +# Create and activate the venv (one-time setup) +python -m venv .venv +source .venv/bin/activate + +# Install dependencies +pip install -r tools/shot_calibration/requirements.txt +``` + +Always activate the venv before running any Python calibration tool: + +```bash +source .venv/bin/activate +``` + +The GDScript tools (`export_physics_csv.gd`, `export_physics_json.gd`) require Godot 4.5+ runtime and do not need the Python venv. + ## Overview The calibration system compares OpenFairway physics simulation output against FlightScope trajectory optimizer data (source of truth). The pipeline: @@ -54,6 +81,15 @@ assets/data/ ├── *.json # Shot input files (BallData from launch monitors) ├── SOT/ │ └── flightscope_reference.json # Source-of-truth reference data +├── shot_session_N/ # Session directories (from ShotRecordingService) +│ ├── shot_1.json # Recorded shot files +│ ├── shot_2.json +│ ├── physics.csv # Session physics export (via --session) +│ ├── flightscope_reference.json # Session FlightScope reference (via --session) +│ ├── flightscope.csv # Session FlightScope CSV (via --session) +│ ├── shot_diff_analysis.csv # Session diff CSV (via --session) +│ └── history/ # Session iteration history (via --session) +│ └── iteration_001.json └── calibration/ ├── physics.json # Physics simulation JSON export ├── physics.csv # Physics simulation CSV export @@ -131,7 +167,7 @@ Requires Godot runtime. Writes `res://assets/data/calibration/physics.json` by d ### `export_physics_csv.gd` -Runs every shot file through the physics export path and emits a CSV. Supports profile overrides via `--profile`. +Runs every shot file through the physics export path and emits a CSV. Supports profile overrides via `--profile` and session directories via `--session`. ```bash # Default export @@ -142,6 +178,9 @@ godot --headless --script tools/shot_calibration/export_physics_csv.gd -- --outp # With profile override (no C# rebuild needed) godot --headless --script tools/shot_calibration/export_physics_csv.gd -- --profile=assets/data/calibration/calibration_profile.json + +# Export from a session directory (reads shots from session dir, writes physics.csv there) +godot --headless --script tools/shot_calibration/export_physics_csv.gd -- --session=assets/data/shot_session_2 ``` Requires Godot runtime. Outputs columns: shot_name, filename, speed, VLA, HLA, spin, carry, total, rollout, apex, hang time, landing speed/angle, Re, spin ratio, Cd, Cl, peak Cl, carry-only. @@ -153,6 +192,9 @@ Exports FlightScope reference values as a matching CSV. Reads shot inputs from ` ```bash python tools/shot_calibration/export_flightscope_csv.py > assets/data/calibration/flightscope.csv python tools/shot_calibration/export_flightscope_csv.py --reference assets/data/SOT/flightscope_reference.json + +# Export from a session directory (reads shots + reference from session dir, outputs to stdout) +python tools/shot_calibration/export_flightscope_csv.py --session assets/data/shot_session_2 > assets/data/shot_session_2/flightscope.csv ``` No Godot runtime required. @@ -175,13 +217,16 @@ Output includes `rollout_physics_yd`, `rollout_flightscope_yd`, `diff_rollout_yd Diagnostic analyzer that reads `shot_diff_analysis.csv` and produces a structured report with error classification, shot regime tagging, parameter suggestions, and conflict detection. ```bash -# Text report (default) +# Text report (default — writes diagnostic_report.txt next to input file) python tools/shot_calibration/calibration_analyzer.py -# Custom input path +# Custom input path (report written next to input: path/to/diagnostic_report.txt) python tools/shot_calibration/calibration_analyzer.py --input path/to/shot_diff_analysis.csv -# JSON output (for programmatic consumption) +# Explicit output path +python tools/shot_calibration/calibration_analyzer.py --input path/to/shot_diff_analysis.csv --output /tmp/diagnostic_report.txt + +# JSON output (writes diagnostic_report.json next to input) python tools/shot_calibration/calibration_analyzer.py --json ``` @@ -235,6 +280,9 @@ python tools/shot_calibration/calibrate.py run --profile assets/data/calibration # Skip Godot export (reuse existing physics.csv) python tools/shot_calibration/calibrate.py run --skip-godot +# Run against a session directory (all outputs in session dir) +python tools/shot_calibration/calibrate.py run --session assets/data/shot_session_2 + # Show last iteration summary python tools/shot_calibration/calibrate.py status @@ -265,11 +313,17 @@ python tools/shot_calibration/flightscope_scraper.py # Scrape specific shots with visible browser python tools/shot_calibration/flightscope_scraper.py --shots driver1.json wood1.json --visible +# Scrape all shots from a session directory +python tools/shot_calibration/flightscope_scraper.py --session assets/data/shot_session_2 --visible + +# Attach to an existing Chrome instance (see "FlightScope Scraper Workaround" below) +python tools/shot_calibration/flightscope_scraper.py --session assets/data/shot_session_2 --debug-port 9222 + # Generate empty template for manual entry python tools/shot_calibration/flightscope_scraper.py --template ``` -Requires: `pip install selenium` and `brave` in your `PATH`. +Requires the project venv (see [Prerequisites](#prerequisites)) and Chrome or Brave in your `PATH`. ### `flightscope_discover.py` @@ -281,7 +335,7 @@ python tools/shot_calibration/flightscope_discover.py --fill-test-shot python tools/shot_calibration/flightscope_discover.py --headless ``` -Requires: `pip install selenium` +Requires the project venv (see [Prerequisites](#prerequisites)). ## Profile Override System @@ -457,6 +511,113 @@ The typical workflow cycles through: Each iteration is tracked in `assets/data/calibration/history/`. The orchestrator warns if previously-passing shots regress. +## Session Calibration + +Session directories (`assets/data/shot_session_N/`) are created by `ShotRecordingService` during gameplay. The `--session` flag lets you run the full calibration pipeline against a session's shots, with all output kept inside that session directory. + +### Session Directory Layout + +After running a full session calibration, the session directory contains: + +``` +assets/data/shot_session_2/ +├── shot_1.json # Recorded shot inputs +├── shot_2.json +├── ... +├── physics.csv # Physics simulation output +├── flightscope_reference.json # FlightScope scraped reference data +├── flightscope.csv # FlightScope reference CSV +├── shot_diff_analysis.csv # Physics vs FlightScope diff +└── history/ + └── iteration_001.json # Iteration snapshot +``` + +### Session Quick Start + +```bash +# Full pipeline: physics export + FlightScope scrape + compare + diagnose +python tools/shot_calibration/calibrate.py run --session assets/data/shot_session_2 + +# Skip Godot if physics.csv already exists in the session dir +python tools/shot_calibration/calibrate.py run --session assets/data/shot_session_2 --skip-godot +``` + +The `run --session` command: +1. Exports physics CSV from the session's shot files (Godot headless) +2. Scrapes FlightScope for each session shot (via `flightscope_scraper.py --session`) +3. Exports FlightScope reference CSV (via `export_flightscope_csv.py --session`) +4. Compares physics vs FlightScope (via `compare_csv.py`) +5. Runs diagnostic analysis and saves iteration to `/history/` + +### Session Tool Usage + +Each tool supports `--session` independently for step-by-step use: + +```bash +# Physics export only +godot --headless --script tools/shot_calibration/export_physics_csv.gd -- --session=assets/data/shot_session_2 + +# FlightScope scrape only (visible browser recommended) +python tools/shot_calibration/flightscope_scraper.py --session assets/data/shot_session_2 --visible + +# FlightScope CSV export only (outputs to stdout, redirect to session dir) +python tools/shot_calibration/export_flightscope_csv.py --session assets/data/shot_session_2 > assets/data/shot_session_2/flightscope.csv + +# Compare (default output is assets/data/calibration/; use --output for session dir) +python tools/shot_calibration/compare_csv.py assets/data/shot_session_2/physics.csv assets/data/shot_session_2/flightscope.csv --output assets/data/shot_session_2/shot_diff_analysis.csv + +# Diagnose (works with any diff CSV path) +python tools/shot_calibration/calibration_analyzer.py --input assets/data/shot_session_2/shot_diff_analysis.csv +``` + +Session mode auto-discovers all `*.json` files in the session directory as shots (no hardcoded shot map needed). The existing non-session workflow is unchanged — omitting `--session` uses the default `assets/data/` paths. + +### FlightScope Scraper Workaround + +The scraper uses `undetected-chromedriver` to avoid reCAPTCHA v3 bot detection. By default it creates a **persistent browser profile** at `~/.config/openfairway/scraper-profile` so reCAPTCHA can build engagement history across runs (fresh profiles score near 0.1, profiles with history score 0.7+). + +#### Standard Mode (persistent profile) + +```bash +source .venv/bin/activate + +# Uses default persistent profile at ~/.config/openfairway/scraper-profile +python tools/shot_calibration/flightscope_scraper.py --session assets/data/shot_session_2 --visible + +# Use a custom profile directory +python tools/shot_calibration/flightscope_scraper.py --browser-profile /tmp/my-profile --visible +``` + +> **Tip:** On first use, run a few manual shots with `--visible` to warm up the profile's reCAPTCHA score before running batch scrapes. + +#### Debug-Port Mode (attach to existing Chrome) + +If the standard mode still gets blocked, use `--debug-port` to attach to a real Chrome session with an established browsing profile. + +**Terminal 1** — Launch Chrome with remote debugging enabled: + +```bash +google-chrome --remote-debugging-port=9222 --user-data-dir=~/.config/openfairway/scraper-profile +``` + +Leave this running. On first use, navigate to https://trajectory.flightscope.com/ manually to establish a reCAPTCHA session. + +**Terminal 2** — Activate the venv and run the scraper attached to that browser: + +```bash +source .venv/bin/activate + +# Scrape session shots using the running Chrome instance +python tools/shot_calibration/flightscope_scraper.py --session assets/data/shot_session_2 --debug-port 9222 + +# Or scrape the default shot set +python tools/shot_calibration/flightscope_scraper.py --debug-port 9222 +``` + +The scraper navigates and fills forms in the already-running browser. When it finishes, the browser stays open (it is not quit by the script). You can re-run the scraper against different sessions without restarting Chrome. + +> **Tip:** If using Brave instead of Chrome, replace `google-chrome` with `brave-browser` in Terminal 1. The `--user-data-dir` path can be anything — it creates a dedicated profile that won't interfere with your normal browsing. + For parameters flagged as conflicting (e.g., `FlightTangentialRetentionBase` needed higher for `driver1` but lower for `wood_low_test_shot`), manual review is required. These usually indicate the physics model needs a regime-specific fix in the C# code rather than a single-value tweak. ## Diagnostic Report diff --git a/tools/shot_calibration/__pycache__/calibration_analyzer.cpython-313.pyc b/tools/shot_calibration/__pycache__/calibration_analyzer.cpython-313.pyc deleted file mode 100644 index 6f66960..0000000 Binary files a/tools/shot_calibration/__pycache__/calibration_analyzer.cpython-313.pyc and /dev/null differ diff --git a/tools/shot_calibration/calibrate.py b/tools/shot_calibration/calibrate.py index 12e199e..a9539c0 100644 --- a/tools/shot_calibration/calibrate.py +++ b/tools/shot_calibration/calibrate.py @@ -22,11 +22,12 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..")) -CALIBRATION_DIR = os.path.join(PROJECT_ROOT, "assets", "data", "calibration") +DATA_DIR = os.path.join(PROJECT_ROOT, "assets", "data") +CALIBRATION_DIR = os.path.join(DATA_DIR, "calibration") HISTORY_DIR = os.path.join(CALIBRATION_DIR, "history") PHYSICS_CSV = os.path.join(CALIBRATION_DIR, "physics.csv") FLIGHTSCOPE_CSV = os.path.join(CALIBRATION_DIR, "flightscope.csv") -SOT_CSV = os.path.join(PROJECT_ROOT, "assets", "data", "SOT", "flightscope_SoT.csv") +SOT_CSV = os.path.join(DATA_DIR, "SOT", "flightscope_SoT.csv") DIFF_CSV = os.path.join(CALIBRATION_DIR, "shot_diff_analysis.csv") DEFAULT_PROFILE = os.path.join(CALIBRATION_DIR, "calibration_profile.json") @@ -34,6 +35,124 @@ from calibration_analyzer import load_diff_csv, analyze, format_report +def discover_session_dirs(): + """Scan assets/data/ for shot_session_* directories.""" + sessions = [] + for entry in sorted(os.listdir(DATA_DIR)): + if entry.startswith("shot_session_") and os.path.isdir(os.path.join(DATA_DIR, entry)): + sessions.append(os.path.join(DATA_DIR, entry)) + return sessions + + +def session_prefix(session_dir): + """Extract prefix like 's2' from 'shot_session_2'.""" + basename = os.path.basename(session_dir) + num = basename.replace("shot_session_", "") + return f"s{num}" + + +def build_dirs_spec(session_dirs): + """Build --dirs spec string for Godot: 'res://assets/data|,res://assets/data/shot_session_2|s2,...'""" + parts = ["res://assets/data|"] + for sd in session_dirs: + rel = os.path.relpath(sd, PROJECT_ROOT).replace(os.sep, "/") + prefix = session_prefix(sd) + parts.append(f"res://{rel}|{prefix}") + return ",".join(parts) + + +def load_session_reference(session_dir): + """Load flightscope_reference.json from a session directory. Returns dict keyed by shot key.""" + ref_path = os.path.join(session_dir, "flightscope_reference.json") + if not os.path.exists(ref_path): + return {} + with open(ref_path, "r") as f: + return json.load(f) + + +def build_merged_flightscope_csv(sot_csv, session_dirs, output_path): + """Merge SoT CSV rows with session flightscope_reference.json entries into a combined CSV. + + Session shots get prefixed names (e.g., s2_shot_10). BackSpin/SideSpin are read from + the shot JSON files since the reference JSON doesn't contain them. + """ + rows = [] + + # Read standard SoT rows + if os.path.exists(sot_csv): + with open(sot_csv, "r") as f: + reader = csv.DictReader(f) + for row in reader: + rows.append(row) + + # Process each session's reference data + for sd in session_dirs: + ref_data = load_session_reference(sd) + prefix = session_prefix(sd) + + for shot_key, entry in sorted(ref_data.items()): + fname = entry.get("filename", f"{shot_key}.json") + shot_path = os.path.join(sd, fname) + + # Read backspin/sidespin from the shot JSON + backspin = 0.0 + sidespin = 0.0 + if os.path.exists(shot_path): + with open(shot_path, "r") as f: + shot_data = json.load(f) + ball = shot_data.get("BallData", shot_data) + backspin = ball.get("BackSpin", 0.0) + sidespin = ball.get("SideSpin", 0.0) + + carry = entry.get("carry_yd", 0.0) + total = entry.get("total_yd", 0.0) + rollout = total - carry if total > 0 and carry > 0 else 0.0 + + rows.append({ + "shot_name": f"{prefix}_{shot_key}", + "filename": fname, + "speed_mph": f"{entry.get('speed_mph', 0.0):.2f}", + "vla_deg": f"{entry.get('vla_deg', 0.0):.2f}", + "hla_deg": f"{entry.get('hla_deg', 0.0):.2f}", + "total_spin_rpm": f"{entry.get('total_spin_rpm', 0.0):.1f}", + "spin_axis_deg": f"{entry.get('spin_axis_deg', 0.0):.2f}", + "backspin_rpm": f"{backspin:.1f}", + "sidespin_rpm": f"{sidespin:.1f}", + "carry_yd": f"{carry:.1f}", + "total_yd": f"{total:.1f}", + "rollout_yd": f"{rollout:.1f}", + "apex_ft": f"{entry.get('apex_ft', 0.0):.1f}", + }) + + # Write combined CSV + os.makedirs(os.path.dirname(output_path), exist_ok=True) + fieldnames = ["shot_name", "filename", "speed_mph", "vla_deg", "hla_deg", + "total_spin_rpm", "spin_axis_deg", "backspin_rpm", "sidespin_rpm", + "carry_yd", "total_yd", "rollout_yd", "apex_ft"] + with open(output_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore") + writer.writeheader() + for row in rows: + writer.writerow(row) + + return rows + + +def filter_physics_csv(physics_csv, reference_shot_names): + """Remove rows from physics CSV whose shot_name is not in the reference set.""" + if not os.path.exists(physics_csv): + return + with open(physics_csv, "r") as f: + reader = csv.DictReader(f) + fieldnames = reader.fieldnames + rows = [row for row in reader if row["shot_name"] in reference_shot_names] + + with open(physics_csv, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + def run_command(cmd, description, cwd=None): """Run a shell command, printing output.""" print(f"\n--- {description} ---") @@ -58,11 +177,11 @@ def find_godot(): return "godot" -def get_next_iteration(): +def _get_next_iteration(history_dir): """Get the next iteration number from history.""" - os.makedirs(HISTORY_DIR, exist_ok=True) + os.makedirs(history_dir, exist_ok=True) existing = [ - f for f in os.listdir(HISTORY_DIR) + f for f in os.listdir(history_dir) if f.startswith("iteration_") and f.endswith(".json") ] if not existing: @@ -77,18 +196,26 @@ def get_next_iteration(): return max(numbers) + 1 if numbers else 1 -def load_iteration(n): +def get_next_iteration(): + return _get_next_iteration(HISTORY_DIR) + + +def _load_iteration(history_dir, n): """Load a specific iteration from history.""" - path = os.path.join(HISTORY_DIR, f"iteration_{n:03d}.json") + path = os.path.join(history_dir, f"iteration_{n:03d}.json") if not os.path.exists(path): return None with open(path, "r") as f: return json.load(f) -def save_iteration(iteration_num, profile_overrides, analysis_result, prev_iteration=None): +def load_iteration(n): + return _load_iteration(HISTORY_DIR, n) + + +def _save_iteration(history_dir, iteration_num, profile_overrides, analysis_result, prev_iteration=None): """Save an iteration snapshot to history.""" - os.makedirs(HISTORY_DIR, exist_ok=True) + os.makedirs(history_dir, exist_ok=True) per_shot = {} for diag in analysis_result["diagnostics"]: @@ -127,7 +254,7 @@ def save_iteration(iteration_num, profile_overrides, analysis_result, prev_itera "conflicts": [c["parameter"] for c in analysis_result["conflicts"]], } - path = os.path.join(HISTORY_DIR, f"iteration_{iteration_num:03d}.json") + path = os.path.join(history_dir, f"iteration_{iteration_num:03d}.json") with open(path, "w") as f: json.dump(snapshot, f, indent=2) f.write("\n") @@ -135,8 +262,27 @@ def save_iteration(iteration_num, profile_overrides, analysis_result, prev_itera return snapshot +def save_iteration(iteration_num, profile_overrides, analysis_result, prev_iteration=None): + return _save_iteration(HISTORY_DIR, iteration_num, profile_overrides, analysis_result, prev_iteration) + + def cmd_run(args): """Run a full calibration iteration.""" + # Resolve session mode: override all path constants when --session is provided + session_dir = None + physics_csv = PHYSICS_CSV + flightscope_csv = FLIGHTSCOPE_CSV + diff_csv = DIFF_CSV + history_dir = HISTORY_DIR + + if args.session: + session_dir = os.path.normpath(os.path.join(PROJECT_ROOT, args.session)) if not os.path.isabs(args.session) else os.path.normpath(args.session) + physics_csv = os.path.join(session_dir, "physics.csv") + flightscope_csv = os.path.join(session_dir, "flightscope.csv") + diff_csv = os.path.join(session_dir, "shot_diff_analysis.csv") + history_dir = os.path.join(session_dir, "history") + print(f"Session mode: {session_dir}") + profile_path = args.profile if not profile_path and os.path.exists(DEFAULT_PROFILE): profile_path = DEFAULT_PROFILE @@ -147,12 +293,24 @@ def cmd_run(args): with open(profile_path, "r") as f: profile_overrides = json.load(f) + # Discover session directories for unified mode (default, non-session) + session_dirs = [] + if not session_dir and not args.no_sessions: + session_dirs = discover_session_dirs() + if session_dirs: + prefixes = [session_prefix(sd) for sd in session_dirs] + print(f"Unified mode: including {len(session_dirs)} session(s): {', '.join(prefixes)}") + # Step 1: Export physics CSV (requires Godot) godot = find_godot() godot_cmd = [godot, "--headless", "--script", "tools/shot_calibration/export_physics_csv.gd", "--"] if profile_path: godot_cmd.append(f"--profile={profile_path}") - godot_cmd.append(f"--output={PHYSICS_CSV}") + if session_dir: + godot_cmd.append(f"--session={session_dir}") + elif session_dirs: + godot_cmd.append(f"--dirs={build_dirs_spec(session_dirs)}") + godot_cmd.append(f"--output={physics_csv}") if not args.skip_godot: if not run_command(godot_cmd, "Exporting physics CSV (Godot headless)"): @@ -160,13 +318,36 @@ def cmd_run(args): sys.exit(1) else: print("\n--- Skipping Godot export (--skip-godot) ---") - if not os.path.exists(PHYSICS_CSV): - print(f"ERROR: Physics CSV not found at {PHYSICS_CSV}", file=sys.stderr) + if not os.path.exists(physics_csv): + print(f"ERROR: Physics CSV not found at {physics_csv}", file=sys.stderr) sys.exit(1) # Step 2: FlightScope reference CSV - os.makedirs(os.path.dirname(FLIGHTSCOPE_CSV), exist_ok=True) - if args.export_flightscope: + os.makedirs(os.path.dirname(flightscope_csv), exist_ok=True) + if session_dir: + # Session mode: scrape FlightScope for session shots, then export CSV + print(f"\n--- Generating FlightScope reference for session ---") + scraper_cmd = [ + sys.executable, os.path.join(SCRIPT_DIR, "flightscope_scraper.py"), + "--session", session_dir, + ] + if not run_command(scraper_cmd, "Scraping FlightScope for session shots"): + print("WARNING: FlightScope scraper failed. Attempting export from existing reference.", file=sys.stderr) + + export_cmd = [ + sys.executable, os.path.join(SCRIPT_DIR, "export_flightscope_csv.py"), + "--session", session_dir, + ] + result = subprocess.run( + export_cmd, cwd=PROJECT_ROOT, capture_output=True, text=True + ) + if result.returncode != 0: + print(f"ERROR: FlightScope CSV export failed: {result.stderr}", file=sys.stderr) + sys.exit(1) + with open(flightscope_csv, "w") as f: + f.write(result.stdout) + print(f" Wrote {flightscope_csv}") + elif args.export_flightscope: # Legacy path: run export_flightscope_csv.py against flightscope_reference.json flightscope_cmd = [ sys.executable, os.path.join(SCRIPT_DIR, "export_flightscope_csv.py"), @@ -178,21 +359,38 @@ def cmd_run(args): if result.returncode != 0: print(f"ERROR: FlightScope export failed: {result.stderr}", file=sys.stderr) sys.exit(1) - with open(FLIGHTSCOPE_CSV, "w") as f: + with open(flightscope_csv, "w") as f: f.write(result.stdout) - print(f" Wrote {FLIGHTSCOPE_CSV}") + print(f" Wrote {flightscope_csv}") else: - # Default: copy the manually-maintained SoT CSV - print(f"\n--- Loading FlightScope SoT CSV ---") + # Default: SoT CSV + session references merged + print(f"\n--- Loading FlightScope reference data ---") if not os.path.exists(SOT_CSV): print(f"ERROR: SoT CSV not found at {SOT_CSV}", file=sys.stderr) print(" Use --export-flightscope to fall back to export_flightscope_csv.py", file=sys.stderr) sys.exit(1) - shutil.copy2(SOT_CSV, FLIGHTSCOPE_CSV) - print(f" Copied {SOT_CSV} -> {FLIGHTSCOPE_CSV}") + + if session_dirs: + merged_rows = build_merged_flightscope_csv(SOT_CSV, session_dirs, flightscope_csv) + # Count standard vs session shots + sot_count = 0 + session_count = 0 + for row in merged_rows: + carry = float(row.get("carry_yd", 0) or 0) + total = float(row.get("total_yd", 0) or 0) + if carry > 0 or total > 0: + if "_" in row["shot_name"] and row["shot_name"].split("_")[0].startswith("s"): + session_count += 1 + else: + sot_count += 1 + print(f" Merged FlightScope CSV: {sot_count} standard + {session_count} session shots") + print(f" Wrote {flightscope_csv}") + else: + shutil.copy2(SOT_CSV, flightscope_csv) + print(f" Copied {SOT_CSV} -> {flightscope_csv}") # Print reference coverage summary - with open(SOT_CSV, "r") as f: + with open(flightscope_csv, "r") as f: reader = csv.DictReader(f) total_shots = 0 shots_with_ref = 0 @@ -203,30 +401,39 @@ def cmd_run(args): if carry > 0 or total > 0: shots_with_ref += 1 missing = total_shots - shots_with_ref - print(f" FlightScope SoT: {shots_with_ref} of {total_shots} shots have reference data ({missing} missing)") + print(f" FlightScope reference: {shots_with_ref} of {total_shots} shots have reference data ({missing} missing)") + + # Step 2b: Filter physics CSV to only include shots with FlightScope reference data + if session_dirs and not session_dir: + with open(flightscope_csv, "r") as f: + reader = csv.DictReader(f) + ref_names = {row["shot_name"] for row in reader} + filter_physics_csv(physics_csv, ref_names) + print(f" Filtered physics CSV to {len(ref_names)} referenced shots") # Step 3: Compare CSVs compare_cmd = [ sys.executable, os.path.join(SCRIPT_DIR, "compare_csv.py"), - PHYSICS_CSV, FLIGHTSCOPE_CSV, - "--output", DIFF_CSV, + physics_csv, flightscope_csv, + "--output", diff_csv, ] if not run_command(compare_cmd, "Comparing physics vs FlightScope"): sys.exit(1) # Step 4: Run diagnostic analyzer print("\n--- Running diagnostic analysis ---") - rows = load_diff_csv(DIFF_CSV) + rows = load_diff_csv(diff_csv) if not rows: print("ERROR: No rows in diff CSV", file=sys.stderr) sys.exit(1) analysis_result = analyze(rows) - # Step 5: Save iteration snapshot - iteration_num = get_next_iteration() - prev_iteration = load_iteration(iteration_num - 1) if iteration_num > 1 else None - snapshot = save_iteration(iteration_num, profile_overrides, analysis_result, prev_iteration) + # Step 5: Save iteration snapshot (use session-local history dir) + os.makedirs(history_dir, exist_ok=True) + iteration_num = _get_next_iteration(history_dir) + prev_iteration = _load_iteration(history_dir, iteration_num - 1) if iteration_num > 1 else None + snapshot = _save_iteration(history_dir, iteration_num, profile_overrides, analysis_result, prev_iteration) # Step 6: Print report print(format_report(analysis_result)) @@ -241,7 +448,7 @@ def cmd_run(args): f"(total_diff: {reg['prev_total_diff']} -> {reg['curr_total_diff']})" ) - print(f"\nIteration {iteration_num} saved to {HISTORY_DIR}/iteration_{iteration_num:03d}.json") + print(f"\nIteration {iteration_num} saved to {history_dir}/iteration_{iteration_num:03d}.json") summary = analysis_result["summary"] print(f"Summary: {summary['pass']} pass, {summary['moderate']} moderate, {summary['severe']} severe") @@ -362,6 +569,8 @@ def parse_args(): run_parser.add_argument("--profile", default=None, help="Path to profile override JSON") run_parser.add_argument("--skip-godot", action="store_true", help="Skip Godot export (use existing physics CSV)") run_parser.add_argument("--export-flightscope", action="store_true", help="Run export_flightscope_csv.py instead of using SoT CSV") + run_parser.add_argument("--session", default=None, help="Session directory path (all outputs go into session dir)") + run_parser.add_argument("--no-sessions", action="store_true", help="Exclude session directories (standard shots only)") subparsers.add_parser("status", help="Show last iteration summary") subparsers.add_parser("history", help="Show all iteration summaries") diff --git a/tools/shot_calibration/calibration_analyzer.py b/tools/shot_calibration/calibration_analyzer.py index 7ac6be6..61f8798 100644 --- a/tools/shot_calibration/calibration_analyzer.py +++ b/tools/shot_calibration/calibration_analyzer.py @@ -8,7 +8,10 @@ Usage: python tools/shot_calibration/calibration_analyzer.py python tools/shot_calibration/calibration_analyzer.py --input path/to/shot_diff_analysis.csv + python tools/shot_calibration/calibration_analyzer.py --output /tmp/diagnostic_report.txt python tools/shot_calibration/calibration_analyzer.py --json + +By default, writes the report file (diagnostic_report.txt or .json) next to the input file. """ import argparse @@ -568,6 +571,11 @@ def parse_args(): action="store_true", help="Output as JSON instead of text report", ) + parser.add_argument( + "--output", + default=None, + help="Path to write report file (default: diagnostic_report.txt/.json next to input)", + ) return parser.parse_args() @@ -586,9 +594,24 @@ def main(): result = analyze(rows) if args.json: - print(json.dumps(result, indent=2)) + output_text = json.dumps(result, indent=2) + else: + output_text = format_report(result) + + # Determine output path: explicit --output, or default next to input file + if args.output: + output_path = args.output else: - print(format_report(result)) + input_dir = os.path.dirname(os.path.abspath(args.input)) + ext = ".json" if args.json else ".txt" + output_path = os.path.join(input_dir, f"diagnostic_report{ext}") + + print(output_text) + with open(output_path, "w") as f: + f.write(output_text) + if not output_text.endswith("\n"): + f.write("\n") + print(f"\nReport written to: {output_path}", file=sys.stderr) if __name__ == "__main__": diff --git a/tools/shot_calibration/export_flightscope_csv.py b/tools/shot_calibration/export_flightscope_csv.py index 1fc7c5c..6dffc3c 100644 --- a/tools/shot_calibration/export_flightscope_csv.py +++ b/tools/shot_calibration/export_flightscope_csv.py @@ -70,18 +70,30 @@ def main(): parser = argparse.ArgumentParser(description="Export FlightScope reference CSV") parser.add_argument( "--reference", - default=os.path.join(DATA_DIR, "SOT", "flightscope_reference.json"), + default=None, help="Path to flightscope_reference.json (default: assets/data/SOT/flightscope_reference.json)", ) parser.add_argument( "--data-dir", - default=DATA_DIR, + default=None, help="Path to shot data directory (default: assets/data/)", ) + parser.add_argument( + "--session", + default=None, + help="Session directory path (overrides --data-dir and --reference defaults)", + ) args = parser.parse_args() - data_dir = os.path.normpath(args.data_dir) - ref = load_reference(args.reference) + # Resolve session mode: --session sets defaults for --data-dir and --reference + if args.session: + session_dir = os.path.normpath(args.session) + data_dir = os.path.normpath(args.data_dir) if args.data_dir else session_dir + reference = args.reference if args.reference else os.path.join(session_dir, "flightscope_reference.json") + else: + data_dir = os.path.normpath(args.data_dir) if args.data_dir else os.path.normpath(DATA_DIR) + reference = args.reference if args.reference else os.path.join(DATA_DIR, "SOT", "flightscope_reference.json") + ref = load_reference(reference) files = discover_shots(data_dir) print(HEADER) diff --git a/tools/shot_calibration/export_physics_csv.gd b/tools/shot_calibration/export_physics_csv.gd index 8f66f15..b2b0d61 100644 --- a/tools/shot_calibration/export_physics_csv.gd +++ b/tools/shot_calibration/export_physics_csv.gd @@ -9,8 +9,21 @@ const PhysicsExportDataScript = preload("res://tools/shot_calibration/physics_ex func _init() -> void: var exporter = PhysicsExportDataScript.new() - var rows := exporter.collect_rows() - var output_path := exporter.resolve_output_path(PhysicsExportDataScript.DEFAULT_CSV_OUTPUT_PATH) + var dirs_spec := exporter.resolve_dirs_spec() + var rows: Array[Dictionary] + var default_output := PhysicsExportDataScript.DEFAULT_CSV_OUTPUT_PATH + + if dirs_spec != "": + rows = exporter.collect_rows_multi(dirs_spec) + else: + var session_path := exporter.resolve_session_path() + var data_dir_override := "" + if session_path != "": + data_dir_override = session_path + default_output = session_path + "/physics.csv" + rows = exporter.collect_rows(data_dir_override) + + var output_path := exporter.resolve_output_path(default_output) var error := exporter.write_csv(rows, output_path) if error != OK: quit(1) diff --git a/tools/shot_calibration/flightscope_scraper.py b/tools/shot_calibration/flightscope_scraper.py index fb9c6ef..b122b93 100644 --- a/tools/shot_calibration/flightscope_scraper.py +++ b/tools/shot_calibration/flightscope_scraper.py @@ -8,7 +8,7 @@ Outputs: assets/data/SOT/flightscope_reference.json Requirements: - pip install selenium + pip install selenium undetected-chromedriver Usage: python tools/shot_calibration/flightscope_scraper.py @@ -21,12 +21,14 @@ import json import math import os +import random import re import shutil import sys import time from pathlib import Path +import undetected_chromedriver as uc from selenium import webdriver from selenium.common.exceptions import TimeoutException from selenium.webdriver.chrome.options import Options @@ -52,8 +54,8 @@ "brave-browser", "brave", ] -FIELD_ACTION_DELAY_SEC = 2.0 -PRE_DISPLAY_CLICK_DELAY_SEC = 2.0 +FIELD_ACTION_DELAY_SEC = 2.5 +PRE_DISPLAY_CLICK_DELAY_SEC = 3.0 RESULT_WAIT_TIMEOUT_SEC = 15 SUBMIT_RETRY_COUNT = 2 RETRY_COOLDOWN_SEC = 2.0 @@ -88,6 +90,34 @@ } +def _human_delay(base_sec: float, jitter_fraction: float = 0.4): + """Sleep for a randomized duration around base_sec (±jitter_fraction).""" + jitter = base_sec * jitter_fraction + time.sleep(max(0.05, base_sec + random.uniform(-jitter, jitter))) + + +def _is_on_flightscope(driver) -> bool: + """Check if the browser is already on the FlightScope page.""" + try: + current = driver.current_url or "" + return "trajectory.flightscope.com" in current + except Exception: + return False + + +def _check_recaptcha_token_status(driver) -> str: + """Check if a reCAPTCHA response token is present.""" + script = """ + const ta = document.querySelector('textarea[name="g-recaptcha-response"]'); + if (!ta) return 'no_textarea'; + return ta.value ? 'token_present:' + ta.value.length : 'empty_token'; + """ + try: + return driver.execute_script(script) + except Exception: + return "check_failed" + + class SubmitBlockedError(RuntimeError): """Raised when a submit attempt is blocked and should not be retried.""" @@ -104,9 +134,9 @@ def __init__(self, reason: str, message: str): self.reason = reason -def load_shot_data(filename: str) -> dict: +def load_shot_data(filename: str, data_dir: Path = None) -> dict: """Load a shot JSON file and extract ball data fields.""" - path = DATA_DIR / filename + path = (data_dir or DATA_DIR) / filename if not path.exists(): print(f" WARNING: {path} not found, skipping") return None @@ -131,6 +161,17 @@ def load_shot_data(filename: str) -> dict: backspin = total_spin * math.cos(axis_rad) sidespin = total_spin * math.sin(axis_rad) + # Filter out shots outside FlightScope's useful input range + if speed <= 45: + print(f" SKIP: {filename} — speed {speed:.1f} mph <= 45 mph") + return None + if vla <= 5: + print(f" SKIP: {filename} — VLA {vla:.1f}° <= 5°") + return None + if total_spin < 1000 or total_spin > 12000: + print(f" SKIP: {filename} — total spin {total_spin:.0f} RPM outside 1000–12000 range") + return None + return { "speed_mph": speed, "vla_deg": vla, @@ -147,8 +188,27 @@ def _log(msg): print(f" [scraper] {msg}") -def _create_driver(visible: bool): - """Create a Selenium ChromeDriver session (Chrome preferred, Brave fallback).""" +def _create_driver(visible: bool, debug_port: int = None, browser_profile: str = None): + """Create a browser session via undetected-chromedriver. + + When *debug_port* is provided, attaches to an already-running Chrome + instance (started with ``--remote-debugging-port=``) using plain + Selenium instead (undetected-chromedriver cannot attach to an existing + browser). + + When *browser_profile* is provided (non-debug-port mode), uses a persistent + Chrome user-data-dir so reCAPTCHA v3 can build engagement history across runs. + """ + # Debug-port mode: attach to existing browser with plain Selenium + if debug_port: + chrome_options = Options() + chrome_options.add_experimental_option( + "debuggerAddress", f"localhost:{debug_port}", + ) + _log(f"Attaching to existing browser on localhost:{debug_port}") + return webdriver.Chrome(options=chrome_options) + + # Normal mode: use undetected-chromedriver to bypass bot detection browser_cmd, browser_path = _resolve_browser_binary() if browser_path is None: raise FileNotFoundError( @@ -156,31 +216,21 @@ def _create_driver(visible: bool): f"{', '.join(BROWSER_COMMAND_PREFERENCES)}" ) - chrome_options = Options() - if not visible: - chrome_options.add_argument("--headless=new") - chrome_options.add_argument("--window-size=1920,1080") - - # Anti-detection: hide automation signals to improve reCAPTCHA v3 score - chrome_options.add_argument("--disable-blink-features=AutomationControlled") - chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) - chrome_options.add_experimental_option("useAutomationExtension", False) - chrome_options.add_argument( - "--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " - "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" - ) - - # Enable performance logging for network diagnostics - chrome_options.set_capability("goog:loggingPrefs", {"performance": "ALL"}) + options = uc.ChromeOptions() + options.add_argument("--window-size=1920,1080") - chrome_options.binary_location = browser_path - _log(f"Launching browser command: {browser_cmd} ({browser_path})") - driver = webdriver.Chrome(options=chrome_options) + if browser_profile: + profile_path = Path(browser_profile) + profile_path.mkdir(parents=True, exist_ok=True) + options.add_argument(f"--user-data-dir={profile_path}") + _log(f"Using persistent browser profile: {profile_path}") - # Remove navigator.webdriver flag via CDP - driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { - "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});" - }) + _log(f"Launching undetected-chromedriver with: {browser_cmd} ({browser_path})") + driver = uc.Chrome( + options=options, + browser_executable_path=browser_path, + headless=not visible, + ) return driver @@ -259,6 +309,94 @@ def _toggle_wind_off(driver): _log(f"Error toggling wind: {e}") +def _warm_up_page(driver): + """Simulate natural browsing behavior before form interaction. + + reCAPTCHA v3 builds a behavioral profile from page load. Scrolling, + hovering, and pausing before interacting raises the trust score. + """ + _log("Warming up page with natural browsing behavior...") + try: + actions = ActionChains(driver) + + # Scroll down slowly + driver.execute_script("window.scrollBy({top: 300, behavior: 'smooth'})") + _human_delay(1.0, jitter_fraction=0.3) + + # Hover over a few visible elements (labels, headings) + hoverable_selectors = ["h1", "h2", "h3", "label", "p", "span"] + hoverable = [] + for sel in hoverable_selectors: + hoverable.extend( + el for el in driver.find_elements(By.TAG_NAME, sel) if el.is_displayed() + ) + if hoverable: + for el in random.sample(hoverable, min(len(hoverable), 3)): + try: + actions.move_to_element(el).pause(random.uniform(0.3, 0.7)).perform() + actions = ActionChains(driver) + except Exception: + pass + + _human_delay(0.8, jitter_fraction=0.4) + + # Scroll back to top + driver.execute_script("window.scrollTo({top: 0, behavior: 'smooth'})") + _human_delay(0.8, jitter_fraction=0.3) + _log("Page warm-up complete.") + except Exception as e: + _log(f"WARNING: Page warm-up failed (non-fatal): {e}") + + +def _between_shot_micro_interaction(driver): + """Add subtle natural behavior between shots to maintain reCAPTCHA score.""" + try: + # Scroll the results table into view + tables = driver.find_elements(By.TAG_NAME, "table") + for table in tables: + if table.is_displayed(): + driver.execute_script( + "arguments[0].scrollIntoView({behavior:'smooth', block:'center'});", + table, + ) + break + + # Move mouse to a random non-input element + non_inputs = [ + el for el in driver.find_elements(By.CSS_SELECTOR, "h1, h2, h3, label, th, td, p") + if el.is_displayed() + ] + if non_inputs: + target = random.choice(non_inputs) + ActionChains(driver).move_to_element(target).pause( + random.uniform(0.2, 0.5) + ).perform() + + # Small random scroll offset + offset = random.randint(-50, 80) + driver.execute_script(f"window.scrollBy({{top: {offset}, behavior: 'smooth'}})") + except Exception: + pass + + +CHAR_INPUT_DELAY_SEC = 0.08 + + +def _type_value_char_by_char(driver, element, value: str): + """Clear a field and type value one character at a time. + + Typing per-character with short delays ensures Vue/Vuetify reactive + input handlers register each keystroke, which bulk JS setter or + full-string send_keys can skip. + """ + # Select all + delete to clear existing content + element.send_keys(Keys.CONTROL + "a") + element.send_keys(Keys.DELETE) + for char in str(value): + element.send_keys(char) + _human_delay(CHAR_INPUT_DELAY_SEC, jitter_fraction=0.6) + + def _fill_field_by_label(driver, label_fragment, value): """Find an input field by nearby label text and fill it with value.""" label_els = driver.find_elements(By.XPATH, @@ -274,20 +412,10 @@ def _fill_field_by_label(driver, label_fragment, value): inputs = ancestor.find_elements(By.TAG_NAME, "input") for inp in inputs: if inp.is_displayed(): - inp.click() - # Use native setter + dispatch events to trigger Vue/Vuetify reactivity - try: - driver.execute_script(""" - const nativeSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value').set; - nativeSetter.call(arguments[0], arguments[1]); - arguments[0].dispatchEvent(new Event('input', { bubbles: true })); - arguments[0].dispatchEvent(new Event('change', { bubbles: true })); - """, inp, str(value)) - except Exception: - # Fallback to send_keys if JS setter fails - inp.send_keys(Keys.CONTROL + "a") - inp.send_keys(str(value)) + ActionChains(driver).move_to_element(inp).pause( + random.uniform(0.1, 0.3) + ).click().perform() + _type_value_char_by_char(driver, inp, value) return True except Exception: continue @@ -330,13 +458,24 @@ def _set_direction_dropdown(driver, label_fragment, direction): def _wait_after_form_action(label): - """Apply fixed pacing between form actions.""" - _log(f"Waiting {FIELD_ACTION_DELAY_SEC:.1f}s after {label}") - time.sleep(FIELD_ACTION_DELAY_SEC) + """Apply humanized pacing between form actions.""" + _log(f"Waiting ~{FIELD_ACTION_DELAY_SEC:.1f}s after {label}") + _human_delay(FIELD_ACTION_DELAY_SEC) def _fill_shot_form(driver, shot_data): """Fill all form fields for a single shot.""" + try: + first_input = driver.find_element(By.TAG_NAME, "input") + if first_input.is_displayed(): + driver.execute_script( + "arguments[0].scrollIntoView({behavior:'smooth', block:'center'});", + first_input, + ) + _human_delay(0.5, jitter_fraction=0.5) + except Exception: + pass + # VLA if not _fill_field_by_label(driver, "Launch V", str(round(shot_data["vla_deg"], 1))): _log("WARNING: Could not fill Launch V field") @@ -525,16 +664,18 @@ def resolve_submit_button(drv): if _is_recaptcha_challenge_visible(driver): return {"ok": False, "reason": "blocked_captcha", "detail": "captcha_challenge_visible"} - _log(f"Waiting {PRE_DISPLAY_CLICK_DELAY_SEC:.1f}s before pressing DISPLAY SHOT") - time.sleep(PRE_DISPLAY_CLICK_DELAY_SEC) + _log(f"Waiting ~{PRE_DISPLAY_CLICK_DELAY_SEC:.1f}s before pressing DISPLAY SHOT") + _human_delay(PRE_DISPLAY_CLICK_DELAY_SEC) # Simulate mouse movement through form inputs to improve reCAPTCHA v3 score try: actions = ActionChains(driver) - for inp in driver.find_elements(By.TAG_NAME, "input")[:3]: - if inp.is_displayed(): - actions.move_to_element(inp).pause(0.3) - actions.move_to_element(btn).pause(0.5).click().perform() + visible_inputs = [inp for inp in driver.find_elements(By.TAG_NAME, "input") + if inp.is_displayed()] + sample_count = min(len(visible_inputs), random.randint(2, 4)) + for inp in random.sample(visible_inputs, sample_count): + actions.move_to_element(inp).pause(random.uniform(0.15, 0.5)) + actions.move_to_element(btn).pause(random.uniform(0.3, 0.8)).click().perform() except Exception: _log("ActionChains click failed, trying native click") try: @@ -675,6 +816,12 @@ def _extract_api_requests(driver): def _capture_debug_artifacts(driver, shot_name: str, attempt: int, reason: str): """Capture debugging artifacts when submit did not produce results.""" + try: + _ = driver.current_url + except Exception: + _log("WARNING: Browser session is stale, cannot capture debug artifacts") + return + DEBUG_ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) timestamp = int(time.time()) safe_reason = re.sub(r"[^a-zA-Z0-9_-]", "_", reason)[:40] @@ -753,40 +900,61 @@ def _build_failed_result_entry(shot_data, reason: str) -> dict: } +def _save_results(results: dict, output_path: Path): + """Incrementally persist results after each shot.""" + if output_path is None: + return + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w") as f: + json.dump(results, f, indent=2) + + def scrape_flightscope( shots: dict, visible: bool = False, + debug_port: int = None, + output_path: Path = None, + browser_profile: str = None, ) -> dict: """ Automate FlightScope trajectory optimizer to get carry/total/apex. - Uses Selenium ChromeDriver with Brave. + When *debug_port* is set, attaches to an existing browser and leaves it + running after scraping completes. """ - driver = _create_driver(visible) + driver = _create_driver(visible, debug_port=debug_port, browser_profile=browser_profile) wait = WebDriverWait(driver, 15) results = {} shot_statuses = {} try: - # Navigate once and do initial setup - _log(f"Navigating to {URL}") - driver.get(URL) - - try: - wait.until(EC.presence_of_element_located((By.TAG_NAME, "input"))) - except Exception: - _log("WARNING: No inputs found after 15s") + # Skip navigation when already on FlightScope (preserves reCAPTCHA v3 score) + already_on_page = debug_port and _is_on_flightscope(driver) - time.sleep(2) + if already_on_page: + _log("Already on FlightScope — skipping navigation to preserve reCAPTCHA score") + try: + wait.until(EC.presence_of_element_located((By.TAG_NAME, "input"))) + except Exception: + _log("WARNING: No inputs found, falling back to navigation") + already_on_page = False - # Dismiss weather popup + toggle wind off (one-time setup) - _dismiss_weather_popup(driver, wait) - time.sleep(1) - _toggle_wind_off(driver) - time.sleep(1) + if not already_on_page: + _log(f"Navigating to {URL}") + driver.get(URL) + try: + wait.until(EC.presence_of_element_located((By.TAG_NAME, "input"))) + except Exception: + _log("WARNING: No inputs found after 15s") + time.sleep(2) + _dismiss_weather_popup(driver, wait) + time.sleep(1) + _toggle_wind_off(driver) + time.sleep(1) + _warm_up_page(driver) # Process each shot on the same page - for shot_name, shot_data in shots.items(): + for shot_index, (shot_name, shot_data) in enumerate(shots.items()): if shot_data is None: continue @@ -804,6 +972,8 @@ def scrape_flightscope( _log(f" table state before submit: {before_state}") click_result = _click_display_shot(driver, wait) + token_status = _check_recaptcha_token_status(driver) + _log(f" reCAPTCHA token: {token_status}") if not click_result["ok"]: failure_reason = click_result["reason"] or failure_reason if failure_reason.startswith("blocked_"): @@ -866,7 +1036,16 @@ def scrape_flightscope( if not table_result: _log(f"ERROR: {shot_name} failed ({failure_reason})") results[shot_name] = _build_failed_result_entry(shot_data, failure_reason) + _save_results(results, output_path) shot_statuses[shot_name] = {"status": "failed", "reason": failure_reason} + if shot_index < len(shots) - 1: + if failure_reason.startswith("blocked_"): + inter_shot_sec = random.uniform(8.0, 15.0) + else: + inter_shot_sec = random.uniform(1.5, 4.0) + _log(f"Inter-shot pause: {inter_shot_sec:.1f}s") + _between_shot_micro_interaction(driver) + time.sleep(inter_shot_sec) continue result_entry = { @@ -880,11 +1059,18 @@ def scrape_flightscope( result_entry.update(table_result) results[shot_name] = result_entry + _save_results(results, output_path) shot_statuses[shot_name] = {"status": "success"} _log(f" -> carry={table_result.get('carry_yd', '?')} yd, " f"total={table_result.get('total_yd', '?')} yd, " f"apex={table_result.get('apex_ft', '?')} ft") + if shot_index < len(shots) - 1: + inter_shot_sec = random.uniform(1.5, 4.0) + _log(f"Inter-shot pause: {inter_shot_sec:.1f}s") + _between_shot_micro_interaction(driver) + time.sleep(inter_shot_sec) + if shot_statuses: _log("Shot status summary:") for shot_name, status in shot_statuses.items(): @@ -894,8 +1080,11 @@ def scrape_flightscope( _log(f" {shot_name}: failed({status['reason']})") finally: - driver.quit() - _log("Browser closed.") + if debug_port: + _log("Detaching from browser (leaving it running).") + else: + driver.quit() + _log("Browser closed.") return results @@ -940,27 +1129,53 @@ def _resolve_shot_arg(shot_arg: str): return name, filename +def _discover_session_shots(session_dir: Path) -> dict: + """Auto-discover all *.json files in a session directory as shot map.""" + shot_map = {} + for path in sorted(session_dir.glob("*.json")): + shot_map[path.stem] = path.name + return shot_map + + def main(): parser = argparse.ArgumentParser(description="Scrape FlightScope trajectory data for calibration") parser.add_argument("--shots", nargs="*", help="Specific shot filenames to scrape (default: all)") + parser.add_argument("--session", type=str, default=None, help="Session directory path (auto-discovers shots, outputs to session dir)") parser.add_argument("--template", action="store_true", help="Generate empty template for manual entry") parser.add_argument("--visible", action="store_true", help="Run with visible browser window (default: headless)") - parser.add_argument("--output", type=str, default=str(OUTPUT_FILE), help="Output file path") + parser.add_argument("--debug-port", type=int, default=None, help="Attach to existing Chrome on this debugging port (e.g. 9222)") + parser.add_argument( + "--browser-profile", type=str, + default=str(Path.home() / ".config/openfairway/scraper-profile"), + help="Chrome user-data-dir for persistent profile (default: ~/.config/openfairway/scraper-profile)", + ) + parser.add_argument("--output", type=str, default=None, help="Output file path") args = parser.parse_args() + # Resolve session mode + session_dir = Path(args.session) if args.session else None + if session_dir and not session_dir.is_absolute(): + session_dir = REPO_ROOT / session_dir + data_dir = session_dir if session_dir else DATA_DIR + output_path = Path(args.output) if args.output else ( + session_dir / "flightscope_reference.json" if session_dir else OUTPUT_FILE + ) + # Build shot list if args.shots: shot_map = {} for shot_arg in args.shots: shot_name, filename = _resolve_shot_arg(shot_arg) shot_map[shot_name] = filename + elif session_dir: + shot_map = _discover_session_shots(session_dir) else: shot_map = DEFAULT_SHOTS # Load shot data shots = {} for name, filename in shot_map.items(): - data = load_shot_data(filename) + data = load_shot_data(filename, data_dir=data_dir) if data: shots[name] = data @@ -969,10 +1184,12 @@ def main(): if args.template: results = create_manual_reference(shots) else: - results = scrape_flightscope(shots, visible=args.visible) + results = scrape_flightscope( + shots, visible=args.visible, debug_port=args.debug_port, + output_path=output_path, browser_profile=args.browser_profile, + ) # Write output - output_path = Path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w") as f: json.dump(results, f, indent=2) diff --git a/tools/shot_calibration/physics_export_data.gd b/tools/shot_calibration/physics_export_data.gd index ae74daa..e803cb8 100644 --- a/tools/shot_calibration/physics_export_data.gd +++ b/tools/shot_calibration/physics_export_data.gd @@ -7,11 +7,12 @@ const CSV_HEADER := "shot_name,filename,speed_mph,vla_deg,hla_deg,total_spin_rpm const DEFAULT_CSV_OUTPUT_PATH := "res://assets/data/calibration/physics.csv" const DEFAULT_JSON_OUTPUT_PATH := "res://assets/data/calibration/physics.json" -func collect_rows() -> Array[Dictionary]: +func collect_rows(data_dir_override: String = "") -> Array[Dictionary]: + var data_dir := data_dir_override if data_dir_override != "" else DATA_DIR_PATH var adapter := _create_adapter() - var dir := DirAccess.open(DATA_DIR_PATH) + var dir := DirAccess.open(data_dir) if dir == null: - push_error("ERROR: cannot open %s" % DATA_DIR_PATH) + push_error("ERROR: cannot open %s" % data_dir) return [] var files: Array[String] = [] @@ -26,12 +27,55 @@ func collect_rows() -> Array[Dictionary]: var rows: Array[Dictionary] = [] for fname_iter in files: - var row := _collect_row(adapter, fname_iter) + var row := _collect_row(adapter, fname_iter, data_dir) if not row.is_empty(): rows.append(row) return rows +func resolve_dirs_spec() -> String: + var args := OS.get_cmdline_user_args() + for i in range(args.size()): + var arg: String = args[i] + if arg.begins_with("--dirs="): + return arg.trim_prefix("--dirs=") + if arg == "--dirs" and i + 1 < args.size(): + return args[i + 1] + return "" + +func collect_rows_multi(dirs_spec: String) -> Array[Dictionary]: + var adapter := _create_adapter() + var all_rows: Array[Dictionary] = [] + var entries := dirs_spec.split(",", false) + for entry in entries: + var parts := entry.split("|", true, 1) + var dir_path := parts[0].strip_edges() + var prefix := parts[1].strip_edges() if parts.size() > 1 else "" + + var dir := DirAccess.open(dir_path) + if dir == null: + push_error("ERROR: cannot open %s" % dir_path) + continue + + var files: Array[String] = [] + dir.list_dir_begin() + var fname := dir.get_next() + while fname != "": + if fname.ends_with(".json") and fname not in SKIP_FILES: + files.append(fname) + fname = dir.get_next() + dir.list_dir_end() + files.sort() + + for fname_iter in files: + var row := _collect_row(adapter, fname_iter, dir_path) + if not row.is_empty(): + if prefix != "": + row["shot_name"] = prefix + "_" + row["shot_name"] + all_rows.append(row) + + return all_rows + func to_keyed_dictionary(rows: Array[Dictionary]) -> Dictionary: var output := {} for row in rows: @@ -68,8 +112,8 @@ func format_csv_row(row: Dictionary) -> String: row["initial_cl"], row["peak_cl"], row["carry_only_yd"], ] -func _collect_row(adapter: PhysicsAdapter, fname_iter: String) -> Dictionary: - var path := "%s/%s" % [DATA_DIR_PATH, fname_iter] +func _collect_row(adapter: PhysicsAdapter, fname_iter: String, data_dir: String = DATA_DIR_PATH) -> Dictionary: + var path := "%s/%s" % [data_dir, fname_iter] var file := FileAccess.open(path, FileAccess.READ) if file == null: push_warning("WARN: cannot open %s" % path) @@ -135,6 +179,16 @@ func resolve_profile_path() -> String: return _normalize_output_path(args[i + 1]) return "" +func resolve_session_path() -> String: + var args := OS.get_cmdline_user_args() + for i in range(args.size()): + var arg: String = args[i] + if arg.begins_with("--session="): + return _normalize_output_path(arg.trim_prefix("--session=")) + if arg == "--session" and i + 1 < args.size(): + return _normalize_output_path(args[i + 1]) + return "" + func _create_adapter() -> PhysicsAdapter: var adapter := PhysicsAdapter.new() var profile_path := resolve_profile_path() diff --git a/tools/shot_calibration/requirements.txt b/tools/shot_calibration/requirements.txt new file mode 100644 index 0000000..828c929 --- /dev/null +++ b/tools/shot_calibration/requirements.txt @@ -0,0 +1,2 @@ +selenium>=4.9.0 +undetected-chromedriver>=3.5.0 diff --git a/ui/SettingsPanel.cs b/ui/SettingsPanel.cs index 5f29beb..c60cf91 100644 --- a/ui/SettingsPanel.cs +++ b/ui/SettingsPanel.cs @@ -35,6 +35,11 @@ public enum SettingsTab private SpinBox _cameraDelayValue; private Label _cameraDelayHelper; private SpinBox _tcpPortValue; + private CheckBox _shotRecordingCheck; + private LineEdit _shotRecordingPathInput; + private Button _shotRecordingBrowseButton; + private Label _shotRecordingHelper; + private FileDialog _shotRecordingFileDialog; private TabContainer _tabs; private GridContainer _panelsGrid; private Label _panelsEmptyLabel; @@ -54,6 +59,9 @@ public enum SettingsTab private Setting _cameraDistanceSetting; private Setting _cameraDelaySetting; private Setting _tcpPortSetting; + private Setting _shotRecordingEnabledSetting; + private Setting _shotRecordingPathSetting; + private ShotRecordingService _shotRecordingService; private bool _isSyncingControls; private bool _isSyncingPanelsGrid; private GridCanvas _boundGridCanvas; @@ -73,6 +81,10 @@ public override void _Ready() _cameraDelayValue = GetNode("Root/Panel/Margin/Content/Tabs/Game/CameraDelayCard/CameraDelayMargin/CameraDelayContent/CameraDelayRow/CameraDelayValue"); _cameraDelayHelper = GetNode