Files
aiRtrafficNN/Code/python/src/aiRNN/dataloader.py
T
2025-11-21 09:24:14 +01:00

357 lines
13 KiB
Python

import pandas as pd
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import numpy as np
import pathlib
from typing import Sequence
import random
from aiRNN import preprocessors
class BaseDataset(Dataset):
def __init__(
self,
filepaths,
n_input,
n_output,
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"),
time_columns=("timestamp",),
target_columns=("lat", "lon", "alt"),
device="cpu",
):
super().__init__()
self.filepaths = [pathlib.Path(fp) for fp in filepaths]
self.n_input = n_input
self.n_output = n_output
self.window_size = n_input + n_output
self.device = device
self.n_windows_per_file = n_windows_per_file
self.step = step
# 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)
self.target_cols = list(target_columns)
# Total samples = windows_per_file * number_of_files
self.total_windows = n_windows_per_file * len(self.filepaths)
for col_list in [self.feature_cols, self.context_cols, self.time_cols, self.target_cols]:
assert all(
col in sample.columns for col in col_list
), "Some specified columns not found in data."
def __len__(self):
return self.total_windows
def _get_start_end_indices(self, idx, n_rows):
# Max valid start index so window stays inside the file
max_start = n_rows - self.window_size * self.step
if max_start < 0:
raise ValueError("File too small for one window.")
# Evenly spaced index:
# raw_start = (n_rows / n_windows_per_file) * local_idx
# mapped into integer space
raw_start = (n_rows / self.n_windows_per_file) * idx
start_idx = int(raw_start // self.step) * self.step
# Clamp to safe zone
if start_idx > max_start:
start_idx = max_start
end_idx = start_idx + self.window_size * self.step
return start_idx, end_idx
def _modify_df(self, df):
"""Hook for subclasses to modify dataframe before slicing windows."""
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"]:
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)
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", "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
def save_entire_dataset(self, filepath):
"""Utility to save the entire dataset to a single pth file."""
X_feat_list = []
X_time_list = []
Y_out_list = []
X_context_list = []
for i in range(len(self)):
X_f, X_t, Y, X_c = self[i]
X_feat_list.append(X_f.cpu())
X_time_list.append(X_t.cpu())
Y_out_list.append(Y.cpu())
if X_c is not None:
X_context_list.append(X_c.cpu())
data_dict = {
"X_feat": torch.stack(X_feat_list),
"X_time": torch.stack(X_time_list),
"Y_out": torch.stack(Y_out_list),
}
if len(X_context_list) > 0:
data_dict["X_context"] = torch.stack(X_context_list)
torch.save(data_dict, filepath)
class SaveDataset(Dataset):
def __init__(self, data_dict, device="cpu"):
super().__init__()
self.X_feat = data_dict["X_feat"].to(device)
self.X_time = data_dict["X_time"].to(device)
self.Y_out = data_dict["Y_out"].to(device)
self.device = device
if "X_context" in data_dict:
self.X_context = data_dict["X_context"].to(device)
else:
self.X_context = None
def __len__(self):
return self.X_feat.shape[0]
def __getitem__(self, idx):
X_f = self.X_feat[idx]
X_t = self.X_time[idx]
Y = self.Y_out[idx]
if self.X_context is not None:
X_c = self.X_context[idx]
else:
X_c = None
return X_f, X_t, Y, X_c
class EvenlySpacedDataset(BaseDataset):
def __init__(
self,
filepaths,
n_input,
n_output,
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"),
time_columns=("timestamp",),
target_columns=("lat", "lon", "alt"),
device="cpu",
):
super().__init__(
filepaths=filepaths,
n_input=n_input,
n_output=n_output,
feature_columns=feature_columns,
context_columns=context_columns,
time_columns=time_columns,
target_columns=target_columns,
device=device,
step=step,
n_windows_per_file=n_windows_per_file,
)
# Preload all data into memory
self.data = []
for fp in self.filepaths:
df = pd.read_csv(fp, dtype={"icao": str, "r": str, "t": str, "timestamp": float, "lat": float, "lon": float, "alt": float, "ias": float})
n_rows = len(df)
# Extract windows
for w in range(n_windows_per_file):
try:
start_idx, end_idx = self._get_start_end_indices(w, n_rows)
window = df.iloc[start_idx:end_idx:self.step]
window = self._modify_df(window)
self.data.append(window)
except ValueError:
self.total_windows -= 1
continue
def __len__(self):
return self.total_windows
def __getitem__(self, idx):
window = self.data[idx]
X_feat = window.iloc[: self.n_input][self.feature_cols].to_numpy(np.float32)
if len(self.context_cols) > 0:
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)
Y_out = window.iloc[self.n_input :][self.target_cols].to_numpy(np.float32)
return (
torch.tensor(X_feat, device=self.device),
torch.tensor(X_time, device=self.device),
torch.tensor(Y_out, device=self.device),
torch.tensor(X_context, device=self.device) if X_context is not None else None,
)
class EvenlySpacedStreamingDataset(BaseDataset):
def __init__(
self,
filepaths,
n_input,
n_output,
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"),
time_columns=("timestamp",),
target_columns=("lat", "lon", "alt"),
device="cpu",
):
super().__init__(
filepaths=filepaths,
n_input=n_input,
n_output=n_output,
feature_columns=feature_columns,
context_columns=context_columns,
time_columns=time_columns,
target_columns=target_columns,
device=device,
step=step,
n_windows_per_file=n_windows_per_file,
)
# Precompute row counts for each file
self.row_counts = [self._count_rows(fp) for fp in self.filepaths]
def _count_rows(self, fp):
with open(fp, "r") as f:
return sum(1 for _ in f) - 1 # minus header
def __len__(self):
return self.total_windows
def __getitem__(self, idx):
# Figure out which file we belong to
file_idx = idx // self.n_windows_per_file
local_idx = idx % self.n_windows_per_file
fp = self.filepaths[file_idx]
n_rows = self.row_counts[file_idx]
start_idx, end_idx = self._get_start_end_indices(local_idx, n_rows)
# Read only the required rows
skip = list(set(range(1, start_idx + 1)))
df = pd.read_csv(fp, skiprows=skip, nrows=self.window_size * self.step, dtype={"icao": str, "r": str, "t": str, "timestamp": float, "lat": float, "lon": float, "alt": float, "ias": float}).iloc[::self.step]
df = self._modify_df(df)
# Slice into input / decoder-input / targets
X_feat = df.iloc[: self.n_input][self.feature_cols].to_numpy(np.float32)
if len(self.context_cols) > 0:
X_context = df.iloc[: self.n_input][self.context_cols].to_numpy(np.float32)
else:
X_context = None
X_time = df.iloc[: self.window_size][self.time_cols].to_numpy(np.float32)
Y_out = df.iloc[self.n_input :][self.target_cols].to_numpy(np.float32)
return (
torch.tensor(X_feat, device=self.device),
torch.tensor(X_time, device=self.device),
torch.tensor(Y_out, device=self.device),
torch.tensor(X_context, device=self.device) if X_context is not None else None,
)
def get_datasets(
files: Sequence[str],
n_input: int,
n_output: int,
n_windows_per_file: int,
step: int = 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"),
time_columns=("timestamp",),
target_columns=("lat", "lon", "alt"),
device="cpu",
test_split: float = 0.2,
val_split: float = 0.1,
seed: int = 42,
streaming: bool = False,
) -> tuple[Dataset, Dataset, Dataset]:
"""Utility to create train/val/test datasets from file list."""
random.seed(seed)
files = list(files)
random.shuffle(files)
n_total = len(files)
n_test = int(n_total * test_split)
n_val = int(n_total * val_split)
test_files = files[:n_test]
val_files = files[n_test : n_test + n_val]
train_files = files[n_test + n_val :]
if not streaming:
ds = EvenlySpacedDataset
else:
ds = EvenlySpacedStreamingDataset
train_dataset = ds(
train_files,
n_input=n_input,
n_output=n_output,
n_windows_per_file=n_windows_per_file,
step=step,
feature_columns=feature_columns,
context_columns=context_columns,
time_columns=time_columns,
target_columns=target_columns,
device=device,
)
val_dataset = ds(
val_files,
n_input=n_input,
n_output=n_output,
n_windows_per_file=n_windows_per_file,
step=step,
feature_columns=feature_columns,
context_columns=context_columns,
time_columns=time_columns,
target_columns=target_columns,
device=device,
)
test_dataset = ds(
test_files,
n_input=n_input,
n_output=n_output,
n_windows_per_file=n_windows_per_file,
step=step,
feature_columns=feature_columns,
context_columns=context_columns,
time_columns=time_columns,
target_columns=target_columns,
device=device,
)
return train_dataset, val_dataset, test_dataset