New ideas

This commit is contained in:
2025-11-21 07:16:05 +01:00
parent 1538b0acf7
commit 5f8493d59e
6 changed files with 85738 additions and 115 deletions
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
# %%
import torch
from aiRNN import dataloader, models, losses
import pathlib
# %%
file_list = pathlib.Path("/home/lars/Documents/Studium/UiO/data_analysis/project3/Code/cpp/known_routes_and_aircraft.csv")
base_path = file_list.parent
file_list = file_list.read_text().splitlines()
file_list = [(base_path / f).resolve() for f in file_list]
# %%
dataset = dataloader.EvenlySpacedDataset(
filepaths=file_list[:500],
n_input=30*10, # 10 minutes input
n_output=30*1, # 1 minutes output
n_windows_per_file=5,
step=1,
feature_columns=("lat", "lon", "alt", "ias"),
context_columns=("last_lat", "last_lon", "last_alt", "last_ias"),
time_columns=("timestamp", "dt"),
target_columns=("lat", "lon", "alt"),
)
# %%
len(dataset)
# %%
test_model = models.ThreeInputRNN(
time_in=2,
feat_in=4,
context_in=4,
hidden_size=128,
rnn_size=256,
out_size=3,
)
# %%
# Example training
optimizer = torch.optim.Adam(test_model.parameters(), lr=1e-2)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5)
criterion = torch.nn.HuberLoss()
for epoch in range(50):
losses = []
for X_f, X_t, y, X_c in torch.utils.data.DataLoader(dataset, batch_size=256, shuffle=True):
optimizer.zero_grad()
y_pred, _ = test_model(X_t, X_f, X_c, 300, 30)
loss = criterion(y_pred, y)
loss.backward()
optimizer.step()
losses.append(loss.item())
loss = sum(losses) / len(losses)
scheduler.step(loss)
print(f"Epoch {epoch}: loss={loss}, lr={optimizer.param_groups[0]['lr']}")
# %%
+21 -22
View File
@@ -17,7 +17,7 @@ class BaseDataset(Dataset):
n_windows_per_file,
step=1,
feature_columns=("lat", "lon", "alt", "ias"),
context_columns=(f"type_encoding_{i}" for i in range(4), "last_lat", "last_lon", "last_alt", "last_ias"),
context_columns=(*[f"type_encoding_{i}" for i in range(4)], "last_lat", "last_lon", "last_alt", "last_ias"),
time_columns=("timestamp",),
target_columns=("lat", "lon", "alt"),
device="cpu",
@@ -33,6 +33,7 @@ class BaseDataset(Dataset):
# Which columns to use
sample = pd.read_csv(self.filepaths[0], nrows=5)
sample = self._modify_df(sample)
self.feature_cols = list(feature_columns)
self.context_cols = list(context_columns)
self.time_cols = list(time_columns)
@@ -69,27 +70,36 @@ class BaseDataset(Dataset):
def _modify_df(self, df):
"""Hook for subclasses to modify dataframe before slicing windows."""
df.loc[:, ["lat", "lon", "alt"]] = preprocessors.norm_coords(
lat_norm, lon_norm, alt_norm = preprocessors.norm_coords(
df["lat"].values, df["lon"].values, df["alt"].values
)
df.loc[:, "lat"] = lat_norm
df.loc[:, "lon"] = lon_norm
df.loc[:, "alt"] = alt_norm
df.loc[:, "ias"] = preprocessors.norm_ias(df["ias"].values)
df.loc[:, "dt"] = df["timestamp"].diff()
df.loc[:, "dt"] = preprocessors.fillna_with_mean(df["dt"], allow_nan_mean=False)
df.loc[:, "timestamp"] = preprocessors.norm_time(df["timestamp"].values)
for col in ["lat", "lon", "alt", "ias"]:
df.loc[:, col] = preprocessors.fillna_with_mean(df[col])
if df[col].isnull().all() and col != "ias":
raise ValueError(f"All values in column {col} are NaN.")
df.loc[:, col] = preprocessors.fillna_with_mean(df[col], allow_nan_mean=(col == "ias"))
mapped_df = preprocessors.map_categories(df, preprocessors.category_mappings)
X_a_1 = F.one_hot(torch.tensor(mapped_df["R_1_IDX"].values)).float()
X_a_2 = F.one_hot(torch.tensor(mapped_df["R_2_IDX"].values)).float()
X_t = F.one_hot(torch.tensor(mapped_df["T_IDX"].values)).float()
max_values = list(preprocessors.category_max_limits.values())
X_a_1 = F.one_hot(torch.tensor(mapped_df["R_1_IDX"].values), max_values[0]).float()
X_a_2 = F.one_hot(torch.tensor(mapped_df["R_2_IDX"].values), max_values[1]).float()
X_t = F.one_hot(torch.tensor(mapped_df["T_IDX"].values), max_values[2]).float()
df.loc[:, [f"type_encoding_{i}" for i in range(4)]] = preprocessors.encode_features(
X_a_1, X_a_2, X_t
)
last_row = df.iloc[-1][["lat", "lon", "alt", "ias"]]
last_row = df.iloc[-1][["lat", "lon", "alt", "ias", "timestamp"]]
df.loc[:, "last_lat"] = last_row["lat"]
df.loc[:, "last_lon"] = last_row["lon"]
df.loc[:, "last_alt"] = last_row["alt"]
df.loc[:, "last_ias"] = last_row["ias"]
df.loc[:, "last_timestamp"] = last_row["timestamp"]
return df
@@ -103,7 +113,7 @@ class EvenlySpacedDataset(BaseDataset):
n_windows_per_file,
step=1,
feature_columns=("lat", "lon", "alt", "ias"),
context_columns=("r","t"),
context_columns=(*[f"type_encoding_{i}" for i in range(4)], "last_lat", "last_lon", "last_alt", "last_ias"),
time_columns=("timestamp",),
target_columns=("lat", "lon", "alt"),
device="cpu",
@@ -145,7 +155,7 @@ class EvenlySpacedDataset(BaseDataset):
X_feat = window.iloc[: self.n_input][self.feature_cols].to_numpy(np.float32)
if len(self.context_cols) > 0:
X_context = window.iloc[: self.n_input][self.context_cols].to_numpy(np.float32)
X_context = window.iloc[0][self.context_cols].to_numpy(np.float32)
else:
X_context = None
X_time = window.iloc[: self.window_size][self.time_cols].to_numpy(np.float32)
@@ -166,7 +176,7 @@ class EvenlySpacedStreamingDataset(BaseDataset):
n_windows_per_file,
step=1,
feature_columns=("lat", "lon", "alt", "ias"),
context_columns=("r","t"),
context_columns=(*[f"type_encoding_{i}" for i in range(4)], "last_lat", "last_lon", "last_alt", "last_ias"),
time_columns=("timestamp",),
target_columns=("lat", "lon", "alt"),
device="cpu",
@@ -232,7 +242,7 @@ def get_datasets(
n_windows_per_file: int,
step: int = 1,
feature_columns=("lat", "lon", "alt", "ias"),
context_columns=("r","t"),
context_columns=(*[f"type_encoding_{i}" for i in range(4)], "last_lat", "last_lon", "last_alt", "last_ias"),
time_columns=("timestamp",),
target_columns=("lat", "lon", "alt"),
device="cpu",
@@ -298,14 +308,3 @@ def get_datasets(
return train_dataset, val_dataset, test_dataset
class EncodingDataset(Dataset):
def __init__(self, X_a_1, X_a_2, X_b):
super().__init__()
self.X_a_1 = X_a_1
self.X_a_2 = X_a_2
self.X_b = X_b
def __len__(self):
return self.X_a_1.shape[0]
def __getitem__(self, idx):
return self.X_a_1[idx], self.X_a_2[idx], self.X_b[idx]
+49 -5
View File
@@ -1,6 +1,7 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
from aiRNN.preprocessors import denorm_coords
import math
@@ -18,10 +19,12 @@ class HaversineMSEAltitudeLoss(nn.Module):
lat/lon expected in degrees.
alt in meters (or same unit in pred/target).
"""
lat1 = torch.deg2rad(pred[..., 0])
lon1 = torch.deg2rad(pred[..., 1])
lat2 = torch.deg2rad(target[..., 0])
lon2 = torch.deg2rad(target[..., 1])
lat1, lon1, alt1 = denorm_coords(pred[...,0], pred[...,1], pred[...,2])
lat2, lon2, alt2 = denorm_coords(target[...,0], target[...,1], target[...,2])
lat1 = torch.deg2rad(lat1)
lon1 = torch.deg2rad(lon1)
lat2 = torch.deg2rad(lat2)
lon2 = torch.deg2rad(lon2)
dlat = lat2 - lat1
dlon = lon2 - lon1
@@ -38,11 +41,52 @@ class HaversineMSEAltitudeLoss(nn.Module):
haversine_mse = torch.mean(dist_km ** 2)
# Altitude penalty
delta_alt = pred[..., 2] - target[..., 2]
delta_alt = alt2 - alt1
alt_penalty = self.alt_const * torch.mean(delta_alt ** 2)
return haversine_mse + alt_penalty
class HaversineAltitudeLoss(nn.Module):
def __init__(self, alt_const=1.0, earth_radius_km=6371.0):
super().__init__()
self.alt_const = alt_const
self.R = earth_radius_km
def forward(self, pred, target):
"""
pred: (..., 3) -> [lat, lon, alt]
target: (..., 3) -> [lat, lon, alt]
lat/lon expected in degrees.
alt in meters (or same unit in pred/target).
"""
lat1, lon1, alt1 = denorm_coords(pred[...,0], pred[...,1], pred[...,2])
lat2, lon2, alt2 = denorm_coords(target[...,0], target[...,1], target[...,2])
lat1 = torch.deg2rad(lat1)
lon1 = torch.deg2rad(lon1)
lat2 = torch.deg2rad(lat2)
lon2 = torch.deg2rad(lon2)
dlat = lat2 - lat1
dlon = lon2 - lon1
# Haversine formula
a = (
torch.sin(dlat / 2) ** 2
+ torch.cos(lat1) * torch.cos(lat2) * torch.sin(dlon / 2) ** 2
)
c = 2 * torch.atan2(torch.sqrt(a), torch.sqrt(1 - a))
dist_km = self.R * c # great-circle distance
# Mean distance
haversine_loss = torch.mean(dist_km)
# Altitude penalty
delta_alt = alt2 - alt1
alt_penalty = self.alt_const * torch.mean(delta_alt ** 2)
return haversine_loss + alt_penalty
class WeightedMSELoss(nn.Module):
def __init__(self, weights):
super().__init__()
+62 -80
View File
@@ -3,98 +3,80 @@ import torch.nn as nn
class BaseRNN(nn.Module):
def __init__(self, time_in, feat_in, context_in, hidden_size, out_size, rnn_type="RNN"):
def __init__(self, time_in, feat_in, context_in, hidden_size,
rnn_size, out_size, rnn_type="RNN"):
super().__init__()
self.time_in = time_in
self.feat_in = feat_in
self.context_in = context_in
self.hidden_size = hidden_size
self.out_size = out_size
# Projecters to a common RNN input size
self.time_proj = nn.Linear(time_in, hidden_size)
self.feat_proj = nn.Linear(feat_in, hidden_size)
if context_in is not None:
self.context_proj = nn.Linear(context_in, hidden_size)
self.context_proj = (
nn.Linear(context_in, hidden_size) if context_in is not None else None
)
# RNN cell
if rnn_type == "RNN":
self.rnn = nn.RNNCell(input_size=hidden_size, hidden_size=hidden_size)
self.rnn = nn.RNN(
input_size=hidden_size,
hidden_size=rnn_size,
batch_first=True
)
elif rnn_type == "LSTM":
self.rnn = nn.LSTMCell(input_size=hidden_size, hidden_size=hidden_size)
self.rnn = nn.LSTM(
input_size=hidden_size,
hidden_size=rnn_size,
batch_first=True
)
else:
raise ValueError(f"Unsupported rnn_type: {rnn_type}")
raise ValueError("Unsupported rnn_type")
# Readout: predict from hidden state (optionally conditioned on time and context too)
readout_in_size = hidden_size + time_in
readout_in = rnn_size + time_in
if context_in is not None:
readout_in_size += context_in
self.readout = nn.Linear(readout_in_size, out_size)
readout_in += context_in
self.readout = nn.Linear(readout_in, out_size)
self.context_in = context_in
self.rnn_size = rnn_size
def forward(self, time_seq, feat_init=None, context=None,
init_steps=0, pred_steps=0, offset=0, hidden=None):
def forward(self, time_seq, feat_init=None, context=None, init_steps=0, pred_steps=0, offset=0, hidden=None):
b, total_len, _ = time_seq.shape
assert init_steps <= total_len
assert init_steps + offset <= total_len
# Project everything once
time_proj = self.time_proj(time_seq) # (b, T, hidden)
if context is not None:
assert self.context_proj is not None
ctx = self.context_proj(context).unsqueeze(1) # (b,1,hidden)
ctx = ctx.expand(-1, total_len, -1) # broadcast
else:
ctx = torch.zeros(b, total_len, time_proj.size(-1),
device=time_seq.device)
# Build the RNN input sequence
rnn_input = time_proj + ctx
if feat_init is not None:
assert feat_init.shape[0] == b and feat_init.shape[1] == init_steps
feat_proj = self.feat_proj(feat_init) # (b, init_steps, hidden)
# Insert feat input *only* in first init_steps
rnn_input[:, :init_steps, :] += feat_proj
if hidden is None:
hidden = torch.zeros(b, self.hidden_size, device=time_seq.device)
# Run the RNN over full sequence
outputs, hidden_next = self.rnn(rnn_input, hidden)
if self.context_in is not None:
assert context is not None and context.shape[0] == b
context_proj = self.context_proj(context) # (b, hidden_size)
else:
context_proj = torch.zeros(b, self.hidden_size, device=time_seq.device)
# Prediction window
pred_h = outputs[:, init_steps:init_steps + pred_steps] # (b, pred, rnn_size)
# --- Initialization phase: feed time + feature in parallel for init_steps ---
for t in range(init_steps + offset):
t_in = time_seq[:, t, :] # (b, time_in)
t_proj = self.time_proj(t_in) # (b, hidden_size)
# Build readout input
time_raw = time_seq[:, init_steps:init_steps + pred_steps]
rnn_in = t_proj + context_proj # start with time + context
if feat_init is not None and t < init_steps:
f_in = feat_init[:, t, :] # (b, feat_in)
f_proj = self.feat_proj(f_in) # (b, hidden_size)
rnn_in = rnn_in + f_proj # combine with feature input
read_list = [pred_h, time_raw]
if context is not None:
read_list.append(context.unsqueeze(1).expand(-1, pred_steps, -1))
read_in = torch.cat(read_list, dim=-1)
if isinstance(self.rnn, nn.LSTMCell):
if isinstance(hidden, tuple):
h_t, c_t = hidden
else:
h_t = hidden
c_t = torch.zeros(b, self.hidden_size, device=time_seq.device)
h_t, c_t = self.rnn(rnn_in, (h_t, c_t))
hidden = (h_t, c_t)
else:
hidden = self.rnn(rnn_in, hidden)
preds = self.readout(read_in)
return preds, hidden_next
# --- Prediction phase: feed only time (+ context) inputs for pred_steps ---
preds = []
for p in range(pred_steps):
t_idx = init_steps + p
assert t_idx < total_len, "time_seq too short for requested pred_steps"
t_in = time_seq[:, t_idx, :] # (b, time_in)
t_proj = self.time_proj(t_in) # project time input
rnn_in = t_proj + context_proj # RNN input: time + context
if isinstance(self.rnn, nn.LSTMCell):
h_t, c_t = hidden
h_t, c_t = self.rnn(rnn_in, (h_t, c_t)) # update hidden
hidden = (h_t, c_t)
else:
hidden = self.rnn(rnn_in, hidden) # update hidden
# Readout uses hidden + raw time (+ raw context input)
read_inputs = [hidden if not isinstance(hidden, tuple) else hidden[0], t_in]
if self.context_in is not None:
read_inputs.append(context)
read = torch.cat(read_inputs, dim=-1)
out = self.readout(read) # (b, out_size)
preds.append(out.unsqueeze(1))
preds = torch.cat(preds, dim=1) # (b, pred_steps, out_size)
return preds, hidden
@@ -107,8 +89,8 @@ class TwoInputRNN(BaseRNN):
After init_steps, only time inputs are provided and the model predicts a sequence.
Predictions can be compared to targets with a specified `offset`.
"""
def __init__(self, time_in, feat_in, hidden_size, out_size):
super().__init__(time_in, feat_in, context_in=None, hidden_size=hidden_size, out_size=out_size)
def __init__(self, time_in, feat_in, hidden_size, rnn_size, out_size):
super().__init__(time_in, feat_in, context_in=None, hidden_size=hidden_size, rnn_size=rnn_size, out_size=out_size)
class ThreeInputRNN(BaseRNN):
"""
@@ -119,8 +101,8 @@ class ThreeInputRNN(BaseRNN):
After init_steps, only time and context inputs are provided and the model predicts a sequence.
Predictions can be compared to targets with a specified `offset`.
"""
def __init__(self, time_in, feat_in, context_in, hidden_size, out_size):
super().__init__(time_in, feat_in, context_in=context_in, hidden_size=hidden_size, out_size=out_size)
def __init__(self, time_in, feat_in, context_in, hidden_size, rnn_size, out_size):
super().__init__(time_in, feat_in, context_in=context_in, hidden_size=hidden_size, rnn_size=rnn_size, out_size=out_size)
class TwoInputLSTM(BaseRNN):
"""
@@ -130,8 +112,8 @@ class TwoInputLSTM(BaseRNN):
After init_steps, only time inputs are provided and the model predicts a sequence.
Predictions can be compared to targets with a specified `offset`.
"""
def __init__(self, time_in, feat_in, hidden_size, out_size):
super().__init__(time_in, feat_in, context_in=None, hidden_size=hidden_size, out_size=out_size, rnn_type="LSTM")
def __init__(self, time_in, feat_in, hidden_size, rnn_size, out_size):
super().__init__(time_in, feat_in, context_in=None, hidden_size=hidden_size, rnn_size=rnn_size, out_size=out_size, rnn_type="LSTM")
class ThreeInputLSTM(BaseRNN):
"""
@@ -142,5 +124,5 @@ class ThreeInputLSTM(BaseRNN):
After init_steps, only time and context inputs are provided and the model predicts a sequence.
Predictions can be compared to targets with a specified `offset`.
"""
def __init__(self, time_in, feat_in, context_in, hidden_size, out_size):
super().__init__(time_in, feat_in, context_in=context_in, hidden_size=hidden_size, out_size=out_size, rnn_type="LSTM")
def __init__(self, time_in, feat_in, context_in, hidden_size, rnn_size, out_size):
super().__init__(time_in, feat_in, context_in=context_in, hidden_size=hidden_size, rnn_size=rnn_size, out_size=out_size, rnn_type="LSTM")
+22 -2
View File
@@ -2,7 +2,8 @@ import torch
from torch import nn
import pathlib
import json
from aiRNN.dataloader import EncodingDataset
import numpy as np
from torch.utils.data import Dataset
MIN_LAT, MAX_LAT = 57.0, 72.0
@@ -36,13 +37,32 @@ def denorm_ias(ias_norm):
return min_max_denormalize(ias_norm, MIN_IAS, MAX_IAS)
def norm_time(timestamp_series):
invalid_mask = (timestamp_series < 1672527600) | np.isnan(timestamp_series) # Jan 1, 2023
if sum(invalid_mask) == len(timestamp_series):
raise ValueError("All timestamps are invalid or NaN.")
timestamp_series[invalid_mask] = timestamp_series[~invalid_mask].mean()
min_time = timestamp_series.min()
return (timestamp_series - min_time) * TIME_NORM_FACTOR
def fillna_with_mean(series):
def fillna_with_mean(series, allow_nan_mean=True):
mean_value = series.mean()
if np.isnan(mean_value):
if not allow_nan_mean:
raise ValueError("Mean is NaN and allow_nan_mean is False.")
mean_value = 0.0
return series.fillna(mean_value)
class EncodingDataset(Dataset):
def __init__(self, X_a_1, X_a_2, X_b):
super().__init__()
self.X_a_1 = X_a_1
self.X_a_2 = X_a_2
self.X_b = X_b
def __len__(self):
return self.X_a_1.shape[0]
def __getitem__(self, idx):
return self.X_a_1[idx], self.X_a_2[idx], self.X_b[idx]
class AutoEncoder(nn.Module):
def __init__(self, input_a_1_size, input_a_2_size, input_b_size, latent_size):
super(AutoEncoder, self).__init__()