def _save_hook(self, textbox: vpython.winput): try: common.write_savefile(self._state, common.savefile(textbox.text)) textbox.text = 'File saved!' except OSError: log.exception('Caught exception during file saving') textbox.text = 'Error writing file!'
def test_hyperbolic_orbital_parameters(self): # Unlike the elliptical test, this tests our favourite extra-solar # visitor to make sure we can calculate Keplerian orbital # characteristics from its orbital state vectors! That's right, we're # talking about Sedna! The expected values are arrived at through # calculation, and also # http://orbitsimulator.com/formulas/OrbitalElements.html physics_state = common.load_savefile( common.savefile('tests/sedna.json')) sun = physics_state[0] oumuamua = physics_state[1] expected_semimajor_axis = -71231070.14146987 self.assertAlmostEqual(calc.semimajor_axis(oumuamua, sun), expected_semimajor_axis, delta=abs(0.01 * expected_semimajor_axis)) expected_eccentricity = 1644.477 self.assertAlmostEqual(calc.fastnorm(calc.eccentricity(oumuamua, sun)), expected_eccentricity, delta=0.01 * expected_eccentricity) expected_periapsis = 1.1714e11 # Through calculation self.assertAlmostEqual(calc.periapsis(sun, oumuamua) + oumuamua.r, expected_periapsis, delta=0.01 * 78989185420.15271)
def main(args: argparse.Namespace): loadfile: Path if os.path.isabs(args.loadfile): loadfile = Path(args.loadfile) else: # Take paths relative to 'data/saves/' loadfile = common.savefile(args.loadfile) physics_engine = physics.PhysicsEngine(common.load_savefile(loadfile)) initial_state = physics_engine.get_state() gui = flight_gui.FlightGui(initial_state, title=name, running_as_mirror=False) atexit.register(gui.shutdown) if args.flamegraph: common.start_flamegraphing() if args.profile: common.start_profiling() while True: state = physics_engine.get_state() # If we have any commands, process them so the simthread has as # much time as possible to restart before next update. physics_engine.handle_requests(gui.pop_commands()) gui.draw(state) gui.rate(common.FRAMERATE)
def test_elliptical_orbital_parameters(self): # Again, see # https://www.wolframalpha.com/input/?i=International+Space+Station # For these expected values physics_state = common.load_savefile( common.savefile('tests/gui-test.json')) iss = physics_state[0] earth = physics_state[1] # The semiaxes are relatively close to expected. self.assertAlmostEqual(calc.semimajor_axis(iss, earth), 6785e3, delta=0.01 * earth.r) # The eccentricity is within 1e-6 of the expected. self.assertAlmostEqual(calc.fastnorm(calc.eccentricity(iss, earth)), 5.893e-4, delta=1e-3) # The apoapsis is relatively close to expected. self.assertAlmostEqual(calc.apoapsis(iss, earth), 418.3e3, delta=0.01 * earth.r) # The periapsis is relatively close to expected. self.assertAlmostEqual(calc.periapsis(iss, earth), 410.3e3, delta=0.01 * earth.r)
def test_speeds(self): physics_state = common.load_savefile( common.savefile('tests/gui-test.json')) iss = physics_state[0] earth = physics_state[1] self.assertAlmostEqual(calc.h_speed(iss, earth), 7665, delta=10) self.assertAlmostEqual(calc.v_speed(iss, earth), -0.1, delta=0.1)
def _save_hook(self, textbox: vpython.winput): full_path = common.savefile(textbox.text) try: full_path = common.write_savefile(self._last_state, full_path) textbox.text = f'Saved to {full_path}!' except OSError: log.exception('Caught exception during file saving') textbox.text = f'Error writing file to {full_path}'
def _load_hook(self, textbox: vpython.winput): full_path = common.savefile(textbox.text) if full_path.is_file(): self._commands.append( network.Request(ident=network.Request.LOAD_SAVEFILE, loadfile=textbox.text)) textbox.text = f'Loaded {full_path}!' else: log.warning(f'Ignored non-existent loadfile: {full_path}') textbox.text = f'{full_path} not found!'
def test_drag(self): """Test that drag is small but noticeable during unpowered flight.""" atmosphere_save = common.load_savefile( common.savefile('tests/atmosphere.json')) # The habitat starts 1 km in the air, the same speed as the Earth. hab = atmosphere_save.craft_entity() hab.vy += 10 atmosphere_save[atmosphere_save.craft] = hab drag = calc.fastnorm(calc.drag(atmosphere_save)) self.assertLess(59, drag) self.assertGreater(60, drag)
def main(args: argparse.Namespace): # Before you make changes to this function, keep in mind that this function # starts a GRPC server that runs in a separate thread! state_server = network.StateServer() loadfile: Path if os.path.isabs(args.loadfile): loadfile = Path(args.loadfile) else: # Take paths relative to 'data/saves/' loadfile = common.savefile(args.loadfile) physics_engine = physics.PhysicsEngine(common.load_savefile(loadfile)) initial_state = physics_engine.get_state() TICKS_BETWEEN_CLIENT_LIST_REFRESHES = 150 ticks_until_next_client_list_refresh = 0 server = grpc.server( concurrent.futures.ThreadPoolExecutor(max_workers=4)) atexit.register(lambda: server.stop(grace=2)) grpc_stubs.add_StateServerServicer_to_server(state_server, server) server.add_insecure_port(f'[::]:{network.DEFAULT_PORT}') state_server.notify_state_change(initial_state.as_proto()) server.start() # This doesn't block! gui = ServerGui() try: if args.flamegraph: common.start_flamegraphing() if args.profile: common.start_profiling() while True: # If we have any commands, process them immediately so input lag # is minimized. commands = state_server.pop_commands() + gui.pop_commands() physics_engine.handle_requests(commands) state = physics_engine.get_state() state_server.notify_state_change(state.as_proto()) if ticks_until_next_client_list_refresh == 0: ticks_until_next_client_list_refresh = \ TICKS_BETWEEN_CLIENT_LIST_REFRESHES state_server.refresh_client_list() ticks_until_next_client_list_refresh -= 1 gui.update(state, state_server.addr_to_connected_clients.values()) finally: server.stop(grace=1)
def _load_hook(self, textbox: vpython.winput): full_path = common.savefile(textbox.text) if full_path.is_file(): self._commands.append( Request(ident=Request.LOAD_SAVEFILE, loadfile=textbox.text)) textbox.text = 'File loaded!' # The file we loaded will have a non-zero time acc, unpause. self.pause(False) # Re-centre on the habitat. self._sidebar.centre_menu._menu.selected = HABITAT self._recentre_dropdown_hook(self._sidebar.centre_menu._menu) else: log.warning(f'Ignored non-existent loadfile: {full_path}') textbox.text = 'File not found!'
def test_longterm_stable_landing(self): """Test that landed ships have stable altitude in the long term.""" savestate = common.load_savefile(common.savefile('OCESS.json')) initial_t = savestate.timestamp with PhysicsEngine('OCESS.json') as physics_engine: initial = physics_engine.get_state(initial_t + 10) physics_engine.handle_requests([ network.Request(ident=network.Request.TIME_ACC_SET, time_acc_set=common.TIME_ACCS[-1].value) ], requested_t=initial_t + 10) final = physics_engine.get_state(initial_t + 100_000) self.assertAlmostEqual(calc.fastnorm(initial['Earth'].pos - initial['Habitat'].pos), initial['Earth'].r + initial['Habitat'].r, delta=1) self.assertAlmostEqual(calc.fastnorm(final['Earth'].pos - final['Habitat'].pos), final['Earth'].r + final['Habitat'].r, delta=1)
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 __init__(self, savefile): self.physics_engine = physics.PhysicsEngine( common.load_savefile(common.savefile(savefile)))
def parse_args(): """Parse CLI arguments. Jupyter might add extra arguments, ignore them.""" parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=( 'This program can be started in either "lead server" mode\n' 'or "mirror" mode.\n' '\n' 'In "lead server" mode, this program simulates a solar system\n' 'using the flight data from the JSON savefile specified as a\n' 'command-line argument to this program. This program will also\n' 'serve physics data on the port specified by --serve-on-port.\n' 'To activate "lead server" mode, pass "file:path/to/file" or\n' '"file:/absolute/path/to/file" as the data_location argument.\n' '\n' 'In "mirror" mode, this program requests physics updates from\n' 'the lead server specified by the hostname and optional port\n' 'argument.\n' 'To activate "mirror" mode, pass "mirror://..." as the\n' 'data_location argument.')) parser.add_argument( 'data_location', type=str, nargs='?', default=('file:OCESS.json'), help=('Where flight data is located. Accepts arguments' ' of the form ' '"mirror://hostname" or "mirror://hostname:port" ' 'or "file:path/to/save.json" or ' '"file:/absolute/path/to/save.json". ' 'See help text for details. Defaults to ' 'file:OCESS.json. Paths are relative to data/saves')) parser.add_argument( '--serve-on-port', type=int, metavar='PORT', help=('For a lead server, specifies which port to serve ' 'physics data on. Specifying this for a mirror is ' ' an error.')) parser.add_argument('--no-gui', action='store_true', default=False, help='Don\'t launch the flight GUI.') parser.add_argument('--no-intro', action='store_true', default=False, help='Skip intro animation for quick start.') parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Logs everything to both logfile and output.') parser.add_argument('--profile', action='store_true', default=False, help='Generating profiling reports, for a flamegraph.') args, unknown = parser.parse_known_args() if unknown: log.warning(f'Got unrecognized args: {unknown}') args.data_location = urllib.parse.urlparse(args.data_location) # Check that the data_location is well-formed assert args.data_location.scheme == 'file' or \ args.data_location.scheme == 'mirror' if args.data_location.scheme == 'file': # We're in lead server mode assert not args.data_location.netloc assert args.data_location.path assert not args.data_location.query assert not args.data_location.fragment if not os.path.isabs(args.data_location.path): # Take relative paths relative to the data/saves/ args.data_location = args.data_location._replace( path=common.savefile(args.data_location.path)) if args.serve_on_port is None: # We can't have a default value of this port, because we want to # check for its existence when we're in mirroring client mode args.serve_on_port = common.DEFAULT_LEAD_SERVER_PORT else: # We're in mirroring client mode assert args.serve_on_port is None # Meaningless in this mode assert args.data_location.netloc assert not args.data_location.path assert not args.data_location.query assert not args.data_location.fragment if not args.data_location.port: # Port is optional. If it does not exist use the default port. args.data_location = args.data_location._replace( netloc=(args.data_location.hostname + ':' + str(common.DEFAULT_LEAD_SERVER_PORT))) if args.verbose: common.enable_verbose_logging() return args