Exemple #1
0
    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)
Exemple #2
0
    def __call__(self, t, y_1d) -> float:
        """Return 0 when the craft is landed but thrusting enough to lift off,
        and a positive value otherwise."""
        y = PhysicsState(y_1d, self.initial_state._proto_state)
        if y.craft is None:
            # There is no craft, return early.
            return np.inf
        craft = y.craft_entity()

        if not craft.landed():
            # We don't have to lift off because we already lifted off.
            return np.inf

        planet = y[craft.landed_on]
        if planet.artificial:
            # If we're docked with another satellite, undocking is governed
            # by other mechanisms. Ignore this.
            return np.inf

        thrust = common.craft_capabilities[craft.name].thrust * craft.throttle
        if y.srb_time > 0 and y.craft == HABITAT:
            thrust += common.SRB_THRUST

        pos = craft.pos - planet.pos
        # This is the G(m1*m2)/r^2 formula
        weight = common.G * craft.mass * planet.mass / np.inner(pos, pos)

        # This should be positive when the craft isn't thrusting enough, and
        # zero when it is thrusting enough.
        return max(0, common.LAUNCH_TWR - thrust / weight)
Exemple #3
0
    def __init__(self, draw_state: PhysicsState, *, title: str,
                 running_as_mirror: bool) -> None:
        assert len(draw_state) >= 1

        self._state = draw_state
        # create a vpython canvas, onto which all 3D and HTML drawing happens.
        self._scene = self._init_canvas(title)

        self._show_label: bool = True
        self._show_trails: bool = DEFAULT_TRAILS
        self._paused: bool = False
        self._removed_saveload_hint: bool = False
        self._origin: Entity
        self.texture_path: Path = Path('data', 'textures')
        self._commands: List[Request] = []
        self._paused_label = vpython.label(
            text="Simulation paused; saving and loading enabled.\n"
            "When finished, unpause by setting a time acceleration.",
            xoffset=0,
            yoffset=200,
            line=False,
            height=25,
            border=20,
            opacity=1,
            visible=False)

        self._3dobjs: Dict[str, ThreeDeeObj] = {}
        # remove vpython ambient lighting
        self._scene.lights = []  # This line shouldn't be removed

        self._set_origin(common.DEFAULT_CENTRE)

        for entity in draw_state:
            self._3dobjs[entity.name] = self._build_threedeeobj(entity)

        self._orbit_projection = OrbitProjection()
        self._3dobjs[draw_state.reference].draw_landing_graphic(
            draw_state.reference_entity())
        self._3dobjs[draw_state.target].draw_landing_graphic(
            draw_state.target_entity())

        self._sidebar = Sidebar(self, running_as_mirror)
        self._scene.bind('keydown', self._handle_keydown)

        # Add an animation when launching the program to describe the solar
        # system and the current location
        while self._scene.range > 600000:
            vpython.rate(100)
            self._scene.range = self._scene.range * 0.92
        self.recentre_camera(common.DEFAULT_CENTRE)
Exemple #4
0
    def _update_obj(self, entity: Entity, state: PhysicsState,
                    origin: Entity) -> None:
        # update planet objects
        self._obj.pos = entity.screen_pos(origin)
        self._obj.axis = calc.angle_to_vpy(entity.heading)

        # update label objects
        self._label.text = self._label_text(entity)
        self._label.pos = entity.screen_pos(origin)
        # update landing graphic objects
        self._update_landing_graphic(self._small_landing_graphic, entity,
                                     state.craft_entity())
        self._update_landing_graphic(self._large_landing_graphic, entity,
                                     state.craft_entity())
Exemple #5
0
    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
Exemple #6
0
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
Exemple #7
0
def relevant_atmosphere(flight_state: PhysicsState) \
        -> Optional[Entity]:
    """Returns the closest entity that has an atmosphere, or None if there are
    no such entities close enough to effect the craft."""
    craft = flight_state.craft_entity()
    closest_atmosphere: Optional[Entity] = None
    closest_distance = np.inf

    atmosphere_indices = flight_state.Atmospheres
    if not atmosphere_indices:
        # There are no entities with atmospheres
        return None

    for index in atmosphere_indices:
        atmosphere = flight_state[index]
        dist = fastnorm(atmosphere.pos - craft.pos)
        # np.inner is the same as the magnitude squared
        # https://stackoverflow.com/a/35213951
        # We use the squared distance because it's much faster to calculate.
        if dist < closest_distance:
            # We found a closer planet with an atmosphere
            exponential = (-(dist - craft.r - atmosphere.r) / 1000 /
                           atmosphere.atmosphere_scaling)
            if exponential > -20:
                # Entity has an atmosphere and it's close enough to be relevant
                closest_distance = dist
                closest_atmosphere = atmosphere

    return closest_atmosphere
