def _one_request(request: Request, y0: PhysicsState) \ -> PhysicsState: """Interface to set habitat controls. Use an argument to change habitat throttle or spinning, and simulation will restart with this new information.""" log.info(f'At simtime={y0.timestamp}, ' f'Got command {MessageToString(request, as_one_line=True)}') if request.ident != Request.TIME_ACC_SET: # Reveal the type of y0.craft as str (not None). assert y0.craft is not None if request.ident == Request.HAB_SPIN_CHANGE: if y0.navmode != Navmode['Manual']: # We're in autopilot, ignore this command return y0 craft = y0.craft_entity() if not craft.landed(): craft.spin += request.spin_change elif request.ident == Request.HAB_THROTTLE_CHANGE: y0.craft_entity().throttle += request.throttle_change elif request.ident == Request.HAB_THROTTLE_SET: y0.craft_entity().throttle = request.throttle_set elif request.ident == Request.TIME_ACC_SET: assert request.time_acc_set >= 0 y0.time_acc = request.time_acc_set elif request.ident == Request.ENGINEERING_UPDATE: # Multiply this value by 100, because OrbitV considers engines at # 100% to be 100x the maximum thrust. common.craft_capabilities[HABITAT] = \ common.craft_capabilities[HABITAT]._replace( thrust=100 * request.engineering_update.max_thrust) hab = y0[HABITAT] ayse = y0[AYSE] hab.fuel = request.engineering_update.hab_fuel ayse.fuel = request.engineering_update.ayse_fuel y0[HABITAT] = hab y0[AYSE] = ayse if request.engineering_update.module_state == \ Request.DETACHED_MODULE and \ MODULE not in y0._entity_names and \ not hab.landed(): # If the Habitat is freely floating and engineering asks us to # detach the Module, spawn in the Module. module = Entity(protos.Entity( name=MODULE, mass=100, r=10, artificial=True)) module.pos = (hab.pos - (module.r + hab.r) * calc.heading_vector(hab.heading)) module.v = calc.rotational_speed(module, hab) y0_proto = y0.as_proto() y0_proto.entities.extend([module.proto]) y0 = PhysicsState(None, y0_proto) elif request.ident == Request.UNDOCK: habitat = y0[HABITAT] if habitat.landed_on == AYSE: ayse = y0[AYSE] habitat.landed_on = '' norm = habitat.pos - ayse.pos unit_norm = norm / calc.fastnorm(norm) habitat.v += unit_norm * common.UNDOCK_PUSH habitat.spin = ayse.spin y0[HABITAT] = habitat elif request.ident == Request.REFERENCE_UPDATE: y0.reference = request.reference elif request.ident == Request.TARGET_UPDATE: y0.target = request.target elif request.ident == Request.LOAD_SAVEFILE: y0 = common.load_savefile(common.savefile(request.loadfile)) elif request.ident == Request.NAVMODE_SET: y0.navmode = Navmode(request.navmode) if y0.navmode == Navmode['Manual']: y0.craft_entity().spin = 0 elif request.ident == Request.PARACHUTE: y0.parachute_deployed = request.deploy_parachute elif request.ident == Request.IGNITE_SRBS: if round(y0.srb_time) == common.SRB_FULL: y0.srb_time = common.SRB_BURNTIME elif request.ident == Request.TOGGLE_COMPONENT: component = y0.engineering.components[request.component_to_toggle] component.connected = not component.connected elif request.ident == Request.TOGGLE_RADIATOR: radiator = y0.engineering.radiators[request.radiator_to_toggle] radiator.functioning = not radiator.functioning elif request.ident == Request.CONNECT_RADIATOR_TO_LOOP: radiator = y0.engineering.radiators[request.radiator_to_loop.rad] radiator.attached_to_coolant_loop = request.radiator_to_loop.loop elif request.ident == Request.TOGGLE_COMPONENT_COOLANT: component = y0.engineering.components[request.component_to_loop.component] # See comments on _component_coolant_cnxn_transitions for more info. component.coolant_connection = ( _component_coolant_cnxn_transitions [request.component_to_loop.loop] [component.coolant_connection] ) return y0
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