def _docking(e1, e2, e2_index): # e1 is an artificial object # if 2 artificial object to be docked on (spacespation) norm = e1.pos - e2.pos collision_angle = np.arctan2(norm[1], norm[0]) collision_angle = collision_angle % (2 * np.pi) ANGLE_MIN = (e2.heading + 0.7 * np.pi) % (2 * np.pi) ANGLE_MAX = (e2.heading + 1.3 * np.pi) % (2 * np.pi) if collision_angle < ANGLE_MIN or collision_angle > ANGLE_MAX: # add damage ? _bounce(e1, e2) return log.info(f'Docking {e1.name} on {e2.name}') e1.landed_on = e2.name # Currently this flag has almost no effect. e1.broken = bool( calc.fastnorm(calc.rotational_speed(e1, e2) - e1.v) > common.craft_capabilities[e1.name].hull_strength ) # set right heading for future takeoff e2_opposite = e2.heading + np.pi e1.pos = e2.pos + (e1.r + e2.r) * calc.heading_vector(e2_opposite) e1.heading = e2_opposite % (2 * np.pi) e1.throttle = 0 e1.spin = e2.spin e1.v = e2.v
def _reconcile_entity_dynamics(y: PhysicsState) -> PhysicsState: """Idempotent helper that sets velocities and spins of some entities. This is in its own function because it has a couple calling points.""" # Navmode auto-rotation if y.navmode != Navmode['Manual']: craft = y.craft_entity() craft.spin = calc.navmode_spin(y) # Keep landed entities glued together landed_on = y.LandedOn for index in landed_on: # If we're landed on something, make sure we move in lockstep. lander = y[index] ground = y[landed_on[index]] if ground.name == AYSE and lander.name == HABITAT: # Always put the Habitat at the docking port. lander.pos = ( ground.pos - calc.heading_vector(ground.heading) * (lander.r + ground.r)) else: norm = lander.pos - ground.pos unit_norm = norm / calc.fastnorm(norm) lander.pos = ground.pos + unit_norm * (ground.r + lander.r) lander.spin = ground.spin lander.v = calc.rotational_speed(lander, ground) return y
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 _derive(self, t: float, y_1d: np.ndarray, pass_through_state: PhysicalState) -> np.ndarray: """ y_1d = [X, Y, VX, VY, Heading, Spin, Fuel, Throttle, LandedOn, Broken] + SRB_time_left + time_acc (these are both single values) + [ComponentData, CoolantLoopData, RadiatorData] returns the derivative of y_1d, i.e. [VX, VY, AX, AY, Spin, 0, Fuel consumption, 0, 0, 0] + -constant + 0 (zeroed-out fields are changed elsewhere) !!!!!!!!!!! IMPORTANT !!!!!!!!!!! This function should return a DERIVATIVE. The numpy.solve_ivp function will do the rest of the work of the simulation, this function just describes how things _move_. At its most basic level, this function takes in the _position_ of everything (plus some random stuff), and returns the _velocity_ of everything (plus some random stuff). Essentially, numpy.solve_ivp does this calculation: new_positions_of_system = t_delta * _derive( current_t_of_system, current_y_of_system) """ # Note: we create this y as a PhysicsState for convenience, but if you # set any values of y, the changes will be discarded! The only way they # will be propagated out of this function is by numpy using the return # value of this function as a derivative, as explained above. # If you want to set values in y, look at _reconcile_entity_dynamics. y = PhysicsState(y_1d, pass_through_state) acc_matrix = calc.grav_acc(y.X, y.Y, self.M, y.Fuel) zeros = np.zeros(y._n) fuel_cons = np.zeros(y._n) # Engine thrust and fuel consumption for artif_index in self._artificials: if y[artif_index].fuel > 0 and y[artif_index].throttle > 0: # We have fuel remaining, calculate thrust entity = y[artif_index] capability = common.craft_capabilities[entity.name] fuel_cons[artif_index] = \ -abs(capability.fuel_cons * entity.throttle) eng_thrust = (capability.thrust * entity.throttle * calc.heading_vector(entity.heading)) mass = entity.mass + entity.fuel if entity.name == AYSE and \ y[HABITAT].landed_on == AYSE: # It's bad that this is hardcoded, but it's also the only # place that this comes up so IMO it's not too bad. hab = y[HABITAT] mass += hab.mass + hab.fuel eng_acc = eng_thrust / mass acc_matrix[artif_index] += eng_acc # And SRB thrust srb_usage = 0 try: if y.srb_time >= 0: hab_index = y._name_to_index(HABITAT) hab = y[hab_index] srb_acc = common.SRB_THRUST / (hab.mass + hab.fuel) srb_acc_vector = srb_acc * calc.heading_vector(hab.heading) acc_matrix[hab_index] += srb_acc_vector srb_usage = -1 except PhysicsState.NoEntityError: # The Habitat doesn't exist. pass # Engineering values R = y.engineering.components.Resistance() I = y.engineering.components.Current() # noqa: E741 # Eventually radiators will affect the temperature. T_deriv = common.eta * np.square(I) * R R_deriv = common.alpha * T_deriv # Voltage we set to be constant. Since I = V/R, I' = V'/R' = 0/R' = 0 V_deriv = I_deriv = np.zeros(data_structures._N_COMPONENTS) # Drag effects craft = y.craft if craft is not None: craft_index = y._name_to_index(y.craft) drag_acc = calc.drag(y) acc_matrix[craft_index] -= drag_acc # Centripetal acceleration to keep landed entities glued to each other. landed_on = y.LandedOn for landed_i in landed_on: lander = y[landed_i] ground = y[landed_on[landed_i]] centripetal_acc = (lander.pos - ground.pos) * ground.spin ** 2 acc_matrix[landed_i] = \ acc_matrix[landed_on[landed_i]] - centripetal_acc # Sets velocity and spin of a couple more entities. # If you want to set the acceleration of an entity, do it above and # keep that logic in _derive. If you want to set the velocity and spin # or any other fields that an Entity has, you should put that logic in # this _reconcile_entity_dynamics helper. y = _reconcile_entity_dynamics(y) return np.concatenate(( y.VX, y.VY, np.hsplit(acc_matrix, 2), y.Spin, zeros, fuel_cons, zeros, zeros, zeros, np.array([srb_usage, 0]), np.zeros(data_structures._N_COMPONENTS), # connected field doesn't change T_deriv, R_deriv, V_deriv, I_deriv, np.zeros(data_structures._N_COMPONENTS), # attached_to_coolant_loop field doesn't change np.zeros(data_structures._N_COOLANT_LOOPS * data_structures._N_COOLANT_FIELDS), np.zeros(data_structures._N_RADIATORS * data_structures._N_RADIATOR_FIELDS) ), axis=None)
def _write_state_to_osbackup(orbitx_state: PhysicsState, osbackup_path: Path, orbitv_names: List[str]): """Writes information to OSbackup.RND. orbitv_names should be the result of entity_list_from_starsr. Currently only writes information relevant to engineering, but if we want to communicate piloting state to other legacy orbit programs we can write other kinds of information. I found out what information I should be writing here by reading the source for engineering (enghabv.bas) and seeing what information it reads from OSbackup.RND""" assert orbitx_state.craft hab = orbitx_state[common.HABITAT] ayse = orbitx_state[common.AYSE] craft = orbitx_state.craft_entity() max_thrust = common.craft_capabilities[craft.name].thrust timestamp = datetime.fromtimestamp(orbitx_state.timestamp) check_byte = random.choice(string.ascii_letters).encode('ascii') target = (orbitx_state.target if orbitx_state.target in orbitv_names else "AYSE") reference = (orbitx_state.reference if orbitx_state.reference in orbitv_names else "Earth") atmosphere = calc.relevant_atmosphere(orbitx_state) if atmosphere is None: pressure = 0.0 else: pressure = calc.pressure(craft, atmosphere) craft_thrust = (max_thrust * craft.throttle * calc.heading_vector(craft.heading)) craft_drag = calc.drag(orbitx_state) artificial_gravity = numpy.linalg.norm(craft_thrust - craft_drag) with open(osbackup_path, 'r+b') as osbackup: # We replicate the logic of orbit5v.bas:1182 (look for label 800) # in the below code. OSBACKUP.RND is a binary, fixed-width field # file. orbit5v.bas writes to it every second, and it's created in # the same way as any OrbitV save file. # If you're reading the orbit5v.bas source code, note that arrays # and strings in QB are 1-indexed not 0-indexed. osbackup.write(check_byte) osbackup.write("ORBIT5S ".encode('ascii')) # OrbitX keeps throttle as a float, where 100% = 1.0 # OrbitV expects 100% = 100.0 _write_single(100 * max(hab.throttle, ayse.throttle), osbackup) # This is Vflag and Aflag, dunno what these are. _write_int(0, osbackup) _write_int(0, osbackup) _write_int(NAVMODE_TO_SFLAG[orbitx_state.navmode], osbackup) _write_double(numpy.linalg.norm(calc.drag(orbitx_state)), osbackup) _write_double(25, osbackup) # A sane value for OrbitV zoom. # TODO: check if this is off by 90 degrees or something. _write_single(hab.heading, osbackup) _write_int(orbitv_names.index("Habitat"), osbackup) # Centre _write_int(orbitv_names.index(target), osbackup) _write_int(orbitv_names.index(reference), osbackup) _write_int(0, osbackup) # Set trails to off. _write_single(0.006 if orbitx_state.parachute_deployed else 0.002, osbackup) _write_single( (common.SRB_THRUST / 100) if orbitx_state.srb_time > 0 else 0, osbackup) _write_int(0, osbackup) # No colour-coded trails. # Valid values of this are 0, 1, 2. # A value of 1 gets OrbitV to display EST timestamps. _write_int(1, osbackup) _write_double(0.125, osbackup) # Set timestep to the slowest. _write_double(0.125, osbackup) # This is OLDts, also slowest value _write_single(0, osbackup) # Something to do with RSCP, zero it. _write_int(0, osbackup) # Eflag? Something that gassimv.bas sets. _write_int(timestamp.year, osbackup) # QB uses day-of-the-year instead of day-of-the-month. _write_int(int(timestamp.strftime('%j')), osbackup) _write_int(timestamp.hour, osbackup) _write_int(timestamp.minute, osbackup) _write_double(timestamp.second, osbackup) _write_single(ayse.heading, osbackup) _write_int(0, osbackup) # AYSEscrape seemingly does nothing. # TODO: Once we implement wind, fill in these fields. _write_single(0, osbackup) _write_single(0, osbackup) _write_single(numpy.degrees(hab.spin), osbackup) _write_int(150 if hab.landed_on == common.AYSE else 0, osbackup) _write_single(0, osbackup) # Rad shields, written by enghabv.bas. _write_int( MODSTATE_TO_MODFLAG[network.Request.DETACHED_MODULE] if common.MODULE in orbitx_state._entity_names else MODSTATE_TO_MODFLAG[network.Request.NO_MODULE], osbackup) # These next two variables are technically AYSEdist and OCESSdist # but they're used for engineering to determine fueld loading from # OCESS or AYSE. Also, if you can dock with AYSE. So we just tell # engineering if we're landed, but don't tell it any more deets. _write_single(150 if hab.landed_on == common.AYSE else 1000, osbackup) _write_single(150 if hab.landed_on == common.EARTH else 1000, osbackup) _write_int(hab.broken, osbackup) # I think an explosion flag. _write_int(ayse.broken, osbackup) # Same as above. # TODO: Once we implement nav malfunctions, set this field. _write_single(0, osbackup) _write_single(max_thrust, osbackup) # TODO: Once we implement nav malfunctions, set this field. # Also, apparently this is the same field as two fields ago? _write_single(0, osbackup) # TODO: Once we implement wind, fill in these fields. _write_long(0, osbackup) # TODO: Some information required from MST.rnd needed to implement. # this field (LONGtarg). _write_single(0, osbackup) _write_single(pressure, osbackup) _write_single(artificial_gravity, osbackup) for i in range(0, 39): try: entity = orbitx_state[orbitv_names[i]] x, y = entity.pos vx, vy = entity.v except ValueError: x = y = vx = vy = 0 _write_double(x, osbackup) _write_double(y, osbackup) _write_double(vx, osbackup) _write_double(vy, osbackup) _write_single(hab.fuel, osbackup) _write_single(ayse.fuel, osbackup) osbackup.write(check_byte)