Exemple #8
0
    def handle_requests(self, requests: List[Request], requested_t=None):
        requested_t = self._simtime(requested_t)
        if len(requests) == 0:
            return

        if len(requests) and requests[0].ident == Request.TIME_ACC_SET:
            # Immediately change the time acceleration, don't wait for the
            # simulation to catch up. This deals with the case where we're at
            # 100,000x time acc, and the program seems frozen for the user and
            # they try lowering time acc. We should immediately be able to
            # restart simulation at a lower time acc without any waiting.
            if len(self._solutions) == 0:
                # We haven't even simulated any solutions yet.
                requested_t = self._last_physical_state.timestamp
            else:
                requested_t = min(self._solutions[-1].t_max, requested_t)

        if len(self._solutions) == 0:
            y0 = PhysicsState(None, self._last_physical_state)
        else:
            y0 = self.get_state(requested_t)

        for request in requests:
            if request.ident == Request.NOOP:
                # We don't care about these requests
                continue
            y0 = _one_request(request, y0)
            if request.ident == Request.TIME_ACC_SET:
                assert request.time_acc_set >= 0
                self._time_acc_changes.append(
                    TimeAccChange(time_acc=y0.time_acc,
                                  start_simtime=y0.timestamp)
                )

        self.set_state(y0)
Exemple #9
0
    def test_gravitation(self):
        """Test that gravitational acceleration at t=0 is as expected."""
        with PhysicsEngine('tests/massive-objects.json') as physics_engine:
            # In this case, the first entity is very massive and the second
            # entity should gravitate towards the first entity.
            initial = physics_engine.get_state(0)
            moved = physics_engine.get_state(1)
            # https://www.wolframalpha.com/input/?i=1e30+kg+*+G+%2F+(1e8+m)%5E2
            # According to the above, this should be somewhere between 6500 and
            # 7000 m/s after one second.
            self.assertTrue(moved[1].vx > 5000,
                            msg=f'vx is actually {moved[1].vx}')

            # Test the internal math that the internal derive function is doing
            # the right calculations. Break out your SPH4U physics equations!!
            y0 = initial
            # Note that dy.X is actually the velocity at 0,
            # and dy.VX is acceleration.
            dy = PhysicsState(
                physics_engine._derive(0, y0.y0(), y0._proto_state),
                y0._proto_state)
            self.assertEqual(len(dy.X), 2)
            self.assertAlmostEqual(dy.X[0], y0.VX[0])
            self.assertAlmostEqual(dy.Y[0], y0.VY[0])
            self.assertEqual(
                round(abs(dy.VX[0])),
                round(common.G * initial[1].mass / (y0.X[0] - y0.X[1])**2))
            self.assertAlmostEqual(dy.VY[0], 0)

            self.assertAlmostEqual(dy.X[1], y0.VX[1])
            self.assertAlmostEqual(dy.Y[1], y0.VY[1])
            self.assertEqual(
                round(abs(dy.VX[1])),
                round(common.G * initial[0].mass / (y0.X[1] - y0.X[0])**2))
            self.assertAlmostEqual(dy.VY[1], 0)
Exemple #10
0
 def test_get_set(self):
     """Test __getitem__ and __setitem__."""
     ps = PhysicsState(None, self.proto_state)
     entity = ps[0]
     entity.landed_on = 'Second'
     ps[0] = entity
     self.assertEqual(ps[0].landed_on, 'Second')
Exemple #11
0
 def __call__(self, t, y_1d) -> float:
     """Return a 0 only when throttle is nonzero."""
     y = PhysicsState(y_1d, self.initial_state._proto_state)
     for index, entity in enumerate(y._proto_state.entities):
         if entity.artificial and y.Throttle[index] != 0:
             return y.Fuel[index]
     return np.inf
