"""Tests for the Frontier F01 control lab.Run from frontier_code/: python -m unittest discover -s tests -p "test_*.py" -vTest your own starter implementation instead of the reference: MANUAL_IMPL=starters python -m unittest discover -s tests -p "test_*.py" -v(PowerShell: $env:MANUAL_IMPL="starters"; python -m unittest discover -s tests -p "test_*.py" -v)"""importimportlibimportmathimportosimportunittestPACKAGE=os.environ.get("MANUAL_IMPL","solutions")lab=importlib.import_module(f"{PACKAGE}.control_lab")MASS=1.5classPIDTests(unittest.TestCase):deftest_proportional_only_output(self):pid=lab.PID(kp=2.0,ki=0.0,kd=0.0)self.assertAlmostEqual(pid.update(10.0,7.0,0.01),6.0)deftest_no_derivative_kick_on_setpoint_jump(self):# Measurement is steady; only the target jumps. Derivative-on-measurement# must contribute nothing, so the output change is purely P + I.pid=lab.PID(kp=1.0,ki=0.0,kd=50.0)pid.update(0.0,5.0,0.01)out=pid.update(10.0,5.0,0.01)self.assertAlmostEqual(out,5.0)deftest_derivative_opposes_rising_measurement(self):pid=lab.PID(kp=0.0,ki=0.0,kd=2.0)pid.update(0.0,0.0,0.1)self.assertAlmostEqual(pid.update(0.0,0.5,0.1),-10.0)deftest_output_is_clamped(self):pid=lab.PID(kp=100.0,ki=0.0,kd=0.0,out_min=-1.0,out_max=1.0)self.assertEqual(pid.update(10.0,0.0,0.01),1.0)self.assertEqual(pid.update(-10.0,0.0,0.01),-1.0)deftest_integral_frozen_while_saturated_in_error_direction(self):pid=lab.PID(kp=10.0,ki=1.0,kd=0.0,out_min=0.0,out_max=1.0)for_inrange(100):pid.update(10.0,0.0,0.01)self.assertEqual(pid.integral,0.0)deftest_rejects_nonpositive_dt(self):withself.assertRaises(ValueError):lab.PID(1.0,0.0,0.0).update(1.0,0.0,0.0)classHoverTests(unittest.TestCase):deftest_p_only_steady_state_error_matches_theory(self):# At rest, thrust must equal weight: kp * e = m g, so e = m g / kp.forkpin(5.0,10.0):s=lab.simulate_hover(kp,0.0,3.0,mass=MASS,duration=30.0)m=lab.step_metrics(s,10.0)self.assertAlmostEqual(m.steady_state_error,MASS*lab.G/kp,delta=0.01)deftest_feedforward_removes_error_when_mass_is_known(self):s=lab.simulate_hover(4.0,0.0,3.0,mass=MASS,feedforward_mass=MASS,duration=15.0)self.assertLess(abs(lab.step_metrics(s,10.0).steady_state_error),0.005)deftest_unknown_payload_leaves_error_without_integral(self):s=lab.simulate_hover(4.0,0.0,3.0,mass=MASS,feedforward_mass=MASS,payload=0.3,duration=15.0)self.assertAlmostEqual(lab.step_metrics(s,10.0).steady_state_error,0.3*lab.G/4.0,delta=0.01)deftest_integral_removes_payload_error(self):s=lab.simulate_hover(4.0,1.0,3.0,mass=MASS,feedforward_mass=MASS,payload=0.3,duration=20.0)self.assertLess(abs(lab.step_metrics(s,10.0).steady_state_error),0.02)deftest_thrust_stays_within_motor_limits(self):s=lab.simulate_hover(50.0,5.0,1.0,mass=MASS,feedforward_mass=MASS,setpoint=40.0,duration=10.0)t_max=2.0*MASS*lab.Gself.assertTrue(all(-1e-9<=x.thrust<=t_max+1e-9forxins))deftest_anti_windup_reduces_overshoot(self):kwargs=dict(mass=MASS,feedforward_mass=MASS,setpoint=50.0,duration=30.0)with_aw=lab.step_metrics(lab.simulate_hover(6.0,2.0,4.0,anti_windup=True,**kwargs),50.0)without=lab.step_metrics(lab.simulate_hover(6.0,2.0,4.0,anti_windup=False,**kwargs),50.0)self.assertLess(with_aw.overshoot_pct,20.0)self.assertGreater(without.overshoot_pct,3.0*with_aw.overshoot_pct)deftest_noise_is_reproducible_with_seed(self):a=lab.simulate_hover(4.0,1.0,3.0,feedforward_mass=MASS,noise_std=0.05,seed=7,duration=2.0)b=lab.simulate_hover(4.0,1.0,3.0,feedforward_mass=MASS,noise_std=0.05,seed=7,duration=2.0)self.assertEqual([x.zforxina],[x.zforxinb])deftest_finite_difference_amplifies_noise(self):# Noise of 1 cm on position becomes ~1.4 m/s of noise in a 0.01 s# backward difference: sqrt(2) * sigma / dt.importrandomrng=random.Random(1)dt,sigma=0.01,0.01noisy=[rng.gauss(0.0,sigma)for_inrange(20000)]d=lab.finite_difference(noisy,dt)[1:]std=math.sqrt(sum(v*vforvind)/len(d))self.assertAlmostEqual(std,math.sqrt(2.0)*sigma/dt,delta=0.05)classCarTests(unittest.TestCase):deftest_constant_steer_traces_circle_of_radius_L_over_tan_delta(self):wheelbase,steer=2.9,math.radians(5.0)radius=wheelbase/math.tan(steer)s=lab.CarState(0.0,0.0,0.0,0.0)for_inrange(3000):s=lab.bicycle_step(s,5.0,steer,wheelbase,0.01)self.assertAlmostEqual(math.hypot(s.x,s.y-radius),radius,delta=0.1)deftest_pure_pursuit_curvature_formula(self):# A car at the origin heading +x; target on the circle of radius R that is# tangent to the heading. Steering must reproduce that circle.wheelbase,radius=2.9,25.0theta=0.4# angle travelled along the circle to reach the targettarget=(radius*math.sin(theta),radius-radius*math.cos(theta))delta=lab.pure_pursuit_steer(lab.CarState(0.0,0.0,0.0,0.0),target,wheelbase)self.assertAlmostEqual(wheelbase/math.tan(delta),radius,places=9)deftest_converges_to_straight_line(self):_,err=lab.simulate_pure_pursuit(lab.straight_path(),start=lab.CarState(0.0,2.0,0.0,0.0),lookahead=8.0,duration=20.0)self.assertLess(err[-1],0.01)deftest_longer_lookahead_converges_slower_but_cuts_more(self):deffirst_settled(errors):returnnext(ifori,einenumerate(errors)ifall(x<0.1forxinerrors[i:]))start=lab.CarState(0.0,2.0,0.0,0.0)_,short=lab.simulate_pure_pursuit(lab.straight_path(),start=start,lookahead=4.0)_,long=lab.simulate_pure_pursuit(lab.straight_path(),start=start,lookahead=16.0)self.assertLess(first_settled(short),first_settled(long))_,lc_short=lab.simulate_pure_pursuit(lab.lane_change_path(),start=lab.CarState(0,0,0,0),lookahead=4.0,duration=18.0)_,lc_long=lab.simulate_pure_pursuit(lab.lane_change_path(),start=lab.CarState(0,0,0,0),lookahead=16.0,duration=18.0)self.assertLess(max(lc_short),max(lc_long))deftest_tracks_a_looping_path(self):# Regression: a global nearest-point search jumps to the second lap.states,err=lab.simulate_pure_pursuit(lab.circle_path(30.0,turns=1.5),start=lab.CarState(0.0,0.0,0.0,0.0),lookahead=8.0,duration=25.0)self.assertLess(max(err),0.1)deftest_slow_steering_plus_short_lookahead_rings(self):start=lab.CarState(0.0,2.0,0.0,0.0)states,_=lab.simulate_pure_pursuit(lab.straight_path(600.0),start=start,lookahead=7.0,speed=20.0,max_steer_rate=0.5,duration=12.0)ys=[s.yforsinstates]crossings=sum(1fora,binzip(ys,ys[1:])ifa*b<0)self.assertGreater(crossings,5)if__name__=="__main__":unittest.main()