Skip to content

Commit 5fedada

Browse files
authored
Merge branch 'main' into release/v1.3.0
2 parents 3369505 + c0bcf01 commit 5fedada

34 files changed

+327
-195
lines changed

.github/workflows/tests.yaml

+13-12
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@ name: tests
22

33
on:
44
push:
5-
branches: [ main ]
5+
branches: [main]
66
pull_request:
77

88
jobs:
99
test:
10-
if: github.event.pull_request.merged == false
11-
runs-on: ubuntu-latest
10+
if: github.event.pull_request.merged == false
11+
runs-on: ubuntu-latest
1212

13-
strategy:
14-
fail-fast: false
15-
matrix:
16-
python-version: ['3.8', '3.9', '3.10', '3.11']
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: ["3.8", "3.9", "3.10", "3.11"]
1717

18-
env:
19-
PYTHON: ${{ matrix.python-version }}
18+
env:
19+
PYTHON: ${{ matrix.python-version }}
2020

21-
steps:
21+
steps:
2222
- uses: actions/checkout@v3
2323

2424
- name: set up python ${{ matrix.python-version }}
@@ -29,6 +29,7 @@ jobs:
2929
- name: Install package
3030
run: |
3131
pip install ".[dev]"
32+
pip install pandas # determinism test dependency
3233
3334
- name: Run mypy
3435
run: mypy . --ignore-missing-imports
@@ -45,6 +46,6 @@ jobs:
4546
run: |
4647
pytest tests/ -v
4748
48-
- name: HIVE Denver Demo test
49+
- name: HIVE determinism test
4950
run: |
50-
hive denver_demo.yaml
51+
python examples/test_for_determinism.py --iterations 2