Exemple #12
0
    def __call__(self, t, y_1d, return_pair=False
                 ) -> Union[float, Tuple[int, int]]:
        """Returns a scalar, with 0 indicating a collision and a sign change
        indicating a collision has happened."""
        y = PhysicsState(y_1d, self.initial_state._proto_state)
        n = len(self.initial_state)
        # 2xN of (x, y) positions
        posns = np.column_stack((y.X, y.Y))
        # An n*n matrix of _altitudes_ between each entity
        alt_matrix = (
                scipy.spatial.distance.cdist(posns, posns) -
                np.array([self.radii]) - np.array([self.radii]).T)
        # To simplify calculations, an entity's altitude from itself is inf
        np.fill_diagonal(alt_matrix, np.inf)
        # For each pair of objects that have collisions disabled between
        # them, also set their altitude to be inf

        # If there are any entities landed on any other entities, ignore
        # both the landed and the landee entity.
        landed_on = y.LandedOn
        for index in landed_on:
            alt_matrix[index, landed_on[index]] = np.inf
            alt_matrix[landed_on[index], index] = np.inf

        if return_pair:
            # Returns the actual pair of indicies instead of a scalar.
            flattened_index = alt_matrix.argmin()
            # flattened_index is a value in the interval [1, n*n]-1.
            # Turn it into a  2D index.
            object_i = flattened_index // n
            object_j = flattened_index % n
            return object_i, object_j
        else:
            # solve_ivp invocation, return scalar
            return np.min(alt_matrix)
Exemple #13
0
 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"
Exemple #14
0
def navmode_heading(flight_state: PhysicsState) -> float:
    """
    Returns the heading that the craft should be facing in current navmode.

    Don't call this with Manual navmode.
    If the navmode is relative to the reference, and the reference is set to
    the craft, this will return the craft's current heading as a sane default.
    """
    navmode = flight_state.navmode
    craft = flight_state.craft_entity()
    ref = flight_state.reference_entity()
    targ = flight_state.target_entity()
    requested_vector: np.ndarray

    if navmode == Navmode['Manual']:
        raise ValueError('Autopilot requested for manual navmode')
    elif navmode.name == 'CCW Prograde':
        if flight_state.reference == flight_state.craft:
            return craft.heading
        normal = craft.pos - ref.pos
        requested_vector = np.array([-normal[1], normal[0]])
    elif navmode == Navmode['CW Retrograde']:
        if flight_state.reference == flight_state.craft:
            return craft.heading
        normal = craft.pos - ref.pos
        requested_vector = np.array([normal[1], -normal[0]])
    elif navmode == Navmode['Depart Reference']:
        if flight_state.reference == flight_state.craft:
            return craft.heading
        requested_vector = craft.pos - ref.pos
    elif navmode == Navmode['Approach Target']:
        if flight_state.target == flight_state.craft:
            return craft.heading
        requested_vector = targ.pos - craft.pos
    elif navmode == Navmode['Pro Targ Velocity']:
        if flight_state.target == flight_state.craft:
            return craft.heading
        requested_vector = craft.v - targ.v
    elif navmode == Navmode['Anti Targ Velocity']:
        if flight_state.target == flight_state.craft:
            return craft.heading
        requested_vector = targ.v - craft.v
    else:
        raise ValueError(f'Got an unexpected NAVMODE: {flight_state.navmode}')

    return np.arctan2(requested_vector[1], requested_vector[0])
