Skip to content

Commit e446960

Browse files
authored
Merge pull request #44 from Association-INTech/option-no-camera
Option no camera
2 parents 615aecb + 4d5165d commit e446960

10 files changed

Lines changed: 241 additions & 45 deletions

File tree

src/simulation/scripts/launch_train_multiprocessing.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77
from stable_baselines3 import PPO
88
from stable_baselines3.common.vec_env import SubprocVecEnv
99

10-
from extractors import ( # noqa: F401
11-
CNN1DExtractor,
12-
CNN1DResNetExtractor,
13-
TemporalResNetExtractor,
14-
)
1510
from simulation import VehicleEnv
1611
from simulation import config as c
1712
from utils import onnx_utils
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from .cnn_1d_extractor import CNN1DExtractor
22
from .cnn_1d_resnet_extractor import CNN1DResNetExtractor
3+
from .cnn_1d_resnet_no_cam_extractor import CNN1DResNetNoCamExtractor
34
from .temporal_resnet_extractor import TemporalResNetExtractor
45

56
__all__ = [
67
"CNN1DExtractor",
78
"CNN1DResNetExtractor",
9+
"CNN1DResNetNoCamExtractor",
810
"TemporalResNetExtractor",
911
]
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import torch
2+
import torch.nn as nn
3+
from gymnasium import spaces
4+
from stable_baselines3.common.torch_layers import BaseFeaturesExtractor
5+
6+
7+
class CNN1DResNetNoCamExtractor(BaseFeaturesExtractor):
8+
context_size = 1
9+
lidar_horizontal_resolution = 1024
10+
camera_horizontal_resolution = 0
11+
n_sensors = 1
12+
13+
# just an alias to avoid confusion because
14+
# the lidar and camera have the same resolution
15+
horizontal_resolution = 1024
16+
17+
def __init__(
18+
self,
19+
space: spaces.Box,
20+
device: str = "cpu",
21+
):
22+
net = nn.Sequential(
23+
# shape = [batch_size, 1, 1024]
24+
Compressor(device),
25+
# shape = [batch_size, 64, 256]
26+
ResidualBlock(64, 64, device=device),
27+
ResidualBlock(64, 64, device=device),
28+
ResidualBlock(64, 64, downsample=True, device=device),
29+
# shape = [batch_size, 128, 128]
30+
ResidualBlock(64, 64, device=device),
31+
ResidualBlock(64, 64, device=device),
32+
ResidualBlock(64, 128, downsample=True, device=device),
33+
# shape = [batch_size, 128, 64]
34+
ResidualBlock(128, 128, device=device),
35+
ResidualBlock(128, 128, device=device),
36+
ResidualBlock(128, 128, downsample=True, device=device),
37+
# shape = [batch_size, 256, 32]
38+
ResidualBlock(128, 128, device=device),
39+
ResidualBlock(128, 128, device=device),
40+
ResidualBlock(128, 256, downsample=True, device=device),
41+
# shape = [batch_size, 256, 16]
42+
ResidualBlock(256, 256, device=device),
43+
ResidualBlock(256, 256, device=device),
44+
ResidualBlock(256, 256, downsample=True, device=device),
45+
# shape = [batch_size, 256, 8]
46+
nn.AvgPool1d(8),
47+
# shape = [batch_size, 256, 1]
48+
nn.Flatten(),
49+
# shape = [batch_size, 256]
50+
)
51+
52+
# Compute shape by doing one forward pass
53+
with torch.no_grad():
54+
n_flatten = net(
55+
torch.zeros(
56+
[1, 1, self.context_size, self.horizontal_resolution], device=device
57+
)
58+
).shape[1]
59+
60+
super().__init__(space, n_flatten)
61+
62+
# we cannot assign this directly to self.cnn before calling the super constructor
63+
self.net = net
64+
65+
def forward(self, observations: torch.Tensor) -> torch.Tensor:
66+
return self.net(observations)
67+
68+
69+
class Compressor(nn.Module):
70+
def __init__(self, device: str = "cpu"):
71+
super().__init__()
72+
# WARNING : do not use inplace=True because it would modify the rollout buffer
73+
self.conv = nn.Conv1d(1, 64, kernel_size=7, stride=2, padding=3, device=device)
74+
self.dropout = nn.Dropout1d(0.3)
75+
self.pool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1)
76+
77+
def forward(self, x: torch.Tensor) -> torch.Tensor:
78+
x = x[:, :, 0]
79+
x = self.conv(x)
80+
x = self.dropout(x)
81+
x = self.pool(x)
82+
return x
83+
84+
85+
class ResidualBlock(nn.Module):
86+
"""
87+
basic block with a residual connection
88+
"""
89+
90+
def __init__(
91+
self,
92+
in_channels: int,
93+
out_channels: int,
94+
downsample: bool = False,
95+
device: str = "cpu",
96+
):
97+
super().__init__()
98+
if downsample:
99+
stride = 2
100+
self.downsample = nn.Conv1d(
101+
in_channels, out_channels, kernel_size=1, stride=2, device=device
102+
)
103+
elif in_channels == out_channels:
104+
stride = 1
105+
self.downsample = nn.Identity()
106+
else:
107+
stride = 1
108+
self.downsample = nn.Conv1d(
109+
in_channels, out_channels, kernel_size=1, stride=1, device=device
110+
)
111+
112+
self.bn1 = nn.BatchNorm1d(in_channels, device=device)
113+
self.conv1 = nn.Conv1d(
114+
in_channels,
115+
out_channels,
116+
kernel_size=3,
117+
stride=stride,
118+
padding=1,
119+
device=device,
120+
)
121+
122+
self.bn2 = nn.BatchNorm1d(out_channels, device=device)
123+
self.conv2 = nn.Conv1d(
124+
out_channels, out_channels, kernel_size=3, padding=1, device=device
125+
)
126+
127+
self.relu = nn.ReLU(inplace=True)
128+
self.dropout = nn.Dropout1d(0.4)
129+
130+
def forward(self, x: torch.Tensor) -> torch.Tensor:
131+
y = self.bn1(x)
132+
y = self.relu(y)
133+
y = self.conv1(y)
134+
135+
y = self.bn2(y)
136+
y = self.relu(y)
137+
y = self.dropout(y)
138+
y = self.conv2(y)
139+
140+
y += self.downsample(x)
141+
142+
return y

