deepSSF Package Walkthrough

Author
Affiliation

Queensland University of Technology, University of Tasmania

Published

October 23, 2026

Abstract

Here we show an example of how to use the functions in the deepSSF package, showing data loading, model training and testing and generating simulations. This walkthrough is relevant for deepSSF v0.3.1.

To install and use the package, visit the GitHub repository here: https://github.com/swforrest/deepSSF_package, which has instructions in the README. If you install the package via PyPI using pip install deepssf, this script will not download, but it can be accessed from the deepSSF_package repo here: https://github.com/swforrest/deepSSF_package/tree/main/examples, under the ‘examples’ folder. You can then download it directly, or clone/fork the repository to download the package locally.

Note, the GitHub repository that is associated with this website https://github.com/swforrest/deepSSF, is NOT the repository for the package, which is https://github.com/swforrest/deepSSF_package. The website illustrates how deepSSF works, and was created to be associated with the deepSSF paper in Methods in Ecology and Evolution, but the package has been created as a separate repository.

We use data that comes with the package in the datasets folder, which is the GPS tracking data of a single water buffalo (~10,000 locations), and two spatial layers: NDVI (the average from 2018-2019, which covers the temporal extent of the data), and slope.

Code
import math
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import rasterio
import rasterio.plot  # submodule is not imported by `import rasterio` alone
import torch
import folium
import time
import networkx as nx

from pathlib import Path
from IPython.display import Image  # For displaying the training GIF
from rasterio.transform import Affine
from matplotlib.colors import LogNorm

# Use this to load the functions used in this notebook, and call them with `deepssf.function_name()`
import deepssf

print(f"deepssf {deepssf.__version__}")
print(f"torch {torch.__version__}")
deepssf 0.3.1
torch 2.12.0

Setup the script

Set random seed and the device for training the model

If you have a GPU, this is where it will be specified (automatically).

Code
# Reproducibility — fix random seeds so results are the same on every run.
torch.manual_seed(42)
np.random.seed(42)

# DEVICE is capitalised — module-level constant (set once, never reassigned).
# get_device() returns 'cuda' if a GPU is available, 'mps' on Apple Silicon,
# or 'cpu' otherwise.  All tensors and the model are moved to this device.
DEVICE = deepssf.get_device()
print(f"Device: {DEVICE}")
Device: mps

Set script paths

If you have cloned/forked/downloaded the GitHub repo (https://github.com/swforrest/deepSSF_package) you shouldn’t need to change these, but for your own analyses you will want to set these to point to where your data is and where you want outputs saved.

All-caps names below are module-level constants — set once here and referenced throughout the notebook without being reassigned. Python has no built-in constant keyword, so capitalisation is the community convention (PEP 8) to signal “do not change this value”.

Code
DATA_DIR = Path("../src/deepssf/datasets/data")  # location of the bundled sample data
CSV_PATH = DATA_DIR / "buffalo_djelk_id2005.csv"  # GPS tracking data, individual 2005
LAYER_PATHS = {
    "ndvi":  str(DATA_DIR / "ndvi_2005.tif"),   # Normalised Difference Vegetation Index
    "slope": str(DATA_DIR / "slope_2005.tif"),  # terrain slope derived from a DEM
}
OUTPUT_DIR   = Path("outputs")                   # figures and model checkpoints go here
SNAPSHOT_DIR = OUTPUT_DIR / "training_snapshots"  # per-batch prediction-map snapshots
OUTPUT_DIR.mkdir(exist_ok=True)

Import data

The csv file is imported with a timestamp and an x and y coordinate. In our case the data was already in a projected format that is specific to Australia (EPSG:3112), which is the same crs as the raster data which will be imported.

Code
raw_df = pd.read_csv(CSV_PATH)
print(f"Raw GPS fixes : {len(raw_df):,}")
print(f"Individuals   : {raw_df['id'].nunique()}")

# Sort by the individual ID and timestamp to ensure correct temporal order
raw_df = raw_df.sort_values(by=["id", "time"]).reset_index(drop=True)

# # # Convert the 'time' column to the correct timezone (here Australia/Darwin) 
# raw_df["time"] = (
#         pd.to_datetime(raw_df["time"], utc=True)
#         .dt.tz_convert("Australia/Darwin")
#     )

raw_df.head()
Raw GPS fixes : 10,297
Individuals   : 1
id time x y
0 2005 2018-07-25T00:04:02Z 41941.331695 -1.435875e+06
1 2005 2018-07-25T01:04:23Z 41969.310875 -1.435671e+06
2 2005 2018-07-25T02:04:39Z 41921.521939 -1.435654e+06
3 2005 2018-07-25T03:04:17Z 41779.439594 -1.435601e+06
4 2005 2018-07-25T04:04:39Z 41841.203272 -1.435635e+06

load_environmental_layers()

Reads each GeoTIFF listed in LAYER_PATHS, min–max

  • Scales every band to [0, 1], and returns:
    • env_layers — dict mapping layer name → 2-D NumPy array (H × W)
    • raster_transform — Affine object that converts pixel (row, col) indices to the projected coordinates (easting, northing) of the raster(s), which should be in the same CRS as the GPS data. This transform is used whenever we need to translate between pixel space and real-world coordinates.
Code
env_layers, raster_transform = deepssf.load_environmental_layers(LAYER_PATHS)
print("Layers loaded:", list(env_layers.keys()))
for name, arr in env_layers.items():
    print(f"  {name}: shape={arr.shape}, min={arr.min():.3f}, max={arr.max():.3f}")

# Each band of a multi-band layer becomes one CNN input channel, so count bands
# (not layers) here and reuse the total when setting ModelParams below.  Deriving
# it means switching rasters in LAYER_PATHS needs no other edits.
N_SPATIAL_CHANNELS = sum(1 if arr.ndim == 2 else arr.shape[0] for arr in env_layers.values())
print(f"Spatial channels: {N_SPATIAL_CHANNELS}")
Layer 'ndvi': min=-0.06614641100168228, max=0.731696367263794 → scaled to [0, 1]
Layer 'slope': min=0.00013558653881773353, max=12.298083305358887 → scaled to [0, 1]
Layers loaded: ['ndvi', 'slope']
  ndvi: shape=(758, 1175), min=0.000, max=1.000
  slope: shape=(758, 1175), min=0.000, max=1.000
Spatial channels: 2

Plot each spatial covariate with the observed GPS locations overlaid.

This confirms the raster extent covers the extent of the tracking data and gives a sense of the landscape the model will learn from.

Code
fig, axes = plt.subplots(1, 2, figsize=(13, 5))

layer_plot = [
    ("ndvi",  "NDVI (scaled 0–1)",  "YlGn"),
    ("slope", "Slope (scaled 0–1)", "viridis"),
]

for ax, (name, label, cmap) in zip(axes, layer_plot, strict=False):
    rasterio.plot.show(env_layers[name], transform=raster_transform, ax=ax, cmap=cmap)
    ax.plot(
        raw_df["x"], raw_df["y"],
        "r-o", alpha=0.5, markersize=1, linewidth=0.8, label="GPS trajectory",
    )
    ax.set_xlabel("Easting (m, projected)")
    ax.set_ylabel("Northing (m, projected)")
    ax.set_title(label)
    ax.legend(loc="upper right", markerscale=6)

plt.suptitle("Spatial covariates with observed GPS locations", y=1.01)
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR / "spatial_covariates.png"), dpi=100)
plt.show()

prepare_movement_df()

Converts a fixes dataframe (one row per GPS location) into a step dataframe (one row per movement between consecutive fixes).

  • For each step it computes:
    • dx, dy — displacement in projected CRS units (metres)
    • bearing — direction of travel (radians; 0 = north, clockwise positive). These entries will also be used to calculate the bearing of the previous step, used as a model input so the network can capture directional persistence by estimating a turning angle
    • dt_hour — time elapsed between consecutive fixes (hours)
    • hour_t1, yday_t1 — decimal hour-of-day and day-of-year at the step start location
    • *_sin1 / *_cos1 — sine/cosine encodings of hour and yday (see SCALAR_COLS below)

The trailing underscore on column names (x1_, y1_, t1_, etc.) flags them as “start of step” variables; x2_, y2_, t2_ are the corresponding “end of step” values, following the convention of the amt package (Signer et al. 2019). The last fix for each individual is dropped because no forward step can be formed.

Code
# The CSV is already in amt step format, but prepare_movement_df() expects one row
# per *fix* and rebuilds the steps itself (it also derives bearing, dt_hour and the
# cyclic time encodings the model needs).  Point it at the step-start columns and
# let it re-form the steps from consecutive fixes.
step_df = deepssf.prepare_movement_df(
    raw_df, 
    id_col="id", 
    time_col="time", 
    x_col="x", 
    y_col="y",
    tz="Australia/Darwin"
)

n_dropped = len(raw_df) - len(step_df)
print(f"Movement steps : {len(step_df):,}  ({n_dropped} dropped — last per individual)")
step_df.head()
Movement steps : 10,296  (1 dropped — last per individual)
id t1_ x1_ y1_ t2_ x2_ y2_ dx dy sl_ bearing bearing_tm1 ta_ dt_hour hour_t1 yday_t1 hour_t1_sin1 hour_t1_cos1 yday_t1_sin1 yday_t1_cos1
0 2005 2018-07-25 09:34:02+09:30 41941.331695 -1.435875e+06 2018-07-25 10:34:23+09:30 41969.310875 -1.435671e+06 27.979180 204.064135 205.973313 1.434536 0.000000 NaN 1.005833 9.566667 206.0 0.594823 -0.803857 -0.391358 -0.920239
1 2005 2018-07-25 10:34:23+09:30 41969.310875 -1.435671e+06 2018-07-25 11:34:39+09:30 41921.521939 -1.435654e+06 -47.788936 16.857110 50.674891 2.802478 1.434536 1.367942 1.004444 10.566667 206.0 0.366501 -0.930418 -0.391358 -0.920239
2 2005 2018-07-25 11:34:39+09:30 41921.521939 -1.435654e+06 2018-07-25 12:34:17+09:30 41779.439594 -1.435601e+06 -142.082345 53.568427 151.845214 2.781049 2.802478 -0.021429 0.993889 11.566667 206.0 0.113203 -0.993572 -0.391358 -0.920239
3 2005 2018-07-25 12:34:17+09:30 41779.439594 -1.435601e+06 2018-07-25 13:34:39+09:30 41841.203272 -1.435635e+06 61.763677 -34.322938 70.659861 -0.507220 2.781049 2.994917 1.006111 12.566667 206.0 -0.147809 -0.989016 -0.391358 -0.920239
4 2005 2018-07-25 13:34:39+09:30 41841.203272 -1.435635e+06 2018-07-25 14:34:27+09:30 41655.463332 -1.435604e+06 -185.739939 31.003534 188.309703 2.976198 -0.507220 -2.799767 0.996667 13.566667 206.0 -0.398749 -0.917060 -0.391358 -0.920239

Check the timezone

Code
step_df["t1_"].dt.tz
zoneinfo.ZoneInfo(key='Australia/Darwin')

Visualise some data summaries

Partly this is diagnostic, to check whether the time is in the correct timezone (we know that buffalo have high movement periods around dawn and dusk, so we can see whether that aligns). We can also check these summaries against summaries of the simulated trajectories to see whether we’ve captured these behaviours.

Distribution of step lengths

Code
# Distribution of step lengths, with the median and mean marked
fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(step_df["sl_"], bins=250, color="tab:blue", alpha=0.7)
ax.axvline(step_df["sl_"].median(), color="black", lw=2, label="Median")
ax.axvline(step_df["sl_"].mean(), color="black", lw=2, ls="--", label="Mean")
ax.set_xlabel("Step length (m)")
ax.set_ylabel("Count")
ax.set_xlim(0, 2000)
ax.legend()
plt.tight_layout()
plt.show()

Log of step lengths

Code
# Distribution of log step lengths, with the median and mean marked
fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(np.log(step_df["sl_"]), bins=250, color="tab:blue", alpha=0.7)
ax.axvline(np.log(step_df["sl_"].median()), color="black", lw=2, label="Median")
ax.axvline(np.log(step_df["sl_"].mean()), color="black", lw=2, ls="--", label="Mean")
ax.set_xlabel("Log step length (m)")
ax.set_ylabel("Count")
ax.legend()
plt.tight_layout()
plt.show()

Step lengths through time

Code
# Step length by hour of day, with the hourly median and mean overlaid
hourly_sl_median = step_df.groupby(step_df["hour_t1"].round().mod(24))["sl_"].median()
hourly_sl_mean = step_df.groupby(step_df["hour_t1"].round().mod(24))["sl_"].mean()

fig, ax = plt.subplots(figsize=(8, 4))
ax.scatter(step_df["hour_t1"], step_df["sl_"], s=4, alpha=0.15, color="tab:blue", label="Steps")
ax.plot(hourly_sl_mean.index, hourly_sl_mean.values, color="red", lw=2, marker="s", ms=4, label="Hourly mean")
ax.plot(hourly_sl_median.index, hourly_sl_median.values, color="black", lw=2, marker="o", ms=4, label="Hourly median")
ax.set_xlabel("Hour of day")
ax.set_ylabel("Step length (m)")
ax.set_xticks(range(0, 25, 3))
ax.set_xlim(0, 24)
ax.set_ylim(0, 1000)
ax.legend()
plt.tight_layout()
plt.show()

Distribution of turning angles

Code
# Distribution of turning angles (NaN for each individual's first step, so dropped)
fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(step_df["ta_"].dropna(), bins=72, range=(-np.pi, np.pi), color="tab:orange", alpha=0.7)
ax.set_xlabel("Turning angle (radians)")
ax.set_ylabel("Count")
ax.set_xticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])
ax.set_xticklabels(["-π", "-π/2", "0", "π/2", "π"])
ax.set_xlim(-np.pi, np.pi)
plt.tight_layout()
plt.show()

Turning angles through time

As turning angles are circular, the mean/median sit near 0 regardless of spread. Instead, we summarise each hour with the circular SD and the shortest arc containing 50% of the angles (which may wrap across ±π), i.e., where are the densest 50% of the turning angles concentrated?

From this plot, we can see that movements are most directed (mean at 0 and most TAs near 0) around dawn and dusk, reflecting the longer steps in those periods. This shows that buffalo appear to be travelling in a directed way during those periods.

Code
# create a helper function to summarise turning angles by hour of day 
# into circular SD and the shortest arc containing 50% of the angles
def circ_summary(theta, prob=0.5):
    """Circular SD and the shortest arc containing `prob` of the angles."""
    theta = np.sort(np.asarray(theta))
    n = len(theta)
    circ_sd = np.sqrt(-2 * np.log(np.abs(np.mean(np.exp(1j * theta)))))
    k = int(np.ceil(prob * n))
    ext = np.concatenate([theta, theta + 2 * np.pi])  # unwrap so arcs can cross ±π
    widths = ext[k - 1 : k - 1 + n] - ext[:n]
    i = np.argmin(widths)
    return pd.Series({"circ_sd": circ_sd, "arc_start": ext[i], "arc_end": ext[i + k - 1]})

ta_df = step_df.dropna(subset=["ta_"])
hourly_ta = ta_df.groupby(ta_df["hour_t1"].round().mod(24))["ta_"].apply(circ_summary).unstack()

fig, (ax, ax_sd) = plt.subplots(2, 1, figsize=(8, 6), sharex=True, height_ratios=[3, 1])
ax.scatter(ta_df["hour_t1"], ta_df["ta_"], s=4, alpha=0.15, color="tab:orange", label="Steps")
for hour, row in hourly_ta.iterrows():
    # draw the arc as one segment, or two if it wraps past π
    segments = [(row.arc_start, min(row.arc_end, np.pi))]
    if row.arc_end > np.pi:
        segments.append((-np.pi, row.arc_end - 2 * np.pi))
    for lo, hi in segments:
        ax.vlines(hour, lo, hi, color="black", lw=3, label="Densest 50%" if hour == hourly_ta.index[0] and lo == row.arc_start else None)
ax.set_ylabel("Turning angle (radians)")
ax.set_yticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])
ax.set_yticklabels(["-π", "-π/2", "0", "π/2", "π"])
ax.set_ylim(-np.pi, np.pi)
ax.legend(loc="upper right")

ax_sd.plot(hourly_ta.index, hourly_ta["circ_sd"], color="black", lw=2, marker="o", ms=4)
ax_sd.set_ylabel("Circular SD")
ax_sd.set_xlabel("Hour of day")
ax_sd.set_xticks(range(0, 25, 3))
ax_sd.set_xlim(0, 24)
plt.tight_layout()
plt.show()

SCALAR_COLS

These are non-spatial inputs. For processing with convolutional layers, they are converted into spatial windows (rasters/arrays), and stacked with the truly spatial covariates.

The model receives a fixed-size spatial window centred on each step’s start location. To let the network express time-varying habitat preferences (e.g. different nocturnal vs diurnal use), scalar covariates are tiled into additional channels of the same H × W size, so every pixel “knows” the current time context when habitat suitability is estimated.

  • hour_t1_sin1 / hour_t1_cos1
    • Sine and cosine of (2π × hour / 24), encoding time-of-day as a circular variable. The circular encoding means 23:00 and 01:00 are treated as adjacent — a plain hour value would have an artificial discontinuity at midnight.
  • yday_t1_sin1 / yday_t1_cos1
    • Same circular encoding for day-of-year (1–365), capturing seasonal variation in habitat use (e.g. dry-season vs wet-season movements).
  • dt_hour
    • Elapsed time between consecutive fixes (hours). Because GPS collars do not always fix at a perfectly regular rate, including dt_hour lets the movement kernel rescale expected displacement proportionally to the actual time interval rather than assuming a fixed fix rate. Note: dt_hour must be supplied at simulation time as well, so that simulate_trajectory() produces inputs consistent with training.
Code
SCALAR_COLS = ["hour_t1_sin1",
               "hour_t1_cos1", 
               "yday_t1_sin1", 
               "yday_t1_cos1", 
               "dt_hour"]

PIXEL_SIZE

Raster resolution in CRS units (metres here). Read straight off raster_transform rather than typed in: the transform is the authority on the grid the model is trained against, and a hand-entered value that disagrees with it silently mis-scales the movement kernel — which is parameterised in metres — against a grid measured in pixels.

Code
# transform.a is the cell width, transform.e the (negative) cell height
PIXEL_SIZE = abs(raster_transform.a)

if not math.isclose(abs(raster_transform.a), abs(raster_transform.e)):
    raise ValueError(
        f"Non-square pixels ({abs(raster_transform.a)} x {abs(raster_transform.e)}); "
        "the model assumes square cells."
    )

print(f"Pixel size: {PIXEL_SIZE} m") 
Pixel size: 25.0 m

WINDOW_SIZE

The size of the local availability window in cells/pixels. This is what the model will crop out during training to define the local availability window.

The maximum step length that the model will allow is the distance between the centre cell and the nearest edge (in reality some steps may be longer if they are in the corners).

Code
WINDOW_SIZE = 101  # odd number of pixels so there is a central pixel

print(f"Pixel size: {PIXEL_SIZE} m  →  window {WINDOW_SIZE} px = "
      f"{WINDOW_SIZE * PIXEL_SIZE:,.0f} m across")

print(f"Maximum possible step: {((WINDOW_SIZE - 1) / 2) * PIXEL_SIZE:,.0f} m")
Pixel size: 25.0 m  →  window 101 px = 2,525 m across
Maximum possible step: 1,250 m

BATCH_SIZE

Number of training samples (movement steps) per training mini-batch. From what I’ve found training seems to be fairly robust this (doesn’t seem to change performace much), but I have not rigorously tested this.

Code
BATCH_SIZE  = 32

N_CONV_LAYERS_MOVE

To set the number of convolutional layers in the movement subnetwork of the model.

Each of the convolution layers has a max pooling layer, so more layers will result in greater dimension reduction that will feed into the fully-connected layer that outputs the movement parameters.

Code
N_CONV_LAYERS_MOVE = 3

N_CONV_LAYERS_HAB

To set the number of convolutional layers in the habitat selection subnetwork of the model.

These convolution layers do not have max pooling layers, so there is no dimension reduction. More layers will increase the depth of the model and allow for more abstracted features to be identified, but will slow model training/prediction.

This count includes the final convolution layer, which is the one that collapses the feature maps down to the single-channel habitat selection surface, so the minimum is 1.

Code
N_CONV_LAYERS_HAB = 4

OUTPUT_CHANNELS

Number of convolutional filters (and therefore feature maps) for each convolution layer, for both the movement and habitat selection convolution layers. More convolutional filters can provide more flexible functions, but will increase the number of parameters and slow down training times.

Code
OUTPUT_CHANNELS = 8

Other convolution layer inputs

These are pretty typical values, so I haven’t investigated how these might change the performance of the model.

KERNEL_SIZE

  • Defines the size (in number of pixles) of the convolutional kernels. Larger convolutional kernels have a greater pixel-neighbourhood awareness. ### KERNEL_SIZE_MP
  • Defines kernel size for max-pooling (if kernel size = stride, a kernel size of 2 will halve the dimensions on each side, resulting in 1/4 of the number of cells) ### STRIDE_MP
  • Defines max-pooling stride size
Code
KERNEL_SIZE     = 3     # conv kernel spatial size
KERNEL_SIZE_MP  = 2     # max-pool kernel size
STRIDE_MP       = 2     # max-pool stride 

Pre-calculate the flattened CNN output size

This value will be the input to the fully-connected layers in the movement subnetwork, and is calculated here so that ModelParams can be set without needing to hard-code it.

flattened_conv_dim() reproduces exactly what the layers inside ConvJointModel do to the spatial dimensions: * conv (padding=1) keeps the spatial dimension unchanged at each layer * maxpool k=2, s=2 halves the spatial dimension (floor division) at each layer

Because every movement conv layer is paired with a max pooling layer, this size depends on N_CONV_LAYERS_MOVE — so it has to be recalculated whenever that (or WINDOW_SIZE, or OUTPUT_CHANNELS) changes. It raises an error if the window is pooled away to nothing, which means too many layers for the window size.

Code
DENSE_DIM = deepssf.flattened_conv_dim(
    image_dim=WINDOW_SIZE,
    n_conv_layers_move=N_CONV_LAYERS_MOVE,
    output_channels=OUTPUT_CHANNELS,
)
print(f"After {N_CONV_LAYERS_MOVE}x conv+maxpool: dense_dim_in_all={DENSE_DIM}")
After 3x conv+maxpool: dense_dim_in_all=1152

DENSE_DIM_HIDDEN

How many weights (‘neurons’) should there be for the hidden layers in the movement kernel?

Code
DENSE_DIM_HIDDEN = 64 # hidden units in the scalar MLP

filter_steps_by_window()

Removes steps whose straight-line displacement exceeds half the spatial window (WINDOW_SIZE × PIXEL_SIZE / 2 metres). Such steps would place the true next location outside the predicted probability map, causing an index error in the loss function.

Code
original_data_size = len(step_df)

step_df = deepssf.filter_steps_by_window(
    step_df, window_size=WINDOW_SIZE, pixel_size=PIXEL_SIZE
)

print(f"Initial number of steps: {original_data_size:,}")
print(f"Steps after window filter: {len(step_df):,}")
print(f"Data reduction: {original_data_size - len(step_df):,} ({(original_data_size - len(step_df)) / original_data_size * 100:.1f}%)")
Initial number of steps: 10,296
Steps after window filter: 10,101
Data reduction: 195 (1.9%)

make_dataloaders()

Splits step_df into train / val / test subsets (here 80 / 10 / 10 %) and returns a PyTorch DataLoader for each.

The split is a torch.utils.data.random_split, not a chronological one — each subset is a random sample of steps drawn from across the whole dataset and all 28 individuals. Consecutive steps are highly autocorrelated, so a random split leaks information between train and test and will make the validation numbers below look better than genuine out-of-sample performance. If you want an honest held-out estimate, split step_df yourself (by individual, or by time) and build the DataLoaders from the resulting subsets.

  • Each DataLoader yields tuples of:
    • x1 — spatial window tensor (batch × n_spatial_channels × H × W)
      • One channel per raster band in LAYER_PATHS, cropped around each step’s start pixel.
    • x2 — scalar covariate tensor (batch × len(SCALAR_COLS))
      • Raw scalar values; broadcast into spatial maps inside the model.
    • x3 — previous bearing tensor (batch × 1)
      • Conditions the movement kernel on the direction of the prior step.
    • (px2, py2) — pixel indices of the true next location within the window
      • Used by the loss function to look up the predicted probability at the observed destination.

The layer_paths argument causes the function to re-read the rasters internally, so the DataLoader is independent of the env_layers dict loaded above.

Code
dl_train, dl_val, dl_test = deepssf.make_dataloaders(
    layer_paths=LAYER_PATHS,
    window_size=WINDOW_SIZE,
    batch_size=BATCH_SIZE,
    train_split=0.8,
    val_split=0.1,
    scalar_cols=SCALAR_COLS,
    df=step_df,
)
print(f"Train batches : {len(dl_train)}")
print(f"Val   batches : {len(dl_val)}")
print(f"Test  batches : {len(dl_test)}")
Layer 'ndvi': min=-0.06614641100168228, max=0.731696367263794 → scaled to [0, 1]
Layer 'slope': min=0.00013558653881773353, max=12.298083305358887 → scaled to [0, 1]
/Users/scottforrest/Library/Mobile Documents/com~apple~CloudDocs/github_repos/deepSSF_package/src/deepssf/data.py:402: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at /Users/runner/work/pytorch/pytorch/torch/csrc/utils/tensor_numpy.cpp:219.)
  self.scalar_to_grid_data = torch.from_numpy(movement_df[cols].values).float()
Train batches : 253
Val   batches : 32
Test  batches : 32

Inspect one batch to verify tensor shapes match ModelParams expectations

x1 has one channel per spatial layer (ndvi + slope = 2). The scalar covariates in x2 are broadcast to full H × W maps inside ConvJointModel and concatenated with x1 there, giving the CNN 2 + len(SCALAR_COLS) = 7 input channels total.

The px2 and py2 are the target that the model is trying to predict, and are the pixel values of the next step (with the centre cell being (50, 50))

next(iter(dl_train)) will call the next batch, which are randomly shuffled, so calling the function again will provide different training samples.

Code
x1, x2, x3, (px2, py2), _ = next(iter(dl_train))
print(f"Spatial   x1 : {x1.shape}   (batch × channels × H × W)")
print(f"Scalars   x2 : {x2.shape}   (batch × len(SCALAR_COLS))")
print(f"Bearing   x3 : {x3.shape}   (batch × 1)")
print(f"Next-step px2: {px2[:4].tolist()}  py2: {py2[:4].tolist()}")
Spatial   x1 : torch.Size([32, 2, 101, 101])   (batch × channels × H × W)
Scalars   x2 : torch.Size([32, 5])   (batch × len(SCALAR_COLS))
Bearing   x3 : torch.Size([32, 1])   (batch × 1)
Next-step px2: [54, 51, 50, 50]  py2: [51, 53, 50, 50]

Plot some samples from the first batch

Plot the spatial window “tiles” from the first batch: one row per sample, one column per raster band. The centre pixel is the step’s start location and the red cross marks the true next location that the loss function will be trying to predict (and used to evaluate the loss function).

Code
N_SHOW = 4  # how many samples from the batch to display

# Band labels straight from the GeoTIFF metadata, in the channel order the model sees
channel_labels = []
for name, arr in env_layers.items():
    if arr.ndim == 2:
        channel_labels.append(name)
        continue
    with rasterio.open(LAYER_PATHS[name]) as src:
        descs = src.descriptions
    for band in range(arr.shape[0]):
        channel_labels.append(descs[band] if descs and descs[band] else f"{name} b{band + 1}")

n_show = min(N_SHOW, x1.shape[0])
n_chan = x1.shape[1]
centre = WINDOW_SIZE // 2

fig, axes = plt.subplots(
    n_show, n_chan, figsize=(3.0 * n_chan, 3.0 * n_show), squeeze=False
)

for row in range(n_show):
    for col in range(n_chan):
        ax = axes[row, col]
        tile = x1[row, col].numpy()
        ax.imshow(tile, cmap="viridis", origin="upper")
        # Start location: always the centre cell of the window by construction
        ax.plot(centre, centre, "w+", markersize=9, markeredgewidth=1.5)
        # True next step — px2 is the column index, py2 the row index
        ax.plot(px2[row].item(), py2[row].item(), "rx", markersize=9, markeredgewidth=1.5)
        ax.set_xticks([])
        ax.set_yticks([])
        if row == 0:
            ax.set_title(channel_labels[col], fontsize=10)
        if col == 0:
            ax.set_ylabel(
                f"sample {row}\n→ ({px2[row].item()}, {py2[row].item()})", fontsize=8
            )

fig.suptitle(
    f"First batch: {WINDOW_SIZE}×{WINDOW_SIZE} tiles  "
    "(white + = step start, red × = observed next step)",
    y=1.0,
)
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR / "batch_tiles.png"), dpi=100, bbox_inches="tight")
plt.show()

DROPOUT

Dropout switches some proportion of the weights off during each forward pass of the model, to prevent the model becoming overly reliant on certain routes through the network, which could result in overfitting and limit generalisability.

Here it only applies to the fully connected layers in the movement kernel, not for any of the convolution layers.

Code
DROPOUT_RATE = 0.2  # proportion of hidden units to drop out during training

ModelParams

A thin dict-like container (it is a wrapper around something else rather than storing data) that groups all architecture hyper-parameters so they can be passed to ConvJointModel and logged together.

Pretty much everything has been set above, and this is where they are stored and how they’re provided to the model. The parameters that are defined here (stride, padding, num_movement_params) should be kept as they are to ensure the model works as expected. The others should be able to be changed, which will change the shape of the model.

Code
params = deepssf.ModelParams({
    "batch_size":                BATCH_SIZE,
    "image_dim":                 WINDOW_SIZE,           # spatial window size (pixels)
    "pixel_size":                PIXEL_SIZE,            # metres per pixel
    "dim_in_nonspatial_to_grid": len(SCALAR_COLS),      # scalar cols tiled into window
    "dense_dim_in_nonspatial":   len(SCALAR_COLS),      # scalar inputs to the dense MLP
    "dense_dim_hidden":          DENSE_DIM_HIDDEN,                   # hidden units in the scalar MLP
    "dense_dim_in_all":          DENSE_DIM,             # flattened CNN output size
    # One channel per raster band (not per layer) + the tiled scalar covariates
    "input_channels":            N_SPATIAL_CHANNELS + len(SCALAR_COLS),
    "output_channels":           OUTPUT_CHANNELS,       # feature maps per conv layer
    "kernel_size":               KERNEL_SIZE,           # conv kernel spatial size
    "stride":                    1,                     # conv stride (should be kept at 1)
    "kernel_size_mp":            KERNEL_SIZE_MP,        # max-pool kernel size
    "stride_mp":                 STRIDE_MP,             # max-pool stride 
    "padding":                   1,                     # same-padding preserves spatial dimensions (should be kept at 1)
    "num_movement_params":       12,                    # movement kernel parameters (changing this will require modifying the movement kernel)
    "n_conv_layers_hab":         N_CONV_LAYERS_HAB,     # conv layers, habitat branch
    "n_conv_layers_move":        N_CONV_LAYERS_MOVE,    # conv+maxpool layers, movement
    "dropout":                   DROPOUT_RATE,                   # dropout probability (0.0 = no dropout, 0.2 = 20% dropout)
    "device":                    DEVICE,
})
print(f"input_channels: {params.input_channels}  (must match x1 channels + len(SCALAR_COLS))")
input_channels: 7  (must match x1 channels + len(SCALAR_COLS))