Exemple #15
0
    def get_state(self, requested_t=None) -> PhysicsState:
        """Return the latest physical state of the simulation."""
        requested_t = self._simtime(requested_t)

        # Wait until there is a solution for our requested_t. The .wait_for()
        # call will block until a new ODE solution is created.
        with self._solutions_cond:
            self._last_simtime = requested_t
            self._solutions_cond.wait_for(
                # Wait until we're paused, there's a solution, or an exception.
                lambda:
                self._last_physical_state.time_acc == 0 or
                (len(self._solutions) != 0 and
                 self._solutions[-1].t_max >= requested_t) or
                self._simthread_exception is not None
            )

            # Check if the simthread crashed
            if self._simthread_exception is not None:
                raise self._simthread_exception

            if self._last_physical_state.time_acc == 0:
                # We're paused, so there are no solutions being generated.
                # Exit this 'with' block and release our _solutions_cond lock.
                pass
            else:
                # We can't integrate backwards, so if integration has gone
                # beyond what we need, fail early.
                assert requested_t >= self._solutions[0].t_min, \
                    (self._solutions[0].t_min, self._solutions[-1].t_max)

                for soln in self._solutions:
                    if soln.t_min <= requested_t <= soln.t_max:
                        solution = soln

        if self._last_physical_state.time_acc == 0:
            # We're paused, so return the only state we have.
            return PhysicsState(None, self._last_physical_state)
        else:
            # We have a solution, return it.
            newest_state = PhysicsState(
                solution(requested_t), self._last_physical_state
            )
            newest_state.timestamp = requested_t
            return newest_state
Exemple #16
0
def engine_acceleration(state: PhysicsState) -> float:
    """Acceleration due to engine thrust."""
    craft = state.craft_entity()
    if craft.name == common.HABITAT and state.srb_time > 0:
        srb_thrust = common.SRB_THRUST
    else:
        srb_thrust = 0

    return (common.craft_capabilities[craft.name].thrust * craft.throttle +
            srb_thrust) / (craft.mass + craft.fuel)
Exemple #17
0
    def test_three_body(self):
        """Test gravitational acceleration between three bodies is expected."""
        with PhysicsEngine('tests/three-body.json') as physics_engine:
            # In this case, three entities form a 90-45-45 triangle, with the
            # entity at the right angle being about as massive as the sun.
            # The first entity is the massive entity, the second is far to the
            # left, and the third is far to the top.
            physics_state = physics_engine.get_state(0)

            # Test that every single entity has the correct accelerations.
            y0 = physics_state
            dy = PhysicsState(
                ode_solver.simulation_differential_function(
                    0, y0.y0(), y0._proto_state, physics_engine.M,
                    physics_engine._artificials), physics_state._proto_state)
            self.assertEqual(len(dy.X), 3)

            self.assertAlmostEqual(dy.X[0], y0.VX[0])
            self.assertAlmostEqual(dy.Y[0], y0.VY[0])
            self.assertEqual(
                round(abs(dy.VX[0])),
                round(common.G * physics_state[1].mass /
                      (y0.X[0] - y0.X[1])**2))
            self.assertEqual(
                round(abs(dy.VY[0])),
                round(common.G * physics_state[2].mass /
                      (y0.Y[0] - y0.Y[2])**2))

            self.assertAlmostEqual(dy.X[1], y0.VX[1])
            self.assertAlmostEqual(dy.Y[1], y0.VY[1])
            self.assertEqual(
                round(abs(dy.VX[1])),
                round(common.G * physics_state[0].mass /
                      (y0.X[1] - y0.X[0])**2 +
                      np.sqrt(2) * common.G * physics_state[2].mass /
                      (y0.X[1] - y0.X[2])**2))
            self.assertEqual(
                round(abs(dy.VY[1])),
                round(
                    np.sqrt(2) * common.G * physics_state[2].mass /
                    (y0.X[1] - y0.X[2])**2))

            self.assertAlmostEqual(dy.X[2], y0.VX[2])
            self.assertAlmostEqual(dy.Y[2], y0.VY[2])
            self.assertEqual(
                round(abs(dy.VX[2])),
                round(
                    np.sqrt(2) * common.G * physics_state[2].mass /
                    (y0.X[1] - y0.X[2])**2))
            self.assertEqual(
                round(abs(dy.VY[2])),
                round(common.G * physics_state[0].mass /
                      (y0.Y[2] - y0.Y[0])**2 +
                      np.sqrt(2) * common.G * physics_state[1].mass /
                      (y0.Y[2] - y0.Y[1])**2))
Exemple #18
0
    def get_state(self, commands: List[Request] = None) \
            -> PhysicsState:

        if commands is None or len(commands) == 0:
            commands_iter = iter(
                [Request(ident=Request.NOOP, client=self.client_type)])
        else:
            for command in commands:
                command.client = self.client_type
            commands_iter = iter(commands)
        return PhysicsState(None, self.stub.get_physical_state(commands_iter))