src/simulation/src/simulation/config.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
from extractors import ( # noqa: F401
1010
CNN1DExtractor,
1111
CNN1DResNetExtractor,
12+
CNN1DResNetNoCamExtractor,
1213
TemporalResNetExtractor,
1314
)
1415

1516
# Webots environments config
16-
n_map = 2
17+
n_map = 3
1718
n_simulations = 1
18-
n_vehicles = 2
19+
n_vehicles = 1
1920
n_stupid_vehicles = 0
2021
n_actions_steering = 16
2122
n_actions_speed = 16
@@ -41,12 +42,19 @@
4142

4243
# Common extractor shared between the policy and value networks
4344
# (cf: https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html)
44-
ExtractorClass = TemporalResNetExtractor
45+
ExtractorClass = CNN1DResNetNoCamExtractor
4546
context_size = ExtractorClass.context_size
4647
lidar_horizontal_resolution = ExtractorClass.lidar_horizontal_resolution
4748
camera_horizontal_resolution = ExtractorClass.camera_horizontal_resolution
4849
n_sensors = ExtractorClass.n_sensors
4950

51+
if (
52+
lidar_horizontal_resolution != camera_horizontal_resolution
53+
and camera_horizontal_resolution != 0
54+
):
55+
raise NotImplementedError(
56+
"Unhomogenous lidar and camera shape is only supported if camera_horizontal_resolution == 0"
57+
)
5058