ConvJointModel

The deepSSF model! A joint habitat–movement model with two heads: 1. Habitat head — a small CNN applied to the spatial window (env layers + tiled scalars) that outputs a per-pixel log-suitability map. 2. Movement head — a dense MLP (fully-connected layer) that takes the scalar covariates and the previous bearing, and produces parameters for a parametric movement kernel (a mixture of wrapped distributions centred on the current location).

The two log-probability surfaces are summed pixel-wise to give the joint predicted probability map for the animal’s next step.

Code
model = deepssf.ConvJointModel(params).to(DEVICE)
n_params = sum(p.numel() for p in model.parameters())
print(f"Model parameters : {n_params:,}")
Model parameters : 82,165

negativeLogLikeLoss

Scores each predicted probability map against the true next-step pixel index; minimising it pushes probability mass toward observed locations.

Code
loss_fn = deepssf.negativeLogLikeLoss(reduction="mean")

make_optimisers()

Creates two Adam optimisers with separate learning rates:

  • lr_habitat
    • controls the CNN (habitat) head; typically higher
  • lr_movement
    • controls the movement-kernel MLP; kept lower for stability

It also returns a ReduceLROnPlateau scheduler for each optimiser that reduces the learning rate by a factor of scheduler_factor (default = 0.1) if validation loss does not improve for scheduler_patience epochs (default = 5).

Code
optimisers, schedulers = deepssf.make_optimisers(
    model, 
    lr_habitat=1e-4, 
    lr_movement=1e-5, 
    scheduler_patience=3, 
    scheduler_factor=0.5
)

EarlyStopping

Monitors validation loss and saves the best model weights to disk whenever a new minimum is reached. Training stops automatically if the loss does not improve for patience consecutive epochs, preventing overfitting.

  • patience
    • If the loss doesn’t improve for patience epochs, terminate training
Code
early_stop = deepssf.EarlyStopping(
    patience=10,
    verbose=True,
    monitor="both",                 # stop only when BOTH heads have plateaued
    path=str(OUTPUT_DIR / "best_model.pt"),
    checkpoint_on="active",         # a habitat-only stage saves on habitat loss
    # Also keep each head's own best epoch, across the whole run
    head_paths={
        "habitat":  str(OUTPUT_DIR / "best_model_habitat.pt"),
        "movement": str(OUTPUT_DIR / "best_model_movement.pt"),
    },
)

fit()

Runs the full training loop.

  • Inputs:
    • model
      • the name of the deepSSF model specified using ConvJointModel above
    • image_trim_pixels
      • how many outer pixel rings to trim from the output images before saving to prevent artifacts with the colour scaling due to the image padding being pulled into the image during the convolution layers.
    • dl_train
      • train dataloader (contains the training data)
    • dl_val
      • validation dataloader (contains the validation data used to assess the learning rate and early stopping)
    • loss_fn
      • loss function
    • optimisers
      • optimisers used to update the model weights
    • schedulers
      • learning-rate schedulers (used to update the learning rates)
    • early_stopping
      • stores the validation loss and stops the model training if it doesn’t improve for a given number of epochs (set using patience in the EarlyStopping definition)
    • snapshot_dir
      • where to save images of the training progress
    • snapshot_item
      • which sample (movement step) from the validation dataset to run the model on to generate the movement, habitat selection and next-step probability surfaces
  • Outputs:
    • history dict with keys train_losses and val_losses (one scalar value per completed epoch)

Index 0 of every history list is a pre-training baseline — a validation pass run before the first epoch, with train_losses[0] = nan and stage[0] = -1. An untrained habitat surface is uniform over the window, and the gap between that and the final val_habitat_losses is a direct read on how much the habitat head actually learned.

Staged training

stages runs the two sub-networks as a coordinate ascent on the same joint likelihood, rather than reweighting it:

  1. movement only — get a sensible availability kernel in place first, since habitat is fitted conditional on it
  2. habitat only — habitat now runs to its own convergence, on its own early-stopping clock, with the movement surface held fixed in the likelihood as an offset
  3. both — a joint pass to settle

A frozen sub-network keeps contributing its surface to the loss; only its weights stop moving. This is why staging is used instead of negativeLogLikeLoss(freeze_movement=True) — that flag drops the movement surface from the combined loss, so habitat would have to explain the movement structure too and would learn a distance-decay bump centred on the previous location, which is the opposite of a clean resource-selection surface.

Early stopping ends the current stage and advances to the next, so the final stage’s early stop ends the run.

Every epoch, a plot of the training and validation loss, and the predicted probability maps (movement, habitat selection and next-step) are written to SNAPSHOT_DIR, which will be turned into a GIF in the next cell.

Code
history = deepssf.fit(
    model=model,
    image_trim_pixels=4,
    window_size=WINDOW_SIZE,
    dl_train=dl_train,
    dl_val=dl_val,
    loss_fn=loss_fn,
    optimisers=optimisers,
    schedulers=schedulers,
    stages=[
        {"epochs": 20, "train": ("movement",)},   # availability kernel first
        {"epochs": 50, "train": ("habitat",)},    # habitat to its own convergence
        {"epochs": 30, "train": ("habitat", "movement")},  # joint settle
    ],
    early_stopping=early_stop,
    snapshot_dir=str(SNAPSHOT_DIR),
    snapshot_item=250,
    batch_size=BATCH_SIZE,
)
Baseline (untrained) val loss: 6.498492  (hab: 9.230684, mov: 6.498499)

=== Stage 1/3: training movement for up to 20 epoch(s) ===

Epoch 1/100
loss:        6.245838  [   32/ 8081]
loss:        6.327562  [  352/ 8081]
loss:        6.775728  [  672/ 8081]
loss:        6.546637  [  992/ 8081]
loss:        7.204468  [ 1312/ 8081]
loss:        6.677533  [ 1632/ 8081]
loss:        6.393209  [ 1952/ 8081]
loss:        6.763928  [ 2272/ 8081]
loss:        7.236836  [ 2592/ 8081]
loss:        6.930830  [ 2912/ 8081]
loss:        6.310127  [ 3232/ 8081]
loss:        6.147870  [ 3552/ 8081]
loss:        6.567813  [ 3872/ 8081]
loss:        6.503868  [ 4192/ 8081]
loss:        6.787545  [ 4512/ 8081]
loss:        6.867551  [ 4832/ 8081]
loss:        6.125998  [ 5152/ 8081]
loss:        6.516959  [ 5472/ 8081]
loss:        7.466505  [ 5792/ 8081]
loss:        6.176094  [ 6112/ 8081]
loss:        6.893275  [ 6432/ 8081]
loss:        6.424982  [ 6752/ 8081]
loss:        6.522388  [ 7072/ 8081]
loss:        6.098142  [ 7392/ 8081]
loss:        6.875458  [ 7712/ 8081]
loss:        6.359963  [ 8032/ 8081]

Avg training loss:        6.485763
Val loss: 6.297949  (hab: 9.230684, mov: 6.297967)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (inf → 6.297967). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (6.297967) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 2/100
loss:        6.099136  [   32/ 8081]
loss:        5.353790  [  352/ 8081]
loss:        5.586840  [  672/ 8081]
loss:        6.247027  [  992/ 8081]
loss:        6.178728  [ 1312/ 8081]
loss:        5.690164  [ 1632/ 8081]
loss:        5.792567  [ 1952/ 8081]
loss:        6.390474  [ 2272/ 8081]
loss:        6.147866  [ 2592/ 8081]
loss:        5.615052  [ 2912/ 8081]
loss:        6.244604  [ 3232/ 8081]
loss:        5.754487  [ 3552/ 8081]
loss:        6.385683  [ 3872/ 8081]
loss:        6.027150  [ 4192/ 8081]
loss:        5.893201  [ 4512/ 8081]
loss:        6.018482  [ 4832/ 8081]
loss:        5.842194  [ 5152/ 8081]
loss:        6.261067  [ 5472/ 8081]
loss:        6.526294  [ 5792/ 8081]
loss:        5.693853  [ 6112/ 8081]
loss:        6.613298  [ 6432/ 8081]
loss:        5.897593  [ 6752/ 8081]
loss:        6.106407  [ 7072/ 8081]
loss:        6.307760  [ 7392/ 8081]
loss:        5.849806  [ 7712/ 8081]
loss:        5.635149  [ 8032/ 8081]

Avg training loss:        6.195054
Val loss: 5.937253  (hab: 9.230684, mov: 5.937276)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (6.297967 → 5.937276). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.937276) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 3/100
loss:        5.839103  [   32/ 8081]
loss:        5.704171  [  352/ 8081]
loss:        5.905136  [  672/ 8081]
loss:        6.292176  [  992/ 8081]
loss:        6.048475  [ 1312/ 8081]
loss:        5.728800  [ 1632/ 8081]
loss:        5.660979  [ 1952/ 8081]
loss:        6.299916  [ 2272/ 8081]
loss:        5.466337  [ 2592/ 8081]
loss:        5.360091  [ 2912/ 8081]
loss:        5.814377  [ 3232/ 8081]
loss:        6.257022  [ 3552/ 8081]
loss:        5.621016  [ 3872/ 8081]
loss:        6.197023  [ 4192/ 8081]
loss:        5.999003  [ 4512/ 8081]
loss:        5.098269  [ 4832/ 8081]
loss:        5.334662  [ 5152/ 8081]
loss:        5.660156  [ 5472/ 8081]
loss:        5.664272  [ 5792/ 8081]
loss:        5.309518  [ 6112/ 8081]
loss:        6.303430  [ 6432/ 8081]
loss:        5.240125  [ 6752/ 8081]
loss:        5.317604  [ 7072/ 8081]
loss:        5.448744  [ 7392/ 8081]
loss:        5.595090  [ 7712/ 8081]
loss:        5.658427  [ 8032/ 8081]

Avg training loss:        5.959020
Val loss: 5.766219  (hab: 9.230684, mov: 5.766239)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.937276 → 5.766239). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.766239) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 4/100
loss:        6.632946  [   32/ 8081]
loss:        5.912785  [  352/ 8081]
loss:        6.195656  [  672/ 8081]
loss:        5.832583  [  992/ 8081]
loss:        5.845481  [ 1312/ 8081]
loss:        5.430091  [ 1632/ 8081]
loss:        6.240309  [ 1952/ 8081]
loss:        5.108929  [ 2272/ 8081]
loss:        5.873037  [ 2592/ 8081]
loss:        4.801184  [ 2912/ 8081]
loss:        7.211305  [ 3232/ 8081]
loss:        6.344507  [ 3552/ 8081]
loss:        6.221582  [ 3872/ 8081]
loss:        6.156475  [ 4192/ 8081]
loss:        5.822242  [ 4512/ 8081]
loss:        5.714087  [ 4832/ 8081]
loss:        5.712467  [ 5152/ 8081]
loss:        5.373091  [ 5472/ 8081]
loss:        6.096395  [ 5792/ 8081]
loss:        5.958269  [ 6112/ 8081]
loss:        6.106349  [ 6432/ 8081]
loss:        6.483172  [ 6752/ 8081]
loss:        5.283493  [ 7072/ 8081]
loss:        5.479865  [ 7392/ 8081]
loss:        6.179856  [ 7712/ 8081]
loss:        6.091197  [ 8032/ 8081]

Avg training loss:        5.865439
Val loss: 5.703318  (hab: 9.230684, mov: 5.703336)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.766239 → 5.703336). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.703336) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 5/100
loss:        5.314751  [   32/ 8081]
loss:        5.612198  [  352/ 8081]
loss:        5.012476  [  672/ 8081]
loss:        5.812667  [  992/ 8081]
loss:        6.247972  [ 1312/ 8081]
loss:        6.105094  [ 1632/ 8081]
loss:        5.333930  [ 1952/ 8081]
loss:        5.522837  [ 2272/ 8081]
loss:        5.355854  [ 2592/ 8081]
loss:        5.125224  [ 2912/ 8081]
loss:        5.607130  [ 3232/ 8081]
loss:        5.688347  [ 3552/ 8081]
loss:        5.220277  [ 3872/ 8081]
loss:        5.451266  [ 4192/ 8081]
loss:        5.623048  [ 4512/ 8081]
loss:        5.481740  [ 4832/ 8081]
loss:        5.350410  [ 5152/ 8081]
loss:        5.953897  [ 5472/ 8081]
loss:        5.119708  [ 5792/ 8081]
loss:        5.758492  [ 6112/ 8081]
loss:        4.626543  [ 6432/ 8081]
loss:        5.989931  [ 6752/ 8081]
loss:        5.971901  [ 7072/ 8081]
loss:        5.458930  [ 7392/ 8081]
loss:        5.882965  [ 7712/ 8081]
loss:        5.668804  [ 8032/ 8081]

Avg training loss:        5.826937
Val loss: 5.670581  (hab: 9.230684, mov: 5.670599)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.703336 → 5.670599). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.670599) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 6/100
loss:        6.092647  [   32/ 8081]
loss:        7.105897  [  352/ 8081]
loss:        6.363903  [  672/ 8081]
loss:        5.123186  [  992/ 8081]
loss:        6.105388  [ 1312/ 8081]
loss:        6.213997  [ 1632/ 8081]
loss:        5.350629  [ 1952/ 8081]
loss:        5.960525  [ 2272/ 8081]
loss:        5.537608  [ 2592/ 8081]
loss:        4.229580  [ 2912/ 8081]
loss:        4.838241  [ 3232/ 8081]
loss:        4.984264  [ 3552/ 8081]
loss:        5.567141  [ 3872/ 8081]
loss:        5.118925  [ 4192/ 8081]
loss:        6.012397  [ 4512/ 8081]
loss:        5.353520  [ 4832/ 8081]
loss:        5.682104  [ 5152/ 8081]
loss:        5.858663  [ 5472/ 8081]
loss:        5.602880  [ 5792/ 8081]
loss:        4.763502  [ 6112/ 8081]
loss:        5.669884  [ 6432/ 8081]
loss:        5.806078  [ 6752/ 8081]
loss:        6.759211  [ 7072/ 8081]
loss:        5.518186  [ 7392/ 8081]
loss:        5.288215  [ 7712/ 8081]
loss:        5.528504  [ 8032/ 8081]

Avg training loss:        5.809345
Val loss: 5.654435  (hab: 9.230684, mov: 5.654451)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.670599 → 5.654451). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.654451) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 7/100
loss:        5.568078  [   32/ 8081]
loss:        4.881546  [  352/ 8081]
loss:        4.778163  [  672/ 8081]
loss:        5.636003  [  992/ 8081]
loss:        5.682333  [ 1312/ 8081]
loss:        5.594457  [ 1632/ 8081]
loss:        5.899773  [ 1952/ 8081]
loss:        6.186626  [ 2272/ 8081]
loss:        6.073818  [ 2592/ 8081]
loss:        6.664757  [ 2912/ 8081]
loss:        6.489501  [ 3232/ 8081]
loss:        6.433125  [ 3552/ 8081]
loss:        6.309841  [ 3872/ 8081]
loss:        5.354329  [ 4192/ 8081]
loss:        5.422113  [ 4512/ 8081]
loss:        7.437659  [ 4832/ 8081]
loss:        5.919934  [ 5152/ 8081]
loss:        5.305979  [ 5472/ 8081]
loss:        7.002405  [ 5792/ 8081]
loss:        4.155359  [ 6112/ 8081]
loss:        5.167731  [ 6432/ 8081]
loss:        5.708542  [ 6752/ 8081]
loss:        4.483506  [ 7072/ 8081]
loss:        5.347366  [ 7392/ 8081]
loss:        6.393681  [ 7712/ 8081]
loss:        6.031096  [ 8032/ 8081]

Avg training loss:        5.792124
Val loss: 5.638611  (hab: 9.230684, mov: 5.638627)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.654451 → 5.638627). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.638627) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 8/100
loss:        5.099235  [   32/ 8081]
loss:        5.743075  [  352/ 8081]
loss:        6.132105  [  672/ 8081]
loss:        5.996379  [  992/ 8081]
loss:        6.032066  [ 1312/ 8081]
loss:        5.987046  [ 1632/ 8081]
loss:        5.831802  [ 1952/ 8081]
loss:        5.307637  [ 2272/ 8081]
loss:        5.724792  [ 2592/ 8081]
loss:        5.897244  [ 2912/ 8081]
loss:        5.991146  [ 3232/ 8081]
loss:        4.891418  [ 3552/ 8081]
loss:        6.974372  [ 3872/ 8081]
loss:        5.660956  [ 4192/ 8081]
loss:        7.223697  [ 4512/ 8081]
loss:        5.804978  [ 4832/ 8081]
loss:        6.393062  [ 5152/ 8081]
loss:        6.182287  [ 5472/ 8081]
loss:        6.341672  [ 5792/ 8081]
loss:        6.084341  [ 6112/ 8081]
loss:        6.284451  [ 6432/ 8081]
loss:        5.453782  [ 6752/ 8081]
loss:        5.336484  [ 7072/ 8081]
loss:        7.075533  [ 7392/ 8081]
loss:        5.086387  [ 7712/ 8081]
loss:        4.845394  [ 8032/ 8081]

Avg training loss:        5.786819
Val loss: 5.632171  (hab: 9.230684, mov: 5.632186)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.638627 → 5.632186). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.632186) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 9/100
loss:        6.308364  [   32/ 8081]
loss:        4.967251  [  352/ 8081]
loss:        5.667865  [  672/ 8081]
loss:        6.454578  [  992/ 8081]
loss:        5.054966  [ 1312/ 8081]
loss:        5.535814  [ 1632/ 8081]
loss:        6.263032  [ 1952/ 8081]
loss:        4.745832  [ 2272/ 8081]
loss:        6.120334  [ 2592/ 8081]
loss:        5.300797  [ 2912/ 8081]
loss:        4.350340  [ 3232/ 8081]
loss:        5.738823  [ 3552/ 8081]
loss:        6.101245  [ 3872/ 8081]
loss:        5.936307  [ 4192/ 8081]
loss:        5.858898  [ 4512/ 8081]
loss:        4.961806  [ 4832/ 8081]
loss:        4.966421  [ 5152/ 8081]
loss:        5.127247  [ 5472/ 8081]
loss:        6.860676  [ 5792/ 8081]
loss:        5.478895  [ 6112/ 8081]
loss:        5.261863  [ 6432/ 8081]
loss:        6.303907  [ 6752/ 8081]
loss:        4.265432  [ 7072/ 8081]
loss:        5.241710  [ 7392/ 8081]
loss:        5.257975  [ 7712/ 8081]
loss:        4.894818  [ 8032/ 8081]

Avg training loss:        5.776724
Val loss: 5.629901  (hab: 9.230684, mov: 5.629916)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.632186 → 5.629916). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.629916) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 10/100
loss:        6.314488  [   32/ 8081]
loss:        5.867524  [  352/ 8081]
loss:        5.768166  [  672/ 8081]
loss:        5.184070  [  992/ 8081]
loss:        6.571313  [ 1312/ 8081]
loss:        6.356519  [ 1632/ 8081]
loss:        5.905964  [ 1952/ 8081]
loss:        5.781095  [ 2272/ 8081]
loss:        5.599715  [ 2592/ 8081]
loss:        5.270180  [ 2912/ 8081]
loss:        5.790470  [ 3232/ 8081]
loss:        5.750713  [ 3552/ 8081]
loss:        6.457706  [ 3872/ 8081]
loss:        5.310520  [ 4192/ 8081]
loss:        5.795385  [ 4512/ 8081]
loss:        5.713717  [ 4832/ 8081]
loss:        6.134362  [ 5152/ 8081]
loss:        5.727392  [ 5472/ 8081]
loss:        5.941470  [ 5792/ 8081]
loss:        6.709103  [ 6112/ 8081]
loss:        5.234346  [ 6432/ 8081]
loss:        5.648595  [ 6752/ 8081]
loss:        6.490264  [ 7072/ 8081]
loss:        6.932870  [ 7392/ 8081]
loss:        6.698299  [ 7712/ 8081]
loss:        6.143182  [ 8032/ 8081]

Avg training loss:        5.768709
Val loss: 5.620571  (hab: 9.230684, mov: 5.620585)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.629916 → 5.620585). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.620585) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 11/100
loss:        6.147740  [   32/ 8081]
loss:        5.848036  [  352/ 8081]
loss:        6.128418  [  672/ 8081]
loss:        5.709217  [  992/ 8081]
loss:        5.834153  [ 1312/ 8081]
loss:        5.872700  [ 1632/ 8081]
loss:        5.539373  [ 1952/ 8081]
loss:        5.604455  [ 2272/ 8081]
loss:        6.045370  [ 2592/ 8081]
loss:        4.917933  [ 2912/ 8081]
loss:        5.876919  [ 3232/ 8081]
loss:        5.395803  [ 3552/ 8081]
loss:        5.595012  [ 3872/ 8081]
loss:        4.589242  [ 4192/ 8081]
loss:        5.743724  [ 4512/ 8081]
loss:        5.778891  [ 4832/ 8081]
loss:        5.282884  [ 5152/ 8081]
loss:        5.544705  [ 5472/ 8081]
loss:        5.846744  [ 5792/ 8081]
loss:        5.917231  [ 6112/ 8081]
loss:        6.161053  [ 6432/ 8081]
loss:        5.534051  [ 6752/ 8081]
loss:        5.505260  [ 7072/ 8081]
loss:        6.237825  [ 7392/ 8081]
loss:        5.684086  [ 7712/ 8081]
loss:        5.539722  [ 8032/ 8081]

Avg training loss:        5.753760
Val loss: 5.613468  (hab: 9.230684, mov: 5.613483)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.620585 → 5.613483). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.613483) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 12/100
loss:        5.487241  [   32/ 8081]
loss:        4.759591  [  352/ 8081]
loss:        4.958356  [  672/ 8081]
loss:        5.543693  [  992/ 8081]
loss:        6.048165  [ 1312/ 8081]
loss:        5.803805  [ 1632/ 8081]
loss:        6.045733  [ 1952/ 8081]
loss:        5.524828  [ 2272/ 8081]
loss:        5.379117  [ 2592/ 8081]
loss:        6.816269  [ 2912/ 8081]
loss:        5.883208  [ 3232/ 8081]
loss:        6.287618  [ 3552/ 8081]
loss:        6.083188  [ 3872/ 8081]
loss:        6.789408  [ 4192/ 8081]
loss:        6.018589  [ 4512/ 8081]
loss:        5.963109  [ 4832/ 8081]
loss:        6.040549  [ 5152/ 8081]
loss:        4.614318  [ 5472/ 8081]
loss:        6.000447  [ 5792/ 8081]
loss:        6.163531  [ 6112/ 8081]
loss:        5.295474  [ 6432/ 8081]
loss:        5.018180  [ 6752/ 8081]
loss:        5.573012  [ 7072/ 8081]
loss:        6.498673  [ 7392/ 8081]
loss:        5.381559  [ 7712/ 8081]
loss:        5.554119  [ 8032/ 8081]

Avg training loss:        5.746052
Val loss: 5.608727  (hab: 9.230684, mov: 5.608741)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.613483 → 5.608741). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.608741) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 13/100
loss:        6.073669  [   32/ 8081]
loss:        5.419466  [  352/ 8081]
loss:        5.843186  [  672/ 8081]
loss:        5.837664  [  992/ 8081]
loss:        5.857747  [ 1312/ 8081]
loss:        4.882453  [ 1632/ 8081]
loss:        6.780400  [ 1952/ 8081]
loss:        5.916022  [ 2272/ 8081]
loss:        6.016525  [ 2592/ 8081]
loss:        6.327806  [ 2912/ 8081]
loss:        5.499141  [ 3232/ 8081]
loss:        5.230143  [ 3552/ 8081]
loss:        4.949910  [ 3872/ 8081]
loss:        6.453809  [ 4192/ 8081]
loss:        6.041904  [ 4512/ 8081]
loss:        5.457885  [ 4832/ 8081]
loss:        5.023824  [ 5152/ 8081]
loss:        5.826265  [ 5472/ 8081]
loss:        6.044719  [ 5792/ 8081]
loss:        6.064189  [ 6112/ 8081]
loss:        6.439377  [ 6432/ 8081]
loss:        5.804107  [ 6752/ 8081]
loss:        5.386705  [ 7072/ 8081]
loss:        5.617929  [ 7392/ 8081]
loss:        6.386126  [ 7712/ 8081]
loss:        6.398268  [ 8032/ 8081]

Avg training loss:        5.744411
Val loss: 5.605536  (hab: 9.230684, mov: 5.605550)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.608741 → 5.605550). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.605550) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 14/100
loss:        6.167862  [   32/ 8081]
loss:        5.421381  [  352/ 8081]
loss:        5.801930  [  672/ 8081]
loss:        6.168296  [  992/ 8081]
loss:        5.170939  [ 1312/ 8081]
loss:        5.607087  [ 1632/ 8081]
loss:        6.067954  [ 1952/ 8081]
loss:        5.593189  [ 2272/ 8081]
loss:        5.857327  [ 2592/ 8081]
loss:        5.154043  [ 2912/ 8081]
loss:        4.849125  [ 3232/ 8081]
loss:        5.506304  [ 3552/ 8081]
loss:        5.656959  [ 3872/ 8081]
loss:        5.387237  [ 4192/ 8081]
loss:        5.295750  [ 4512/ 8081]
loss:        5.182643  [ 4832/ 8081]
loss:        6.232894  [ 5152/ 8081]
loss:        5.986517  [ 5472/ 8081]
loss:        5.494692  [ 5792/ 8081]
loss:        5.424548  [ 6112/ 8081]
loss:        5.677889  [ 6432/ 8081]
loss:        5.413161  [ 6752/ 8081]
loss:        5.684591  [ 7072/ 8081]
loss:        4.958488  [ 7392/ 8081]
loss:        5.986359  [ 7712/ 8081]
loss:        5.468246  [ 8032/ 8081]

Avg training loss:        5.738241
Val loss: 5.602485  (hab: 9.230684, mov: 5.602499)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.605550 → 5.602499). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.602499) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 15/100
loss:        4.989216  [   32/ 8081]
loss:        5.912005  [  352/ 8081]
loss:        5.557985  [  672/ 8081]
loss:        5.653116  [  992/ 8081]
loss:        4.846440  [ 1312/ 8081]
loss:        6.104223  [ 1632/ 8081]
loss:        5.232669  [ 1952/ 8081]
loss:        5.468318  [ 2272/ 8081]
loss:        6.651891  [ 2592/ 8081]
loss:        5.294498  [ 2912/ 8081]
loss:        5.919892  [ 3232/ 8081]
loss:        6.003839  [ 3552/ 8081]
loss:        6.305511  [ 3872/ 8081]
loss:        5.008946  [ 4192/ 8081]
loss:        6.894829  [ 4512/ 8081]
loss:        5.664158  [ 4832/ 8081]
loss:        6.371749  [ 5152/ 8081]
loss:        5.040733  [ 5472/ 8081]
loss:        5.726504  [ 5792/ 8081]
loss:        5.415521  [ 6112/ 8081]
loss:        6.107200  [ 6432/ 8081]
loss:        5.186206  [ 6752/ 8081]
loss:        6.264306  [ 7072/ 8081]
loss:        5.869187  [ 7392/ 8081]
loss:        4.688340  [ 7712/ 8081]
loss:        5.336164  [ 8032/ 8081]

Avg training loss:        5.733183
Val loss: 5.597341  (hab: 9.230684, mov: 5.597354)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.602499 → 5.597354). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.597354) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 16/100
loss:        5.766613  [   32/ 8081]
loss:        5.339662  [  352/ 8081]
loss:        6.394774  [  672/ 8081]
loss:        6.040304  [  992/ 8081]
loss:        5.244086  [ 1312/ 8081]
loss:        6.205747  [ 1632/ 8081]
loss:        6.052759  [ 1952/ 8081]
loss:        5.535648  [ 2272/ 8081]
loss:        4.936183  [ 2592/ 8081]
loss:        5.625336  [ 2912/ 8081]
loss:        5.325573  [ 3232/ 8081]
loss:        5.831590  [ 3552/ 8081]
loss:        5.649969  [ 3872/ 8081]
loss:        6.708189  [ 4192/ 8081]
loss:        5.466768  [ 4512/ 8081]
loss:        6.284393  [ 4832/ 8081]
loss:        5.145835  [ 5152/ 8081]
loss:        4.971982  [ 5472/ 8081]
loss:        5.969846  [ 5792/ 8081]
loss:        5.591209  [ 6112/ 8081]
loss:        4.685674  [ 6432/ 8081]
loss:        5.046867  [ 6752/ 8081]
loss:        5.818790  [ 7072/ 8081]
loss:        5.235944  [ 7392/ 8081]
loss:        5.770116  [ 7712/ 8081]
loss:        5.626723  [ 8032/ 8081]

Avg training loss:        5.730803
Val loss: 5.594410  (hab: 9.230684, mov: 5.594423)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.597354 → 5.594423). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.594423) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 17/100
loss:        4.871367  [   32/ 8081]
loss:        5.736880  [  352/ 8081]
loss:        5.175937  [  672/ 8081]
loss:        4.820874  [  992/ 8081]
loss:        5.622364  [ 1312/ 8081]
loss:        5.579250  [ 1632/ 8081]
loss:        5.850883  [ 1952/ 8081]
loss:        5.875285  [ 2272/ 8081]
loss:        5.656169  [ 2592/ 8081]
loss:        5.553955  [ 2912/ 8081]
loss:        4.745771  [ 3232/ 8081]
loss:        5.905107  [ 3552/ 8081]
loss:        5.187916  [ 3872/ 8081]
loss:        6.295620  [ 4192/ 8081]
loss:        4.986555  [ 4512/ 8081]
loss:        5.114381  [ 4832/ 8081]
loss:        5.839475  [ 5152/ 8081]
loss:        4.913824  [ 5472/ 8081]
loss:        5.647020  [ 5792/ 8081]
loss:        5.907448  [ 6112/ 8081]
loss:        5.920348  [ 6432/ 8081]
loss:        5.641849  [ 6752/ 8081]
loss:        5.379340  [ 7072/ 8081]
loss:        5.512960  [ 7392/ 8081]
loss:        5.705914  [ 7712/ 8081]
loss:        6.004600  [ 8032/ 8081]

