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 _bounce(e1, e2): # Resolve a collision by: # 1. calculating positions and velocities of the two entities # 2. do a 1D collision calculation along the normal between the two # 3. recombine the velocity vectors log.info(f'Bouncing {e1.name} and {e2.name}') norm = e1.pos - e2.pos unit_norm = norm / calc.fastnorm(norm) # The unit tangent is perpendicular to the unit normal vector unit_tang = np.asarray([-unit_norm[1], unit_norm[0]]) # Calculate both normal and tangent velocities for both entities v1n = np.dot(unit_norm, e1.v) v1t = np.dot(unit_tang, e1.v) v2n = np.dot(unit_norm, e2.v) v2t = np.dot(unit_tang, e2.v) # Use https://en.wikipedia.org/wiki/Elastic_collision # to find the new normal velocities (a 1D collision) new_v1n = ((v1n * (e1.mass - e2.mass) + 2 * e2.mass * v2n) / (e1.mass + e2.mass)) new_v2n = ((v2n * (e2.mass - e1.mass) + 2 * e1.mass * v1n) / (e1.mass + e2.mass)) # Calculate new velocities e1.v = new_v1n * unit_norm + v1t * unit_tang e2.v = new_v2n * unit_norm + v2t * unit_tang
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 _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 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_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 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 update(self, state: PhysicsState, origin: Entity): if not self._visible: self._hyperbola.visible = False self._ring.visible = False return orb_params = calc.orbit_parameters(state.reference_entity(), state.craft_entity()) thickness = min( self.PROJECTION_THICKNESS * vpython.canvas.get_selected().range, abs(0.1 * max(orb_params.major_axis, orb_params.minor_axis))) pos = orb_params.centre - origin.pos direction = vpython.vector(*orb_params.eccentricity, 0) e_mag = calc.fastnorm(orb_params.eccentricity) if e_mag < 1: # Project an elliptical orbit self._hyperbola.visible = False self._ring.pos = vpython.vector(*pos, 0) # size.x is actually the thickness, confusingly. # size.y and size.z are the width and length of the ellipse. self._ring.size.y = orb_params.major_axis self._ring.size.z = orb_params.minor_axis self._ring.up = direction self._ring.size.x = thickness self._ring.visible = True elif e_mag > 1: # Project a hyperbolic orbit self._ring.visible = False self._hyperbola.origin = vpython.vector(*pos, 0) # For a vpython.curve object, setting the axis is confusing. # To control direction, set the .up attribute (magnitude doesn't # matter for .up) # To control size, set the .size attribute (magnitude matters). # Setting .axis will also change .size, not sure how. self._hyperbola.size = vpython.vector(orb_params.minor_axis / 2, orb_params.major_axis / 2, 1) self._hyperbola.up = -direction self._hyperbola.radius = thickness self._hyperbola.visible = True assert self._hyperbola.axis.z == 0, self._hyperbola.axis assert self._hyperbola.up.z == 0, self._hyperbola.up else: # e == 1 pretty much never happens, and if it does we'll just wait # until it goes away return
def __call__(self, t: float, y_1d: np.ndarray) -> float: """Return positive if the current time acceleration is accurate, zero then negative otherwise.""" if self.current_acc == 1: # If we can't lower the time acc, don't bother doing any work. return np.inf derive_result = self.derive(t, y_1d) max_acc_mag = 0.0005 # A small nonzero value. for artif_index in self.artificials: accel = (derive_result[self.ax_offset] + artif_index, derive_result[self.ay_offset] + artif_index) acc_mag = calc.fastnorm(accel) if acc_mag > max_acc_mag: max_acc_mag = acc_mag return max(self.acc_bound - acc_mag, 0)
def _land(e1, e2): # e1 is an artificial object # if 2 artificial object collide (habitat, spacespation) # or small astroid collision (need deletion), handle later log.info(f'Landing {e1.name} on {e2.name}') assert e2.artificial is False e1.landed_on = e2.name # Currently does nothing 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 norm = e1.pos - e2.pos e1.heading = np.arctan2(norm[1], norm[0]) e1.throttle = 0 e1.spin = e2.spin e1.v = calc.rotational_speed(e1, e2)
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 _create_wtexts(self): vpython.canvas.get_selected().caption += "<table>\n" self._wtexts: List[TableText] = [] self._wtexts.append( TableText("Simulation time", lambda state: datetime.fromtimestamp( state.timestamp, common.TIMEZONE).strftime('%x %X'), "Current time in simulation", new_section=False)) self._wtexts.append( TableText("Orbit speed", lambda state: common.format_num( calc.orb_speed(state.craft_entity(), state.reference_entity()), " m/s"), "Speed required for circular orbit at current altitude", new_section=False)) self._wtexts.append( TableText( "Periapsis", lambda state: common.format_num(calc.periapsis( state.craft_entity(), state.reference_entity()) / 1000, " km", decimals=3), "Lowest altitude in naïve orbit around reference", new_section=False)) self._wtexts.append( TableText( "Apoapsis", lambda state: common.format_num(calc.apoapsis( state.craft_entity(), state.reference_entity()) / 1000, " km", decimals=3), "Highest altitude in naïve orbit around reference", new_section=False)) self._wtexts.append( TableText( # The H in HRT stands for Habitat, even though craft is more # general and covers Ayse, but HRT is the familiar triple name and # the Hawking III says trans rights. "HRT phase θ", lambda state: common.format_num( calc.phase_angle( state.craft_entity(), state.reference_entity(), state.target_entity()), "°") or "Same ref and targ", "Angle between Habitat, Reference, and Target", new_section=False)) self._wtexts.append( TableText( "Throttle", lambda state: "{:.1%}".format(state.craft_entity().throttle), "Percentage of habitat's maximum rated engines", new_section=True)) self._wtexts.append( TableText("Engine Acceleration", lambda state: common.format_num( calc.engine_acceleration(state), " m/s/s") + (' [SRB]' if state.srb_time > 0 else ''), "Acceleration due to craft's engine thrust", new_section=False)) self._wtexts.append( TableText("Drag", lambda state: common.format_num( calc.fastnorm(calc.drag(state)), " m/s/s"), "Atmospheric drag acting on the craft", new_section=False)) self._wtexts.append( TableText("Fuel", lambda state: common.format_num( state.craft_entity().fuel, " kg"), "Remaining fuel of craft", new_section=False)) def rotation_formatter(state: PhysicsState) -> str: deg_spin = round(np.degrees(state.craft_entity().spin), ndigits=1) if deg_spin < 0: return f"{-deg_spin} °/s cw" elif deg_spin > 0: return f"{deg_spin} °/s ccw" else: return f"{deg_spin} °/s" self._wtexts.append( TableText("Rotation", rotation_formatter, "Rotation speed of craft", new_section=False)) self._wtexts.append( TableText( "Ref altitude", lambda state: common.format_num(calc.altitude( state.craft_entity(), state.reference_entity()) / 1000, " km", decimals=3), "Altitude of habitat above reference surface", new_section=True)) self._wtexts.append( TableText("Ref speed", lambda state: common.format_num( calc.speed(state.craft_entity(), state.reference_entity()), " m/s"), "Speed of habitat above reference surface", new_section=False)) self._wtexts.append( TableText( "Vertical speed", lambda state: common.format_num( calc.v_speed(state.craft_entity(), state.reference_entity( )), " m/s "), "Vertical speed of habitat towards/away reference surface", new_section=False)) self._wtexts.append( TableText("Horizontal speed", lambda state: common.format_num( calc.h_speed(state.craft_entity(), state.reference_entity()), " m/s "), "Horizontal speed of habitat across reference surface", new_section=False)) self._wtexts.append( TableText("Pitch θ", lambda state: common.format_num( np.degrees( calc.pitch(state.craft_entity( ), state.reference_entity())) % 360, "°"), "Horizontal speed of habitat across reference surface", new_section=False)) self._wtexts.append( TableText("Targ altitude", lambda state: common.format_num(calc.altitude( state.craft_entity(), state.target_entity()) / 1000, " km", decimals=3), "Altitude of habitat above reference surface", new_section=True)) self._wtexts.append( TableText("Targ speed", lambda state: common.format_num( calc.speed(state.craft_entity(), state.target_entity( )), " m/s"), "Speed of habitat above target surface", new_section=False)) self._wtexts.append( TableText( "Landing acc", lambda state: common.format_num( calc.landing_acceleration(state.craft_entity(), state.target_entity()), " m/s/s" ) or "no vertical landing", "Constant engine acc to land during vertical descent to target", new_section=False)) vpython.canvas.get_selected().caption += "</table>"