Exemple #19
0
    def __init__(self, client: protos.Command.ClientType, hostname: str):
        self.channel = grpc.insecure_channel(f'{hostname}:{DEFAULT_PORT}')
        self.stub = grpc_stubs.StateServerStub(self.channel)
        self.client_type = client

        initial_state = PhysicsState(
            None,
            self.stub.get_physical_state(
                iter([Request(ident=Request.NOOP, client=self.client_type)])))

        self._caching_physics_engine = physics.PhysicsEngine(initial_state)
        self._time_of_next_network_update = 0.0
Exemple #20
0
    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)
Exemple #21
0
    def set_state(self, physical_state: PhysicsState):
        self._stop_simthread()

        physical_state = _reconcile_entity_dynamics(physical_state)
        self._artificials = np.where(
            np.array([entity.artificial for entity in physical_state]) >= 1)[0]

        # We keep track of the PhysicalState because our simulation
        # only simulates things that change like position and velocity,
        # not things that stay constant like names and mass.
        # self._last_physical_state contains these constants.
        self._last_physical_state = physical_state.as_proto()
        self.R = np.array([entity.r for entity in physical_state])
        self.M = np.array([entity.mass for entity in physical_state])

        self._start_simthread(physical_state.timestamp, physical_state)
Exemple #22
0
def navmode_spin(flight_state: PhysicsState) -> float:
    """Returns a spin that will orient the craft according to the navmode."""

    craft = flight_state.craft_entity()
    requested_heading = navmode_heading(flight_state)
    ccw_distance = (requested_heading - craft.heading) % np.radians(360)
    cw_distance = (craft.heading - requested_heading) % np.radians(360)
    if ccw_distance < cw_distance:
        heading_difference = ccw_distance
    else:
        heading_difference = -cw_distance
    if abs(heading_difference) < common.AUTOPILOT_FINE_CONTROL_RADIUS:
        # The unit analysis doesn't work out here I'm sorry. Basically,
        # the closer we are to the target heading, the slower we adjust.
        return heading_difference
    else:
        # We can spin at most common.AUTOPILOT_SPEED
        return np.sign(heading_difference) * common.AUTOPILOT_SPEED
Exemple #23
0
    def get_state(self, commands: List[Request] = None) \
            -> PhysicsState:

        current_time = time.monotonic()
        commands_to_send = False

        if commands is None or len(commands) == 0:
            commands_iter = iter(
                [Request(ident=Request.NOOP, client=self.client_type)])
        else:
            for command in commands:
                command.client = self.client_type
            commands_iter = iter(commands)
            commands_to_send = True

        returned_state: PhysicsState

        if commands_to_send or current_time > self._time_of_next_network_update:
            # We need to make a network request.
            # TODO: what if this fails? Do anything smarter than an exception?
            returned_state = PhysicsState(
                None, self.stub.get_physical_state(commands_iter))
            self._caching_physics_engine.set_state(returned_state)

            if commands_to_send:
                # If we sent a user command, still ask for an update soon so
                # the user input can be reflected in hab flight's GUI as soon
                # as possible.
                # Magic number alert! The constant we add should be enough that
                # the physics server has had enough time to simulate the effect
                # of our input, but we should minimize the constant to minimize
                # input lag.
                self._time_of_next_network_update = current_time + 0.15
            else:
                self._time_of_next_network_update = (
                    current_time + common.TIME_BETWEEN_NETWORK_UPDATES)

        else:
            returned_state = self._caching_physics_engine.get_state()

        return returned_state