Avg training loss:        5.729424
Val loss: 5.590032  (hab: 9.230684, mov: 5.590045)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.594423 → 5.590045). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.590045) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 18/100
loss:        6.336276  [   32/ 8081]
loss:        5.780512  [  352/ 8081]
loss:        4.655159  [  672/ 8081]
loss:        6.247569  [  992/ 8081]
loss:        5.853029  [ 1312/ 8081]
loss:        5.985885  [ 1632/ 8081]
loss:        4.970037  [ 1952/ 8081]
loss:        5.095961  [ 2272/ 8081]
loss:        5.903147  [ 2592/ 8081]
loss:        5.427063  [ 2912/ 8081]
loss:        5.625571  [ 3232/ 8081]
loss:        5.285646  [ 3552/ 8081]
loss:        5.009737  [ 3872/ 8081]
loss:        5.156672  [ 4192/ 8081]
loss:        5.327857  [ 4512/ 8081]
loss:        5.724604  [ 4832/ 8081]
loss:        5.683038  [ 5152/ 8081]
loss:        5.354910  [ 5472/ 8081]
loss:        6.357274  [ 5792/ 8081]
loss:        5.163094  [ 6112/ 8081]
loss:        6.224330  [ 6432/ 8081]
loss:        5.599071  [ 6752/ 8081]
loss:        5.574611  [ 7072/ 8081]
loss:        5.239250  [ 7392/ 8081]
loss:        4.997913  [ 7712/ 8081]
loss:        6.280746  [ 8032/ 8081]

Avg training loss:        5.722111
Val loss: 5.587126  (hab: 9.230684, mov: 5.587139)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.590045 → 5.587139). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.587139) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 19/100
loss:        5.980573  [   32/ 8081]
loss:        5.602824  [  352/ 8081]
loss:        6.092440  [  672/ 8081]
loss:        5.996263  [  992/ 8081]
loss:        5.080707  [ 1312/ 8081]
loss:        5.649462  [ 1632/ 8081]
loss:        6.369906  [ 1952/ 8081]
loss:        5.880649  [ 2272/ 8081]
loss:        7.192008  [ 2592/ 8081]
loss:        5.894856  [ 2912/ 8081]
loss:        5.283854  [ 3232/ 8081]
loss:        5.592988  [ 3552/ 8081]
loss:        5.117173  [ 3872/ 8081]
loss:        7.270640  [ 4192/ 8081]
loss:        5.565506  [ 4512/ 8081]
loss:        7.196802  [ 4832/ 8081]
loss:        5.414187  [ 5152/ 8081]
loss:        5.921555  [ 5472/ 8081]
loss:        5.096357  [ 5792/ 8081]
loss:        6.105429  [ 6112/ 8081]
loss:        5.477416  [ 6432/ 8081]
loss:        6.091595  [ 6752/ 8081]
loss:        6.075043  [ 7072/ 8081]
loss:        5.058344  [ 7392/ 8081]
loss:        6.770479  [ 7712/ 8081]
loss:        6.078857  [ 8032/ 8081]

Avg training loss:        5.722099
Val loss: 5.585112  (hab: 9.230684, mov: 5.585125)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.587139 → 5.585125). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.585125) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

Epoch 20/100
loss:        5.495479  [   32/ 8081]
loss:        5.742344  [  352/ 8081]
loss:        4.841171  [  672/ 8081]
loss:        5.995773  [  992/ 8081]
loss:        5.092164  [ 1312/ 8081]
loss:        5.473918  [ 1632/ 8081]
loss:        5.701757  [ 1952/ 8081]
loss:        4.637615  [ 2272/ 8081]
loss:        6.149223  [ 2592/ 8081]
loss:        5.093746  [ 2912/ 8081]
loss:        5.734341  [ 3232/ 8081]
loss:        6.036654  [ 3552/ 8081]
loss:        5.749770  [ 3872/ 8081]
loss:        5.803497  [ 4192/ 8081]
loss:        5.484233  [ 4512/ 8081]
loss:        6.318474  [ 4832/ 8081]
loss:        5.720933  [ 5152/ 8081]
loss:        6.420022  [ 5472/ 8081]
loss:        6.222536  [ 5792/ 8081]
loss:        7.128765  [ 6112/ 8081]
loss:        6.121245  [ 6432/ 8081]
loss:        6.209498  [ 6752/ 8081]
loss:        5.643752  [ 7072/ 8081]
loss:        5.566283  [ 7392/ 8081]
loss:        4.954899  [ 7712/ 8081]
loss:        5.356214  [ 8032/ 8081]

