Repository: NREL/hive (language: python)

Issue:
Fix mypy errors
Running mypy over the hive project currently results in 586 errors. It would be nice to eventually go through and pick these off. 


Discussion / hints:

Fix patch:
diff --git a/examples/cosim_custom_dispatcher.py b/examples/cosim_custom_dispatcher.py
index 80f05898..cdb0bf2f 100644
--- a/examples/cosim_custom_dispatcher.py
+++ b/examples/cosim_custom_dispatcher.py
@@ -7,9 +7,7 @@
 
 from nrel.hive import package_root
 from nrel.hive.dispatcher.instruction.instructions import IdleInstruction
-from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
-    InstructionGenerator,
-)
+from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
 from nrel.hive.runner.runner_payload_ops import (
     get_instruction_generator,
     update_instruction_generator,
@@ -59,9 +57,7 @@ def run():
         crank_result = hc.crank(rp, 10)
 
         # get dispatcher to update it
-        dispatcher: CustomDispatcher = get_instruction_generator(
-            crank_result.runner_payload, CustomDispatcher
-        )
+        dispatcher = get_instruction_generator(crank_result.runner_payload, CustomDispatcher)
 
         # apply some change to the state of the dispatcher
         updated_dispatcher = dispatcher.new_random_state()
diff --git a/mypy.ini b/mypy.ini
index 522fe245..18a5eb2d 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -6,4 +6,4 @@ exclude = (?x)(
     docs/    
     | build/  
     | dist/   
-  )
\ No newline at end of file
+  )
diff --git a/nrel/hive/__init__.py b/nrel/hive/__init__.py
index 97e1fa92..3579bd36 100644
--- a/nrel/hive/__init__.py
+++ b/nrel/hive/__init__.py
@@ -29,9 +29,7 @@
 from nrel.hive.config import HiveConfig
 from nrel.hive.dispatcher import *
 from nrel.hive.state.simulation_state.update.update import Update
-from nrel.hive.state.simulation_state.update.step_simulation import (
-    StepSimulation,
-)
+from nrel.hive.state.simulation_state.update.step_simulation import StepSimulation
 
 
 def package_root() -> Path:
diff --git a/nrel/hive/app/hive_cosim.py b/nrel/hive/app/hive_cosim.py
index 84194da5..22a6d629 100644
--- a/nrel/hive/app/hive_cosim.py
+++ b/nrel/hive/app/hive_cosim.py
@@ -1,33 +1,29 @@
 import functools as ft
 from pathlib import Path
-from typing import Tuple, NamedTuple, Optional
+from typing import Tuple, NamedTuple, Optional, TypeVar
 
 import pandas as pd
 from pandas import DataFrame
 from tqdm import tqdm
 
 from nrel.hive import Update
-from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
-    InstructionGenerator,
-)
-from nrel.hive.dispatcher.instruction_generator.charging_fleet_manager import (
-    ChargingFleetManager,
-)
+from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
+from nrel.hive.dispatcher.instruction_generator.charging_fleet_manager import ChargingFleetManager
 from nrel.hive.dispatcher.instruction_generator.dispatcher import Dispatcher
 from nrel.hive.initialization.load import load_simulation
 from nrel.hive.model.sim_time import SimTime
 from nrel.hive.reporting import reporter_ops
-from nrel.hive.reporting.handler.vehicle_charge_events_handler import (
-    VehicleChargeEventsHandler,
-)
+from nrel.hive.reporting.handler.vehicle_charge_events_handler import VehicleChargeEventsHandler
 from nrel.hive.runner import RunnerPayload
 from nrel.hive.util import SimulationStateError
 from nrel.hive.util.fp import throw_on_failure
 
+T = TypeVar("T", bound=InstructionGenerator)
+
 
 def load_scenario(
     scenario_file: Path,
-    custom_instruction_generators: Optional[Tuple[InstructionGenerator, ...]] = None,
+    custom_instruction_generators: Optional[Tuple[T, ...]] = None,
 ) -> RunnerPayload:
     """
     load a HIVE scenario from file and return the initial simulation state
@@ -49,13 +45,13 @@ def load_scenario(
             ChargingFleetManager(env.config.dispatcher),
             Dispatcher(env.config.dispatcher),
         )
+        update = Update.build(env.config, instruction_generators)
     else:
-        instruction_generators = custom_instruction_generators
+        update = Update.build(env.config, custom_instruction_generators)
 
     # add a specialized Reporter handler that catches vehicle charge events
     env.reporter.add_handler(VehicleChargeEventsHandler())
 
-    update = Update.build(env.config, instruction_generators)
     initial_payload = RunnerPayload(sim, env, update)
 
     return initial_payload
diff --git a/nrel/hive/app/run.py b/nrel/hive/app/run.py
index f72de398..e4a4c56e 100644
--- a/nrel/hive/app/run.py
+++ b/nrel/hive/app/run.py
@@ -10,9 +10,7 @@
 import pkg_resources
 import yaml
 
-from nrel.hive.dispatcher.instruction_generator.charging_fleet_manager import (
-    ChargingFleetManager,
-)
+from nrel.hive.dispatcher.instruction_generator.charging_fleet_manager import ChargingFleetManager
 from nrel.hive.dispatcher.instruction_generator.dispatcher import Dispatcher
 from nrel.hive.initialization.load import load_simulation
 from nrel.hive.reporting import reporter_ops
diff --git a/nrel/hive/app/run_batch.py b/nrel/hive/app/run_batch.py
index 1ecc37fe..5aaf6483 100644
--- a/nrel/hive/app/run_batch.py
+++ b/nrel/hive/app/run_batch.py
@@ -72,7 +72,12 @@ def run() -> int:
     sim_args = [SimArgs(f, i) for i, f in enumerate(config.scenario_files)]
 
     # check to make sure we don't exceed system CPU
-    max_cpu = os.cpu_count()
+    os_cpu = os.cpu_count()
+
+    if os_cpu is None:
+        max_cpu = 1
+    else:
+        max_cpu = os_cpu
 
     if len(config.scenario_files) > max_cpu:
         cpu = max_cpu
diff --git a/nrel/hive/app/run_tune.py b/nrel/hive/app/run_tune.py
index b9cf0865..cd111baa 100644
--- a/nrel/hive/app/run_tune.py
+++ b/nrel/hive/app/run_tune.py
@@ -7,9 +7,7 @@
 import ray
 from ray import tune
 
-from nrel.hive.dispatcher.instruction_generator.charging_fleet_manager import (
-    ChargingFleetManager,
-)
+from nrel.hive.dispatcher.instruction_generator.charging_fleet_manager import ChargingFleetManager
 from nrel.hive.dispatcher.instruction_generator.dispatcher import Dispatcher
 from nrel.hive.initialization.load import load_simulation
 from nrel.hive.reporting.reporter import Reporter
@@ -54,7 +52,7 @@ def _scoring_function(payload: RunnerPayload) -> float:
         print("VEHICLES ", payload.s.vehicles.values())
         return score
 
-    def _setup(self, config: Dict[str:int]):
+    def _setup(self, config: Dict[str, int]):
         scenarios = {
             1: "denver_demo.yaml",
             2: "denver_demo_constrained_charging.yaml",
diff --git a/nrel/hive/config/config_builder.py b/nrel/hive/config/config_builder.py
index 7ca56c67..014e8dd6 100644
--- a/nrel/hive/config/config_builder.py
+++ b/nrel/hive/config/config_builder.py
@@ -12,8 +12,8 @@ def build(
         cls,
         default_config: Dict,
         required_config: Tuple[str, ...],
-        config_constructor: Callable[[Dict], Union[Exception, T]],
-        config: Dict = None,
+        config_constructor: Callable[[Dict], T],
+        config: Optional[Dict] = None,
     ) -> T:
         """
         constructs a Config from a configuration Dict
diff --git a/nrel/hive/config/dispatcher_config.py b/nrel/hive/config/dispatcher_config.py
index 1a251f21..d431856a 100644
--- a/nrel/hive/config/dispatcher_config.py
+++ b/nrel/hive/config/dispatcher_config.py
@@ -3,9 +3,7 @@
 from typing import NamedTuple, Dict, Union, Tuple, Optional
 
 from nrel.hive.config.config_builder import ConfigBuilder
-from nrel.hive.dispatcher.instruction_generator.charging_search_type import (
-    ChargingSearchType,
-)
+from nrel.hive.dispatcher.instruction_generator.charging_search_type import ChargingSearchType
 from nrel.hive.util.units import Ratio, Seconds, Kilometers
 
 
@@ -41,12 +39,12 @@ def build(cls, config: Optional[Dict] = None) -> DispatcherConfig:
         )
 
     @classmethod
-    def from_dict(cls, d: Dict) -> Union[IOError, DispatcherConfig]:
+    def from_dict(cls, d: Dict) -> DispatcherConfig:
         try:
             d["valid_dispatch_states"] = tuple(s.lower() for s in d["valid_dispatch_states"])
             d["charging_search_type"] = ChargingSearchType.from_string(d["charging_search_type"])
         except ValueError:
-            return IOError("valid_dispatch_states and active_states must be in a list format")
+            raise IOError("valid_dispatch_states and active_states must be in a list format")
 
         return DispatcherConfig(**d)
 
diff --git a/nrel/hive/config/hive_config.py b/nrel/hive/config/hive_config.py
index adc882e2..4ea9b544 100644
--- a/nrel/hive/config/hive_config.py
+++ b/nrel/hive/config/hive_config.py
@@ -34,7 +34,7 @@ class HiveConfig(NamedTuple):
     def build(
         cls,
         scenario_file_path: Path,
-        config: Dict = None,
+        config: Optional[Dict] = None,
         output_suffix: Optional[str] = datetime.now().strftime("%Y-%m-%d_%H-%M-%S"),
     ) -> Union[Exception, HiveConfig]:
         """
@@ -55,7 +55,7 @@ def build(
     @classmethod
     def from_dict(
         cls, d: Dict, scenario_file_path: Path, output_suffix: Optional[str]
-    ) -> Union[Exception, HiveConfig]:
+    ) -> HiveConfig:
         # collect the global hive configuration
         global_config = fs.global_hive_config_search()
 
@@ -142,7 +142,7 @@ def asdict(self) -> Dict:
         out_dict["cache"] = cache
 
         for name, config in self._asdict().items():
-            if issubclass(config.__class__, Tuple):
+            if issubclass(config.__class__, tuple):
                 out_dict[name] = config.asdict()
             else:
                 out_dict[name] = config
diff --git a/nrel/hive/config/input.py b/nrel/hive/config/input.py
index 85ef6ee3..8c32acb6 100644
--- a/nrel/hive/config/input.py
+++ b/nrel/hive/config/input.py
@@ -3,7 +3,7 @@
 import hashlib
 import logging
 from pathlib import Path
-from typing import NamedTuple, Tuple, Dict, Optional
+from typing import NamedTuple, Tuple, Dict, Optional, Union
 
 from nrel.hive.config.config_builder import ConfigBuilder
 from nrel.hive.util import fs
@@ -19,14 +19,14 @@ class Input(NamedTuple):
     bases_file: str
     stations_file: str
     mechatronics_file: str
-    schedules_file: Optional[str]
-    chargers_file: Optional[str]
-    road_network_file: Optional[str]
-    geofence_file: Optional[str]
-    rate_structure_file: Optional[str]
-    charging_price_file: Optional[str]
-    demand_forecast_file: Optional[str]
-    fleets_file: Optional[str]
+    chargers_file: str
+    schedules_file: Optional[str] = None
+    road_network_file: Optional[str] = None
+    geofence_file: Optional[str] = None
+    rate_structure_file: Optional[str] = None
+    charging_price_file: Optional[str] = None
+    demand_forecast_file: Optional[str] = None
+    fleets_file: Optional[str] = None
 
     @classmethod
     def default_config(cls) -> Dict:
@@ -124,7 +124,7 @@ def from_dict(cls, d: Dict, scenario_file_path: Path, cache: Optional[Dict]) ->
             else None
         )
 
-        input = {
+        input_config = {
             "scenario_directory": str(scenario_directory),
             "scenario_file": scenario_file,
             "vehicles_file": vehicles_file,
@@ -147,11 +147,27 @@ def from_dict(cls, d: Dict, scenario_file_path: Path, cache: Optional[Dict]) ->
             for (
                 name,
                 path,
-            ) in input.items():  # input_config.asdict(absolute_paths=True).items():
+            ) in input_config.items():  # input_config.asdict(absolute_paths=True).items():
                 if path:
                     cls._check_md5_checksum(path, cache[name])
 
-        return Input(**input)
+        return Input(
+            scenario_directory=str(scenario_directory),
+            scenario_file=scenario_file,
+            vehicles_file=vehicles_file,
+            requests_file=requests_file,
+            bases_file=bases_file,
+            stations_file=stations_file,
+            schedules_file=schedules_file,
+            chargers_file=chargers_file,
+            mechatronics_file=mechatronics_file,
+            road_network_file=road_network_file,
+            geofence_file=geofence_file,
+            rate_structure_file=rate_structure_file,
+            charging_price_file=charging_price_file,
+            demand_forecast_file=demand_forecast_file,
+            fleets_file=fleets_file,
+        )
 
     @staticmethod
     def _check_md5_checksum(filepath: str, existing_md5_sum: str):
diff --git a/nrel/hive/config/network.py b/nrel/hive/config/network.py
index 82902f44..6ecae2bc 100644
--- a/nrel/hive/config/network.py
+++ b/nrel/hive/config/network.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import NamedTuple, Dict, Union, Tuple
+from typing import NamedTuple, Dict, Optional, Tuple
 
 from nrel.hive.config.config_builder import ConfigBuilder
 
@@ -18,7 +18,7 @@ def required_config(cls) -> Tuple[str, ...]:
         return ()
 
     @classmethod
-    def build(cls, config: Dict = None) -> Union[Exception, Network]:
+    def build(cls, config: Optional[Dict] = None) -> Network:
         return ConfigBuilder.build(
             default_config=cls.default_config(),
             required_config=cls.required_config(),
diff --git a/nrel/hive/config/sim.py b/nrel/hive/config/sim.py
index 6e5a72f3..3016197b 100644
--- a/nrel/hive/config/sim.py
+++ b/nrel/hive/config/sim.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import NamedTuple, Dict, Union, Tuple
+from typing import NamedTuple, Dict, Optional, Union, Tuple
 
 from nrel.hive.config.config_builder import ConfigBuilder
 from nrel.hive.model.vehicle.schedules.schedule_type import ScheduleType
@@ -31,7 +31,7 @@ def required_config(cls) -> Tuple[str, ...]:
         )
 
     @classmethod
-    def build(cls, config: Dict = None) -> Union[IOError, Sim]:
+    def build(cls, config: Optional[Dict] = None) -> Sim:
         return ConfigBuilder.build(
             default_config=cls.default_config(),
             required_config=cls.required_config(),
@@ -40,14 +40,10 @@ def build(cls, config: Dict = None) -> Union[IOError, Sim]:
         )
 
     @classmethod
-    def from_dict(cls, d: Dict) -> Union[IOError, Sim]:
+    def from_dict(cls, d: Dict) -> Sim:
         start_time = SimTime.build(d["start_time"])
-        if isinstance(start_time, IOError):
-            return start_time
 
         end_time = SimTime.build(d["end_time"])
-        if isinstance(end_time, IOError):
-            return end_time
 
         schedule_type = ScheduleType.from_string(d["schedule_type"])
 
diff --git a/nrel/hive/dispatcher/forecaster/__init__.py b/nrel/hive/dispatcher/forecaster/__init__.py
index 3b1da145..a9f2cda7 100644
--- a/nrel/hive/dispatcher/forecaster/__init__.py
+++ b/nrel/hive/dispatcher/forecaster/__init__.py
@@ -1,5 +1,3 @@
 from nrel.hive.dispatcher.forecaster.basic_forecaster import BasicForecaster
 from nrel.hive.dispatcher.forecaster.forecast import Forecast, ForecastType
-from nrel.hive.dispatcher.forecaster.forecaster_interface import (
-    ForecasterInterface,
-)
+from nrel.hive.dispatcher.forecaster.forecaster_interface import ForecasterInterface
diff --git a/nrel/hive/dispatcher/forecaster/basic_forecaster.py b/nrel/hive/dispatcher/forecaster/basic_forecaster.py
index d01cdd1d..3d36b860 100644
--- a/nrel/hive/dispatcher/forecaster/basic_forecaster.py
+++ b/nrel/hive/dispatcher/forecaster/basic_forecaster.py
@@ -2,15 +2,16 @@
 from dataclasses import dataclass
 
 from pathlib import Path
-from typing import Tuple, NamedTuple
+from typing import Tuple, NamedTuple, TYPE_CHECKING
 
 from nrel.hive.model.sim_time import SimTime
 from nrel.hive.dispatcher.forecaster.forecast import Forecast, ForecastType
-from nrel.hive.dispatcher.forecaster.forecaster_interface import (
-    ForecasterInterface,
-)
+from nrel.hive.dispatcher.forecaster.forecaster_interface import ForecasterInterface
 from nrel.hive.util.iterators import DictReaderStepper
 
+if TYPE_CHECKING:
+    from hive.state.simulation_state.simulation_state import SimulationState
+
 
 @dataclass(frozen=True)
 class BasicForecaster(ForecasterInterface):
@@ -38,10 +39,13 @@ def build(cls, demand_forecast_file: str) -> BasicForecaster:
         if error:
             raise error
         else:
+            if reader is None:
+                raise Exception("No reader supplied by the DictReaderStepper")
+
             return BasicForecaster(reader)
 
     def generate_forecast(
-        self, simulation_state: "SimulationState"
+        self, simulation_state: SimulationState
     ) -> Tuple[BasicForecaster, Forecast]:
         """
         Generate fleet targets to be consumed by the dispatcher.
diff --git a/nrel/hive/dispatcher/forecaster/forecaster_interface.py b/nrel/hive/dispatcher/forecaster/forecaster_interface.py
index d1c5be3b..a7eb2a22 100644
--- a/nrel/hive/dispatcher/forecaster/forecaster_interface.py
+++ b/nrel/hive/dispatcher/forecaster/forecaster_interface.py
@@ -4,9 +4,7 @@
 from typing import Tuple, TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 from nrel.hive.dispatcher.forecaster.forecast import Forecast
 
diff --git a/nrel/hive/dispatcher/instruction/instruction.py b/nrel/hive/dispatcher/instruction/instruction.py
index 3087f56b..f46f2430 100644
--- a/nrel/hive/dispatcher/instruction/instruction.py
+++ b/nrel/hive/dispatcher/instruction/instruction.py
@@ -6,15 +6,11 @@
 
 import immutables
 
-from nrel.hive.dispatcher.instruction.instruction_result import (
-    InstructionResult,
-)
+from nrel.hive.dispatcher.instruction.instruction_result import InstructionResult
 from nrel.hive.util.typealiases import VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
 
 
diff --git a/nrel/hive/dispatcher/instruction/instruction_ops.py b/nrel/hive/dispatcher/instruction/instruction_ops.py
index 2c4bcb21..e090ce02 100644
--- a/nrel/hive/dispatcher/instruction/instruction_ops.py
+++ b/nrel/hive/dispatcher/instruction/instruction_ops.py
@@ -1,22 +1,16 @@
 from __future__ import annotations
 
-from typing import Tuple, Optional, FrozenSet
+from typing import Tuple, Optional, FrozenSet, TYPE_CHECKING
 import functools as ft
 
 import logging
 
-import immutables
-
-from nrel.hive.model.request import Request
-from nrel.hive.model.roadnetwork.route import Route
-from nrel.hive.model.roadnetwork.link import Link
 from nrel.hive.model.vehicle.trip_phase import TripPhase
-from nrel.hive.model.vehicle.vehicle import Vehicle, RequestId
-from nrel.hive.state.vehicle_state.servicing_pooling_trip import (
-    ServicingPoolingTrip,
-)
-from nrel.hive.util import GeoId
-from nrel.hive.util.exception import InstructionError
+from nrel.hive.model.vehicle.vehicle import RequestId
+
+if TYPE_CHECKING:
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
+    from nrel.hive.state.vehicle_state.servicing_pooling_trip import ServicingPoolingTrip
 
 log = logging.getLogger(__name__)
 