Exemple #24
0
def drag(flight_state: PhysicsState) -> np.ndarray:
    """Calculates the directional drag exerted on the craft by an atmosphere.
    """
    atmosphere = relevant_atmosphere(flight_state)
    if atmosphere is None:
        return np.array([0, 0])

    craft = flight_state.craft_entity()
    air_v = rotational_speed(craft, atmosphere)
    wind = craft.v - air_v
    if np.inner(wind, wind) < 0.01:
        # The craft is stationary
        return np.array([0, 0])
    wind_angle = np.arctan2(wind[1], wind[0])

    # I know I've said this about other pieces of code, but I really have no
    # idea how this code works. I just lifted it from OrbitV because I couldn't
    # find simple atmospheric drag models.
    # Also sorry, unit analysis doesn't work inside this function.
    # TODO: how does this work outside of the atmosphere?
    # TODO: disabled while I figure out if this only works during telemetry
    i_know_what_this_aoa_code_does = False
    if i_know_what_this_aoa_code_does:
        aoa = wind_angle - pitch(craft, atmosphere)
        aoa = np.sign(np.sign(np.cos(aoa)) - 1)
        aoa = aoa**3
        if aoa > 0.5:
            aoa = 1 - aoa
        aoa_vector = -abs(aoa) * np.array([
            np.sin(wind_angle + (np.pi / 2) * np.sign(aoa)),
            np.cos(wind_angle + (np.pi / 2) * np.sign(aoa))
        ])
        return aoa_vector

    drag_profile = common.HAB_DRAG_PROFILE
    if flight_state.parachute_deployed:
        drag_profile += common.PARACHUTE_DRAG_PROFILE
    drag_acc = \
        pressure(craft, atmosphere) * fastnorm(wind) ** 2 * drag_profile
    return drag_acc * (wind / fastnorm(wind))