Avg training loss:        5.718319
Val loss: 5.579656  (hab: 9.230684, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
movement validation loss decreased (5.585125 → 5.579670). Saving model…
  new best habitat (9.230684) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — movement: 0/10

=== Stage 2/3: training habitat for up to 50 epoch(s) ===

Epoch 21/100
loss:        6.214893  [   32/ 8081]
loss:        5.260427  [  352/ 8081]
loss:        6.455642  [  672/ 8081]
loss:        6.041101  [  992/ 8081]
loss:        6.043737  [ 1312/ 8081]
loss:        5.876389  [ 1632/ 8081]
loss:        6.167425  [ 1952/ 8081]
loss:        5.528721  [ 2272/ 8081]
loss:        5.156203  [ 2592/ 8081]
loss:        5.736268  [ 2912/ 8081]
loss:        5.556381  [ 3232/ 8081]
loss:        4.834304  [ 3552/ 8081]
loss:        5.192009  [ 3872/ 8081]
loss:        5.215473  [ 4192/ 8081]
loss:        4.965912  [ 4512/ 8081]
loss:        5.664655  [ 4832/ 8081]
loss:        5.654807  [ 5152/ 8081]
loss:        6.580978  [ 5472/ 8081]
loss:        6.449681  [ 5792/ 8081]
loss:        5.459794  [ 6112/ 8081]
loss:        5.351525  [ 6432/ 8081]
loss:        4.545648  [ 6752/ 8081]
loss:        6.095381  [ 7072/ 8081]
loss:        7.183198  [ 7392/ 8081]
loss:        6.631365  [ 7712/ 8081]
loss:        4.528363  [ 8032/ 8081]

Avg training loss:        5.714955
Val loss: 5.579576  (hab: 9.230192, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (inf → 9.230192). Saving model…
  new best habitat (9.230192) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 22/100
loss:        5.545901  [   32/ 8081]
loss:        5.874181  [  352/ 8081]
loss:        5.416817  [  672/ 8081]
loss:        5.730185  [  992/ 8081]
loss:        4.973294  [ 1312/ 8081]
loss:        4.594021  [ 1632/ 8081]
loss:        5.822527  [ 1952/ 8081]
loss:        5.320210  [ 2272/ 8081]
loss:        6.180506  [ 2592/ 8081]
loss:        5.218982  [ 2912/ 8081]
loss:        6.415107  [ 3232/ 8081]
loss:        5.100583  [ 3552/ 8081]
loss:        6.107276  [ 3872/ 8081]
loss:        5.014849  [ 4192/ 8081]
loss:        6.073889  [ 4512/ 8081]
loss:        6.449848  [ 4832/ 8081]
loss:        5.193040  [ 5152/ 8081]
loss:        5.050825  [ 5472/ 8081]
loss:        5.827705  [ 5792/ 8081]
loss:        6.314666  [ 6112/ 8081]
loss:        6.185558  [ 6432/ 8081]
loss:        5.958315  [ 6752/ 8081]
loss:        5.975613  [ 7072/ 8081]
loss:        6.144325  [ 7392/ 8081]
loss:        5.418290  [ 7712/ 8081]
loss:        5.256663  [ 8032/ 8081]

Avg training loss:        5.714180
Val loss: 5.578885  (hab: 9.228170, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.230192 → 9.228170). Saving model…
  new best habitat (9.228170) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 23/100
loss:        5.653369  [   32/ 8081]
loss:        5.685206  [  352/ 8081]
loss:        6.392813  [  672/ 8081]
loss:        5.465648  [  992/ 8081]
loss:        5.735848  [ 1312/ 8081]
loss:        6.919081  [ 1632/ 8081]
loss:        6.736339  [ 1952/ 8081]
loss:        5.200480  [ 2272/ 8081]
loss:        5.550709  [ 2592/ 8081]
loss:        6.431253  [ 2912/ 8081]
loss:        4.936907  [ 3232/ 8081]
loss:        6.082497  [ 3552/ 8081]
loss:        5.069165  [ 3872/ 8081]
loss:        6.105947  [ 4192/ 8081]
loss:        5.244616  [ 4512/ 8081]
loss:        5.995945  [ 4832/ 8081]
loss:        5.721663  [ 5152/ 8081]
loss:        5.560308  [ 5472/ 8081]
loss:        5.644754  [ 5792/ 8081]
loss:        4.425217  [ 6112/ 8081]
loss:        5.438462  [ 6432/ 8081]
loss:        5.644345  [ 6752/ 8081]
loss:        6.223389  [ 7072/ 8081]
loss:        5.554729  [ 7392/ 8081]
loss:        5.507235  [ 7712/ 8081]
loss:        5.304576  [ 8032/ 8081]

Avg training loss:        5.714334
Val loss: 5.576211  (hab: 9.223297, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.228170 → 9.223297). Saving model…
  new best habitat (9.223297) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 24/100
loss:        5.307393  [   32/ 8081]
loss:        6.052416  [  352/ 8081]
loss:        6.947698  [  672/ 8081]
loss:        4.966177  [  992/ 8081]
loss:        5.354320  [ 1312/ 8081]
loss:        5.011671  [ 1632/ 8081]
loss:        5.077588  [ 1952/ 8081]
loss:        6.410150  [ 2272/ 8081]
loss:        5.908555  [ 2592/ 8081]
loss:        5.191885  [ 2912/ 8081]
loss:        6.326991  [ 3232/ 8081]
loss:        5.950275  [ 3552/ 8081]
loss:        5.182536  [ 3872/ 8081]
loss:        5.854231  [ 4192/ 8081]
loss:        5.781198  [ 4512/ 8081]
loss:        4.758808  [ 4832/ 8081]
loss:        5.441116  [ 5152/ 8081]
loss:        6.130911  [ 5472/ 8081]
loss:        5.521577  [ 5792/ 8081]
loss:        5.110908  [ 6112/ 8081]
loss:        6.249578  [ 6432/ 8081]
loss:        5.340546  [ 6752/ 8081]
loss:        5.915018  [ 7072/ 8081]
loss:        6.110167  [ 7392/ 8081]
loss:        6.595109  [ 7712/ 8081]
loss:        5.572565  [ 8032/ 8081]

Avg training loss:        5.702035
Val loss: 5.570370  (hab: 9.219717, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.223297 → 9.219717). Saving model…
  new best habitat (9.219717) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 25/100
loss:        6.576512  [   32/ 8081]
loss:        5.906764  [  352/ 8081]
loss:        4.642290  [  672/ 8081]
loss:        6.289025  [  992/ 8081]
loss:        6.225452  [ 1312/ 8081]
loss:        5.799870  [ 1632/ 8081]
loss:        6.388820  [ 1952/ 8081]
loss:        6.104104  [ 2272/ 8081]
loss:        5.029119  [ 2592/ 8081]
loss:        5.541561  [ 2912/ 8081]
loss:        5.225651  [ 3232/ 8081]
loss:        6.731432  [ 3552/ 8081]
loss:        5.620647  [ 3872/ 8081]
loss:        5.570349  [ 4192/ 8081]
loss:        6.379705  [ 4512/ 8081]
loss:        6.033603  [ 4832/ 8081]
loss:        5.366957  [ 5152/ 8081]
loss:        6.212232  [ 5472/ 8081]
loss:        5.891926  [ 5792/ 8081]
loss:        5.058862  [ 6112/ 8081]
loss:        5.418173  [ 6432/ 8081]
loss:        6.253366  [ 6752/ 8081]
loss:        4.993002  [ 7072/ 8081]
loss:        5.156422  [ 7392/ 8081]
loss:        5.365839  [ 7712/ 8081]
loss:        6.760598  [ 8032/ 8081]

Avg training loss:        5.697390
Val loss: 5.563745  (hab: 9.213537, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.219717 → 9.213537). Saving model…
  new best habitat (9.213537) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 26/100
loss:        5.571324  [   32/ 8081]
loss:        5.093164  [  352/ 8081]
loss:        5.406750  [  672/ 8081]
loss:        5.983043  [  992/ 8081]
loss:        6.425274  [ 1312/ 8081]
loss:        4.884968  [ 1632/ 8081]
loss:        5.942920  [ 1952/ 8081]
loss:        4.736323  [ 2272/ 8081]
loss:        5.991195  [ 2592/ 8081]
loss:        6.163450  [ 2912/ 8081]
loss:        6.665747  [ 3232/ 8081]
loss:        6.281912  [ 3552/ 8081]
loss:        6.116368  [ 3872/ 8081]
loss:        5.132730  [ 4192/ 8081]
loss:        5.814788  [ 4512/ 8081]
loss:        5.192154  [ 4832/ 8081]
loss:        5.920406  [ 5152/ 8081]
loss:        5.658549  [ 5472/ 8081]
loss:        5.287663  [ 5792/ 8081]
loss:        6.904008  [ 6112/ 8081]
loss:        5.647901  [ 6432/ 8081]
loss:        5.092663  [ 6752/ 8081]
loss:        6.106362  [ 7072/ 8081]
loss:        5.918185  [ 7392/ 8081]
loss:        5.526569  [ 7712/ 8081]
loss:        5.206008  [ 8032/ 8081]

Avg training loss:        5.686113
Val loss: 5.553567  (hab: 9.207697, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.213537 → 9.207697). Saving model…
  new best habitat (9.207697) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 27/100
loss:        5.768501  [   32/ 8081]
loss:        6.528740  [  352/ 8081]
loss:        4.605237  [  672/ 8081]
loss:        4.995129  [  992/ 8081]
loss:        5.742946  [ 1312/ 8081]
loss:        5.843820  [ 1632/ 8081]
loss:        5.605895  [ 1952/ 8081]
loss:        5.382057  [ 2272/ 8081]
loss:        5.940536  [ 2592/ 8081]
loss:        6.289454  [ 2912/ 8081]
loss:        6.307106  [ 3232/ 8081]
loss:        5.149521  [ 3552/ 8081]
loss:        6.333331  [ 3872/ 8081]
loss:        5.876357  [ 4192/ 8081]
loss:        5.429687  [ 4512/ 8081]
loss:        5.239026  [ 4832/ 8081]
loss:        5.141148  [ 5152/ 8081]
loss:        5.240422  [ 5472/ 8081]
loss:        5.276767  [ 5792/ 8081]
loss:        4.440660  [ 6112/ 8081]
loss:        5.166564  [ 6432/ 8081]
loss:        5.638910  [ 6752/ 8081]
loss:        5.833482  [ 7072/ 8081]
loss:        6.694550  [ 7392/ 8081]
loss:        5.925578  [ 7712/ 8081]
loss:        4.050676  [ 8032/ 8081]

Avg training loss:        5.683887
Val loss: 5.551470  (hab: 9.216839, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 1/10

Epoch 28/100
loss:        5.398245  [   32/ 8081]
loss:        5.676805  [  352/ 8081]
loss:        5.790808  [  672/ 8081]
loss:        5.104578  [  992/ 8081]
loss:        4.464424  [ 1312/ 8081]
loss:        5.452138  [ 1632/ 8081]
loss:        5.439054  [ 1952/ 8081]
loss:        6.643448  [ 2272/ 8081]
loss:        5.680412  [ 2592/ 8081]
loss:        6.271834  [ 2912/ 8081]
loss:        5.770246  [ 3232/ 8081]
loss:        4.955185  [ 3552/ 8081]
loss:        5.492970  [ 3872/ 8081]
loss:        5.747366  [ 4192/ 8081]
loss:        5.466230  [ 4512/ 8081]
loss:        5.639440  [ 4832/ 8081]
loss:        5.633723  [ 5152/ 8081]
loss:        6.061408  [ 5472/ 8081]
loss:        5.216766  [ 5792/ 8081]
loss:        5.682209  [ 6112/ 8081]
loss:        6.221254  [ 6432/ 8081]
loss:        5.966249  [ 6752/ 8081]
loss:        6.696516  [ 7072/ 8081]
loss:        6.208097  [ 7392/ 8081]
loss:        6.080026  [ 7712/ 8081]
loss:        6.059775  [ 8032/ 8081]

Avg training loss:        5.684379
Val loss: 5.543834  (hab: 9.194668, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.207697 → 9.194668). Saving model…
  new best habitat (9.194668) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 29/100
loss:        5.983919  [   32/ 8081]
loss:        5.953605  [  352/ 8081]
loss:        4.506452  [  672/ 8081]
loss:        5.663980  [  992/ 8081]
loss:        5.609444  [ 1312/ 8081]
loss:        6.052475  [ 1632/ 8081]
loss:        6.573792  [ 1952/ 8081]
loss:        5.793353  [ 2272/ 8081]
loss:        4.948423  [ 2592/ 8081]
loss:        5.310138  [ 2912/ 8081]
loss:        6.865760  [ 3232/ 8081]
loss:        5.498428  [ 3552/ 8081]
loss:        5.976036  [ 3872/ 8081]
loss:        5.241432  [ 4192/ 8081]
loss:        4.956198  [ 4512/ 8081]
loss:        5.638568  [ 4832/ 8081]
loss:        4.483184  [ 5152/ 8081]
loss:        5.451896  [ 5472/ 8081]
loss:        6.129908  [ 5792/ 8081]
loss:        5.199718  [ 6112/ 8081]
loss:        5.781012  [ 6432/ 8081]
loss:        5.665868  [ 6752/ 8081]
loss:        5.653917  [ 7072/ 8081]
loss:        6.277304  [ 7392/ 8081]
loss:        4.847164  [ 7712/ 8081]
loss:        6.992944  [ 8032/ 8081]

Avg training loss:        5.681584
Val loss: 5.544224  (hab: 9.196121, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 1/10

Epoch 30/100
loss:        5.659962  [   32/ 8081]
loss:        5.616187  [  352/ 8081]
loss:        5.399122  [  672/ 8081]
loss:        5.060857  [  992/ 8081]
loss:        6.068506  [ 1312/ 8081]
loss:        5.094334  [ 1632/ 8081]
loss:        5.945361  [ 1952/ 8081]
loss:        5.564237  [ 2272/ 8081]
loss:        6.023519  [ 2592/ 8081]
loss:        6.494512  [ 2912/ 8081]
loss:        5.563352  [ 3232/ 8081]
loss:        5.911071  [ 3552/ 8081]
loss:        5.836109  [ 3872/ 8081]
loss:        5.576584  [ 4192/ 8081]
loss:        6.792779  [ 4512/ 8081]
loss:        5.782731  [ 4832/ 8081]
loss:        5.951058  [ 5152/ 8081]
loss:        5.372826  [ 5472/ 8081]
loss:        5.631414  [ 5792/ 8081]
loss:        5.116663  [ 6112/ 8081]
loss:        6.573839  [ 6432/ 8081]
loss:        5.865407  [ 6752/ 8081]
loss:        5.231947  [ 7072/ 8081]
loss:        5.086476  [ 7392/ 8081]
loss:        6.096708  [ 7712/ 8081]
loss:        5.728494  [ 8032/ 8081]

Avg training loss:        5.682838
Val loss: 5.540275  (hab: 9.178395, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.194668 → 9.178395). Saving model…
  new best habitat (9.178395) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 31/100
loss:        5.891778  [   32/ 8081]
loss:        5.683362  [  352/ 8081]
loss:        4.646068  [  672/ 8081]
loss:        5.463505  [  992/ 8081]
loss:        6.140056  [ 1312/ 8081]
loss:        5.123763  [ 1632/ 8081]
loss:        5.265921  [ 1952/ 8081]
loss:        5.612885  [ 2272/ 8081]
loss:        5.631809  [ 2592/ 8081]
loss:        5.987524  [ 2912/ 8081]
loss:        5.326333  [ 3232/ 8081]
loss:        6.434807  [ 3552/ 8081]
loss:        6.142035  [ 3872/ 8081]
loss:        4.710371  [ 4192/ 8081]
loss:        6.080554  [ 4512/ 8081]
loss:        5.493956  [ 4832/ 8081]
loss:        5.118391  [ 5152/ 8081]
loss:        6.415235  [ 5472/ 8081]
loss:        5.485146  [ 5792/ 8081]
loss:        5.408470  [ 6112/ 8081]
loss:        5.831583  [ 6432/ 8081]
loss:        5.796210  [ 6752/ 8081]
loss:        6.348933  [ 7072/ 8081]
loss:        5.381494  [ 7392/ 8081]
loss:        5.963407  [ 7712/ 8081]
loss:        5.492370  [ 8032/ 8081]

Avg training loss:        5.677093
Val loss: 5.537004  (hab: 9.172251, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.178395 → 9.172251). Saving model…
  new best habitat (9.172251) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 32/100
loss:        6.710577  [   32/ 8081]
loss:        5.358533  [  352/ 8081]
loss:        6.494641  [  672/ 8081]
loss:        5.164561  [  992/ 8081]
loss:        6.075105  [ 1312/ 8081]
loss:        5.833843  [ 1632/ 8081]
loss:        5.589430  [ 1952/ 8081]
loss:        5.930730  [ 2272/ 8081]
loss:        5.542312  [ 2592/ 8081]
loss:        5.535332  [ 2912/ 8081]
loss:        6.143559  [ 3232/ 8081]
loss:        6.555410  [ 3552/ 8081]
loss:        5.737753  [ 3872/ 8081]
loss:        4.299313  [ 4192/ 8081]
loss:        5.811183  [ 4512/ 8081]
loss:        6.545938  [ 4832/ 8081]
loss:        6.054370  [ 5152/ 8081]
loss:        5.223416  [ 5472/ 8081]
loss:        5.879354  [ 5792/ 8081]
loss:        5.881961  [ 6112/ 8081]
loss:        5.901062  [ 6432/ 8081]
loss:        6.172688  [ 6752/ 8081]
loss:        6.385845  [ 7072/ 8081]
loss:        4.783300  [ 7392/ 8081]
loss:        6.645689  [ 7712/ 8081]
loss:        5.958378  [ 8032/ 8081]

Avg training loss:        5.680565
Val loss: 5.534242  (hab: 9.159212, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.172251 → 9.159212). Saving model…
  new best habitat (9.159212) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 33/100
loss:        6.087860  [   32/ 8081]
loss:        6.726515  [  352/ 8081]
loss:        5.205856  [  672/ 8081]
loss:        6.369033  [  992/ 8081]
loss:        5.989912  [ 1312/ 8081]
loss:        6.498851  [ 1632/ 8081]
loss:        5.965680  [ 1952/ 8081]
loss:        6.305436  [ 2272/ 8081]
loss:        6.921688  [ 2592/ 8081]
loss:        4.912704  [ 2912/ 8081]
loss:        4.874135  [ 3232/ 8081]
loss:        6.072099  [ 3552/ 8081]
loss:        5.097146  [ 3872/ 8081]
loss:        5.270378  [ 4192/ 8081]
loss:        5.755275  [ 4512/ 8081]
loss:        5.061832  [ 4832/ 8081]
loss:        6.027698  [ 5152/ 8081]
loss:        6.603996  [ 5472/ 8081]
loss:        5.379167  [ 5792/ 8081]
loss:        5.956470  [ 6112/ 8081]
loss:        6.389346  [ 6432/ 8081]
loss:        6.439223  [ 6752/ 8081]
loss:        5.510054  [ 7072/ 8081]
loss:        5.348186  [ 7392/ 8081]
loss:        5.622303  [ 7712/ 8081]
loss:        6.081584  [ 8032/ 8081]

Avg training loss:        5.670719
Val loss: 5.532465  (hab: 9.159620, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 1/10

Epoch 34/100
loss:        4.856123  [   32/ 8081]
loss:        5.066793  [  352/ 8081]
loss:        5.681656  [  672/ 8081]
loss:        5.529285  [  992/ 8081]
loss:        5.936635  [ 1312/ 8081]
loss:        5.660625  [ 1632/ 8081]
loss:        5.035449  [ 1952/ 8081]
loss:        5.421399  [ 2272/ 8081]
loss:        6.066647  [ 2592/ 8081]
loss:        6.373960  [ 2912/ 8081]
loss:        5.218757  [ 3232/ 8081]
loss:        5.729639  [ 3552/ 8081]
loss:        5.673229  [ 3872/ 8081]
loss:        5.588160  [ 4192/ 8081]
loss:        6.556441  [ 4512/ 8081]
loss:        5.569387  [ 4832/ 8081]
loss:        5.129972  [ 5152/ 8081]
loss:        6.942590  [ 5472/ 8081]
loss:        6.004828  [ 5792/ 8081]
loss:        5.552630  [ 6112/ 8081]
loss:        5.845505  [ 6432/ 8081]
loss:        4.793906  [ 6752/ 8081]
loss:        5.760399  [ 7072/ 8081]
loss:        5.716479  [ 7392/ 8081]
loss:        6.990435  [ 7712/ 8081]
loss:        4.820612  [ 8032/ 8081]

Avg training loss:        5.672857
Val loss: 5.530927  (hab: 9.154097, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.159212 → 9.154097). Saving model…
  new best habitat (9.154097) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 35/100
loss:        5.010150  [   32/ 8081]
loss:        4.668930  [  352/ 8081]
loss:        5.645073  [  672/ 8081]
loss:        5.488615  [  992/ 8081]
loss:        5.805965  [ 1312/ 8081]
loss:        6.627283  [ 1632/ 8081]
loss:        5.665799  [ 1952/ 8081]
loss:        5.894986  [ 2272/ 8081]
loss:        6.829393  [ 2592/ 8081]
loss:        5.456647  [ 2912/ 8081]
loss:        4.946234  [ 3232/ 8081]
loss:        4.547249  [ 3552/ 8081]
loss:        5.895032  [ 3872/ 8081]
loss:        6.116237  [ 4192/ 8081]
loss:        6.096629  [ 4512/ 8081]
loss:        5.625025  [ 4832/ 8081]
loss:        5.476202  [ 5152/ 8081]
loss:        5.392685  [ 5472/ 8081]
loss:        5.116040  [ 5792/ 8081]
loss:        5.943837  [ 6112/ 8081]
loss:        5.240388  [ 6432/ 8081]
loss:        5.591935  [ 6752/ 8081]
loss:        5.982331  [ 7072/ 8081]
loss:        5.391228  [ 7392/ 8081]
loss:        5.361643  [ 7712/ 8081]
loss:        5.721389  [ 8032/ 8081]

Avg training loss:        5.672867
Val loss: 5.530808  (hab: 9.148220, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.154097 → 9.148220). Saving model…
  new best habitat (9.148220) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 36/100
loss:        6.130375  [   32/ 8081]
loss:        6.370407  [  352/ 8081]
loss:        5.916040  [  672/ 8081]
loss:        5.343210  [  992/ 8081]
loss:        5.435072  [ 1312/ 8081]
loss:        5.641809  [ 1632/ 8081]
loss:        4.764863  [ 1952/ 8081]
loss:        6.302557  [ 2272/ 8081]
loss:        5.708288  [ 2592/ 8081]
loss:        6.304676  [ 2912/ 8081]
loss:        5.813638  [ 3232/ 8081]
loss:        5.873507  [ 3552/ 8081]
loss:        6.370828  [ 3872/ 8081]
loss:        5.393686  [ 4192/ 8081]
loss:        4.838700  [ 4512/ 8081]
loss:        4.930577  [ 4832/ 8081]
loss:        5.339914  [ 5152/ 8081]
loss:        5.559121  [ 5472/ 8081]
loss:        6.206648  [ 5792/ 8081]
loss:        5.213750  [ 6112/ 8081]
loss:        5.024416  [ 6432/ 8081]
loss:        5.026672  [ 6752/ 8081]
loss:        5.219367  [ 7072/ 8081]
loss:        5.678801  [ 7392/ 8081]
loss:        5.480056  [ 7712/ 8081]
loss:        6.451475  [ 8032/ 8081]

Avg training loss:        5.671136
Val loss: 5.530247  (hab: 9.156577, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 1/10

Epoch 37/100
loss:        6.288858  [   32/ 8081]
loss:        5.008344  [  352/ 8081]
loss:        5.845813  [  672/ 8081]
loss:        5.603943  [  992/ 8081]
loss:        6.017891  [ 1312/ 8081]
loss:        5.257273  [ 1632/ 8081]
loss:        5.471480  [ 1952/ 8081]
loss:        5.682749  [ 2272/ 8081]
loss:        5.949576  [ 2592/ 8081]
loss:        5.992301  [ 2912/ 8081]
loss:        4.490629  [ 3232/ 8081]
loss:        4.881692  [ 3552/ 8081]
loss:        5.130895  [ 3872/ 8081]
loss:        5.300719  [ 4192/ 8081]
loss:        4.955243  [ 4512/ 8081]
loss:        5.260942  [ 4832/ 8081]
loss:        5.672223  [ 5152/ 8081]
loss:        5.732101  [ 5472/ 8081]
loss:        5.437889  [ 5792/ 8081]
loss:        6.669717  [ 6112/ 8081]
loss:        5.325085  [ 6432/ 8081]
loss:        5.359512  [ 6752/ 8081]
loss:        5.250093  [ 7072/ 8081]
loss:        5.249368  [ 7392/ 8081]
loss:        5.474268  [ 7712/ 8081]
loss:        5.477094  [ 8032/ 8081]

Avg training loss:        5.668663
Val loss: 5.529146  (hab: 9.148766, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 2/10

Epoch 38/100
loss:        5.332274  [   32/ 8081]
loss:        5.085305  [  352/ 8081]
loss:        4.766563  [  672/ 8081]
loss:        5.723351  [  992/ 8081]
loss:        6.440807  [ 1312/ 8081]
loss:        6.003646  [ 1632/ 8081]
loss:        5.640621  [ 1952/ 8081]
loss:        6.396418  [ 2272/ 8081]
loss:        4.414577  [ 2592/ 8081]
loss:        5.727540  [ 2912/ 8081]
loss:        5.848539  [ 3232/ 8081]
loss:        6.443843  [ 3552/ 8081]
loss:        5.050669  [ 3872/ 8081]
loss:        5.805529  [ 4192/ 8081]
loss:        6.356796  [ 4512/ 8081]
loss:        5.703508  [ 4832/ 8081]
loss:        4.964018  [ 5152/ 8081]
loss:        5.215416  [ 5472/ 8081]
loss:        6.055335  [ 5792/ 8081]
loss:        5.204418  [ 6112/ 8081]
loss:        5.862002  [ 6432/ 8081]
loss:        5.250345  [ 6752/ 8081]
loss:        5.892265  [ 7072/ 8081]
loss:        5.110984  [ 7392/ 8081]
loss:        5.272231  [ 7712/ 8081]
loss:        6.261198  [ 8032/ 8081]

Avg training loss:        5.665581
Val loss: 5.527646  (hab: 9.137206, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
habitat validation loss decreased (9.148220 → 9.137206). Saving model…
  new best habitat (9.137206) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 39/100
loss:        5.036810  [   32/ 8081]
loss:        5.214328  [  352/ 8081]
loss:        5.196079  [  672/ 8081]
loss:        6.498976  [  992/ 8081]
loss:        5.602283  [ 1312/ 8081]
loss:        4.943366  [ 1632/ 8081]
loss:        6.416192  [ 1952/ 8081]
loss:        5.926390  [ 2272/ 8081]
loss:        6.419197  [ 2592/ 8081]
loss:        5.434674  [ 2912/ 8081]
loss:        6.340547  [ 3232/ 8081]
loss:        5.560699  [ 3552/ 8081]
loss:        5.837213  [ 3872/ 8081]
loss:        5.447258  [ 4192/ 8081]
loss:        5.665487  [ 4512/ 8081]
loss:        5.250518  [ 4832/ 8081]
loss:        5.200864  [ 5152/ 8081]
loss:        5.631220  [ 5472/ 8081]
loss:        5.811540  [ 5792/ 8081]
loss:        5.374275  [ 6112/ 8081]
loss:        5.321422  [ 6432/ 8081]
loss:        5.128225  [ 6752/ 8081]
loss:        5.160042  [ 7072/ 8081]
loss:        5.991592  [ 7392/ 8081]
loss:        6.914597  [ 7712/ 8081]
loss:        5.462342  [ 8032/ 8081]

Avg training loss:        5.662931
Val loss: 5.525425  (hab: 9.152211, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 1/10

Epoch 40/100
loss:        5.859059  [   32/ 8081]
loss:        5.727133  [  352/ 8081]
loss:        6.222307  [  672/ 8081]
loss:        5.249685  [  992/ 8081]
loss:        5.614727  [ 1312/ 8081]
loss:        5.405943  [ 1632/ 8081]
loss:        5.872947  [ 1952/ 8081]
loss:        5.284168  [ 2272/ 8081]
loss:        5.390779  [ 2592/ 8081]
loss:        4.930513  [ 2912/ 8081]
loss:        5.246387  [ 3232/ 8081]
loss:        6.977936  [ 3552/ 8081]
loss:        5.791405  [ 3872/ 8081]
loss:        4.733526  [ 4192/ 8081]
loss:        6.150668  [ 4512/ 8081]
loss:        5.957928  [ 4832/ 8081]
loss:        5.837332  [ 5152/ 8081]
loss:        5.881738  [ 5472/ 8081]
loss:        6.677011  [ 5792/ 8081]
loss:        4.890715  [ 6112/ 8081]
loss:        5.186887  [ 6432/ 8081]
loss:        5.368212  [ 6752/ 8081]
loss:        4.914428  [ 7072/ 8081]
loss:        5.095100  [ 7392/ 8081]
loss:        4.878122  [ 7712/ 8081]
loss:        6.786408  [ 8032/ 8081]

Avg training loss:        5.669070
Val loss: 5.523605  (hab: 9.142813, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 2/10

Epoch 41/100
loss:        6.311285  [   32/ 8081]
loss:        6.066874  [  352/ 8081]
loss:        5.395192  [  672/ 8081]
loss:        5.732865  [  992/ 8081]
loss:        5.333429  [ 1312/ 8081]
loss:        4.976534  [ 1632/ 8081]
loss:        6.537928  [ 1952/ 8081]
loss:        4.947510  [ 2272/ 8081]
loss:        5.961261  [ 2592/ 8081]
loss:        5.202216  [ 2912/ 8081]
loss:        6.103026  [ 3232/ 8081]
loss:        6.142170  [ 3552/ 8081]
loss:        5.247060  [ 3872/ 8081]
loss:        4.426946  [ 4192/ 8081]
loss:        5.769004  [ 4512/ 8081]
loss:        6.137143  [ 4832/ 8081]
loss:        6.728745  [ 5152/ 8081]
loss:        6.314181  [ 5472/ 8081]
loss:        6.429296  [ 5792/ 8081]
loss:        4.439626  [ 6112/ 8081]
loss:        5.653983  [ 6432/ 8081]
loss:        6.139348  [ 6752/ 8081]
loss:        5.559180  [ 7072/ 8081]
loss:        6.407027  [ 7392/ 8081]
loss:        5.876490  [ 7712/ 8081]
loss:        4.783723  [ 8032/ 8081]

Avg training loss:        5.666431
Val loss: 5.523897  (hab: 9.140973, mov: 5.579670)
Learning rates: hab: 1.000e-04, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 3/10

Epoch 42/100
loss:        7.022293  [   32/ 8081]
loss:        4.958503  [  352/ 8081]
loss:        5.281545  [  672/ 8081]
loss:        4.865613  [  992/ 8081]
loss:        5.947698  [ 1312/ 8081]
loss:        6.410549  [ 1632/ 8081]
loss:        5.014714  [ 1952/ 8081]
loss:        5.244724  [ 2272/ 8081]
loss:        5.399100  [ 2592/ 8081]
loss:        6.423370  [ 2912/ 8081]
loss:        5.884768  [ 3232/ 8081]
loss:        5.251521  [ 3552/ 8081]
loss:        5.333348  [ 3872/ 8081]
loss:        6.341861  [ 4192/ 8081]
loss:        5.252539  [ 4512/ 8081]
loss:        5.492242  [ 4832/ 8081]
loss:        5.852105  [ 5152/ 8081]
loss:        5.858068  [ 5472/ 8081]
loss:        5.670166  [ 5792/ 8081]
loss:        5.915601  [ 6112/ 8081]
loss:        5.949331  [ 6432/ 8081]
loss:        5.190772  [ 6752/ 8081]
loss:        5.998057  [ 7072/ 8081]
loss:        5.033205  [ 7392/ 8081]
loss:        5.237436  [ 7712/ 8081]
loss:        5.728716  [ 8032/ 8081]

Avg training loss:        5.662187
Val loss: 5.521639  (hab: 9.142217, mov: 5.579670)
Learning rates: hab: 5.000e-05 (decreased from 1.000e-04), mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 4/10

Epoch 43/100
loss:        5.897785  [   32/ 8081]
loss:        5.672285  [  352/ 8081]
loss:        5.327921  [  672/ 8081]
loss:        4.898768  [  992/ 8081]
loss:        5.588938  [ 1312/ 8081]
loss:        6.266418  [ 1632/ 8081]
loss:        6.022236  [ 1952/ 8081]
loss:        5.578998  [ 2272/ 8081]
loss:        6.777715  [ 2592/ 8081]
loss:        6.006792  [ 2912/ 8081]
loss:        5.763668  [ 3232/ 8081]
loss:        5.005836  [ 3552/ 8081]
loss:        5.465352  [ 3872/ 8081]
loss:        5.495025  [ 4192/ 8081]
loss:        5.649711  [ 4512/ 8081]
loss:        5.712841  [ 4832/ 8081]
loss:        5.768684  [ 5152/ 8081]
loss:        5.523283  [ 5472/ 8081]
loss:        5.807250  [ 5792/ 8081]
loss:        5.314686  [ 6112/ 8081]
loss:        5.926568  [ 6432/ 8081]
loss:        5.652478  [ 6752/ 8081]
loss:        5.772177  [ 7072/ 8081]
loss:        6.158460  [ 7392/ 8081]
loss:        5.702784  [ 7712/ 8081]
loss:        5.738538  [ 8032/ 8081]

Avg training loss:        5.669862
Val loss: 5.522019  (hab: 9.134058, mov: 5.579670)
Learning rates: hab: 5.000e-05, mov: 1.000e-05
habitat validation loss decreased (9.137206 → 9.134058). Saving model…
  new best habitat (9.134058) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 44/100
loss:        5.144200  [   32/ 8081]
loss:        5.742032  [  352/ 8081]
loss:        4.436221  [  672/ 8081]
loss:        5.758897  [  992/ 8081]
loss:        5.401932  [ 1312/ 8081]
loss:        5.968377  [ 1632/ 8081]
loss:        6.337979  [ 1952/ 8081]
loss:        5.330321  [ 2272/ 8081]
loss:        5.351970  [ 2592/ 8081]
loss:        4.840862  [ 2912/ 8081]
loss:        4.333344  [ 3232/ 8081]
loss:        5.349402  [ 3552/ 8081]
loss:        5.831436  [ 3872/ 8081]
loss:        5.939790  [ 4192/ 8081]
loss:        5.517396  [ 4512/ 8081]
loss:        6.192120  [ 4832/ 8081]
loss:        5.360706  [ 5152/ 8081]
loss:        4.804186  [ 5472/ 8081]
loss:        6.103284  [ 5792/ 8081]
loss:        6.558006  [ 6112/ 8081]
loss:        5.876123  [ 6432/ 8081]
loss:        5.092310  [ 6752/ 8081]
loss:        5.548155  [ 7072/ 8081]
loss:        7.097572  [ 7392/ 8081]
loss:        5.171304  [ 7712/ 8081]
loss:        5.313572  [ 8032/ 8081]

Avg training loss:        5.656315
Val loss: 5.520759  (hab: 9.131758, mov: 5.579670)
Learning rates: hab: 5.000e-05, mov: 1.000e-05
habitat validation loss decreased (9.134058 → 9.131758). Saving model…
  new best habitat (9.131758) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 45/100
loss:        5.321779  [   32/ 8081]
loss:        4.786499  [  352/ 8081]
loss:        5.706874  [  672/ 8081]
loss:        6.282902  [  992/ 8081]
loss:        5.710300  [ 1312/ 8081]
loss:        5.336893  [ 1632/ 8081]
loss:        5.029823  [ 1952/ 8081]
loss:        5.340590  [ 2272/ 8081]
loss:        6.537576  [ 2592/ 8081]
loss:        6.552740  [ 2912/ 8081]
loss:        6.491404  [ 3232/ 8081]
loss:        4.738231  [ 3552/ 8081]
loss:        5.647418  [ 3872/ 8081]
loss:        5.685692  [ 4192/ 8081]
loss:        5.467692  [ 4512/ 8081]
loss:        6.517156  [ 4832/ 8081]
loss:        4.818586  [ 5152/ 8081]
loss:        5.939748  [ 5472/ 8081]
loss:        6.889446  [ 5792/ 8081]
loss:        5.738359  [ 6112/ 8081]
loss:        5.694431  [ 6432/ 8081]
loss:        5.811762  [ 6752/ 8081]
loss:        5.724278  [ 7072/ 8081]
loss:        5.231427  [ 7392/ 8081]
loss:        5.266511  [ 7712/ 8081]
loss:        4.847194  [ 8032/ 8081]

Avg training loss:        5.660581
Val loss: 5.522113  (hab: 9.137865, mov: 5.579670)
Learning rates: hab: 5.000e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 1/10

Epoch 46/100
loss:        5.895260  [   32/ 8081]
loss:        5.261802  [  352/ 8081]
loss:        6.264820  [  672/ 8081]
loss:        5.382800  [  992/ 8081]
loss:        5.410489  [ 1312/ 8081]
loss:        4.716128  [ 1632/ 8081]
loss:        4.244218  [ 1952/ 8081]
loss:        6.247499  [ 2272/ 8081]
loss:        5.262657  [ 2592/ 8081]
loss:        5.530381  [ 2912/ 8081]
loss:        6.006838  [ 3232/ 8081]
loss:        5.443788  [ 3552/ 8081]
loss:        5.650639  [ 3872/ 8081]
loss:        5.834536  [ 4192/ 8081]
loss:        5.532172  [ 4512/ 8081]
loss:        5.138124  [ 4832/ 8081]
loss:        5.866557  [ 5152/ 8081]
loss:        5.728910  [ 5472/ 8081]
loss:        5.309165  [ 5792/ 8081]
loss:        4.840456  [ 6112/ 8081]
loss:        6.076933  [ 6432/ 8081]
loss:        5.810665  [ 6752/ 8081]
loss:        5.277090  [ 7072/ 8081]
loss:        5.757834  [ 7392/ 8081]
loss:        6.445642  [ 7712/ 8081]
loss:        6.486310  [ 8032/ 8081]

Avg training loss:        5.661636
Val loss: 5.520749  (hab: 9.137481, mov: 5.579670)
Learning rates: hab: 5.000e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 2/10

Epoch 47/100
loss:        6.304659  [   32/ 8081]
loss:        5.796867  [  352/ 8081]
loss:        5.541860  [  672/ 8081]
loss:        5.390038  [  992/ 8081]
loss:        6.184792  [ 1312/ 8081]
loss:        5.310475  [ 1632/ 8081]
loss:        5.893677  [ 1952/ 8081]
loss:        5.905855  [ 2272/ 8081]
loss:        5.054792  [ 2592/ 8081]
loss:        5.395776  [ 2912/ 8081]
loss:        5.693206  [ 3232/ 8081]
loss:        5.116519  [ 3552/ 8081]
loss:        6.109075  [ 3872/ 8081]
loss:        5.326317  [ 4192/ 8081]
loss:        5.057263  [ 4512/ 8081]
loss:        4.677438  [ 4832/ 8081]
loss:        6.563509  [ 5152/ 8081]
loss:        5.663967  [ 5472/ 8081]
loss:        6.114722  [ 5792/ 8081]
loss:        6.515405  [ 6112/ 8081]
loss:        6.340902  [ 6432/ 8081]
loss:        5.208425  [ 6752/ 8081]
loss:        6.314600  [ 7072/ 8081]
loss:        5.433854  [ 7392/ 8081]
loss:        5.811426  [ 7712/ 8081]
loss:        6.017889  [ 8032/ 8081]

Avg training loss:        5.658509
Val loss: 5.521224  (hab: 9.137274, mov: 5.579670)
Learning rates: hab: 5.000e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 3/10

Epoch 48/100
loss:        6.198850  [   32/ 8081]
loss:        6.260042  [  352/ 8081]
loss:        6.042982  [  672/ 8081]
loss:        5.962645  [  992/ 8081]
loss:        6.017270  [ 1312/ 8081]
loss:        5.059440  [ 1632/ 8081]
loss:        5.005844  [ 1952/ 8081]
loss:        5.440996  [ 2272/ 8081]
loss:        4.730888  [ 2592/ 8081]
loss:        4.997714  [ 2912/ 8081]
loss:        5.243274  [ 3232/ 8081]
loss:        6.082825  [ 3552/ 8081]
loss:        5.545503  [ 3872/ 8081]
loss:        5.873924  [ 4192/ 8081]
loss:        5.622471  [ 4512/ 8081]
loss:        5.532614  [ 4832/ 8081]
loss:        6.518985  [ 5152/ 8081]
loss:        6.284297  [ 5472/ 8081]
loss:        5.568346  [ 5792/ 8081]
loss:        5.737114  [ 6112/ 8081]
loss:        5.000188  [ 6432/ 8081]
loss:        6.632066  [ 6752/ 8081]
loss:        6.149480  [ 7072/ 8081]
loss:        6.410388  [ 7392/ 8081]
loss:        4.295050  [ 7712/ 8081]
loss:        5.041851  [ 8032/ 8081]

Avg training loss:        5.661953
Val loss: 5.520298  (hab: 9.136897, mov: 5.579670)
Learning rates: hab: 2.500e-05 (decreased from 5.000e-05), mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 4/10

Epoch 49/100
loss:        5.682422  [   32/ 8081]
loss:        5.673647  [  352/ 8081]
loss:        5.606563  [  672/ 8081]
loss:        5.270475  [  992/ 8081]
loss:        5.969738  [ 1312/ 8081]
loss:        5.941535  [ 1632/ 8081]
loss:        5.315184  [ 1952/ 8081]
loss:        5.168659  [ 2272/ 8081]
loss:        6.108777  [ 2592/ 8081]
loss:        5.364019  [ 2912/ 8081]
loss:        5.538062  [ 3232/ 8081]
loss:        4.487485  [ 3552/ 8081]
loss:        5.927518  [ 3872/ 8081]
loss:        5.518707  [ 4192/ 8081]
loss:        5.251425  [ 4512/ 8081]
loss:        5.860307  [ 4832/ 8081]
loss:        5.680009  [ 5152/ 8081]
loss:        6.209067  [ 5472/ 8081]
loss:        5.564365  [ 5792/ 8081]
loss:        5.377419  [ 6112/ 8081]
loss:        4.921079  [ 6432/ 8081]
loss:        5.433037  [ 6752/ 8081]
loss:        6.670821  [ 7072/ 8081]
loss:        5.735277  [ 7392/ 8081]
loss:        4.992699  [ 7712/ 8081]
loss:        5.868934  [ 8032/ 8081]

Avg training loss:        5.656242
Val loss: 5.518995  (hab: 9.129542, mov: 5.579670)
Learning rates: hab: 2.500e-05, mov: 1.000e-05
habitat validation loss decreased (9.131758 → 9.129542). Saving model…
  new best habitat (9.129542) → outputs/best_model_habitat.pt
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10

Epoch 50/100
loss:        5.980393  [   32/ 8081]
loss:        5.974240  [  352/ 8081]
loss:        5.108555  [  672/ 8081]
loss:        5.429205  [  992/ 8081]
loss:        5.607038  [ 1312/ 8081]
loss:        5.353492  [ 1632/ 8081]
loss:        5.320700  [ 1952/ 8081]
loss:        5.164296  [ 2272/ 8081]
loss:        5.844227  [ 2592/ 8081]
loss:        5.533587  [ 2912/ 8081]
loss:        6.317193  [ 3232/ 8081]
loss:        6.733093  [ 3552/ 8081]
loss:        6.710370  [ 3872/ 8081]
loss:        5.678350  [ 4192/ 8081]
loss:        5.354743  [ 4512/ 8081]
loss:        5.890395  [ 4832/ 8081]
loss:        5.261918  [ 5152/ 8081]
loss:        5.824592  [ 5472/ 8081]
loss:        6.052550  [ 5792/ 8081]
loss:        6.458363  [ 6112/ 8081]
loss:        6.473911  [ 6432/ 8081]
loss:        4.700408  [ 6752/ 8081]
loss:        6.207803  [ 7072/ 8081]
loss:        5.862787  [ 7392/ 8081]
loss:        4.907096  [ 7712/ 8081]
loss:        7.442554  [ 8032/ 8081]

Avg training loss:        5.659665
Val loss: 5.519545  (hab: 9.135691, mov: 5.579670)
Learning rates: hab: 2.500e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 1/10

Epoch 51/100
loss:        5.338743  [   32/ 8081]
loss:        5.476829  [  352/ 8081]
loss:        5.889359  [  672/ 8081]
loss:        6.332553  [  992/ 8081]
loss:        5.301053  [ 1312/ 8081]
loss:        5.541804  [ 1632/ 8081]
loss:        5.993145  [ 1952/ 8081]
loss:        6.552125  [ 2272/ 8081]
loss:        5.131002  [ 2592/ 8081]
loss:        5.600188  [ 2912/ 8081]
loss:        6.154308  [ 3232/ 8081]
loss:        4.159619  [ 3552/ 8081]
loss:        6.026532  [ 3872/ 8081]
loss:        5.723530  [ 4192/ 8081]
loss:        4.628966  [ 4512/ 8081]
loss:        4.942873  [ 4832/ 8081]
loss:        5.240351  [ 5152/ 8081]
loss:        5.670751  [ 5472/ 8081]
loss:        5.054800  [ 5792/ 8081]
loss:        5.610710  [ 6112/ 8081]
loss:        4.586905  [ 6432/ 8081]
loss:        6.045436  [ 6752/ 8081]
loss:        6.677966  [ 7072/ 8081]
loss:        5.073793  [ 7392/ 8081]
loss:        5.985007  [ 7712/ 8081]
loss:        6.399843  [ 8032/ 8081]

Avg training loss:        5.659743
Val loss: 5.519263  (hab: 9.135848, mov: 5.579670)
Learning rates: hab: 2.500e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 2/10

Epoch 52/100
loss:        5.509240  [   32/ 8081]
loss:        5.768742  [  352/ 8081]
loss:        5.717967  [  672/ 8081]
loss:        4.979650  [  992/ 8081]
loss:        4.932138  [ 1312/ 8081]
loss:        6.170556  [ 1632/ 8081]
loss:        5.494575  [ 1952/ 8081]
loss:        4.683487  [ 2272/ 8081]
loss:        5.762437  [ 2592/ 8081]
loss:        5.329520  [ 2912/ 8081]
loss:        5.531595  [ 3232/ 8081]
loss:        6.179363  [ 3552/ 8081]
loss:        5.711234  [ 3872/ 8081]
loss:        6.204116  [ 4192/ 8081]
loss:        4.520830  [ 4512/ 8081]
loss:        5.107217  [ 4832/ 8081]
loss:        6.276742  [ 5152/ 8081]
loss:        5.131181  [ 5472/ 8081]
loss:        5.793100  [ 5792/ 8081]
loss:        5.497476  [ 6112/ 8081]
loss:        5.419806  [ 6432/ 8081]
loss:        5.835128  [ 6752/ 8081]
loss:        5.994698  [ 7072/ 8081]
loss:        6.206824  [ 7392/ 8081]
loss:        5.019129  [ 7712/ 8081]
loss:        6.163091  [ 8032/ 8081]

Avg training loss:        5.663972
Val loss: 5.518810  (hab: 9.133638, mov: 5.579670)
Learning rates: hab: 2.500e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 3/10

Epoch 53/100
loss:        5.460737  [   32/ 8081]
loss:        5.833458  [  352/ 8081]
loss:        5.814044  [  672/ 8081]
loss:        5.924604  [  992/ 8081]
loss:        5.785505  [ 1312/ 8081]
loss:        5.733563  [ 1632/ 8081]
loss:        4.972561  [ 1952/ 8081]
loss:        6.583223  [ 2272/ 8081]
loss:        5.318804  [ 2592/ 8081]
loss:        6.167712  [ 2912/ 8081]
loss:        4.348044  [ 3232/ 8081]
loss:        6.329242  [ 3552/ 8081]
loss:        5.948264  [ 3872/ 8081]
loss:        5.519669  [ 4192/ 8081]
loss:        6.624630  [ 4512/ 8081]
loss:        6.020040  [ 4832/ 8081]
loss:        5.040162  [ 5152/ 8081]
loss:        4.678307  [ 5472/ 8081]
loss:        6.468158  [ 5792/ 8081]
loss:        5.195184  [ 6112/ 8081]
loss:        6.419618  [ 6432/ 8081]
loss:        5.561605  [ 6752/ 8081]
loss:        6.902629  [ 7072/ 8081]
loss:        5.520972  [ 7392/ 8081]
loss:        5.146769  [ 7712/ 8081]
loss:        6.686140  [ 8032/ 8081]

Avg training loss:        5.662129
Val loss: 5.519124  (hab: 9.136573, mov: 5.579670)
Learning rates: hab: 1.250e-05 (decreased from 2.500e-05), mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 4/10

Epoch 54/100
loss:        5.555470  [   32/ 8081]
loss:        6.222959  [  352/ 8081]
loss:        5.614961  [  672/ 8081]
loss:        6.032071  [  992/ 8081]
loss:        5.512796  [ 1312/ 8081]
loss:        5.181257  [ 1632/ 8081]
loss:        4.813510  [ 1952/ 8081]
loss:        5.833237  [ 2272/ 8081]
loss:        5.407711  [ 2592/ 8081]
loss:        5.155222  [ 2912/ 8081]
loss:        5.407894  [ 3232/ 8081]
loss:        6.002144  [ 3552/ 8081]
loss:        5.950695  [ 3872/ 8081]
loss:        6.104459  [ 4192/ 8081]
loss:        5.992763  [ 4512/ 8081]
loss:        5.734492  [ 4832/ 8081]
loss:        5.803051  [ 5152/ 8081]
loss:        6.371073  [ 5472/ 8081]
loss:        4.602274  [ 5792/ 8081]
loss:        5.662807  [ 6112/ 8081]
loss:        5.207982  [ 6432/ 8081]
loss:        5.759853  [ 6752/ 8081]
loss:        5.923870  [ 7072/ 8081]
loss:        5.199556  [ 7392/ 8081]
loss:        4.353986  [ 7712/ 8081]
loss:        5.073519  [ 8032/ 8081]

Avg training loss:        5.657494
Val loss: 5.518244  (hab: 9.132512, mov: 5.579670)
Learning rates: hab: 1.250e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 5/10

Epoch 55/100
loss:        5.459167  [   32/ 8081]
loss:        6.259498  [  352/ 8081]
loss:        5.463336  [  672/ 8081]
loss:        5.829850  [  992/ 8081]
loss:        5.940257  [ 1312/ 8081]
loss:        6.074276  [ 1632/ 8081]
loss:        6.311945  [ 1952/ 8081]
loss:        5.791299  [ 2272/ 8081]
loss:        5.056565  [ 2592/ 8081]
loss:        5.815616  [ 2912/ 8081]
loss:        6.905226  [ 3232/ 8081]
loss:        5.808208  [ 3552/ 8081]
loss:        4.808090  [ 3872/ 8081]
loss:        5.003482  [ 4192/ 8081]
loss:        6.047105  [ 4512/ 8081]
loss:        5.362399  [ 4832/ 8081]
loss:        5.538964  [ 5152/ 8081]
loss:        5.649759  [ 5472/ 8081]
loss:        5.025969  [ 5792/ 8081]
loss:        5.664685  [ 6112/ 8081]
loss:        6.184643  [ 6432/ 8081]
loss:        5.836655  [ 6752/ 8081]
loss:        6.667353  [ 7072/ 8081]
loss:        6.029863  [ 7392/ 8081]
loss:        5.594702  [ 7712/ 8081]
loss:        6.485946  [ 8032/ 8081]

Avg training loss:        5.660229
Val loss: 5.518536  (hab: 9.134029, mov: 5.579670)
Learning rates: hab: 1.250e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 6/10

Epoch 56/100
loss:        6.565683  [   32/ 8081]
loss:        4.997747  [  352/ 8081]
loss:        6.010538  [  672/ 8081]
loss:        4.935389  [  992/ 8081]
loss:        5.062060  [ 1312/ 8081]
loss:        4.412249  [ 1632/ 8081]
loss:        5.811884  [ 1952/ 8081]
loss:        6.564093  [ 2272/ 8081]
loss:        6.007530  [ 2592/ 8081]
loss:        5.053657  [ 2912/ 8081]
loss:        5.330781  [ 3232/ 8081]
loss:        5.289261  [ 3552/ 8081]
loss:        5.702361  [ 3872/ 8081]
loss:        6.052120  [ 4192/ 8081]
loss:        5.945775  [ 4512/ 8081]
loss:        5.445115  [ 4832/ 8081]
loss:        4.999338  [ 5152/ 8081]
loss:        5.374511  [ 5472/ 8081]
loss:        6.145279  [ 5792/ 8081]
loss:        6.679270  [ 6112/ 8081]
loss:        5.382257  [ 6432/ 8081]
loss:        5.827774  [ 6752/ 8081]
loss:        5.786506  [ 7072/ 8081]
loss:        4.603727  [ 7392/ 8081]
loss:        6.450315  [ 7712/ 8081]
loss:        5.518553  [ 8032/ 8081]

Avg training loss:        5.655860
Val loss: 5.518564  (hab: 9.135832, mov: 5.579670)
Learning rates: hab: 1.250e-05, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 7/10

Epoch 57/100
loss:        5.798457  [   32/ 8081]
loss:        5.611054  [  352/ 8081]
loss:        5.758144  [  672/ 8081]
loss:        5.373639  [  992/ 8081]
loss:        5.090552  [ 1312/ 8081]
loss:        6.772118  [ 1632/ 8081]
loss:        6.156831  [ 1952/ 8081]
loss:        5.419808  [ 2272/ 8081]
loss:        5.474562  [ 2592/ 8081]
loss:        5.882651  [ 2912/ 8081]
loss:        7.493993  [ 3232/ 8081]
loss:        5.018743  [ 3552/ 8081]
loss:        5.731715  [ 3872/ 8081]
loss:        5.506112  [ 4192/ 8081]
loss:        5.033979  [ 4512/ 8081]
loss:        5.185174  [ 4832/ 8081]
loss:        6.339293  [ 5152/ 8081]
loss:        4.704543  [ 5472/ 8081]
loss:        5.679595  [ 5792/ 8081]
loss:        6.401530  [ 6112/ 8081]
loss:        5.995231  [ 6432/ 8081]
loss:        5.587892  [ 6752/ 8081]
loss:        5.241594  [ 7072/ 8081]
loss:        5.977066  [ 7392/ 8081]
loss:        5.315480  [ 7712/ 8081]
loss:        5.359548  [ 8032/ 8081]

Avg training loss:        5.656932
Val loss: 5.518333  (hab: 9.133270, mov: 5.579670)
Learning rates: hab: 6.250e-06 (decreased from 1.250e-05), mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 8/10

Epoch 58/100
loss:        5.301577  [   32/ 8081]
loss:        5.709770  [  352/ 8081]
loss:        4.922097  [  672/ 8081]
loss:        5.525185  [  992/ 8081]
loss:        5.832800  [ 1312/ 8081]
loss:        6.304031  [ 1632/ 8081]
loss:        5.335675  [ 1952/ 8081]
loss:        5.397144  [ 2272/ 8081]
loss:        5.752774  [ 2592/ 8081]
loss:        5.386626  [ 2912/ 8081]
loss:        4.946604  [ 3232/ 8081]
loss:        5.457506  [ 3552/ 8081]
loss:        5.958965  [ 3872/ 8081]
loss:        5.122050  [ 4192/ 8081]
loss:        6.117485  [ 4512/ 8081]
loss:        6.368277  [ 4832/ 8081]
loss:        5.227844  [ 5152/ 8081]
loss:        4.467236  [ 5472/ 8081]
loss:        6.337330  [ 5792/ 8081]
loss:        6.662388  [ 6112/ 8081]
loss:        6.050961  [ 6432/ 8081]
loss:        5.217427  [ 6752/ 8081]
loss:        5.992336  [ 7072/ 8081]
loss:        6.146977  [ 7392/ 8081]
loss:        5.438122  [ 7712/ 8081]
loss:        5.349228  [ 8032/ 8081]

Avg training loss:        5.657058
Val loss: 5.518242  (hab: 9.134342, mov: 5.579670)
Learning rates: hab: 6.250e-06, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 9/10

Epoch 59/100
loss:        4.769571  [   32/ 8081]
loss:        6.386826  [  352/ 8081]
loss:        5.683900  [  672/ 8081]
loss:        5.718356  [  992/ 8081]
loss:        5.141163  [ 1312/ 8081]
loss:        6.759483  [ 1632/ 8081]
loss:        5.562092  [ 1952/ 8081]
loss:        5.965920  [ 2272/ 8081]
loss:        5.156981  [ 2592/ 8081]
loss:        5.400375  [ 2912/ 8081]
loss:        6.088895  [ 3232/ 8081]
loss:        6.194445  [ 3552/ 8081]
loss:        5.424789  [ 3872/ 8081]
loss:        6.326275  [ 4192/ 8081]
loss:        5.583797  [ 4512/ 8081]
loss:        5.483855  [ 4832/ 8081]
loss:        5.527748  [ 5152/ 8081]
loss:        4.923814  [ 5472/ 8081]
loss:        5.744041  [ 5792/ 8081]
loss:        5.269781  [ 6112/ 8081]
loss:        6.041680  [ 6432/ 8081]
loss:        5.430574  [ 6752/ 8081]
loss:        6.336226  [ 7072/ 8081]
loss:        5.880184  [ 7392/ 8081]
loss:        6.204988  [ 7712/ 8081]
loss:        5.573607  [ 8032/ 8081]

Avg training loss:        5.653561
Val loss: 5.518234  (hab: 9.135071, mov: 5.579670)
Learning rates: hab: 6.250e-06, mov: 1.000e-05
  new best movement (5.579670) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 10/10
Stage 2 converged at epoch 59; moving on.

=== Stage 3/3: training habitat + movement for up to 30 epoch(s) ===

Epoch 60/100
loss:        6.165023  [   32/ 8081]
loss:        5.879417  [  352/ 8081]
loss:        5.362621  [  672/ 8081]
loss:        5.935786  [  992/ 8081]
loss:        5.434112  [ 1312/ 8081]
loss:        5.185086  [ 1632/ 8081]
loss:        5.615590  [ 1952/ 8081]
loss:        5.797837  [ 2272/ 8081]
loss:        5.111373  [ 2592/ 8081]
loss:        5.984820  [ 2912/ 8081]
loss:        5.352618  [ 3232/ 8081]
loss:        5.620452  [ 3552/ 8081]
loss:        6.079937  [ 3872/ 8081]
loss:        5.364606  [ 4192/ 8081]
loss:        5.305182  [ 4512/ 8081]
loss:        6.150834  [ 4832/ 8081]
loss:        6.211440  [ 5152/ 8081]
loss:        6.050583  [ 5472/ 8081]
loss:        6.314599  [ 5792/ 8081]
loss:        5.835132  [ 6112/ 8081]
loss:        5.460766  [ 6432/ 8081]
loss:        4.768877  [ 6752/ 8081]
loss:        4.685184  [ 7072/ 8081]
loss:        6.400288  [ 7392/ 8081]
loss:        5.338955  [ 7712/ 8081]
loss:        5.818192  [ 8032/ 8081]

Avg training loss:        5.652285
Val loss: 5.515719  (hab: 9.134556, mov: 5.577937)
Learning rates: hab: 6.250e-06, mov: 1.000e-05
total validation loss decreased (inf → 5.515719). Saving model…
  new best movement (5.577937) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10, movement: 0/10

Epoch 61/100
loss:        6.343354  [   32/ 8081]
loss:        5.093129  [  352/ 8081]
loss:        6.003186  [  672/ 8081]
loss:        5.373261  [  992/ 8081]
loss:        5.269666  [ 1312/ 8081]
loss:        5.868841  [ 1632/ 8081]
loss:        5.889802  [ 1952/ 8081]
loss:        6.029429  [ 2272/ 8081]
loss:        5.674853  [ 2592/ 8081]
loss:        6.324826  [ 2912/ 8081]
loss:        5.851480  [ 3232/ 8081]
loss:        6.060325  [ 3552/ 8081]
loss:        6.360299  [ 3872/ 8081]
loss:        5.836318  [ 4192/ 8081]
loss:        5.617624  [ 4512/ 8081]
loss:        5.992677  [ 4832/ 8081]
loss:        5.858509  [ 5152/ 8081]
loss:        5.061381  [ 5472/ 8081]
loss:        4.864799  [ 5792/ 8081]
loss:        5.259315  [ 6112/ 8081]
loss:        5.312628  [ 6432/ 8081]
loss:        5.680539  [ 6752/ 8081]
loss:        5.380571  [ 7072/ 8081]
loss:        5.543283  [ 7392/ 8081]
loss:        5.013337  [ 7712/ 8081]
loss:        6.030162  [ 8032/ 8081]

Avg training loss:        5.651148
Val loss: 5.511400  (hab: 9.133363, mov: 5.573617)
Learning rates: hab: 3.125e-06 (decreased from 6.250e-06), mov: 1.000e-05
total validation loss decreased (5.515719 → 5.511400). Saving model…
  new best movement (5.573617) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 0/10, movement: 0/10

Epoch 62/100
loss:        5.352799  [   32/ 8081]
loss:        6.083902  [  352/ 8081]
loss:        5.679685  [  672/ 8081]
loss:        5.464170  [  992/ 8081]
loss:        5.426394  [ 1312/ 8081]
loss:        6.225701  [ 1632/ 8081]
loss:        5.303734  [ 1952/ 8081]
loss:        6.092421  [ 2272/ 8081]
loss:        6.391000  [ 2592/ 8081]
loss:        4.359735  [ 2912/ 8081]
loss:        5.927668  [ 3232/ 8081]
loss:        5.139613  [ 3552/ 8081]
loss:        5.359861  [ 3872/ 8081]
loss:        4.984237  [ 4192/ 8081]
loss:        6.220632  [ 4512/ 8081]
loss:        4.755053  [ 4832/ 8081]
loss:        6.100753  [ 5152/ 8081]
loss:        6.382903  [ 5472/ 8081]
loss:        6.415565  [ 5792/ 8081]
loss:        5.246841  [ 6112/ 8081]
loss:        5.478046  [ 6432/ 8081]
loss:        4.806028  [ 6752/ 8081]
loss:        5.138412  [ 7072/ 8081]
loss:        4.934842  [ 7392/ 8081]
loss:        5.642243  [ 7712/ 8081]
loss:        5.980451  [ 8032/ 8081]

Avg training loss:        5.640719
Val loss: 5.506524  (hab: 9.133679, mov: 5.568254)
Learning rates: hab: 3.125e-06, mov: 1.000e-05
total validation loss decreased (5.511400 → 5.506524). Saving model…
  new best movement (5.568254) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 1/10, movement: 0/10

Epoch 63/100
loss:        6.187352  [   32/ 8081]
loss:        5.992198  [  352/ 8081]
loss:        5.431767  [  672/ 8081]
loss:        5.908827  [  992/ 8081]
loss:        6.132372  [ 1312/ 8081]
loss:        5.604450  [ 1632/ 8081]
loss:        5.973135  [ 1952/ 8081]
loss:        6.166081  [ 2272/ 8081]
loss:        5.717855  [ 2592/ 8081]
loss:        5.540296  [ 2912/ 8081]
loss:        5.827331  [ 3232/ 8081]
loss:        5.792781  [ 3552/ 8081]
loss:        5.700490  [ 3872/ 8081]
loss:        4.926766  [ 4192/ 8081]
loss:        5.301070  [ 4512/ 8081]
loss:        5.662399  [ 4832/ 8081]
loss:        5.893357  [ 5152/ 8081]
loss:        6.183538  [ 5472/ 8081]
loss:        5.335289  [ 5792/ 8081]
loss:        5.978237  [ 6112/ 8081]
loss:        5.877348  [ 6432/ 8081]
loss:        5.232896  [ 6752/ 8081]
loss:        5.951500  [ 7072/ 8081]
loss:        5.269121  [ 7392/ 8081]
loss:        5.096188  [ 7712/ 8081]
loss:        4.431066  [ 8032/ 8081]

Avg training loss:        5.646483
Val loss: 5.501691  (hab: 9.134011, mov: 5.562999)
Learning rates: hab: 3.125e-06, mov: 1.000e-05
total validation loss decreased (5.506524 → 5.501691). Saving model…
  new best movement (5.562999) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 2/10, movement: 0/10

Epoch 64/100
loss:        5.221969  [   32/ 8081]
loss:        5.446278  [  352/ 8081]
loss:        5.998045  [  672/ 8081]
loss:        6.537796  [  992/ 8081]
loss:        4.759778  [ 1312/ 8081]
loss:        5.479456  [ 1632/ 8081]
loss:        5.596062  [ 1952/ 8081]
loss:        5.838643  [ 2272/ 8081]
loss:        5.160219  [ 2592/ 8081]
loss:        5.415286  [ 2912/ 8081]
loss:        5.193935  [ 3232/ 8081]
loss:        5.155421  [ 3552/ 8081]
loss:        6.389012  [ 3872/ 8081]
loss:        6.380416  [ 4192/ 8081]
loss:        5.833337  [ 4512/ 8081]
loss:        5.342879  [ 4832/ 8081]
loss:        5.440716  [ 5152/ 8081]
loss:        4.224050  [ 5472/ 8081]
loss:        4.927687  [ 5792/ 8081]
loss:        5.468663  [ 6112/ 8081]
loss:        6.806590  [ 6432/ 8081]
loss:        5.718962  [ 6752/ 8081]
loss:        6.156057  [ 7072/ 8081]
loss:        6.420247  [ 7392/ 8081]
loss:        5.684424  [ 7712/ 8081]
loss:        5.963620  [ 8032/ 8081]

Avg training loss:        5.639883
Val loss: 5.500701  (hab: 9.134093, mov: 5.562976)
Learning rates: hab: 3.125e-06, mov: 1.000e-05
total validation loss decreased (5.501691 → 5.500701). Saving model…
  new best movement (5.562976) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 3/10, movement: 0/10

Epoch 65/100
loss:        5.450703  [   32/ 8081]
loss:        6.007255  [  352/ 8081]
loss:        5.162724  [  672/ 8081]
loss:        5.348856  [  992/ 8081]
loss:        5.702245  [ 1312/ 8081]
loss:        5.860806  [ 1632/ 8081]
loss:        5.661339  [ 1952/ 8081]
loss:        5.777259  [ 2272/ 8081]
loss:        6.220114  [ 2592/ 8081]
loss:        6.079422  [ 2912/ 8081]
loss:        6.010940  [ 3232/ 8081]
loss:        5.403613  [ 3552/ 8081]
loss:        5.130002  [ 3872/ 8081]
loss:        5.848298  [ 4192/ 8081]
loss:        5.230469  [ 4512/ 8081]
loss:        5.358596  [ 4832/ 8081]
loss:        6.310765  [ 5152/ 8081]
loss:        4.936250  [ 5472/ 8081]
loss:        5.965114  [ 5792/ 8081]
loss:        5.401882  [ 6112/ 8081]
loss:        6.532587  [ 6432/ 8081]
loss:        4.725352  [ 6752/ 8081]
loss:        6.419041  [ 7072/ 8081]
loss:        5.760632  [ 7392/ 8081]
loss:        5.184685  [ 7712/ 8081]
loss:        5.718621  [ 8032/ 8081]

Avg training loss:        5.636266
Val loss: 5.493873  (hab: 9.133855, mov: 5.555220)
Learning rates: hab: 1.563e-06 (decreased from 3.125e-06), mov: 1.000e-05
total validation loss decreased (5.500701 → 5.493873). Saving model…
  new best movement (5.555220) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 4/10, movement: 0/10

Epoch 66/100
loss:        5.790228  [   32/ 8081]
loss:        5.306352  [  352/ 8081]
loss:        5.198020  [  672/ 8081]
loss:        6.348582  [  992/ 8081]
loss:        6.620947  [ 1312/ 8081]
loss:        5.174995  [ 1632/ 8081]
loss:        5.159679  [ 1952/ 8081]
loss:        5.835268  [ 2272/ 8081]
loss:        5.638731  [ 2592/ 8081]
loss:        5.593339  [ 2912/ 8081]
loss:        5.795867  [ 3232/ 8081]
loss:        6.072206  [ 3552/ 8081]
loss:        5.239961  [ 3872/ 8081]
loss:        5.769509  [ 4192/ 8081]
loss:        5.999186  [ 4512/ 8081]
loss:        5.041251  [ 4832/ 8081]
loss:        5.673138  [ 5152/ 8081]
loss:        6.322588  [ 5472/ 8081]
loss:        5.683174  [ 5792/ 8081]
loss:        5.417451  [ 6112/ 8081]
loss:        5.284272  [ 6432/ 8081]
loss:        5.300341  [ 6752/ 8081]
loss:        5.142917  [ 7072/ 8081]
loss:        5.177952  [ 7392/ 8081]
loss:        5.738287  [ 7712/ 8081]
loss:        5.078168  [ 8032/ 8081]

Avg training loss:        5.632569
Val loss: 5.489992  (hab: 9.133947, mov: 5.551386)
Learning rates: hab: 1.563e-06, mov: 1.000e-05
total validation loss decreased (5.493873 → 5.489992). Saving model…
  new best movement (5.551386) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 5/10, movement: 0/10

Epoch 67/100
loss:        5.842425  [   32/ 8081]
loss:        4.961863  [  352/ 8081]
loss:        5.556984  [  672/ 8081]
loss:        5.333685  [  992/ 8081]
loss:        6.408626  [ 1312/ 8081]
loss:        6.037914  [ 1632/ 8081]
loss:        5.853109  [ 1952/ 8081]
loss:        5.660337  [ 2272/ 8081]
loss:        5.911697  [ 2592/ 8081]
loss:        5.079055  [ 2912/ 8081]
loss:        4.402115  [ 3232/ 8081]
loss:        5.766750  [ 3552/ 8081]
loss:        4.842758  [ 3872/ 8081]
loss:        5.390579  [ 4192/ 8081]
loss:        5.559499  [ 4512/ 8081]
loss:        5.395057  [ 4832/ 8081]
loss:        5.791644  [ 5152/ 8081]
loss:        5.307212  [ 5472/ 8081]
loss:        5.176686  [ 5792/ 8081]
loss:        5.700828  [ 6112/ 8081]
loss:        5.914255  [ 6432/ 8081]
loss:        5.235528  [ 6752/ 8081]
loss:        5.532273  [ 7072/ 8081]
loss:        5.235760  [ 7392/ 8081]
loss:        6.458383  [ 7712/ 8081]
loss:        5.550854  [ 8032/ 8081]

Avg training loss:        5.630858
Val loss: 5.485638  (hab: 9.133991, mov: 5.546800)
Learning rates: hab: 1.563e-06, mov: 1.000e-05
total validation loss decreased (5.489992 → 5.485638). Saving model…
  new best movement (5.546800) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 6/10, movement: 0/10

Epoch 68/100
loss:        5.696639  [   32/ 8081]
loss:        5.852695  [  352/ 8081]
loss:        5.807289  [  672/ 8081]
loss:        5.207594  [  992/ 8081]
loss:        5.806383  [ 1312/ 8081]
loss:        6.222836  [ 1632/ 8081]
loss:        5.477994  [ 1952/ 8081]
loss:        5.312665  [ 2272/ 8081]
loss:        4.276788  [ 2592/ 8081]
loss:        5.952018  [ 2912/ 8081]
loss:        5.524648  [ 3232/ 8081]
loss:        5.201440  [ 3552/ 8081]
loss:        5.362140  [ 3872/ 8081]
loss:        5.639888  [ 4192/ 8081]
loss:        6.680044  [ 4512/ 8081]
loss:        6.050379  [ 4832/ 8081]
loss:        5.925601  [ 5152/ 8081]
loss:        5.790759  [ 5472/ 8081]
loss:        3.934268  [ 5792/ 8081]
loss:        5.304850  [ 6112/ 8081]
loss:        5.902845  [ 6432/ 8081]
loss:        5.702676  [ 6752/ 8081]
loss:        4.116590  [ 7072/ 8081]
loss:        5.660512  [ 7392/ 8081]
loss:        5.843771  [ 7712/ 8081]
loss:        5.380571  [ 8032/ 8081]

Avg training loss:        5.622056
Val loss: 5.481181  (hab: 9.134070, mov: 5.542294)
Learning rates: hab: 1.563e-06, mov: 1.000e-05
total validation loss decreased (5.485638 → 5.481181). Saving model…
  new best movement (5.542294) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 7/10, movement: 0/10

Epoch 69/100
loss:        5.966575  [   32/ 8081]
loss:        6.176599  [  352/ 8081]
loss:        5.095712  [  672/ 8081]
loss:        5.611154  [  992/ 8081]
loss:        6.118464  [ 1312/ 8081]
loss:        5.857560  [ 1632/ 8081]
loss:        6.108392  [ 1952/ 8081]
loss:        5.187197  [ 2272/ 8081]
loss:        4.513987  [ 2592/ 8081]
loss:        5.557925  [ 2912/ 8081]
loss:        6.718334  [ 3232/ 8081]
loss:        5.081272  [ 3552/ 8081]
loss:        6.061957  [ 3872/ 8081]
loss:        6.127605  [ 4192/ 8081]
loss:        5.788713  [ 4512/ 8081]
loss:        5.028554  [ 4832/ 8081]
loss:        5.184258  [ 5152/ 8081]
loss:        5.251672  [ 5472/ 8081]
loss:        5.351951  [ 5792/ 8081]
loss:        5.659822  [ 6112/ 8081]
loss:        5.378826  [ 6432/ 8081]
loss:        5.093343  [ 6752/ 8081]
loss:        4.301287  [ 7072/ 8081]
loss:        5.880022  [ 7392/ 8081]
loss:        6.028570  [ 7712/ 8081]
loss:        4.072504  [ 8032/ 8081]

Avg training loss:        5.619459
Val loss: 5.474833  (hab: 9.134194, mov: 5.535107)
Learning rates: hab: 7.813e-07 (decreased from 1.563e-06), mov: 1.000e-05
total validation loss decreased (5.481181 → 5.474833). Saving model…
  new best movement (5.535107) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 8/10, movement: 0/10

Epoch 70/100
loss:        6.142711  [   32/ 8081]
loss:        6.146108  [  352/ 8081]
loss:        6.528981  [  672/ 8081]
loss:        5.156920  [  992/ 8081]
loss:        5.485294  [ 1312/ 8081]
loss:        6.375265  [ 1632/ 8081]
loss:        6.381258  [ 1952/ 8081]
loss:        6.050369  [ 2272/ 8081]
loss:        4.934493  [ 2592/ 8081]
loss:        5.187383  [ 2912/ 8081]
loss:        5.279340  [ 3232/ 8081]
loss:        5.507360  [ 3552/ 8081]
loss:        5.120495  [ 3872/ 8081]
loss:        5.728399  [ 4192/ 8081]
loss:        6.159866  [ 4512/ 8081]
loss:        6.114216  [ 4832/ 8081]
loss:        6.035473  [ 5152/ 8081]
loss:        6.172215  [ 5472/ 8081]
loss:        6.055015  [ 5792/ 8081]
loss:        4.540790  [ 6112/ 8081]
loss:        6.105020  [ 6432/ 8081]
loss:        5.613883  [ 6752/ 8081]
loss:        5.683322  [ 7072/ 8081]
loss:        5.684685  [ 7392/ 8081]
loss:        5.660670  [ 7712/ 8081]
loss:        5.588565  [ 8032/ 8081]

Avg training loss:        5.619547
Val loss: 5.472147  (hab: 9.134116, mov: 5.532683)
Learning rates: hab: 7.813e-07, mov: 1.000e-05
total validation loss decreased (5.474833 → 5.472147). Saving model…
  new best movement (5.532683) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 9/10, movement: 0/10

Epoch 71/100
loss:        5.585128  [   32/ 8081]
loss:        6.041797  [  352/ 8081]
loss:        4.969955  [  672/ 8081]
loss:        4.935095  [  992/ 8081]
loss:        6.324829  [ 1312/ 8081]
loss:        6.346156  [ 1632/ 8081]
loss:        5.805640  [ 1952/ 8081]
loss:        5.022897  [ 2272/ 8081]
loss:        5.020932  [ 2592/ 8081]
loss:        5.631002  [ 2912/ 8081]
loss:        5.161789  [ 3232/ 8081]
loss:        5.710543  [ 3552/ 8081]
loss:        5.563759  [ 3872/ 8081]
loss:        5.650821  [ 4192/ 8081]
loss:        6.037214  [ 4512/ 8081]
loss:        6.744835  [ 4832/ 8081]
loss:        5.406845  [ 5152/ 8081]
loss:        5.696598  [ 5472/ 8081]
loss:        6.364797  [ 5792/ 8081]
loss:        6.465062  [ 6112/ 8081]
loss:        5.952533  [ 6432/ 8081]
loss:        5.459927  [ 6752/ 8081]
loss:        5.629455  [ 7072/ 8081]
loss:        5.239370  [ 7392/ 8081]
loss:        5.899606  [ 7712/ 8081]
loss:        5.563251  [ 8032/ 8081]

Avg training loss:        5.613278
Val loss: 5.466199  (hab: 9.134189, mov: 5.525713)
Learning rates: hab: 7.813e-07, mov: 1.000e-05
total validation loss decreased (5.472147 → 5.466199). Saving model…
  new best movement (5.525713) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 10/10, movement: 0/10

Epoch 72/100
loss:        5.731434  [   32/ 8081]
loss:        6.240873  [  352/ 8081]
loss:        6.163750  [  672/ 8081]
loss:        5.680518  [  992/ 8081]
loss:        5.409707  [ 1312/ 8081]
loss:        5.838750  [ 1632/ 8081]
loss:        4.677449  [ 1952/ 8081]
loss:        5.882855  [ 2272/ 8081]
loss:        6.560460  [ 2592/ 8081]
loss:        6.590908  [ 2912/ 8081]
loss:        6.101795  [ 3232/ 8081]
loss:        5.418462  [ 3552/ 8081]
loss:        5.746140  [ 3872/ 8081]
loss:        4.838361  [ 4192/ 8081]
loss:        5.586443  [ 4512/ 8081]
loss:        6.335096  [ 4832/ 8081]
loss:        5.280098  [ 5152/ 8081]
loss:        5.504012  [ 5472/ 8081]
loss:        5.040554  [ 5792/ 8081]
loss:        5.370686  [ 6112/ 8081]
loss:        5.634340  [ 6432/ 8081]
loss:        5.278184  [ 6752/ 8081]
loss:        5.626905  [ 7072/ 8081]
loss:        5.356143  [ 7392/ 8081]
loss:        5.822465  [ 7712/ 8081]
loss:        5.160389  [ 8032/ 8081]

Avg training loss:        5.616246
Val loss: 5.463326  (hab: 9.134214, mov: 5.523001)
Learning rates: hab: 7.813e-07, mov: 1.000e-05
total validation loss decreased (5.466199 → 5.463326). Saving model…
  new best movement (5.523001) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 11/10, movement: 0/10

Epoch 73/100
loss:        5.791680  [   32/ 8081]
loss:        5.821917  [  352/ 8081]
loss:        5.812261  [  672/ 8081]
loss:        5.945704  [  992/ 8081]
loss:        6.113282  [ 1312/ 8081]
loss:        5.494761  [ 1632/ 8081]
loss:        5.298041  [ 1952/ 8081]
loss:        5.805657  [ 2272/ 8081]
loss:        5.756581  [ 2592/ 8081]
loss:        5.707522  [ 2912/ 8081]
loss:        5.192412  [ 3232/ 8081]
loss:        5.490039  [ 3552/ 8081]
loss:        5.632133  [ 3872/ 8081]
loss:        5.378099  [ 4192/ 8081]
loss:        6.259345  [ 4512/ 8081]
loss:        5.546213  [ 4832/ 8081]
loss:        5.253529  [ 5152/ 8081]
loss:        5.498854  [ 5472/ 8081]
loss:        5.719670  [ 5792/ 8081]
loss:        5.492167  [ 6112/ 8081]
loss:        4.182658  [ 6432/ 8081]
loss:        5.653461  [ 6752/ 8081]
loss:        5.521145  [ 7072/ 8081]
loss:        5.811027  [ 7392/ 8081]
loss:        6.162478  [ 7712/ 8081]
loss:        6.510781  [ 8032/ 8081]

Avg training loss:        5.605226
Val loss: 5.460324  (hab: 9.134161, mov: 5.519821)
Learning rates: hab: 3.906e-07 (decreased from 7.813e-07), mov: 1.000e-05
total validation loss decreased (5.463326 → 5.460324). Saving model…
  new best movement (5.519821) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 12/10, movement: 0/10

Epoch 74/100
loss:        5.286860  [   32/ 8081]
loss:        5.202303  [  352/ 8081]
loss:        5.588665  [  672/ 8081]
loss:        5.742562  [  992/ 8081]
loss:        6.426132  [ 1312/ 8081]
loss:        5.759145  [ 1632/ 8081]
loss:        5.260735  [ 1952/ 8081]
loss:        5.091084  [ 2272/ 8081]
loss:        6.499503  [ 2592/ 8081]
loss:        6.473749  [ 2912/ 8081]
loss:        5.254852  [ 3232/ 8081]
loss:        6.334579  [ 3552/ 8081]
loss:        6.395741  [ 3872/ 8081]
loss:        5.490905  [ 4192/ 8081]
loss:        5.885211  [ 4512/ 8081]
loss:        6.154772  [ 4832/ 8081]
loss:        5.005662  [ 5152/ 8081]
loss:        5.992707  [ 5472/ 8081]
loss:        5.578530  [ 5792/ 8081]
loss:        5.567686  [ 6112/ 8081]
loss:        5.439797  [ 6432/ 8081]
loss:        5.486871  [ 6752/ 8081]
loss:        6.750906  [ 7072/ 8081]
loss:        5.683652  [ 7392/ 8081]
loss:        6.129304  [ 7712/ 8081]
loss:        5.655571  [ 8032/ 8081]

Avg training loss:        5.600949
Val loss: 5.459041  (hab: 9.134208, mov: 5.519029)
Learning rates: hab: 3.906e-07, mov: 1.000e-05
total validation loss decreased (5.460324 → 5.459041). Saving model…
  new best movement (5.519029) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 13/10, movement: 0/10

Epoch 75/100
loss:        4.543365  [   32/ 8081]
loss:        5.209057  [  352/ 8081]
loss:        6.924462  [  672/ 8081]
loss:        5.775857  [  992/ 8081]
loss:        5.286479  [ 1312/ 8081]
loss:        4.682442  [ 1632/ 8081]
loss:        4.685610  [ 1952/ 8081]
loss:        4.793156  [ 2272/ 8081]
loss:        5.517650  [ 2592/ 8081]
loss:        5.123556  [ 2912/ 8081]
loss:        5.919244  [ 3232/ 8081]
loss:        5.555888  [ 3552/ 8081]
loss:        5.611817  [ 3872/ 8081]
loss:        4.487603  [ 4192/ 8081]
loss:        5.324736  [ 4512/ 8081]
loss:        5.816631  [ 4832/ 8081]
loss:        6.335066  [ 5152/ 8081]
loss:        4.877960  [ 5472/ 8081]
loss:        5.367828  [ 5792/ 8081]
loss:        5.922069  [ 6112/ 8081]
loss:        6.577061  [ 6432/ 8081]
loss:        5.670949  [ 6752/ 8081]
loss:        6.017617  [ 7072/ 8081]
loss:        5.936334  [ 7392/ 8081]
loss:        4.930374  [ 7712/ 8081]
loss:        5.580783  [ 8032/ 8081]

Avg training loss:        5.600815
Val loss: 5.456025  (hab: 9.134176, mov: 5.515808)
Learning rates: hab: 3.906e-07, mov: 1.000e-05
total validation loss decreased (5.459041 → 5.456025). Saving model…
  new best movement (5.515808) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 14/10, movement: 0/10

Epoch 76/100
loss:        5.187961  [   32/ 8081]
loss:        6.391647  [  352/ 8081]
loss:        4.665327  [  672/ 8081]
loss:        5.435277  [  992/ 8081]
loss:        4.779835  [ 1312/ 8081]
loss:        5.619170  [ 1632/ 8081]
loss:        5.002689  [ 1952/ 8081]
loss:        5.886822  [ 2272/ 8081]
loss:        5.138041  [ 2592/ 8081]
loss:        4.939719  [ 2912/ 8081]
loss:        5.622924  [ 3232/ 8081]
loss:        5.367014  [ 3552/ 8081]
loss:        5.917531  [ 3872/ 8081]
loss:        5.543915  [ 4192/ 8081]
loss:        4.827287  [ 4512/ 8081]
loss:        5.138378  [ 4832/ 8081]
loss:        5.356059  [ 5152/ 8081]
loss:        5.211435  [ 5472/ 8081]
loss:        5.054351  [ 5792/ 8081]
loss:        5.036340  [ 6112/ 8081]
loss:        6.327859  [ 6432/ 8081]
loss:        6.076316  [ 6752/ 8081]
loss:        5.482130  [ 7072/ 8081]
loss:        5.640325  [ 7392/ 8081]
loss:        5.624595  [ 7712/ 8081]
loss:        5.407310  [ 8032/ 8081]

Avg training loss:        5.596364
Val loss: 5.453026  (hab: 9.134144, mov: 5.512563)
Learning rates: hab: 3.906e-07, mov: 1.000e-05
total validation loss decreased (5.456025 → 5.453026). Saving model…
  new best movement (5.512563) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 15/10, movement: 0/10

Epoch 77/100
loss:        6.424075  [   32/ 8081]
loss:        5.951219  [  352/ 8081]
loss:        4.415655  [  672/ 8081]
loss:        5.207916  [  992/ 8081]
loss:        5.463240  [ 1312/ 8081]
loss:        5.450148  [ 1632/ 8081]
loss:        6.174263  [ 1952/ 8081]
loss:        6.777855  [ 2272/ 8081]
loss:        6.197016  [ 2592/ 8081]
loss:        6.272507  [ 2912/ 8081]
loss:        5.176200  [ 3232/ 8081]
loss:        5.820226  [ 3552/ 8081]
loss:        5.630890  [ 3872/ 8081]
loss:        6.049506  [ 4192/ 8081]
loss:        6.402318  [ 4512/ 8081]
loss:        4.632614  [ 4832/ 8081]
loss:        4.941594  [ 5152/ 8081]
loss:        5.122215  [ 5472/ 8081]
loss:        4.933431  [ 5792/ 8081]
loss:        6.389747  [ 6112/ 8081]
loss:        5.013392  [ 6432/ 8081]
loss:        5.449681  [ 6752/ 8081]
loss:        4.940957  [ 7072/ 8081]
loss:        4.971219  [ 7392/ 8081]
loss:        5.558498  [ 7712/ 8081]
loss:        5.564751  [ 8032/ 8081]

Avg training loss:        5.594735
Val loss: 5.449562  (hab: 9.134181, mov: 5.508518)
Learning rates: hab: 1.953e-07 (decreased from 3.906e-07), mov: 1.000e-05
total validation loss decreased (5.453026 → 5.449562). Saving model…
  new best movement (5.508518) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 16/10, movement: 0/10

Epoch 78/100
loss:        5.563879  [   32/ 8081]
loss:        5.567474  [  352/ 8081]
loss:        4.956521  [  672/ 8081]
loss:        6.029359  [  992/ 8081]
loss:        5.103352  [ 1312/ 8081]
loss:        5.950511  [ 1632/ 8081]
loss:        5.181183  [ 1952/ 8081]
loss:        6.098184  [ 2272/ 8081]
loss:        6.233322  [ 2592/ 8081]
loss:        4.655641  [ 2912/ 8081]
loss:        5.700926  [ 3232/ 8081]
loss:        5.683294  [ 3552/ 8081]
loss:        5.576233  [ 3872/ 8081]
loss:        5.862514  [ 4192/ 8081]
loss:        5.508728  [ 4512/ 8081]
loss:        5.096618  [ 4832/ 8081]
loss:        4.781209  [ 5152/ 8081]
loss:        6.421771  [ 5472/ 8081]
loss:        5.060112  [ 5792/ 8081]
loss:        4.463225  [ 6112/ 8081]
loss:        5.984342  [ 6432/ 8081]
loss:        5.218701  [ 6752/ 8081]
loss:        5.498835  [ 7072/ 8081]
loss:        5.535333  [ 7392/ 8081]
loss:        6.297646  [ 7712/ 8081]
loss:        5.744939  [ 8032/ 8081]

Avg training loss:        5.596641
Val loss: 5.447831  (hab: 9.134159, mov: 5.506848)
Learning rates: hab: 1.953e-07, mov: 1.000e-05
total validation loss decreased (5.449562 → 5.447831). Saving model…
  new best movement (5.506848) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 17/10, movement: 0/10

Epoch 79/100
loss:        6.044155  [   32/ 8081]
loss:        5.397960  [  352/ 8081]
loss:        6.145573  [  672/ 8081]
loss:        6.601372  [  992/ 8081]
loss:        5.271502  [ 1312/ 8081]
loss:        5.368112  [ 1632/ 8081]
loss:        5.616035  [ 1952/ 8081]
loss:        3.960553  [ 2272/ 8081]
loss:        5.141513  [ 2592/ 8081]
loss:        5.196155  [ 2912/ 8081]
loss:        6.411037  [ 3232/ 8081]
loss:        5.051981  [ 3552/ 8081]
loss:        5.896464  [ 3872/ 8081]
loss:        5.999895  [ 4192/ 8081]
loss:        5.133582  [ 4512/ 8081]
loss:        5.990287  [ 4832/ 8081]
loss:        6.146287  [ 5152/ 8081]
loss:        5.474133  [ 5472/ 8081]
loss:        5.283760  [ 5792/ 8081]
loss:        5.007821  [ 6112/ 8081]
loss:        5.451013  [ 6432/ 8081]
loss:        4.631217  [ 6752/ 8081]
loss:        5.089136  [ 7072/ 8081]
loss:        4.467146  [ 7392/ 8081]
loss:        5.291319  [ 7712/ 8081]
loss:        5.208137  [ 8032/ 8081]

Avg training loss:        5.593714
Val loss: 5.445216  (hab: 9.134154, mov: 5.503818)
Learning rates: hab: 1.953e-07, mov: 1.000e-05
total validation loss decreased (5.447831 → 5.445216). Saving model…
  new best movement (5.503818) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 18/10, movement: 0/10

Epoch 80/100
loss:        5.229815  [   32/ 8081]
loss:        5.113379  [  352/ 8081]
loss:        5.876107  [  672/ 8081]
loss:        5.791145  [  992/ 8081]
loss:        5.760366  [ 1312/ 8081]
loss:        6.077374  [ 1632/ 8081]
loss:        5.084460  [ 1952/ 8081]
loss:        5.880211  [ 2272/ 8081]
loss:        5.605198  [ 2592/ 8081]
loss:        5.404860  [ 2912/ 8081]
loss:        5.401994  [ 3232/ 8081]
loss:        5.657501  [ 3552/ 8081]
loss:        5.531752  [ 3872/ 8081]
loss:        5.800417  [ 4192/ 8081]
loss:        4.720482  [ 4512/ 8081]
loss:        5.433350  [ 4832/ 8081]
loss:        6.728275  [ 5152/ 8081]
loss:        5.612870  [ 5472/ 8081]
loss:        4.788445  [ 5792/ 8081]
loss:        6.063241  [ 6112/ 8081]
loss:        5.693293  [ 6432/ 8081]
loss:        5.903297  [ 6752/ 8081]
loss:        6.027605  [ 7072/ 8081]
loss:        5.458837  [ 7392/ 8081]
loss:        6.005687  [ 7712/ 8081]
loss:        5.394153  [ 8032/ 8081]

Avg training loss:        5.587838
Val loss: 5.444229  (hab: 9.134150, mov: 5.502949)
Learning rates: hab: 1.953e-07, mov: 1.000e-05
total validation loss decreased (5.445216 → 5.444229). Saving model…
  new best movement (5.502949) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 19/10, movement: 0/10

Epoch 81/100
loss:        5.326712  [   32/ 8081]
loss:        6.302388  [  352/ 8081]
loss:        5.621000  [  672/ 8081]
loss:        5.261885  [  992/ 8081]
loss:        6.071630  [ 1312/ 8081]
loss:        5.965168  [ 1632/ 8081]
loss:        4.676309  [ 1952/ 8081]
loss:        4.963332  [ 2272/ 8081]
loss:        5.621298  [ 2592/ 8081]
loss:        5.904263  [ 2912/ 8081]
loss:        5.909752  [ 3232/ 8081]
loss:        6.136995  [ 3552/ 8081]
loss:        5.648190  [ 3872/ 8081]
loss:        5.689287  [ 4192/ 8081]
loss:        6.646731  [ 4512/ 8081]
loss:        5.952599  [ 4832/ 8081]
loss:        5.747954  [ 5152/ 8081]
loss:        5.275624  [ 5472/ 8081]
loss:        5.770371  [ 5792/ 8081]
loss:        5.882869  [ 6112/ 8081]
loss:        5.852943  [ 6432/ 8081]
loss:        5.600416  [ 6752/ 8081]
loss:        5.909398  [ 7072/ 8081]
loss:        6.185442  [ 7392/ 8081]
loss:        6.196318  [ 7712/ 8081]
loss:        6.446422  [ 8032/ 8081]

Avg training loss:        5.589333
Val loss: 5.442669  (hab: 9.134139, mov: 5.501499)
Learning rates: hab: 9.766e-08 (decreased from 1.953e-07), mov: 1.000e-05
total validation loss decreased (5.444229 → 5.442669). Saving model…
  new best movement (5.501499) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 20/10, movement: 0/10

Epoch 82/100
loss:        6.374242  [   32/ 8081]
loss:        5.117414  [  352/ 8081]
loss:        6.028985  [  672/ 8081]
loss:        4.704012  [  992/ 8081]
loss:        5.882717  [ 1312/ 8081]
loss:        4.508272  [ 1632/ 8081]
loss:        5.345216  [ 1952/ 8081]
loss:        6.089338  [ 2272/ 8081]
loss:        5.351864  [ 2592/ 8081]
loss:        5.937910  [ 2912/ 8081]
loss:        5.945849  [ 3232/ 8081]
loss:        5.525422  [ 3552/ 8081]
loss:        6.470016  [ 3872/ 8081]
loss:        3.937431  [ 4192/ 8081]
loss:        5.236245  [ 4512/ 8081]
loss:        5.864991  [ 4832/ 8081]
loss:        5.254672  [ 5152/ 8081]
loss:        4.952799  [ 5472/ 8081]
loss:        6.237307  [ 5792/ 8081]
loss:        6.408334  [ 6112/ 8081]
loss:        5.838151  [ 6432/ 8081]
loss:        5.936861  [ 6752/ 8081]
loss:        6.344625  [ 7072/ 8081]
loss:        5.275671  [ 7392/ 8081]
loss:        5.441391  [ 7712/ 8081]
loss:        5.303125  [ 8032/ 8081]

Avg training loss:        5.586454
Val loss: 5.441010  (hab: 9.134140, mov: 5.499631)
Learning rates: hab: 9.766e-08, mov: 1.000e-05
total validation loss decreased (5.442669 → 5.441010). Saving model…
  new best movement (5.499631) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 21/10, movement: 0/10

Epoch 83/100
loss:        5.621386  [   32/ 8081]
loss:        4.589754  [  352/ 8081]
loss:        5.221540  [  672/ 8081]
loss:        5.286973  [  992/ 8081]
loss:        5.312942  [ 1312/ 8081]
loss:        6.371216  [ 1632/ 8081]
loss:        4.788128  [ 1952/ 8081]
loss:        5.667160  [ 2272/ 8081]
loss:        5.335620  [ 2592/ 8081]
loss:        5.525732  [ 2912/ 8081]
loss:        5.500477  [ 3232/ 8081]
loss:        5.013430  [ 3552/ 8081]
loss:        5.079269  [ 3872/ 8081]
loss:        5.817650  [ 4192/ 8081]
loss:        5.053629  [ 4512/ 8081]
loss:        5.903511  [ 4832/ 8081]
loss:        5.991950  [ 5152/ 8081]
loss:        5.108594  [ 5472/ 8081]
loss:        6.707988  [ 5792/ 8081]
loss:        5.661417  [ 6112/ 8081]
loss:        5.033405  [ 6432/ 8081]
loss:        6.299872  [ 6752/ 8081]
loss:        4.912801  [ 7072/ 8081]
loss:        4.664351  [ 7392/ 8081]
loss:        5.861489  [ 7712/ 8081]
loss:        6.441594  [ 8032/ 8081]

Avg training loss:        5.585394
Val loss: 5.439793  (hab: 9.134140, mov: 5.498393)
Learning rates: hab: 9.766e-08, mov: 1.000e-05
total validation loss decreased (5.441010 → 5.439793). Saving model…
  new best movement (5.498393) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 22/10, movement: 0/10

Epoch 84/100
loss:        5.969073  [   32/ 8081]
loss:        4.850781  [  352/ 8081]
loss:        5.374628  [  672/ 8081]
loss:        6.318278  [  992/ 8081]
loss:        5.721091  [ 1312/ 8081]
loss:        5.500141  [ 1632/ 8081]
loss:        5.651479  [ 1952/ 8081]
loss:        6.292879  [ 2272/ 8081]
loss:        4.733443  [ 2592/ 8081]
loss:        6.225115  [ 2912/ 8081]
loss:        5.686523  [ 3232/ 8081]
loss:        5.520974  [ 3552/ 8081]
loss:        5.076478  [ 3872/ 8081]
loss:        5.978598  [ 4192/ 8081]
loss:        5.242075  [ 4512/ 8081]
loss:        5.243134  [ 4832/ 8081]
loss:        5.014195  [ 5152/ 8081]
loss:        4.038332  [ 5472/ 8081]
loss:        6.076380  [ 5792/ 8081]
loss:        5.462429  [ 6112/ 8081]
loss:        5.290164  [ 6432/ 8081]
loss:        5.773977  [ 6752/ 8081]
loss:        6.169828  [ 7072/ 8081]
loss:        7.092647  [ 7392/ 8081]
loss:        5.137810  [ 7712/ 8081]
loss:        4.615131  [ 8032/ 8081]

Avg training loss:        5.583104
Val loss: 5.439202  (hab: 9.134139, mov: 5.497951)
Learning rates: hab: 9.766e-08, mov: 1.000e-05
total validation loss decreased (5.439793 → 5.439202). Saving model…
  new best movement (5.497951) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 23/10, movement: 0/10

Epoch 85/100
loss:        4.423296  [   32/ 8081]
loss:        6.035933  [  352/ 8081]
loss:        4.783934  [  672/ 8081]
loss:        5.922391  [  992/ 8081]
loss:        5.383216  [ 1312/ 8081]
loss:        6.059066  [ 1632/ 8081]
loss:        6.382401  [ 1952/ 8081]
loss:        5.685079  [ 2272/ 8081]
loss:        5.673153  [ 2592/ 8081]
loss:        5.218044  [ 2912/ 8081]
loss:        6.776034  [ 3232/ 8081]
loss:        5.911880  [ 3552/ 8081]
loss:        5.807014  [ 3872/ 8081]
loss:        5.770979  [ 4192/ 8081]
loss:        5.759410  [ 4512/ 8081]
loss:        5.228075  [ 4832/ 8081]
loss:        5.334975  [ 5152/ 8081]
loss:        5.469007  [ 5472/ 8081]
loss:        5.759439  [ 5792/ 8081]
loss:        5.069817  [ 6112/ 8081]
loss:        5.861602  [ 6432/ 8081]
loss:        5.668021  [ 6752/ 8081]
loss:        5.972435  [ 7072/ 8081]
loss:        5.145638  [ 7392/ 8081]
loss:        5.776136  [ 7712/ 8081]
loss:        5.322128  [ 8032/ 8081]

Avg training loss:        5.580813
Val loss: 5.436328  (hab: 9.134121, mov: 5.494549)
Learning rates: hab: 4.883e-08 (decreased from 9.766e-08), mov: 1.000e-05
total validation loss decreased (5.439202 → 5.436328). Saving model…
  new best movement (5.494549) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 24/10, movement: 0/10

Epoch 86/100
loss:        6.440750  [   32/ 8081]
loss:        5.375180  [  352/ 8081]
loss:        5.274136  [  672/ 8081]
loss:        5.445200  [  992/ 8081]
loss:        4.480088  [ 1312/ 8081]
loss:        6.199287  [ 1632/ 8081]
loss:        5.653214  [ 1952/ 8081]
loss:        5.300159  [ 2272/ 8081]
loss:        6.115678  [ 2592/ 8081]
loss:        5.300458  [ 2912/ 8081]
loss:        5.707288  [ 3232/ 8081]
loss:        6.097451  [ 3552/ 8081]
loss:        6.418979  [ 3872/ 8081]
loss:        5.577236  [ 4192/ 8081]
loss:        4.801233  [ 4512/ 8081]
loss:        5.518409  [ 4832/ 8081]
loss:        5.478925  [ 5152/ 8081]
loss:        5.243186  [ 5472/ 8081]
loss:        6.114219  [ 5792/ 8081]
loss:        6.446285  [ 6112/ 8081]
loss:        5.826892  [ 6432/ 8081]
loss:        4.790387  [ 6752/ 8081]
loss:        4.696035  [ 7072/ 8081]
loss:        6.020323  [ 7392/ 8081]
loss:        4.588045  [ 7712/ 8081]
loss:        6.220433  [ 8032/ 8081]

Avg training loss:        5.584390
Val loss: 5.437382  (hab: 9.134123, mov: 5.496106)
Learning rates: hab: 4.883e-08, mov: 1.000e-05
EarlyStopping counters — habitat: 25/10, movement: 1/10

Epoch 87/100
loss:        6.254244  [   32/ 8081]
loss:        5.563831  [  352/ 8081]
loss:        5.353840  [  672/ 8081]
loss:        5.457825  [  992/ 8081]
loss:        5.830113  [ 1312/ 8081]
loss:        5.341715  [ 1632/ 8081]
loss:        5.532096  [ 1952/ 8081]
loss:        5.163390  [ 2272/ 8081]
loss:        5.841530  [ 2592/ 8081]
loss:        5.664957  [ 2912/ 8081]
loss:        4.929718  [ 3232/ 8081]
loss:        6.418080  [ 3552/ 8081]
loss:        4.998969  [ 3872/ 8081]
loss:        5.378165  [ 4192/ 8081]
loss:        5.667681  [ 4512/ 8081]
loss:        6.725570  [ 4832/ 8081]
loss:        6.048672  [ 5152/ 8081]
loss:        6.280179  [ 5472/ 8081]
loss:        5.925344  [ 5792/ 8081]
loss:        5.826093  [ 6112/ 8081]
loss:        5.706185  [ 6432/ 8081]
loss:        4.821203  [ 6752/ 8081]
loss:        5.529234  [ 7072/ 8081]
loss:        5.500741  [ 7392/ 8081]
loss:        5.224956  [ 7712/ 8081]
loss:        5.537958  [ 8032/ 8081]

Avg training loss:        5.581128
Val loss: 5.435810  (hab: 9.134113, mov: 5.494316)
Learning rates: hab: 4.883e-08, mov: 1.000e-05
total validation loss decreased (5.436328 → 5.435810). Saving model…
  new best movement (5.494316) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 26/10, movement: 0/10

Epoch 88/100
loss:        5.842586  [   32/ 8081]
loss:        5.936138  [  352/ 8081]
loss:        5.526673  [  672/ 8081]
loss:        4.807163  [  992/ 8081]
loss:        5.632049  [ 1312/ 8081]
loss:        5.077741  [ 1632/ 8081]
loss:        5.279888  [ 1952/ 8081]
loss:        7.091421  [ 2272/ 8081]
loss:        6.047476  [ 2592/ 8081]
loss:        5.027669  [ 2912/ 8081]
loss:        5.113256  [ 3232/ 8081]
loss:        5.440072  [ 3552/ 8081]
loss:        5.469429  [ 3872/ 8081]
loss:        5.958839  [ 4192/ 8081]
loss:        5.443745  [ 4512/ 8081]
loss:        5.579510  [ 4832/ 8081]
loss:        5.478087  [ 5152/ 8081]
loss:        5.988472  [ 5472/ 8081]
loss:        6.537951  [ 5792/ 8081]
loss:        5.773495  [ 6112/ 8081]
loss:        5.392382  [ 6432/ 8081]
loss:        5.467836  [ 6752/ 8081]
loss:        5.677238  [ 7072/ 8081]
loss:        5.407579  [ 7392/ 8081]
loss:        5.934291  [ 7712/ 8081]
loss:        5.183417  [ 8032/ 8081]

Avg training loss:        5.580055
Val loss: 5.433188  (hab: 9.134111, mov: 5.491032)
Learning rates: hab: 4.883e-08, mov: 1.000e-05
total validation loss decreased (5.435810 → 5.433188). Saving model…
  new best movement (5.491032) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 27/10, movement: 0/10

Epoch 89/100
loss:        4.482453  [   32/ 8081]
loss:        5.520621  [  352/ 8081]
loss:        4.907447  [  672/ 8081]
loss:        6.205781  [  992/ 8081]
loss:        6.581367  [ 1312/ 8081]
loss:        5.982771  [ 1632/ 8081]
loss:        5.097293  [ 1952/ 8081]
loss:        6.034305  [ 2272/ 8081]
loss:        5.874189  [ 2592/ 8081]
loss:        5.664598  [ 2912/ 8081]
loss:        5.677784  [ 3232/ 8081]
loss:        5.902174  [ 3552/ 8081]
loss:        6.178339  [ 3872/ 8081]
loss:        5.569479  [ 4192/ 8081]
loss:        4.755732  [ 4512/ 8081]
loss:        6.039156  [ 4832/ 8081]
loss:        5.661906  [ 5152/ 8081]
loss:        5.698646  [ 5472/ 8081]
loss:        5.444194  [ 5792/ 8081]
loss:        5.719250  [ 6112/ 8081]
loss:        5.709826  [ 6432/ 8081]
loss:        5.066060  [ 6752/ 8081]
loss:        5.453597  [ 7072/ 8081]
loss:        6.049791  [ 7392/ 8081]
loss:        5.722983  [ 7712/ 8081]
loss:        5.743300  [ 8032/ 8081]

Avg training loss:        5.575417
Val loss: 5.432310  (hab: 9.134109, mov: 5.490316)
Learning rates: hab: 2.441e-08 (decreased from 4.883e-08), mov: 1.000e-05
total validation loss decreased (5.433188 → 5.432310). Saving model…
  new best movement (5.490316) → outputs/best_model_movement.pt
EarlyStopping counters — habitat: 28/10, movement: 0/10

create_gif()

Assembles the snapshots in snapshot_dir into an animation so you can see how the predictions evolve as training progresses.

Display it with IPython.display.Image, which reads the file and embeds the bytes in the notebook’s output. A hand-written <img src="outputs/....gif"> tag does not work here: the notebook’s HTML output is rendered in a sandboxed webview whose base URL is not the notebook’s directory, so a relative path resolves to nothing and you get an empty box. Nothing is generating a still frame — there is simply no image being loaded at all. Embedding also means the animation survives being exported to HTML or shared as a standalone .ipynb.

Code
gif_path = str(OUTPUT_DIR / "training_progress.gif")
deepssf.create_gif(str(SNAPSHOT_DIR), gif_path, fps=5)
print(f"Training GIF → {gif_path}")

# Image() embeds the file's bytes in the output, so it renders wherever the
# notebook is opened.  Leave it as the last expression to display it.
Image(filename=gif_path)
Animation saved: outputs/training_progress.gif
Training GIF → outputs/training_progress.gif
<IPython.core.display.Image object>

Training - loss across epochs

How did the predictive performance on the validation dataset change across the training period?

Code
# Three panels rather than one: the habitat and movement components move on very
# different scales, so sharing an axis would flatten the habitat curve into a
# horizontal line.  x = 0 is the pre-training baseline recorded by fit().
uniform_nll = np.log(WINDOW_SIZE ** 2)
epochs = np.arange(len(history["val_losses"]))
stages = np.asarray(history["stage"])

fig, axes = plt.subplots(1, 3, figsize=(14, 3.6), constrained_layout=True)

panels = [
    ("Combined", history["val_losses"], history["train_losses"]),
    ("Habitat", history["val_habitat_losses"], None),
    ("Movement", history["val_movement_losses"], None),
]

for ax, (title, val, train) in zip(axes, panels):
    if train is not None:
        ax.plot(epochs, train, color="0.6", lw=1.5, label="train")
    ax.plot(epochs, val, color="C0", lw=1.5, label="val")
    if title == "Habitat":
        # A uniform surface over the window scores log(H*W), so this line is the
        # "learned nothing" mark — the whole question for the habitat head is how
        # far below it the curve gets.  Movement sits far below it from the first
        # epoch, and drawing it there would only compress that panel's axis.
        ax.axhline(uniform_nll, color="0.4", ls="--", lw=1,
                   label=f"uniform ({uniform_nll:.2f})")
    # Shade the staged-training blocks so the schedule is readable off the curve.
    for s in np.unique(stages[stages >= 0])[1::2]:
        within = epochs[stages == s]
        ax.axvspan(within.min() - 0.5, within.max() + 0.5, color="0.92", zorder=0)
    ax.set_xlabel("Epoch (0 = untrained baseline)")
    ax.set_ylabel("NLL (nats)")
    ax.set_title(title)
    ax.legend(frameon=False, fontsize=8)
    ax.spines[["top", "right"]].set_visible(False)

fig.suptitle("Training history — combined loss and its two components")
plt.savefig(str(OUTPUT_DIR / "loss_history.png"), dpi=100)
plt.show()

Choosing which checkpoint to use

fit() leaves the model holding its final-epoch weights, not the best ones, so load a checkpoint before testing or simulating.

  • best_model.pt — the model to use for simulation. Both heads from one epoch, so the joint likelihood is the one that was actually measured.
  • best_model_habitat.pt / best_model_movement.pt — each head at its own best epoch, tracked across the whole run.

For simulating trajectories, you will likely want to use the best performing model overall (best_model.pt).

Code
# The joint model — what the simulations further down should use
ckpt = deepssf.load_checkpoint(str(OUTPUT_DIR / "best_model.pt"), model)
print(f"best_model.pt         saved on '{ckpt['checkpoint_criterion']}' "
      f"= {ckpt['checkpoint_score']:.6f}")

hab_ckpt = deepssf.load_checkpoint(str(OUTPUT_DIR / "best_model_habitat.pt"))
print(f"best_model_habitat.pt  saved on '{hab_ckpt['checkpoint_criterion']}' "
      f"= {hab_ckpt['checkpoint_score']:.6f}")
best_model.pt         saved on 'total' = 5.432310
best_model_habitat.pt  saved on 'habitat' = 9.129542

Test the model against withheld data

Convert the NumPy rasters loaded earlier into PyTorch tensors.

Both validate_next_step_probs() and simulate_trajectory() call get_landscape(month_index) at each step to fetch the current environmental layers as a list of 2-D tensors.

With static (non-seasonal) rasters the month_index argument is unused. For seasonal data (e.g. monthly NDVI composites) you would index into a stack of layers here based on the current month.

Code
# validate_next_step_probs() and simulate_trajectory() both stack the list returned
# by get_landscape() along a new channel dimension, so every element must be a *2-D*
# tensor.  Unbind the band dimension of any multi-band layer so the channel order
# matches what MovementDataset fed the model during training.
landscape_tensors = []
for arr in env_layers.values():
    t = torch.from_numpy(np.asarray(arr).astype(np.float32))
    landscape_tensors.extend([t] if t.ndim == 2 else list(t))

print(f"Landscape channels: {len(landscape_tensors)}  (each {tuple(landscape_tensors[0].shape)})")

def get_landscape(_month_index):
    # Returns one 2-D tensor per spatial channel, in the same order as LAYER_PATHS.
    return landscape_tensors

# Take the last 10 % of steps as a test sample.  CAVEAT: make_dataloaders() uses a
# random_split, so these steps were mostly *seen during training* — the numbers below
# are not a clean out-of-sample estimate.  Because step_df is ordered by individual
# name, this slice is also just the last few ids alphabetically rather than the
# latest steps in time.  Hold out individuals or a date range explicitly for a real test.
n_test      = int(len(step_df) * 0.1)
test_sample = step_df.iloc[-n_test:].reset_index(drop=True)
print(f"Testing on {len(test_sample):,} steps")
Landscape channels: 2  (each (758, 1175))
Testing on 1,010 steps

validate_next_step_probs()

Runs the model forward on each test step and records the probability mass assigned to the true next location.

  • Inputs:
    • model
    • test_sample
      • test dataframe that contains movement steps (from the data loaded above)
    • get_landscape
      • retrieves the landscape layers (spatial covariates) that were used to train the model
    • transform
      • raster Affine transform
    • window_size
      • window size of the local availability window (should be the same as for training)
    • scalar_cols
      • scalar column names (should be set from above)
    • yday_col
      • which column has the yday column to ensure the correct spatial layers (if dynamic through time) are indexed
    • month_index_fn
      • a function mapping day-of-year → month index (used by the get_landscape function). Here we are using static spatial layers so we can set it to 0.
  • Outputs:
    • test_sample dataframe with three new columns appended:
      • habitat_prob — probability at the true location from the habitat head alone
      • move_prob — probability at the true location from the movement head alone
      • next_step_prob — joint (habitat × movement) probability at the true location

Higher values mean the model performed better (there was higher predicted probability at the observed next-step).

Code
# Use the last 10 % of steps as the test set (matches the DataLoader split)
n_test      = int(len(step_df) * 0.1)
test_sample = step_df.iloc[-n_test:].reset_index(drop=True)
print(f"Validating on {len(test_sample):,} steps")

val_results = deepssf.validate_next_step_probs(
    model,
    test_sample,
    get_landscape=get_landscape,
    transform=raster_transform,
    window_size=WINDOW_SIZE,
    scalar_cols=tuple(SCALAR_COLS),
    yday_col="yday_t1",
    bearing_col="bearing_tm1",
    month_index_fn=lambda _yday: 0,   # static layers
)
print(val_results[["habitat_prob", "move_prob", "next_step_prob"]].describe())
Validating on 1,010 steps
       habitat_prob    move_prob  next_step_prob
count   1010.000000  1010.000000     1010.000000
mean       0.000132     0.032928        0.035526
std        0.000070     0.083410        0.089361
min        0.000000     0.000000        0.000000
25%        0.000086     0.000335        0.000341
50%        0.000108     0.004936        0.005571
75%        0.000176     0.029579        0.033213
max        0.000604     0.452846        0.529077

Plot the predictive performance on the held-out data

Code
# The uniform probability of any cell in the window is 1/(W*H) = 1/(W^2) since W=H.
uniform_prob = 1/(WINDOW_SIZE ** 2)
uniform_prob

fig, axes = plt.subplots(1, 3, figsize=(12, 3))
for ax, col, title in zip(
    axes,
    ["habitat_prob", "move_prob", "next_step_prob"],
    ["Habitat", "Movement", "Next-step"],
    strict=True,
):
    data = val_results[col].dropna()
    ax.hist(data, bins=40, edgecolor="white")
    ax.axvline(
        uniform_prob,
        color="black",
        linestyle="--",
        linewidth=1.5,
        label="Uniform (1/W²)",
    )
    ax.set_title(title)
    ax.set_xlabel("Probability")
    ax.ticklabel_format(axis="x", style="sci", scilimits=(-3, 3))
    ax.legend()

axes[0].set_ylabel("Count")
plt.suptitle("Next-step probability distributions (test set)", y=1.02)
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR / "validation_probs.png"), dpi=100, bbox_inches="tight")
plt.show()

Habitat selection across the full landscape

make_simulation_inputs

This function creates the sin/cos encodings for specific days and hours of the year, and is used when simulating to convert from days of the year and hours into sin/cos encodings. It will return a tuple of length n_steps.

Here, we just want the sin/cos encodings for a single point in time, so we just set n_steps to 1 and input a day of the year (PRED_YDAY, integer) and fractional hour (PRED_HOUR, float).

predict_habitat_landscape()

Everything so far has run the model on a WINDOW_SIZE crop centred on the animal. But the habitat sub-network is fully convolutional — multiple layers of 3x3 convolutions with stride 1 and padding 1, no pooling and no dense layer — so the same trained filters can be swept across the entire raster in a single pass. Convolution is translation-equivariant, so the value at a pixel is exactly what the model would have produced for a window centred on that pixel.

That gives a landscape-scale habitat selection surface. Read it as a relative ranking of cells: “if the animal could access anywhere on this landscape, how attractive is each cell at a certain point in time”. It ignores the movement process, which defines availability. The two are only combined over a local window, which is what simulate_next_step() does.

  • Inputs:
    • model
      • the trained model; only model.conv_habitat is used
    • landscape_rasters
      • the spatial covariates as a list of 2-D tensors, in the same order and on the same scaling as training — i.e. exactly what get_landscape() returns
    • scalars
      • the scalar covariates for the moment being predicted, in SCALAR_COLS order. make_simulation_inputs() builds this row for us, so the encoding cannot drift from what the simulation uses. Each value is broadcast to a constant layer covering the whole landscape (the same thing Scalar_to_Grid_Block does for a window — that block can’t be reused here because its output size is fixed to WINDOW_SIZE)
    • normalise
      • subtract the log-sum-exp over the whole landscape, making the surface a log-probability that sums to 1 across all valid cells
    • edge_buffer
      • how many border pixels to mask as NaN. Defaults to habitat_edge_buffer(model) — the exact width the zero-padding reaches, 1 pixel per conv layer, so 4 here. Those pixels saw padding rather than landscape and are not comparable with the interior
    • chunk_rows
      • process this many raster rows at a time. A full-landscape activation is several hundred MB, enough to exhaust a GPU; the chunks overlap by edge_buffer so the result is identical to a single pass
  • Outputs: a [height, width] NumPy array of log-probabilities, NaN in the border. Take np.exp() for probabilities.
Code
# Predict for one moment in time: 9:30 am on day 200.  
PRED_YDAY = 200 # what day of the year to simulate (1–365, integer)
PRED_HOUR = 9.5 # what hour of the day to simulate (0–24, float)

# Get one the sin/cos encodings at PRED_YDAY and PRED_HOUR
scalars_pred = deepssf.make_simulation_inputs(
    n_steps=1, 
    starting_yday=PRED_YDAY, 
    starting_hour=PRED_HOUR,
    time_between_steps=1.0,  # this isn't used here, but this argument is used to create the step intervals (in hours)
)[0][0]
print(f"scalar covariates ({', '.join(SCALAR_COLS)}):\n  {scalars_pred}")

habitat_log = deepssf.predict_habitat_landscape(
    model,
    landscape_rasters=get_landscape(0),   # static layers → month index 0
    scalars=scalars_pred,
)
print(f"\nSurface {habitat_log.shape}, masked border "
      f"{deepssf.habitat_edge_buffer(model)} px")
print(f"log-probability range: {np.nanmin(habitat_log):.2f} to "
      f"{np.nanmax(habitat_log):.2f}")
print(f"probabilities sum to  : {np.nansum(np.exp(habitat_log)):.4f}")
scalar covariates (hour_t1_sin1, hour_t1_cos1, yday_t1_sin1, yday_t1_cos1, dt_hour):
  [ 0.60876143 -0.79335334 -0.29446162 -0.95566331  1.        ]

Surface (758, 1175), masked border 4 px
log-probability range: -23.34 to -12.67
probabilities sum to  : 1.0000

Plot the landscape-scale habitat selection surface

Plotted on the log scale. Across millions of cells the probabilities are tiny (a uniform surface would give something on the order of ~1e-7 per cell) and a handful of very attractive cells dominate the linear range, so a linear colour scale washes the whole map out. The colour limits are clipped to percentiles for the same reason.

Code
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

# Percentile limits for the colour scale: otherwise a few extreme cells can dominate the colour range
vmin, vmax = np.nanpercentile(habitat_log, [1.0, 99.0])

# Convert the fractional hour to hours and minutes for the title
h, m = divmod(round(PRED_HOUR * 60), 60)

rasterio.plot.show(
    habitat_log, transform=raster_transform, ax=axes[0],
    cmap="viridis", vmin=vmin, vmax=vmax,
)
axes[0].set_title(f"Habitat selection (log prob) — yday {PRED_YDAY}, "
                  f"{h}:{m:02d}")

rasterio.plot.show(
    habitat_log, transform=raster_transform, ax=axes[1],
    cmap="viridis", vmin=vmin, vmax=vmax,
)
axes[1].plot(
    step_df["x1_"], step_df["y1_"],
    "r.", alpha=0.25, markersize=0.6, label="observed GPS fixes",
)
axes[1].legend(loc="upper right", markerscale=10)
axes[1].set_title("with observed locations overlaid")

for ax in axes:
    ax.set_xlabel("Easting (m, projected)")
    ax.set_ylabel("Northing (m, projected)")

# One colourbar for both panels
fig.colorbar(axes[0].images[0], ax=axes, shrink=0.8, label="log probability")
plt.savefig(str(OUTPUT_DIR / "habitat_landscape.png"), dpi=120, bbox_inches="tight")
plt.show()

How does habitat selection change through the day?

The scalar covariates are inputs to the habitat CNN, so the same landscape produces a different surface at different times of day. Re-running the prediction at different times of day shows what the model has learned.

Each surface is normalised over the landscape independently, so the panels show where selection is concentrated at that hour, not the overall level of activity.

These surfaces only show the habitat selection, and not where would expect to find buffalo, as these maps do not take movement into consideration. To generate distribution maps we need to simulate movement trajectories.

Code
PRED_HOURS = [3, 9.5, 15, 21]

fig, axes = plt.subplots(2, 2, figsize=(13, 10))
surfaces = {}
for ax, hour in zip(axes.flat, PRED_HOURS, strict=True):
    scalars_h = deepssf.make_simulation_inputs(
        n_steps=1, starting_yday=PRED_YDAY, starting_hour=float(hour)
    )[0][0]
    surf = deepssf.predict_habitat_landscape(
        model, landscape_rasters=get_landscape(0), scalars=scalars_h
    )
    surfaces[hour] = surf

    # Convert the fractional hour to hours and minutes for the title
    h, m = divmod(round(hour * 60), 60)

    lo, hi = np.nanpercentile(surf, [1.0, 99.0])
    rasterio.plot.show(
        surf, transform=raster_transform, ax=ax, cmap="viridis", vmin=lo, vmax=hi
    )
    ax.set_title(f"{h:02d}:{m:02d}")
    ax.set_xlabel("Easting (m)")
    ax.set_ylabel("Northing (m)")

plt.suptitle(f"Habitat selection through the day (yday {PRED_YDAY})")
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR / "habitat_landscape_by_hour.png"), dpi=120)
plt.show()

# How much does the surface actually move between hours?  If these differences
# are ~0 the model has not learned any diel variation in habitat use.
ref = surfaces[PRED_HOURS[0]]
for hour in PRED_HOURS[1:]:
    h, m = divmod(round(hour * 60), 60)
    diff = np.nanmax(np.abs(surfaces[hour] - ref))
    print(f"max |log-prob difference| between 00:00 and {h:02d}:{m:02d}: {diff:.3f}")

max |log-prob difference| between 00:00 and 09:30: 2.975
max |log-prob difference| between 00:00 and 15:00: 3.026
max |log-prob difference| between 00:00 and 21:00: 1.789

Create a GIF of the hourly habitat selection surfaces

Save each hourly habitat selection map as a PNG (within a subdirectory called ‘habitat_selection_maps’), and then combine them into a GIF.

These maps are just the habitat selection probabilities from the convolution layers, and ignore the movement process. They are therefore more of a diagnostic tool to assess how habitat selection changes throughout the day.

Code
HABITAT_MAP_DIR = OUTPUT_DIR / "habitat_selection_maps"
HABITAT_MAP_DIR.mkdir(exist_ok=True)

# Predict all 24 hours first, then draw them.  The two passes are what lets every
# frame share one colour scale: with per-frame percentile limits (as in the 2x2
# figure above) each surface is re-normalised to its own range, so the animation
# flickers without showing where selection has actually moved.  
hourly_surfaces = {}
for hour in range(24):
    scalars_h = deepssf.make_simulation_inputs(
        n_steps=1, 
        starting_yday=PRED_YDAY, 
        starting_hour=float(hour),
        time_between_steps=1.0,
    )[0][0]
    h, m = divmod(round(hour * 60), 60)
    hourly_surfaces[hour] = deepssf.predict_habitat_landscape(
        model, landscape_rasters=get_landscape(0), scalars=scalars_h
    )
    print(f"  {h:02d}:{m:02d} done", end="\r")

# Shared colour limits over the whole day, clipped to the same percentiles used
# for the single-hour map (a handful of extreme cells otherwise take the range).
VMIN = min(np.nanpercentile(s, 1.0) for s in hourly_surfaces.values())
VMAX = max(np.nanpercentile(s, 99.0) for s in hourly_surfaces.values())
print(f"shared colour limits: {VMIN:.2f} to {VMAX:.2f}")

for hour, surf in hourly_surfaces.items():
    fig, ax = plt.subplots(figsize=(7.5, 6))
    rasterio.plot.show(
        surf, transform=raster_transform, ax=ax,
        cmap="viridis", vmin=VMIN, vmax=VMAX,
    )
    h, m = divmod(round(hour * 60), 60)
    ax.set_title(f"Habitat selection (log prob) — yday {PRED_YDAY}, {h:02d}:{m:02d}")
    ax.set_xlabel("Easting (m, projected)")
    ax.set_ylabel("Northing (m, projected)")
    fig.colorbar(ax.images[0], ax=ax, shrink=0.8, label="log probability")

    # Zero-padded hour so the filenames sort into clock order.  Note the absence
    # of bbox_inches="tight": it trims each figure to its own content, and frames
    # of differing pixel size cannot be stacked into a GIF.
    fig.savefig(HABITAT_MAP_DIR / f"habitat_hour_{hour:02d}.png", dpi=300)
    plt.close(fig)   # close, or 24 figures are drawn into the notebook output

print(f"Wrote 24 frames to {HABITAT_MAP_DIR}")
shared colour limits: -18.19 to -12.20
Wrote 24 frames to outputs/habitat_selection_maps

Display the GIF of hourly habitat selection

Code
import imageio.v2 as imageio

habitat_gif = OUTPUT_DIR / "habitat_selection_hourly.gif"

# Read the frames back off disk, sorted by filename — the zero-padded hour in the
# name puts them in clock order.  create_gif() (used for the training snapshots
# above) is deliberately not used here: it orders frames by an `epoch_<n>` field
# in the filename and, with no such field to key on, would leave them in whatever
# order the filesystem happened to list them.
frame_paths = sorted(HABITAT_MAP_DIR.glob("habitat_hour_*.png"))
frames = [imageio.imread(p) for p in frame_paths]

# duration is milliseconds *per frame*: half a second an hour 
# (`fps=` is accepted but deprecated in imageio v2 and warns.)
# loop=0 means loop forever rather than play once.
imageio.mimsave(habitat_gif, frames, duration=200, loop=0)
print(f"{len(frames)} frames → {habitat_gif} "
      f"({habitat_gif.stat().st_size / 1e6:.1f} MB)")

# As with the training GIF, Image() embeds the bytes so it renders wherever the
# notebook is opened.
Image(filename=str(habitat_gif))
24 frames → outputs/habitat_selection_hourly.gif (6.0 MB)
<IPython.core.display.Image object>

Save the hourly habitat selection surfaces as a raster (TIF)

Writing them out with the raster’s transform and CRS means they can be opened directly in QGIS/ArcGIS or read back with rasterio, and overlaid on the other layers in mapping/.

One band per hour rather than one file per hour: the 24 surfaces share a grid, a transform and a CRS, so a single stack keeps them together and lets GIS software step through the day with the band slider. Band n is hour n - 1 (GDAL numbers bands from 1), and each band carries its hour in both its description and its tags — so src.read(15) is 14:00.

Code
# The CRS is not returned by load_environmental_layers(), so read it off the
# source raster the transform came from.
with rasterio.open(next(iter(LAYER_PATHS.values()))) as src:
    raster_crs = src.crs

# Reuses the surfaces predicted for the GIF above, so run that cell first.
# Sorting the keys rather than trusting dict insertion order means band n is
# always hour n - 1, however the surfaces were generated.
hours_sorted = sorted(hourly_surfaces)
height, width = hourly_surfaces[hours_sorted[0]].shape

habitat_tif = OUTPUT_DIR / f"habitat_landscape_yday{PRED_YDAY}_hourly.tif"
with rasterio.open(
    habitat_tif, "w",
    driver="GTiff",
    height=height,
    width=width,
    count=len(hours_sorted),      # one band per hour
    dtype="float32",
    crs=raster_crs,
    transform=raster_transform,
    nodata=np.nan,          # the masked edge buffer
    compress="deflate",
) as dst:
    for band, hour in enumerate(hours_sorted, start=1):   # GDAL bands are 1-indexed
        dst.write(hourly_surfaces[hour].astype("float32"), band)
        # The description is what QGIS shows in its band dropdown; the tags keep
        # the hour machine-readable for anything parsing the metadata.
        dst.set_band_description(band, f"habitat selection log prob {hour:02d}:00")
        dst.update_tags(band, hour=hour, yday=PRED_YDAY)
    dst.update_tags(prediction_yday=PRED_YDAY, hours=",".join(str(h) for h in hours_sorted))

print(f"Wrote {habitat_tif}")
print(f"  {len(hours_sorted)} bands, {width} x {height} px "
      f"({habitat_tif.stat().st_size / 1e6:.1f} MB)")
Wrote outputs/habitat_landscape_yday200_hourly.tif
  24 bands, 1175 x 758 px (67.5 MB)

Simulate new data

Select starting parameters for the model.

We’ve set the starting location to be the first location of the observed data.

Code
# the starting id of the step (should be 1)
START_IDX = 1

# where should the simulation start?  
# here we use the first step in the observed data, but any valid (x, y) within the
# raster extent could be used. 
start_x = step_df["x1_"].iloc[START_IDX]
start_y = step_df["y1_"].iloc[START_IDX]
print(f"Starting location  : ({start_x:.0f}, {start_y:.0f})")

# the timestamp of the first step in the observed data
START_TIME = step_df["t1_"].iloc[START_IDX]
# extract the day of year and hour of day as floats for the simulation inputs
START_YDAY = START_TIME.timetuple().tm_yday
START_HOUR = START_TIME.hour + START_TIME.minute / 60.0

print(f"Starting timestamp : {START_TIME}")
print(f"Starting time      : day {START_YDAY:.0f}, {START_HOUR:.2f} h")

# hours between simulated steps - this should ideally be the same as the observed step interval, 
# but the model can be used to simulate at any interval
# (but it may not have learned a meaningful movement kernel for other intervals).
STEP_INTERVAL = 1.0 
Starting location  : (41969, -1435671)
Starting timestamp : 2018-07-25 10:34:23+09:30
Starting time      : day 206, 10.57 h

Simulate a trajectory

simulate_trajectory()

Generates a sequence of steps by iteratively: 1. Extracting a spatial window centred on the current location. 2. Running the model to obtain a joint log-probability surface over that window. 3. Sampling the next pixel from that surface (softmax-normalised probabilities). 4. Converting the sampled pixel back to projected coordinates and advancing time.

  • Inputs:
    • model
      • the trained model
    • get_landscape
      • retrieves the landscape layers (spatial covariates) that were used to train the model
    • transform
      • raster Affine transform
    • start_x
      • x coordinate of starting location
    • start_y
      • y coordinate of starting location
    • n_steps
      • number of steps
    • starting_yday
      • starting day-of-year
    • starting_hour
      • starting hour-of-day
    • start_time
      • what time should the simulation start
    • time_between_steps
      • time between steps in hours
    • window_size
      • size in pixels of the local landscape
    • month_index_fn
      • a function mapping day-of-year → month index (used by the get_landscape function). Here we are using static spatial layers so we can set it to 0.
  • Outputs: DataFrame with one row per simulated step containing:
    • x, y
      — projected coordinates of the simulated location
    • hour, yday
      — time variables updated at each step
    • month_index
      • the month index used to index the spatial variables (will be 0 if using static spatial covariates)
Code
sim_df = deepssf.simulate_trajectory(
    model,
    get_landscape=get_landscape,
    transform=raster_transform,
    start_x=start_x,
    start_y=start_y,
    n_steps=1000,
    starting_yday=START_YDAY,
    starting_hour=START_HOUR,
    start_time=START_TIME,
    time_between_steps=STEP_INTERVAL,
    window_size=WINDOW_SIZE,
    month_index_fn=lambda _yday: 0,   # static layers
)
print(f"Simulated {len(sim_df)} steps")
sim_df.head()
Simulated 1000 steps
x y time hour yday month_index
0 41955.272779 -1.435665e+06 2018-07-25 10:34:23+09:30 10.566667 206.000000 0
1 41947.764883 -1.435617e+06 2018-07-25 11:34:23+09:30 11.566667 206.041667 0
2 41883.724292 -1.435059e+06 2018-07-25 12:34:23+09:30 12.566667 206.083333 0
3 41989.072755 -1.434875e+06 2018-07-25 13:34:23+09:30 13.566667 206.125000 0
4 41992.806893 -1.434853e+06 2018-07-25 14:34:23+09:30 14.566667 206.166667 0

Plot the simulated data

Code
fig, ax = plt.subplots(figsize=(7, 6))
rasterio.plot.show(
    env_layers["ndvi"], transform=raster_transform, ax=ax, cmap="viridis"
)
ax.plot(
    step_df["x1_"], step_df["y1_"],
    "w-o", alpha=0.5, markersize=1, linewidth=0.8, label="observed",
)
ax.plot(
    sim_df["x"], sim_df["y"],
    "r-o", markersize=1, linewidth=0.8, label="simulated",
)
ax.set_xlabel("Easting (m, projected)")
ax.set_ylabel("Northing (m, projected)")
ax.legend()
ax.set_title("Simulated vs observed buffalo locations")
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR / "simulated_trajectory.png"), dpi=100)
plt.show()

Plot with an interactive map

plot_trajectories_folium()

A static figure is fine for checking the overall shape of a trajectory, but not for asking where the animal actually went. This draws the simulated (and optionally observed) locations over a satellite basemap so you can zoom in on individual steps.

  • Inputs:
    • trajectories
      • a simulated-locations DataFrame. If it has a trajectory_id column (as simulate_trajectories() returns) each id is drawn as its own toggleable line; a plain sim_df is drawn as a single line.
    • observed
      • observed locations to overlay, e.g. step_df. Drawn as points rather than a line by default, because step_df holds 28 individuals and a single line would connect them with jumps that never happened.
    • src_crs
      • CRS of the input coordinates. Everything is reprojected to EPSG:4326 (lat/lon) for the web map — our data is in EPSG:3112.
    • basemap
    • max_observed_points
      • observed locations are thinned to this many points. folium renders every point as its own element in the page, so all 100k+ fixes would lock up the notebook.
    • save_path
      • optionally also write a standalone HTML file.
  • Outputs: a folium.Map — leave it as the last line of a cell to display it.

folium is an optional dependency: pip install folium (or pip install "deepSSF[maps]").

Code
fmap = deepssf.plot_trajectories_folium(
    sim_df,
    observed=step_df,
    src_crs="EPSG:3112",          # projected CRS of the GPS data and rasters
    basemap="esri_imagery",
    zoom_start=12,
    max_observed_points = 10000, 
    save_path=str(OUTPUT_DIR / "simulated_trajectory_map.html"),
)
fmap
Map saved: outputs/simulated_trajectory_map.html
Make this Notebook Trusted to load map: File -> Trust Notebook

Save the trajectory to a CSV

save_trajectories()

Writes the simulated locations to outputs/deepSSF_trajectories/. The filename records how many trajectories, how many steps and the date, and the function never overwrites an existing file — re-running the cell appends a _2, _3, … run counter instead, so repeated runs accumulate rather than clobber.

  • Inputs:
    • trajectories — a DataFrame (or list of DataFrames)
    • output_dir — created if it doesn’t exist
    • prefix — leading part of the filename
    • split — write one file per trajectory_id instead of one combined file
  • Outputs: the list of paths written
Code
# set up a directory to hold the CSV files for the simulated trajectories
TRAJ_DIR = OUTPUT_DIR / "deepSSF_trajectories"

# save the simulated trajectory to a CSV file
deepssf.save_trajectories(sim_df, TRAJ_DIR, prefix="deepSSF_buffalo")
Saved: outputs/deepSSF_trajectories/deepSSF_buffalo_1traj_1000steps_2026-10-23_2.csv
[PosixPath('outputs/deepSSF_trajectories/deepSSF_buffalo_1traj_1000steps_2026-10-23_2.csv')]

Simulating many trajectories

simulate_trajectories()

Runs n_trajectories simulations and returns them stacked in one long-format DataFrame with trajectory_id and step columns.

Three methods are available:

method how it works when to use it
"batched" (default) every trajectory takes its step together, so the model sees a batch of n_trajectories windows per forward pass instead of one almost always — the forward pass is the whole cost of the simulation, and it is far better used at batch size 50 than at batch size 1
"sequential" a plain loop over simulate_trajectory() reference behaviour; needed if trajectories start at different times
"parallel" the same loop across a thread pool when start times vary and you want some overlap; the speed-up is modest (PyTorch releases the GIL inside its ops, but torch is already using several threads per op) and results are not reproducible from a seed

The one restriction on "batched" is that all trajectories share a clock, so starting_yday / starting_hour must be scalars. Start locations can still differ per trajectory — pass a list to start_x / start_y to release simulated animals from different points.

Code
N_TRAJECTORIES  = 1000   # how many simulated trajectories
N_SIM_STEPS     = 1000  # how many steps per trajectory

sim_many = deepssf.simulate_trajectories(
    model,
    get_landscape=get_landscape,
    transform=raster_transform,
    start_x=start_x,
    start_y=start_y,
    n_steps=N_SIM_STEPS,
    n_trajectories=N_TRAJECTORIES,
    method="batched",
    starting_yday=START_YDAY,
    starting_hour=START_HOUR,
    start_time=START_TIME,
    time_between_steps=1.0,
    window_size=WINDOW_SIZE,
    month_index_fn=lambda _yday: 0,   # static layers
)
print(f"{sim_many['trajectory_id'].nunique()} trajectories x "
      f"{N_SIM_STEPS} steps = {len(sim_many):,} locations")
sim_many.head()
1000 trajectories x 1000 steps = 1,000,000 locations
trajectory_id step x y time hour yday month_index
0 0 0 41890.803746 -1.435684e+06 2018-07-25 10:34:23+09:30 10.566667 206.000000 0
1 0 1 41840.087957 -1.435688e+06 2018-07-25 11:34:23+09:30 11.566667 206.041667 0
2 0 2 41798.320172 -1.435689e+06 2018-07-25 12:34:23+09:30 12.566667 206.083333 0
3 0 3 42094.215734 -1.435577e+06 2018-07-25 13:34:23+09:30 13.566667 206.125000 0
4 0 4 41758.668748 -1.435583e+06 2018-07-25 14:34:23+09:30 14.566667 206.166667 0

Releasing simulated animals from different starting points

Passing a sequence to start_x / start_y gives each simulated animal its own release site. Here we sample starting locations from the observed data.

Code
# Draw starting locations from the observed fixes
start_locations = step_df.sample(N_TRAJECTORIES, random_state=42)
start_locations
id t1_ x1_ y1_ t2_ x2_ y2_ dx dy sl_ bearing bearing_tm1 ta_ dt_hour hour_t1 yday_t1 hour_t1_sin1 hour_t1_cos1 yday_t1_sin1 yday_t1_cos1
3071 2005 2018-12-08 03:31:17+09:30 38724.432904 -1.442733e+06 2018-12-08 04:31:17+09:30 38711.887644 -1.442717e+06 -12.545260 15.683253 20.083525 2.245485 -1.156672 -2.881029 1.000000 3.516667 342.0 0.796002 0.605294 -0.389378 0.921078
3382 2005 2018-12-23 15:31:02+09:30 32302.151910 -1.436937e+06 2018-12-23 16:31:03+09:30 32416.475315 -1.436875e+06 114.323404 62.375716 130.232756 0.499465 -1.122225 1.621690 1.000278 15.516667 357.0 -0.796002 -0.605294 -0.141444 0.989946
2075 2005 2018-10-23 17:34:02+09:30 50771.488128 -1.434188e+06 2018-10-23 18:32:35+09:30 50616.727801 -1.433808e+06 -154.760327 380.830813 411.075257 1.956787 1.824749 0.132037 0.975833 17.566667 296.0 -0.993572 -0.113203 -0.928839 0.370483
2833 2005 2018-11-26 17:32:35+09:30 50955.629015 -1.434180e+06 2018-11-26 18:31:56+09:30 51207.069420 -1.433855e+06 251.440405 325.513331 411.316430 0.913086 0.136925 0.776161 0.989167 17.533333 330.0 -0.992546 -0.121869 -0.569901 0.821713
304 2005 2018-08-07 19:34:32+09:30 42149.874476 -1.437065e+06 2018-08-07 22:35:16+09:30 42055.149254 -1.436782e+06 -94.725222 283.722816 299.117876 1.893026 1.756156 0.136870 3.012222 19.566667 219.0 -0.917060 0.398749 -0.585696 -0.810531
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
9729 2005 2019-10-13 04:24:41+09:30 41873.419525 -1.437756e+06 2019-10-13 05:25:57+09:30 41867.647236 -1.437757e+06 -5.772289 -0.885411 5.839801 -2.989389 2.667349 0.626447 1.021111 4.400000 286.0 0.913545 0.406737 -0.978548 0.206018
5662 2005 2019-04-09 22:29:58+09:30 36063.398064 -1.438004e+06 2019-04-09 23:29:01+09:30 36063.837032 -1.438040e+06 0.438968 -36.189482 36.192144 -1.558667 2.422380 2.302138 0.984167 22.483333 99.0 -0.386711 0.922201 0.991269 -0.131859
4414 2005 2019-02-11 15:31:03+09:30 33474.953067 -1.436783e+06 2019-02-11 16:30:03+09:30 34174.004679 -1.436687e+06 699.051611 96.391087 705.665924 0.137024 -0.589320 0.726345 0.983333 15.516667 42.0 -0.796002 -0.605294 0.661263 0.750154
1470 2005 2018-09-27 02:33:10+09:30 41030.299612 -1.436130e+06 2018-09-27 03:33:23+09:30 41091.976071 -1.436160e+06 61.676459 -29.849832 68.520056 -0.450745 2.340561 -2.791307 1.003611 2.550000 270.0 0.619094 0.785317 -0.997707 -0.067683
9408 2005 2019-09-28 04:25:30+09:30 41184.097040 -1.437146e+06 2019-09-28 05:25:37+09:30 41220.742743 -1.437162e+06 36.645703 -16.414469 40.153983 -0.421126 -0.431929 0.010803 1.001944 4.416667 271.0 0.915311 0.402747 -0.998724 -0.050511

1000 rows × 20 columns

Simulate the trajectories

Code
sim_scattered = deepssf.simulate_trajectories(
    model,
    get_landscape=get_landscape,
    transform=raster_transform,
    start_x=start_locations["x1_"].to_numpy(),
    start_y=start_locations["y1_"].to_numpy(),
    n_steps=N_SIM_STEPS,
    n_trajectories=N_TRAJECTORIES,
    method="batched",
    starting_yday=START_YDAY,
    starting_hour=START_HOUR,
    start_time=START_TIME,
    window_size=WINDOW_SIZE,
    month_index_fn=lambda _yday: 0,
)
print(f"{len(sim_scattered):,} locations from {N_TRAJECTORIES} release sites")
1,000,000 locations from 1000 release sites

Save the simulated trajectoes

split=False (the default) writes one combined CSV with the trajectory_id column; split=True writes one file per simulated individual.

Code
deepssf.save_trajectories(sim_many, TRAJ_DIR, prefix="deepSSF_buffalo_many")
deepssf.save_trajectories(sim_scattered, TRAJ_DIR, prefix="deepSSF_buffalo_scattered")

# ...or one file per simulated animal:
# save_trajectories(sim_many, TRAJ_DIR, prefix="deepSSF_buffalo", split=True)
Saved: outputs/deepSSF_trajectories/deepSSF_buffalo_many_1000traj_1000steps_2026-10-23_2.csv
Saved: outputs/deepSSF_trajectories/deepSSF_buffalo_scattered_1000traj_1000steps_2026-10-23_2.csv
[PosixPath('outputs/deepSSF_trajectories/deepSSF_buffalo_scattered_1000traj_1000steps_2026-10-23_2.csv')]

To read in previously saved trajectories

Code
# # to read previously run simulations from disk, rather than re-running the simulation above
# SIM_PATH = TRAJ_DIR / "deepSSF_buffalo_scattered_1000traj_1000steps_2026-10-22.csv"
# sim_scattered = pd.read_csv(SIM_PATH)
# print(f"Read {len(sim_scattered):,} locations from {SIM_PATH}")
# sim_scattered.head()

Plot all of the simulated trajectories

Code
fig, axes = plt.subplots(1, 2, figsize=(13, 6))
for ax, df, title in zip(
    axes,
    [sim_many, sim_scattered],
    ["Same release site", "Different release sites"],
    strict=True,
):
    rasterio.plot.show(
        # select the first channel (NDVI) for the background, use the index to change layers
        landscape_tensors[0].numpy(), transform=raster_transform, ax=ax, cmap="viridis"
    )
    ax.plot(
        step_df["x1_"], step_df["y1_"],
        "w.", alpha=0.2, markersize=0.5, label="observed",
    )
    # One colour per simulated individual
    for tid, sub in df.groupby("trajectory_id"):
        ax.plot(sub["x"], sub["y"], "-", linewidth=0.7, alpha=0.9, label=f"sim {tid}")
    ax.set_xlabel("Easting (m, projected)")
    ax.set_ylabel("Northing (m, projected)")
    ax.set_title(title)

plt.suptitle(f"{N_TRAJECTORIES} simulated trajectories")
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR / "simulated_trajectories.png"), dpi=100)
plt.show()

A subset of the same trajectories on the interactive map

Each trajectory gets its own colour and its own entry in the layer control (top right), so individual animals can be switched on and off.

Code
# folium writes every trajectory into the page as its own feature group, so all
# 599 make a map too heavy to open.  Draw a subset — the heatmap below is what
# summarises the full set.
N_MAP_TRAJECTORIES = 10
map_ids = sim_scattered["trajectory_id"].unique()[:N_MAP_TRAJECTORIES]

fmap_traj = deepssf.plot_trajectories_folium(
    sim_scattered[sim_scattered["trajectory_id"].isin(map_ids)],
    observed=step_df,
    src_crs="EPSG:3112",
    basemap="esri_imagery",
    zoom_start=11,
    max_observed_points = 100000,
)

traj_html = OUTPUT_DIR / "simulated_trajectories_map.html"
fmap_traj.save(str(traj_html))
print(f"Map saved: {traj_html}")
fmap_traj
Map saved: outputs/simulated_trajectories_map.html
Make this Notebook Trusted to load map: File -> Trust Notebook

Visualise some data summaries of the simulated trajectories

Here we can check the summaries of the observed data against summaries of the simulated trajectories, to see whether we’ve captured those behaviours.

Calculate step lengths and turning angles from the simulated data

Code
sim_scattered_df = deepssf.prepare_movement_df(
    sim_scattered, 
    id_col="trajectory_id", 
    time_col="time", 
    x_col="x", 
    y_col="y"
)

n_dropped = len(sim_scattered) - len(sim_scattered_df)
print(f"Simulated movement steps : {len(sim_scattered_df):,}  ({n_dropped} dropped — last per individual)")
sim_scattered_df.head()
Simulated movement steps : 999,000  (1000 dropped — last per individual)
id t1_ x1_ y1_ t2_ x2_ y2_ dx dy sl_ bearing bearing_tm1 ta_ dt_hour hour_t1 yday_t1 hour_t1_sin1 hour_t1_cos1 yday_t1_sin1 yday_t1_cos1
0 0 2018-07-25 10:34:23+09:30 38718.353183 -1.442732e+06 2018-07-25 11:34:23+09:30 38885.254545 -1.442946e+06 166.901362 -214.114087 271.479109 -0.908683 0.000000 NaN 1.0 10.566667 206.0 0.366501 -0.930418 -0.391358 -0.920239
1 0 2018-07-25 11:34:23+09:30 38885.254545 -1.442946e+06 2018-07-25 12:34:23+09:30 38914.841767 -1.442936e+06 29.587222 9.878628 31.192804 0.322244 -0.908683 1.230927 1.0 11.566667 206.0 0.113203 -0.993572 -0.391358 -0.920239
2 0 2018-07-25 12:34:23+09:30 38914.841767 -1.442936e+06 2018-07-25 13:34:23+09:30 38946.832028 -1.442933e+06 31.990261 2.762687 32.109333 0.086147 0.322244 -0.236097 1.0 12.566667 206.0 -0.147809 -0.989016 -0.391358 -0.920239
3 0 2018-07-25 13:34:23+09:30 38946.832028 -1.442933e+06 2018-07-25 14:34:23+09:30 38914.853633 -1.442717e+06 -31.978395 215.985069 218.339570 1.717787 0.086147 1.631640 1.0 13.566667 206.0 -0.398749 -0.917060 -0.391358 -0.920239
4 0 2018-07-25 14:34:23+09:30 38914.853633 -1.442717e+06 2018-07-25 15:34:23+09:30 38933.369820 -1.443263e+06 18.516187 -545.431483 545.745684 -1.536862 1.717787 3.028537 1.0 14.566667 206.0 -0.622515 -0.782608 -0.391358 -0.920239

Distribution of step lengths

Code
# Distribution of step lengths, with the median and mean marked
fig, ax = plt.subplots(figsize=(8, 4))
# observed step lengths
ax.hist(step_df["sl_"], bins=250, density=True, color="tab:blue", alpha=0.5, label="observed")
ax.axvline(step_df["sl_"].median(), color="tab:blue", lw=2, label="Median")
ax.axvline(step_df["sl_"].mean(), color="tab:blue", lw=2, ls="--", label="Mean")
# simulated step lengths
ax.hist(sim_scattered_df["sl_"], bins=250, density=True, color="tab:purple", alpha=0.5, label="simulated")
ax.axvline(sim_scattered_df["sl_"].median(), color="tab:purple", lw=2, label="Median")
ax.axvline(sim_scattered_df["sl_"].mean(), color="tab:purple", lw=2, ls="--", label="Mean")
ax.set_xlabel("Step length (m)")
ax.set_ylabel("Count")
ax.set_xlim(0, 2000)
ax.legend()
plt.tight_layout()
plt.show()

Log of step lengths

Code
# Distribution of log step lengths, with the median and mean marked
fig, ax = plt.subplots(figsize=(8, 4))
# observed step lengths
ax.hist(np.log(step_df["sl_"]), bins=250, density=True, color="tab:blue", alpha=0.5, label="observed")
ax.axvline(np.log(step_df["sl_"].median()), color="tab:blue", lw=2, label="Median")
ax.axvline(np.log(step_df["sl_"].mean()), color="tab:blue", lw=2, ls="--", label="Mean")
# simulated step lengths
ax.hist(np.log(sim_scattered_df["sl_"]), bins=250, density=True, color="tab:purple", alpha=0.5, label="simulated")
ax.axvline(np.log(sim_scattered_df["sl_"].median()), color="tab:purple", lw=2, label="Median")
ax.axvline(np.log(sim_scattered_df["sl_"].mean()), color="tab:purple", lw=2, ls="--", label="Mean")
ax.set_xlabel("Log step length (m)")
ax.set_ylabel("Count")
ax.legend()
plt.tight_layout()
plt.show()

Step lengths through time

Code
# Step length by hour of day, with the hourly median and mean overlaid
# observed data
hourly_sl_obs_median = step_df.groupby(step_df["hour_t1"].round().mod(24))["sl_"].median()
hourly_sl_obs_mean = step_df.groupby(step_df["hour_t1"].round().mod(24))["sl_"].mean()
# simulated data
hourly_sl_sim_median = sim_scattered_df.groupby(sim_scattered_df["hour_t1"].round().mod(24))["sl_"].median()
hourly_sl_sim_mean = sim_scattered_df.groupby(sim_scattered_df["hour_t1"].round().mod(24))["sl_"].mean()

fig, ax = plt.subplots(figsize=(8, 4))
# observed data
ax.scatter(step_df["hour_t1"], step_df["sl_"], s=4, alpha=0.15, color="tab:blue", label="Steps")
ax.plot(hourly_sl_obs_mean.index, hourly_sl_obs_mean.values, color="tab:blue", lw=2, marker="s", ms=4, label="Hourly mean")
ax.plot(hourly_sl_obs_median.index, hourly_sl_obs_median.values, color="blue", lw=2, marker="o", ms=4, label="Hourly median")
# simulated data
ax.scatter(sim_scattered_df["hour_t1"], sim_scattered_df["sl_"], s=4, alpha=0.15, color="tab:purple", label="Steps (sim)")
ax.plot(hourly_sl_sim_mean.index, hourly_sl_sim_mean.values, color="tab:purple", lw=2, marker="s", ms=4, label="Hourly mean (sim)")
ax.plot(hourly_sl_sim_median.index, hourly_sl_sim_median.values, color="purple", lw=2, marker="o", ms=4, label="Hourly median (sim)")
ax.set_xlabel("Hour of day")
ax.set_ylabel("Step length (m)")
ax.set_xticks(range(0, 25, 3))
ax.set_xlim(0, 24)
ax.set_ylim(0, 1000)
ax.legend()
plt.tight_layout()
plt.show()

Distribution of turning angles

Code
# Distribution of turning angles (NaN for each individual's first step, so dropped)
fig, ax = plt.subplots(figsize=(8, 4))
# observed turning angles
ax.hist(step_df["ta_"].dropna(), bins=72, density=True, range=(-np.pi, np.pi), color="tab:orange", alpha=0.5, label="Observed")
# simulated turning angles
ax.hist(sim_scattered_df["ta_"].dropna(), bins=72, density=True, range=(-np.pi, np.pi), color="tab:green", alpha=0.5, label="Simulated")
ax.set_xlabel("Turning angle (radians)")
ax.set_ylabel("Count")
ax.set_xticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])
ax.set_xticklabels(["-π", "-π/2", "0", "π/2", "π"])
ax.set_xlim(-np.pi, np.pi)
plt.tight_layout()
plt.show()

