Skip to content

Commit 1c45d4e

Browse files
Theodore-ChatziioannouTheodore-Chatziioannou
Theodore-Chatziioannou
authored and
Theodore-Chatziioannou
committed
simple scheduling model
1 parent 3f817a6 commit 1c45d4e

File tree

6 files changed

+168
-3
lines changed

6 files changed

+168
-3
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ mike-*.yml
3838
.ipynb_checkpoints
3939
examples
4040
**/outputs/
41-
**/tmp/
41+
**/tmp/
42+
temp/

requirements/base.txt

+2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ prettytable >= 3, < 4
1212
python-Levenshtein >= 0.21, < 0.26
1313
rich >= 12, < 14
1414
Rtree >= 1, < 2
15+
seaborn
1516
s2sphere < 0.3
1617
scikit-learn >= 1.2, < 2
1718
shapely >= 1, < 3
19+
tensforflow
1820
xlrd >= 2, < 3

src/pam/planner/choice_scheduling.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from typing import Optional
2+
3+
import numpy as np
4+
from tensorflow import keras
5+
6+
from pam.core import Population
7+
from pam.planner.encoder import PlansSequenceEncoder
8+
9+
10+
class ScheduleModelSimple:
11+
def __init__(
12+
self, population: Population, n_units: Optional[int] = 50, dropout: Optional[float] = 0.1
13+
) -> None:
14+
self.encoder = PlansSequenceEncoder(population=population)
15+
16+
# build model
17+
input_acts = keras.layers.Input(shape=[self.encoder.acts.shape[1]])
18+
emb_acts = keras.layers.Embedding(
19+
len(self.encoder.activity_encoder.labels), 1, mask_zero=True, name="emb"
20+
)(input_acts)
21+
encoder_h1, encoder_h, encoder_c = keras.layers.LSTM(
22+
n_units, return_state=True, name="encoder_h1"
23+
)(emb_acts)
24+
encoder_state = [encoder_h, encoder_c]
25+
26+
decoder_input = keras.layers.Input(shape=[self.encoder.durations.shape[1] - 1, 1])
27+
decoder_h1 = keras.layers.LSTM(
28+
n_units, name="decoder_h1", dropout=dropout, return_sequences=True
29+
)(decoder_input, initial_state=encoder_state)
30+
decoder_h2 = keras.layers.LSTM(
31+
n_units, name="decoder_h2", dropout=dropout, return_sequences=True
32+
)(decoder_h1)
33+
decoder_output = keras.layers.Dense(1, activation="relu", name="decoder_output")(decoder_h2)
34+
model = keras.models.Model(inputs=[input_acts, decoder_input], outputs=[decoder_output])
35+
36+
model.compile(loss="mean_squared_error", optimizer="adam", metrics=["accuracy"])
37+
model.summary()
38+
39+
self.model = model
40+
41+
def fit(self, epochs: int = 500) -> None:
42+
"""Fit the sceduling model.
43+
44+
Args:
45+
epochs (int, optional): Number of epochs to run. Defaults to 500.
46+
"""
47+
X = self.encoder.acts[:, ::-1]
48+
durations = self.encoder.durations
49+
self.history = self.model.fit([X, durations[:, :-1]], durations[:, 1:], epochs=epochs)
50+
51+
def predict(self, population: Population) -> np.array:
52+
"""Predict the activity durations of a population.
53+
54+
Args:
55+
population (Population): A PAM population.
56+
57+
Returns:
58+
np.array: Durations array. Each row represents a plan.
59+
"""
60+
encoder = PlansSequenceEncoder(
61+
population=population, activity_encoder=self.encoder.activity_encoder
62+
)
63+
X = encoder.acts[:, ::-1]
64+
y_pred = np.zeros(shape=encoder.durations.shape)
65+
for i in range(1, y_pred.shape[1]):
66+
y_pred[:, i] = self.model.predict([X, y_pred[:, :i]])[:, -1, 0]
67+
68+
return y_pred

src/pam/planner/encoder.py

+64-2
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@
44

55
if TYPE_CHECKING:
66
from pam.activity import Plan
7+
from pam.core import Population
78

89
from datetime import timedelta as td
910
from itertools import groupby
1011
from typing import List, Optional, Union
1112

1213
import numpy as np
14+
import pandas as pd
1315

1416
from pam import activity
1517
from pam.variables import START_OF_DAY
1618

1719

