def test_entity_view(self): """Test that setting and getting _EntityView attrs propagate.""" ps = PhysicsState(None, self.proto_state) self.assertEqual(ps[0].name, 'First') entity = ps[0] self.assertTrue(isinstance(entity, _EntityView)) self.assertEqual(entity.x, 10) self.assertEqual(entity.y, 20) self.assertEqual(entity.vx, 30) self.assertEqual(entity.vy, 40) self.assertEqual(entity.spin, 50) self.assertEqual(entity.fuel, 60) self.assertEqual(entity.landed_on, '') self.assertEqual(entity.throttle, 70) ps.y0() self.assertEqual(entity.heading, 7 % (2 * np.pi)) ps[0].landed_on = 'Second' self.assertEqual(entity.landed_on, 'Second') entity.x = 500 self.assertEqual(ps[0].x, 500) entity.pos = np.array([55, 66]) self.assertEqual(ps['First'].x, 55) self.assertEqual(ps['First'].y, 66)
def test_y_vector_init(self): """Test that initializing with a y-vector uses y-vector values.""" y0 = np.concatenate(( np.array([ 10, 20, # x 30, 40, # y 50, 60, # vx 0, 0, # vy 0, 0, # heading 70, 80, # spin 90, 100, # fuel 0, 0, # throttle 1, -1, # only First is landed on Second 0, 1, # Second is broken common.SRB_EMPTY, 1 # time_acc ]), np.zeros(EngineeringState.N_ENGINEERING_FIELDS))) ps = PhysicsState(y0, self.proto_state) self.assertTrue(np.array_equal(ps.y0(), y0.astype(ps.y0().dtype))) self.assertEqual(ps['First'].landed_on, 'Second') proto_state = ps.as_proto() proto_state.timestamp = 50 self.assertEqual(proto_state.timestamp, 50) self.assertEqual(proto_state.entities[0].fuel, 90) self.assertTrue(proto_state.entities[1].broken)
def _run_simulation(self, t: float, y: PhysicsState) -> None: # An overview of how time is managed: # # self._last_simtime is the main thread's latest idea of # what the current time is in the simulation. Every call to # get_state(), self._timetime_of_last_request is incremented by the # amount of time that passed since the last call to get_state(), # factoring in time_acc # # self._solutions is a fixed-size queue of ODE solutions. # Each element has an attribute, t_max, which describes the largest # time that the solution can be evaluated at and still be accurate. # The highest such t_max should always be larger than the current # simulation time, i.e. self._last_simtime proto_state = y._proto_state while not self._stopping_simthread: derive_func = functools.partial( self._derive, pass_through_state=proto_state) events: List[Event] = [ CollisionEvent(y, self.R), HabFuelEvent(y), LiftoffEvent(y), SrbFuelEvent(), HabReactorTempEvent(), AyseReactorTempEvent() ] if y.craft is not None: events.append(HighAccEvent( derive_func, self._artificials, TIME_ACC_TO_BOUND[round(y.time_acc)], y.time_acc, len(y))) ivp_out = scipy.integrate.solve_ivp( fun=derive_func, t_span=[t, t + min(y.time_acc, 10 * self.MAX_STEP_SIZE)], # solve_ivp requires a 1D y0 array y0=y.y0(), events=events, dense_output=True, max_step=self.MAX_STEP_SIZE ) if not ivp_out.success: # Integration error raise Exception(ivp_out.message) # When we create a new solution, let other people know. with self._solutions_cond: # If adding another solution to our max-sized deque would drop # our oldest solution, and the main thread is still asking for # state in the t interval of our oldest solution, take a break # until the main thread has caught up. self._solutions_cond.wait_for( lambda: len(self._solutions) < SOLUTION_CACHE_SIZE or self._last_simtime > self._solutions[0].t_max or self._stopping_simthread ) if self._stopping_simthread: break # self._solutions contains ODE solutions for the interval # [self._solutions[0].t_min, self._solutions[-1].t_max]. self._solutions.append(ivp_out.sol) self._solutions_cond.notify_all() y = PhysicsState(ivp_out.y[:, -1], proto_state) t = ivp_out.t[-1] if ivp_out.status > 0: log.info(f'Got event: {ivp_out.t_events} at t={t}.') for index, event_t in enumerate(ivp_out.t_events): if len(event_t) == 0: # If this event didn't occur, then event_t == [] continue event = events[index] if isinstance(event, CollisionEvent): # Collision, simulation ended. Handled it and continue. assert len(ivp_out.t_events[0]) == 1 assert len(ivp_out.t) >= 2 y = _collision_decision(t, y, events[0]) y = _reconcile_entity_dynamics(y) if isinstance(event, HabFuelEvent): # Something ran out of fuel. for artificial_index in self._artificials: artificial = y[artificial_index] if round(artificial.fuel) != 0: continue log.info(f'{artificial.name} ran out of fuel.') # This craft is out of fuel, the next iteration # won't consume any fuel. Set throttle to zero. artificial.throttle = 0 # Set fuel to a negative value, so it doesn't # trigger the event function. artificial.fuel = 0 if isinstance(event, LiftoffEvent): # A craft has a TWR > 1 craft = y.craft_entity() log.info( 'We have liftoff of the ' f'{craft.name} from {craft.landed_on} at {t}.') craft.landed_on = '' if isinstance(event, SrbFuelEvent): # SRB fuel exhaustion. log.info('SRB exhausted.') y.srb_time = common.SRB_EMPTY if isinstance(event, HighAccEvent): # The acceleration acting on the craft is high, might # result in inaccurate results. SLOOWWWW DOWWWWNNNN. slower_time_acc_index = list( TIME_ACC_TO_BOUND.keys() ).index(round(y.time_acc)) - 1 assert slower_time_acc_index >= 0 slower_time_acc = \ common.TIME_ACCS[slower_time_acc_index] assert slower_time_acc.value > 0 log.info( f'{y.time_acc} is too fast, ' f'slowing down to {slower_time_acc.value}') # We should lower the time acc. y.time_acc = slower_time_acc.value raise PhysicsEngine.RestartSimulationException(t, y) if isinstance(event, HabReactorTempEvent): y.engineering.hab_reactor_alarm = not y.engineering.hab_reactor_alarm if isinstance(event, AyseReactorTempEvent): y.engineering.ayse_reactor_alarm = not y.engineering.ayse_reactor_alarm