Turning angles through time

As turning angles are circular, the mean/median sit near 0 regardless of spread. Instead, we summarise each hour with the circular SD and the shortest arc containing 50% of the angles (which may wrap across ±π), i.e., where are the densest 50% of the turning angles concentrated?

From this plot, we can see that movements are most directed (mean at 0 and most TAs near 0) around dawn and dusk, reflecting the longer steps in those periods. This shows that buffalo appear to be travelling in a directed way during those periods.

Code
# observed data
ta_obs_df = step_df.dropna(subset=["ta_"])
hourly_ta = ta_obs_df.groupby(ta_obs_df["hour_t1"].round().mod(24))["ta_"].apply(circ_summary).unstack()
# simulated data
ta_sim_df = sim_scattered_df.dropna(subset=["ta_"])
hourly_ta_sim = ta_sim_df.groupby(ta_sim_df["hour_t1"].round().mod(24))["ta_"].apply(circ_summary).unstack()

fig, (ax, ax_sd) = plt.subplots(2, 1, figsize=(8, 6), sharex=True, height_ratios=[3, 1])
ax.scatter(ta_obs_df["hour_t1"], ta_obs_df["ta_"], s=4, alpha=0.15, color="tab:orange", label="Steps")
ax.scatter(ta_sim_df["hour_t1"], ta_sim_df["ta_"], s=4, alpha=0.15, color="tab:green", label="Simulated")