1820
class Encoder:
1921
def __init__(self, labels: List[str], travel_act="travel") -> None:
20-
self.labels = set(labels)
22+
self.labels = list(labels)
2123
if travel_act not in self.labels:
22-
self.labels.add(travel_act)
24+
self.labels.append(travel_act)
2325
self.label_code = self.get_mapping(self.labels)
2426
self.code_label = {v: k for k, v in self.label_code.items()}
2527

@@ -169,3 +171,63 @@ class PlansOneHotEncoder(PlansEncoder):
169171
"""
170172

171173
plans_encoder_class = PlanOneHotEncoder
174+
175+
176+
class PlansSequenceEncoder:
177+
def __init__(self, population: Population, activity_encoder: Optional[Encoder] = None) -> None:
178+
"""Encodes the plans of a population into arrays representing sequencies of activities and durations.
179+
180+
Args:
181+
population (Population): A PAM population.
182+
activity_encoder (Optional[Encoder], optional): Encoder of activity types. Defaults to None.
183+
"""
184+
185+
self.population = population
186+
act_labels = ["NA", "SOS", "EOS"] + list(population.activity_classes)
187+
188+
if activity_encoder is None:
189+
self.activity_encoder = StringIntEncoder(act_labels)
190+
else:
191+
self.activity_encoder = activity_encoder
192+
193+
self.acts = None
194+
self.acts_labels = None
195+
self.durations = None
196+
197+
self.encode_plans()
198+
199+
def encode_plans(self) -> None:
200+
"""Encode sequencies of activities and durations into numpy arrays."""
201+
acts = []
202+
acts_labels = []
203+
durations = []
204+
for hid, pid, person in self.population.people():
205+
# start-of-sequence values
206+
person_acts = [1]
207+
person_acts_labels = []
208+
person_durations = [0]
209+
210+
# collect activities and durations
211+
for act in person.activities:
212+
person_acts.append(self.activity_encoder.encode(act.act))
213+
person_acts_labels.append(act.act)
214+
person_durations.append(act.duration / pd.Timedelta(hours=24))
215+
216+
# end-of-sequence values
217+
person_acts.append(2)
218+
person_durations.append(0)
219+
220+
# append
221+
acts.append(person_acts)
222+
acts_labels.append(person_acts_labels)
223+
durations.append(person_durations)
224+
225+
# convert to arrays
226+
acts = pd.DataFrame(acts).fillna(0).values.astype(int)
227+
durations = pd.DataFrame(durations).fillna(0).values
228+
durations = durations / durations.sum(1).reshape(-1, 1) # add up to 24 hours
229+
230+
# store
231+
self.acts = acts
232+
self.acts_labels = acts_labels
233+
self.durations = durations

tests/conftest.py

+8
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,14 @@ def population_no_args(test_trips_pathv12):
11201120
return read.read_matsim(test_trips_pathv12, version=12)
11211121

11221122

1123+
@pytest.fixture()
1124+
def population_simple():
1125+
df_diaries = pd.read_csv(TEST_DATA_DIR / "simple_travel_diaries.csv")
1126+
df_persons = pd.read_csv(TEST_DATA_DIR / "simple_persons_data.csv")
1127+
population = read.load_travel_diary(trips=df_diaries, persons_attributes=df_persons)
1128+
return population
1129+
1130+
11231131
@pytest.fixture
11241132
def population_experienced(test_experienced_pathv12):
11251133
return read.read_matsim(test_experienced_pathv12, version=12)

tests/test_29_planner_scheduling.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import numpy as np
2+
import pytest
3+
from pam.planner.choice_scheduling import ScheduleModelSimple
4+
from tensorflow import keras
5+
6+
7+
@pytest.fixture
8+
def model_simple(population_simple) -> ScheduleModelSimple:
9+
return ScheduleModelSimple(population_simple)
10+
11+
12+
def test_start_end_tokens(model_simple):
13+
assert model_simple.encoder.activity_encoder.label_code["SOS"] == 1
14+
assert model_simple.encoder.activity_encoder.label_code["EOS"] == 2
15+
16+
17+
def test_prediction_shape_matches_input(model_simple, population_simple):
18+
model_simple.fit(epochs=2)
19+
y_pred = model_simple.predict(population_simple)
20+
np.testing.assert_equal(y_pred.shape, model_simple.encoder.durations.shape)
21+
22+
23+
def test_model_built(model_simple):
24+
assert isinstance(model_simple.model, keras.models.Model)

0 commit comments

Comments
 (0)