docs/source/developer/design.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Design
2+
3+
This page describes details specific to HIVE for new developers interacting with the library.
4+
5+
## table of contents
6+
7+
- **[determinism](#determinism)**: details related to keeping HIVE runs deterministic
8+
9+
### determinism
10+
11+
#### immutables.Map does not iterate based on insertion order
12+
13+
Most of the HIVE state is stored in hash maps. A [3rd party library](https://github.com/MagicStack/immutables) provides an immutable hash map via the Hash Array Mapped Trie (HAMT) data structure. While it is, for the most part, a drop-in replacement for a python Dict, it has one caveat, which is that insertion order is not guaranteed. This has determinism implications for HIVE. For this reason, any iteration of HAMT data structures must first be _sorted_. This is the default behavior for accessing the entity collections on a `SimulationState`, that they are first sorted by `EntityId`, such as `sim.get_vehicles()`.
14+
15+
Deeper within HIVE, whenever the HAMT data structure is interacted with, we must take care. There are two possible situations:
16+
1. the iteration order is irrelevant (for example, when iterating on a collection in order to write reports, or when updating a collection)
17+
- here, use of `.items()` iteration is acceptable
18+
2. the iteration order is sorted (exclusively when retrieving a Map as an _iterator_)
19+
- here, prefer `DictOps.iterate_vals()` or `DictOps.iterate_items()` which first sort by key
20+
- if key sorting is not preferred, write a specialized sort
21+
22+
When making a specialized sort function over a set of entities, consider bundling the cost value with the entity id. If two entities have the same value, the id can be used to "break the tie" in a deterministic way. Example:
23+
24+
```python
25+
vs: List[Vehicle] = ... #
26+
sorted(vs, key=lambda v: v.distance_traveled_km) # bad
27+
sorted(vs, key=lambda v: (v.distance_traveled_km, v.id)) # good
28+
```

docs/source/developer/index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ Developer Docs
66

77
release
88
contributing
9-
9+
design

examples/test_for_determinism.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from pathlib import Path
2+
from typing import Dict, List
3+
from pkg_resources import resource_filename
4+
from nrel.hive.initialization.load import load_config, load_simulation
5+
import random
6+
import numpy
7+
import pandas
8+
import argparse
9+
import sys
10+
11+
from nrel.hive.runner.local_simulation_runner import LocalSimulationRunner
12+
13+
# this utility demonstrates a set of runs have the same high-level results
14+
if __name__ == "__main__":
15+
denver = Path(resource_filename(
16+
"nrel.hive.resources.scenarios.denver_downtown",
17+
"denver_demo.yaml"))
18+
parser = argparse.ArgumentParser()
19+
parser.add_argument('--scenario', type=Path, default=denver)
20+
parser.add_argument('--iterations', type=int, default=5)
21+
parser.add_argument('--outfile', type=Path, required=False)
22+
args = parser.parse_args()
23+
24+
iterations = args.iterations
25+
data: List[Dict] = []
26+
for i in range(iterations):
27+
# set up config with scenario + limited (stats only) logging
28+
config_no_log = load_config(args.scenario).suppress_logging()
29+
config = config_no_log._replace(
30+
global_config=config_no_log.global_config._replace(
31+
log_stats=True
32+
)
33+
)
34+
# set random seed from Sim config
35+
if config.sim.seed is not None:
36+
random.seed(config.sim.seed)
37+
numpy.random.seed(config.sim.seed)
38+
39+
rp0 = load_simulation(config)
40+
rp1 = LocalSimulationRunner.run(rp0)
41+
stats = rp1.e.reporter.get_summary_stats(rp1)
42+
if stats is None:
43+
raise Exception("hive result missing stats object")
44+
stats['iteration'] = i
45+
# flatten vehicle states
46+
vs = stats['vehicle_state'].copy()
47+
del stats['vehicle_state']
48+
for k, v in vs.items():
49+
stats[f'{k}StatePct'] = v['observed_percent']
50+
stats[f'{k}StateVkt'] = v['vkt']
51+
52+
data.append(stats)
53+
print(f"finished iteration {i}")
54+
55+
56+
df = pandas.DataFrame(data)
57+
58+
test_cols = [
59+
'mean_final_soc',
60+
'requests_served_percent',
61+
'total_vkt',
62+
'total_kwh_expended',
63+
'total_gge_expended',
64+
'total_kwh_dispensed',
65+
'total_gge_dispensed'
66+
]
67+
68+
print(f'testing for determinism between {args.iterations} runs')
69+
exit_code = 0
70+
for col in test_cols:
71+
n = df[col].nunique()
72+
if n == 1:
73+
print(f'{col} is good, all values match')
74+
else:
75+
exit_code = 1
76+
entries = '[' + ', '.join(df[col].unique()) + ']'
77+
print(f'{col} no good, has {n} unique entries (should be one): {entries}')
78+
79+
if args.outfile:
80+
df.to_csv(args.outfile)
81+
82+
sys.exit(exit_code)

nrel/hive/app/hive_cosim.py

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from typing import Iterable, Tuple, NamedTuple, Optional, TypeVar
44

55
from tqdm import tqdm
6+
import random
7+
import numpy
68

79
from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
810
from nrel.hive.initialization.initialize_simulation import InitFunction
@@ -27,6 +29,11 @@ def load_scenario(
2729
:raises: Error when issues with files
2830
"""
2931
config = load_config(scenario_file, output_suffix)
32+
33+
if config.sim.seed is not None:
34+
random.seed(config.sim.seed)
35+
numpy.random.seed(config.sim.seed)
36+
3037
initial_payload = load_simulation(config, custom_instruction_generators, custom_init_functions)
3138

3239
# add a specialized Reporter handler that catches vehicle charge events

nrel/hive/app/run.py

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import pkg_resources
1010
import yaml
11+
import random
12+
import numpy
1113

1214
from nrel.hive.dispatcher.instruction_generator.instruction_generator import InstructionGenerator
1315
from nrel.hive.initialization.initialize_simulation import InitFunction
@@ -52,6 +54,10 @@ def run_sim(
5254

5355
config = load_config(scenario_file)
5456

57+
if config.sim.seed is not None:
58+
random.seed(config.sim.seed)
59+
numpy.random.seed(config.sim.seed)
60+
5561
initial_payload = load_simulation(
5662
config,
5763
custom_instruction_generators=custom_instruction_generators,

nrel/hive/config/sim.py

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Sim(NamedTuple):
1919
request_cancel_time_seconds: int
2020
schedule_type: ScheduleType
2121
min_delta_energy_change: Ratio = 0.0001
22+
seed: Optional[int] = 0
2223

2324
@classmethod
2425
def default_config(cls) -> Dict:

nrel/hive/dispatcher/instruction_generator/assignment_ops.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -303,15 +303,15 @@ def _time_to_full(v: Vehicle) -> Seconds:
303303

304304
return _time_to_full
305305

306-
def _sort_enqueue_time(v: Vehicle) -> int:
306+
def _sort_enqueue_time(v: Vehicle) -> Tuple[int, str]:
307307
if isinstance(v.vehicle_state, ChargeQueueing):
308308
enqueue_time = int(v.vehicle_state.enqueue_time)
309309
else:
310310
log.error(
311311
"calling _sort_enqueue_time on a vehicle state that is not ChargeQueueing"
312312
)
313313
enqueue_time = 0
314-
return enqueue_time
314+
return (enqueue_time, v.id)
315315

316316
def _greedy_assignment(
317317
_charging: Tuple[Seconds, ...],
@@ -374,12 +374,11 @@ def _greedy_assignment(
374374
vehicles_at_station = sim.get_vehicles(filter_function=_veh_at_station)
375375
vehicles_enqueued = sim.get_vehicles(
376376
filter_function=_veh_enqueued,
377-
sort=True,
378377
sort_key=_sort_enqueue_time,
379378
)
380379

381380
estimates: Dict[ChargerId, int] = {}
382-
for charger_id in station.state.keys():
381+
for charger_id in sorted(station.state.keys()):
383382
charger_state = station.state.get(charger_id)
384383
charger = charger_state.charger if charger_state is not None else None
385384

nrel/hive/dispatcher/instruction_generator/dispatcher.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,7 @@ def _valid_request(r: Request) -> bool:
9696
)
9797

9898
unassigned_requests = simulation_state.get_requests(
99-
sort=True,
100-
sort_key=lambda r: r.value,
101-
sort_reversed=True,
99+
sort_key=lambda r: -r.value,
102100
filter_function=_valid_request,
103101
)
104102

nrel/hive/dispatcher/instruction_generator/instruction_generator_ops.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
log = logging.getLogger(__name__)
1818

19-
random.seed(123)
20-
2119
if TYPE_CHECKING:
2220
from nrel.hive.model.vehicle.vehicle import Vehicle
2321
from nrel.hive.state.simulation_state.simulation_state import SimulationState
@@ -85,7 +83,7 @@ def add_driver_instructions(self, simulation_state, environment):
8583
),
8684
)
8785
+ acc,
88-
simulation_state.vehicles.values(),
86+
simulation_state.get_vehicles(),
8987
(),
9088
)
9189

@@ -313,7 +311,7 @@ def get_nearest_valid_station_distance(
313311

314312
nearest_station = H3Ops.nearest_entity(
315313
geoid=geoid,
316-
entities=simulation_state.stations.values(),
314+
entities=simulation_state.get_stations(),
317315
entity_search=simulation_state.s_search,
318316
sim_h3_search_resolution=simulation_state.sim_h3_search_resolution,
319317
max_search_distance_km=max_search_radius_km,

nrel/hive/initialization/initialize_simulation_with_sampling.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,7 @@ def _add_row_unsafe(
186186
)
187187

188188
# add all stations to the simulation once we know they are complete
189-
sim_with_stations = simulation_state_ops.add_entities(
190-
simulation_state, stations_builder.values()
191-
)
189+
stations = DictOps.iterate_vals(stations_builder)
190+
sim_with_stations = simulation_state_ops.add_entities(simulation_state, stations)
192191

193192
return sim_with_stations

nrel/hive/initialization/sample_requests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,6 @@ def default_request_sampler(
6767

6868
id_counter += 1
6969

70-
sorted_reqeusts = sorted(requests, key=lambda r: r.departure_time)
70+
sorted_reqeusts = sorted(requests, key=lambda r: (r.departure_time, r.id))
7171

7272
return tuple(sorted_reqeusts)

nrel/hive/model/roadnetwork/roadnetwork.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def position_from_geoid(self, geoid: GeoId) -> Optional[EntityPosition]:
7979
position = EntityPosition(link.link_id, geoid)
8080
return position
8181
else:
82-
hexes_by_dist = sorted(hexes_on_link, key=lambda h: h3.h3_distance(geoid, h))
82+
hexes_by_dist = sorted(hexes_on_link, key=lambda h: (h3.h3_distance(geoid, h), h))
8383
closest_hex_to_query = hexes_by_dist[0]
8484
position = EntityPosition(link.link_id, closest_hex_to_query)
8585
return position

nrel/hive/model/station/station.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
station_state_updates,
2424
)
2525
from nrel.hive.runner.environment import Environment
26+
from nrel.hive.util.dict_ops import DictOps
2627
from nrel.hive.util.error_or_result import ErrorOr
2728
from nrel.hive.util.exception import H3Error, SimulationStateError
2829
from nrel.hive.util.typealiases import *
@@ -109,7 +110,7 @@ def _chargers(acc, charger_data):
109110
return None, updated_builder
110111

111112
initial = None, immutables.Map[ChargerId, ChargerState]()
112-
error, charger_states = ft.reduce(_chargers, chargers.items(), initial)
113+
error, charger_states = ft.reduce(_chargers, DictOps.iterate_items(chargers), initial)
113114
if error is not None:
114115
raise error
115116
if charger_states is None:

nrel/hive/reporting/handler/stateful_handler.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,23 @@ def handle(self, reports: List[Report], runner_payload: RunnerPayload):
3535
sim_state = runner_payload.s
3636
if ReportType.DRIVER_STATE in self.global_config.log_sim_config:
3737
self._report_entities(
38-
entities=sim_state.vehicles.values(),
38+
entities=sim_state.get_vehicles(),
3939
asdict=self.driver_asdict,
4040
sim_time=sim_state.sim_time,
4141
report_type=ReportType.DRIVER_STATE,
4242
)
4343

4444
if ReportType.VEHICLE_STATE in self.global_config.log_sim_config:
4545
self._report_entities(
46-
entities=sim_state.vehicles.values(),
46+
entities=sim_state.get_vehicles(),
4747
asdict=self.vehicle_asdict,
4848
sim_time=sim_state.sim_time,
4949
report_type=ReportType.VEHICLE_STATE,
5050
)
5151

5252
if ReportType.STATION_STATE in self.global_config.log_sim_config:
5353
self._report_entities(
54-
entities=sim_state.stations.values(),
54+
entities=sim_state.get_stations(),
5555
asdict=self.station_asdict,
5656
sim_time=sim_state.sim_time,
5757
report_type=ReportType.STATION_STATE,

nrel/hive/reporting/handler/summary_stats.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,19 @@ def compile_stats(self, rp: RunnerPayload) -> Dict[str, Any]:
4848
self.mean_final_soc = mean(
4949
[
5050
env.mechatronics[v.mechatronics_id].fuel_source_soc(v)
51-
for v in sim_state.vehicles.values()
51+
for v in sim_state.get_vehicles()
5252
]
5353
)
5454

5555
self.station_revenue = reduce(
5656
lambda income, station: income + station.balance,
57-
sim_state.stations.values(),
57+
sim_state.get_stations(),
5858
0.0,
5959
)
6060

6161
self.fleet_revenue = reduce(
6262
lambda income, vehicle: income + vehicle.balance,
63-
sim_state.vehicles.values(),
63+
sim_state.get_vehicles(),
6464
0.0,
6565
)
6666

0 commit comments

Comments
 (0)