offset = 0.15  # hours either side of the true hour
for summary, colour, label, shift in [
    (hourly_ta, "black", "Observed densest 50%", -offset),
    (hourly_ta_sim, "purple", "Simulated densest 50%", +offset),
]:
    for hour, row in summary.iterrows():
        # draw the arc as one segment, or two if it wraps past π
        segments = [(row.arc_start, min(row.arc_end, np.pi))]
        if row.arc_end > np.pi:
            segments.append((-np.pi, row.arc_end - 2 * np.pi))
        for lo, hi in segments:
            ax.vlines(hour + shift, lo, hi, color=colour, lw=3,
                      label=label if hour == summary.index[0] and lo == row.arc_start else None)

ax.set_ylabel("Turning angle (radians)")
ax.set_yticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi])
ax.set_yticklabels(["-π", "-π/2", "0", "π/2", "π"])
ax.set_ylim(-np.pi, np.pi)
ax.legend(loc="upper right")

ax_sd.plot(hourly_ta.index, hourly_ta["circ_sd"], color="black", lw=2, marker="o", ms=4)
ax_sd.plot(hourly_ta_sim.index, hourly_ta_sim["circ_sd"], color="purple", lw=2, marker="o", ms=4)
ax_sd.set_ylabel("Circular SD")
ax_sd.set_xlabel("Hour of day")
ax_sd.set_xticks(range(0, 25, 3))
ax_sd.set_xlim(0, 24)
plt.tight_layout()
plt.show()