def clone_orbitv_state(rnd_path: Path) -> PhysicsState:
    """Gathers information from an OrbitV savefile, in the *.RND format,
    as well as a STARSr file.
    Returns a PhysicsState representing everything we can read."""
    proto_state = protos.PhysicalState()
    starsr_path = rnd_path.parent / 'STARSr'

    log.info(f"Reading OrbitV file {rnd_path}, "
             f"using info from adjacent STARSr file {starsr_path}")

    hab_index: Optional[int] = None
    ayse_index: Optional[int] = None

    with open(starsr_path, 'r') as starsr_file:
        starsr = csv.reader(starsr_file, delimiter=',')

        background_star_line = next(starsr)
        while len(background_star_line) == 3:
            background_star_line = next(starsr)

        # After the last while loop, the value of background_star_line is
        # actually a gravity_pair line (a pair of indices, which we also don't
        # care about).
        gravity_pair_line = background_star_line
        while len(gravity_pair_line) == 2:
            gravity_pair_line = next(starsr)

        entity_constants_line = gravity_pair_line
        while len(entity_constants_line) == 6:
            proto_entity = proto_state.entities.add()

            # Cast all the elements on a line to floats.
            # Some strings will be the form '1.234D+04', convert these too.
            (colour, mass, radius,
                atmosphere_thickness, atmosphere_scaling, atmosphere_height) \
                = map(_string_to_float, entity_constants_line)
            mass = max(1, mass)

            proto_entity.mass = mass
            proto_entity.r = radius
            if atmosphere_thickness and atmosphere_scaling:
                # Only set atmosphere fields if they both have a value.
                proto_entity.atmosphere_thickness = atmosphere_thickness
                proto_entity.atmosphere_scaling = atmosphere_scaling

            entity_constants_line = next(starsr)

        # We skip a line here. It's the timestamp line, but the .RND file will
        # have more up-to-date timestamp info.
        entity_positions_line = next(starsr)

        # We don't care about entity positions, we'll get this from the .RND
        # file as well.
        while len(entity_positions_line) == 6:
            entity_positions_line = next(starsr)

        entity_name_line = entity_positions_line
        entity_index = 0
        while len(entity_name_line) == 1:
            name = entity_name_line[0].strip()
            proto_state.entities[entity_index].name = name

            if name == common.HABITAT:
                hab_index = entity_index
            elif name == common.AYSE:
                ayse_index = entity_index

            entity_index += 1
            entity_name_line = next(starsr)

    assert proto_state.entities[0].name == common.SUN, (
        'We assume that the Sun is the first OrbitV entity, this has '
        'implications for how we populate entity positions.')
    assert hab_index is not None, \
        "We didn't see the Habitat in the STARSr file!"
    assert ayse_index is not None, \
        "We didn't see AYSE in the STARSr file!"

    hab = proto_state.entities[hab_index]
    ayse = proto_state.entities[ayse_index]

    with open(rnd_path, 'rb') as rnd:
        check_byte = rnd.read(1)
        rnd.read(len("ORBIT5S        "))
        craft_throttle = _read_single(rnd) / 100

        # I don't know what Vflag and Aflag do.
        _read_int(rnd), _read_int(rnd)
        navmode = Navmode(SFLAG_TO_NAVMODE[_read_int(rnd)])
        if navmode is not None:
            proto_state.navmode = navmode.value

        _read_double(rnd)  # This is drag, we calculate this ourselves.
        _read_double(rnd)  # This is zoom, we ignore this as well.
        hab.heading = _read_single(rnd)
        # Skip centre, ref, and targ information.
        _read_int(rnd), _read_int(rnd), _read_int(rnd)

        # We don't track if trails are set.
        _read_int(rnd)
        drag_profile = _read_single(rnd)
        proto_state.parachute_deployed = (drag_profile > 0.002)

        current_srb_output = _read_single(rnd)
        if current_srb_output != 0:
            proto_state.srb_time = common.SRB_BURNTIME

        # We don't care about colour trails or how times are displayed.
        _read_int(rnd), _read_int(rnd)
        _read_double(rnd), _read_double(rnd)  # No time acc info either.
        _read_single(rnd)  # Ignore some RCPS info
        _read_int(rnd)  # Eflag? Something that gassimv.bas sets.

        year = _read_int(rnd)
        day = _read_int(rnd)
        hour = _read_int(rnd)
        minute = _read_int(rnd)
        second = _read_double(rnd)

        timestamp = datetime(
            year, 1, 1, hour=hour, minute=minute,
            second=int(second)) + timedelta(day - 1)
        proto_state.timestamp = timestamp.timestamp()

        ayse.heading = _read_single(rnd)
        _read_int(rnd)  # AYSEscrape seems to do nothing.

        # TODO: Once we implement wind, fill in these fields.
        _read_single(rnd), _read_single(rnd)

        hab.spin = numpy.radians(_read_single(rnd))
        docked_constant = _read_int(rnd)
        if docked_constant != 0:
            hab.landed_on = common.AYSE

        _read_single(rnd)  # Rad shields, overwritten by enghabv.bas.
        # TODO: once module is implemented, have it be launched here.
        _read_int(rnd)
        # module_state = MODFLAG_TO_MODSTATE[modflag]

        _read_single(rnd), _read_single(rnd)  # AYSEdist and OCESSdist.

        hab.broken = bool(_read_int(rnd))
        ayse.broken = bool(_read_int(rnd))

        # TODO: Once we implement nav malfunctions, set this field.
        _read_single(rnd)

        # Max thrust, ignored because we'll read it from ORBITSSE.rnd
        _read_single(rnd)

        # TODO: Once we implement nav malfunctions, set this field.
        # Also, apparently this is the same field as two fields ago?
        _read_single(rnd)

        # TODO: Once we implement wind, fill in these fields.
        _read_long(rnd)

        # TODO: There is some information required from MST.rnd to implement
        # this field (LONGtarg).
        _read_single(rnd)

        # We calculate pressure and agrav.
        _read_single(rnd), _read_single(rnd)
        # Note, we skip the first entity, the Sun, since OrbitV does.
        for i in range(1, min(39, len(proto_state.entities))):
            entity = proto_state.entities[i]
            entity.x, entity.y, entity.vx, entity.vy = (_read_double(rnd),
                                                        _read_double(rnd),
                                                        _read_double(rnd),
                                                        _read_double(rnd))

        hab.fuel = _read_single(rnd)
        ayse.fuel = _read_single(rnd)

        check_byte_2 = rnd.read(1)

    if check_byte != check_byte_2:
        log.warning('RND file had inconsistent check bytes: '
                    f'{check_byte} != {check_byte_2}')

    # Delete any entity with a (0, 0) position that isn't the Sun.
    # TODO: We also delete the OCESS launch tower, but once we implement
    # OCESS we should no longer do this.
    entity_index = 0
    while entity_index < len(proto_state.entities):
        proto_entity = proto_state.entities[entity_index]
        if round(proto_entity.x) == 0 and round(proto_entity.y) == 0 and \
                proto_entity.name != common.SUN or \
                proto_entity.name == common.OCESS:
            del proto_state.entities[entity_index]
        else:
            entity_index += 1

    hab.artificial = True
    ayse.artificial = True
    # Habitat and AYSE masses are hardcoded in OrbitV.
    hab.mass = 275000
    ayse.mass = 20000000

    orbitx_state = PhysicsState(None, proto_state)
    orbitx_state[common.HABITAT] = Entity(hab)
    orbitx_state[common.AYSE] = Entity(ayse)

    craft = orbitx_state.craft_entity()
    craft.throttle = craft_throttle

    log.debug(f'Interpreted {rnd_path} and {starsr_path} into this state:')
    log.debug(repr(orbitx_state))
    orbitx_state = _separate_landed_entities(orbitx_state)
    return orbitx_state