@@ -32,8 +26,8 @@ def trip_plan_covers_previous(
     :param new_trip_plan: the proposed trip plan
     :return: True, if the plan is valid
     """
-    prev_req_ids = set(map(lambda r_id, phase: r_id, previous_state.trip_plan))
-    plan_req_ids = set(map(lambda r_id, phase: r_id, new_trip_plan))
+    prev_req_ids = set(map(lambda rid_and_phase: rid_and_phase[0], previous_state.trip_plan))
+    plan_req_ids = set(map(lambda rid_and_phase: rid_and_phase[0], new_trip_plan))
 
     previous_plan_not_covered = len(prev_req_ids.difference(plan_req_ids)) > 0
 
@@ -42,7 +36,7 @@ def trip_plan_covers_previous(
 
 def trip_plan_ordering_is_valid(
     new_trip_plan: Tuple[Tuple[RequestId, TripPhase], ...],
-    previous_state: Optional[ServicingPoolingTrip] = None,
+    previous_state: ServicingPoolingTrip,
 ) -> bool:
     """
     checks that the incoming trip plan has a logical pickup and dropoff ordering and that
@@ -55,7 +49,7 @@ def trip_plan_ordering_is_valid(
     # inspect previous state and test for coverage of requests/state
 
     boarded_req_ids = (
-        frozenset(map(lambda trip: trip.request.id, previous_state.trips))
+        frozenset(map(lambda trip: trip[0], previous_state.trip_plan))
         if previous_state
         else frozenset()
     )
@@ -93,7 +87,7 @@ def _test(
 
 
 def trip_plan_all_requests_allow_pooling(
-    sim: "SimulationState", trip_plan: Tuple[Tuple[RequestId, TripPhase], ...]
+    sim: SimulationState, trip_plan: Tuple[Tuple[RequestId, TripPhase], ...]
 ) -> Optional[str]:
     """
     confirm that each request in the trip plan allows pooling
@@ -119,7 +113,7 @@ def _test_req(
 
     req_ids, _ = frozenset(zip(*trip_plan))
     req_ids_unique = frozenset(req_ids)
-    initial_errors = ((), ())
+    initial_errors: Tuple[Tuple[str, ...], Tuple[str, ...]] = ((), ())
     sim_error_req_ids, pool_error_req_ids = ft.reduce(_test_req, req_ids_unique, initial_errors)
     if len(sim_error_req_ids) > 0 and len(pool_error_req_ids) > 0:
         msg = f"reqs not in sim: {sim_error_req_ids}; reqs which don't allow pooling: {pool_error_req_ids}"
diff --git a/nrel/hive/dispatcher/instruction/instructions.py b/nrel/hive/dispatcher/instruction/instructions.py
index 10788f42..6eb8a848 100644
--- a/nrel/hive/dispatcher/instruction/instructions.py
+++ b/nrel/hive/dispatcher/instruction/instructions.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass
 
 import logging
-from typing import NamedTuple, Optional, TYPE_CHECKING, Tuple, Union
+from typing import Optional, TYPE_CHECKING, Tuple
 
 from nrel.hive.dispatcher.instruction.instruction import Instruction
 from nrel.hive.dispatcher.instruction.instruction_ops import (
@@ -10,10 +10,8 @@
     trip_plan_all_requests_allow_pooling,
     trip_plan_covers_previous,
 )
-from nrel.hive.dispatcher.instruction.instruction_result import (
-    InstructionResult,
-)
-from nrel.hive.model.roadnetwork.link import Link, EntityPosition
+from nrel.hive.dispatcher.instruction.instruction_result import InstructionResult
+from nrel.hive.model.entity_position import EntityPosition
 from nrel.hive.model.vehicle.trip_phase import TripPhase
 from nrel.hive.state.vehicle_state import dispatch_ops
 from nrel.hive.state.vehicle_state.charging_base import ChargingBase
@@ -24,17 +22,13 @@
 from nrel.hive.state.vehicle_state.idle import Idle
 from nrel.hive.state.vehicle_state.repositioning import Repositioning
 from nrel.hive.state.vehicle_state.reserve_base import ReserveBase
-from nrel.hive.state.vehicle_state.servicing_pooling_trip import (
-    ServicingPoolingTrip,
-)
+from nrel.hive.state.vehicle_state.servicing_pooling_trip import ServicingPoolingTrip
 from nrel.hive.util.exception import SimulationStateError, InstructionError
 
 log = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.util.typealiases import (
         StationId,
         VehicleId,
@@ -42,7 +36,6 @@
         BaseId,
         ChargerId,
         LinkId,
-        GeoId,
     )
     from nrel.hive.runner.environment import Environment
 
@@ -94,7 +87,7 @@ def apply_instruction(
             )
         else:
             start = vehicle.position
-            end = request.origin_position
+            end = request.position
             route = sim_state.road_network.route(start, end)
             prev_state = vehicle.vehicle_state
             next_state = DispatchTrip.build(self.vehicle_id, self.request_id, route)
@@ -112,23 +105,33 @@ def apply_instruction(
     ) -> Tuple[Optional[Exception], Optional[InstructionResult]]:
         # see https://github.com/NREL/hive/issues/9 for implementation plan
         vehicle = sim_state.vehicles.get(self.vehicle_id)
+        if vehicle is None:
+            veh_error_msg = f"Vehicle {self.vehicle_id} not found in simulation"
+            return SimulationStateError(veh_error_msg), None
+
         v_state = vehicle.vehicle_state
+        if not isinstance(v_state, ServicingPoolingTrip):
+            msg = (
+                "DispatchPoolingTripInstruction can only be applied to ServicingPoolingTrip states"
+            )
+            error = InstructionError(msg)
+            return error, None
 
         req_allow_pooling_error_msg = trip_plan_all_requests_allow_pooling(
             sim_state, self.trip_plan
         )
         # seating_error = check_if_vehicle_has_seats(sim_state, vehicle, self.trip_plan)
 
-        if isinstance(v_state, ServicingPoolingTrip) and not trip_plan_covers_previous(
-            v_state, self.trip_plan
-        ):
+        if not trip_plan_covers_previous(v_state, self.trip_plan):
             msg = "DispatchPoolingTripInstruction updates an active pooling state but doesn't include all previous requests"
             error = InstructionError(msg)
             return error, None
+
         elif not trip_plan_ordering_is_valid(self.trip_plan, v_state):
             msg = f"DispatchPoolingTripInstruction trip order is unsound :{self.trip_plan}"
             error = InstructionError(msg)
             return error, None
+
         elif not vehicle.driver_state.allows_pooling:
             msg = f"attempting to assign a pooling trip to vehicle {self.vehicle_id} which does not allow pooling"
             error = InstructionError(msg)
@@ -147,11 +150,13 @@ def apply_instruction(
             error = InstructionError(msg)
             return error, None
         else:
-            (error, next_state,) = dispatch_ops.begin_or_replan_dispatch_pooling_state(
+            (disp_error, next_state,) = dispatch_ops.begin_or_replan_dispatch_pooling_state(
                 sim_state, self.vehicle_id, self.trip_plan
             )
-            if error is not None:
-                return error, None
+            if disp_error is not None:
+                return disp_error, None
+            elif next_state is None:
+                return Exception("Next state should not be none"), None
             else:
                 return None, InstructionResult(vehicle.vehicle_state, next_state)
 
diff --git a/nrel/hive/dispatcher/instruction_generator/assignment_ops.py b/nrel/hive/dispatcher/instruction_generator/assignment_ops.py
index e197b86e..3d446e90 100644
--- a/nrel/hive/dispatcher/instruction_generator/assignment_ops.py
+++ b/nrel/hive/dispatcher/instruction_generator/assignment_ops.py
@@ -1,9 +1,8 @@
 from __future__ import annotations
 
 import functools as ft
-from typing import Tuple, Callable, NamedTuple, Dict, Optional
+from typing import Dict, Tuple, Callable, NamedTuple, Optional, TYPE_CHECKING
 
-import math
 import logging
 import h3
 import numpy as np
@@ -20,23 +19,18 @@
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
 from nrel.hive.state.vehicle_state.charge_queueing import ChargeQueueing
 from nrel.hive.state.vehicle_state.charging_station import ChargingStation
-from nrel.hive.util import H3Ops, GeoId, Seconds, Ratio, TupleOps
-from nrel.hive.util.typealiases import ChargerId
+from nrel.hive.util.h3_ops import H3Ops
+from nrel.hive.util.tuple_ops import TupleOps
 
-log = logging.getLogger(__name__)
-
-EntityId = str
-MAX_DIST = 999999999.0
+if TYPE_CHECKING:
+    from nrel.hive.util.units import Ratio, Seconds
+    from nrel.hive.util.typealiases import *
+    from nrel.hive.model.entity import EntityABC
 
 
-class Entity:
-    """
-    this class is used as a type hint (duck-typing style) for the following functions
-    but is not intended to be implemented.
-    """
+log = logging.getLogger(__name__)
 
-    id: EntityId
-    geoid: GeoId
+MAX_DIST = 999999999.0
 
 
 class AssignmentSolution(NamedTuple):
@@ -56,9 +50,9 @@ def add(self, pair: Tuple[EntityId, EntityId], cost: float) -> AssignmentSolutio
 
 
 def find_assignment(
-    assignees: Tuple[Entity, ...],
-    targets: Tuple[Entity, ...],
-    cost_fn: Callable[[Entity, Entity], float],
+    assignees: Tuple[EntityABC, ...],
+    targets: Tuple[EntityABC, ...],
+    cost_fn: Callable[[EntityABC, EntityABC], float],
 ) -> AssignmentSolution:
     """
 
@@ -102,7 +96,7 @@ def _add_to_solution(assignment_solution: AssignmentSolution, i: int) -> Assignm
         return solution
 
 
-def h3_distance_cost(a: Entity, b: Entity) -> float:
+def h3_distance_cost(a: EntityABC, b: EntityABC) -> float:
     """
     cost function based on the h3_distance between two entities
 
@@ -114,7 +108,7 @@ def h3_distance_cost(a: Entity, b: Entity) -> float:
     return distance
 
 
-def great_circle_distance_cost(a: Entity, b: Entity) -> float:
+def great_circle_distance_cost(a: EntityABC, b: EntityABC) -> float:
     """
     cost function based on the great circle distance between two entities.
     reverts h3 geoid to a lat/lon pair and calculates the haversine distance.
@@ -154,7 +148,7 @@ def fn(station: Station) -> float:
 
 def nearest_shortest_queue_ranking(
     vehicle: Vehicle, station: Station, env: Environment, max_dist=999999999.0
-) -> Optional[Tuple[ChargerId, float]]:
+) -> Tuple[Optional[ChargerId], float]:
     """
     sort ordering that prioritizes short vehicle queues where possible, using h3_distance
     as the base distance metric and extending that value by the proportion of available chargers
@@ -167,12 +161,20 @@ def nearest_shortest_queue_ranking(
     """
 
     distance = h3.h3_distance(vehicle.geoid, station.geoid)
-    vehicle_mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
 
     def _inner(
         acc: Tuple[Optional[ChargerId], float], charger_id: ChargerId
-    ) -> Tuple[ChargerId, float]:
+    ) -> Tuple[Optional[ChargerId], float]:
+        vehicle_mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
+        if vehicle_mechatronics is None:
+            log.error(f"mechatronics {vehicle.mechatronics_id} not found for vehicle {vehicle.id}")
+            return (None, 0.0)
+
         charger = env.chargers.get(charger_id)
+        if charger is None:
+            log.error(f"charger id {charger_id} not found in environment")
+            return (None, 0.0)
+
         total_chargers = station.get_total_chargers(charger_id)
         if (
             not vehicle_mechatronics.valid_charger(charger)
@@ -185,6 +187,10 @@ def _inner(
         else:
             prev_best_charger_id, prev_best_distance_metric = acc
             enqueued_for_charger_id = station.enqueued_vehicle_count_for_charger(charger_id)
+            if enqueued_for_charger_id is None:
+                log.error(f"charger id {charger_id} not found at station {station.id}")
+                enqueued_for_charger_id = 0
+
             queue_factor = enqueued_for_charger_id / total_chargers
             this_distance_metric = distance + distance * queue_factor
             if prev_best_distance_metric < this_distance_metric:
@@ -194,7 +200,7 @@ def _inner(
 
     # find the lowest nearest_shortest_queue distance metric
     # amongst the possible on-shift charging options at this station
-    initial = (None, max_dist)
+    initial: Tuple[Optional[str], float] = (None, max_dist)
     best_charger_id, best_charger_rank = ft.reduce(
         _inner, station.on_shift_access_chargers, initial
     )
@@ -221,7 +227,7 @@ def shortest_time_to_charge_distance(
     :return: the distance metric for this vehicle/station pair (lower is better)
     """
 
-    def fn(station: Station) -> Seconds:
+    def fn(station: Station) -> float:
         result = shortest_time_to_charge_ranking(sim, env, vehicle, station, target_soc)
         dist = 999999999.0 if result is None else result[1]
         return dist
@@ -291,8 +297,14 @@ def _time_to_full(v: Vehicle) -> Seconds:
 
             return _time_to_full
 
-        def _sort_enqueue_time(v: Vehicle) -> float:
-            enqueue_time = v.vehicle_state.enqueue_time
+        def _sort_enqueue_time(v: Vehicle) -> int:
+            if isinstance(v.vehicle_state, ChargeQueueing):
+                enqueue_time = int(v.vehicle_state.enqueue_time)
+            else:
+                log.error(
+                    "calling _sort_enqueue_time on a vehicle state that is not ChargeQueueing"
+                )
+                enqueue_time = 0
             return enqueue_time
 
         def _greedy_assignment(
@@ -311,9 +323,14 @@ def _greedy_assignment(
             :param time_passed: the amount of time that has been estimated
             :return: the time in the future we should expect to begin charging, determined by a greedy assignment
             """
+            total_chargers = station.get_total_chargers(_charger_id)
+            if total_chargers is None:
+                log.error(f"charger id {_charger_id} not found at station {station.id}")
+                total_chargers = 0
+
             if len(_charging) == len(_enqueued) == 0:
                 return time_passed
-            elif len(_charging) < station.get_total_chargers(charger_id):
+            elif len(_charging) < total_chargers:
                 return time_passed
             else:
                 # advance time
@@ -325,7 +342,7 @@ def _greedy_assignment(
                 _charging_time_advanced = map(lambda t: t - next_released_charger_time, _charging)
                 _charging_vacated = tuple(filter(lambda t: t > 0, _charging_time_advanced))
 
-                vacancies = station.get_total_chargers(_charger_id) - len(_charging_vacated)
+                vacancies = total_chargers - len(_charging_vacated)
                 if vacancies <= 0:
                     # no space for any changes from enqueued -> charging
                     return _greedy_assignment(
@@ -355,7 +372,7 @@ def _greedy_assignment(
             sort_key=_sort_enqueue_time,
         )
 
-        estimates = {}
+        estimates: Dict[ChargerId, int] = {}
         for charger_id in station.state.keys():
             charger_state = station.state.get(charger_id)
             charger = charger_state.charger if charger_state is not None else None
@@ -373,9 +390,21 @@ def _greedy_assignment(
                 sim.sim_timestep_duration_seconds,
             )
 
+            def _using_charger(charging_vehicle: Vehicle) -> bool:
+                if isinstance(charging_vehicle.vehicle_state, ChargingStation):
+                    if charging_vehicle.vehicle_state.charger_id == charger_id:
+                        return True
+                return False
+
+            def _waiting_for_charger(enqueued_vehicle: Vehicle) -> bool:
+                if isinstance(enqueued_vehicle.vehicle_state, ChargeQueueing):
+                    if enqueued_vehicle.vehicle_state.charger_id == charger_id:
+                        return True
+                return False
+
             # collect all estimated remaining charge times for charging vehicles and sort them
             charging = filter(
-                lambda v: v.vehicle_state.charger_id == charger_id,
+                _using_charger,
                 vehicles_at_station,
             )
             charging_time_to_full: Tuple[Seconds, ...] = tuple(
@@ -385,7 +414,7 @@ def _greedy_assignment(
             # collect estimated remaining charge times for vehicles enqueued for this charger
             # leave them sorted by enqueue time
             enqueued = filter(
-                lambda v: v.vehicle_state.charger_id == charger_id,
+                _waiting_for_charger,
                 vehicles_enqueued,
             )
             enqueued_time_to_full: Tuple[Seconds, ...] = tuple(
@@ -405,9 +434,9 @@ def _greedy_assignment(
             # there are no chargers the vehicle can use.
             return None
 
-        best_charger_id = min(estimates, key=estimates.get)
+        best_charger_id = min(estimates, key=estimates.__getitem__)
 
         # return the best "distance" aka shortest estimated time to finish charging
-        best_overall_time = estimates.get(best_charger_id)
+        best_overall_time = estimates[best_charger_id]
         dispatch_time_seconds = route_travel_time_seconds(route)
         return best_charger_id, dispatch_time_seconds + best_overall_time
diff --git a/nrel/hive/dispatcher/instruction_generator/charging_fleet_manager.py b/nrel/hive/dispatcher/instruction_generator/charging_fleet_manager.py
index b515915d..e2b61169 100644
--- a/nrel/hive/dispatcher/instruction_generator/charging_fleet_manager.py
+++ b/nrel/hive/dispatcher/instruction_generator/charging_fleet_manager.py
@@ -10,16 +10,12 @@
 
 if TYPE_CHECKING:
     from nrel.hive.model.vehicle.vehicle import Vehicle
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
     from nrel.hive.dispatcher.instruction.instruction import Instruction
     from nrel.hive.config.dispatcher_config import DispatcherConfig
 
-from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
-    InstructionGenerator,
-)
+from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
 from nrel.hive.dispatcher.instruction_generator.instruction_generator_ops import (
     instruct_vehicles_to_dispatch_to_station,
     get_nearest_valid_station_distance,
@@ -60,6 +56,10 @@ def charge_candidate(v: Vehicle) -> bool:
                 return False
 
             mechatronics = environment.mechatronics.get(v.mechatronics_id)
+            if mechatronics is None:
+                log.error(f"mechatronics {v.mechatronics_id} missing for vehicle {v.id}")
+                return False
+
             range_remaining_km = mechatronics.range_remaining_km(v)
             if range_remaining_km > environment.config.dispatcher.charging_range_km_soft_threshold:
                 # don't even check station distance if vehicle range is over soft threshold
diff --git a/nrel/hive/dispatcher/instruction_generator/dispatcher.py b/nrel/hive/dispatcher/instruction_generator/dispatcher.py
index 0ca90bbb..ab9b0dd0 100644
--- a/nrel/hive/dispatcher/instruction_generator/dispatcher.py
+++ b/nrel/hive/dispatcher/instruction_generator/dispatcher.py
@@ -1,29 +1,26 @@
 from __future__ import annotations
 from dataclasses import dataclass
 
+import logging
 import functools as ft
 from typing import Tuple, TYPE_CHECKING, Optional
 
 from nrel.hive.dispatcher.instruction_generator import assignment_ops
 from nrel.hive.state.vehicle_state.charging_base import ChargingBase
-from nrel.hive.util.typealiases import MembershipId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
     from nrel.hive.dispatcher.instruction.instruction import Instruction
     from nrel.hive.model.vehicle.vehicle import Vehicle
     from nrel.hive.model.request.request import Request
     from nrel.hive.config.dispatcher_config import DispatcherConfig
+    from nrel.hive.util.typealiases import MembershipId
 
-from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
-    InstructionGenerator,
-)
-from nrel.hive.dispatcher.instruction.instructions import (
-    DispatchTripInstruction,
-)
+from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
+from nrel.hive.dispatcher.instruction.instructions import DispatchTripInstruction
+
+log = logging.getLogger(__name__)
 
 
 @dataclass(frozen=True)
@@ -67,6 +64,10 @@ def _is_valid_for_dispatch(vehicle: Vehicle) -> bool:
                     return False
 
                 mechatronics = environment.mechatronics.get(vehicle.mechatronics_id)
+                if mechatronics is None:
+                    log.error(f"mechatonrics not found for vehicle {vehicle.id}")
+                    return False
+
                 range_remaining_km = mechatronics.range_remaining_km(vehicle)
 
                 # if we are at a base, do we have enough remaining range to leave the base?
@@ -82,7 +83,11 @@ def _is_valid_for_dispatch(vehicle: Vehicle) -> bool:
 
             def _valid_request(r: Request) -> bool:
                 not_already_dispatched = not r.dispatched_vehicle
-                valid_access = r.membership.grant_access_to_membership_id(membership_id)
+                valid_access = (
+                    r.membership.grant_access_to_membership_id(membership_id)
+                    if membership_id is not None
+                    else True
+                )
                 return not_already_dispatched and valid_access
 
             # collect the vehicles and requests for the assignment algorithm
@@ -119,10 +124,12 @@ def _valid_request(r: Request) -> bool:
         else:
             fleet_ids = frozenset([None])
 
+        initial_instructions: Tuple[DispatchTripInstruction, ...] = tuple()
+
         all_instructions = ft.reduce(
             _solve_assignment,
             fleet_ids,
-            (),
+            initial_instructions,
         )
 
         return self, all_instructions
diff --git a/nrel/hive/dispatcher/instruction_generator/instruction_generator.py b/nrel/hive/dispatcher/instruction_generator/instruction_generator.py
index 55098649..7d397451 100644
--- a/nrel/hive/dispatcher/instruction_generator/instruction_generator.py
+++ b/nrel/hive/dispatcher/instruction_generator/instruction_generator.py
@@ -4,9 +4,7 @@
 from typing import Tuple, TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
     from nrel.hive.dispatcher.instruction.instruction import Instruction
 
diff --git a/nrel/hive/dispatcher/instruction_generator/instruction_generator_ops.py b/nrel/hive/dispatcher/instruction_generator/instruction_generator_ops.py
index 0f3dd9bb..d7ecb0d7 100644
--- a/nrel/hive/dispatcher/instruction_generator/instruction_generator_ops.py
+++ b/nrel/hive/dispatcher/instruction_generator/instruction_generator_ops.py
@@ -2,19 +2,16 @@
 
 import functools as ft
 import random
-from typing import List, Callable
+from typing import List, Callable, NamedTuple
 
 import immutables
 
 from nrel.hive.dispatcher.instruction.instructions import *
 from nrel.hive.dispatcher.instruction_generator import assignment_ops
-from nrel.hive.dispatcher.instruction_generator.charging_search_type import (
-    ChargingSearchType,
-)
+from nrel.hive.dispatcher.instruction_generator.charging_search_type import ChargingSearchType
 from nrel.hive.model.station.station import Station
-from nrel.hive.util.typealiases import GeoId
-from nrel.hive.util import Ratio, DictOps
 from nrel.hive.util.h3_ops import H3Ops
+from nrel.hive.util.dict_ops import DictOps
 from nrel.hive.util.units import Kilometers
 
 log = logging.getLogger(__name__)
@@ -23,19 +20,18 @@
 
 if TYPE_CHECKING:
     from nrel.hive.model.vehicle.vehicle import Vehicle
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
         InstructionGenerator,
     )
-    from nrel.hive.util.typealiases import MembershipId
+    from nrel.hive.util.typealiases import GeoId
+    from nrel.hive.util.units import Ratio
 
 i_map: immutables.Map[VehicleId, List[Instruction]] = immutables.Map()
 
 
 class InstructionGenerationResult(NamedTuple):
-    instruction_stack: immutables.Map[VehicleId, Tuple[Instruction]] = immutables.Map()
+    instruction_stack: immutables.Map[VehicleId, Tuple[Instruction, ...]] = immutables.Map()
     updated_instruction_generators: Tuple[InstructionGenerator, ...] = ()
 
     def apply_instruction_generator(
@@ -145,12 +141,17 @@ def valid_station_for_vehicle(vehicle: Vehicle, env: Environment) -> Callable[[S
     mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
 
     def _inner(station: Station):
+        if mechatronics is None:
+            # TODO: make a safe version of this using returns
+            log.error(f"mechatronics {vehicle.mechatronics_id} not found in environment")
+            return False
+
         vehicle_has_access = station.membership.grant_access_to_membership(vehicle.membership)
         if not vehicle_has_access:
             return False
         else:
             station_has_valid_charger = any(
-                [mechatronics.valid_charger(env.chargers.get(cid)) for cid in station.state.keys()]
+                [mechatronics.valid_charger(env.chargers[cid]) for cid in station.state.keys()]
             )
             return station_has_valid_charger
 
@@ -165,7 +166,7 @@ def instruct_vehicles_to_dispatch_to_station(
     environment: Environment,
     target_soc: Ratio,
     charging_search_type: ChargingSearchType,
-) -> Tuple[Instruction]:
+) -> Tuple[Instruction, ...]:
     """
     a helper function to set n vehicles to charge at a station
 
@@ -179,7 +180,7 @@ def instruct_vehicles_to_dispatch_to_station(
     :return: instructions for vehicles to charge at stations
     """
 
-    instructions = ()
+    instructions: Tuple[Instruction, ...] = ()
 
     for veh in vehicles:
 
@@ -210,26 +211,46 @@ def instruct_vehicles_to_dispatch_to_station(
             is_valid=valid_station_for_vehicle(veh, environment),
             distance_function=distance_fn,
         )
-        if nearest_station:
+        if nearest_station is not None:
+            if not isinstance(nearest_station, Station):
+                log.error(
+                    "got wrong type back from nearest entity search; "
+                    f"expected station but got: {type(nearest_station)}"
+                )
+                continue
             # get the best charger id for this station. re-computes distance ranking one last time
             # this could be removed if our nearest entity search also returned the best charger id
             # these both could return "None" but that shouldn't be possible if we found a nearest station
             if charging_search_type == ChargingSearchType.NEAREST_SHORTEST_QUEUE:
-                (
-                    best_charger_id,
-                    best_charger_rank,
-                ) = assignment_ops.nearest_shortest_queue_ranking(veh, nearest_station, environment)
+                queue_result = assignment_ops.nearest_shortest_queue_ranking(
+                    veh, nearest_station, environment
+                )
+                if queue_result is None:
+                    continue
+                else:
+                    (
+                        best_charger_id,
+                        best_charger_rank,
+                    ) = queue_result
+
             else:  # charging_search_type == ChargingSearchType.SHORTEST_TIME_TO_CHARGE:
-                (
-                    best_charger_id,
-                    best_charger_rank,
-                ) = assignment_ops.shortest_time_to_charge_ranking(
+                time_result = assignment_ops.shortest_time_to_charge_ranking(
                     vehicle=veh,
                     station=nearest_station,
                     sim=simulation_state,
                     env=environment,
                     target_soc=target_soc,
                 )
+                if time_result is None:
+                    continue
+                else:
+                    (
+                        best_charger_id,
+                        best_charger_rank,
+                    ) = time_result
+
+            if best_charger_id is None:
+                continue
 
             instruction = DispatchStationInstruction(
                 vehicle_id=veh.id,
@@ -272,7 +293,7 @@ def get_nearest_valid_station_distance(
     else:  # charging_search_type == ChargingSearchType.SHORTEST_TIME_TO_CHARGE:
         # use the search-based metric which considers travel, queueing, and charging time
 
-        distance_fn, cache = assignment_ops.shortest_time_to_charge_distance(
+        distance_fn = assignment_ops.shortest_time_to_charge_distance(
             vehicle=vehicle,
             sim=simulation_state,
             env=environment,
diff --git a/nrel/hive/external/__init__.py b/nrel/hive/external/__init__.py
deleted file mode 100644
index 690820f8..00000000
--- a/nrel/hive/external/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-__doc__ = """
-a module containing utilities to integrate with external tools, data sources, and libraries 
-"""
diff --git a/nrel/hive/external/demo_base_target/__init__.py b/nrel/hive/external/demo_base_target/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/nrel/hive/external/demo_base_target/fleet_targets.csv b/nrel/hive/external/demo_base_target/fleet_targets.csv
deleted file mode 100644
index b748725a..00000000
--- a/nrel/hive/external/demo_base_target/fleet_targets.csv
+++ /dev/null
@@ -1,146 +0,0 @@
-seconds,base_charging,station_charging,field_vehicles
-0,0,0,0
-600,4,0,0
-1200,8,0,6
-1800,12,0,12
-2400,19,0,19
-3000,19,1,25
-3600,19,1,29
-4200,23,2,29
-4800,23,3,39
-5400,27,5,41
-6000,34,10,51
-6600,41,10,53
-7200,45,10,56
-7800,49,12,64
-8400,53,14,67
-9000,57,14,77
-9600,61,8,80
-10200,65,4,81
-10800,65,2,86
-11400,69,0,92
-12000,73,0,98
-12600,80,3,99
-13200,84,3,101
-13800,91,8,109
-14400,95,8,117
-15000,95,12,118
-15600,99,16,123
-16200,106,21,133
-16800,106,24,139
-17400,106,28,141
-18000,110,29,144
-18600,114,31,146
-19200,114,32,146
-19800,114,36,147
-20400,118,38,147
-21000,118,41,148
-21600,122,45,150
-22200,126,49,150
-22800,126,50,152
-23400,126,54,152
-24000,126,56,153
-24600,126,58,154
-25200,122,58,156
-25800,122,60,155
-26400,122,64,156
-27000,118,66,157
-27600,114,68,157
-28200,110,68,158
-28800,110,68,158
-29400,106,68,158
-30000,99,68,158
-30600,92,68,157
-31200,85,68,158
-31800,78,68,159
-32400,74,65,159
-33000,70,62,161
-33600,66,58,162
-34200,66,54,163
-34800,66,52,164
-35400,62,49,165
-36000,58,48,165
-36600,58,44,167
-37200,58,40,167
-37800,58,37,168
-38400,58,36,170
-39000,58,34,170
-39600,58,31,169
-40200,58,31,169
-40800,58,27,169
-41400,58,23,169
-42000,58,19,169
-42600,58,19,170
-43200,58,17,170
-43800,58,12,171
-44400,58,11,170
-45000,58,11,171
-45600,58,8,172
-46200,58,5,172
-46800,58,4,172
-47400,58,0,174
-48000,65,0,174
-48600,72,0,173
-49200,76,0,174
-49800,83,0,173
-50400,83,0,175
-51000,83,0,176
-51600,87,0,177
-52200,91,0,177
-52800,75,0,176
-53400,60,0,175
-54000,40,0,174
-54600,25,0,173
-55200,0,0,169
-55800,0,0,168
-56400,0,0,163
-57000,0,0,158
-57600,0,0,154
-58200,0,0,149
-58800,0,3,146
-59400,0,7,143
-60000,0,7,139
-60600,0,10,139
-61200,0,11,136
-61800,15,15,135
-62400,20,17,131
-63000,50,18,129
-63600,60,21,128
-64200,120,26,125
-64800,150,31,120
-65400,170,31,115
-66000,177,33,115
-66600,184,34,113
-67200,188,36,112
-67800,195,36,110
-68400,195,36,110
-69000,199,32,106
-69600,203,30,104
-70200,210,28,102
-70800,214,24,98
-71400,218,23,97
-72000,222,23,97
-72600,226,18,92
-73200,230,13,87
-73800,234,10,84
-74400,238,6,80
-75000,242,3,77
-75600,242,0,73
-76200,242,0,71
-76800,246,0,71
-77400,250,0,70
-78000,254,0,67
-78600,261,0,63
-79200,261,0,61
-79800,261,0,60
-80400,261,0,56
-81000,257,0,54
-81600,253,0,49
-82200,249,0,45
-82800,245,0,41
-83400,241,0,36
-84000,237,0,34
-84600,233,0,31
-85200,226,0,31
-85800,222,0,27
-86400,215,0,27
\ No newline at end of file
diff --git a/nrel/hive/external/demo_base_target/fleet_targets.py b/nrel/hive/external/demo_base_target/fleet_targets.py
deleted file mode 100644
index 8e2d0061..00000000
--- a/nrel/hive/external/demo_base_target/fleet_targets.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import os
-
-import numpy as np
-import pandas as pd
-
-from nrel.hive.model.sim_time import SimTime
-
-THIS_DIR = os.path.dirname(os.path.realpath(__file__))
-
-
-class FleetTarget:
-    def __init__(self):
-        fleet_target_path = os.path.join(THIS_DIR, "fleet_targets.csv")
-        df = pd.read_csv(fleet_target_path)
-        self.base_target = pd.Series(index=df.seconds.values, data=df.base_charging.values)
-        self.station_target = pd.Series(index=df.seconds.values, data=df.station_charging.values)
-        self.active_target = pd.Series(index=df.seconds.values, data=df.field_vehicles.values)
-
-    def get_base_target(self, sim_time: SimTime) -> int:
-        if sim_time not in self.base_target:
-            self.base_target.at[sim_time] = np.nan
-            self.base_target = self.base_target.sort_index().interpolate(method="index")
-
-        return self.base_target[sim_time]
-
-    def get_station_target(self, sim_time: SimTime) -> int:
-        if sim_time not in self.station_target:
-            self.station_target.at[sim_time] = np.nan
-            self.station_target = self.station_target.sort_index().interpolate(method="index")
-
-        return self.station_target[sim_time]
-
-    def get_active_target(self, sim_time: SimTime) -> int:
-        if sim_time not in self.active_target:
-            self.active_target.at[sim_time] = np.nan
-            self.active_target = self.active_target.sort_index().interpolate(method="index")
-
-        return self.active_target[sim_time]
diff --git a/nrel/hive/external/miniosmnx/__init__.py b/nrel/hive/external/miniosmnx/__init__.py
deleted file mode 100644
index 368bd2f3..00000000
--- a/nrel/hive/external/miniosmnx/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""
-The MIT License (MIT)
-
-Copyright (c) 2020 Geoff Boeing https://geoffboeing.com/
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-"""
diff --git a/nrel/hive/external/miniosmnx/core.py b/nrel/hive/external/miniosmnx/core.py
deleted file mode 100644
index 7fff69cf..00000000
--- a/nrel/hive/external/miniosmnx/core.py
+++ /dev/null
@@ -1,375 +0,0 @@
-from itertools import groupby
-
-import networkx as nx
-import numpy as np
-import pandas as pd
-
-from .geo_utils import get_largest_component
-from .geo_utils import great_circle_vec
-from .geo_utils import overpass_json_from_file
-
-# useful osm tags - note that load_graphml expects a consistent set of tag names
-# for parsing
-USEFUL_TAGS_NODE = ["ref", "highway"]
-USEFUL_TAGS_PATH = [
-    "bridge",
-    "tunnel",
-    "oneway",
-    "lanes",
-    "ref",
-    "name",
-    "highway",
-    "maxspeed",
-    "service",
-    "access",
-    "area",
-    "landuse",
-    "width",
-    "est_width",
-    "junction",
-]
-# all one-way mode to maintain original OSM node order
-# when constructing graphs specifically to save to .osm xml file
-ALL_ONEWAY = True
-
-# default CRS to set when creating graphs
-DEFAULT_CRS = "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
-
-
-def get_node(element):
-    """
-    Convert an OSM node element into the format for a networkx node.
-
-    Parameters
-    ----------
-    element : dict
-        an OSM node element
-
-    Returns
-    -------
-    dict
-    """
-
-    node = {}
-    node["y"] = element["lat"]
-    node["x"] = element["lon"]
-    node["osmid"] = element["id"]
-    if "tags" in element:
-        for useful_tag in USEFUL_TAGS_NODE:
-            if useful_tag in element["tags"]:
-                node[useful_tag] = element["tags"][useful_tag]
-    return node
-
-
-def get_path(element):
-    """
-    Convert an OSM way element into the format for a networkx graph path.
-
-    Parameters
-    ----------
-    element : dict
-        an OSM way element
-
-    Returns
-    -------
-    dict
-    """
-
-    path = {}
-    path["osmid"] = element["id"]
-
-    # remove any consecutive duplicate elements in the list of nodes
-    grouped_list = groupby(element["nodes"])
-    path["nodes"] = [group[0] for group in grouped_list]
-
-    if "tags" in element:
-        for useful_tag in USEFUL_TAGS_PATH:
-            if useful_tag in element["tags"]:
-                path[useful_tag] = element["tags"][useful_tag]
-    return path
-
-
-def parse_osm_nodes_paths(osm_data):
-    """
-    Construct dicts of nodes and paths with key=osmid and value=dict of
-    attributes.
-
-    Parameters
-    ----------
-    osm_data : dict
-        JSON response from from the Overpass API
-
-    Returns
-    -------
-    nodes, paths : tuple
-    """
-
-    nodes = {}
-    paths = {}
-    for element in osm_data["elements"]:
-        if element["type"] == "node":
-            key = element["id"]
-            nodes[key] = get_node(element)
-        elif element["type"] == "way":  # osm calls network paths 'ways'
-            key = element["id"]
-            paths[key] = get_path(element)
-
-    return nodes, paths
-
-
-def remove_isolated_nodes(G):
-    """
-    Remove from a graph all the nodes that have no incident edges (ie, node
-    degree = 0).
-
-    Parameters
-    ----------
-    G : networkx multidigraph
-        the graph from which to remove nodes
-
-    Returns
-    -------
-    networkx multidigraph
-    """
-
-    isolated_nodes = [node for node, degree in dict(G.degree()).items() if degree < 1]
-    G.remove_nodes_from(isolated_nodes)
-    return G
-
-
-def add_edge_lengths(G):
-    """
-    Add length (meters) attribute to each edge by great circle distance between
-    nodes u and v.
-
-    Parameters
-    ----------
-    G : networkx multidigraph
-
-    Returns
-    -------
-    G : networkx multidigraph
-    """
-
-    # first load all the edges' origin and destination coordinates as a
-    # dataframe indexed by u, v, key
-    coords = np.array(
-        [
-            [
-                u,
-                v,
-                k,
-                G.nodes[u]["y"],
-                G.nodes[u]["x"],
-                G.nodes[v]["y"],
-                G.nodes[v]["x"],
-            ]
-            for u, v, k in G.edges(keys=True)
-        ]
-    )
-    df_coords = pd.DataFrame(coords, columns=["u", "v", "k", "u_y", "u_x", "v_y", "v_x"])
-    df_coords[["u", "v", "k"]] = df_coords[["u", "v", "k"]].astype(np.int64)
-    df_coords = df_coords.set_index(["u", "v", "k"])
-
-    # then calculate the great circle distance with the vectorized function
-    gc_distances = great_circle_vec(
-        lat1=df_coords["u_y"],
-        lng1=df_coords["u_x"],
-        lat2=df_coords["v_y"],
-        lng2=df_coords["v_x"],
-    )
-
-    # fill nulls with zeros and round to the millimeter
-    gc_distances = gc_distances.fillna(value=0).round(3)
-    nx.set_edge_attributes(G, name="length", values=gc_distances.to_dict())
-
-    return G
-
-
-def add_path(G, data, one_way):
-    """
-    Add a path to the graph.
-
-    Parameters
-    ----------
-    G : networkx multidigraph
-    data : dict
-        the attributes of the path
-    one_way : bool
-        if this path is one-way or if it is bi-directional
-
-    Returns
-    -------
-    None
-    """
-
-    # extract the ordered list of nodes from this path element, then delete it
-    # so we don't add it as an attribute to the edge later
-    path_nodes = data["nodes"]
-    del data["nodes"]
-
-    # set the oneway attribute to the passed-in value, to make it consistent
-    # True/False values, but only do this if you aren't forcing all edges to
-    # oneway with the ALL_ONEWAY setting. With the ALL_ONEWAY setting, you
-    # likely still want to preserve the original OSM oneway attribute.
-    if not ALL_ONEWAY:
-        data["oneway"] = one_way
-
-    # zip together the path nodes so you get tuples like (0,1), (1,2), (2,3)
-    # and so on
-    path_edges = list(zip(path_nodes[:-1], path_nodes[1:]))
-    G.add_edges_from(path_edges, **data)
-
-    # if the path is NOT one-way
-    if not one_way:
-        # reverse the direction of each edge and add this path going the
-        # opposite direction
-        path_edges_opposite_direction = [(v, u) for u, v in path_edges]
-        G.add_edges_from(path_edges_opposite_direction, **data)
-
-
-def add_paths(G, paths, bidirectional=False):
-    """
-    Add a collection of paths to the graph.
-
-    Parameters
-    ----------
-    G : networkx multidigraph
-    paths : dict
-        the paths from OSM
-    bidirectional : bool
-        if True, create bidirectional edges for one-way streets
-
-
-    Returns
-    -------
-    None
-    """
-
-    # the list of values OSM uses in its 'oneway' tag to denote True
-    # updated list of of values OSM uses based on https://www.geofabrik.de/de/data/geofabrik-osm-gis-standard-0.7.pdf
-    osm_oneway_values = ["yes", "true", "1", "-1", "T", "F"]
-
-    for data in paths.values():
-
-        if ALL_ONEWAY is True:
-            add_path(G, data, one_way=True)
-        # if this path is tagged as one-way and if it is not a walking network,
-        # then we'll add the path in one direction only
-        elif ("oneway" in data and data["oneway"] in osm_oneway_values) and not bidirectional:
-            if data["oneway"] == "-1" or data["oneway"] == "T":
-                # paths with a one-way value of -1 or T are one-way, but in the
-                # reverse direction of the nodes' order, see osm documentation
-                data["nodes"] = list(reversed(data["nodes"]))
-            # add this path (in only one direction) to the graph
-            add_path(G, data, one_way=True)
-
-        elif ("junction" in data and data["junction"] == "roundabout") and not bidirectional:
-            # roundabout are also oneway but not tagged as is
-            add_path(G, data, one_way=True)
-
-        # else, this path is not tagged as one-way or it is a walking network
-        # (you can walk both directions on a one-way street)
-        else:
-            # add this path (in both directions) to the graph and set its
-            # 'oneway' attribute to False. if this is a walking network, this
-            # may very well be a one-way street (as cars/bikes go), but in a
-            # walking-only network it is a bi-directional edge
-            add_path(G, data, one_way=False)
-
-    return G
-
-
-def create_graph(response_jsons, name="unnamed", retain_all=False, bidirectional=False):
-    """
-    Create a networkx graph from Overpass API HTTP response objects.
-
-    Parameters
-    ----------
-    response_jsons : list
-        list of dicts of JSON responses from from the Overpass API
-    name : string
-        the name of the graph
-    retain_all : bool
-        if True, return the entire graph even if it is not connected
-    bidirectional : bool
-        if True, create bidirectional edges for one-way streets
-
-    Returns
-    -------
-    networkx multidigraph
-    """
-
-    # make sure we got data back from the server requests
-    elements = []
-    for response_json in response_jsons:
-        elements.extend(response_json["elements"])
-    if len(elements) < 1:
-        raise Exception("There are no data elements in the response JSON objects")
-
-    # create the graph as a MultiDiGraph and set the original CRS to DEFAULT_CRS
-    G = nx.MultiDiGraph(name=name, crs=DEFAULT_CRS)
-
-    # extract nodes and paths from the downloaded osm data
-    nodes = {}
-    paths = {}
-    for osm_data in response_jsons:
-        nodes_temp, paths_temp = parse_osm_nodes_paths(osm_data)
-        for key, value in nodes_temp.items():
-            nodes[key] = value
-        for key, value in paths_temp.items():
-            paths[key] = value
-
-    # add each osm node to the graph
-    for node, data in nodes.items():
-        G.add_node(node, **data)
-
-    # add each osm way (aka, path) to the graph
-    G = add_paths(G, paths, bidirectional=bidirectional)
-
-    # retain only the largest connected component, if caller did not
-    # set retain_all=True
-    if not retain_all:
-        G = get_largest_component(G)
-
-    # add length (great circle distance between nodes) attribute to each edge to
-    # use as weight
-    if len(G.edges) > 0:
-        G = add_edge_lengths(G)
-
-    return G
-
-
-def graph_from_file(filename, bidirectional=False, retain_all=False, name="unnamed"):
-    """
-    Create a networkx graph from OSM data in an XML file.
-
-    Parameters
-    ----------
-    filename : string
-        the name of a file containing OSM XML data
-    bidirectional : bool
-        if True, create bidirectional edges for one-way streets
-    simplify : bool
-        if True, simplify the graph topology
-    retain_all : bool
-        if True, return the entire graph even if it is not connected
-    name : string
-        the name of the graph
-
-    Returns
-    -------
-    networkx multidigraph
-    """
-    # transmogrify file of OSM XML data into JSON
-    response_jsons = [overpass_json_from_file(filename)]
-
-    # create graph using this response JSON
-    G = create_graph(
-        response_jsons,
-        bidirectional=bidirectional,
-        retain_all=retain_all,
-        name=name,
-    )
-
-    return G
diff --git a/nrel/hive/external/miniosmnx/geo_utils.py b/nrel/hive/external/miniosmnx/geo_utils.py
deleted file mode 100644
index 2d4982d2..00000000
--- a/nrel/hive/external/miniosmnx/geo_utils.py
+++ /dev/null
@@ -1,199 +0,0 @@
-import os
-import xml.sax
-
-import networkx as nx
-import numpy as np
-
-
-def great_circle_vec(lat1, lng1, lat2, lng2, earth_radius=6371009):
-    """
-    Vectorized function to calculate the great-circle distance between two
-    points or between vectors of points, using haversine.
-
-    Parameters
-    ----------
-    lat1 : float or array of float
-    lng1 : float or array of float
-    lat2 : float or array of float
-    lng2 : float or array of float
-    earth_radius : numeric
-        radius of earth in units in which distance will be returned (default is
-        meters)
-
-    Returns
-    -------
-    distance : float or vector of floats
-        distance or vector of distances from (lat1, lng1) to (lat2, lng2) in
-        units of earth_radius
-    """
-
-    phi1 = np.deg2rad(lat1)
-    phi2 = np.deg2rad(lat2)
-    d_phi = phi2 - phi1
-
-    theta1 = np.deg2rad(lng1)
-    theta2 = np.deg2rad(lng2)
-    d_theta = theta2 - theta1
-
-    h = np.sin(d_phi / 2) ** 2 + np.cos(phi1) * np.cos(phi2) * np.sin(d_theta / 2) ** 2
-    h = np.minimum(1.0, h)  # protect against floating point errors
-
-    arc = 2 * np.arcsin(np.sqrt(h))
-
-    # return distance in units of earth_radius
-    distance = arc * earth_radius
-    return distance
-
-
-class OSMContentHandler(xml.sax.handler.ContentHandler):
-    """
-    SAX content handler for OSM XML.
-
-    Used to build an Overpass-like response JSON object in self.object. For format
-    notes, see http://wiki.openstreetmap.org/wiki/OSM_XML#OSM_XML_file_format_notes
-    and http://overpass-api.de/output_formats.html#json
-    """
-
-    def __init__(self):
-        self._element = None
-        self.object = {"elements": []}
-
-    def startElement(self, name, attrs):
-        if name == "osm":
-            self.object.update({k: attrs[k] for k in attrs.keys() if k in ("version", "generator")})
-
-        elif name in ("node", "way"):
-            self._element = dict(type=name, tags={}, nodes=[], **attrs)
-            self._element.update({k: float(attrs[k]) for k in attrs.keys() if k in ("lat", "lon")})
-            self._element.update(
-                {
-                    k: int(attrs[k])
-                    for k in attrs.keys()
-                    if k in ("id", "uid", "version", "changeset")
-                }
-            )
-
-        elif name == "tag":
-            self._element["tags"].update({attrs["k"]: attrs["v"]})
-
-        elif name == "nd":
-            self._element["nodes"].append(int(attrs["ref"]))
-
-        elif name == "relation":
-            # Placeholder for future relation support.
-            # Look for nested members and tags.
-            pass
-
-    def endElement(self, name):
-        if name in ("node", "way"):
-            self.object["elements"].append(self._element)
-
-
-def induce_subgraph(G, node_subset):
-    """
-    Induce a subgraph of G.
-
-    Parameters
-    ----------
-    G : networkx multidigraph
-    node_subset : list-like
-        the subset of nodes to induce a subgraph of G
-
-    Returns
-    -------
-    G2 : networkx multidigraph
-        the subgraph of G induced by node_subset
-    """
-
-    node_subset = set(node_subset)
-
-    # copy nodes into new graph
-    G2 = G.__class__()
-    G2.add_nodes_from((n, G.nodes[n]) for n in node_subset)
-
-    # copy edges to new graph, including parallel edges
-    if G2.is_multigraph:
-        G2.add_edges_from(
-            (n, nbr, key, d)
-            for n, nbrs in G.adj.items()
-            if n in node_subset
-            for nbr, keydict in nbrs.items()
-            if nbr in node_subset
-            for key, d in keydict.items()
-        )
-    else:
-        G2.add_edges_from(
-            (n, nbr, d)
-            for n, nbrs in G.adj.items()
-            if n in node_subset
-            for nbr, d in nbrs.items()
-            if nbr in node_subset
-        )
-
-    # update graph attribute dict, and return graph
-    G2.graph.update(G.graph)
-    return G2
-
-
-def get_largest_component(G, strongly=False):
-    """
-    Return a subgraph of the largest weakly or strongly connected component
-    from a directed graph.
-
-    Parameters
-    ----------
-    G : networkx multidigraph
-    strongly : bool
-        if True, return the largest strongly instead of weakly connected
-        component
-
-    Returns
-    -------
-    G : networkx multidigraph
-        the largest connected component subgraph from the original graph
-    """
-
-    if strongly:
-        # if the graph is not connected retain only the largest strongly connected component
-        if not nx.is_strongly_connected(G):
-            # get all the strongly connected components in graph then identify the largest
-            sccs = nx.strongly_connected_components(G)
-            largest_scc = max(sccs, key=len)
-            G = induce_subgraph(G, largest_scc)
-
-    else:
-        # if the graph is not connected retain only the largest weakly connected component
-        if not nx.is_weakly_connected(G):
-            # get all the weakly connected components in graph then identify the largest
-            wccs = nx.weakly_connected_components(G)
-            largest_wcc = max(wccs, key=len)
-            G = induce_subgraph(G, largest_wcc)
-
-    return G
-
-
-def overpass_json_from_file(filename):
-    """
-    Read OSM XML from input_config filename and return Overpass-like JSON.
-
-    Parameters
-    ----------
-    filename : string
-        name of file containing OSM XML data
-
-    Returns
-    -------
-    OSMContentHandler object
-    """
-
-    _, ext = os.path.splitext(filename)
-
-    if ext != ".xml":
-        raise NotImplementedError("only .xml files are supported")
-    else:
-        opener = lambda fn: open(fn, mode="rb")
-
-    with opener(filename) as file:
-        handler = OSMContentHandler()
-        xml.sax.parse(file, handler)
-        return handler.object
diff --git a/nrel/hive/external/nyc_tlc/__init__.py b/nrel/hive/external/nyc_tlc/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/nrel/hive/external/nyc_tlc/nyc_tlc_parsers.py b/nrel/hive/external/nyc_tlc/nyc_tlc_parsers.py
deleted file mode 100644
index 59f3f6ae..00000000
--- a/nrel/hive/external/nyc_tlc/nyc_tlc_parsers.py
+++ /dev/null
@@ -1,87 +0,0 @@
-from datetime import datetime
-from typing import Dict, Union
-
-import h3
-
-from nrel.hive.model.passenger import Passenger, create_passenger_id
-from nrel.hive.model.request import Request
-from nrel.hive.model.roadnetwork.link import Link
-
-
-def parse_yellow_tripdata_row(
-    row: Dict[str, str],
-    id_number: int,
-    cancel_time: int,
-    sim_h3_location_resolution: int,
-    default_passengers: int = 1,
-    use_date_in_request_id: bool = False,
-) -> Union[Exception, Request]:
-    """
-    takes a row (via a DictReader) from a Yellow Cab Taxi Company data source and
-    converts it to a Request, unless it is missing a required field.
-
-    it uses provided id_number (and optionally the date) to construct an id, and
-    uses the time to construct a departure time (dropping the notion of "day",
-    so that all requests occur on the same day). passengers default to 1.
-
-    see https://www1.nyc.gov/site/tlc/about/tlc-trip-record-data.page
-
-
-    :param row: a row from a TLC Trip Record file
-
-    :param id_number: a unique number to add to this request's name
-
-    :param cancel_time: the duration_seconds that the request will be active, in seconds, before it cancels
-
-    :param sim_h3_location_resolution: the h3 spatial resolution requests are stored at
-
-    :param default_passengers: the fill value to use when passengers is missing from the row
-
-    :param use_date_in_request_id: if True, adds the date as part of the Request's name,
-    to make it easier to trace back to the source data, at the cost of a larger (unnecessary) memory footprint
-    :return: a Request, or an exception if row is invalid
-    """
-    try:
-        # time
-        date_time = datetime.strptime(row["pickup_datetime"], "%Y-%m-%d %H:%M:%S")
-        start_of_day = date_time.replace(hour=0, minute=0, second=0, microsecond=0)
-        time_diff = date_time - start_of_day
-        departure_time = time_diff.seconds
-        cancel_time = min(departure_time + cancel_time, 86399)  # 11:59:59
-
-        # agent id
-        agent_id = f"{id_number}#{date_time.date()}" if use_date_in_request_id else id_number
-
-        # locations
-        o_lat, o_lon = float(row["pickup_latitude"]), float(row["pickup_longitude"])
-        d_lat, d_lon = float(row["dropoff_latitude"]), float(row["dropoff_longitude"])
-        origin = h3.geo_to_h3(o_lat, o_lon, sim_h3_location_resolution)
-        destination = h3.geo_to_h3(d_lat, d_lon, sim_h3_location_resolution)
-
-        origin_link = Link("o", origin, origin, 0, 0)
-        destination_link = Link("d", destination, destination, 0, 0)
-
-        # passengers (
-        passengers = int(row["passengers"]) if "passengers" in row else default_passengers
-
-        request_as_passengers = [
-            Passenger(
-                create_passenger_id(agent_id, pass_idx),
-                origin_link.start,
-                destination_link.end,
-                departure_time,
-            )
-            for pass_idx in range(0, passengers)
-        ]
-
-        return Request(
-            id=agent_id,
-            origin_link=origin_link,
-            destination_link=destination_link,
-            departure_time=departure_time,
-            cancel_time=cancel_time,
-            passengers=tuple(request_as_passengers),
-        )
-
-    except Exception as e:
-        return e
diff --git a/nrel/hive/external/nyc_tlc/nyc_tlc_sampler.py b/nrel/hive/external/nyc_tlc/nyc_tlc_sampler.py
deleted file mode 100644
index 47d7774d..00000000
--- a/nrel/hive/external/nyc_tlc/nyc_tlc_sampler.py
+++ /dev/null
@@ -1,180 +0,0 @@
-import json
-import random
-from csv import DictReader, DictWriter
-
-import h3
-from pkg_resources import resource_filename
-
-from nrel.hive.external.nyc_tlc.nyc_tlc_parsers import (
-    parse_yellow_tripdata_row,
-)
-from nrel.hive.util.units import KwH, Kw
-
-
-def down_sample_nyc_tlc_data(
-    in_file: str,
-    out_file: str,
-    sample_size: int,
-    request_cancel_time_buffer: int = 300,
-    sim_h3_location_resolution: int = 15,
-    boundary_h3_resolution: int = 10,
-):
-    """
-    down-samples in input_config TLC data file for Yellow cab data
-
-
-    :param in_file: the source TLC data file
-
-    :param out_file: a file in HIVE Request data format
-
-    :param sample_size: number of agents to sample - performs no randomization
-
-    :param request_cancel_time_buffer: the cancel time to set on each agent
-
-    :param sim_h3_location_resolution:
-
-    :param boundary_h3_resolution: the resolution for the bounding set of the nyc polygon
-    :return:
-    """
-
-    # this could clearly be generalized for other polygons/request sets
-    with open(resource_filename("nrel.hive.resources.geofence", "nyc_single_polygon.geojson")) as f:
-
-        # needs to be a Polygon, not a MultiPolygon feature
-        # used for ETL to confirm that requests sampled are in fact within the study area (some are not)
-        geojson = json.load(f)
-        hexes = h3.polyfill(
-            geojson=geojson["geometry"],
-            res=boundary_h3_resolution,
-            geo_json_conformant=True,
-        )
-
-        with open(in_file) as f:
-            with open(out_file, "w", newline="") as w:
-                reader = DictReader(f)
-                header = [
-                    "request_id",
-                    "o_lat",
-                    "o_lon",
-                    "d_lat",
-                    "d_lon",
-                    "departure_time",
-                    "passengers",
-                ]
-
-                writer = DictWriter(w, header)
-                writer.writeheader()
-
-                # while loop state
-                attempted_count = 0
-                absolute_cutoff = sample_size * 2  # arbitrary cutoff here; errors should be few
-                recorded_count = 0
-
-                while recorded_count < sample_size and attempted_count < absolute_cutoff:
-                    # parse the next row of data
-                    row = next(reader)
-                    req = parse_yellow_tripdata_row(
-                        row,
-                        recorded_count,
-                        request_cancel_time_buffer,
-                        sim_h3_location_resolution,
-                    )
-
-                    if (
-                        not isinstance(req, Exception)
-                        and h3.h3_to_parent(req.geoid, boundary_h3_resolution) in hexes
-                    ):
-                        # request_id,o_lat,o_lon,d_lat,d_lon,departure_time,cancel_time,passengers
-                        out_row = {
-                            "request_id": req.id,
-                            "o_lat": row["pickup_latitude"],
-                            "o_lon": row["pickup_longitude"],
-                            "d_lat": row["dropoff_latitude"],
-                            "d_lon": row["dropoff_longitude"],
-                            "departure_time": req.departure_time,
-                            "passengers": len(req.passengers),
-                        }
-                        writer.writerow(out_row)
-                        recorded_count += 1
-                    else:
-                        print(f"row for id {recorded_count} failed: {row}")
-
-    if attempted_count == absolute_cutoff:
-        raise IOError(
-            f"too many errors, input_config file {in_file} (and corresponding file {out_file}) may be corrupt"
-        )
-
-
-def sample_vehicles_in_geofence(
-    num: int,
-    out_file: str,
-    powertrain_id: str,
-    powercurve_id: str,
-    capacity: KwH,
-    initial_soc: float,
-    ideal_energy_limit: KwH,
-    max_charge_acceptance_kw: Kw,
-):
-    """
-    samples points in the NYC polygon and creates Vehicles from them, writing to an output file
-
-    :param num: number of vehicles
-
-    :param out_file: the file to write the vehicles
-
-    :param powertrain_id:
-
-    :param powercurve_id:
-
-    :param capacity:
-
-    :param initial_soc:
-
-    :param ideal_energy_limit:
-
-    :param max_charge_acceptance_kw:
-    """
-
-    # load geojson file with boundary polygon
-    with open(resource_filename("nrel.hive.resources.geofence", "nyc_single_polygon.geojson")) as f:
-
-        # needs to be a Polygon, not a MultiPolygon feature
-        geojson = json.load(f)
-        hexes = list(h3.polyfill(geojson=geojson["geometry"], res=10, geo_json_conformant=True))
-
-        with open(out_file, "w", newline="") as w:
-            header = [
-                "vehicle_id",
-                "lat",
-                "lon",
-                "powertrain_id",
-                "powercurve_id",
-                "capacity",
-                "ideal_energy_limit",
-                "max_charge_acceptance",
-                "initial_soc",
-            ]
-
-            writer = DictWriter(w, header)
-            writer.writeheader()
-            for i in range(0, num):
-                vehicle_id = f"v{i}"
-
-                random_upper_hex = random.choice(hexes)
-                children = list(h3.h3_to_children(random_upper_hex, 15))
-                random_lower_hex = random.choice(children)
-                lat, lon = h3.h3_to_geo(random_lower_hex)
-
-                row = {
-                    "vehicle_id": vehicle_id,
-                    "lat": lat,
-                    "lon": lon,
-                    "powertrain_id": powertrain_id,
-                    "powercurve_id": powercurve_id,
-                    "capacity": capacity,
-                    "initial_soc": initial_soc,
-                    "ideal_energy_limit": ideal_energy_limit,
-                    "max_charge_acceptance": max_charge_acceptance_kw,
-                }
-
-                writer.writerow(row)
diff --git a/nrel/hive/initialization/initialize_ops.py b/nrel/hive/initialization/initialize_ops.py
index b4348ac1..a64b8260 100644
--- a/nrel/hive/initialization/initialize_ops.py
+++ b/nrel/hive/initialization/initialize_ops.py
@@ -17,7 +17,7 @@ def process_fleet_file(fleet_file: str, entity_type: str) -> MembershipMap:
     :return: an immutable map mapping all entity ids with the appropriate memberships
     :raises Exception: from KEYErrors parsing the fleets file
     """
-    fleet_id_map = immutables.Map()
+    fleet_id_map: MembershipMap = immutables.Map()
     fleet_id_map_mutation = fleet_id_map.mutate()
 
     with open(fleet_file) as f:
diff --git a/nrel/hive/initialization/initialize_simulation.py b/nrel/hive/initialization/initialize_simulation.py
index 19963907..600ce35f 100644
--- a/nrel/hive/initialization/initialize_simulation.py
+++ b/nrel/hive/initialization/initialize_simulation.py
@@ -8,8 +8,6 @@
 
 import immutables
 
-from returns.result import ResultE, Success, Failure
-
 from nrel.hive.config import HiveConfig
 from nrel.hive.initialization.initialize_ops import (
     process_fleet_file,
@@ -18,9 +16,7 @@
 from nrel.hive.model.base import Base
 from nrel.hive.model.energy.charger import build_chargers_table
 from nrel.hive.model.roadnetwork.geofence import GeoFence
-from nrel.hive.model.roadnetwork.haversine_roadnetwork import (
-    HaversineRoadNetwork,
-)
+from nrel.hive.model.roadnetwork.haversine_roadnetwork import HaversineRoadNetwork
 from nrel.hive.model.roadnetwork.osm.osm_roadnetwork import OSMRoadNetwork
 from nrel.hive.model.station.station import Station
 from nrel.hive.model.vehicle.mechatronics import build_mechatronics_table
@@ -29,11 +25,11 @@
 from nrel.hive.runner.environment import Environment
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.util import DictOps
-from nrel.hive.util.fp import throw_or_return
+from nrel.hive.util.dict_ops import DictOps
 
 if TYPE_CHECKING:
-    from nrel.hive.util.typealiases import MembershipMap
+    from nrel.hive.util.typealiases import MembershipMap, ScheduleId
+    from nrel.hive.model.vehicle.schedules import ScheduleFunction
 
 log = logging.getLogger(__name__)
 
@@ -68,7 +64,10 @@ def initialize_simulation(
             geofence=geofence, sim_h3_resolution=config.sim.sim_h3_resolution
         )
     elif config.network.network_type == "osm_network":
-        road_network = OSMRoadNetwork(
+        if config.input_config.road_network_file is None:
+            raise IOError("Must supply a road network file when using the osm_network")
+
+        road_network = OSMRoadNetwork(  # type: ignore
             geofence=geofence,
             sim_h3_resolution=config.sim.sim_h3_resolution,
             road_network_file=Path(config.input_config.road_network_file),
@@ -92,7 +91,7 @@ def initialize_simulation(
     fleet_ids = (
         read_fleet_ids_from_file(config.input_config.fleets_file)
         if config.input_config.fleets_file
-        else []
+        else frozenset()
     )
 
     # read in fleet memberships for vehicles/stations/bases
@@ -112,6 +111,13 @@ def initialize_simulation(
         else None
     )
 
+    if config.input_config.schedules_file is None:
+        schedules: immutables.Map[ScheduleId, ScheduleFunction] = immutables.Map()
+    else:
+        schedules = build_schedules_table(
+            config.sim.schedule_type, config.input_config.schedules_file
+        )
+
     env_initial = Environment(
         config=config,
         mechatronics=build_mechatronics_table(
@@ -119,9 +125,7 @@ def initialize_simulation(
             config.input_config.scenario_directory,
         ),
         chargers=build_chargers_table(config.input_config.chargers_file),
-        schedules=build_schedules_table(
-            config.sim.schedule_type, config.input_config.schedules_file
-        ),
+        schedules=schedules,
         fleet_ids=fleet_ids,
     )
 
@@ -153,7 +157,7 @@ def initialize_simulation(
 
 def _build_vehicles(
     vehicles_file: str,
-    vehicle_member_ids: MembershipMap,
+    vehicle_member_ids: Optional[MembershipMap],
     simulation_state: SimulationState,
     environment: Environment,
     vehicle_filter: Callable[[Vehicle], bool],
@@ -187,12 +191,8 @@ def _collect_vehicle(row: Dict[str, str]) -> Optional[Vehicle]:
     # open vehicles file and add each row
     with open(vehicles_file, "r", encoding="utf-8-sig") as vf:
         reader = csv.DictReader(vf)
-        vehicles = list(
-            filter(
-                lambda v: v is not None,
-                [_collect_vehicle(row) for row in reader],
-            )
-        )
+        vehicles_or_none = [_collect_vehicle(row) for row in reader]
+        vehicles = [v for v in vehicles_or_none if v is not None]
         sim_with_vehicles = simulation_state_ops.add_entities(simulation_state, vehicles)
 
     return sim_with_vehicles
@@ -200,7 +200,7 @@ def _collect_vehicle(row: Dict[str, str]) -> Optional[Vehicle]:
 
 def _build_bases(
     bases_file: str,
-    base_member_ids: MembershipMap,
+    base_member_ids: Optional[MembershipMap],
     simulation_state: SimulationState,
     base_filter: Callable[[Base], bool],
 ) -> SimulationState:
@@ -229,7 +229,8 @@ def _collect_base(row: Dict[str, str]) -> Optional[Base]:
     # add all bases from the base file
     with open(bases_file, "r", encoding="utf-8-sig") as bf:
         reader = csv.DictReader(bf)
-        bases = list(filter(lambda b: b is not None, [_collect_base(row) for row in reader]))
+        bases_or_none = [_collect_base(row) for row in reader]
+        bases = [b for b in bases_or_none if b is not None]
 
     sim_w_bases = simulation_state_ops.add_entities(simulation_state, bases)
 
@@ -261,23 +262,28 @@ def _find_human_drivers(acc: SimulationState, v: Vehicle) -> SimulationState:
                 home_base_membership_id = f"{v.id}_private_{home_base_id}"
                 updated_v = v.add_membership(home_base_membership_id)
                 updated_b = home_base.add_membership(home_base_membership_id)
-                station = sim.stations.get(home_base.station_id)
-                updated_s = station.add_membership(home_base_membership_id) if station else None
 
                 error_v, with_v = simulation_state_ops.modify_vehicle(acc, updated_v)
                 if error_v:
                     log.error(error_v)
                     return acc
+                elif with_v is None:
+                    return acc
                 else:
                     error_b, with_b = simulation_state_ops.modify_base(with_v, updated_b)
                     if error_b:
                         log.error(error_b)
                         return acc
+                    elif with_b is None:
+                        return acc
                     else:
-                        # bases are not required to have stations (they are optional)
-                        if not station:
+                        if home_base.station_id is None:
                             return with_b
                         else:
+                            station = sim.stations.get(home_base.station_id)
+                            if station is None:
+                                return with_b
+                            updated_s = station.add_membership(home_base_membership_id)
                             (
                                 error_s,
                                 with_s,
@@ -285,6 +291,8 @@ def _find_human_drivers(acc: SimulationState, v: Vehicle) -> SimulationState:
                             if error_s:
                                 log.error(error_s)
                                 return acc
+                            elif with_s is None:
+                                return acc
                             else:
                                 return with_s
 
@@ -294,7 +302,7 @@ def _find_human_drivers(acc: SimulationState, v: Vehicle) -> SimulationState:
 
 def _build_stations(
     stations_file: str,
-    station_member_ids: MembershipMap,
+    station_member_ids: Optional[MembershipMap],
     simulation_state: SimulationState,
     station_filter: Callable[[Station], bool],
     env: Environment,
diff --git a/nrel/hive/initialization/initialize_simulation_with_sampling.py b/nrel/hive/initialization/initialize_simulation_with_sampling.py
index 318c0a5e..8856b1b6 100644
--- a/nrel/hive/initialization/initialize_simulation_with_sampling.py
+++ b/nrel/hive/initialization/initialize_simulation_with_sampling.py
@@ -18,13 +18,11 @@
 from nrel.hive.model.energy.charger import build_chargers_table
 from nrel.hive.model.roadnetwork.link import Link
 from nrel.hive.model.roadnetwork.geofence import GeoFence
-from nrel.hive.model.roadnetwork.haversine_roadnetwork import (
-    HaversineRoadNetwork,
-)
+from nrel.hive.model.roadnetwork.haversine_roadnetwork import HaversineRoadNetwork
 from nrel.hive.model.roadnetwork.osm.osm_roadnetwork import OSMRoadNetwork
 from nrel.hive.model.station.station import Station
 from nrel.hive.model.vehicle.mechatronics import build_mechatronics_table
-from nrel.hive.model.vehicle.schedules import build_schedules_table
+from nrel.hive.model.vehicle.schedules import build_schedules_table, ScheduleId, ScheduleFunction
 from nrel.hive.runner.environment import Environment
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
@@ -63,30 +61,38 @@ def initialize_simulation_with_sampling(
 
     # set up road network based on user-configured road network type
     if config.network.network_type == "euclidean":
-        road_network = HaversineRoadNetwork(
+        haversine_road_network = HaversineRoadNetwork(
             geofence=geofence, sim_h3_resolution=config.sim.sim_h3_resolution
         )
+        sim_initial = SimulationState(
+            road_network=haversine_road_network,
+            sim_time=config.sim.start_time,
+            sim_timestep_duration_seconds=config.sim.timestep_duration_seconds,
+            sim_h3_location_resolution=config.sim.sim_h3_resolution,
+            sim_h3_search_resolution=config.sim.sim_h3_search_resolution,
+        )
     elif config.network.network_type == "osm_network":
-        road_network = OSMRoadNetwork(
+        if config.input_config.road_network_file is None:
+            raise ValueError("road_network_file required for osm_network")
+
+        osm_road_network = OSMRoadNetwork(
             geofence=geofence,
             sim_h3_resolution=config.sim.sim_h3_resolution,
             road_network_file=Path(config.input_config.road_network_file),
             default_speed_kmph=config.network.default_speed_kmph,
         )
+        sim_initial = SimulationState(
+            road_network=osm_road_network,
+            sim_time=config.sim.start_time,
+            sim_timestep_duration_seconds=config.sim.timestep_duration_seconds,
+            sim_h3_location_resolution=config.sim.sim_h3_resolution,
+            sim_h3_search_resolution=config.sim.sim_h3_search_resolution,
+        )
     else:
         raise IOError(
             f"road network type {config.network.network_type} not valid, must be one of {{euclidean|osm_network}}"
         )
 
-    # initial sim state with road network and no entities
-    sim_initial = SimulationState(
-        road_network=road_network,
-        sim_time=config.sim.start_time,
-        sim_timestep_duration_seconds=config.sim.timestep_duration_seconds,
-        sim_h3_location_resolution=config.sim.sim_h3_resolution,
-        sim_h3_search_resolution=config.sim.sim_h3_search_resolution,
-    )
-
     # create simulation environment
     if config.input_config.fleets_file:
         log.warning(
@@ -94,6 +100,13 @@ def initialize_simulation_with_sampling(
             "this input will be ignored and entities will not have any fleet information."
         )
 
+    if config.input_config.schedules_file is None:
+        schedules: immutables.Map[ScheduleId, ScheduleFunction] = immutables.Map()
+    else:
+        schedules = build_schedules_table(
+            config.sim.schedule_type, config.input_config.schedules_file
+        )
+
     env = Environment(
         config=config,
         mechatronics=build_mechatronics_table(
@@ -101,9 +114,7 @@ def initialize_simulation_with_sampling(
             config.input_config.scenario_directory,
         ),
         chargers=build_chargers_table(config.input_config.chargers_file),
-        schedules=build_schedules_table(
-            config.sim.schedule_type, config.input_config.schedules_file
-        ),
+        schedules=schedules,
     )
 
     # populate simulation with static entities
diff --git a/nrel/hive/initialization/load.py b/nrel/hive/initialization/load.py
index 9a299c2a..e833eb40 100644
--- a/nrel/hive/initialization/load.py
+++ b/nrel/hive/initialization/load.py
@@ -10,14 +10,10 @@
 from nrel.hive.reporting.handler.instruction_handler import InstructionHandler
 from nrel.hive.reporting.handler.stateful_handler import StatefulHandler
 from nrel.hive.reporting.handler.stats_handler import StatsHandler
-from nrel.hive.reporting.handler.time_step_stats_handler import (
-    TimeStepStatsHandler,
-)
+from nrel.hive.reporting.handler.time_step_stats_handler import TimeStepStatsHandler
 from nrel.hive.reporting.reporter import Reporter
 from nrel.hive.runner.environment import Environment
-from nrel.hive.initialization.initialize_simulation import (
-    initialize_simulation,
-)
+from nrel.hive.initialization.initialize_simulation import initialize_simulation
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 run_log = logging.getLogger(__name__)
@@ -37,11 +33,12 @@ def load_simulation(
     with scenario_file_path.open("r") as f:
         config_builder = yaml.safe_load(f)
 
-    try:
-        config = HiveConfig.build(scenario_file_path, config_builder)
-    except Exception as e:
+    config_or_error = HiveConfig.build(scenario_file_path, config_builder)
+    if isinstance(config_or_error, Exception):
         run_log.exception("attempted to load scenario config file but failed")
-        raise e
+        raise config_or_error
+    else:
+        config: HiveConfig = config_or_error
 
     if config.global_config.write_outputs:
         config.scenario_output_directory.mkdir()
diff --git a/nrel/hive/initialization/sample_requests.py b/nrel/hive/initialization/sample_requests.py
index 9eb564bb..b74f908e 100644
--- a/nrel/hive/initialization/sample_requests.py
+++ b/nrel/hive/initialization/sample_requests.py
@@ -1,7 +1,8 @@
 import random
-from typing import Tuple
+from typing import Tuple, List
 
 from nrel.hive.model.request import Request
+from nrel.hive.model.sim_time import SimTime
 from nrel.hive.runner import Environment
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
 from nrel.hive.model.roadnetwork.osm.osm_roadnetwork import OSMRoadNetwork
@@ -27,9 +28,12 @@ def default_request_sampler(
     if not isinstance(simulation_state.road_network, OSMRoadNetwork):
         raise NotImplementedError("request sampling is only implemented for the OSMRoadNetwork")
 
+    if simulation_state.road_network.link_helper is None:
+        raise Exception("Expected link helper on OSMRoadNetwork but found None")
+
     random.seed(random_seed)
 
-    requests = []
+    requests: List[Request] = []
 
     possible_timesteps = list(
         range(
@@ -54,7 +58,7 @@ def default_request_sampler(
             origin=random_source_link.start,
             destination=random_destination_link.end,
             road_network=simulation_state.road_network,
-            departure_time=random.choice(possible_timesteps),
+            departure_time=SimTime(random.choice(possible_timesteps)),
             passengers=random.choice([1, 2, 3, 4]),
             allows_pooling=allow_pooling,
         )
diff --git a/nrel/hive/initialization/sample_vehicles.py b/nrel/hive/initialization/sample_vehicles.py
index 58e6f0d3..6df9416a 100644
--- a/nrel/hive/initialization/sample_vehicles.py
+++ b/nrel/hive/initialization/sample_vehicles.py
@@ -9,6 +9,7 @@
 from nrel.hive.model.roadnetwork.link import Link
 from nrel.hive.model.roadnetwork.osm.osm_roadnetwork import OSMRoadNetwork
 from nrel.hive.model.vehicle.vehicle import Vehicle
+from nrel.hive.model.membership import Membership
 from nrel.hive.runner import Environment
 from nrel.hive.state.driver_state.autonomous_driver_state.autonomous_available import (
     AutonomousAvailable,
@@ -17,9 +18,7 @@
     AutonomousDriverAttributes,
 )
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.simulation_state.simulation_state_ops import (
-    add_vehicle_safe,
-)
+from nrel.hive.state.simulation_state.simulation_state_ops import add_vehicle_safe
 from nrel.hive.state.vehicle_state.idle import Idle
 from nrel.hive.util import Ratio
 
@@ -70,6 +69,8 @@ def _inner(
                 :param s: the SimulationState to update
                 :return: the updated simulation state, or, an exception
                 """
+                if not mechatronics:
+                    return Failure(KeyError(f"mechatronics with id {mechatronics_id} not found"))
                 try:
                     vehicle_id = f"v{i}"
                     initial_soc = soc_sampling_function()
@@ -86,6 +87,7 @@ def _inner(
                         vehicle_state=vehicle_state,
                         driver_state=driver_state,
                         total_seats=total_seats,
+                        membership=Membership(),
                     )
                     add_result = add_vehicle_safe(s, vehicle)
                     return add_result
@@ -109,7 +111,7 @@ def _inner(
         return result
 
 
-def build_default_location_sampling_fn(seed: int = 0) -> Callable[[], Link]:
+def build_default_location_sampling_fn(seed: int = 0) -> Callable[[SimulationState], Link]:
     """
     constructs a link sampling function that uniformly samples from the provided base locations
 
@@ -125,6 +127,10 @@ def _inner(sim: SimulationState) -> Link:
             raise NotImplementedError(
                 f"this sampling function is only implemented for the OSMRoadNetwork"
             )
+
+        if sim.road_network.link_helper is None:
+            raise Exception("Expected link helper on OSMRoadNetwork but found None")
+
         links = list(sim.road_network.link_helper.links.values())
         if len(links) == 0:
             raise AssertionError(f"must have at least one link to sample from")
diff --git a/nrel/hive/model/base.py b/nrel/hive/model/base.py
index 81450ed2..601f02e4 100644
--- a/nrel/hive/model/base.py
+++ b/nrel/hive/model/base.py
@@ -1,17 +1,22 @@
 from __future__ import annotations
 
-from typing import Optional, NamedTuple, Dict
+from typing import Optional, Dict, TYPE_CHECKING
+from dataclasses import dataclass, replace
 
 import h3
 
+from nrel.hive.model.entity import Entity
 from nrel.hive.model.entity_position import EntityPosition
 from nrel.hive.model.membership import Membership
 from nrel.hive.model.roadnetwork.roadnetwork import RoadNetwork
 from nrel.hive.util.exception import SimulationStateError
-from nrel.hive.util.typealiases import *
 
+if TYPE_CHECKING:
+    from nrel.hive.util.typealiases import *
 
-class Base(NamedTuple):
+
+@dataclass(frozen=True)
+class Base(Entity):
     """
     Represents a base within the simulation.
 
@@ -34,12 +39,12 @@ class Base(NamedTuple):
 
     id: BaseId
     position: EntityPosition
+    membership: Membership
+
     total_stalls: int
     available_stalls: int
     station_id: Optional[StationId]
 
-    membership: Membership = Membership
-
     @property
     def geoid(self):
         return self.position.geoid
@@ -56,7 +61,17 @@ def build(
     ):
 
         position = road_network.position_from_geoid(geoid)
-        return Base(id, position, stall_count, stall_count, station_id, membership)
+        if position is None:
+            raise ValueError("cannot position base on road network")
+
+        return Base(
+            id=id,
+            position=position,
+            membership=membership,
+            total_stalls=stall_count,
+            available_stalls=stall_count,
+            station_id=station_id,
+        )
 
     @classmethod
     def from_row(
@@ -125,7 +140,7 @@ def checkout_stall(self) -> Optional[Base]:
         if stalls < 1:
             return None
         else:
-            return self._replace(available_stalls=stalls - 1)
+            return replace(self, available_stalls=stalls - 1)
 
     def return_stall(self) -> Tuple[Optional[Exception], Optional[Base]]:
         """
@@ -142,7 +157,7 @@ def return_stall(self) -> Tuple[Optional[Exception], Optional[Base]]:
                 None,
             )
         else:
-            return None, self._replace(available_stalls=stalls + 1)
+            return None, replace(self, available_stalls=stalls + 1)
 
     def set_membership(self, member_ids: Tuple[str, ...]) -> Base:
         """
@@ -151,7 +166,7 @@ def set_membership(self, member_ids: Tuple[str, ...]) -> Base:
         :param member_ids: a Tuple containing updated membership(s) of the base
         :return:
         """
-        return self._replace(membership=Membership.from_tuple(member_ids))
+        return replace(self, membership=Membership.from_tuple(member_ids))
 
     def add_membership(self, membership_id: MembershipId) -> Base:
         """
@@ -161,4 +176,4 @@ def add_membership(self, membership_id: MembershipId) -> Base:
         :return: updated base
         """
         updated_membership = self.membership.add_membership(membership_id)
-        return self._replace(membership=updated_membership)
+        return replace(self, membership=updated_membership)
diff --git a/nrel/hive/model/energy/charger/__init__.py b/nrel/hive/model/energy/charger/__init__.py
index acdad734..8d9dde2f 100644
--- a/nrel/hive/model/energy/charger/__init__.py
+++ b/nrel/hive/model/energy/charger/__init__.py
@@ -2,12 +2,14 @@
 from pathlib import Path
 from typing import Dict
 
+from immutables import Map
+
 from nrel.hive.model.energy.charger.charger import Charger
 from nrel.hive.model.energy.energytype import EnergyType
 from nrel.hive.util.typealiases import ChargerId
 
 
-def build_chargers_table(chargers_file: str) -> Dict[ChargerId, Charger]:
+def build_chargers_table(chargers_file: str) -> Map[ChargerId, Charger]:
     """
     constructs a table of the chargers available in this simulation
 
@@ -53,4 +55,4 @@ def build_chargers_table(chargers_file: str) -> Dict[ChargerId, Charger]:
                         units=units,
                     )
                     chargers_table.update({charger_id: new_charger})
-        return chargers_table
+        return Map(chargers_table)
diff --git a/nrel/hive/model/entity.py b/nrel/hive/model/entity.py
new file mode 100644
index 00000000..3170f04a
--- /dev/null
+++ b/nrel/hive/model/entity.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+from abc import abstractmethod, ABC
+from dataclasses import dataclass
+from typing import Tuple
+
+
+from nrel.hive.model.entity_position import EntityPosition
+from nrel.hive.model.membership import Membership
+from nrel.hive.util.typealiases import MembershipId
+
+
+@dataclass(frozen=True)
+class EntityMixin:
+    id: str
+    position: EntityPosition
+    membership: Membership
+
+
+class EntityABC(ABC):
+    """
+    Interface for creating a generic entity
+    """
+
+    @property
+    @abstractmethod
+    def geoid(self) -> str:
+        """
+        returns the geoid of the entity
+
+        :return: the geoid of the entity
+        """
+
+    @abstractmethod
+    def set_membership(self, member_ids: Tuple[str, ...]) -> EntityABC:
+        """
+        sets the membership(s) of the entity
+
+        :param member_ids: a Tuple containing updated membership(s) of the entity
+        :return: the updated entity
+        """
+
+    @abstractmethod
+    def add_membership(self, membership_id: MembershipId) -> EntityABC:
+        """
+        adds a membership to the entity
+
+        :param membership_id: a membership id for the entity
+        :return: the updated entity
+        """
+
+
+class Entity(EntityMixin, EntityABC):
+    """"""
diff --git a/nrel/hive/model/entity_position.py b/nrel/hive/model/entity_position.py
index 5c693566..03b1d1ae 100644
--- a/nrel/hive/model/entity_position.py
+++ b/nrel/hive/model/entity_position.py
@@ -1,6 +1,6 @@
 from typing import NamedTuple
 
-from nrel.hive.util import LinkId, GeoId
+from nrel.hive.util.typealiases import LinkId, GeoId
 
 
 class EntityPosition(NamedTuple):
diff --git a/nrel/hive/model/membership.py b/nrel/hive/model/membership.py
index df2fb3f7..07427a03 100644
--- a/nrel/hive/model/membership.py
+++ b/nrel/hive/model/membership.py
@@ -1,8 +1,9 @@
 from __future__ import annotations
 
-from typing import Tuple, NamedTuple, FrozenSet
+from typing import Tuple, NamedTuple, FrozenSet, TYPE_CHECKING
 
-from nrel.hive.util.typealiases import MembershipId
+if TYPE_CHECKING:
+    from nrel.hive.util.typealiases import MembershipId
 
 PUBLIC_MEMBERSHIP_ID = "public"
 
diff --git a/nrel/hive/model/passenger.py b/nrel/hive/model/passenger.py
index 2458b7fe..601b8e3f 100644
--- a/nrel/hive/model/passenger.py
+++ b/nrel/hive/model/passenger.py
@@ -1,14 +1,18 @@
 from __future__ import annotations
 
 import functools as ft
-from typing import NamedTuple, Optional
+from typing import Optional, TYPE_CHECKING
+from dataclasses import dataclass, replace
 
 from nrel.hive.model.membership import Membership
 from nrel.hive.model.sim_time import SimTime
-from nrel.hive.util.typealiases import *
 
+if TYPE_CHECKING:
+    from nrel.hive.util.typealiases import *
 
-class Passenger(NamedTuple):
+
+@dataclass(frozen=True)
+class Passenger:
     """
     A tuple representing a passenger in the simulation.
 
@@ -44,7 +48,7 @@ def add_vehicle_id(self, vehicle_id: VehicleId) -> Passenger:
         :param vehicle_id: vehicle id
         :return: updated Passenger
         """
-        return self._replace(vehicle_id=vehicle_id)
+        return replace(self, vehicle_id=vehicle_id)
 
 
 def create_passenger_id(request_id: RequestId, passenger_id: int) -> PassengerId:
diff --git a/nrel/hive/model/request/request.py b/nrel/hive/model/request/request.py
index 5073165b..c0017f6e 100644
--- a/nrel/hive/model/request/request.py
+++ b/nrel/hive/model/request/request.py
@@ -1,24 +1,27 @@
 from __future__ import annotations
 
 from typing import NamedTuple, Optional, Dict, TYPE_CHECKING
+from dataclasses import dataclass, replace
 
 import h3
 
+from nrel.hive.model.entity import Entity
 from nrel.hive.model.membership import Membership
 from nrel.hive.model.passenger import Passenger, create_passenger_id
 from nrel.hive.model.entity_position import EntityPosition
 from nrel.hive.model.roadnetwork.roadnetwork import RoadNetwork
 from nrel.hive.model.sim_time import SimTime
 from nrel.hive.util.exception import TimeParseError
-from nrel.hive.util.typealiases import *
 from nrel.hive.util.units import Currency, KM_TO_MILE
 
 if TYPE_CHECKING:
     from nrel.hive.model.request import RequestRateStructure
     from nrel.hive.runner.environment import Environment
+    from nrel.hive.util.typealiases import *
 
 
-class Request(NamedTuple):
+@dataclass(frozen=True)
+class Request(Entity):
     """
     A ride hail request which is alive in the simulation but not yet serviced.
     It should only exist if the current sim time >= self.departure_time.
@@ -37,19 +40,24 @@ class Request(NamedTuple):
     """
 
     id: RequestId
-    origin_position: EntityPosition
+    position: EntityPosition
+    membership: Membership
+
     destination_position: EntityPosition
     departure_time: SimTime
     passengers: Tuple[Passenger, ...]
     allows_pooling: bool
-    membership: Membership = Membership()
     value: Currency = 0
     dispatched_vehicle: Optional[VehicleId] = None
     dispatched_vehicle_time: Optional[SimTime] = None
 
+    @property
+    def geoid(self):
+        return self.position.geoid
+
     @property
     def origin(self):
-        return self.origin_position.geoid
+        return self.position.geoid
 
     @property
     def destination(self):
@@ -71,7 +79,15 @@ def build(
         assert departure_time >= 0
         assert passengers > 0
         origin_position = road_network.position_from_geoid(origin)
+        if origin_position is None:
+            raise ValueError(
+                f"request {request_id} origin cannot be positioned on the road network"
+            )
         destination_position = road_network.position_from_geoid(destination)
+        if destination_position is None:
+            raise ValueError(
+                f"request {request_id} destination cannot be positioned on the road network"
+            )
         if fleet_id:
             membership = Membership.single_membership(fleet_id)
         else:
@@ -90,7 +106,7 @@ def build(
 
         request = Request(
             id=request_id,
-            origin_position=origin_position,
+            position=origin_position,
             destination_position=destination_position,
             departure_time=departure_time,
             passengers=tuple(request_as_passengers),
@@ -100,10 +116,6 @@ def build(
         )
         return request
 
-    @property
-    def geoid(self):
-        return self.origin
-
     @classmethod
     def from_row(
         cls, row: Dict[str, str], env: Environment, road_network: RoadNetwork
@@ -164,9 +176,10 @@ def from_row(
                 o_geoid = h3.geo_to_h3(o_lat, o_lon, env.config.sim.sim_h3_resolution)
                 d_geoid = h3.geo_to_h3(d_lat, d_lon, env.config.sim.sim_h3_resolution)
 
-                departure_time_result = SimTime.build(row["departure_time"])
-                if isinstance(departure_time_result, TimeParseError):
-                    return departure_time_result, None
+                try:
+                    departure_time_result = SimTime.build(row["departure_time"])
+                except TimeParseError as e:
+                    return e, None
 
                 passengers = int(row["passengers"])
                 allows_pooling = (
@@ -203,14 +216,14 @@ def assign_dispatched_vehicle(self, vehicle_id: VehicleId, current_time: SimTime
         :param current_time: the current simulation time
         :return: the updated Request
         """
-        return self._replace(dispatched_vehicle=vehicle_id, dispatched_vehicle_time=current_time)
+        return replace(self, dispatched_vehicle=vehicle_id, dispatched_vehicle_time=current_time)
 
     def unassign_dispatched_vehicle(self) -> Request:
         """
         removes any vehicle listed as assigned to this request
         :return: the updated request
         """
-        updated = self._replace(dispatched_vehicle=None, dispatched_vehicle_time=None)
+        updated = replace(self, dispatched_vehicle=None, dispatched_vehicle_time=None)
         return updated
 
     def assign_value(
@@ -228,4 +241,23 @@ def assign_value(
         distance_km = road_network.distance_by_geoid_km(self.origin, self.destination)
         distance_miles = distance_km * KM_TO_MILE
         price = rate_structure.base_price + (rate_structure.price_per_mile * distance_miles)
-        return self._replace(value=max(rate_structure.minimum_price, price))
+        return replace(self, value=max(rate_structure.minimum_price, price))
+
+    def set_membership(self, member_ids: Tuple[str, ...]) -> Request:
+        """
+        sets the membership(s) of the request
+
+        :param member_ids: a Tuple containing updated membership(s) of therequest
+        :return:
+        """
+        return replace(self, membership=Membership.from_tuple(member_ids))
+
+    def add_membership(self, membership_id: MembershipId) -> Request:
+        """
+        adds the membership to the request
+
+        :param membership_id: a membership for the request
+        :return:
+        """
+        updated_membership = self.membership.add_membership(membership_id)
+        return replace(self, membership=updated_membership)
diff --git a/nrel/hive/model/roadnetwork/link.py b/nrel/hive/model/roadnetwork/link.py
index 47495e9e..5ac93835 100644
--- a/nrel/hive/model/roadnetwork/link.py
+++ b/nrel/hive/model/roadnetwork/link.py
@@ -16,16 +16,6 @@
 )
 
 
-class EntityPosition(NamedTuple):
-    """
-    A pairing of a geoid at the simulation resolution and a link id which yields the position of an entity
-    with the context of a link for directionality.
-    """
-
-    link_id: LinkId
-    geoid: GeoId
-
-
 class Link(NamedTuple):
     """
     a directed edge on the road network from node a -> node b
diff --git a/nrel/hive/model/roadnetwork/link_id.py b/nrel/hive/model/roadnetwork/link_id.py
index 4c15d681..ff6b1c8a 100644
--- a/nrel/hive/model/roadnetwork/link_id.py
+++ b/nrel/hive/model/roadnetwork/link_id.py
@@ -1,9 +1,8 @@
 from nrel.hive.util.typealiases import LinkId
-from typing import Optional, Tuple, TypeVar
+from typing import Optional, Tuple, Any
 from ast import literal_eval
-import re
 
-NodeId = TypeVar("NodeId")
+NodeId = Any
 
 
 def create_link_id(src: int, dst: int) -> LinkId:
@@ -61,6 +60,9 @@ def reverse_link_id(
         response = Exception(f"failure during link reversal for link {link_id}")
         response.__cause__ = error
         return response, None
+    elif node_ids is None:
+        response = Exception(f"failure during link reversal for link {link_id}")
+        return response, None
     else:
         src, dst = node_ids
         result = create_link_id(dst, src)
diff --git a/nrel/hive/model/roadnetwork/linktraversal.py b/nrel/hive/model/roadnetwork/linktraversal.py
index 0a1ac05a..412f29d4 100644
--- a/nrel/hive/model/roadnetwork/linktraversal.py
+++ b/nrel/hive/model/roadnetwork/linktraversal.py
@@ -1,10 +1,13 @@
 from __future__ import annotations
 
-from typing import Optional, NamedTuple, Tuple
+from typing import Optional, NamedTuple, Tuple, TYPE_CHECKING
 
-from nrel.hive.util.h3_ops import H3Ops, LinkId, GeoId
+from nrel.hive.util.h3_ops import H3Ops
 from nrel.hive.util.units import Seconds, Kilometers, Kmph, hours_to_seconds
 
+if TYPE_CHECKING:
+    from nrel.hive.util.typealiases import LinkId, GeoId
+
 
 class LinkTraversal(NamedTuple):
     """
diff --git a/nrel/hive/model/roadnetwork/osm/osm_road_network_link_helper.py b/nrel/hive/model/roadnetwork/osm/osm_road_network_link_helper.py
index 2cb61317..785a63f3 100644
--- a/nrel/hive/model/roadnetwork/osm/osm_road_network_link_helper.py
+++ b/nrel/hive/model/roadnetwork/osm/osm_road_network_link_helper.py
@@ -10,9 +10,7 @@
 
 from nrel.hive.model.roadnetwork.link import Link
 from nrel.hive.model.roadnetwork.link_id import create_link_id
-from nrel.hive.model.roadnetwork.osm.osm_roadnetwork_ops import (
-    safe_get_node_coordinates,
-)
+from nrel.hive.model.roadnetwork.osm.osm_roadnetwork_ops import safe_get_node_coordinates
 from nrel.hive.util.typealiases import GeoId, LinkId
 from nrel.hive.util.units import M_TO_KM, Kmph, Kilometers
 
@@ -101,7 +99,7 @@ def add_link(self, link: Link) -> Tuple[Optional[Exception], Optional[Accumulato
                     # we want to look up edges by their midpoint. that said, two edges will share the same
                     # endpoints, one for each direction. since these two edges would share the same midpoint,
                     # we aim here to make both centroids _just barely_ different by subtracting the midpoint index by 1.
-                    midpoint_h3_line_index = round(len(h3_line) / 2) if len(h3_line) > 0 else None
+                    midpoint_h3_line_index = round(len(h3_line) / 2)
                     src_oriented_midpoint_index = (
                         midpoint_h3_line_index - 1
                         if midpoint_h3_line_index > 0
@@ -131,6 +129,8 @@ def create_link_entry(
             acc_error, accumulator = acc
             if acc_error:
                 return acc
+            elif accumulator is None:
+                return acc
             else:
                 try:
                     src, dst, _ = link_tuple
@@ -151,6 +151,16 @@ def create_link_entry(
                         )
                         response.__cause__ = dst_coord_err
                         return response, None
+                    elif src_coord is None:
+                        response = Exception(
+                            f"failure getting node coordinates while building OSMRoadNetworkLinkHelper"
+                        )
+                        return response, None
+                    elif dst_coord is None:
+                        response = Exception(
+                            f"failure getting node coordinates while building OSMRoadNetworkLinkHelper"
+                        )
+                        return response, None
                     else:
                         src_lat, src_lon = src_coord
                         dst_lat, dst_lon = dst_coord
@@ -188,12 +198,15 @@ def create_link_entry(
 
         # process each link, building the collection of Links by LinkId, and
         # the collections which will be used to build a spatial index over the edge centroids
-        initial = None, Accumulator()
+        initial: Tuple[Optional[Exception], Optional[Accumulator]] = (None, Accumulator())
         error, accumulator = ft.reduce(create_link_entry, graph.edges, initial)
         if error:
             response = Exception(f"failure building OSMRoadNetworkLinkHelper")
             response.__cause__ = error
             return response, None
+        elif accumulator is None:
+            response = Exception(f"failure building OSMRoadNetworkLinkHelper")
+            return response, None
         else:
             # construct the spatial index
             tree = cKDTree(accumulator.link_centroids)
diff --git a/nrel/hive/model/roadnetwork/osm/osm_roadnetwork.py b/nrel/hive/model/roadnetwork/osm/osm_roadnetwork.py
index 9bdda46f..e78b36b7 100644
--- a/nrel/hive/model/roadnetwork/osm/osm_roadnetwork.py
+++ b/nrel/hive/model/roadnetwork/osm/osm_roadnetwork.py
@@ -3,18 +3,15 @@
 import json
 import logging
 from pathlib import Path
-from typing import Optional, Union
+from typing import Optional, Tuple, Union
 
 import networkx as nx
 
-from nrel.hive.external.miniosmnx.core import graph_from_file
 from nrel.hive.model.roadnetwork.geofence import GeoFence
 from nrel.hive.model.entity_position import EntityPosition
 from nrel.hive.model.roadnetwork.link import Link
-from nrel.hive.model.roadnetwork.link_id import extract_node_ids
-from nrel.hive.model.roadnetwork.osm.osm_road_network_link_helper import (
-    OSMRoadNetworkLinkHelper,
-)
+from nrel.hive.model.roadnetwork.link_id import extract_node_ids, NodeId
+from nrel.hive.model.roadnetwork.osm.osm_road_network_link_helper import OSMRoadNetworkLinkHelper
 from nrel.hive.model.roadnetwork.osm.osm_roadnetwork_ops import (
     route_from_nx_path,
     resolve_route_src_dst_positions,
@@ -29,6 +26,7 @@
 from nrel.hive.util import LinkId
 from nrel.hive.util.typealiases import GeoId, H3Resolution
 from nrel.hive.util.units import Kmph, Kilometers
+from nrel.hive.model.roadnetwork.link_id import extract_node_ids
 
 log = logging.getLogger(__name__)
 
@@ -51,12 +49,7 @@ def __init__(
         self.geofence = geofence
 
         # read in the network file
-        if road_network_file.suffix == ".xml":
-            log.warning(
-                ".xml files have been deprecated in hive. please switch to using .json formats"
-            )
-            graph = graph_from_file(str(road_network_file))
-        elif road_network_file.suffix == ".json":
+        if road_network_file.suffix == ".json":
             with road_network_file.open("r") as f:
                 graph = nx.node_link_graph(json.load(f))
         else:
@@ -97,7 +90,7 @@ def _valid_node_id(nid: Union[int, tuple]) -> bool:
         elif missing_speed > 0:
             log.warning(
                 f"found {missing_speed} links in the road network that don't have speed information.\n"
-                f"hive will automatically set these to {self.default_speed_kmph} kmph."
+                f"hive will automatically set these to {default_speed_kmph} kmph."
             )
 
         # build tables on the network edges for spatial lookup and LinkId lookup
@@ -106,6 +99,8 @@ def _valid_node_id(nid: Union[int, tuple]) -> bool:
         )
         if link_helper_error:
             raise link_helper_error
+        elif link_helper is None:
+            raise Exception("Was not able to build link helper")
         else:
             # finish constructing OSMRoadNetwork instance
             self.graph = graph
@@ -130,6 +125,10 @@ def route(self, origin: EntityPosition, destination: EntityPosition) -> Route:
         elif extract_dst_err:
             log.error(extract_dst_err)
             return empty_route()
+        elif src_nodes is None:
+            return empty_route()
+        elif dst_nodes is None:
+            return empty_route()
         else:
             _, origin_node_id = src_nodes
             destination_node_id, _ = dst_nodes
@@ -145,6 +144,8 @@ def route(self, origin: EntityPosition, destination: EntityPosition) -> Route:
                     f"origin node {origin_node_id}, destination node {destination_node_id}, shortest path node list result: {nx_path}"
                 )
                 return empty_route()
+            elif inner_link_path is None:
+                return empty_route()
             else:
                 # modify the start and end GeoIds based on the positions in the src/dst links
                 resolved_route = resolve_route_src_dst_positions(
diff --git a/nrel/hive/model/roadnetwork/osm/osm_roadnetwork_ops.py b/nrel/hive/model/roadnetwork/osm/osm_roadnetwork_ops.py
index bd7bbaa0..cac1c09e 100644
--- a/nrel/hive/model/roadnetwork/osm/osm_roadnetwork_ops.py
+++ b/nrel/hive/model/roadnetwork/osm/osm_roadnetwork_ops.py
@@ -60,6 +60,8 @@ def _accumulate_links(
             err, prev_links = acc
             if err:
                 return acc
+            elif prev_links is None:
+                return acc
             else:
                 # build a LinkId from the node pair, then look up the associated Link and return it
                 src, dst = node_id_pair
@@ -75,7 +77,7 @@ def _accumulate_links(
                     return None, updated_links
 
         nx_path_adj_pairs = [(nx_path[i], nx_path[i + 1]) for i in range(0, len(nx_path) - 1)]
-        initial = None, ()
+        initial: Tuple[Optional[Exception], Optional[Tuple[LinkTraversal, ...]]] = (None, ())
         result = ft.reduce(_accumulate_links, nx_path_adj_pairs, initial)
         return result
 
diff --git a/nrel/hive/model/roadnetwork/route.py b/nrel/hive/model/roadnetwork/route.py
index de6e5208..5d391933 100644
--- a/nrel/hive/model/roadnetwork/route.py
+++ b/nrel/hive/model/roadnetwork/route.py
@@ -1,5 +1,5 @@
 import functools as ft
-from typing import Tuple, Optional
+from typing import Any, Tuple, Optional
 
 import h3
 
@@ -26,7 +26,7 @@ def route_distance_km(route: Route) -> Kilometers:
     :rtype: :py:obj:`kilometers`
     :return: the distance in kilometers
     """
-    distance_km = 0
+    distance_km = 0.0
     for l in route:
         distance_km += l.distance_km
 
@@ -89,10 +89,11 @@ def to_linestring(route: Route, env: Environment) -> str:
         linestring = wkt.linestring_2d((src, dst), env.config.global_config.wkt_x_y_ordering)
         return linestring
     else:
+        inital: Tuple[Any, ...] = ()
         points = ft.reduce(
             lambda acc, l: acc + (h3.h3_to_geo(l.start), h3.h3_to_geo(l.end)),
             route,
-            (),
+            inital,
         )
         linestring = wkt.linestring_2d(points, env.config.global_config.wkt_x_y_ordering)
         return linestring
diff --git a/nrel/hive/model/roadnetwork/routetraversal.py b/nrel/hive/model/roadnetwork/routetraversal.py
index 41b565f4..715d1f86 100644
--- a/nrel/hive/model/roadnetwork/routetraversal.py
+++ b/nrel/hive/model/roadnetwork/routetraversal.py
@@ -85,7 +85,7 @@ def add_link_not_traversed(self, link: LinkTraversal) -> RouteTraversal:
 
 def traverse(
     route_estimate: Route, duration_seconds: Seconds
-) -> Tuple[Optional[Exception], RouteTraversal]:
+) -> Tuple[Optional[Exception], Optional[RouteTraversal]]:
     """
     step through the route from the current agent position (assumed to be start.link_id) toward the destination
 
@@ -110,6 +110,11 @@ def _traverse(
             link: LinkTraversal,
         ) -> Tuple[Optional[Exception], Optional[RouteTraversal]]:
             acc_failures, acc_traversal = acc
+            if acc_failures:
+                return acc
+            elif acc_traversal is None:
+                return acc
+
             if acc_traversal.no_time_left():
                 return acc_failures, acc_traversal.add_link_not_traversed(link)
             # traverse this link as far as we can go
@@ -118,12 +123,15 @@ def _traverse(
                 response = Exception(f"failure during traverse")
                 response.__cause__ = error
                 return response, None
+            elif traverse_result is None:
+                response = Exception(f"failure during traverse")
+                return response, None
             else:
                 updated_experienced_route = acc_traversal.add_traversal(traverse_result)
                 return acc_failures, updated_experienced_route
 
         # initial search state has a route traversal and an Optional[Exception]
-        initial = (
+        initial: Tuple[Optional[Exception], Optional[RouteTraversal]] = (
             None,
             RouteTraversal(remaining_time_seconds=duration_seconds),
         )
diff --git a/nrel/hive/model/sim_time.py b/nrel/hive/model/sim_time.py
index 95ff82dd..d1defa12 100644
--- a/nrel/hive/model/sim_time.py
+++ b/nrel/hive/model/sim_time.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from datetime import datetime
+from datetime import datetime, time
 from typing import Union
 
 from nrel.hive.util.exception import TimeParseError
@@ -12,7 +12,7 @@ class SimTime(int):
     )
 
     @classmethod
-    def build(cls, value: Union[str, int]) -> Union[TimeParseError, SimTime]:
+    def build(cls, value: Union[str, int]) -> SimTime:
         if isinstance(value, str):
             try:
                 time = datetime.fromisoformat(value)
@@ -20,14 +20,14 @@ def build(cls, value: Union[str, int]) -> Union[TimeParseError, SimTime]:
                 try:
                     time = datetime.utcfromtimestamp(int(value))
                 except (ValueError, TypeError):
-                    return TimeParseError(cls.ERROR_MSG + f" got {value}")
+                    raise TimeParseError(cls.ERROR_MSG + f" got {value}")
         elif isinstance(value, int):
             try:
                 time = datetime.utcfromtimestamp(value)
             except (ValueError, TypeError):
-                return TimeParseError(cls.ERROR_MSG + f" got {value}")
+                raise TimeParseError(cls.ERROR_MSG + f" got {value}")
         else:
-            return TimeParseError(cls.ERROR_MSG + f" got {value}")
+            raise TimeParseError(cls.ERROR_MSG + f" got {value}")
 
         if time.tzinfo:
             time = time.replace(tzinfo=None)
@@ -57,7 +57,7 @@ def __repr__(self):
     def __str__(self):
         return self.as_iso_time()
 
-    def as_datetime_time(self) -> datetime.time:
+    def as_datetime_time(self) -> time:
         return datetime.utcfromtimestamp(int(self)).time()
 
     def as_epoch_time(self) -> int:
diff --git a/nrel/hive/model/station/charger_state.py b/nrel/hive/model/station/charger_state.py
index 42d405d9..8f7020d5 100644
--- a/nrel/hive/model/station/charger_state.py
+++ b/nrel/hive/model/station/charger_state.py
@@ -145,10 +145,3 @@ def scale_charge_rate(self, factor: float) -> ResultE[ChargerState]:
         else:
             updated = self._replace(charger=self.charger._replace(rate=self.charger.rate * factor))
             return Success(updated)
-
-    def reset_charge_rate(self) -> ChargerState:
-        """
-        resets the charge rate to the factory charge rate
-        :return: updated charger state
-        """
-        return self._replace(current_charge_rate=self.charger.rate)
diff --git a/nrel/hive/model/station/station.py b/nrel/hive/model/station/station.py
index e7f336a0..731b745d 100644
--- a/nrel/hive/model/station/station.py
+++ b/nrel/hive/model/station/station.py
@@ -2,22 +2,21 @@
 
 from distutils.util import strtobool
 import functools as ft
-from typing import NamedTuple, Dict, Optional, Union
+from typing import Dict, Optional, Union
+from dataclasses import dataclass, replace
 
 import h3
 import immutables
 import logging
 
 from returns.result import ResultE, Success, Failure
-from nrel.hive.model.energy import charger
 
+from nrel.hive.model.entity import Entity
 from nrel.hive.runner.environment import Environment
 from nrel.hive.model.station.charger_state import ChargerState
-from nrel.hive.model.energy import EnergyType
 from nrel.hive.model.energy.charger import Charger
 from nrel.hive.model.membership import Membership
 from nrel.hive.model.entity_position import EntityPosition
-from nrel.hive.model.roadnetwork.link import Link
 from nrel.hive.model.roadnetwork.roadnetwork import RoadNetwork
 from nrel.hive.model.station.station_ops import (
     station_state_update,
@@ -33,7 +32,8 @@
 log = logging.getLogger(__name__)
 
 
-class Station(NamedTuple):
+@dataclass(frozen=True)
+class Station(Entity):
     """
     A station that vehicles can use to refuel
 
@@ -56,10 +56,10 @@ class Station(NamedTuple):
 
     id: StationId
     position: EntityPosition
+    membership: Membership
     state: immutables.Map[ChargerId, ChargerState]
     on_shift_access_chargers: FrozenSet[ChargerId]
     balance: Currency = 0.0
-    membership: Membership = Membership()
 
     @property
     def geoid(self) -> GeoId:
@@ -161,7 +161,8 @@ def append_chargers(
 
         updated_station_state = self.state.update({charger_id: append_cs})
         updated_on_shift = self.on_shift_access_chargers.union([charger_id])
-        updated_station = self._replace(
+        updated_station = replace(
+            self,
             state=updated_station_state,
             on_shift_access_chargers=updated_on_shift,
         )
@@ -231,6 +232,8 @@ def from_row(
             error, updated_station = prev_station.append_chargers(charger_id, charger_count, env)
             if error is not None:
                 raise error
+            elif updated_station is None:
+                raise Exception("got error and no station")
             return updated_station
 
     def get_price(self, charger_id: ChargerId) -> Optional[Currency]:
@@ -362,7 +365,7 @@ def set_charger_rate(self, charger_id: ChargerId, rate: KwH) -> ResultE[Station]
         else:
             new_charger_state = new_charger_state_or_err.unwrap()
             new_state = self.state.set(charger_id, new_charger_state)
-            new_station = self._replace(state=new_state)
+            new_station = replace(self, state=new_state)
             return Success(new_station)
 
     def scale_charger_rate(self, charger_id: ChargerId, scale: float) -> ResultE[Station]:
@@ -387,7 +390,7 @@ def scale_charger_rate(self, charger_id: ChargerId, scale: float) -> ResultE[Sta
         else:
             new_charger_state = new_charger_state_or_err.unwrap()
             new_state = self.state.set(charger_id, new_charger_state)
-            new_station = self._replace(state=new_state)
+            new_station = replace(self, state=new_state)
             return Success(new_station)
 
     def update_prices(self, new_prices: immutables.Map[ChargerId, Currency]) -> ErrorOr[Station]:
@@ -403,7 +406,7 @@ def receive_payment(self, currency_received: Currency) -> Station:
         :param currency_received: the currency received for a charge event
         :return: the updated Station
         """
-        return self._replace(balance=self.balance + currency_received)
+        return replace(self, balance=self.balance + currency_received)
 
     def enqueue_for_charger(self, charger_id: ChargerId) -> ErrorOr[Station]:
         """
@@ -452,7 +455,7 @@ def set_membership(self, member_ids: Tuple[str, ...]) -> Station:
         :param member_ids: a Tuple containing updated membership(s) of the station
         :return:
         """
-        return self._replace(membership=Membership.from_tuple(member_ids))
+        return replace(self, membership=Membership.from_tuple(member_ids))
 
     def add_membership(self, membership_id: MembershipId) -> Station:
         """
@@ -462,4 +465,4 @@ def add_membership(self, membership_id: MembershipId) -> Station:
         :return: updated station
         """
         updated_membership = self.membership.add_membership(membership_id)
-        return self._replace(membership=updated_membership)
+        return replace(self, membership=updated_membership)
diff --git a/nrel/hive/model/station/station_ops.py b/nrel/hive/model/station/station_ops.py
index 5edad812..9ccbf4eb 100644
--- a/nrel/hive/model/station/station_ops.py
+++ b/nrel/hive/model/station/station_ops.py
@@ -1,4 +1,6 @@
 from typing import Iterable, Optional, TypeVar, Callable, TYPE_CHECKING
+from dataclasses import replace
+
 from nrel.hive.util.error_or_result import ErrorOr
 from nrel.hive.util.typealiases import *
 
@@ -44,9 +46,11 @@ def station_state_update(
         err, updated = op(charger_state)
         if err is not None:
             return err, None
+        elif updated is None:
+            return Exception("got no error and no station"), None
         else:
             updated_s = station.state.set(charger_id, updated)
-            result = station._replace(state=updated_s)
+            result = replace(station, state=updated_s)
             return None, result
 
 
@@ -80,7 +84,7 @@ def station_state_optional_update(
             return None, None
         else:
             updated_s = station.state.set(charger_id, updated)
-            result = station._replace(state=updated_s)
+            result = replace(station, state=updated_s)
             return None, result
 
 
diff --git a/nrel/hive/model/vehicle/mechatronics/__init__.py b/nrel/hive/model/vehicle/mechatronics/__init__.py
index 2724ef89..bc4e86f5 100644
--- a/nrel/hive/model/vehicle/mechatronics/__init__.py
+++ b/nrel/hive/model/vehicle/mechatronics/__init__.py
@@ -1,27 +1,21 @@
 from __future__ import annotations
 
 from typing import Dict
-from pathlib import Path
 
 import yaml
 
+from immutables import Map
+
 from nrel.hive.model.vehicle.mechatronics.bev import BEV
 from nrel.hive.model.vehicle.mechatronics.ice import ICE
-from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import (
-    MechatronicsInterface,
-)
+from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import MechatronicsInterface
 from nrel.hive.util import fs
 from nrel.hive.util.typealiases import MechatronicsId
 
-mechatronic_models = {
-    "bev": BEV,
-    "ice": ICE,
-}
-
 
 def build_mechatronics_table(
     mechatronics_file: str, scenario_directory: str
-) -> Dict[MechatronicsId, MechatronicsInterface]:
+) -> Map[MechatronicsId, MechatronicsInterface]:
     """
     constructs a dictionary containing all of the provided vehicle configurations where the key is the mechatronics ID
     and the contents are the appropriate mechatronics models with the desired attributes
@@ -34,7 +28,7 @@ def build_mechatronics_table(
     :raises Exception due to IOErrors, missing required parameters in the mechatronics yaml
     :raises Exception due to FileNotFoundErrors, missing the mechatronics, powertrain, or powercurve file(s)
     """
-    mechatronics = {}
+    mechatronics: Dict[str, MechatronicsInterface] = {}
     with open(mechatronics_file) as f:
         config_dict = yaml.safe_load(f)
         for mechatronics_id in config_dict:
@@ -44,10 +38,6 @@ def build_mechatronics_table(
                 mechatronics_type = config_dict[mechatronics_id]["mechatronics_type"]
             except KeyError:
                 raise IOError(f"could not find mechatronics_type in {mechatronics_id}")
-            try:
-                model = mechatronic_models[mechatronics_type]
-            except KeyError:
-                raise IOError(f"{mechatronics_type} not registered with hive")
 
             if "powertrain_file" in config_dict[mechatronics_id]:
                 powertrain_file = fs.construct_asset_path(
@@ -67,6 +57,13 @@ def build_mechatronics_table(
                 )
                 config_dict[mechatronics_id]["powercurve_file"] = powercurve_file
 
-            mechatronics[mechatronics_id] = model.from_dict(config_dict[mechatronics_id])
+            if mechatronics_type == "bev":
+                mechatronics[mechatronics_id] = BEV.from_dict(config_dict[mechatronics_id])
+            elif mechatronics_type == "ice":
+                mechatronics[mechatronics_id] = ICE.from_dict(config_dict[mechatronics_id])
+            else:
+                raise ValueError(
+                    f"got unexpected mechatronics type {mechatronics_type}; try `bev` or `ice`"
+                )
 
-    return mechatronics
+    return Map(mechatronics)
diff --git a/nrel/hive/model/vehicle/mechatronics/bev.py b/nrel/hive/model/vehicle/mechatronics/bev.py
index 7b54cdf1..e0b9fb7f 100644
--- a/nrel/hive/model/vehicle/mechatronics/bev.py
+++ b/nrel/hive/model/vehicle/mechatronics/bev.py
@@ -8,9 +8,7 @@
 import immutables
 
 from nrel.hive.model.energy.energytype import EnergyType
-from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import (
-    MechatronicsInterface,
-)
+from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import MechatronicsInterface
 from nrel.hive.model.vehicle.mechatronics.powercurve import build_powercurve
 from nrel.hive.model.vehicle.mechatronics.powertrain import build_powertrain
 from nrel.hive.util.typealiases import MechatronicsId
@@ -20,12 +18,8 @@
     from nrel.hive.model.energy.charger import Charger
     from nrel.hive.model.vehicle.vehicle import Vehicle
     from nrel.hive.model.roadnetwork.route import Route
-    from nrel.hive.model.vehicle.mechatronics.powertrain.powertrain import (
-        Powertrain,
-    )
-    from nrel.hive.model.vehicle.mechatronics.powercurve.powercurve import (
-        Powercurve,
-    )
+    from nrel.hive.model.vehicle.mechatronics.powertrain.powertrain import Powertrain
+    from nrel.hive.model.vehicle.mechatronics.powercurve.powercurve import Powercurve
 
 log = logging.getLogger(__name__)
 
@@ -54,7 +48,7 @@ def from_dict(cls, d: Dict[str, str]) -> BEV:
         :param d: the dictionary to build from
         :return: the built Mechatronics object
         """
-        nominal_watt_hour_per_mile = float(d["nominal_watt_hour_per_mile"])
+        nominal_watt_hour_per_mile = d["nominal_watt_hour_per_mile"]
 
         # set scale factor in config dict so the tabular powertrain can use it to scale the normalized lookup
         updated_d = d.copy()
@@ -77,7 +71,7 @@ def from_dict(cls, d: Dict[str, str]) -> BEV:
             idle_kwh_per_hour=idle_kwh_per_hour,
             powertrain=powertrain,
             powercurve=powercurve,
-            nominal_watt_hour_per_mile=nominal_watt_hour_per_mile,
+            nominal_watt_hour_per_mile=float(nominal_watt_hour_per_mile),
             charge_taper_cutoff_kw=charge_taper_cutoff_kw,
         )
 
diff --git a/nrel/hive/model/vehicle/mechatronics/ice.py b/nrel/hive/model/vehicle/mechatronics/ice.py
index d905b624..3590ec32 100644
--- a/nrel/hive/model/vehicle/mechatronics/ice.py
+++ b/nrel/hive/model/vehicle/mechatronics/ice.py
@@ -8,9 +8,7 @@
 import immutables
 
 from nrel.hive.model.energy.energytype import EnergyType
-from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import (
-    MechatronicsInterface,
-)
+from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import MechatronicsInterface
 from nrel.hive.model.vehicle.mechatronics.powertrain import build_powertrain
 from nrel.hive.util.typealiases import MechatronicsId
 from nrel.hive.util.units import *
@@ -19,9 +17,7 @@
     from nrel.hive.model.energy.charger.charger import Charger
     from nrel.hive.model.vehicle.vehicle import Vehicle
     from nrel.hive.model.roadnetwork.route import Route
-    from nrel.hive.model.vehicle.mechatronics.powertrain.powertrain import (
-        Powertrain,
-    )
+    from nrel.hive.model.vehicle.mechatronics.powertrain.powertrain import Powertrain
 
 log = logging.getLogger(__name__)
 
@@ -72,14 +68,14 @@ def valid_charger(self, charger: Charger) -> bool:
         """
         return charger.energy_type == EnergyType.GASOLINE
 
-    def initial_energy(self, percent_full: Ratio) -> Dict[EnergyType, float]:
+    def initial_energy(self, percent_full: Ratio) -> immutables.Map[EnergyType, float]:
         """
         return an energy dictionary from an initial soc
 
         :param percent_full:
         :return:
         """
-        return {EnergyType.GASOLINE: self.tank_capacity_gallons * percent_full}
+        return immutables.Map({EnergyType.GASOLINE: self.tank_capacity_gallons * percent_full})
 
     def range_remaining_km(self, vehicle: Vehicle) -> Kilometers:
         """
diff --git a/nrel/hive/model/vehicle/mechatronics/mechatronics_interface.py b/nrel/hive/model/vehicle/mechatronics/mechatronics_interface.py
index cfd59adc..39695e86 100644
--- a/nrel/hive/model/vehicle/mechatronics/mechatronics_interface.py
+++ b/nrel/hive/model/vehicle/mechatronics/mechatronics_interface.py
@@ -61,6 +61,12 @@ def range_remaining_km(self, vehicle: Vehicle) -> Kilometers:
         :return:
         """
 
+    @abstractmethod
+    def calc_required_soc(self, required_range: Kilometers) -> Ratio:
+        """
+        How much SOC needed to go required_range
+        """
+
     @abstractmethod
     def fuel_source_soc(self, vehicle: Vehicle) -> Ratio:
         """
diff --git a/nrel/hive/model/vehicle/mechatronics/powercurve/__init__.py b/nrel/hive/model/vehicle/mechatronics/powercurve/__init__.py
index 46e84fab..df3fc576 100644
--- a/nrel/hive/model/vehicle/mechatronics/powercurve/__init__.py
+++ b/nrel/hive/model/vehicle/mechatronics/powercurve/__init__.py
@@ -2,12 +2,8 @@
 
 import yaml
 
-from nrel.hive.model.vehicle.mechatronics.powercurve.powercurve import (
-    Powercurve,
-)
-from nrel.hive.model.vehicle.mechatronics.powercurve.tabular_powercurve import (
-    TabularPowercurve,
-)
+from nrel.hive.model.vehicle.mechatronics.powercurve.powercurve import Powercurve
+from nrel.hive.model.vehicle.mechatronics.powercurve.tabular_powercurve import TabularPowercurve
 
 powercurve_models = {"tabular": TabularPowercurve}
 
diff --git a/nrel/hive/model/vehicle/mechatronics/powercurve/tabular_powercurve.py b/nrel/hive/model/vehicle/mechatronics/powercurve/tabular_powercurve.py
index 4c039a1c..6d1b8577 100644
--- a/nrel/hive/model/vehicle/mechatronics/powercurve/tabular_powercurve.py
+++ b/nrel/hive/model/vehicle/mechatronics/powercurve/tabular_powercurve.py
@@ -1,13 +1,11 @@
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Optional, Dict, Tuple
+from typing import TYPE_CHECKING, Optional, Dict, Tuple, Any
 
 import numpy as np
 
 from nrel.hive.model.energy.energytype import EnergyType
-from nrel.hive.model.vehicle.mechatronics.powercurve.powercurve import (
-    Powercurve,
-)
+from nrel.hive.model.vehicle.mechatronics.powercurve.powercurve import Powercurve
 from nrel.hive.util.units import Seconds, SECONDS_TO_HOURS, Ratio
 
 if TYPE_CHECKING:
@@ -21,7 +19,7 @@ class TabularPowercurve(Powercurve):
 
     def __init__(
         self,
-        data: Dict[str, str],
+        data: Dict[str, Any],
         nominal_max_charge_kw: Optional[Kw] = None,
         battery_capacity_kwh: Optional[KwH] = None,
     ):
@@ -91,8 +89,8 @@ def charge(
         t = 0
         energy_kwh = start_soc
         while t < duration_seconds and energy_kwh < full_soc:
-            veh_kw_rate = np.interp(
-                energy_kwh, self._charging_energy_kwh, self._charging_rate_kw
+            veh_kw_rate = float(
+                np.interp(energy_kwh, self._charging_energy_kwh, self._charging_rate_kw)
             )  # kilowatt
             charge_power_kw = min(veh_kw_rate, power_kw)  # kilowatt
             kwh = charge_power_kw * (self.step_size_seconds * SECONDS_TO_HOURS)  # kilowatt-hours
diff --git a/nrel/hive/model/vehicle/mechatronics/powertrain/__init__.py b/nrel/hive/model/vehicle/mechatronics/powertrain/__init__.py
index 0da39a0a..6e3255a5 100644
--- a/nrel/hive/model/vehicle/mechatronics/powertrain/__init__.py
+++ b/nrel/hive/model/vehicle/mechatronics/powertrain/__init__.py
@@ -2,12 +2,8 @@
 
 import yaml
 
-from nrel.hive.model.vehicle.mechatronics.powertrain.powertrain import (
-    Powertrain,
-)
-from nrel.hive.model.vehicle.mechatronics.powertrain.tabular_powertrain import (
-    TabularPowertrain,
-)
+from nrel.hive.model.vehicle.mechatronics.powertrain.powertrain import Powertrain
+from nrel.hive.model.vehicle.mechatronics.powertrain.tabular_powertrain import TabularPowertrain
 
 powertrain_models = {"tabular": TabularPowertrain}
 
diff --git a/nrel/hive/model/vehicle/mechatronics/powertrain/tabular_powertrain.py b/nrel/hive/model/vehicle/mechatronics/powertrain/tabular_powertrain.py
index 465a3757..9f2a5755 100644
--- a/nrel/hive/model/vehicle/mechatronics/powertrain/tabular_powertrain.py
+++ b/nrel/hive/model/vehicle/mechatronics/powertrain/tabular_powertrain.py
@@ -1,14 +1,12 @@
 from dataclasses import dataclass
-from turtle import speed
-from typing import Dict
+from typing import Dict, Any
 
 import numpy as np
 
 from nrel.hive.model.roadnetwork.link import Link
+from nrel.hive.model.roadnetwork.linktraversal import LinkTraversal
 from nrel.hive.model.roadnetwork.routetraversal import Route
-from nrel.hive.model.vehicle.mechatronics.powertrain.powertrain import (
-    Powertrain,
-)
+from nrel.hive.model.vehicle.mechatronics.powertrain.powertrain import Powertrain
 from nrel.hive.util.units import valid_unit, get_unit_conversion
 
 
@@ -22,13 +20,13 @@ class TabularPowertrain(Powertrain):
     distance_units: str
     energy_units: str
 
-    consumption_speed: np.array
-    consumption_energy_per_distance: np.array
+    consumption_speed: np.ndarray
+    consumption_energy_per_distance: np.ndarray
 
     @classmethod
     def from_data(
         self,
-        data: Dict[str, str],
+        data: Dict[str, Any],
     ):
         try:
             scale_factor = float(data["scale_factor"])
@@ -72,7 +70,7 @@ def from_data(
             consumption_energy_per_distance,
         )
 
-    def link_cost(self, link: Link) -> float:
+    def link_cost(self, link: LinkTraversal) -> float:
         """
         uses mph tabular value to calculate energy over a link
 
@@ -83,10 +81,12 @@ def link_cost(self, link: Link) -> float:
         # convert kilometers per hour to whatever units are used by this powertrain
         link_speed = link.speed_kmph * get_unit_conversion("kmph", self.speed_units)
 
-        energy_per_distance = np.interp(
-            link_speed,
-            self.consumption_speed,
-            self.consumption_energy_per_distance,
+        energy_per_distance = float(
+            np.interp(
+                link_speed,
+                self.consumption_speed,
+                self.consumption_energy_per_distance,
+            )
         )
         # link distance is in kilometers
         link_distance = link.distance_km * get_unit_conversion("kilometer", self.distance_units)
diff --git a/nrel/hive/model/vehicle/schedules/__init__.py b/nrel/hive/model/vehicle/schedules/__init__.py
index 8b9e9492..6ba752ba 100644
--- a/nrel/hive/model/vehicle/schedules/__init__.py
+++ b/nrel/hive/model/vehicle/schedules/__init__.py
@@ -1,9 +1,8 @@
 from immutables import Map
 
-from nrel.hive.model.vehicle.schedules.time_range_schedule import (
-    time_range_schedules_from_file,
-)
-from nrel.hive.util.typealiases import ScheduleFunction, ScheduleId
+from nrel.hive.model.vehicle.schedules.time_range_schedule import time_range_schedules_from_file
+from nrel.hive.util.typealiases import ScheduleId
+from nrel.hive.model.vehicle.schedules.schedule import ScheduleFunction
 from nrel.hive.model.vehicle.schedules.schedule_type import ScheduleType
 
 
diff --git a/nrel/hive/model/vehicle/schedules/schedule.py b/nrel/hive/model/vehicle/schedules/schedule.py
new file mode 100644
index 00000000..2a5c57d3
--- /dev/null
+++ b/nrel/hive/model/vehicle/schedules/schedule.py
@@ -0,0 +1,8 @@
+from typing import Callable
+
+from nrel.hive.state.simulation_state.simulation_state import SimulationState
+from nrel.hive.util.typealiases import VehicleId
+
+# if we create a DriverId type, and it has a lookup table on SimulationState, we may
+# want to change this from VehicleId to DriverId
+ScheduleFunction = Callable[[SimulationState, VehicleId], bool]
diff --git a/nrel/hive/model/vehicle/schedules/time_range_schedule.py b/nrel/hive/model/vehicle/schedules/time_range_schedule.py
index 123c1aa3..626d97b1 100644
--- a/nrel/hive/model/vehicle/schedules/time_range_schedule.py
+++ b/nrel/hive/model/vehicle/schedules/time_range_schedule.py
@@ -2,12 +2,13 @@
 from csv import DictReader
 from datetime import datetime
 from pathlib import Path
-from typing import Dict
+from typing import Dict, Optional
 
 from immutables import Map
 
 from nrel.hive.util.time_helpers import read_time_string, time_in_range
-from nrel.hive.util.typealiases import ScheduleFunction, ScheduleId, VehicleId
+from nrel.hive.util.typealiases import VehicleId, ScheduleId
+from nrel.hive.model.vehicle.schedules.schedule import ScheduleFunction
 
 
 def time_range_schedules_from_file(
@@ -55,7 +56,9 @@ def read_time_range_row(acc: Map[ScheduleId, ScheduleFunction], row: Dict):
     :param row: the DictReader row of the time range file
     :return: the updated accumulator
     """
-    schedule_id = row.get("schedule_id")
+    schedule_id: Optional[ScheduleId] = row.get("schedule_id")
+    if schedule_id is None:
+        raise KeyError("must provide schedule id")
     start_time_string = row.get("start_time")
     if not start_time_string:
         raise KeyError("time range file missing start_time column or entry missing")
@@ -65,10 +68,10 @@ def read_time_range_row(acc: Map[ScheduleId, ScheduleFunction], row: Dict):
     start_time = read_time_string(start_time_string)
     end_time = read_time_string(end_time_string)
 
-    def _schedule_fn(sim: "SimulationState", vehicle_id: VehicleId) -> bool:
+    def _schedule_fn(sim, vehicle_id: VehicleId) -> bool:
         sim_time = datetime.utcfromtimestamp(sim.sim_time).time()
         within_scheduled_time = time_in_range(start_time, end_time, sim_time)
         return within_scheduled_time
 
-    updated_schedules = acc.update({schedule_id: _schedule_fn})
+    updated_schedules = acc.set(schedule_id, _schedule_fn)
     return updated_schedules
diff --git a/nrel/hive/model/vehicle/vehicle.py b/nrel/hive/model/vehicle/vehicle.py
index c45dae1c..e56bce7e 100644
--- a/nrel/hive/model/vehicle/vehicle.py
+++ b/nrel/hive/model/vehicle/vehicle.py
@@ -1,10 +1,12 @@
 from __future__ import annotations
 
-from typing import NamedTuple, Dict
+from typing import Dict
+from dataclasses import dataclass, replace
 
 import h3
 import immutables
 
+from nrel.hive.model.entity import Entity
 from nrel.hive.model.energy.energytype import EnergyType
 from nrel.hive.model.membership import Membership
 from nrel.hive.model.entity_position import EntityPosition
@@ -17,7 +19,8 @@
 from nrel.hive.util.units import Kilometers, Currency
 
 
-class Vehicle(NamedTuple):
+@dataclass(frozen=True)
+class Vehicle(Entity):
     """
     Tuple that represents a vehicle in the simulation.
 
@@ -34,13 +37,15 @@ class Vehicle(NamedTuple):
     # core vehicle properties
     id: VehicleId
 
+    # location
+    position: EntityPosition
+
+    membership: Membership
+
     # mechatronic properties
     mechatronics_id: MechatronicsId
     energy: immutables.Map[EnergyType, float]
 
-    # location
-    position: EntityPosition
-
     # vehicle planning/operational properties
     vehicle_state: VehicleState
     driver_state: DriverState
@@ -51,8 +56,6 @@ class Vehicle(NamedTuple):
     balance: Currency = 0.0
     distance_traveled_km: Kilometers = 0.0
 
-    membership: Membership = Membership()
-
     @property
     def geoid(self):
         return self.position.geoid
@@ -118,10 +121,16 @@ def from_row(
                 geoid = h3.geo_to_h3(lat, lon, road_network.sim_h3_resolution)
                 start_position = road_network.position_from_geoid(geoid)
 
+                if start_position is None:
+                    raise IOError(
+                        f"vehicle {vehicle_id} cannot be positioned on the road network; check the input lat/lon"
+                    )
+
                 return Vehicle(
                     id=vehicle_id,
                     mechatronics_id=mechatronics_id,
                     energy=energy,
+                    membership=Membership(),
                     position=start_position,
                     vehicle_state=Idle.build(vehicle_id),
                     driver_state=driver_state,
@@ -142,7 +151,7 @@ def modify_energy(self, energy: immutables.Map[EnergyType, float]) -> Vehicle:
         :param energy:
         :return:
         """
-        return self._replace(energy=energy)
+        return replace(self, energy=energy)
 
     def modify_vehicle_state(self, vehicle_state: VehicleState) -> Vehicle:
         """
@@ -151,7 +160,7 @@ def modify_vehicle_state(self, vehicle_state: VehicleState) -> Vehicle:
         :param vehicle_state:
         :return:
         """
-        return self._replace(vehicle_state=vehicle_state)
+        return replace(self, vehicle_state=vehicle_state)
 
     def modify_driver_state(self, driver_state: DriverState) -> Vehicle:
         """
@@ -160,7 +169,7 @@ def modify_driver_state(self, driver_state: DriverState) -> Vehicle:
         :param driver_state:
         :return:
         """
-        return self._replace(driver_state=driver_state)
+        return replace(self, driver_state=driver_state)
 
     def modify_position(self, position: EntityPosition) -> Vehicle:
         """
@@ -169,7 +178,7 @@ def modify_position(self, position: EntityPosition) -> Vehicle:
         :param position:
         :return:
         """
-        return self._replace(position=position)
+        return replace(self, position=position)
 
     def send_payment(self, amount: Currency) -> Vehicle:
         """
@@ -178,7 +187,7 @@ def send_payment(self, amount: Currency) -> Vehicle:
         :param amount: the amount to pay
         :return: the updated Vehicle
         """
-        return self._replace(balance=self.balance - amount)
+        return replace(self, balance=self.balance - amount)
 
     def receive_payment(self, amount: Currency) -> Vehicle:
         """
@@ -187,7 +196,7 @@ def receive_payment(self, amount: Currency) -> Vehicle:
         :param amount: the amount to be paid
         :return: the updated Vehicle
         """
-        return self._replace(balance=self.balance + amount)
+        return replace(self, balance=self.balance + amount)
 
     def tick_distance_traveled_km(self, delta_d_km: Kilometers) -> Vehicle:
         """
@@ -196,7 +205,7 @@ def tick_distance_traveled_km(self, delta_d_km: Kilometers) -> Vehicle:
         :param delta_d_km:
         :return:
         """
-        return self._replace(distance_traveled_km=self.distance_traveled_km + delta_d_km)
+        return replace(self, distance_traveled_km=self.distance_traveled_km + delta_d_km)
 
     def set_membership(self, member_ids: Tuple[str, ...]) -> Vehicle:
         """
@@ -205,7 +214,7 @@ def set_membership(self, member_ids: Tuple[str, ...]) -> Vehicle:
         :param member_ids: a Tuple containing updated membership(s) of the vehicle
         :return:
         """
-        return self._replace(membership=Membership.from_tuple(member_ids))
+        return replace(self, membership=Membership.from_tuple(member_ids))
 
     def add_membership(self, membership_id: MembershipId) -> Vehicle:
         """
@@ -215,4 +224,4 @@ def add_membership(self, membership_id: MembershipId) -> Vehicle:
         :return:
         """
         updated_membership = self.membership.add_membership(membership_id)
-        return self._replace(membership=updated_membership)
+        return replace(self, membership=updated_membership)
diff --git a/nrel/hive/reporting/driver_event_ops.py b/nrel/hive/reporting/driver_event_ops.py
index df8e98d6..0f5a0d01 100644
--- a/nrel/hive/reporting/driver_event_ops.py
+++ b/nrel/hive/reporting/driver_event_ops.py
@@ -1,9 +1,12 @@
+from typing import TYPE_CHECKING
+
 import h3
 from nrel.hive.model.vehicle.vehicle import Vehicle
 from nrel.hive.reporting.report_type import ReportType
 from nrel.hive.reporting.reporter import Report
-from nrel.hive.runner import Environment
 from nrel.hive.util import wkt
+from nrel.hive.runner import Environment
+from nrel.hive.state.simulation_state.simulation_state import SimulationState
 from enum import Enum
 
 
@@ -13,7 +16,7 @@ class ScheduleEventType(Enum):
 
 
 def driver_schedule_event(
-    sim: "SimulationState",
+    sim: SimulationState,
     env: Environment,
     vehicle: Vehicle,
     schedule_event: ScheduleEventType,
diff --git a/nrel/hive/reporting/handler/eventful_handler.py b/nrel/hive/reporting/handler/eventful_handler.py
index 0b3ef02c..3dfb09a8 100644
--- a/nrel/hive/reporting/handler/eventful_handler.py
+++ b/nrel/hive/reporting/handler/eventful_handler.py
@@ -56,4 +56,3 @@ def handle(self, reports: List[Report], runner_payload: RunnerPayload):
 
     def close(self, runner_payload: RunnerPayload):
         self.log_file.close()
-        self.log_file = None
diff --git a/nrel/hive/reporting/handler/instruction_handler.py b/nrel/hive/reporting/handler/instruction_handler.py
index 385ae043..95c887dd 100644
--- a/nrel/hive/reporting/handler/instruction_handler.py
+++ b/nrel/hive/reporting/handler/instruction_handler.py
@@ -43,4 +43,3 @@ def handle(self, reports: List[Report], runner_payload: RunnerPayload):
 
     def close(self, runner_payload: RunnerPayload):
         self.log_file.close()
-        self.log_file = None
diff --git a/nrel/hive/reporting/handler/stateful_handler.py b/nrel/hive/reporting/handler/stateful_handler.py
index f60961ec..ce7a85c3 100644
--- a/nrel/hive/reporting/handler/stateful_handler.py
+++ b/nrel/hive/reporting/handler/stateful_handler.py
@@ -1,7 +1,7 @@
 import json
-import os
 from pathlib import Path
 from typing import List
+from dataclasses import asdict
 
 from nrel.hive.config.global_config import GlobalConfig
 from nrel.hive.model.station.station import Station
@@ -10,7 +10,6 @@
 from nrel.hive.reporting.report_type import ReportType
 from nrel.hive.reporting.reporter import Report
 from nrel.hive.runner import RunnerPayload
-from nrel.hive.state.driver_state.driver_state import DriverState
 
 
 class StatefulHandler(Handler):
@@ -61,7 +60,6 @@ def handle(self, reports: List[Report], runner_payload: RunnerPayload):
 
     def close(self, runner_payload: RunnerPayload):
         self.log_file.close()
-        self.log_file = None
 
     @staticmethod
     def driver_asdict(vehicle: Vehicle) -> dict:
@@ -87,9 +85,9 @@ def vehicle_asdict(vehicle: Vehicle) -> dict:
         }
 
         # deconstruct energy source
-        for key, val in vehicle.energy.items():
-            new_key = "energy_" + key.name
-            output[new_key] = val
+        for energy_type, energy_val in vehicle.energy.items():
+            new_key = "energy_" + energy_type.name
+            output[new_key] = energy_val
         # del (output['energy'])
 
         # deconstruct link
@@ -102,7 +100,7 @@ def vehicle_asdict(vehicle: Vehicle) -> dict:
 
     @staticmethod
     def station_asdict(station: Station) -> dict:
-        out_dict = station._asdict()
+        out_dict = asdict(station)
         del out_dict["id"]
         del out_dict["state"]
 
diff --git a/nrel/hive/reporting/handler/summary_stats.py b/nrel/hive/reporting/handler/summary_stats.py
index 2ab6d4b1..323b6597 100644
--- a/nrel/hive/reporting/handler/summary_stats.py
+++ b/nrel/hive/reporting/handler/summary_stats.py
@@ -4,9 +4,8 @@
 from collections import Counter
 from dataclasses import dataclass, field
 from functools import reduce
-from typing import TYPE_CHECKING, Dict
-
-import numpy as np
+from typing import TYPE_CHECKING, Dict, Any
+from statistics import mean
 
 if TYPE_CHECKING:
     from nrel.hive.runner.runner_payload import RunnerPayload
@@ -30,7 +29,7 @@ class SummaryStats:
     station_revenue: float = 0
     fleet_revenue: float = 0
 
-    def compile_stats(self, rp: RunnerPayload) -> Dict[str, float]:
+    def compile_stats(self, rp: RunnerPayload) -> Dict[str, Any]:
         """
         computes all stats based on values accumulated throughout this run
         :return: a dictionary with stat values by key
@@ -39,9 +38,9 @@ def compile_stats(self, rp: RunnerPayload) -> Dict[str, float]:
         sim_state = rp.s
         env = rp.e
 
-        self.mean_final_soc = np.mean(
+        self.mean_final_soc = mean(
             [
-                env.mechatronics.get(v.mechatronics_id).fuel_source_soc(v)
+                env.mechatronics[v.mechatronics_id].fuel_source_soc(v)
                 for v in sim_state.vehicles.values()
             ]
         )
@@ -58,17 +57,21 @@ def compile_stats(self, rp: RunnerPayload) -> Dict[str, float]:
             0.0,
         )
 
-        requests_served_percent = (
-            1 - (self.cancelled_requests / self.requests) if self.requests > 0 else 0
-        )
+        if self.requests > 0:
+            requests_served_percent = 1 - (self.cancelled_requests / self.requests)
+        else:
+            requests_served_percent = 0.0
+
         total_state_count = sum(self.state_count.values())
         total_vkt = sum(self.vkt.values())
         vehicle_state_output = {}
         vehicle_states_observed = set(self.state_count.keys()).union(self.vkt.keys())
         for v in vehicle_states_observed:
-            observed_pct = (
-                self.state_count.get(v) / total_state_count if self.state_count.get(v) else 0
-            )
+            state_count = self.state_count.get(v)
+            if state_count is None:
+                observed_pct = 0.0
+            else:
+                observed_pct = state_count / total_state_count
             vkt = self.vkt.get(v, 0)
             data = {"observed_percent": observed_pct, "vkt": vkt}
             vehicle_state_output.update({v: data})
diff --git a/nrel/hive/reporting/handler/time_step_stats_handler.py b/nrel/hive/reporting/handler/time_step_stats_handler.py
index fa274fd4..994d9d53 100644
--- a/nrel/hive/reporting/handler/time_step_stats_handler.py
+++ b/nrel/hive/reporting/handler/time_step_stats_handler.py
@@ -8,7 +8,7 @@
 import pandas as pd
 from pandas import DataFrame
 from pathlib import Path
-from typing import TYPE_CHECKING, Callable, FrozenSet, List, Optional
+from typing import TYPE_CHECKING, Any, Callable, Dict, FrozenSet, List, Optional
 
 from nrel.hive.reporting.handler.handler import Handler
 from nrel.hive.reporting.report_type import ReportType
@@ -29,7 +29,7 @@ def __init__(
         self,
         config: HiveConfig,
         scenario_output_directory: Path,
-        fleet_ids: FrozenSet[MembershipId],
+        fleet_ids: FrozenSet[Optional[MembershipId]],
         file_name: Optional[str] = "time_step_stats",
     ):
         self.file_name = file_name
@@ -41,7 +41,7 @@ def __init__(
 
         if config.global_config.log_time_step_stats:
             self.log_time_step_stats = True
-            self.data = []
+            self.data: List[Dict[str, Any]] = []
             self.time_step_stats_outpath = scenario_output_directory.joinpath(
                 f"{file_name}_all.csv"
             )
@@ -53,10 +53,12 @@ def __init__(
             self.fleets_timestep_stats_outpath = scenario_output_directory.joinpath(
                 "fleet_time_step_stats/"
             )
-            self.fleets_data = {}
+            self.fleets_data: Dict[str, List[Dict[str, Any]]] = {}
             for fleet_id in fleet_ids:
-                self.fleets_data[fleet_id] = []
-            self.fleets_data["none"] = []
+                if fleet_id is None:
+                    self.fleets_data["none"] = []
+                else:
+                    self.fleets_data[fleet_id] = []
         else:
             self.log_fleet_time_step_stats = False
 
@@ -73,14 +75,12 @@ def get_time_step_stats(self) -> Optional[DataFrame]:
 
     def get_fleet_time_step_stats(
         self,
-    ) -> Optional[Map[MembershipId, DataFrame]]:
+    ) -> Map[MembershipId, DataFrame]:
         """
         return an immutable map of time step stat DataFrames by membership id.
 
         :return: the immutable map containing time step stats DataFrames by membership id
         """
-        if not self.log_fleet_time_step_stats:
-            return None
         result = Map(
             {
                 fleet_id: DataFrame(data) if len(data) > 0 else None
@@ -110,7 +110,7 @@ def handle(self, reports: List[Report], runner_payload: RunnerPayload):
         )
 
         # sort reports by type
-        reports_by_type = {}
+        reports_by_type: Dict[ReportType, List[Report]] = {}
         for report in reports:
             if report.report_type not in reports_by_type.keys():
                 reports_by_type[report.report_type] = []
@@ -155,7 +155,7 @@ def handle(self, reports: List[Report], runner_payload: RunnerPayload):
             if len(sim_state.get_vehicles()) > 0:
                 stats_row["avg_soc_percent"] = 100 * np.mean(
                     [
-                        env.mechatronics.get(v.mechatronics_id).fuel_source_soc(v)
+                        env.mechatronics[v.mechatronics_id].fuel_source_soc(v)
                         for v in sim_state.get_vehicles()
                     ]
                 )
@@ -166,7 +166,7 @@ def handle(self, reports: List[Report], runner_payload: RunnerPayload):
             if ReportType.VEHICLE_MOVE_EVENT in reports_by_type.keys():
                 stats_row["vkt"] = sum(
                     [
-                        me.report["distance_km"]
+                        float(me.report["distance_km"])
                         for me in reports_by_type[ReportType.VEHICLE_MOVE_EVENT]
                     ]
                 )
@@ -184,7 +184,7 @@ def handle(self, reports: List[Report], runner_payload: RunnerPayload):
 
             # get count of requests currently being serviced by a vehicle
             pooling_request_count = sum(
-                [len(v.vehicle_state.boarded_requests) for v in veh_pooling]
+                [len(v.vehicle_state.boarded_requests) for v in veh_pooling]  # type: ignore
             )
             stats_row["servicing_requests"] = (
                 veh_state_counts[VehicleStateType.SERVICING_TRIP.name] + pooling_request_count
@@ -297,7 +297,7 @@ def _get_report_filter_func(
                 if len(veh_in_fleet) > 0:
                     fleet_stats_row["avg_soc_percent"] = 100 * np.mean(
                         [
-                            env.mechatronics.get(v.mechatronics_id).fuel_source_soc(v)
+                            env.mechatronics[v.mechatronics_id].fuel_source_soc(v)
                             for v in veh_in_fleet
                         ]
                     )
@@ -308,7 +308,7 @@ def _get_report_filter_func(
                 if ReportType.VEHICLE_MOVE_EVENT in requests_in_fleet.keys():
                     fleet_stats_row["vkt"] = sum(
                         [
-                            me.report["distance_km"]
+                            float(me.report["distance_km"])
                             for me in requests_in_fleet[ReportType.VEHICLE_MOVE_EVENT]
                         ]
                     )
@@ -318,7 +318,7 @@ def _get_report_filter_func(
                 # get number of assigned requests in this fleet
                 assigned_requests = veh_state_counts_in_fleet[VehicleStateType.DISPATCH_TRIP.name]
                 assigned_pooling_requests = sum(
-                    [len(v.vehicle_state.trip_plan) for v in veh_dispatch_pooling_in_fleet]
+                    [len(v.vehicle_state.trip_plan) for v in veh_dispatch_pooling_in_fleet]  # type: ignore
                 )
                 fleet_stats_row["assigned_requests"] = assigned_requests + assigned_pooling_requests
 
@@ -331,7 +331,7 @@ def _get_report_filter_func(
                 # get count of requests currently being serviced by a vehicle
                 servicing_requests = veh_state_counts_in_fleet[VehicleStateType.SERVICING_TRIP.name]
                 servicing_pooling_requests = sum(
-                    [len(v.vehicle_state.boarded_requests) for v in veh_pooling_in_fleet]
+                    [len(v.vehicle_state.boarded_requests) for v in veh_pooling_in_fleet]  # type: ignore
                 )
                 fleet_stats_row["servicing_requests"] = (
                     servicing_requests + servicing_pooling_requests
diff --git a/nrel/hive/reporting/handler/vehicle_charge_events_handler.py b/nrel/hive/reporting/handler/vehicle_charge_events_handler.py
index 5b41e210..7970444e 100644
--- a/nrel/hive/reporting/handler/vehicle_charge_events_handler.py
+++ b/nrel/hive/reporting/handler/vehicle_charge_events_handler.py
@@ -1,4 +1,4 @@
-from typing import List
+from typing import List, Dict
 
 import pandas as pd
 from nrel.hive.reporting.handler.handler import Handler
@@ -14,7 +14,7 @@ class VehicleChargeEventsHandler(Handler):
     """
 
     def __init__(self) -> None:
-        self.prototype = {
+        self.prototype: Dict[str, List] = {
             "vehicle_id": [],
             "sim_time_start": [],
             "sim_time_end": [],
diff --git a/nrel/hive/reporting/instruction_generator_event_ops.py b/nrel/hive/reporting/instruction_generator_event_ops.py
index 89d4e588..0ef32aad 100644
--- a/nrel/hive/reporting/instruction_generator_event_ops.py
+++ b/nrel/hive/reporting/instruction_generator_event_ops.py
@@ -10,9 +10,7 @@
 if TYPE_CHECKING:
     from nrel.hive.model.vehicle.vehicle import Vehicle
     from nrel.hive.runner import Environment
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 
 def refuel_search_event(vehicle: Vehicle, sim: SimulationState, env: Environment) -> Report:
@@ -33,8 +31,8 @@ def refuel_search_event(vehicle: Vehicle, sim: SimulationState, env: Environment
             "vehicle_id": vehicle.id,
             "vehicle_state": vehicle.vehicle_state.__class__.__name__,
             "vehicle_memberships": vehicle.membership.to_json(),
-            "sim_time_start": sim.sim_time,
-            "sim_time_end": next_sim_time,
+            "sim_time_start": str(sim.sim_time),
+            "sim_time_end": str(next_sim_time),
             "lat": lat,
             "lon": lon,
             "geoid": vehicle.geoid,
diff --git a/nrel/hive/reporting/reporter.py b/nrel/hive/reporting/reporter.py
index 8699dc3b..14c6c5d1 100644
--- a/nrel/hive/reporting/reporter.py
+++ b/nrel/hive/reporting/reporter.py
@@ -6,15 +6,12 @@
 
 from nrel.hive.reporting.report_type import ReportType
 from nrel.hive.reporting.handler.stats_handler import StatsHandler
-from nrel.hive.reporting.handler.time_step_stats_handler import (
-    TimeStepStatsHandler,
-)
+from nrel.hive.reporting.handler.time_step_stats_handler import TimeStepStatsHandler
 
 if TYPE_CHECKING:
     from nrel.hive.model.membership import MembershipId
     from nrel.hive.runner.runner_payload import RunnerPayload
-    from nrel.hive.reporting.handler import Handler
-    from nrel.hive.config.global_config import GlobalConfig
+    from nrel.hive.reporting.handler.handler import Handler
 
 
 class Report(NamedTuple):
@@ -33,8 +30,8 @@ class Reporter:
     """
 
     def __init__(self):
-        self.reports: List[Report] = []
-        self.handlers: List[Handler] = []
+        self.reports = []
+        self.handlers = []
 
     def add_handler(self, handler: Handler):
         self.handlers.append(handler)
diff --git a/nrel/hive/reporting/reporter_ops.py b/nrel/hive/reporting/reporter_ops.py
index 0e59167b..4132e2bc 100644
--- a/nrel/hive/reporting/reporter_ops.py
+++ b/nrel/hive/reporting/reporter_ops.py
@@ -1,7 +1,7 @@
 import csv
 import functools as ft
 from pathlib import Path
-from typing import Tuple
+from typing import Dict, Tuple, Any
 
 import immutables
 from returns.io import IOResult, IOResultE
@@ -21,7 +21,7 @@ def log_station_capacities(sim: SimulationState, env: Environment) -> IOResultE[
     :return: nothing, or, any exception
     """
 
-    def _station_energy(station: Station) -> dict:
+    def _station_energy(station: Station) -> Dict[str, Any]:
         """
         get the Kw capacity of a given station
 
@@ -39,7 +39,7 @@ def _station_energy(station: Station) -> dict:
         return {"station_id": station.id, "rate": rate}
 
     try:
-        result: Tuple[immutables.Map, ...] = ft.reduce(
+        result: Tuple[Dict[str, Any], ...] = ft.reduce(
             lambda acc, s: acc + (_station_energy(s),),
             sim.stations.values(),
             (),
diff --git a/nrel/hive/reporting/vehicle_event_ops.py b/nrel/hive/reporting/vehicle_event_ops.py
index 57fafb45..ac8afea6 100644
--- a/nrel/hive/reporting/vehicle_event_ops.py
+++ b/nrel/hive/reporting/vehicle_event_ops.py
@@ -19,9 +19,7 @@
 
 if TYPE_CHECKING:
     from nrel.hive.model.request.request import Request
-    from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import (
-        MechatronicsInterface,
-    )
+    from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import MechatronicsInterface
     from nrel.hive.state.vehicle_state.vehicle_state_ops import MoveResult
 
 
@@ -51,14 +49,14 @@ def vehicle_move_event(
             f"Energy types do not match: {set(prev_vehicle.energy.keys())} != {set(next_vehicle.energy.keys())}"
         )
     elif len(next_vehicle.energy.keys()) > 1:
-        raise NotImplemented("hive doesn't currently support multiple energy types")
+        raise NotImplementedError("hive doesn't currently support multiple energy types")
     else:
         energy_units = list(next_vehicle.energy.keys())[0].units
 
     delta_energy = ft.reduce(
-        lambda acc, e_type: acc + next_vehicle.energy.get(e_type) - prev_vehicle.energy.get(e_type),
+        lambda acc, e_type: acc + next_vehicle.energy[e_type] - prev_vehicle.energy[e_type],
         next_vehicle.energy.keys(),
-        0,
+        0.0,
     )
 
     geoid = next_vehicle.geoid
@@ -232,7 +230,7 @@ def report_dropoff_request(vehicle: Vehicle, sim: SimulationState, request: Requ
 
 
 def construct_station_load_events(
-    reports: Tuple[Report], sim: SimulationState
+    reports: Tuple[Report, ...], sim: SimulationState
 ) -> Tuple[Report, ...]:
     """
     a station load report takes any vehicle charge events and attributes them to a
@@ -268,21 +266,21 @@ def _add(acc: immutables.Map, report: Report) -> immutables.Map:
 
             return updated_acc
 
-    def _to_reports(acc: immutables.Map[StationId, float]) -> Tuple[Report, ...]:
+    def _to_reports(acc: immutables.Map[StationId, Tuple[float, str]]) -> Tuple[Report, ...]:
         """
         transforms the accumulated values into Reports
         :return: a collection of STATION_LOAD_EVENT reports
         """
 
         def _cast_as_report(station_id: StationId):
-            energy, energy_units = acc.get(station_id)
+            energy, energy_units = acc[station_id]
             report = Report(
                 report_type=ReportType.STATION_LOAD_EVENT,
                 report={
                     "station_id": station_id,
-                    "sim_time_start": sim_time_start,
-                    "sim_time_end": sim_time_end,
-                    "energy": energy,
+                    "sim_time_start": str(sim_time_start),
+                    "sim_time_end": str(sim_time_end),
+                    "energy": str(energy),
                     "energy_units": energy_units,
                 },
             )
@@ -292,7 +290,9 @@ def _cast_as_report(station_id: StationId):
         return these_reports
 
     # collect vehicle charge events
-    reported_charge_events_accumulator = ft.reduce(_add, reports, immutables.Map())
+    reported_charge_events_accumulator: immutables.Map[StationId, Tuple[float, str]] = ft.reduce(
+        _add, reports, immutables.Map()
+    )
 
     # create entries for stations with no charge events reported
     reported_stations: Set[StationId] = set(reported_charge_events_accumulator.keys())
diff --git a/nrel/hive/resources/mock_lobster.py b/nrel/hive/resources/mock_lobster.py
index 2c6e66dd..bd2fb211 100644
--- a/nrel/hive/resources/mock_lobster.py
+++ b/nrel/hive/resources/mock_lobster.py
@@ -1,57 +1,39 @@
-import functools as ft
-import math
 import tempfile
 from pathlib import Path
-from typing import Dict
+from typing import Dict, Union, Callable
 
 import h3
 import immutables
 import yaml
 from pkg_resources import resource_filename
 
-from returns.result import ResultE, Success, Failure
 
 from nrel.hive.config import HiveConfig
 from nrel.hive.dispatcher.forecaster.forecast import Forecast, ForecastType
-from nrel.hive.dispatcher.forecaster.forecaster_interface import (
-    ForecasterInterface,
-)
+from nrel.hive.dispatcher.forecaster.forecaster_interface import ForecasterInterface
 from nrel.hive.dispatcher.instruction.instructions import *
-from nrel.hive.dispatcher.instruction_generator.charging_fleet_manager import (
-    ChargingFleetManager,
-)
+from nrel.hive.dispatcher.instruction_generator.charging_fleet_manager import ChargingFleetManager
 from nrel.hive.dispatcher.instruction_generator.dispatcher import Dispatcher
-from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
-    InstructionGenerator,
-)
+from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
 from nrel.hive.model.base import Base
 from nrel.hive.model.energy.charger import Charger
 from nrel.hive.model.energy.energytype import EnergyType
 from nrel.hive.model.membership import Membership
 from nrel.hive.model.request import Request, RequestRateStructure
 from nrel.hive.model.roadnetwork.geofence import GeoFence
-from nrel.hive.model.roadnetwork.haversine_roadnetwork import (
-    HaversineRoadNetwork,
-)
+from nrel.hive.model.roadnetwork.haversine_roadnetwork import HaversineRoadNetwork
 from nrel.hive.model.roadnetwork.link import Link
 from nrel.hive.model.roadnetwork.osm.osm_roadnetwork import OSMRoadNetwork
 from nrel.hive.model.roadnetwork.roadnetwork import RoadNetwork
-from nrel.hive.model.roadnetwork.route import Route
 from nrel.hive.model.sim_time import SimTime
 from nrel.hive.model.station.station import Station
 from nrel.hive.model.vehicle.mechatronics.bev import BEV
 from nrel.hive.model.vehicle.mechatronics.ice import ICE
-from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import (
-    MechatronicsInterface,
-)
-from nrel.hive.model.vehicle.mechatronics.powercurve.tabular_powercurve import (
-    TabularPowercurve,
-)
-from nrel.hive.model.vehicle.mechatronics.powertrain.tabular_powertrain import (
-    TabularPowertrain,
-)
+from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import MechatronicsInterface
+from nrel.hive.model.vehicle.mechatronics.powercurve.tabular_powercurve import TabularPowercurve
+from nrel.hive.model.vehicle.mechatronics.powertrain.tabular_powertrain import TabularPowertrain
 from nrel.hive.model.vehicle.vehicle import Vehicle
-from nrel.hive.reporting.reporter import Reporter
+from nrel.hive.reporting.reporter import Reporter, Report
 from nrel.hive.runner.environment import Environment
 from nrel.hive.runner.runner_payload import RunnerPayload
 from nrel.hive.state.driver_state.autonomous_driver_state.autonomous_available import (
@@ -70,15 +52,11 @@
 )
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.simulation_state.update.step_simulation import (
-    StepSimulation,
-)
+from nrel.hive.state.simulation_state.update.step_simulation import StepSimulation
 from nrel.hive.state.simulation_state.update.update import Update
 from nrel.hive.state.vehicle_state.vehicle_state import VehicleState
-from nrel.hive.util.h3_ops import H3Ops
 from nrel.hive.util.typealiases import *
 from nrel.hive.util.units import *
-from nrel.hive.util.fp import throw_or_return
 
 
 class DefaultIds:
@@ -220,15 +198,6 @@ def mock_station(
         road_network=road_network,
         membership=membership,
     )
-    # return Station.build(
-    #     id=station_id,
-    #     geoid=h3.geo_to_h3(lat, lon, h3_res),
-    #     road_network=road_network,
-    #     charger=mock_l2_charger(),
-    #     charger_count=1,
-    #     on_shift_access=on_shift_access_chargers,
-    #     membership=membership
-    # ).add_charger(mock_dcfc_charger())
 
 
 def mock_station_from_geoid(
@@ -280,7 +249,7 @@ def mock_request(
     d_lon: float = -104.978,
     h3_res: int = 15,
     road_network: RoadNetwork = mock_network(),
-    departure_time: SimTime = 0,
+    departure_time: SimTime = SimTime(0),
     passengers: int = 1,
     fleet_id: Optional[MembershipId] = None,
     allows_pooling: bool = False,
@@ -411,6 +380,10 @@ def mock_vehicle(
         else AutonomousAvailable(AutonomousDriverAttributes(vehicle_id))
     )
     position = road_network.position_from_geoid(geoid)
+
+    if position is None:
+        raise ValueError(f"geoid {geoid} is outside of boundary of road network")
+
     return Vehicle(
         id=vehicle_id,
         mechatronics_id=mechatronics.mechatronics_id,
@@ -441,6 +414,10 @@ def mock_vehicle_from_geoid(
         else AutonomousAvailable(AutonomousDriverAttributes(vehicle_id))
     )
     position = mock_network().position_from_geoid(geoid)
+
+    if position is None:
+        raise ValueError(f"geoid {geoid} is outside of boundary of road network")
+
     return Vehicle(
         id=vehicle_id,
         mechatronics_id=mechatronics.mechatronics_id,
@@ -501,10 +478,10 @@ def mock_config(
     timestep_duration_seconds: Seconds = 1,
     sim_h3_location_resolution: int = 15,
     sim_h3_search_resolution: int = 9,
-    input: Dict = None,
+    input_config: Optional[Dict] = None,
 ) -> HiveConfig:
-    if not input:
-        input = {
+    if not input_config:
+        input_config = {
             "vehicles_file": "denver_demo_vehicles.csv",
             "requests_file": "denver_demo_requests.csv",
             "bases_file": "denver_demo_bases.csv",
@@ -533,11 +510,14 @@ def mock_config(
                 "sim_h3_search_resolution": sim_h3_search_resolution,
                 "sim_name": "test_sim",
             },
-            "input": input,
+            "input": input_config,
             "network": {},
             "dispatcher": {},
         },
     )
+    if isinstance(conf_without_temp_dir, Exception):
+        raise conf_without_temp_dir
+
     updated_global = conf_without_temp_dir.global_config._replace(
         output_base_directory=test_output_directory.name
     )
@@ -548,7 +528,7 @@ def mock_config(
 def mock_env(
     config: HiveConfig = mock_config(),
     mechatronics: Optional[Dict[MechatronicsId, MechatronicsInterface]] = None,
-    chargers: Optional[Dict[Charger, Charger]] = None,
+    chargers: Optional[Dict[ChargerId, Charger]] = None,
     schedules: Optional[Dict[ScheduleId, Callable[["SimulationState", VehicleId], bool]]] = None,
     fleet_ids: FrozenSet[MembershipId] = frozenset([DefaultIds.mock_membership_id()]),
 ) -> Environment:
@@ -558,11 +538,15 @@ def mock_env(
         }
 
     if chargers is None:
-        chargers = {
-            mock_l1_charger_id(): mock_l1_charger(),
-            mock_l2_charger_id(): mock_l2_charger(),
-            mock_dcfc_charger_id(): mock_dcfc_charger(),
-        }
+        env_chargers = immutables.Map(
+            {
+                mock_l1_charger_id(): mock_l1_charger(),
+                mock_l2_charger_id(): mock_l2_charger(),
+                mock_dcfc_charger_id(): mock_dcfc_charger(),
+            }
+        )
+    else:
+        env_chargers = immutables.Map(chargers)
 
     if schedules is None:
 
@@ -575,7 +559,7 @@ def always_on_schedule(a, b):
         config=config,
         reporter=mock_reporter(),
         mechatronics=immutables.Map(mechatronics),
-        chargers=immutables.Map(chargers),
+        chargers=env_chargers,
         schedules=immutables.Map(schedules),
         fleet_ids=fleet_ids,
     )
@@ -588,10 +572,10 @@ class MockReporter(Reporter):
         def __init__(self):
             super().__init__()
 
-        def flush(self, sim_state: SimulationState):
+        def flush(self, runner_payload: RunnerPayload):
             pass
 
-        def file_report(self, report: dict):
+        def file_report(self, report: Report):
             pass
 
         def close(self, runner_payload: RunnerPayload):
@@ -600,60 +584,6 @@ def close(self, runner_payload: RunnerPayload):
     return MockReporter()
 
 
-def mock_haversine_zigzag_route(
-    n: int = 3,
-    lat_step_size: int = 5,
-    lon_step_size: int = 5,
-    speed_kmph: Kmph = 40,
-    h3_res: int = 15,
-) -> Route:
-    """
-    "zigs" lat steps and "zags" lon steps. all your base belong to us.
-
-    :param n: number of steps
-
-    :param lat_step_size: lat-wise step size
-
-    :param lon_step_size: lon-wise step size
-
-    :param speed_kmph: road speed
-
-    :param h3_res: h3 resolution
-    :return: a route
-    """
-
-    def step(acc: Tuple[Link, ...], i: int) -> Tuple[Link, ...]:
-        """
-        constructs the next Link
-
-        :param acc: the route so far
-
-        :param i: what link we are making
-        :return: the route with another link added
-        """
-        lat_pos, lon_pos = (
-            math.floor(i / 2.0) * lat_step_size,
-            math.floor((i + 1) / 2.0) * lon_step_size,
-        )
-        lat_dest, lon_dest = (
-            math.floor((i + 1) / 2.0) * lat_step_size,
-            math.floor((i + 2) / 2.0) * lon_step_size,
-        )
-        start = h3.geo_to_h3(lat_pos, lon_pos, h3_res)
-        end = h3.geo_to_h3(lat_dest, lon_dest, h3_res)
-        distance_km = H3Ops.great_circle_distance(start, end)
-        link = Link(
-            link_id=f"link_{i}",
-            start=start,
-            end=end,
-            distance_km=distance_km,
-            speed_kmph=speed_kmph,
-        )
-        return acc + (link,)
-
-    return ft.reduce(step, range(0, n), ())
-
-
 def mock_route_from_geoids(src: GeoId, dst: GeoId, speed_kmph: Kmph = 1) -> Tuple[Link, ...]:
     link = Link.build("1", src, dst, speed_kmph=speed_kmph)
     return (link,)
@@ -694,7 +624,7 @@ def mock_route(h3_res: int = 15, speed_kmph: Kmph = 1) -> Tuple[Link, ...]:
 
 
 def mock_forecaster(forecast: int = 1) -> ForecasterInterface:
-    class MockForecaster(NamedTuple, ForecasterInterface):
+    class MockForecaster(ForecasterInterface):
         def generate_forecast(
             self, simulation_state: SimulationState
         ) -> Tuple[ForecasterInterface, Forecast]:
diff --git a/nrel/hive/runner/environment.py b/nrel/hive/runner/environment.py
index d775e366..d496244e 100644
--- a/nrel/hive/runner/environment.py
+++ b/nrel/hive/runner/environment.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import NamedTuple, TYPE_CHECKING, FrozenSet
+from typing import NamedTuple, TYPE_CHECKING, FrozenSet, Optional
 
 from nrel.hive.reporting.reporter import Reporter
 
@@ -8,16 +8,13 @@
 
 if TYPE_CHECKING:
     from nrel.hive.model.energy.charger.charger import Charger
-    from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import (
-        MechatronicsInterface,
-    )
+    from nrel.hive.model.vehicle.mechatronics.mechatronics_interface import MechatronicsInterface
     from nrel.hive.config import HiveConfig
+    from nrel.hive.model.vehicle.schedules.schedule import ScheduleFunction
     from nrel.hive.util.typealiases import (
         ChargerId,
         MechatronicsId,
         MembershipId,
-        MembershipMap,
-        ScheduleFunction,
         ScheduleId,
     )
 
@@ -32,7 +29,7 @@ class Environment(NamedTuple):
     mechatronics: immutables.Map[MechatronicsId, MechatronicsInterface] = immutables.Map()
     chargers: immutables.Map[ChargerId, Charger] = immutables.Map()
     schedules: immutables.Map[ScheduleId, ScheduleFunction] = immutables.Map()
-    fleet_ids: FrozenSet[MembershipId] = frozenset()
+    fleet_ids: FrozenSet[Optional[MembershipId]] = frozenset()
 
     reporter: Reporter = Reporter()
 
diff --git a/nrel/hive/runner/runner_payload.py b/nrel/hive/runner/runner_payload.py
index 1a685df8..60648394 100644
--- a/nrel/hive/runner/runner_payload.py
+++ b/nrel/hive/runner/runner_payload.py
@@ -6,9 +6,7 @@
 if TYPE_CHECKING:
     from nrel.hive.state.simulation_state.update.update import Update
     from nrel.hive.runner.environment import Environment
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 
 class RunnerPayload(NamedTuple):
diff --git a/nrel/hive/runner/runner_payload_ops.py b/nrel/hive/runner/runner_payload_ops.py
index 0ddc3201..ea43a9b2 100644
--- a/nrel/hive/runner/runner_payload_ops.py
+++ b/nrel/hive/runner/runner_payload_ops.py
@@ -7,11 +7,9 @@
     modify_entities_safe as _modify_entities_safe,
 )
 
-from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
-    InstructionGenerator,
-)
+from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
 from nrel.hive.util.fp import throw_or_return
-from nrel.hive.util.typealiases import Entity
+from nrel.hive.model.entity import Entity
 
 
 def update_instruction_generator_safe(
diff --git a/nrel/hive/state/driver_state/autonomous_driver_state/autonomous_available.py b/nrel/hive/state/driver_state/autonomous_driver_state/autonomous_available.py
index 4d26b99b..bb03da28 100644
--- a/nrel/hive/state/driver_state/autonomous_driver_state/autonomous_available.py
+++ b/nrel/hive/state/driver_state/autonomous_driver_state/autonomous_available.py
@@ -2,6 +2,7 @@
 from dataclasses import dataclass
 
 import logging
+from multiprocessing.sharedctypes import Value
 from typing import Tuple, Optional, TYPE_CHECKING
 
 from nrel.hive.dispatcher.instruction.instruction import Instruction
@@ -20,9 +21,7 @@
 from nrel.hive.util import BaseId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
     from nrel.hive.util.typealiases import ScheduleId
 
@@ -35,7 +34,7 @@ class AutonomousAvailable(DriverState):
     an autonomous driver that is available to work
     """
 
-    attributes: AutonomousDriverAttributes = AutonomousDriverAttributes
+    attributes: AutonomousDriverAttributes
 
     @property
     def schedule_id(cls) -> Optional[ScheduleId]:
@@ -62,6 +61,10 @@ def generate_instruction(
 
         my_vehicle = sim.vehicles.get(self.attributes.vehicle_id)
 
+        if my_vehicle is None:
+            log.error(f"vehicle {self.attributes.vehicle_id} not found for driver")
+            return None
+
         if isinstance(my_vehicle.vehicle_state, ReserveBase):
             return av_charge_base_instruction(my_vehicle, sim, env)
         elif isinstance(my_vehicle.vehicle_state, Idle):
diff --git a/nrel/hive/state/driver_state/driver_instruction_ops.py b/nrel/hive/state/driver_state/driver_instruction_ops.py
index aac49b87..c1992ae7 100644
--- a/nrel/hive/state/driver_state/driver_instruction_ops.py
+++ b/nrel/hive/state/driver_state/driver_instruction_ops.py
@@ -1,7 +1,8 @@
 from __future__ import annotations
 
 import logging
-from typing import Optional, TYPE_CHECKING
+from turtle import home
+from typing import Optional, TYPE_CHECKING, Tuple
 
 import h3
 
@@ -12,23 +13,24 @@
     IdleInstruction,
     RepositionInstruction,
 )
-from nrel.hive.model.roadnetwork.link import Link
 from nrel.hive.dispatcher.instruction_generator.instruction_generator_ops import (
     instruct_vehicles_to_dispatch_to_station,
     get_nearest_valid_station_distance,
 )
+from nrel.hive.model.entity import Entity
 from nrel.hive.util import TupleOps, H3Ops
+from nrel.hive.state.vehicle_state.idle import Idle
+from nrel.hive.state.vehicle_state.charging_base import ChargingBase
+from nrel.hive.state.vehicle_state.reserve_base import ReserveBase
 from nrel.hive.model.energy.energytype import EnergyType
-from nrel.hive.model.roadnetwork.route import route_distance_km
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
     from nrel.hive.model.vehicle.vehicle import Vehicle
     from nrel.hive.model.base import Base
-    from nrel.hive.util.typealiases import GeoId, BaseId
+    from nrel.hive.model.energy.charger import Charger
+    from nrel.hive.model.entity_position import EntityPosition
 
 log = logging.getLogger(__name__)
 
@@ -49,9 +51,17 @@ def human_charge_at_home(
     :return:
     """
 
+    if home_base.station_id is None:
+        # can't charge at home, no station
+        return None
+
     my_station = sim.stations.get(home_base.station_id)
     my_mechatronics = env.mechatronics.get(veh.mechatronics_id)
 
+    if my_mechatronics is None:
+        log.error(f"no mechatronics {veh.mechatronics_id} found for vehicle {veh.id}")
+        return None
+
     if not my_station:
         log.error(f"could not find station {home_base.station_id} for home_base {home_base.id}")
         return None
@@ -60,7 +70,7 @@ def human_charge_at_home(
 
     chargers = tuple(
         filter(
-            lambda c: my_mechatronics.valid_charger(c),
+            my_mechatronics.valid_charger,
             [env.chargers[cid] for cid in my_station.state.keys()],
         )
     )
@@ -68,7 +78,7 @@ def human_charge_at_home(
         return None
     else:
         # take the lowest power charger
-        charger = sorted(chargers, key=lambda c: c.rate)[0]
+        charger: Charger = sorted(chargers, key=lambda c: c.rate)[0]
         return ChargeBaseInstruction(veh.id, home_base.id, charger.id)
 
 
@@ -90,6 +100,10 @@ def human_go_home(
     :return: the instruction for this driver
     """
     mechatronics = env.mechatronics.get(veh.mechatronics_id)
+    if mechatronics is None:
+        log.error(f"no mechatronics {veh.mechatronics_id} found for vehicle {veh.id}")
+        return None
+
     remaining_range = mechatronics.range_remaining_km(veh) if mechatronics else None
     if not remaining_range:
         return None
@@ -144,7 +158,7 @@ def human_look_for_requests(
     :return:
     """
 
-    def _get_reposition_location() -> Optional[Link]:
+    def _get_reposition_location() -> Optional[EntityPosition]:
         """
         takes the most dense request search hex as a proxy for high demand areas
         :return:
@@ -183,10 +197,16 @@ def idle_if_at_soc_limit(
     :return:
     """
     mechatronics = env.mechatronics.get(veh.mechatronics_id)
+    if mechatronics is None:
+        log.error(f"no mechatronics {veh.mechatronics_id} found for vehicle {veh.id}")
+        return None
+
     battery_soc = mechatronics.fuel_source_soc(veh)
     if battery_soc >= env.config.dispatcher.ideal_fastcharge_soc_limit:
         return IdleInstruction(veh.id)
 
+    return None
+
 
 def av_charge_base_instruction(
     veh: Vehicle,
@@ -201,8 +221,12 @@ def av_charge_base_instruction(
     :param env:
     :return:
     """
+    base_state = veh.vehicle_state
+    if not isinstance(base_state, (ReserveBase, ChargingBase)):
+        # this instruction is only valid for these two states
+        return None
 
-    my_base = sim.bases.get(veh.vehicle_state.base_id)
+    my_base = sim.bases.get(base_state.base_id)
     my_mechatronics = env.mechatronics.get(veh.mechatronics_id)
     if not my_base:
         return None
@@ -215,12 +239,12 @@ def av_charge_base_instruction(
 
     my_station = sim.stations.get(my_base.station_id)
     if not my_station:
-        log.error(f"could not find station {my_base.station_id} for base {my_base.base_id}")
+        log.error(f"could not find station {my_base.station_id} for base {my_base.id}")
         return None
 
-    chargers = tuple(
+    chargers: Tuple[Charger, ...] = tuple(
         filter(
-            lambda c: my_mechatronics.valid_charger(c),
+            my_mechatronics.valid_charger,
             [cs.charger for cs in my_station.state.values()],
         )
     )
@@ -229,7 +253,7 @@ def av_charge_base_instruction(
         return None
 
     # take the lowest power charger
-    charger = sorted(chargers, key=lambda c: c.rate)[0]
+    charger: Charger = sorted(chargers, key=lambda c: c.rate)[0]
     return ChargeBaseInstruction(veh.id, my_base.id, charger.id)
 
 
@@ -247,10 +271,16 @@ def av_dispatch_base_instruction(
     :param env:
     :return:
     """
-    if veh.vehicle_state.idle_duration > env.config.dispatcher.idle_time_out_seconds:
+    idle_state = veh.vehicle_state
+
+    if not isinstance(idle_state, Idle):
+        # this instruction is only valid for idle state
+        return None
+
+    if idle_state.idle_duration > env.config.dispatcher.idle_time_out_seconds:
         # timeout after being idle too long
 
-        def valid_fn(base: Base) -> bool:
+        def valid_fn(base: Entity) -> bool:
             vehicle_has_access = base.membership.grant_access_to_membership(veh.membership)
             return vehicle_has_access
 
diff --git a/nrel/hive/state/driver_state/driver_state.py b/nrel/hive/state/driver_state/driver_state.py
index 410bf631..22641be2 100644
--- a/nrel/hive/state/driver_state/driver_state.py
+++ b/nrel/hive/state/driver_state/driver_state.py
@@ -7,9 +7,7 @@
 from nrel.hive.state.simulation_state import simulation_state_ops
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
     from nrel.hive.util.typealiases import ScheduleId, BaseId, VehicleId
     from nrel.hive.dispatcher.instruction.instruction import Instruction
@@ -44,6 +42,12 @@ def allows_pooling(cls) -> bool:
     def home_base_id(cls) -> Optional[BaseId]:
         pass
 
+    @abstractmethod
+    def update(
+        self, sim: SimulationState, env: Environment
+    ) -> Tuple[Optional[Exception], Optional[SimulationState]]:
+        pass
+
     @abstractmethod
     def generate_instruction(
         self,
@@ -146,12 +150,10 @@ def build(
         )
 
         if not schedule_id:
-            driver_state = AutonomousAvailable(AutonomousDriverAttributes(vehicle_id))
-            return driver_state
+            return AutonomousAvailable(AutonomousDriverAttributes(vehicle_id))
         else:
             if not base_id:
                 raise Exception("cannot build a vehicle with schedule but not a home base id")
-            driver_state = HumanUnavailable(
+            return HumanUnavailable(
                 HumanDriverAttributes(vehicle_id, schedule_id, base_id, allows_pooling)
             )
-            return driver_state
diff --git a/nrel/hive/state/driver_state/human_driver_state/human_driver_state.py b/nrel/hive/state/driver_state/human_driver_state/human_driver_state.py
index 8eebed7f..45421b3d 100644
--- a/nrel/hive/state/driver_state/human_driver_state/human_driver_state.py
+++ b/nrel/hive/state/driver_state/human_driver_state/human_driver_state.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass
 
 import logging
-from typing import NamedTuple, Tuple, Optional, TYPE_CHECKING
+from typing import Tuple, Optional, TYPE_CHECKING
 
 from nrel.hive.dispatcher.instruction.instruction import Instruction
 from nrel.hive.dispatcher.instruction.instructions import (
@@ -35,9 +35,7 @@
 from nrel.hive.util import SimulationStateError, BaseId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
     from nrel.hive.util.typealiases import ScheduleId
 
@@ -82,6 +80,7 @@ def generate_instruction(
         my_vehicle = sim.vehicles.get(self.attributes.vehicle_id)
         if not my_vehicle:
             log.error(f"could not find vehicle {self.attributes.vehicle_id} in simulation")
+            return None
 
         state = my_vehicle.vehicle_state
 
@@ -97,8 +96,8 @@ def generate_instruction(
             # pastures
             if state.idle_duration > env.config.dispatcher.idle_time_out_seconds:
                 return human_look_for_requests(my_vehicle, sim)
-        else:
-            return None
+
+        return None
 
     def update(
         self, sim: SimulationState, env: Environment
@@ -195,6 +194,12 @@ def generate_instruction(
             at_home = my_base.geoid == my_vehicle.geoid
             my_mechatronics = env.mechatronics.get(my_vehicle.mechatronics_id)
 
+            if my_mechatronics is None:
+                log.error(
+                    f"could not find mechatronics instance {my_vehicle.mechatronics_id} for vehicle {my_vehicle.id} in simulation"
+                )
+                return None
+
             if not at_home:
                 if isinstance(my_vehicle.vehicle_state, DispatchBase):
                     # stick with the plan
diff --git a/nrel/hive/state/driver_state/human_driver_state/human_unavailable_charge_parameters.py b/nrel/hive/state/driver_state/human_driver_state/human_unavailable_charge_parameters.py
index fb71b9db..6af5d150 100644
--- a/nrel/hive/state/driver_state/human_driver_state/human_unavailable_charge_parameters.py
+++ b/nrel/hive/state/driver_state/human_driver_state/human_unavailable_charge_parameters.py
@@ -7,9 +7,7 @@
 )
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.model.vehicle.vehicle import Vehicle
     from nrel.hive.runner.environment import Environment
 
diff --git a/nrel/hive/state/entity_state/entity_state.py b/nrel/hive/state/entity_state/entity_state.py
deleted file mode 100644
index d5b310ae..00000000
--- a/nrel/hive/state/entity_state/entity_state.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from __future__ import annotations
-from abc import abstractmethod, ABC
-from typing import Tuple, Optional
-
-
-# if TYPE_CHECKING:
-#     from nrel.hive.state.simulation_state import SimulationState
-#     from nrel.hive.runner.environment import Environment
-
-
-class EntityState(ABC):
-    """
-    a state representation along with methods for state transitions and discrete time step updates
-    """
-
-    @abstractmethod
-    def update(
-        self,
-        sim: "SimulationState",
-        env: "Environment",
-    ) -> Tuple[Optional[Exception], Optional["SimulationState"]]:
-        """
-        apply any effects due to an entity being advanced one discrete time unit in this EntityState
-
-        :param sim: the simulation state
-        :param env: the simulation environment
-        :return: an exception due to failure or an optional updated simulation
-        """
-        pass
-
-    @abstractmethod
-    def enter(
-        self, sim: "SimulationState", env: "Environment"
-    ) -> Tuple[Optional[Exception], Optional["SimulationState"]]:
-        """
-        apply any effects due to an entity transitioning into this state
-
-        :param sim: the simulation state
-        :param env: the simulation environment
-        :return: an exception due to failure or an optional updated simulation, or (None, None) if invalid
-        """
-        pass
-
-    @abstractmethod
-    def exit(
-        self,
-        next_state: EntityState,
-        sim: "SimulationState",
-        env: "Environment",
-    ) -> Tuple[Optional[Exception], Optional["SimulationState"]]:
-        """
-        apply any effects due to an entity transitioning out of this state
-
-        :param next_state the EntityState to transition to
-        :param sim: the simulation state
-        :param env: the simulation environment
-        :return: an exception due to failure or an optional updated simulation, or (None, None) if invalid
-        """
-        pass
diff --git a/nrel/hive/state/entity_state/entity_state_ops.py b/nrel/hive/state/entity_state/entity_state_ops.py
index c9d81584..e1620d16 100644
--- a/nrel/hive/state/entity_state/entity_state_ops.py
+++ b/nrel/hive/state/entity_state/entity_state_ops.py
@@ -1,17 +1,20 @@
 from __future__ import annotations
-from typing import Tuple, Optional
+from typing import Tuple, Optional, TYPE_CHECKING
 
-from nrel.hive.runner.environment import Environment
-from nrel.hive.state.entity_state.entity_state import EntityState
 from nrel.hive.util.exception import StateTransitionError
 
+if TYPE_CHECKING:
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
+    from nrel.hive.state.vehicle_state.vehicle_state import VehicleState
+    from nrel.hive.runner.environment import Environment
+
 
 def transition_previous_to_next(
-    sim: "SimulationState",
+    sim: SimulationState,
     env: Environment,
-    prev_state: EntityState,
-    next_state: EntityState,
-) -> Tuple[Optional[Exception], Optional["SimulationState"]]:
+    prev_state: VehicleState,
+    next_state: VehicleState,
+) -> Tuple[Optional[Exception], Optional[SimulationState]]:
     """
     exits the previous state and enters the next state
 
diff --git a/nrel/hive/state/simulation_state/simulation_state.py b/nrel/hive/state/simulation_state/simulation_state.py
index bed2142a..346760aa 100644
--- a/nrel/hive/state/simulation_state/simulation_state.py
+++ b/nrel/hive/state/simulation_state/simulation_state.py
@@ -12,9 +12,7 @@
 
 import immutables
 
-from nrel.hive.state.simulation_state.at_location_response import (
-    AtLocationResponse,
-)
+from nrel.hive.state.simulation_state.at_location_response import AtLocationResponse
 from nrel.hive.model.membership import PUBLIC_MEMBERSHIP_ID
 from nrel.hive.model.sim_time import SimTime
 from nrel.hive.util import geo
diff --git a/nrel/hive/state/simulation_state/simulation_state_ops.py b/nrel/hive/state/simulation_state/simulation_state_ops.py
index a2d2683f..ab5bfedf 100644
--- a/nrel/hive/state/simulation_state/simulation_state_ops.py
+++ b/nrel/hive/state/simulation_state/simulation_state_ops.py
@@ -1,25 +1,26 @@
 from __future__ import annotations
 
-from typing import Iterable, Optional, TYPE_CHECKING
+from typing import Iterable, Optional, TYPE_CHECKING, Tuple, cast
 
 import h3
+import immutables
 from returns.result import Success, Failure, ResultE
 
 from nrel.hive.model.sim_time import SimTime
-from nrel.hive.util import DictOps
+from nrel.hive.util.dict_ops import DictOps
 from nrel.hive.util.exception import SimulationStateError
 from nrel.hive.util.fp import apply_op_to_accumulator, throw_or_return
-from nrel.hive.util.typealiases import *
+from nrel.hive.util.typealiases import RequestId, StationId, VehicleId, BaseId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
+    from nrel.hive.model.entity import Entity
     from nrel.hive.model.base import Base
     from nrel.hive.model.request import Request
     from nrel.hive.model.station.station import Station
     from nrel.hive.model.vehicle.vehicle import Vehicle
 
+
 """
 a collection of operations to modify the SimulationState which are not
 intended to be exposed to HIVE users
@@ -100,13 +101,13 @@ def add_entity_safe(sim: SimulationState, entity: Entity) -> ResultE[SimulationS
     :return: the updated simulation state or an error
     """
     if entity.__class__.__name__ == "Vehicle":
-        return add_vehicle_safe(sim, entity)
+        return add_vehicle_safe(sim, entity)  # type: ignore
     if entity.__class__.__name__ == "Station":
-        return add_station_safe(sim, entity)
+        return add_station_safe(sim, entity)  # type: ignore
     if entity.__class__.__name__ == "Base":
-        return add_base_safe(sim, entity)
+        return add_base_safe(sim, entity)  # type: ignore
     if entity.__class__.__name__ == "Request":
-        return add_request_safe(sim, entity)
+        return add_request_safe(sim, entity)  # type: ignore
     else:
         err = SimulationStateError(f"cannot add entity {entity} to simulation")
         return Failure(err)
@@ -141,13 +142,13 @@ def modify_entity_safe(sim: SimulationState, entity: Entity) -> ResultE[Simulati
     :return: the updated simulation state or an error
     """
     if entity.__class__.__name__ == "Vehicle":
-        return modify_vehicle_safe(sim, entity)
+        return modify_vehicle_safe(sim, entity)  # type: ignore
     if entity.__class__.__name__ == "Station":
-        return modify_station_safe(sim, entity)
+        return modify_station_safe(sim, entity)  # type: ignore
     if entity.__class__.__name__ == "Base":
-        return modify_base_safe(sim, entity)
+        return modify_base_safe(sim, entity)  # type: ignore
     if entity.__class__.__name__ == "Request":
-        return modify_request_safe(sim, entity)
+        return modify_request_safe(sim, entity)  # type: ignore
     else:
         err = SimulationStateError(f"cannot modify entity {entity} to simulation")
         return Failure(err)
@@ -281,7 +282,7 @@ def modify_request_safe(sim: SimulationState, updated_request: Request) -> Resul
         )
 
         updated_sim = sim._replace(
-            requests=result.entities if result.entities else sim.requests,
+            requests=result.entities if result.entities else sim.requests,  # type: ignore
             r_locations=result.locations if result.locations else sim.r_locations,
             r_search=result.search if result.search else sim.r_search,
         )
@@ -340,9 +341,9 @@ def modify_vehicle_safe(sim: SimulationState, updated_vehicle: Vehicle) -> Resul
     #   traveling between two protruding hexes. I think we can allow this since we guarantee that a request
     #   o-d pair will always be within the geofence.
     vehicle = sim.vehicles.get(updated_vehicle.id)
-    if not vehicle:
+    if vehicle is None:
         error = SimulationStateError(
-            f"cannot update vehicle {vehicle.id}, it was not already in the sim"
+            f"cannot update vehicle {updated_vehicle.id}, it was not already in the sim"
         )
         return Failure(error)
     elif not sim.road_network.geoid_within_geofence(updated_vehicle.geoid):
@@ -360,7 +361,7 @@ def modify_vehicle_safe(sim: SimulationState, updated_vehicle: Vehicle) -> Resul
         )
 
         updated_sim = sim._replace(
-            vehicles=updated_dictionaries.entities
+            vehicles=updated_dictionaries.entities  # type: ignore
             if updated_dictionaries.entities
             else sim.vehicles,
             v_locations=updated_dictionaries.locations
diff --git a/nrel/hive/state/simulation_state/update/cancel_requests.py b/nrel/hive/state/simulation_state/update/cancel_requests.py
index 9d261da1..ea591d94 100644
--- a/nrel/hive/state/simulation_state/update/cancel_requests.py
+++ b/nrel/hive/state/simulation_state/update/cancel_requests.py
@@ -7,9 +7,7 @@
 from nrel.hive.runner.environment import Environment
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.simulation_state.update.simulation_update import (
-    SimulationUpdateFunction,
-)
+from nrel.hive.state.simulation_state.update.simulation_update import SimulationUpdateFunction
 from nrel.hive.util.typealiases import RequestId
 from nrel.hive.reporting.reporter import Report, ReportType
 
@@ -56,6 +54,8 @@ def _remove_from_sim(sim: SimulationState, request_id: RequestId) -> SimulationS
                 if update_error:
                     log.error(update_error)
                     return sim
+                elif updated_sim is None:
+                    return sim
                 else:
                     env.reporter.file_report(_gen_report(request_id, sim))
                     return updated_sim
@@ -83,8 +83,8 @@ def _gen_report(r_id: RequestId, sim: SimulationState) -> Report:
     membership = str(req.membership) if req else ""
     report_data = {
         "request_id": r_id,
-        "departure_time": dep_t,
-        "cancel_time": sim_t,
+        "departure_time": str(dep_t),
+        "cancel_time": str(sim_t),
         "fleet_id": membership,
     }
     return Report(ReportType.CANCEL_REQUEST_EVENT, report_data)
diff --git a/nrel/hive/state/simulation_state/update/charging_price_update.py b/nrel/hive/state/simulation_state/update/charging_price_update.py
index f125170a..4568eb5d 100644
--- a/nrel/hive/state/simulation_state/update/charging_price_update.py
+++ b/nrel/hive/state/simulation_state/update/charging_price_update.py
@@ -5,7 +5,7 @@
 import logging
 from csv import DictReader
 from pathlib import Path
-from typing import Tuple, Optional, Dict
+from typing import Any, Tuple, Optional, Dict
 
 import immutables
 import h3
@@ -15,9 +15,7 @@
 from nrel.hive.runner.environment import Environment
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.simulation_state.update.simulation_update import (
-    SimulationUpdateFunction,
-)
+from nrel.hive.state.simulation_state.update.simulation_update import SimulationUpdateFunction
 from nrel.hive.util.iterators import DictReaderStepper
 from nrel.hive.util import DictOps
 from nrel.hive.util.typealiases import StationId, ChargerId
@@ -70,7 +68,8 @@ def create_default_price(
                 }
                 return acc + (default_price,)
 
-            fallback_values = ft.reduce(create_default_price, table.keys(), ())
+            initial: Tuple[Dict, ...] = ()
+            fallback_values = ft.reduce(create_default_price, table.keys(), initial)
             stepper = DictReaderStepper.from_iterator(
                 iter(fallback_values), "time", parser=SimTime.build
             )
@@ -81,11 +80,15 @@ def create_default_price(
                 raise IOError(f"{charging_price_file} is not a valid path to a request file")
             else:
                 if lazy_file_reading:
-                    error, stepper = DictReaderStepper.build(
+                    error, maybe_stepper = DictReaderStepper.build(
                         charging_path, "time", parser=SimTime.build
                     )
                     if error:
                         raise error
+                    elif maybe_stepper is None:
+                        raise Exception("Was not able to build DictReaderStepper")
+                    else:
+                        stepper = maybe_stepper
                 else:
                     with charging_path.open() as f:
                         reader = iter(tuple(DictReader(f)))
@@ -111,10 +114,11 @@ def stop_condition(value: int) -> bool:
             return value < current_sim_time
 
         # parse the most recently available charger_id price data up to the current sim time
+        initial: immutables.Map[str, immutables.Map[ChargerId, Currency]] = immutables.Map()
         charger_update = ft.reduce(
             _add_row_to_this_update,
             self.reader.read_until_stop_condition(stop_condition),
-            immutables.Map(),
+            initial,
         )
 
         if len(charger_update) == 0:
@@ -204,6 +208,8 @@ def _update_station_prices(
         if error is not None:
             log.error(error)
             return simulation_state
+        elif updated_station is None:
+            return simulation_state
         else:
             error, updated_sim = simulation_state_ops.modify_station(
                 simulation_state, updated_station
@@ -211,6 +217,8 @@ def _update_station_prices(
             if error:
                 log.error(error)
                 return simulation_state
+            elif updated_sim is None:
+                return simulation_state
             else:
                 return updated_sim
 
@@ -238,7 +246,7 @@ def _map_to_station_ids(
 
                 # find the set of all station search geoids corresponding with the
                 # provided station charge price geoid
-                search_geoids = (k,)
+                search_geoids: Tuple[Any, ...] = (k,)
                 if res > sim.sim_h3_search_resolution:
                     search_geoids = (h3.h3_to_parent(k, sim.sim_h3_search_resolution),)
                 elif res < sim.sim_h3_search_resolution:
@@ -248,7 +256,7 @@ def _map_to_station_ids(
                     station_id
                     for search_geoid in search_geoids
                     if sim.s_search.get(search_geoid)
-                    for station_id in sim.s_search.get(search_geoid)
+                    for station_id in sim.s_search[search_geoid]
                 )
 
                 # all of these station ids should get entries managers the provided geoid
diff --git a/nrel/hive/state/simulation_state/update/simulation_update.py b/nrel/hive/state/simulation_state/update/simulation_update.py
index 7c3430e7..5b273b1e 100644
--- a/nrel/hive/state/simulation_state/update/simulation_update.py
+++ b/nrel/hive/state/simulation_state/update/simulation_update.py
@@ -4,9 +4,7 @@
 from typing import Optional, Tuple, TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
 
 
diff --git a/nrel/hive/state/simulation_state/update/step_simulation.py b/nrel/hive/state/simulation_state/update/step_simulation.py
index 5d7d4827..9505d6d7 100644
--- a/nrel/hive/state/simulation_state/update/step_simulation.py
+++ b/nrel/hive/state/simulation_state/update/step_simulation.py
@@ -19,9 +19,7 @@
 )
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.simulation_state.update.simulation_update import (
-    SimulationUpdateFunction,
-)
+from nrel.hive.state.simulation_state.update.simulation_update import SimulationUpdateFunction
 from nrel.hive.state.simulation_state.update.step_simulation_ops import (
     perform_vehicle_state_updates,
     apply_instructions,
@@ -78,7 +76,7 @@ def update(
         self,
         simulation_state: SimulationState,
         env: Environment,
-    ) -> Tuple[SimulationState, Optional[StepSimulation]]:
+    ) -> Tuple[SimulationState, StepSimulation]:
         """
         generates all instructions for this time step and then attempts to apply them to the SimulationState
         upon completion, returns the modified simulation state along with any reports and additionally, returns
diff --git a/nrel/hive/state/simulation_state/update/step_simulation_ops.py b/nrel/hive/state/simulation_state/update/step_simulation_ops.py
index 4e45ac02..8c320846 100644
--- a/nrel/hive/state/simulation_state/update/step_simulation_ops.py
+++ b/nrel/hive/state/simulation_state/update/step_simulation_ops.py
@@ -6,12 +6,8 @@
 from typing import Tuple, Optional, TYPE_CHECKING, Callable, NamedTuple
 
 from nrel.hive.dispatcher.instruction.instruction import Instruction
-from nrel.hive.dispatcher.instruction.instruction_result import (
-    InstructionResult,
-)
-from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
-    InstructionGenerator,
-)
+from nrel.hive.dispatcher.instruction.instruction_result import InstructionResult
+from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
 from nrel.hive.model.vehicle.vehicle import Vehicle
 from nrel.hive.reporting.report_type import ReportType
 from nrel.hive.reporting.reporter import Report
@@ -23,10 +19,9 @@
 
 if TYPE_CHECKING:
     from nrel.hive.runner.environment import Environment
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
-    from nrel.hive.util.typealiases import SimTime, VehicleId
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
+    from nrel.hive.model.sim_time import SimTime
+    from nrel.hive.util.typealiases import VehicleId
 
 log = logging.getLogger(__name__)
 
@@ -39,7 +34,7 @@ def _instruction_to_report(i: Instruction, sim_time: SimTime) -> Report:
     return Report(ReportType.INSTRUCTION, i_dict)
 
 
-def log_instructions(instructions: Tuple[Instruction], env: Environment, sim_time: SimTime):
+def log_instructions(instructions: Tuple[Instruction, ...], env: Environment, sim_time: SimTime):
     for i in instructions:
         env.reporter.file_report(_instruction_to_report(i, sim_time))
 
@@ -68,11 +63,15 @@ def step_vehicle(
 
     if driver_error:
         return driver_error, None
+    if driver_sim is None:
+        return None, None
 
     vehicle_error, vehicle_sim = vehicle.vehicle_state.update(driver_sim, env)
 
     if vehicle_error:
         return vehicle_error, None
+    if vehicle_sim is None:
+        return None, None
 
     next_time_sim = tick(vehicle_sim)
 
@@ -145,14 +144,16 @@ def _sort_by_vehicle_state(vs: Tuple[Vehicle, ...]) -> Tuple[Vehicle, ...]:
         sorted_charge_queueing_vehicles = tuple(
             sorted(
                 charge_queueing_vehicles,
-                key=lambda v: v.vehicle_state.enqueue_time,
+                key=lambda v: v.vehicle_state.enqueue_time
+                if isinstance(v.vehicle_state, ChargeQueueing)
+                else 0,
             )
         )
 
         return other_vehicles + sorted_charge_queueing_vehicles
 
     # why sort here? see _sort_by_vehicle_state for an explanation
-    vehicles = _sort_by_vehicle_state(simulation_state.vehicles.values())
+    vehicles = _sort_by_vehicle_state(tuple(simulation_state.vehicles.values()))
 
     next_state = ft.reduce(_step_vehicle, vehicles, simulation_state)
 
@@ -163,7 +164,7 @@ def _sort_by_vehicle_state(vs: Tuple[Vehicle, ...]) -> Tuple[Vehicle, ...]:
 
 
 def apply_instructions(
-    sim: SimulationState, env: Environment, instructions: Tuple[Instruction]
+    sim: SimulationState, env: Environment, instructions: Tuple[Instruction, ...]
 ) -> SimulationState:
     """
     this helper function takes a map with one instruction per agent at most, and attempts to apply each
@@ -175,64 +176,37 @@ def apply_instructions(
     :return: the simulation state modified by all successful Instructions
     """
     # construct the vehicle state transitions
-    run_in_parallel = False  # env.config.system.local_parallelism > 1
-
-    if run_in_parallel:
-        # run in parallel
-        # todo: inject some means for parallel execution of the apply instruction operation
-        #   requires shared memory access to SimulationState and Environment,
-        #   and a serialization codec to ship Instructions and Instruction.apply_instruction remotely
-        instruction_results_and_errors, updated_sim = ((NotImplementedError, None),), sim
-    else:
-        # run in a synchronous loop
-        def apply_instructions(
-            acc: Tuple[Tuple[InstructionApplicationResult, ...], SimulationState],
-            i: Instruction,
-        ) -> Tuple[Tuple[InstructionApplicationResult, ...], SimulationState]:
-            results, inner_sim = acc
-            err, instruction_result = i.apply_instruction(inner_sim, env)
-            if err is not None:
-                log.error(err)
-                return acc
-            else:
-                updated_instructions = inner_sim.applied_instructions.update({i.vehicle_id: i})
-                updated_sim = inner_sim._replace(applied_instructions=updated_instructions)
-                return results + ((None, instruction_result),), updated_sim
 
-        instruction_results_and_errors, updated_sim = ft.reduce(
-            apply_instructions, instructions, ((), sim)
+    results: list[InstructionResult] = []
+    for instruction in instructions:
+        err, result = instruction.apply_instruction(sim, env)
+        if err is not None:
+            log.error(err)
+            continue
+        if result is None:
+            log.error("this should not be none if error is not none")
+            continue
+
+        updated_instructions = sim.applied_instructions.update(
+            {instruction.vehicle_id: instruction}
         )
+        sim = sim._replace(applied_instructions=updated_instructions)
 
-    has_errors, no_errors = TupleOps.partition(
-        lambda t: t[0] is not None, instruction_results_and_errors
-    )
-    valid_instruction_results = map(lambda t: t[1], no_errors)
-    # report any errors from applying instructions
-    if len(has_errors) > 0:
-        # at least one failed
-        for err, _ in has_errors:
-            log.error(err)
+        results.append(result)
 
-    # update the simulation with each vehicle state transition in sequence
-    def _add_instruction(
-        s: SimulationState,
-        i: InstructionResult,
-    ) -> SimulationState:
-        (
-            update_error,
-            updated_sim,
-        ) = entity_state_ops.transition_previous_to_next(s, env, i.prev_state, i.next_state)
+    for instruction_result in results:
+        (update_error, updated_sim,) = entity_state_ops.transition_previous_to_next(
+            sim, env, instruction_result.prev_state, instruction_result.next_state
+        )
         if update_error:
             log.error(update_error)
-            return s
+            continue
         elif updated_sim is None:
-            return s
+            continue
         else:
-            return updated_sim
-
-    result = ft.reduce(_add_instruction, valid_instruction_results, updated_sim)
+            sim = updated_sim
 
-    return result
+    return sim
 
 
 class UserProvidedUpdateAccumulator(NamedTuple):
diff --git a/nrel/hive/state/simulation_state/update/update.py b/nrel/hive/state/simulation_state/update/update.py
index 443b9381..a83541bd 100644
--- a/nrel/hive/state/simulation_state/update/update.py
+++ b/nrel/hive/state/simulation_state/update/update.py
@@ -6,25 +6,13 @@
 import immutables
 
 from nrel.hive.config import HiveConfig
-from nrel.hive.dispatcher.instruction_generator.instruction_generator import (
-    InstructionGenerator,
-)
+from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.simulation_state.update.cancel_requests import (
-    CancelRequests,
-)
-from nrel.hive.state.simulation_state.update.charging_price_update import (
-    ChargingPriceUpdate,
-)
-from nrel.hive.state.simulation_state.update.simulation_update import (
-    SimulationUpdateFunction,
-)
-from nrel.hive.state.simulation_state.update.step_simulation import (
-    StepSimulation,
-)
-from nrel.hive.state.simulation_state.update.update_requests_from_file import (
-    UpdateRequestsFromFile,
-)
+from nrel.hive.state.simulation_state.update.cancel_requests import CancelRequests
+from nrel.hive.state.simulation_state.update.charging_price_update import ChargingPriceUpdate
+from nrel.hive.state.simulation_state.update.simulation_update import SimulationUpdateFunction
+from nrel.hive.state.simulation_state.update.step_simulation import StepSimulation
+from nrel.hive.state.simulation_state.update.update_requests_from_file import UpdateRequestsFromFile
 
 if TYPE_CHECKING:
     from nrel.hive.runner import RunnerPayload
diff --git a/nrel/hive/state/simulation_state/update/update_requests_from_file.py b/nrel/hive/state/simulation_state/update/update_requests_from_file.py
index aa7c9a37..9efc6fb5 100644
--- a/nrel/hive/state/simulation_state/update/update_requests_from_file.py
+++ b/nrel/hive/state/simulation_state/update/update_requests_from_file.py
@@ -15,9 +15,7 @@
 from nrel.hive.runner.environment import Environment
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.simulation_state.update.simulation_update import (
-    SimulationUpdateFunction,
-)
+from nrel.hive.state.simulation_state.update.simulation_update import SimulationUpdateFunction
 from nrel.hive.util.iterators import DictReaderStepper
 
 log = logging.getLogger(__name__)
@@ -68,13 +66,15 @@ def build(
             )
             if error:
                 raise error
+            if stepper is None:
+                raise ValueError("DictReaderStepper should have returned a non-null value")
         else:
             with req_path.open() as f:
                 # converting to tuple then back to iterator should bring the whole file into memory
-                reader = iter(tuple(DictReader(f)))
+                reader_iter = iter(tuple(DictReader(f)))
 
             stepper = DictReaderStepper.from_iterator(
-                reader, "departure_time", parser=SimTime.build
+                reader_iter, "departure_time", parser=SimTime.build
             )
 
         return UpdateRequestsFromFile(reader=stepper, rate_structure=rate_structure)
@@ -182,10 +182,10 @@ def _update(
                     log.warning(warning)
                     return sim
                 else:
-                    dep_t = sim_updated.requests.get(req.id).departure_time
+                    dep_t = req_in_sim.departure_time
                     report_data = {
                         "request_id": req.id,
-                        "departure_time": dep_t,
+                        "departure_time": str(dep_t),
                         "fleet_id": str(req.membership),
                     }
                     env.reporter.file_report(Report(ReportType.ADD_REQUEST_EVENT, report_data))
diff --git a/nrel/hive/state/simulation_state/update/update_requests_sampling.py b/nrel/hive/state/simulation_state/update/update_requests_sampling.py
index 7a8c7f95..b72e2e41 100644
--- a/nrel/hive/state/simulation_state/update/update_requests_sampling.py
+++ b/nrel/hive/state/simulation_state/update/update_requests_sampling.py
@@ -15,10 +15,8 @@
 from nrel.hive.runner.environment import Environment
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
 from nrel.hive.state.simulation_state import simulation_state_ops
-from nrel.hive.state.simulation_state.update.simulation_update import (
-    SimulationUpdateFunction,
-)
-from nrel.hive.util.iterators import NamedTupleIterator
+from nrel.hive.state.simulation_state.update.simulation_update import SimulationUpdateFunction
+from nrel.hive.util.iterators import ObjectIterator
 
 log = logging.getLogger(__name__)
 
@@ -29,7 +27,7 @@ class UpdateRequestsSampling(SimulationUpdateFunction):
     injects requests into the simulation based on set of pre-sampled requests.
     """
 
-    request_iterator: NamedTupleIterator
+    request_iterator: ObjectIterator
     rate_structure: RequestRateStructure
 
     @classmethod
@@ -58,7 +56,7 @@ def build(
         else:
             rate_structure = RequestRateStructure()
 
-        stepper = NamedTupleIterator(
+        stepper = ObjectIterator(
             items=sampled_requests,
             step_attr_name="departure_time",
             stop_condition=lambda dt: dt < 0,
@@ -103,7 +101,7 @@ def _add_request(sim: SimulationState, request: Request) -> SimulationState:
                 new_sim = new_sim_or_error.unwrap()
                 report_data = {
                     "request_id": request.id,
-                    "departure_time": request.departure_time,
+                    "departure_time": str(request.departure_time),
                     "fleet_id": str(request.membership),
                 }
                 env.reporter.file_report(Report(ReportType.ADD_REQUEST_EVENT, report_data))
diff --git a/nrel/hive/state/vehicle_state/charge_queueing.py b/nrel/hive/state/vehicle_state/charge_queueing.py
index 598ca64b..d56a95b4 100644
--- a/nrel/hive/state/vehicle_state/charge_queueing.py
+++ b/nrel/hive/state/vehicle_state/charge_queueing.py
@@ -18,9 +18,7 @@
 from nrel.hive.util.typealiases import VehicleId, StationId, ChargerId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 log = logging.getLogger(__name__)
 
@@ -99,6 +97,8 @@ def enter(
             err1, updated_station = station.enqueue_for_charger(self.charger_id)
             if err1 is not None:
                 return err1, None
+            elif updated_station is None:
+                return None, None
             else:
                 err2, updated_sim = simulation_state_ops.modify_station(sim, updated_station)
                 if err2 is not None:
@@ -108,6 +108,8 @@ def enter(
                     response.__cause__ = err2
                     return response, None
                 else:
+                    if updated_sim is None:
+                        return Exception("sim was none when error was not none"), None
                     return VehicleState.apply_new_vehicle_state(updated_sim, self.vehicle_id, self)
 
     def update(
@@ -140,6 +142,8 @@ def exit(
             error, updated_station = station.dequeue_for_charger(self.charger_id)
             if error is not None:
                 return error, None
+            elif updated_station is None:
+                return None, None
             else:
                 error, updated_sim = simulation_state_ops.modify_station(sim, updated_station)
                 if error:
diff --git a/nrel/hive/state/vehicle_state/charging_base.py b/nrel/hive/state/vehicle_state/charging_base.py
index c4a00276..0cd171e5 100644
--- a/nrel/hive/state/vehicle_state/charging_base.py
+++ b/nrel/hive/state/vehicle_state/charging_base.py
@@ -17,9 +17,7 @@
 from nrel.hive.util.typealiases import BaseId, VehicleId, ChargerId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
 
 log = logging.getLogger(__name__)
@@ -98,6 +96,8 @@ def enter(
                 charger_err, charger = station.get_charger_instance(self.charger_id)
                 if charger_err is not None:
                     return charger_err, None
+                elif charger is None:
+                    return None, None
                 elif not mechatronics.valid_charger(charger):
                     msg = f"vehicle of type {vehicle.mechatronics_id} can't use charger; context: {context}"
                     return SimulationStateError(msg), None
@@ -120,6 +120,11 @@ def enter(
                             )
                             response.__cause__ = err1
                             return response, None
+                        elif sim2 is None:
+                            return (
+                                Exception("sim state should not be none if there was no error"),
+                                None,
+                            )
                         else:
                             err2, sim3 = simulation_state_ops.modify_station(sim2, updated_station)
                             if err2:
@@ -128,6 +133,11 @@ def enter(
                                 )
                                 response.__cause__ = err2
                                 return response, None
+                            elif sim3 is None:
+                                return (
+                                    Exception("sim state should not be none if there was no error"),
+                                    None,
+                                )
                             else:
                                 return VehicleState.apply_new_vehicle_state(
                                     sim3, self.vehicle_id, self
@@ -148,10 +158,16 @@ def exit(
         :param env: the simulation environment
         :return: an exception due to failure or an optional updated simulation
         """
+        context = f"vehicle {self.vehicle_id} exiting charging base state at base {self.base_id}"
         vehicle = sim.vehicles.get(self.vehicle_id)
         base = sim.bases.get(self.base_id)
+        if base is None:
+            return (
+                SimulationStateError(f"base not found; context: {context}"),
+                None,
+            )
+
         station = sim.stations.get(base.station_id) if base.station_id else None
-        context = f"vehicle {self.vehicle_id} exiting charging base state at base {self.base_id}"
 
         if not vehicle:
             return (
@@ -171,6 +187,8 @@ def exit(
                 )
                 response.__cause__ = err1
                 return response, None
+            elif updated_base is None:
+                return Exception("base should not be none if there was no error"), None
             else:
                 err2, sim2 = simulation_state_ops.modify_base(sim, updated_base)
                 if err2:
@@ -179,6 +197,8 @@ def exit(
                     )
                     response.__cause__ = err2
                     return response, None
+                elif sim2 is None:
+                    return Exception("sim should not be none if there was no error"), None
                 else:
                     err3, updated_station = station.return_charger(self.charger_id)
                     if err3:
@@ -187,6 +207,8 @@ def exit(
                         )
                         response.__cause__ = err3
                         return response, None
+                    elif updated_station is None:
+                        return Exception("station should not be none if there was no error"), None
                     return simulation_state_ops.modify_station(sim2, updated_station)
 
     def _has_reached_terminal_state_condition(self, sim: SimulationState, env: Environment) -> bool:
@@ -198,9 +220,11 @@ def _has_reached_terminal_state_condition(self, sim: SimulationState, env: Envir
         :return: True if the vehicle is fully charged
         """
         vehicle = sim.vehicles.get(self.vehicle_id)
-        mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
         if not vehicle:
             return False
+        mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
+        if mechatronics is None:
+            return False
         else:
             return mechatronics.is_full(vehicle)
 
diff --git a/nrel/hive/state/vehicle_state/charging_station.py b/nrel/hive/state/vehicle_state/charging_station.py
index 199aaba4..1d328c4d 100644
--- a/nrel/hive/state/vehicle_state/charging_station.py
+++ b/nrel/hive/state/vehicle_state/charging_station.py
@@ -18,9 +18,7 @@
 from nrel.hive.util.typealiases import StationId, VehicleId, ChargerId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
 
 log = logging.getLogger(__name__)
@@ -109,6 +107,8 @@ def enter(
             return SimulationStateError(msg), None
         elif charger_err is not None:
             return charger_err, None
+        elif charger is None:
+            return None, None
         elif not mechatronics.valid_charger(charger):
             msg = f"vehicle {vehicle.id} of type {vehicle.mechatronics_id} can't use charger {charger.id}"
             return SimulationStateError(msg), None
@@ -126,6 +126,8 @@ def enter(
                     )
                     response.__cause__ = error
                     return response, None
+                elif updated_sim is None:
+                    return None, None
                 else:
                     return VehicleState.apply_new_vehicle_state(updated_sim, self.vehicle_id, self)
 
@@ -169,6 +171,8 @@ def exit(
                 )
                 response.__cause__ = error
                 return response, None
+            elif updated_station is None:
+                return None, None
             return simulation_state_ops.modify_station(sim, updated_station)
 
     def _has_reached_terminal_state_condition(
@@ -183,9 +187,13 @@ def _has_reached_terminal_state_condition(
         """
         vehicle = sim.vehicles.get(self.vehicle_id)
         if not vehicle:
+            log.error(f"could not find vehicle {self.vehicle_id} in sim state")
             return False
         else:
             mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
+            if mechatronics is None:
+                log.error(f"could not find mechatronics {vehicle.mechatronics_id} in environemnt")
+                return False
             return mechatronics.is_full(vehicle)
 
     def _default_terminal_state(
diff --git a/nrel/hive/state/vehicle_state/dispatch_base.py b/nrel/hive/state/vehicle_state/dispatch_base.py
index 2033530a..4577e30e 100644
--- a/nrel/hive/state/vehicle_state/dispatch_base.py
+++ b/nrel/hive/state/vehicle_state/dispatch_base.py
@@ -21,9 +21,7 @@
 from nrel.hive.util.typealiases import BaseId, VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 log = logging.getLogger(__name__)
 
@@ -113,6 +111,9 @@ def _default_terminal_state(
         context = (
             f"vehicle {self.vehicle_id} entering terminal state for dispatch base at {self.base_id}"
         )
+        if not vehicle:
+            msg = f"vehicle not found; context: {context}"
+            return SimulationStateError(msg), None
         if not base:
             msg = f"base not found; context: {context}"
             return SimulationStateError(msg), None
@@ -122,10 +123,9 @@ def _default_terminal_state(
             return SimulationStateError(message), None
         else:
             if base.available_stalls > 0:
-                next_state = ReserveBase.build(self.vehicle_id, self.base_id)
+                return None, ReserveBase.build(self.vehicle_id, self.base_id)
             else:
-                next_state = Idle.build(self.vehicle_id)
-            return None, next_state
+                return None, Idle.build(self.vehicle_id)
 
     def _perform_update(
         self, sim: SimulationState, env: Environment
diff --git a/nrel/hive/state/vehicle_state/dispatch_ops.py b/nrel/hive/state/vehicle_state/dispatch_ops.py
index 94bc27b3..c3e89741 100644
--- a/nrel/hive/state/vehicle_state/dispatch_ops.py
+++ b/nrel/hive/state/vehicle_state/dispatch_ops.py
@@ -2,15 +2,13 @@
 from typing import Tuple, Optional
 
 from nrel.hive.model.roadnetwork.route import Route
-from nrel.hive.model.roadnetwork.link import Link, EntityPosition
+from nrel.hive.model.entity_position import EntityPosition
 from nrel.hive.model.vehicle.trip_phase import TripPhase
 from nrel.hive.model.vehicle.vehicle import Vehicle
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.vehicle_state.dispatch_pooling_trip import (
-    DispatchPoolingTrip,
-)
-from nrel.hive.state.vehicle_state.vehicle_state_type import VehicleStateType
+from nrel.hive.state.vehicle_state.dispatch_pooling_trip import DispatchPoolingTrip
+from nrel.hive.state.vehicle_state.servicing_pooling_trip import ServicingPoolingTrip
 from nrel.hive.util import (
     VehicleId,
     RequestId,
@@ -130,7 +128,7 @@ def get_position_for_phase(
     if req is None:
         return None
     elif trip_phase == TripPhase.PICKUP:
-        return req.origin_position
+        return req.position
     elif trip_phase == TripPhase.DROPOFF:
         return req.destination_position
     else:
@@ -176,7 +174,7 @@ def begin_or_replan_dispatch_pooling_state(
 
             # todo: this should become a ServicingPoolingTrip instead
             #  - maybe we need a TripPhase.REPLANNING for the leg that is the interruption?
-            if vehicle.vehicle_state.vehicle_state_type == VehicleStateType.SERVICING_POOLING_TRIP:
+            if isinstance(vehicle_state, ServicingPoolingTrip):
                 # already servicing pooling - copy over passenger state
                 next_state = DispatchPoolingTrip.build(
                     vehicle_id,
diff --git a/nrel/hive/state/vehicle_state/dispatch_pooling_trip.py b/nrel/hive/state/vehicle_state/dispatch_pooling_trip.py
index 01d6d75a..6bdd1bf8 100644
--- a/nrel/hive/state/vehicle_state/dispatch_pooling_trip.py
+++ b/nrel/hive/state/vehicle_state/dispatch_pooling_trip.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass, replace
 
 import logging
-from typing import NamedTuple, Tuple, Optional, TYPE_CHECKING
+from typing import Tuple, Optional, TYPE_CHECKING
 from uuid import uuid4
 
 import immutables
@@ -15,15 +15,11 @@
 from nrel.hive.model.sim_time import SimTime
 from nrel.hive.model.vehicle.trip_phase import TripPhase
 from nrel.hive.runner.environment import Environment
-from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.vehicle_state import (
     vehicle_state_ops,
     dispatch_ops,
-    servicing_ops,
-)
-from nrel.hive.state.vehicle_state.servicing_pooling_trip import (
-    ServicingPoolingTrip,
 )
+from nrel.hive.state.vehicle_state.servicing_pooling_trip import ServicingPoolingTrip
 from nrel.hive.state.vehicle_state.vehicle_state import (
     VehicleState,
     VehicleStateInstanceId,
@@ -34,9 +30,7 @@
 from nrel.hive.util.typealiases import RequestId, VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 log = logging.getLogger(__name__)
 
@@ -106,17 +100,20 @@ def enter(
         else:
             req_ids, _ = tuple(zip(*self.trip_plan))
             vehicle = sim.vehicles.get(self.vehicle_id)
+            if vehicle is None:
+                return (
+                    SimulationStateError(f"vehicle {self.vehicle_id} missing from simulation"),
+                    None,
+                )
             reqs_exist_and_match_membership = dispatch_ops.requests_exist_and_match_membership(
                 sim, vehicle, req_ids
             )
             first_req_id, first_phase = first_stop
             first_req = sim.requests.get(first_req_id)
-            is_valid = (
-                route_cooresponds_with_entities(
-                    self.route, vehicle.position, first_req.origin_position
-                )
-                if vehicle and first_req
-                else False
+            if first_req is None:
+                return None, None
+            is_valid = route_cooresponds_with_entities(
+                self.route, vehicle.position, first_req.position
             )
 
             context = f"vehicle {self.vehicle_id} entering dispatch pooling state"
@@ -130,15 +127,17 @@ def enter(
                 log.debug(f"bad route to connect vehicle {vehicle.id} to request {first_req.id}")
                 return None, None
             else:
-                error, updated_sim = dispatch_ops.modify_vehicle_assignment(
+                veh_mod_error, updated_sim = dispatch_ops.modify_vehicle_assignment(
                     sim, self.vehicle_id, req_ids
                 )
-                if error:
+                if veh_mod_error:
                     response = SimulationStateError(
                         f"failure during DispatchPoolingTrip.enter for vehicle {self.vehicle_id}"
                     )
-                    response.__cause__ = error
+                    response.__cause__ = veh_mod_error
                     return response, None
+                elif updated_sim is None:
+                    return Exception("sim should not be none when no error exists"), None
                 else:
                     result = VehicleState.apply_new_vehicle_state(
                         updated_sim, self.vehicle_id, self
diff --git a/nrel/hive/state/vehicle_state/dispatch_station.py b/nrel/hive/state/vehicle_state/dispatch_station.py
index 1bf43534..9e575d6c 100644
--- a/nrel/hive/state/vehicle_state/dispatch_station.py
+++ b/nrel/hive/state/vehicle_state/dispatch_station.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass, replace
 
 import logging
-from typing import NamedTuple, Tuple, Optional, TYPE_CHECKING
+from typing import Tuple, Optional, TYPE_CHECKING
 from uuid import uuid4
 
 from nrel.hive.model.roadnetwork.route import (
@@ -17,7 +17,6 @@
     VehicleState,
     VehicleStateInstanceId,
 )
-from nrel.hive.state.vehicle_state.vehicle_state_ops import move
 from nrel.hive.state.vehicle_state.vehicle_state_type import VehicleStateType
 from nrel.hive.util.exception import SimulationStateError
 from nrel.hive.util.typealiases import StationId, VehicleId, ChargerId
@@ -25,9 +24,7 @@
 log = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 
 @dataclass(frozen=True)
diff --git a/nrel/hive/state/vehicle_state/dispatch_trip.py b/nrel/hive/state/vehicle_state/dispatch_trip.py
index f3e67ad4..e18f683b 100644
--- a/nrel/hive/state/vehicle_state/dispatch_trip.py
+++ b/nrel/hive/state/vehicle_state/dispatch_trip.py
@@ -14,14 +14,10 @@
 from nrel.hive.model.vehicle.trip_phase import TripPhase
 from nrel.hive.runner.environment import Environment
 from nrel.hive.state.simulation_state import simulation_state_ops
-from nrel.hive.state.simulation_state.simulation_state_ops import (
-    modify_request,
-)
+from nrel.hive.state.simulation_state.simulation_state_ops import modify_request
 from nrel.hive.state.vehicle_state import vehicle_state_ops
 from nrel.hive.state.vehicle_state.idle import Idle
-from nrel.hive.state.vehicle_state.servicing_pooling_trip import (
-    ServicingPoolingTrip,
-)
+from nrel.hive.state.vehicle_state.servicing_pooling_trip import ServicingPoolingTrip
 from nrel.hive.state.vehicle_state.servicing_trip import ServicingTrip
 from nrel.hive.state.vehicle_state.vehicle_state import (
     VehicleState,
@@ -32,9 +28,7 @@
 from nrel.hive.util.typealiases import RequestId, VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 log = logging.getLogger(__name__)
 
@@ -82,7 +76,7 @@ def enter(
         vehicle = sim.vehicles.get(self.vehicle_id)
         request = sim.requests.get(self.request_id)
         is_valid = (
-            route_cooresponds_with_entities(self.route, vehicle.position, request.origin_position)
+            route_cooresponds_with_entities(self.route, vehicle.position, request.position)
             if vehicle and request
             else False
         )
@@ -109,6 +103,8 @@ def enter(
                 )
                 response.__cause__ = error
                 return response, None
+            elif updated_sim is None:
+                return Exception("simulation state should note be none if no error"), None
             else:
                 result = VehicleState.apply_new_vehicle_state(updated_sim, self.vehicle_id, self)
                 return result
@@ -154,18 +150,23 @@ def _default_terminal_state(
         :return: an exception due to failure or the next_state after finishing a task
         """
         vehicle = sim.vehicles.get(self.vehicle_id)
+        if vehicle is None:
+            return (
+                SimulationStateError(f"vehicle {self.vehicle_id} does not exist in sim state"),
+                None,
+            )
         request = sim.requests.get(self.request_id)
-        if request and request.geoid != vehicle.geoid:
+        if request is not None and request.geoid != vehicle.geoid:
             locations = f"{request.geoid} != {vehicle.geoid}"
             message = f"vehicle {self.vehicle_id} ended dispatch trip to request {self.request_id} but locations do not match: {locations}. sim_time: {sim.sim_time}"
             return SimulationStateError(message), None
         elif not request:
             # request already got picked up or was cancelled; go an Idle state
-            next_state = Idle.build(self.vehicle_id)
-            return None, next_state
+            idle_next_state = Idle.build(self.vehicle_id)
+            return None, idle_next_state
         else:
             # request exists: pick up the trip and enter a ServicingTrip state
-            route = sim.road_network.route(request.origin_position, request.destination_position)
+            route = sim.road_network.route(request.position, request.destination_position)
             # apply next state
             # generate the data to describe the trip for this request
             # where the pickup phase is currently happening + doesn't need to be added to the trip plan
@@ -174,12 +175,12 @@ def _default_terminal_state(
 
             # create the state (pooling, or, standard servicing trip, depending on the sitch)
             pooling_trip = vehicle.driver_state.allows_pooling and request.allows_pooling
-            next_state = (
+            pooling_next_state = (
                 ServicingPoolingTrip.build(
                     vehicle_id=vehicle.id,
                     trip_plan=trip_plan,
                     boarded_requests=immutables.Map({request.id: request}),
-                    departure_times=immutables.Map({request.id, departure_time}),
+                    departure_times=immutables.Map({request.id: departure_time}),
                     routes=(route,),
                     num_passengers=len(request.passengers),
                 )
@@ -191,7 +192,7 @@ def _default_terminal_state(
                     route=route,
                 )
             )
-            return None, next_state
+            return None, pooling_next_state
 
     def _perform_update(
         self, sim: SimulationState, env: Environment
diff --git a/nrel/hive/state/vehicle_state/idle.py b/nrel/hive/state/vehicle_state/idle.py
index cddef592..283479c4 100644
--- a/nrel/hive/state/vehicle_state/idle.py
+++ b/nrel/hive/state/vehicle_state/idle.py
@@ -1,7 +1,9 @@
 from __future__ import annotations
 from dataclasses import dataclass, replace
 
-from typing import NamedTuple, Tuple, Optional, TYPE_CHECKING
+import logging
+
+from typing import Tuple, Optional, TYPE_CHECKING
 from uuid import uuid4
 
 from nrel.hive.runner.environment import Environment
@@ -17,9 +19,9 @@
 from nrel.hive.util.units import Seconds
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
+
+log = logging.getLogger(__name__)
 
 
 @dataclass(frozen=True)
@@ -63,7 +65,14 @@ def _has_reached_terminal_state_condition(self, sim: SimulationState, env: Envir
         :return: True if we have run out of energy
         """
         vehicle = sim.vehicles.get(self.vehicle_id)
+        if vehicle is None:
+            log.error(f"vehicle {self.vehicle_id} not found in sim")
+            return False
+
         mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
+        if mechatronics is None:
+            log.error(f"mechatronics {vehicle.mechatronics_id} not found in sim")
+            return False
         return not vehicle or mechatronics.is_empty(vehicle)
 
     def _default_terminal_state(
@@ -90,14 +99,15 @@ def _perform_update(
         :return: the sim state with vehicle moved
         """
         vehicle = sim.vehicles.get(self.vehicle_id)
-        mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
         context = f"vehicle {self.vehicle_id} idling"
         if not vehicle:
             return (
                 SimulationStateError(f"vehicle not found; context: {context}"),
                 None,
             )
-        elif not mechatronics:
+        mechatronics = env.mechatronics.get(vehicle.mechatronics_id)
+
+        if not mechatronics:
             return (
                 SimulationStateError(f"cannot find {vehicle.mechatronics_id} in environment"),
                 None,
diff --git a/nrel/hive/state/vehicle_state/out_of_service.py b/nrel/hive/state/vehicle_state/out_of_service.py
index e1bc1171..1c254f30 100644
--- a/nrel/hive/state/vehicle_state/out_of_service.py
+++ b/nrel/hive/state/vehicle_state/out_of_service.py
@@ -14,9 +14,7 @@
 from nrel.hive.util.typealiases import VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 
 @dataclass(frozen=True)
diff --git a/nrel/hive/state/vehicle_state/repositioning.py b/nrel/hive/state/vehicle_state/repositioning.py
index b95680f5..a7226988 100644
--- a/nrel/hive/state/vehicle_state/repositioning.py
+++ b/nrel/hive/state/vehicle_state/repositioning.py
@@ -20,9 +20,7 @@
 from nrel.hive.util.typealiases import VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 
 @dataclass(frozen=True)
diff --git a/nrel/hive/state/vehicle_state/reserve_base.py b/nrel/hive/state/vehicle_state/reserve_base.py
index ef47ae2f..2cff18ad 100644
--- a/nrel/hive/state/vehicle_state/reserve_base.py
+++ b/nrel/hive/state/vehicle_state/reserve_base.py
@@ -16,9 +16,7 @@
 from nrel.hive.util.typealiases import VehicleId, BaseId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 log = logging.getLogger(__name__)
 
@@ -84,6 +82,8 @@ def enter(
                     )
                     response.__cause__ = error
                     return response, None
+                elif updated_sim is None:
+                    return None, None
                 else:
                     return VehicleState.apply_new_vehicle_state(updated_sim, self.vehicle_id, self)
 
@@ -109,6 +109,8 @@ def exit(
                 )
                 response.__cause__ = error
                 return response, None
+            elif updated_base is None:
+                return None, None
             return simulation_state_ops.modify_base(sim, updated_base)
 
     def _has_reached_terminal_state_condition(self, sim: SimulationState, env: Environment) -> bool:
diff --git a/nrel/hive/state/vehicle_state/servicing_ops.py b/nrel/hive/state/vehicle_state/servicing_ops.py
index 2904ae96..01463064 100644
--- a/nrel/hive/state/vehicle_state/servicing_ops.py
+++ b/nrel/hive/state/vehicle_state/servicing_ops.py
@@ -12,18 +12,14 @@
     report_dropoff_request,
 )
 from nrel.hive.runner import Environment
+from nrel.hive.state.vehicle_state.vehicle_state_type import VehicleStateType
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.simulation_state.simulation_state import SimulationState
-from nrel.hive.state.simulation_state.simulation_state_ops import (
-    modify_vehicle,
-)
-from nrel.hive.state.vehicle_state.vehicle_state_type import VehicleStateType
+from nrel.hive.state.simulation_state.simulation_state_ops import modify_vehicle
 from nrel.hive.util import RequestId, TupleOps, SimulationStateError, VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.vehicle_state.servicing_pooling_trip import (
-        ServicingPoolingTrip,
-    )
+    from nrel.hive.state.vehicle_state.servicing_pooling_trip import ServicingPoolingTrip
 
 
 class ActivePoolingTrip(NamedTuple):
@@ -78,7 +74,14 @@ def complete_trip_phase(
     :return: an error, or, the updated simulation state
     """
 
-    vehicle_state = vehicle.vehicle_state
+    if not vehicle.vehicle_state.vehicle_state_type == VehicleStateType.SERVICING_POOLING_TRIP:
+        return (
+            Exception(f"vehicle state is not ServicingPoolingTrip but {type(vehicle_state)}"),
+            None,
+        )
+    else:
+        vehicle_state: ServicingPoolingTrip = vehicle.vehicle_state  # type: ignore
+
     updated_trip_plan = TupleOps.tail(vehicle_state.trip_plan)
     updated_routes = TupleOps.tail(vehicle_state.routes)
     if active_trip.trip_phase == TripPhase.PICKUP:
@@ -97,6 +100,8 @@ def complete_trip_phase(
                 )
                 response.__cause__ = err2
                 return response, None
+            elif sim2 is None:
+                return None, None
             else:
                 # add this request to the boarded vehicles
                 updated_boarded_requests = vehicle_state.boarded_requests.set(request.id, request)
@@ -118,7 +123,7 @@ def complete_trip_phase(
 
     elif active_trip.trip_phase == TripPhase.DROPOFF:
         # perform dropoff operation and update remaining route plan
-        request = vehicle.vehicle_state.boarded_requests.get(active_trip.request_id)
+        request = vehicle_state.boarded_requests.get(active_trip.request_id)
         if request is None:
             error = SimulationStateError(
                 f"request {active_trip.request_id} should have boarded pooling with vehicle {vehicle.id} but not found"
@@ -134,6 +139,8 @@ def complete_trip_phase(
                 )
                 response.__cause__ = err2
                 return response, None
+            elif sim2 is None:
+                return None, None
             else:
                 # remove this request from the boarded vehicles
                 updated_boarded_requests = vehicle_state.boarded_requests.delete(request.id)
@@ -158,7 +165,7 @@ def complete_trip_phase(
 def update_active_pooling_trip(
     sim: SimulationState,
     env: Environment,
-    vehicle_state: ServicingPoolingTrip,
+    vehicle_id: VehicleId,
 ) -> Tuple[Optional[Exception], Optional[SimulationState]]:
     """
     helper to update the route of the leading trip when no route changes are required
@@ -166,27 +173,34 @@ def update_active_pooling_trip(
     :param vehicle_state:
     :return:
     """
-    context = f"updating active pooling trip for vehicle {vehicle_state.vehicle_id}"
-    vehicle = sim.vehicles.get(vehicle_state.vehicle_id)
+    context = f"updating active pooling trip for vehicle {vehicle_id}"
+    vehicle = sim.vehicles.get(vehicle_id)
     if vehicle is None:
         return (
             SimulationStateError(f"vehicle not found; context: {context}"),
             None,
         )
-    current_route = vehicle.vehicle_state.route
+    if not vehicle.vehicle_state.vehicle_state_type == VehicleStateType.SERVICING_POOLING_TRIP:
+        return Exception("can only update vehicles in ServicingPoolingTrip states"), None
+    else:
+        pooling_state: ServicingPoolingTrip = vehicle.vehicle_state  # type: ignore
+
+    current_route = pooling_state.route
     if len(current_route) > 0:
         # vehicle still on current route, noop
         return None, sim
     else:
         # we reached the end of a route, so, we need to perform whatever trip phase
         # action is required and then update the vehicle state accordingly
-        err1, active_trip = get_active_pooling_trip(vehicle_state)
+        err1, active_trip = get_active_pooling_trip(pooling_state)
         if err1 is not None:
             response = SimulationStateError(
                 f"failure during update_active_pooling_trip for vehicle {vehicle.id}"
             )
             response.__cause__ = err1
             return response, None
+        elif active_trip is None:
+            return None, None
         else:
             result = complete_trip_phase(sim, env, vehicle, active_trip)
             return result
@@ -230,6 +244,8 @@ def pick_up_trip(
             response = SimulationStateError(f"failure during pick_up_trip for vehicle {vehicle.id}")
             response.__cause__ = mod_error
             return response, None
+        elif maybe_sim_with_vehicle is None:
+            return None, None
         else:
             try:
                 report = report_pickup_request(updated_vehicle, request, maybe_sim_with_vehicle)
@@ -276,43 +292,3 @@ def drop_off_trip(
         env.reporter.file_report(report)
 
         return None, sim
-
-
-def remove_completed_trip(
-    sim: SimulationState,
-    env: Environment,
-    vehicle_id: VehicleId,
-) -> Tuple[Optional[Exception], Optional[Tuple[SimulationState, int]]]:
-    """
-    removes the first trip from a ServicingPoolingTrip state, because
-    it has reached its destination.
-
-    :param sim: the simulation state
-    :param env: the simulation environment
-    :param vehicle_id: the vehicle to remove it's completed trip
-    :return: the updated simulation state along with the count of remaining trips
-    """
-    vehicle = sim.vehicles.get(vehicle_id)
-    state = vehicle.vehicle_state if vehicle else None
-    context = f"remove completed trip for vehicle {vehicle_id}"
-    if state is None:
-        error = SimulationStateError(f"vehicle not found in simulation state; context: {context}")
-        return error, None
-    elif not state.vehicle_state_type == VehicleStateType.SERVICING_POOLING_TRIP:
-        error = SimulationStateError(
-            f"vehicle {vehicle_id} state not pooling but attempting to remove it's oldest pooling trip"
-        )
-        return error, None
-    elif len(state.trips) == 0:
-        error = SimulationStateError(
-            f"remove first trip called on vehicle with no trips; context: {context}"
-        )
-        return error, None
-    else:
-        vehicle = sim.vehicles.get(state.vehicle_id)
-        removed_trip_request_id, updated_trip_order = TupleOps.head_tail(state.trip_order)
-        updated_trips = state.trips.delete(removed_trip_request_id)
-        updated_state = replace(state, trip_order=updated_trip_order, trips=updated_trips)
-        updated_vehicle = vehicle.modify_vehicle_state(updated_state)
-        error, result = simulation_state_ops.modify_vehicle(sim, updated_vehicle)
-        return error, (result, len(updated_trips))
diff --git a/nrel/hive/state/vehicle_state/servicing_pooling_trip.py b/nrel/hive/state/vehicle_state/servicing_pooling_trip.py
index d55f0d1b..37b8f334 100644
--- a/nrel/hive/state/vehicle_state/servicing_pooling_trip.py
+++ b/nrel/hive/state/vehicle_state/servicing_pooling_trip.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass, replace
 
 import logging
-from typing import NamedTuple, Tuple, TYPE_CHECKING, Optional
+from typing import Tuple, TYPE_CHECKING, Optional
 from uuid import uuid4
 
 import immutables
@@ -16,9 +16,7 @@
 from nrel.hive.state.vehicle_state.idle import Idle
 from nrel.hive.state.vehicle_state.servicing_ops import (
     get_active_pooling_trip,
-    pick_up_trip,
     update_active_pooling_trip,
-    ActivePoolingTrip,
 )
 from nrel.hive.state.vehicle_state.vehicle_state import (
     VehicleState,
@@ -30,9 +28,7 @@
 from nrel.hive.util.typealiases import RequestId, VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 log = logging.getLogger(__name__)
 
@@ -139,11 +135,13 @@ def enter(
                 sim, env, self.vehicle_id, first_req_id
             )
             if pickup_error:
-                result = SimulationStateError(
+                pool_pickup_error = SimulationStateError(
                     f"failed to pick up first trip in ServicingPoolingTrip {self}"
                 )
-                result.__cause__ = pickup_error
-                return result, None
+                pool_pickup_error.__cause__ = pickup_error
+                return pool_pickup_error, None
+            elif pickup_sim is None:
+                return None, None
             else:
                 # enter ServicingPoolingTrip state with first request boarded
                 vehicle_state_with_first_trip = replace(
@@ -236,7 +234,9 @@ def _perform_update(
                 )
             elif moved_vehicle.vehicle_state.vehicle_state_type == VehicleStateType.OUT_OF_SERVICE:
                 return None, move_sim
+            elif not move_sim:
+                return None, None
             else:
                 # update the state of the pooling trip
-                result = update_active_pooling_trip(move_sim, env, self)
+                result = update_active_pooling_trip(move_sim, env, self.vehicle_id)
                 return result
diff --git a/nrel/hive/state/vehicle_state/servicing_trip.py b/nrel/hive/state/vehicle_state/servicing_trip.py
index 9ee9879c..e8d49ff8 100644
--- a/nrel/hive/state/vehicle_state/servicing_trip.py
+++ b/nrel/hive/state/vehicle_state/servicing_trip.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass, replace
 
 import logging
-from typing import NamedTuple, Tuple, Optional, TYPE_CHECKING
+from typing import Tuple, Optional, TYPE_CHECKING
 from uuid import uuid4
 
 from nrel.hive.model.request import Request
@@ -12,7 +12,6 @@
 )
 from nrel.hive.model.sim_time import SimTime
 from nrel.hive.runner.environment import Environment
-from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.vehicle_state.idle import Idle
 from nrel.hive.state.vehicle_state.servicing_ops import (
     drop_off_trip,
@@ -28,9 +27,7 @@
 from nrel.hive.util.typealiases import VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 log = logging.getLogger(__name__)
 
@@ -97,7 +94,7 @@ def enter(
         is_valid = (
             route_cooresponds_with_entities(
                 self.route,
-                request.origin_position,
+                request.position,
                 request.destination_position,
             )
             if vehicle and request
@@ -131,7 +128,7 @@ def enter(
             log.warning(msg)
             return None, None
         elif not route_cooresponds_with_entities(
-            self.route, request.origin_position, request.destination_position
+            self.route, request.position, request.destination_position
         ):
             msg = f"vehicle {vehicle.id} attempting to service request {self.request.id} invalid route (doesn't match o/d of request)"
             log.warning(msg)
@@ -144,6 +141,8 @@ def enter(
                 )
                 response.__cause__ = pickup_error
                 return response, None
+            elif pickup_sim is None:
+                return None, None
             else:
                 enter_result = VehicleState.apply_new_vehicle_state(
                     pickup_sim, self.vehicle_id, self
@@ -208,6 +207,8 @@ def _perform_update(
             )
             response.__cause__ = move_error
             return response, None
+        elif move_sim is None:
+            return None, None
 
         moved_vehicle = move_sim.vehicles.get(self.vehicle_id)
 
@@ -218,10 +219,14 @@ def _perform_update(
             )
         elif moved_vehicle.vehicle_state.vehicle_state_type == VehicleStateType.OUT_OF_SERVICE:
             return None, move_sim
-        elif len(moved_vehicle.vehicle_state.route) == 0:
-            # reached destination.
-            # let's drop the passengers off during this time step
-            result = drop_off_trip(move_sim, env, self.vehicle_id, self.request)
-            return result
+
+        if isinstance(moved_vehicle.vehicle_state, ServicingTrip):
+            if len(moved_vehicle.vehicle_state.route) == 0:
+                # reached destination.
+                # let's drop the passengers off during this time step
+                result = drop_off_trip(move_sim, env, self.vehicle_id, self.request)
+                return result
+            else:
+                return None, move_sim
         else:
             return None, move_sim
diff --git a/nrel/hive/state/vehicle_state/vehicle_state.py b/nrel/hive/state/vehicle_state/vehicle_state.py
index 14877ba0..ce250151 100644
--- a/nrel/hive/state/vehicle_state/vehicle_state.py
+++ b/nrel/hive/state/vehicle_state/vehicle_state.py
@@ -3,11 +3,10 @@
 
 from uuid import UUID
 
-from abc import abstractmethod, abstractproperty
+from abc import abstractmethod, ABC
 from typing import Tuple, Optional, TYPE_CHECKING
 
 from nrel.hive.state.entity_state import entity_state_ops
-from nrel.hive.state.entity_state.entity_state import EntityState
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.vehicle_state.vehicle_state_type import VehicleStateType
 from nrel.hive.util.exception import SimulationStateError, StateTransitionError
@@ -15,9 +14,7 @@
 
 if TYPE_CHECKING:
     from nrel.hive.runner.environment import Environment
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
 
 VehicleStateInstanceId = UUID
 
@@ -28,7 +25,7 @@ class Mixin:
     instance_id: VehicleStateInstanceId
 
 
-class VehicleStateABC(EntityState):
+class VehicleStateABC(ABC):
     """
     a state representation along with methods for state transitions and discrete time step updates
 
@@ -77,6 +74,8 @@ def default_update(
                 )
                 err_res.__cause__ = err1
                 return err_res, None
+            elif next_state is None:
+                return None, None
             else:
                 # perform default state transition
                 (
@@ -170,6 +169,51 @@ def _perform_update(
         """
         pass
 
+    @abstractmethod
+    def update(
+        self,
+        sim: SimulationState,
+        env: Environment,
+    ) -> Tuple[Optional[Exception], Optional[SimulationState]]:
+        """
+        apply any effects due to an entity being advanced one discrete time unit in this EntityState
+
+        :param sim: the simulation state
+        :param env: the simulation environment
+        :return: an exception due to failure or an optional updated simulation
+        """
+        pass
+
+    @abstractmethod
+    def enter(
+        self, sim: SimulationState, env: Environment
+    ) -> Tuple[Optional[Exception], Optional[SimulationState]]:
+        """
+        apply any effects due to an entity transitioning into this state
+
+        :param sim: the simulation state
+        :param env: the simulation environment
+        :return: an exception due to failure or an optional updated simulation, or (None, None) if invalid
+        """
+        pass
+
+    @abstractmethod
+    def exit(
+        self,
+        next_state: VehicleState,
+        sim: SimulationState,
+        env: Environment,
+    ) -> Tuple[Optional[Exception], Optional[SimulationState]]:
+        """
+        apply any effects due to an entity transitioning out of this state
+
+        :param next_state the EntityState to transition to
+        :param sim: the simulation state
+        :param env: the simulation environment
+        :return: an exception due to failure or an optional updated simulation, or (None, None) if invalid
+        """
+        pass
+
 
 class VehicleState(Mixin, VehicleStateABC):
     """ """
diff --git a/nrel/hive/state/vehicle_state/vehicle_state_ops.py b/nrel/hive/state/vehicle_state/vehicle_state_ops.py
index 29b7375c..2bbe98eb 100644
--- a/nrel/hive/state/vehicle_state/vehicle_state_ops.py
+++ b/nrel/hive/state/vehicle_state/vehicle_state_ops.py
@@ -12,15 +12,12 @@
 )
 from nrel.hive.state.simulation_state import simulation_state_ops
 from nrel.hive.state.vehicle_state.out_of_service import OutOfService
-from nrel.hive.state.vehicle_state.vehicle_state_type import VehicleStateType
 from nrel.hive.util.exception import SimulationStateError
 from nrel.hive.util.typealiases import StationId, ChargerId
 from nrel.hive.util.typealiases import VehicleId
 
 if TYPE_CHECKING:
-    from nrel.hive.state.simulation_state.simulation_state import (
-        SimulationState,
-    )
+    from nrel.hive.state.simulation_state.simulation_state import SimulationState
     from nrel.hive.runner.environment import Environment
 
 
@@ -41,13 +38,18 @@ def charge(
     :param charger_id: the charger_id we are using
     :return: an exception due to failure or an optional updated simulation
     """
+    context = f"vehicle {vehicle_id} attempting to charge at station {station_id} with charger {charger_id}"
 
     vehicle = sim.vehicles.get(vehicle_id)
     mechatronics = env.mechatronics.get(vehicle.mechatronics_id) if vehicle else None
     station = sim.stations.get(station_id)
-    charger_err, charger = station.get_charger_instance(charger_id) if station is not None else None
+    if station is None:
+        return (
+            SimulationStateError(f"station not found; context {context}"),
+            None,
+        )
 
-    context = f"vehicle {vehicle_id} attempting to charge at station {station_id} with charger {charger_id}"
+    charger_err, charger = station.get_charger_instance(charger_id)
 
     if not vehicle:
         return (
@@ -101,6 +103,8 @@ def charge(
             response = SimulationStateError(f"failure during charge for vehicle {vehicle.id}")
             response.__cause__ = veh_error
             return response, None
+        elif sim_with_vehicle is None:
+            return None, None
         else:
             report = vehicle_charge_event(
                 vehicle,
@@ -175,6 +179,13 @@ def move(
             SimulationStateError(f"vehicle state does not have route; context {context}"),
             None,
         )
+    elif not hasattr(vehicle.vehicle_state, "update_route"):
+        return (
+            SimulationStateError(
+                f"vehicle state does not have update_route method; context {context}"
+            ),
+            None,
+        )
     else:
         route = vehicle.vehicle_state.route
 
@@ -184,10 +195,13 @@ def move(
     )
     if error:
         return error, None
+    elif traverse_result is None:
+        return None, None
 
     if not traverse_result.experienced_route:
         # vehicle did not traverse so we set an empty route
-        updated_vehicle_state = vehicle.vehicle_state.update_route(route=empty_route())
+        # ignore mypy error since we explicitly check for attribute above
+        updated_vehicle_state = vehicle.vehicle_state.update_route(route=empty_route())  # type: ignore
         updated_vehicle = vehicle.modify_vehicle_state(updated_vehicle_state)
     else:
         experienced_route = traverse_result.experienced_route
@@ -205,7 +219,8 @@ def move(
             position=vehicle_position
         ).tick_distance_traveled_km(step_distance_km)
 
-        new_route_state = new_position_vehicle.vehicle_state.update_route(route=remaining_route)
+        # ignore mypy error since we explicitly check for attribute above
+        new_route_state = new_position_vehicle.vehicle_state.update_route(route=remaining_route)  # type: ignore
         updated_vehicle = new_position_vehicle.modify_vehicle_state(new_route_state)
 
         report = vehicle_move_event(sim, vehicle, updated_vehicle, traverse_result, env)
diff --git a/nrel/hive/util/__init__.py b/nrel/hive/util/__init__.py
index 6804654b..3e6f2994 100644
--- a/nrel/hive/util/__init__.py
+++ b/nrel/hive/util/__init__.py
@@ -9,7 +9,6 @@
     H3Error,
 )
 from nrel.hive.util.h3_ops import H3Ops
-from nrel.hive.util.switch_case import SwitchCase
 from nrel.hive.util.dict_ops import DictOps
 from nrel.hive.util.tuple_ops import TupleOps
 from nrel.hive.util.typealiases import (
diff --git a/nrel/hive/util/dict_ops.py b/nrel/hive/util/dict_ops.py
index 71353f84..314c917e 100644
--- a/nrel/hive/util/dict_ops.py
+++ b/nrel/hive/util/dict_ops.py
@@ -1,24 +1,20 @@
 from __future__ import annotations
 
-from typing import (
-    NamedTuple,
-    Tuple,
-    Optional,
-    TypeVar,
-    Dict,
-    FrozenSet,
-)
+from typing import NamedTuple, Tuple, Optional, TypeVar, FrozenSet, TYPE_CHECKING
 
 import h3
 import immutables
 
-from nrel.hive.util.typealiases import GeoId
+
+if TYPE_CHECKING:
+    from nrel.hive.util.typealiases import EntityId, GeoId
+    from nrel.hive.model.entity import Entity
 
 
 class EntityUpdateResult(NamedTuple):
-    entities: Optional[Dict] = None
-    locations: Optional[Dict] = None
-    search: Optional[Dict] = None
+    entities: Optional[immutables.Map[EntityId, Entity]] = None
+    locations: Optional[immutables.Map[GeoId, FrozenSet[EntityId]]] = None
+    search: Optional[immutables.Map[GeoId, FrozenSet[EntityId]]] = None
 
 
 class DictOps:
@@ -74,7 +70,7 @@ def add_to_collection_dict(
         cls,
         xs: immutables.Map[str, FrozenSet[V]],
         collection_id: str,
-        obj_id: str,
+        obj_id: V,
     ) -> immutables.Map[str, FrozenSet[V]]:
         """
         updates Dicts that track collections of entities
@@ -92,8 +88,8 @@ def add_to_collection_dict(
 
     @classmethod
     def add_to_stack_dict(
-        cls, xs: immutables.Map[str, Tuple[V]], collection_id: str, obj: V
-    ) -> immutables.Map[str, Tuple[V]]:
+        cls, xs: immutables.Map[str, Tuple[V, ...]], collection_id: str, obj: V
+    ) -> immutables.Map[str, Tuple[V, ...]]:
         """
         updates Dicts that hold a stack of entities;
         note that the head of the tuple represents the top of the stack; elements always get inserted into the head;
@@ -105,16 +101,16 @@ def add_to_stack_dict(
         :param obj_id:
         :return:
         """
-        stack = xs.get(collection_id, tuple())
+        stack = xs.get(collection_id, ())
         updated_stack = (obj,) + stack
         return xs.set(collection_id, updated_stack)
 
     @classmethod
     def pop_from_stack_dict(
         cls,
-        xs: immutables.Map[str, Tuple[V]],
+        xs: immutables.Map[str, Tuple[V, ...]],
         collection_id: str,
-    ) -> Tuple[Optional[V], immutables.Map[str, Tuple[V]]]:
+    ) -> Tuple[Optional[V], immutables.Map[str, Tuple[V, ...]]]:
         """
         pops an element from the stack and returns it;
         note that the head of the tuple represents the top of the stack; popped elements come from the tuple head;
@@ -126,7 +122,7 @@ def pop_from_stack_dict(
         :param obj_id:
         :return:
         """
-        stack = xs.get(collection_id, tuple())
+        stack = xs.get(collection_id, ())
         if stack:
             obj, updated_stack = stack[0], stack[1:]
         else:
@@ -162,10 +158,10 @@ def remove_from_collection_dict(
     @classmethod
     def update_entity_dictionaries(
         cls,
-        updated_entity: V,
-        entities: immutables.Map[K, Tuple[V, ...]],
-        locations: immutables.Map[GeoId, Tuple[K, ...]],
-        search: immutables.Map[GeoId, Tuple[K, ...]],
+        updated_entity: Entity,
+        entities: immutables.Map[EntityId, Entity],
+        locations: immutables.Map[GeoId, FrozenSet[EntityId]],
+        search: immutables.Map[GeoId, FrozenSet[EntityId]],
         sim_h3_search_resolution: int,
     ) -> EntityUpdateResult:
         """
diff --git a/nrel/hive/util/error_or_result.py b/nrel/hive/util/error_or_result.py
index 860b63d5..49be2cce 100644
--- a/nrel/hive/util/error_or_result.py
+++ b/nrel/hive/util/error_or_result.py
@@ -2,4 +2,4 @@
 
 T = TypeVar("T")
 
-ErrorOr = Tuple[Optional[Exception], T]
+ErrorOr = Tuple[Optional[Exception], Optional[T]]
diff --git a/nrel/hive/util/fp.py b/nrel/hive/util/fp.py
index d4ce438b..b476d9c4 100644
--- a/nrel/hive/util/fp.py
+++ b/nrel/hive/util/fp.py
@@ -6,7 +6,7 @@
 import functools as ft
 
 
-def throw_on_failure(io_result: IOResult):
+def throw_on_failure(io_result):
     """
     helper that throws an io result if it contains a Failure
 
diff --git a/nrel/hive/util/fs.py b/nrel/hive/util/fs.py
index 94d05e33..2d502934 100644
--- a/nrel/hive/util/fs.py
+++ b/nrel/hive/util/fs.py
@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Optional
+from typing import Optional, Union
 
 import pkg_resources
 import yaml
@@ -14,15 +14,18 @@ def global_hive_config_search() -> GlobalConfig:
     """
     # this searches up the path to the root of the file system
     def _backprop_search(search_path: Path) -> Optional[Path]:
-        search_file = search_path.joinpath(".hive.yaml")
-        if search_file.is_file():
-            return search_file
-        else:
-            updated_search_path = search_path.parent
-            if updated_search_path == search_path:
-                return None
+        try:
+            search_file = search_path.joinpath(".hive.yaml")
+            if search_file.is_file():
+                return search_file
             else:
-                return _backprop_search(updated_search_path)
+                updated_search_path = search_path.parent
+                if updated_search_path == search_path:
+                    return None
+                else:
+                    return _backprop_search(updated_search_path)
+        except FileNotFoundError:
+            return None
 
     # load the default file to be merged with any found files
     default_global_config_file_path = pkg_resources.resource_filename(
@@ -33,11 +36,11 @@ def _backprop_search(search_path: Path) -> Optional[Path]:
 
     # search up the directory tree for a config file
     try:
-        file_found_in_backprop = _backprop_search(Path.cwd())
-    except FileNotFoundError as f:
-        # when running tests on hive, it seems that cwd can be deleted
-        # here we simply drop the cwd backprop search in this case
-        file_found_in_backprop = None
+        cwd = Path.cwd()
+    except FileNotFoundError:
+        cwd = Path("~/")
+
+    file_found_in_backprop = _backprop_search(cwd)
 
     # check user home directory for a config file
     file_at_home_directory = Path.home().joinpath(".hive.yaml")
@@ -61,8 +64,8 @@ def _backprop_search(search_path: Path) -> Optional[Path]:
 
 
 def construct_asset_path(
-    file: str,
-    scenario_directory: str,
+    file: Union[str, Path],
+    scenario_directory: Union[str, Path],
     default_directory_name: str,
     resources_subdirectory: str,
 ) -> str:
@@ -83,13 +86,14 @@ def construct_asset_path(
     :return: the path string if the file exists, otherwise None
     :raises: FileNotFoundError if asset is not found
     """
+    file = Path(file)
     try:
         result = construct_scenario_asset_path(file, scenario_directory, default_directory_name)
         return result
     except FileNotFoundError:
         # try the resources directory fallback
         fallback = pkg_resources.resource_filename(
-            f"nrel.hive.resources.{resources_subdirectory}", file
+            f"nrel.hive.resources.{resources_subdirectory}", str(file)
         )
         if Path(fallback).is_file():
             return fallback
@@ -105,7 +109,7 @@ def construct_asset_path(
 
 
 def construct_scenario_asset_path(
-    file: str, scenario_directory: str, default_directory_name: str
+    file: Union[str, Path], scenario_directory: Union[str, Path], default_directory_name: str
 ) -> str:
     """
     constructs the path to a scenario asset relative to a scenario directory. attempts to load at both
diff --git a/nrel/hive/util/h3_ops.py b/nrel/hive/util/h3_ops.py
index 5dee452f..6b8fd242 100644
--- a/nrel/hive/util/h3_ops.py
+++ b/nrel/hive/util/h3_ops.py
@@ -1,16 +1,17 @@
 from __future__ import annotations
 
 from math import radians, cos, sin, asin, sqrt, ceil
-from typing import Dict, Optional, TYPE_CHECKING
+from typing import Any, Dict, Optional, TYPE_CHECKING, FrozenSet, Iterable, Callable, Tuple
 
 import h3
 import immutables
 
 from nrel.hive.util.exception import H3Error
-from nrel.hive.util.typealiases import *
+from nrel.hive.util.typealiases import EntityId, GeoId
 from nrel.hive.util.units import Kilometers, Seconds, SECONDS_TO_HOURS
 
 if TYPE_CHECKING:
+    from nrel.hive.model.entity import Entity
     from nrel.hive.model.roadnetwork.linktraversal import LinkTraversal
 
 
@@ -19,10 +20,10 @@ class H3Ops:
     def nearest_entity_by_great_circle_distance(
         cls,
         geoid: GeoId,
-        entities: immutables.Map[EntityId, Entity],
-        entity_search: immutables.Map[GeoId, Tuple[EntityId, ...]],
+        entities: Iterable[Entity],
+        entity_search: immutables.Map[GeoId, FrozenSet[EntityId]],
         sim_h3_search_resolution: int,
-        is_valid: Callable[[Entity], bool] = lambda x: True,
+        is_valid: Callable[[Any], bool] = lambda x: True,
         max_search_distance_km: Kilometers = 10,  # kilometers
     ) -> Optional[Entity]:
         """
@@ -53,11 +54,11 @@ def nearest_entity_by_great_circle_distance(
     def nearest_entity(
         cls,
         geoid: GeoId,
-        entities: Tuple[Entity, ...],
-        entity_search: immutables.Map[GeoId, Tuple[EntityId, ...]],
+        entities: Iterable[Entity],
+        entity_search: immutables.Map[GeoId, FrozenSet[EntityId]],
         sim_h3_search_resolution: int,
-        distance_function: Callable[[Entity], float],
-        is_valid: Callable[[Entity], bool] = lambda x: True,
+        distance_function: Callable[[Any], float],
+        is_valid: Callable[[Any], bool] = lambda x: True,
         max_search_distance_km: Kilometers = 10,  # kilometers
     ) -> Optional[Entity]:
         """
@@ -100,7 +101,7 @@ def _search(current_k: int = 0) -> Optional[Entity]:
                     for entity in cls.get_entities_at_cell(cell, entity_search, entities)
                 )
 
-                best_dist_km = 1000000
+                best_dist_km = 1000000.0
                 best_entity = None
 
                 count = 0
@@ -123,8 +124,8 @@ def _search(current_k: int = 0) -> Optional[Entity]:
     def get_entities_at_cell(
         cls,
         search_cell: GeoId,
-        entity_search: immutables.Map[GeoId, Tuple[EntityId, ...]],
-        entities: Tuple[Entity, ...],
+        entity_search: immutables.Map[GeoId, FrozenSet[EntityId]],
+        entities: Iterable[Entity],
     ) -> Tuple[Entity, ...]:
         """
         gives us entities within a high-level search cell
@@ -161,7 +162,7 @@ def nearest_entity_point_to_point(
         :return: an optional entity if found
         """
 
-        best_dist_km = 1000000
+        best_dist_km = 1000000.0
         best_e = None
         for e_geoid, e_ids in entity_locations.items():
             dist_km = cls.great_circle_distance(geoid, e_geoid)
diff --git a/nrel/hive/util/iterators.py b/nrel/hive/util/iterators.py
index c3f8fc0d..5d4a7d2c 100644
--- a/nrel/hive/util/iterators.py
+++ b/nrel/hive/util/iterators.py
@@ -2,6 +2,7 @@
 
 import csv
 import logging
+from pathlib import Path
 from typing import (
     Iterator,
     Dict,
@@ -9,9 +10,11 @@
     Optional,
     Callable,
     Tuple,
+    Any,
     NamedTuple,
     Iterable,
     Generator,
+    Union,
 )
 
 from itertools import islice, tee
@@ -19,14 +22,14 @@
 log = logging.getLogger(__name__)
 
 
-class NamedTupleIterator:
+class ObjectIterator:
     """
     iterator that deals with a set of named tuples
     """
 
     def __init__(
         self,
-        items: Tuple[NamedTuple, ...],
+        items: Tuple[Any, ...],
         step_attr_name: str,
         stop_condition: Callable,
     ):
@@ -153,7 +156,7 @@ def __init__(
     @classmethod
     def build(
         cls,
-        file: str,
+        file: Union[str, Path],
         step_column_name: str,
         initial_stop_condition: Callable = lambda x: x < 0,
         parser: Callable = lambda x: x,
@@ -168,14 +171,18 @@ def build(
         :param parser: an optional parameter for parsing the input_config value
         :return: a new reader or an exception
         """
+        file = Path(file)
         try:
-            f = open(file, "r")
-            return None, cls(
-                csv.DictReader(f),
-                f,
-                step_column_name,
-                initial_stop_condition,
-                parser,
+            f = file.open("r")
+            return (
+                None,
+                cls(
+                    csv.DictReader(f),
+                    f,
+                    step_column_name,
+                    initial_stop_condition,
+                    parser,
+                ),
             )
         except Exception as e:
             return e, None
diff --git a/nrel/hive/util/switch_case.py b/nrel/hive/util/switch_case.py
deleted file mode 100644
index 5081ceeb..00000000
--- a/nrel/hive/util/switch_case.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from __future__ import annotations
-
-from abc import abstractmethod, ABC
-from typing import Dict
-
-from nrel.hive.util.typealiases import *
-
-
-Key = TypeVar("Key")
-"""
-the type used to switch off of
-"""
-
-Arguments = TypeVar("Arguments")
-"""
-the type of the arguments fed to the inner switch clause
-"""
-
-Result = TypeVar("Result")
-"""
-the type returned from the SwitchCase (can be "Any")
-"""
-
-
-class SwitchCase(ABC):
-    @abstractmethod
-    def _default(self, arguments: Arguments) -> Result:
-        """
-        called when "key" does not exist in the SwitchCase
-
-
-        :param arguments: the arguments to pass in the default case
-        :return:
-        """
-
-    case_statement: Dict[Key, Callable[[Arguments], Result]] = {}
-
-    @classmethod
-    def switch(cls, case, payload: Arguments) -> Result:
-        return cls.case_statement.get(case, cls._default)(cls, payload)
diff --git a/nrel/hive/util/time_helpers.py b/nrel/hive/util/time_helpers.py
index 6f756e61..8c2b4276 100644
--- a/nrel/hive/util/time_helpers.py
+++ b/nrel/hive/util/time_helpers.py
@@ -1,5 +1,4 @@
-from datetime import time, datetime, date, timedelta
-import re
+from datetime import datetime, timedelta, date, time
 
 
 def time_in_range(start, end, x):
@@ -13,24 +12,14 @@ def time_in_range(start, end, x):
         return start <= x or x < end
 
 
-def read_time_string(s: str) -> time:
+def read_time_string(input_string: str) -> time:
     """
     parses a string in HH:MM:SS format into a datetime.time object
 
-    :param s: the input string
+    :param input_string: the input string
     :return: the time parsed, or, an error
     """
-    time_regex = r"(\d|0\d|1\d|2[0123]):([012345]\d):([012345]\d)"
-    matches = re.match(time_regex, s.strip())
-    if not matches:
-        raise ValueError(f"could not parse HH:MM:SS from {s}")
-    else:
-        try:
-            h, m, s = tuple(map(lambda x: int(x), matches.groups()))
-            time_value = time(hour=h, minute=m, second=s)
-            return time_value
-        except Exception as e:
-            raise ValueError("unexpected error condition while parsing time from string") from e
+    return datetime.strptime(input_string, "%H:%M:%S").time()
 
 
 def time_diff(start: time, end: time) -> timedelta:
diff --git a/nrel/hive/util/typealiases.py b/nrel/hive/util/typealiases.py
index b7ef9c23..255fe59b 100644
--- a/nrel/hive/util/typealiases.py
+++ b/nrel/hive/util/typealiases.py
@@ -1,7 +1,8 @@
-from typing import Tuple, FrozenSet, Callable, TypeVar
+from typing import Tuple, FrozenSet
 
 from immutables import Map
 
+
 # MODEL ID TYPES
 RequestId = str
 VehicleId = str
@@ -13,11 +14,10 @@
 VehicleTypeId = str
 MechatronicsId = str
 ChargerId = str
-ScheduleId = str
 MembershipId = str
+ScheduleId = str
 
-Entity = TypeVar("Entity")
-EntityId = TypeVar("EntityId")
+EntityId = str
 
 # Collections
 MembershipMap = Map[EntityId, Tuple[MembershipId, ...]]
@@ -36,4 +36,4 @@
 
 # if we create a DriverId type, and it has a lookup table on SimulationState, we may
 # want to change this from VehicleId to DriverId
-ScheduleFunction = Callable[["SimulationState", VehicleId], bool]
+# ScheduleFunction = Callable[[SimulationState, VehicleId], bool]
diff --git a/nrel/hive/util/units.py b/nrel/hive/util/units.py
index bf450eb8..6a4687bf 100644
--- a/nrel/hive/util/units.py
+++ b/nrel/hive/util/units.py
@@ -1,4 +1,7 @@
 # Energy
+from typing import Dict
+
+
 KwH = float  # kilowatt-hours
 J = float  # joules
 KwH_per_H = float
@@ -59,7 +62,7 @@ def hours_to_seconds(hours: Hours) -> Seconds:
 KWH_TO_WH = 1 / WH_TO_KWH
 MilesPerGallon = float
 
-_unit_conversions = {
+UNIT_CONVERSIONS: Dict[str, Dict[str, float]] = {
     "mph": {
         "kmph": MPH_TO_KMPH,
     },
@@ -83,7 +86,7 @@ def hours_to_seconds(hours: Hours) -> Seconds:
 
 
 def valid_unit(unit: str) -> bool:
-    return unit in _unit_conversions.keys()
+    return unit in UNIT_CONVERSIONS.keys()
 
 
 def get_unit_conversion(from_unit: str, to_unit: str) -> float:
@@ -94,9 +97,12 @@ def get_unit_conversion(from_unit: str, to_unit: str) -> float:
     elif from_unit == to_unit:
         return 1
 
-    try:
-        conversion = _unit_conversions[from_unit][to_unit]
-    except KeyError:
-        raise NotImplemented(f"no conversion exists for {from_unit} to {to_unit}")
+    from_conversion = UNIT_CONVERSIONS.get(from_unit)
+    if from_conversion is None:
+        raise ValueError(f"no unit conversion for from_unit: {from_unit}")
+
+    to_conversion = from_conversion.get(to_unit)
+    if to_conversion is None:
+        raise ValueError(f"no unit conversion for to_unit: {to_unit}")
 
-    return conversion
+    return to_conversion
diff --git a/nrel/hive/util/wkt.py b/nrel/hive/util/wkt.py
index 7e96207b..b6db6313 100644
--- a/nrel/hive/util/wkt.py
+++ b/nrel/hive/util/wkt.py
@@ -45,15 +45,16 @@ def linestring_2d(points: Tuple[Tuple[float, float], ...], x_y_ordering: bool) -
     :param x_y_ordering: if the output should be reversed into (x, y) ordering
     :return: a wkt representation
     """
-    if not isinstance(points, Tuple) or len(points) == 0:
+    if not isinstance(points, tuple) or len(points) == 0:
         return polygon_empty()
     elif len(points) == 1:
         return point_2d(points[0], x_y_ordering)
     else:
+        initial: Tuple[str, ...] = tuple()
         pts_strings = ft.reduce(
             lambda acc, pair: acc + (f"{_point_to_string(pair, x_y_ordering)}",),
             points,
-            (),
+            initial,
         )
         inner_content = ", ".join(pts_strings)
         linestring = f"LINESTRING ({inner_content})"
diff --git a/pyproject.toml b/pyproject.toml
index 10be4121..21a221d5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,7 +42,15 @@ docs = [
     "myst-parser",
     "sphinx-autodoc-typehints",
 ]
-dev = ["nrel.hive[docs]", "pytest", "black", "mypy", "twine"]
+dev = [
+    "nrel.hive[docs]",
+    "pytest",
+    "black",
+    "mypy",
+    "twine",
+    "types-PyYAML",
+    "types-setuptools",
+]
 
 [project.urls]
 Homepage = "https://github.com/NREL/hive"
diff --git a/setup.py b/setup.py
index fc1f76c8..60684932 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,3 @@
 from setuptools import setup
 
-setup()
\ No newline at end of file
+setup()


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 87c52656..4c4cbe8e 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -18,10 +18,10 @@ jobs:
         PYTHON: ${{ matrix.python-version }}
 
       steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: set up python ${{ matrix.python-version }}
-        uses: actions/setup-python@v1
+        uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python-version }}
 
@@ -29,6 +29,9 @@ jobs:
         run: |
           pip install ".[dev]" 
 
+      - name: Run mypy
+        run: mypy . --ignore-missing-imports 
+
       - name: Run black
         run: |
           black nrel tests --check
diff --git a/tests/test_build_mechatronics_table.py b/tests/test_build_mechatronics_table.py
index d27fe5f8..ab6856e0 100644
--- a/tests/test_build_mechatronics_table.py
+++ b/tests/test_build_mechatronics_table.py
@@ -1,9 +1,7 @@
 from unittest import TestCase
 from pathlib import Path
 
-from nrel.hive.model.vehicle.mechatronics.__init__ import (
-    build_mechatronics_table,
-)
+from nrel.hive.model.vehicle.mechatronics import build_mechatronics_table
 
 test_dir = Path(__file__).parent
 
@@ -28,7 +26,7 @@ def test_missing_type_field(self):
 
     def test_bad_type(self):
         self.assertRaises(
-            IOError,
+            ValueError,
             build_mechatronics_table,
             test_dir / "test_assets/mechatronics_bad_type.yaml",
             "hive.resources.scenarios.denver_downtown",
diff --git a/tests/test_cancel_requests.py b/tests/test_cancel_requests.py
index 07188fe0..5a529df6 100644
--- a/tests/test_cancel_requests.py
+++ b/tests/test_cancel_requests.py
@@ -2,9 +2,7 @@
 
 from returns.result import Success
 
-from nrel.hive.state.simulation_state.update.cancel_requests import (
-    CancelRequests,
-)
+from nrel.hive.state.simulation_state.update.cancel_requests import CancelRequests
 from nrel.hive.resources.mock_lobster import *
 
 
diff --git a/tests/test_charging_price_update.py b/tests/test_charging_price_update.py
index 9aef847d..9dcf26d3 100644
--- a/tests/test_charging_price_update.py
+++ b/tests/test_charging_price_update.py
@@ -1,8 +1,6 @@
 from unittest import TestCase
 
-from nrel.hive.state.simulation_state.update.charging_price_update import (
-    ChargingPriceUpdate,
-)
+from nrel.hive.state.simulation_state.update.charging_price_update import ChargingPriceUpdate
 from nrel.hive.resources.mock_lobster import *
 
 
diff --git a/tests/test_h3_ops.py b/tests/test_h3_ops.py
index c3418653..940a2009 100644
--- a/tests/test_h3_ops.py
+++ b/tests/test_h3_ops.py
@@ -1,6 +1,8 @@
 from unittest import TestCase
 
 from nrel.hive.resources.mock_lobster import *
+from nrel.hive.util.h3_ops import H3Ops
+from nrel.hive.util.fp import throw_or_return
 
 
 class TestH3Ops(TestCase):
diff --git a/tests/test_haversine_roadnetwork.py b/tests/test_haversine_roadnetwork.py
index c2f34a47..b83c014c 100644
--- a/tests/test_haversine_roadnetwork.py
+++ b/tests/test_haversine_roadnetwork.py
@@ -4,7 +4,7 @@
 
 
 class TestHaversineRoadNetwork(TestCase):
-    @skip
+    @skip("")
     def test_geoid_within_geofence(self):
         somewhere_out_of_geofence = h3.geo_to_h3(0, 0, 15)
         somewhere_within_geofence = h3.geo_to_h3(39.76138151, -104.982001, 15)
diff --git a/tests/test_human_driver_state.py b/tests/test_human_driver_state.py
index 48b03c5f..81e5aaf5 100644
--- a/tests/test_human_driver_state.py
+++ b/tests/test_human_driver_state.py
@@ -1,6 +1,7 @@
 from unittest import TestCase
 
 from nrel.hive.resources.mock_lobster import *
+from nrel.hive.util.fp import throw_or_return
 
 
 def on_schedule(a, b):
diff --git a/tests/test_initialize_simulation.py b/tests/test_initialize_simulation.py
index d5f6765a..f537c52b 100644
--- a/tests/test_initialize_simulation.py
+++ b/tests/test_initialize_simulation.py
@@ -1,9 +1,7 @@
 from unittest import TestCase
 
 from nrel.hive.config.network import Network
-from nrel.hive.initialization.initialize_simulation import (
-    initialize_simulation,
-)
+from nrel.hive.initialization.initialize_simulation import initialize_simulation
 from nrel.hive.initialization.initialize_simulation_with_sampling import (
     initialize_simulation_with_sampling,
 )
@@ -25,14 +23,17 @@ def test_initialize_simulation_with_filtering(self):
         def filter_veh(v: Vehicle) -> bool:
             if v.id == "v1":
                 return False
+            return True
 
         def filter_base(b: Base) -> bool:
             if b.id == "b1":
                 return False
+            return True
 
         def filter_station(s: Station) -> bool:
             if s.id == "s1":
                 return False
+            return True
 
         sim, env = initialize_simulation(
             conf,
diff --git a/tests/test_instruction_generator_ops.py b/tests/test_instruction_generator_ops.py
index 61dba0b9..13dcc1bd 100644
--- a/tests/test_instruction_generator_ops.py
+++ b/tests/test_instruction_generator_ops.py
@@ -3,9 +3,7 @@
 from nrel.hive.dispatcher.instruction_generator.instruction_generator_ops import (
     instruct_vehicles_to_dispatch_to_station,
 )
-from nrel.hive.dispatcher.instruction_generator.charging_search_type import (
-    ChargingSearchType,
-)
+from nrel.hive.dispatcher.instruction_generator.charging_search_type import ChargingSearchType
 from nrel.hive.resources.mock_lobster import *
 
 
diff --git a/tests/test_local_simulation_runner.py b/tests/test_local_simulation_runner.py
index cd5d1df9..2c8101dc 100644
--- a/tests/test_local_simulation_runner.py
+++ b/tests/test_local_simulation_runner.py
@@ -2,9 +2,7 @@
 
 from nrel.hive.runner import LocalSimulationRunner
 from nrel.hive.runner import RunnerPayload
-from nrel.hive.state.simulation_state.update.cancel_requests import (
-    CancelRequests,
-)
+from nrel.hive.state.simulation_state.update.cancel_requests import CancelRequests
 from nrel.hive.resources.mock_lobster import *
 
 
diff --git a/tests/test_osm_roadnetwork.py b/tests/test_osm_roadnetwork.py
index ba7ab04f..a4aaa45f 100644
--- a/tests/test_osm_roadnetwork.py
+++ b/tests/test_osm_roadnetwork.py
@@ -4,7 +4,7 @@
 
 
 class TestOSMRoadNetwork(TestCase):
-    @skip
+    @skip("")
     def test_geoid_within_geofence(self):
         somewhere_out_of_geofence = h3.geo_to_h3(0, 0, 15)
         somewhere_within_geofence = h3.geo_to_h3(39.76138151, -104.982001, 15)
diff --git a/tests/test_request.py b/tests/test_request.py
index 0986cf5c..39d672ac 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -20,7 +20,7 @@ class TestRequest(TestCase):
         d_lat=3,
         d_lon=4,
         h3_res=11,
-        departure_time=28800,
+        departure_time=SimTime(28800),
         passengers=2,
     )
 
diff --git a/tests/test_run_cosim.py b/tests/test_run_cosim.py
index 7ab2e278..f8be97ad 100644
--- a/tests/test_run_cosim.py
+++ b/tests/test_run_cosim.py
@@ -1,9 +1,7 @@
 from unittest import TestCase
 
 from nrel.hive.app import hive_cosim
-from nrel.hive.reporting.handler.vehicle_charge_events_handler import (
-    VehicleChargeEventsHandler,
-)
+from nrel.hive.reporting.handler.vehicle_charge_events_handler import VehicleChargeEventsHandler
 from nrel.hive.resources.mock_lobster import mock_env, mock_sim, mock_update
 from nrel.hive.runner.runner_payload import RunnerPayload
 
diff --git a/tests/test_sample_functions.py b/tests/test_sample_functions.py
index 60c09ab0..855a932a 100644
--- a/tests/test_sample_functions.py
+++ b/tests/test_sample_functions.py
@@ -42,7 +42,7 @@ def test_sample_n_vehicles_default(self):
         loc_fn = build_default_location_sampling_fn()
         soc_fn = build_default_soc_sampling_fn()
 
-        result: Result = sample_vehicles(
+        result = sample_vehicles(
             count=n,
             sim=sim,
             env=env,
@@ -87,7 +87,7 @@ def wonky_loc_fn(s) -> Link:
 
         soc_fn = build_default_soc_sampling_fn()
 
-        result: Result = sample_vehicles(
+        result = sample_vehicles(
             count=n,
             sim=sim,
             env=env,
diff --git a/tests/test_schedules.py b/tests/test_schedules.py
index 0be797a3..f4cbae27 100644
--- a/tests/test_schedules.py
+++ b/tests/test_schedules.py
@@ -1,8 +1,6 @@
 from unittest import TestCase
 
-from nrel.hive.model.vehicle.schedules.time_range_schedule import (
-    time_range_schedules_from_string,
-)
+from nrel.hive.model.vehicle.schedules.time_range_schedule import time_range_schedules_from_string
 from nrel.hive.resources.mock_lobster import *
 
 
@@ -21,9 +19,9 @@ def test_time_range_schedule(self):
         eleven_fifty_nine_fifty_nine = 86399
         # not_a_time = 87000  # todo: maybe worth adding a test for times outside of the day range?
 
-        first_shift_schedule_fn: ScheduleFunction = time_range_schedules.get("first")
-        second_shift_schedule_fn: ScheduleFunction = time_range_schedules.get("second")
-        third_shift_schedule_fn: ScheduleFunction = time_range_schedules.get("third")
+        first_shift_schedule_fn = time_range_schedules["first"]
+        second_shift_schedule_fn = time_range_schedules["second"]
+        third_shift_schedule_fn = time_range_schedules["third"]
         unused = DefaultIds.mock_vehicle_id()
 
         self.assertFalse(first_shift_schedule_fn(mock_sim(sim_time=eight_am), unused))
diff --git a/tests/test_simulation_state_ops.py b/tests/test_simulation_state_ops.py
index cae58068..7a4562a6 100644
--- a/tests/test_simulation_state_ops.py
+++ b/tests/test_simulation_state_ops.py
@@ -1,3 +1,4 @@
+from dataclasses import replace
 from unittest import TestCase
 
 from returns.result import Success
@@ -202,7 +203,7 @@ def test_update_vehicle(self):
         sim_with_veh = sim_or_error.unwrap()
 
         # modify some value on the vehicle
-        updated_vehicle = veh._replace(mechatronics_id="test_update")
+        updated_vehicle = replace(veh, mechatronics_id="test_update")
 
         e2, sim_after_update = simulation_state_ops.modify_vehicle(sim_with_veh, updated_vehicle)
 
@@ -306,7 +307,7 @@ def test_update_station(self):
         station = mock_station()
         sim = mock_sim(stations=(station,))
         new_position = EntityPosition("test", station.geoid)
-        updated_station = station._replace(position=new_position)
+        updated_station = replace(station, position=new_position)
         error, sim_after_station = simulation_state_ops.modify_station(sim, updated_station)
         self.assertIsNone(error, "should have no error")
 
diff --git a/tests/test_simulationstate.py b/tests/test_simulationstate.py
index 3bfbea21..e066b0a1 100644
--- a/tests/test_simulationstate.py
+++ b/tests/test_simulationstate.py
@@ -1,9 +1,7 @@
 from unittest import TestCase
 
 from nrel.hive.state.entity_state import entity_state_ops
-from nrel.hive.state.simulation_state.update.step_simulation import (
-    perform_vehicle_state_updates,
-)
+from nrel.hive.state.simulation_state.update.step_simulation import perform_vehicle_state_updates
 from nrel.hive.state.vehicle_state.out_of_service import OutOfService
 from nrel.hive.state.vehicle_state.servicing_trip import ServicingTrip
 from nrel.hive.resources.mock_lobster import *
diff --git a/tests/test_station.py b/tests/test_station.py
index b84f4c03..266a928f 100644
--- a/tests/test_station.py
+++ b/tests/test_station.py
@@ -3,6 +3,8 @@
 
 from nrel.hive.resources.mock_lobster import *
 
+from returns.result import Failure
+
 
 class TestStation(TestCase):
     def test_from_row(self):
diff --git a/tests/test_station_load_handler.py b/tests/test_station_load_handler.py
index 2856b87f..a09525c1 100644
--- a/tests/test_station_load_handler.py
+++ b/tests/test_station_load_handler.py
@@ -53,7 +53,7 @@ def test_correctly_reads_one_vehicle_charge_event(self):
             "should have captured which station this happened at",
         )
         self.assertGreater(
-            r.report["energy"],
+            float(r.report["energy"]),
             0.0,
             "we should have captured the effect of 60 seconds of charge time",
         )
diff --git a/tests/test_step_simulation_ops.py b/tests/test_step_simulation_ops.py
index a47dfc83..f0405fd0 100644
--- a/tests/test_step_simulation_ops.py
+++ b/tests/test_step_simulation_ops.py
@@ -1,7 +1,5 @@
 from unittest import TestCase
-from nrel.hive.state.simulation_state.update.step_simulation_ops import (
-    step_vehicle,
-)
+from nrel.hive.state.simulation_state.update.step_simulation_ops import step_vehicle
 from nrel.hive.resources.mock_lobster import *
 
 
@@ -22,8 +20,8 @@ def test_step_vehicle(self):
             if err is not None:
                 self.fail(err)
 
-        vehicle1: Vehicle = sim.vehicles.get("1")
-        vehicle2: Vehicle = sim.vehicles.get("2")
+        vehicle1 = sim.vehicles["1"]
+        vehicle2 = sim.vehicles["2"]
 
         veh1_idle_time = vehicle1.vehicle_state.idle_duration
         veh2_idle_time = vehicle2.vehicle_state.idle_duration
@@ -65,8 +63,8 @@ def test_step_vehicle_state_change(self):
             if err is not None:
                 self.fail(err)
 
-        vehicle1: Vehicle = sim.vehicles.get("1")
-        vehicle2: Vehicle = sim.vehicles.get("2")
+        vehicle1 = sim.vehicles["1"]
+        vehicle2 = sim.vehicles["2"]
 
         veh1_state = vehicle1.vehicle_state
         veh2_state = vehicle2.vehicle_state
diff --git a/tests/test_update.py b/tests/test_update.py
deleted file mode 100644
index 5839f637..00000000
--- a/tests/test_update.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from unittest import TestCase, skip
-
-from nrel.hive.runner import RunnerPayload
-from nrel.hive.resources.mock_lobster import *
-
-
-class TestUpdate(TestCase):
-    @skip("")
-    def test_apply_update_with_user_provided_generator_update_function(self):
-        """
-        tests that a user can inject a function like `user_provided_update_fn` below and that it will
-        be called when the simulation is stepped.
-
-        this is validated by modifying a `stored_magic_number` attribute in the provided update fn,
-        which itself ensures it is safely applied via an isinstace test.
-        """
-
-        old_magic_number = 7
-        new_magic_number = 42
-
-        class MockGenerator(NamedTuple, InstructionGenerator):
-            stored_magic_number: int = old_magic_number
-
-            def generate_instructions(
-                self,
-                simulation_state: SimulationState,
-                envronment: Environment,
-            ):
-                return self, ()
-
-        def user_provided_update_fn(instr_gen, sim):
-            if isinstance(instr_gen, MockGenerator):
-                return instr_gen._replace(stored_magic_number=new_magic_number)
-            else:
-                return None
-
-        sim = mock_sim()
-        env = mock_env()
-        u = Update.build(mock_config(), (MockGenerator(),), user_provided_update_fn)
-        runner = RunnerPayload(sim, env, u)
-        result = u.apply_update(runner)
-        updated_mock_gen = result.u.step_update.instruction_generators[0]
-        self.assertEqual(
-            updated_mock_gen.stored_magic_number,
-            42,
-            "the user provided update function should have been called",
-        )
diff --git a/tests/test_update_requests.py b/tests/test_update_requests.py
index 8d1c6ab0..a5586400 100644
--- a/tests/test_update_requests.py
+++ b/tests/test_update_requests.py
@@ -1,8 +1,6 @@
 from unittest import TestCase
 
-from nrel.hive.state.simulation_state.update.update_requests_from_file import (
-    UpdateRequestsFromFile,
-)
+from nrel.hive.state.simulation_state.update.update_requests_from_file import UpdateRequestsFromFile
 from nrel.hive.resources.mock_lobster import *
 
 
diff --git a/tests/test_update_requests_sampling.py b/tests/test_update_requests_sampling.py
index 5033e4df..f6c447c8 100644
--- a/tests/test_update_requests_sampling.py
+++ b/tests/test_update_requests_sampling.py
@@ -1,8 +1,6 @@
 from unittest import TestCase
 
-from nrel.hive.state.simulation_state.update.update_requests_sampling import (
-    UpdateRequestsSampling,
-)
+from nrel.hive.state.simulation_state.update.update_requests_sampling import UpdateRequestsSampling
 from nrel.hive.resources.mock_lobster import *
 
 
diff --git a/tests/test_vehicle_state.py b/tests/test_vehicle_state.py
index 962277d0..438ce329 100644
--- a/tests/test_vehicle_state.py
+++ b/tests/test_vehicle_state.py
@@ -1,10 +1,9 @@
+from dataclasses import replace
 from unittest import TestCase
 
 from nrel.hive.state.entity_state import entity_state_ops
 from nrel.hive.state.vehicle_state.charge_queueing import ChargeQueueing
-from nrel.hive.state.vehicle_state.dispatch_pooling_trip import (
-    DispatchPoolingTrip,
-)
+from nrel.hive.state.vehicle_state.dispatch_pooling_trip import DispatchPoolingTrip
 from nrel.hive.state.vehicle_state.servicing_trip import ServicingTrip
 from nrel.hive.state.vehicle_state.out_of_service import OutOfService
 from nrel.hive.resources.mock_lobster import *
@@ -976,10 +975,11 @@ def test_dispatch_trip_update(self):
 
     def test_dispatch_trip_update_terminal(self):
         vehicle = mock_vehicle_from_geoid(geoid="8f268cdac30e2d3")
-        request = mock_request_from_geoids(
-            origin="8f268cdac30e2d3",
-            destination="8f268cdac70e2d3",
-        )._replace(  # a hack so we can test equality on this request later
+        request = replace(
+            mock_request_from_geoids(
+                origin="8f268cdac30e2d3",
+                destination="8f268cdac70e2d3",
+            ),
             dispatched_vehicle=vehicle.id,
             dispatched_vehicle_time=mock_sim().sim_time,
         )