Create a heatmap of the simulated trajectories

Count the number of simulated locations per cell/pixel, with an option to discard the first n steps to reduce bias towards the initial parameters.

trajectory_heatmap()

  • Inputs
    • trajectories
      • simulated locations, as returned by simulate_trajectories() (or a list of DataFrames).
    • transform, shape
      • the landscape rasters’ affine transform and (height, width), so the heatmap grid shares their origin and CRS.
    • burn_in
      • discard the first n steps of every trajectory. Early steps cluster around the release site(s) and reflect the starting conditions rather than the fitted model.
    • agg
      • aggregate agg x agg raster pixels into one heatmap cell. At the native 20 m almost every occupied cell holds a single location, so some coarsening makes the surface readable.
  • Outputs: (counts, heatmap_transform) — an int32 array of locations per cell, and its affine transform.

HEATMAP_PERIOD selects which part of the year the counts come from: "full" uses every simulated location, "wet" and "dry" keep only those whose day-of-year falls in the November–April wet season or the May–October dry season. The period is filtered before trajectory_heatmap() is called and named in the output filenames, so the three surfaces can be written side by side.

The result is visualised as a PNG below, written to a GeoTIFF with save_raster() so it can be opened in QGIS, and added to the interactive map as a layer that can be toggled on and off with add_heatmap_overlay().