Exemple #26
0
 def test_landed_on(self):
     """Test that the special .landed_on field is properly set."""
     ps = PhysicsState(None, self.proto_state)
     self.assertEqual(ps['First'].landed_on, '')
     self.assertEqual(ps['Second'].landed_on, 'First')
Exemple #27
0
    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
Exemple #28
0
    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)
Exemple #29
0
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
Exemple #30
0
    def _update_obj(self, entity: Entity, state: PhysicsState, origin: Entity):
        super()._update_obj(entity, state, origin)
        self._obj.boosters.pos = self._obj.pos
        self._obj.boosters.axis = self._obj.axis
        # Attach the parachute to the forward cone of the habitat.
        self._obj.parachute.pos = (
            self._obj.pos + calc.angle_to_vpy(entity.heading) * entity.r * 0.8)

        parachute_is_visible = ((state.craft == entity.name)
                                and state.parachute_deployed)
        if parachute_is_visible:
            drag = calc.drag(state)
            drag_mag = np.inner(drag, drag)
        for parachute in [self._obj.parachute, self._small_habitat.parachute]:
            if not parachute_is_visible or drag_mag < 0.00001:
                # If parachute_is_visible == False, don't show the parachute.
                # If the drag is basically zero, don't show the parachute.
                parachute.visible = False
                continue

            if drag_mag > 0.1:
                parachute.width = self.PARACHUTE_RADIUS * 2
                parachute.height = self.PARACHUTE_RADIUS * 2
            else:
                # Below a certain threshold the parachute deflates.
                parachute.width = self.PARACHUTE_RADIUS
                parachute.height = self.PARACHUTE_RADIUS

            parachute.axis = -vpython.vec(*drag, 0)
            parachute.visible = True

        if not self._broken and entity.broken:
            # We weren't broken before, but looking at new data we realize
            # we're now broken. Change the habitat texture.
            new_texture = self._obj.texture.replace('Habitat.jpg',
                                                    'Habitat-broken.jpg')
            assert new_texture != self._obj.texture, \
                f'{new_texture!r} == {self._obj.texture!r}'
            self._obj.texture = new_texture
            self._small_habitat.texture = new_texture
            self._broken = entity.broken
        elif self._broken and not entity.broken:
            # We were broken before, but we've repaired ourselves somehow.
            new_texture = self._obj.texture.replace('Habitat-broken.jpg',
                                                    'Habitat.jpg')
            assert new_texture != self._obj.texture, \
                f'{new_texture!r} == {self._obj.texture!r}'
            self._obj.texture = new_texture
            self._small_habitat.texture = new_texture
            self._broken = entity.broken

        # Set reference and target arrows of the minimap habitat.
        same = state.reference == entity.name
        default = vpython.vector(0, 0, -1)

        ref_arrow_axis = (entity.screen_pos(state.reference_entity()).norm() *
                          entity.r * -1.2)
        v = entity.v - state.reference_entity().v
        velocity_arrow_axis = \
            vpython.vector(*v, 0).norm() * entity.r

        self._ref_arrow.axis = default if same else ref_arrow_axis
        self._velocity_arrow.axis = default if same else velocity_arrow_axis
        self._small_habitat.axis = self._obj.axis
        self._small_habitat.boosters.axis = self._obj.axis

        # Invisible-ize the SRBs if we ran out.
        if state.srb_time == common.SRB_EMPTY and self._obj.boosters.visible:
            self._obj.boosters.visible = False
            self._small_habitat.boosters.visible = False