5159
# Architecture of the model
5260
policy_kwargs: Dict[str, Any] = dict(

src/simulation/src/simulation/vehicle_env.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def __init__(self, simulation_rank: int, vehicle_rank: int):
4646
self.vehicle_rank = vehicle_rank
4747

4848
self.handler = logging.FileHandler(
49-
f"/tmp/autotech/Voiture_{self.simulation_rank}_{self.vehicle_rank}.log"
49+
f"/tmp/autotech/vehicle_{self.simulation_rank}_{self.vehicle_rank}.log"
5050
)
5151
self.handler.setFormatter(c.FORMATTER)
5252
self.log = logging.getLogger(
@@ -57,15 +57,24 @@ def __init__(self, simulation_rank: int, vehicle_rank: int):
5757

5858
self.log.info("Initialisation started")
5959

60-
# this is only true if lidar_horizontal_resolution = camera_horizontal_resolution
60+
# 2 channels if camera_horizontal_resolution != 0
61+
# 1 channel otherwise
6162
box_min = np.zeros(
62-
[2, c.context_size, c.lidar_horizontal_resolution], dtype=np.float32
63+
[
64+
1 + (c.camera_horizontal_resolution != 0),
65+
c.context_size,
66+
c.lidar_horizontal_resolution,
67+
],
68+
dtype=np.float32,
6369
)
64-
box_max = (
65-
np.ones(
66-
[2, c.context_size, c.lidar_horizontal_resolution], dtype=np.float32
67-
)
68-
* 30
70+
box_max = np.full(
71+
[
72+
1 + (c.camera_horizontal_resolution != 0),
73+
c.context_size,
74+
c.lidar_horizontal_resolution,
75+
],
76+
30,
77+
dtype=np.float32,
6978
)
7079

7180
self.observation_space = gym.spaces.Box(box_min, box_max, dtype=np.float32)
@@ -91,6 +100,7 @@ def __init__(self, simulation_rank: int, vehicle_rank: int):
91100
"--mode=fast",
92101
"--minimize",
93102
"--batch",
103+
# "--no-rendering",
94104
]
95105
)
96106

@@ -111,11 +121,17 @@ def __init__(self, simulation_rank: int, vehicle_rank: int):
111121
def reset(self, seed: int | None = None, **options) -> Tuple[ObsType, Dict]:
112122
# basically useless function
113123

114-
# lidar data
115-
# this is true for lidar_horizontal_resolution = camera_horizontal_resolution
124+
# 2 channels if camera_horizontal_resolution != 0
125+
# 1 channel otherwise
116126
self.context = obs = np.zeros(
117-
[2, c.context_size, c.lidar_horizontal_resolution], dtype=np.float32
127+
[
128+
1 + (c.camera_horizontal_resolution != 0),
129+
c.context_size,
130+
c.lidar_horizontal_resolution,
131+
],
132+
dtype=np.float32,
118133
)
134+
119135
info = {}
120136
self.log.info("reset finished\n")
121137
return obs, info
@@ -127,7 +143,7 @@ def step(self, action: ActType):
127143
self.fifo_w.flush()
128144

129145
# communication with the supervisor
130-
self.log.debug("trying to get info from supervisor")
146+
self.log.info(f"Waiting for (obs, reward, done, truncated) from SUPERVISOR_{self.simulation_rank}_{self.vehicle_rank}")
131147
cur_state = np.frombuffer(
132148
self.fifo_r.read(
133149
np.dtype(np.float32).itemsize
@@ -159,9 +175,14 @@ def step(self, action: ActType):
159175
lidar_obs = cur_state[: c.lidar_horizontal_resolution]
160176
camera_obs = cur_state[c.lidar_horizontal_resolution :]
161177

162-
self.context = obs = np.concatenate(
163-
[self.context[:, 1:], [lidar_obs[None], camera_obs[None]]], axis=1
164-
)
178+
if c.camera_horizontal_resolution == 0:
179+
self.context = obs = np.concatenate(
180+
[self.context[:, 1:], lidar_obs[None, None]], axis=1
181+
)
182+
else:
183+
self.context = obs = np.concatenate(
184+
[self.context[:, 1:], [lidar_obs[None], camera_obs[None]]], axis=1
185+
)
165186

166187
self.log.info("step over")
167188

src/simulation/src/utils/onnx_utils.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ def export_onnx(sb_model: OnPolicyAlgorithm, path: str):
2222
torch_model = get_torch_model(sb_model)
2323
torch_model.eval()
2424

25-
example_input = torch.randn(1, 2, c.context_size, c.lidar_horizontal_resolution)
25+
example_input = torch.randn(
26+
1,
27+
1 + (c.camera_horizontal_resolution != 0),
28+
c.context_size,
29+
c.lidar_horizontal_resolution,
30+
)
2631

2732
with torch.no_grad():
2833
torch.onnx.export(
@@ -50,7 +55,12 @@ def test_onnx(model: OnPolicyAlgorithm):
5055
true_model = get_torch_model(model)
5156

5257
loss_fn = nn.MSELoss()
53-
x = torch.randn(1000, 2, c.context_size, c.lidar_horizontal_resolution)
58+
x = torch.randn(
59+
1000,
60+
1 + (c.camera_horizontal_resolution != 0),
61+
c.context_size,
62+
c.lidar_horizontal_resolution,
63+
)
5464

5565
try:
5666
class_name = model.policy.features_extractor.__class__.__name__

src/simulation/src/webots/controllers/controller_vehicle_driver/controller_vehicle_driver.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ def __init__(self):
6969

7070
# Logger
7171
self.handler = logging.FileHandler(
72-
f"/tmp/autotech/Voiture_{self.simulation_rank}_{self.vehicle_rank}.log"
72+
f"/tmp/autotech/vehicle_{self.simulation_rank}_{self.vehicle_rank}.log"
7373
)
7474
self.handler.setFormatter(c.FORMATTER)
7575
self.log = logging.getLogger(
76-
f"CLIENT_{self.simulation_rank}_{self.vehicle_rank}"
76+
f"VEHICLE_{self.simulation_rank}_{self.vehicle_rank}"
7777
)
7878
self.log.setLevel(level=c.LOG_LEVEL)
7979
self.log.addHandler(self.handler)
@@ -96,19 +96,23 @@ def observe(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
9696

9797
lidar_data = np.array(self.lidar.getRangeImage(), dtype=np.float32)
9898

99-
camera_data = np.array(self.camera.getImageArray(), dtype=np.float32)
100-
# shape = (1080, 1, 3)
101-
camera_data = camera_data.transpose(1, 2, 0)[0]
102-
# shape = (3, 1080)
103-
color = np.argmax(camera_data, axis=0)
104-
camera_data = (
105-
(color == 0).astype(np.float32) * -1
106-
+ (color == 1).astype(np.float32) * 1
107-
+ (color == 2).astype(np.float32) * 0
108-
)
109-
# red -> -1
110-
# green -> 1
111-
# blue -> 0
99+
if c.camera_horizontal_resolution == 0:
100+
# empty array
101+
camera_data = np.zeros([0], dtype=np.float32)
102+
else:
103+
camera_data = np.array(self.camera.getImageArray(), dtype=np.float32)
104+
# shape = (camera_horizontal_resolution, 1, 3)
105+
camera_data = camera_data.transpose(1, 2, 0)[0]
106+
# shape = (3, 1080)
107+
color = np.argmax(camera_data, axis=0)
108+
camera_data = (
109+
(color == 0).astype(np.float32) * -1
110+
+ (color == 1).astype(np.float32) * 1
111+
+ (color == 2).astype(np.float32) * 0
112+
)
113+
# red -> -1
114+
# green -> 1
115+
# blue -> 0
112116

113117
return (sensor_data, lidar_data, camera_data)
114118

@@ -142,7 +146,7 @@ def ai(self):
142146
self.fifo_w.write(np.concatenate(obs).tobytes())
143147
self.fifo_w.flush()
144148

145-
self.log.debug("Trying to read action from the server")
149+
self.log.info(f"Waiting for an action from SERVER_{self.simulation_rank}_{self.vehicle_rank}")
146150
action = np.frombuffer(
147151
self.fifo_r.read(np.dtype(np.int64).itemsize * 2), dtype=np.int64
148152
)

0 commit comments

Comments
 (0)