Code
HEATMAP_DF     = sim_scattered  # trajectories to summarise (sim_scatteredor sim_many)
BURN_IN        = 0              # drop the first n steps of every trajectory
HEATMAP_AGG    = 8              # heatmap cell = HEATMAP_AGG x HEATMAP_AGG raster pixels

heatmap, heatmap_transform = deepssf.trajectory_heatmap(
    HEATMAP_DF,
    raster_transform,
    landscape_tensors[0].shape,
    burn_in=BURN_IN,
    agg=HEATMAP_AGG,
)

print(f"Heatmap grid: {heatmap.shape[0]} x {heatmap.shape[1]} (n = {heatmap.shape[0] * heatmap.shape[1]:,}) cells of "
      f"{PIXEL_SIZE * HEATMAP_AGG:.0f} m")
print(f"{heatmap.sum():,} locations counted after dropping the first {BURN_IN} steps "
      f"of each trajectory")
print(f"{(heatmap > 0).sum():,} occupied cells, up to {heatmap.max():,} locations in one cell")
Heatmap grid: 95 x 147 (n = 13,965) cells of 200 m
1,000,000 locations counted after dropping the first 0 steps of each trajectory
13,887 occupied cells, up to 814 locations in one cell

Plot the heatmap

Counts are drawn on a log colour scale — a handful of cells in some cells usually hold an order of magnitude more locations than the rest (possibly due to bias towards the release sites or getting ‘stuck’ in certain areas), which would flatten a linear scale to a single bright dot. Empty cells are masked so the landscape shows through.

Code
fig, ax = plt.subplots(figsize=(8.5, 7))
# Greyscale background so the (colourful) heatmap stays readable on top
rasterio.plot.show(
    landscape_tensors[0].numpy(), transform=raster_transform, ax=ax, cmap="Greys_r"
)
# ax.plot(
#     step_df["x1_"], step_df["y1_"],
#     "c.", alpha=0.15, markersize=0.5, label="observed",
# )
img = ax.imshow(
    np.ma.masked_equal(heatmap, 0),  # empty cells stay transparent
    extent=rasterio.plot.plotting_extent(heatmap, heatmap_transform),
    cmap="magma",
    norm=LogNorm(vmin=1, vmax=max(int(heatmap.max()), 2)),
    interpolation="nearest",
    alpha=0.85,
)
fig.colorbar(img, ax=ax, label=f"simulated locations per {PIXEL_SIZE * HEATMAP_AGG:.0f} m cell")
ax.set_xlabel("Easting (m, projected)")
ax.set_ylabel("Northing (m, projected)")
ax.set_title(
    f"Simulated space use ({heatmap.sum():,} locations, "
    f"steps {BURN_IN}+)"
)
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR
                / f"simulated_heatmap_{heatmap.sum()}loc"
                f"_{PIXEL_SIZE * HEATMAP_AGG:.0f}m_burnin{BURN_IN}.png"), dpi=300)
plt.show()

Save the heatmap as a raster (TIF)

save_raster()

Writes any landscape-shaped array out as a GeoTIFF, given the transform and CRS that georeference it — the heatmap here, or the habitat selection surface above. nodata=0 means unvisited cells are drawn as transparent rather than as a solid zero-valued block.

Code
# load_environmental_layers() does not return the CRS, so read it off the source
# raster the transform came from.
with rasterio.open(next(iter(LAYER_PATHS.values()))) as src:
    raster_crs = src.crs

heatmap_tif = deepssf.save_raster(
    heatmap,
    OUTPUT_DIR
    / f"simulated_heatmap_{heatmap.sum()}loc"
      f"_{PIXEL_SIZE * HEATMAP_AGG:.0f}m_burnin{BURN_IN}.tif",
    transform=heatmap_transform,
    crs=raster_crs,
    nodata=0,               # cells never visited
    band_descriptions=f"simulated locations per cell",
)
Wrote outputs/simulated_heatmap_1000000loc_200m_burnin0.tif (0.02 MB)

Toggle the heatmap on the interactive map

add_heatmap_overlay()

plot_trajectories_folium() returns the folium.Map, so the heatmap can be added to it afterwards as an image overlay. folium writes each layer’s JavaScript in the order it was added, so add_heatmap_overlay() moves the layer control back to the end — otherwise it would run before the overlay exists and throw, taking the rest of the map’s script with it. The heatmap then picks up its own checkbox alongside the observed locations and each simulated trajectory.

The counts are warped from the projected CRS to Web Mercator — the projection Leaflet stretches an overlay in — and coloured with the same log scale as the PNG, with cells below vmin (1 by default) left fully transparent.

Code
# add some of the simulated trajectories to the map
heat_ids = HEATMAP_DF["trajectory_id"].unique()[:3]

fmap_heat = deepssf.plot_trajectories_folium(
    HEATMAP_DF[HEATMAP_DF["trajectory_id"].isin(heat_ids)],
    observed=step_df,
    src_crs="EPSG:3112",
    basemap="esri_imagery",
    zoom_start=11,
    max_observed_points=100000,
)

deepssf.add_heatmap_overlay(
    fmap_heat,
    heatmap,
    heatmap_transform,
    src_crs="EPSG:3112",
    name=f"heatmap, (max {heatmap.max():,} per cell)",
    cmap="magma",
    opacity=0.75,
)

heatmap_html = OUTPUT_DIR / "simulated_heatmap_map.html"
fmap_heat.save(str(heatmap_html))
print(f"Map saved: {heatmap_html}")
fmap_heat
Map saved: outputs/simulated_heatmap_map.html
Make this Notebook Trusted to load map: File -> Trust Notebook

Estimating landscape connectivity with betweenness

Such as from:

Bastille-Rousseau, G., Douglas-Hamilton, I., Blake, S., Northrup, J. M., & Wittemyer, G. (2018). Applying network theory to animal movements to identify properties of landscape space use. Ecological Applications: A Publication of the Ecological Society of America, 28(3), 854–864. https://doi.org/10.1002/eap.1697

and

Hofmann, D. D., Cozzi, G., McNutt, J. W., Ozgul, A., & Behr, D. M. (2023). A three-step approach for assessing landscape connectivity via simulated dispersal: African wild dog case study. Landscape Ecology, 38(4), 981–998. https://doi.org/10.1007/s10980-023-01602-4

Betweenness

Quantifies the importance of different elements of the matrix in facilitating connectivity between other patches or habitats within the landscape. It identifies key corridors or bottlenecks that serve as critical links for maintaining overall connectivity in the landscape. It is calculated as the number of times the simulated individuals enter each raster cell (which are typically aggregated to a coarser scale than the spatial covariates used in the simulations). Betweenness measures the importance of each cell within the landscape for maintaining functional connectivity

Adapted from: https://github.com/DavidDHofmann/DispersalSimulation/blob/main/AWDCaseStudy/02_Analysis/09_Betweenness.R

How it is calculated here

Following the R script, in four steps:

  1. A coarse grid of nodes. The landscape rasters are 25 m, which can make calculating betweenness very computationally expensive, so they are aggregated by BETWEENNESS_AGG into larger cells. Each cell is one node in the network. The grid keeps the rasters’ origin and CRS, so it lines up with the covariates and with the heatmap above (although different aggregation factors may mean they don’t have the same resolution).
  2. Transitions become edges. Each move from one cell into another is an edge. It is constrained here that trajectory contributes a given (from, to) pair only once, however often it repeats it (singlecount = TRUE in the R script), so the pooled count is “how many of the simulated individuals ever made this move”, not “how many times it happened”. This prevents artifacts with really high counts from if some simulated individuals got stuck into a small area and went back and forth thousands of times between just a couple cells.
  3. Edge weights invert the counts: weight = mean(count) / count. networkx reads the weight as a cost, so a transition many simulated individuals made is cheap and shortest paths follow the routes the simulation actually took.
  4. Betweenness of a node is the number of those shortest paths running through it. Cells with high betweenness are the ones movement is funnelled through.

path_cells() and transition_counts()

  • Inputs
    • BETWEENNESS_DF
      • simulated locations, as returned by simulate_trajectories() — the same input trajectory_heatmap() takes.
    • BETWEENNESS_AGG
      • aggregate agg x agg raster pixels into one network cell. Betweenness is an all-pairs shortest path calculation, so the node count sets the runtime: 500 m cells give 3,672 nodes and about a minute, 250 m cells give 15,651 and about twenty.
    • BETWEENNESS_EPS
      • spacing, in cell widths, at which each step is densified before the cells under it are read off. Simulated steps have a median length of ~26 m but reach ~700 m, so a long step can jump clean over a 500 m cell; densifying catches it.
    • BETWEENNESS_BURN_IN, BETWEENNESS_MAX_STEPS
      • drop the first n steps of every trajectory, then keep at most m of what is left.
    • BETWEENNESS_K
      • None computes betweenness exactly; an integer estimates it from that many randomly chosen pivot nodes, which is what makes a finer grid tractable.
  • Outputs: betweenness — a float32 array on the coarsened grid, plus its affine transform bet_transform, ready for save_raster() and add_heatmap_overlay() just like the heatmap.

BETWEENNESS_MAX_STEPS Given enough steps every trajectory eventually visits most of the landscape, every adjacent pair of cells picks up an edge from nearly every trajectory, the weights flatten towards uniform, and betweenness ends up describing the grid rather than the movement. The R script on David’s GitHub handles this by recomputing the map at several step budgets (design$steps = c(125, 500, 2000)) and checking the pattern has settled; the same is worth doing here.

Code
BETWEENNESS_DF        = sim_scattered  # trajectories to build the network from
BETWEENNESS_AGG       = 8              # network cell = AGG x AGG raster pixels
BETWEENNESS_BURN_IN   = 0              # drop the first n steps of every trajectory
BETWEENNESS_MAX_STEPS = 10_000         # ...and keep at most this many of what is left
BETWEENNESS_EPS       = 0.25           # densify each step to this spacing, in cell widths
BETWEENNESS_K         = None           # None = exact; an int samples that many pivot nodes

# The network grid: the landscape rasters' origin and CRS with cells AGG times larger.
# ceil() rather than round() so the grid covers the whole raster — the same arithmetic
# trajectory_heatmap() uses, which is what makes the two grids line up cell for cell.
bet_height = math.ceil(landscape_tensors[0].shape[0] / BETWEENNESS_AGG)
bet_width  = math.ceil(landscape_tensors[0].shape[1] / BETWEENNESS_AGG)
bet_transform = raster_transform * Affine.scale(BETWEENNESS_AGG)
N_NODES = bet_height * bet_width
BET_CELL_M = PIXEL_SIZE * BETWEENNESS_AGG

def path_cells(x, y, eps=BETWEENNESS_EPS):
    """The sequence of network cells one simulated path passes through.

    Points outside the raster come back as -1 rather than being dropped, so that
    a buffalo which leaves the landscape and returns does not look like one long
    move between the cells either side of the excursion.
    """
    col, row = ~bet_transform * (np.asarray(x, dtype=float), np.asarray(y, dtype=float))
    if col.size < 2:
        return np.empty(0, dtype=np.int64)

    # Densify each step into enough sub-points that none can skip over a cell.
    # Most steps are far shorter than a cell and get n_sub = 1, so this costs
    # little; the occasional ~700 m step is the reason it is here at all.
    dcol, drow = np.diff(col), np.diff(row)
    n_sub = np.maximum(1, np.ceil(np.hypot(dcol, drow) / eps)).astype(np.int64)
    seg   = np.repeat(np.arange(n_sub.size), n_sub)      # step each sub-point belongs to
    start = np.repeat(np.cumsum(n_sub) - n_sub, n_sub)   # index of that step's first one
    frac  = (np.arange(n_sub.sum()) - start) / np.repeat(n_sub, n_sub)
    # Each step contributes its start and the points along it; the very last
    # location is the start of no step, so it is appended by hand.
    col_i = np.append(col[seg] + frac * dcol[seg], col[-1])
    row_i = np.append(row[seg] + frac * drow[seg], row[-1])

    c = np.floor(col_i).astype(np.int64)
    r = np.floor(row_i).astype(np.int64)
    inside = (r >= 0) & (r < bet_height) & (c >= 0) & (c < bet_width)
    node = np.where(inside, r * bet_width + c, -1)   # node ids run in raster order
    # Consecutive sub-points usually land in the same cell.  Keeping only the
    # changes leaves one entry per cell entered, and no from == to self-loops.
    return node[np.r_[True, node[1:] != node[:-1]]]


def transition_counts(df, id_col="trajectory_id"):
    """Pool cell-to-cell transitions over every trajectory.

    A trajectory contributes each (from, to) pair once however often it repeats
    it, so the counts returned are the number of *trajectories* that ever made
    each move.  Trajectories are handled one at a time rather than as a single
    array: at 599 x 100,000 locations the intermediate keys would otherwise run
    to several GB.
    """
    keys, n_edge_drops = [], 0
    for _tid, sub in df.groupby(id_col, sort=True):
        node = path_cells(sub["x"].to_numpy(), sub["y"].to_numpy())
        if node.size < 2:
            continue
        src, dst = node[:-1], node[1:]
        ok = (src >= 0) & (dst >= 0)          # drop moves touching the outside
        n_edge_drops += int((~ok).sum())
        keys.append(np.unique(src[ok] * N_NODES + dst[ok]))
    if not keys:
        raise ValueError(
            "no transitions — no trajectory entered more than one network cell"
        )
    key, count = np.unique(np.concatenate(keys), return_counts=True)
    return key // N_NODES, key % N_NODES, count, n_edge_drops


print(f"Network grid: {bet_height} x {bet_width} = {N_NODES:,} cells of {BET_CELL_M:.0f} m")
Network grid: 95 x 147 = 13,965 cells of 200 m

Build the network and calculate betweenness

The period filter is the same filter_period() the heatmap uses, so a betweenness surface and a heatmap built with the same PERIOD describe the same slice of the year.

This chunk can take some time to run!

Code
bet_df = BETWEENNESS_DF[
    (BETWEENNESS_DF["step"] >= BETWEENNESS_BURN_IN)
    & (BETWEENNESS_DF["step"] < BETWEENNESS_BURN_IN + BETWEENNESS_MAX_STEPS)
]
if bet_df.empty:
    raise ValueError(
        f"no simulated locations left — steps {BETWEENNESS_BURN_IN} to "
        f"{BETWEENNESS_BURN_IN + BETWEENNESS_MAX_STEPS}"
    )

# Reused by the plot, GeoTIFF and web map below so they all describe the same subset.
N_BET_TRAJ = bet_df["trajectory_id"].nunique()

t0 = time.time()
bet_src, bet_dst, bet_count, n_edge_drops = transition_counts(bet_df)
print(f"Period: {len(bet_df):,} locations from {N_BET_TRAJ} "
      f"trajectories, steps {BETWEENNESS_BURN_IN} to "
      f"{BETWEENNESS_BURN_IN + BETWEENNESS_MAX_STEPS - 1}")
print(f"{len(bet_src):,} unique transitions found in {time.time() - t0:.0f} s "
      f"({n_edge_drops:,} dropped at the raster edge)")

# networkx reads the weight as a cost, so invert the counts: a transition many
# trajectories made is cheap, and shortest paths prefer it.
bet_weight = bet_count.mean() / bet_count

# Every cell is a node, including the ones no trajectory reached — they sit in the
# network isolated and score zero, as `vertices = ver` does in the R script.
bet_net = nx.DiGraph()
bet_net.add_nodes_from(range(N_NODES))
bet_net.add_weighted_edges_from(
    zip(bet_src.tolist(), bet_dst.tolist(), bet_weight.tolist(), strict=True)
)
print(f"Network: {bet_net.number_of_nodes():,} nodes, {bet_net.number_of_edges():,} edges")

t0 = time.time()
bet_scores = nx.betweenness_centrality(
    bet_net, k=BETWEENNESS_K, weight="weight", normalized=False, seed=1
)
print(f"Betweenness computed in {time.time() - t0:.0f} s"
      + ("" if BETWEENNESS_K is None else f" (approximate, {BETWEENNESS_K} pivots)"))

# Cells no trajectory ever entered are left as NaN rather than 0, so the map can
# tell them apart from cells that are in the network but on no shortest path.
in_network = np.zeros(N_NODES, dtype=bool)
in_network[bet_src] = True
in_network[bet_dst] = True
betweenness = np.full(N_NODES, np.nan, dtype=np.float32)
betweenness[in_network] = [bet_scores[n] for n in np.flatnonzero(in_network)]
betweenness = betweenness.reshape(bet_height, bet_width)

print(f"{in_network.sum():,} of {N_NODES:,} cells were entered by at least one "
      f"trajectory; betweenness runs from 0 to {np.nanmax(betweenness):,.0f}")
Period: 1,000,000 locations from 1000 trajectories, steps 0 to 9999
87,631 unique transitions found in 0 s (0 dropped at the raster edge)
Network: 13,965 nodes, 87,631 edges
Betweenness computed in 720 s
13,958 of 13,965 cells were entered by at least one trajectory; betweenness runs from 0 to 34,891,108

Plot the betweenness surface

Drawn on a square-root scale, as in the R script — a handful of corridors carry far more shortest paths than anywhere else, and on a linear scale everything but those few cells goes black. Cells no trajectory entered are masked so the landscape shows through, the same way empty cells are masked on the heatmap.

Code
fig, ax = plt.subplots(figsize=(8.5, 7))
# Greyscale background so the (colourful) betweenness stays readable on top
rasterio.plot.show(
    landscape_tensors[0].numpy(), transform=raster_transform, ax=ax, cmap="Greys_r"
)
img = ax.imshow(
    np.ma.masked_invalid(np.sqrt(betweenness)),  # unentered cells stay transparent
    extent=rasterio.plot.plotting_extent(betweenness, bet_transform),
    cmap="magma",
    interpolation="nearest",
    alpha=1.0,
)
fig.colorbar(img, ax=ax, label=f"sqrt(betweenness) per {BET_CELL_M:.0f} m cell")
ax.set_xlabel("Easting (m, projected)")
ax.set_ylabel("Northing (m, projected)")
ax.set_title(
    f"Landscape connectivity ({N_BET_TRAJ} trajectories, "
    f"{BETWEENNESS_MAX_STEPS:,} steps)"
)
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR
                / f"betweenness_{N_BET_TRAJ}traj_{BET_CELL_M:.0f}m"
                f"_steps{BETWEENNESS_MAX_STEPS}.png"), dpi=300)
plt.show()

With some transparency to see the landscape features below

Here we can see the plateau feature being a barrier which appears to channel movement around it.

Code
fig, ax = plt.subplots(figsize=(8.5, 7))
# Greyscale background so the (colourful) betweenness stays readable on top
rasterio.plot.show(
    landscape_tensors[0].numpy(), transform=raster_transform, ax=ax, cmap="Greys_r"
)
img = ax.imshow(
    np.ma.masked_invalid(np.sqrt(betweenness)),  # unentered cells stay transparent
    extent=rasterio.plot.plotting_extent(betweenness, bet_transform),
    cmap="magma",
    interpolation="nearest",
    alpha=0.5,
)
fig.colorbar(img, ax=ax, label=f"sqrt(betweenness) per {BET_CELL_M:.0f} m cell")
ax.set_xlabel("Easting (m, projected)")
ax.set_ylabel("Northing (m, projected)")
ax.set_title(
    f"Landscape connectivity ({N_BET_TRAJ} trajectories, "
    f"{BETWEENNESS_MAX_STEPS:,} steps)"
)
plt.tight_layout()
plt.savefig(str(OUTPUT_DIR
                / f"betweenness_{N_BET_TRAJ}traj_{BET_CELL_M:.0f}m"
                f"_steps{BETWEENNESS_MAX_STEPS}.png"), dpi=300)
plt.show()

Save the betweenness surface as a raster (TIF)

Same save_raster() as the heatmap, on the coarsened transform. nodata=np.nan marks the cells no trajectory entered, so they open as transparent rather than as real zeros next to cells that genuinely scored zero.

Code
betweenness_tif = deepssf.save_raster(
    betweenness,
    OUTPUT_DIR
    / f"betweenness_{N_BET_TRAJ}traj_{BET_CELL_M:.0f}m"
      f"_steps{BETWEENNESS_MAX_STEPS}.tif",
    transform=bet_transform,
    crs=raster_crs,
    nodata=np.nan,          # cells no trajectory entered
    band_descriptions="betweenness per cell",
)
Wrote outputs/betweenness_1000traj_200m_steps10000.tif (0.05 MB)

Toggle betweenness on the interactive map

add_heatmap_overlay() takes any array on a regular grid, not just counts, so the betweenness surface goes onto a folium map exactly as the heatmap did — on the same square-root scale as the PNG above.

Code
bet_ids = bet_df["trajectory_id"].unique()[:3]

fmap_bet = deepssf.plot_trajectories_folium(
    bet_df[bet_df["trajectory_id"].isin(bet_ids)],
    observed=step_df,
    src_crs="EPSG:3112",
    basemap="esri_imagery",
    zoom_start=11,
    max_observed_points=100000,
)

# add_heatmap_overlay() warps the array with rasterio and has no NaN handling, so the
# unentered cells go back to 0 and are hidden by vmin instead.  sqrt rather than the
# function's log scale, to match the PNG.
deepssf.add_heatmap_overlay(
    fmap_bet,
    np.sqrt(np.nan_to_num(betweenness, nan=0.0)),
    bet_transform,
    src_crs="EPSG:3112",
    name=f"betweenness (max {np.nanmax(betweenness):,.0f})",
    cmap="magma",
    log_scale=False,
    vmin=1,
    opacity=0.95,
)

betweenness_html = OUTPUT_DIR / f"betweenness_map.html"
fmap_bet.save(str(betweenness_html))
print(f"Map saved: {betweenness_html}")
fmap_bet
Map saved: outputs/betweenness_map.html
Make this Notebook Trusted to load map: File -> Trust Notebook
Back to top

References

Signer, Johannes, John Fieberg, and Tal Avgar. 2019. “Animal Movement Tools (Amt): R Package for Managing Tracking Data and Conducting Habitat Selection Analyses.” Ecology and Evolution 9 (2): 880–90. https://doi.org/10.1002/ece3.4823.