Cheese3D enables sensitive detection and analysis of whole-face movement in mice.
The 19 matches
- [1] § Results › Uncovering underlying physiology from external facial movements ↔ paper/fig3-part1-cheese3d-general-anesthesia-eeg.ipynb, lines 553–643 · score 0.88 · EEG spectrogram, vertical scale bar, 0.2–1 Hz, EEG frequency band, FFT window, nose bulge volume
- [2] § Methods › Analysis of chewing kinematics ↔ paper/fig4-part1-chewing-whole-face-kinematics.ipynb, lines 681–738 · score 0.86 · find_peaks, median filtering, linearly interpolating, Kneedle, envelope, mouth area
- [3] § Methods › Analysis of in vivo electrical stimulation and electrophysiological recording ↔ fig5-part2-cheese3d-synchronized-electrophysiology.ipynb, lines 1110–1204 · score 0.82 · cyclic shuffling, corresponding lag, cross correlated, firing rates, facial movements, facial feature
- [4] § Methods › Anatomical-based interpretable feature selection ↔ packages/cheese3d/cheese3d/anatomy.py, lines 309–335 · score 0.81 · convex hull, right pad side, right pad top, nose bottom, axis, volume
- [5] § Results › Linking facial movement to motor control machineries using synchronized Cheese3D with electrophysiology ↔ fig5-part2-cheese3d-synchronized-electrophysiology.ipynb, lines 1110–1204 · score 0.81 · confidence interval, peak correlation, shuffled spike, cross correlation, firing rate, facial movements
- [6] § Methods › Analysis of in vivo electrical stimulation and electrophysiological recording ↔ fig5-part3-prediction-of-neural-activity-from-cheese3d.ipynb, lines 1113–1255 · score 0.80 · neural activity, cross validated, populationglm, intercept, GLMs, chunk
- [7] § Methods › Analysis of kinematics during anesthesia ↔ paper/fig3-part1-cheese3d-general-anesthesia-eeg.ipynb, lines 853–954 · score 0.79 · squared error, cross validation, eye height, nose bulge, PolynomialFeatures, ear angle
- [8] § Results › Reduction of tracking noise enables precise measurement of subtle and transient movements across facial regions ↔ paper/fig2-cheese3d-jitter-analysis.ipynb, lines 705–759 · score 0.75 · post triangulation, Jitter comparison, lateralized facial features, midline features, single mouse, camera view
- [9] § Results › Uncovering underlying physiology from external facial movements ↔ paper/fig3-part2-prediction-of-eeg-from-facial-features.ipynb, lines 1068–1157 · score 0.65 · sub delta, nose bulge volume, linear model, eye height, ear angle, VAR
- [10] § Results › Reduction of tracking noise enables precise measurement of subtle and transient movements across facial regions ↔ supfig5-cheese3d-jitter.ipynb, lines 925–1025 · score 0.63 · Whisker pad bulge, percentile jitter, Eye area, Nose bulge, Ear angle, facial region
- [11] § Results › Uncovering underlying physiology from external facial movements ↔ paper/fig3-part1-cheese3d-general-anesthesia-eeg.ipynb, lines 1155–1221 · score 0.61 · sub delta frequency, EEG power, frequency band power, theta, anesthetic, injection
- [12] § Results › Uncovering underlying physiology from external facial movements ↔ paper/fig3-part1-cheese3d-general-anesthesia-eeg.ipynb, lines 1223–1287 · score 0.56 · general anesthesia, movement raster, jitter threshold, facial movement, poses, vertical
- [13] § Methods › Video capture, synchronization and 3D calibration system ↔ packages/cheese3d/cheese3d/project.py, lines 478–557 · score 0.56 · square side length, board, ChArUco, pipeline, Anipose, triangulation
- [14] § Results › Uncovering underlying physiology from external facial movements ↔ paper/fig3-part2-prediction-of-eeg-from-facial-features.ipynb, lines 1377–1447 · score 0.54 · sub delta, EEG power, theta, latent, variance, predict
- [15] § Results › Linking facial movement to motor control machineries using synchronized Cheese3D with electrophysiology ↔ fig5-part3-prediction-of-neural-activity-from-cheese3d.ipynb, lines 1662–1774 · score 0.53 · explained variance, neural activity, ear angle, Poisson, predict, mice
- [16] § Results › Reduction of tracking noise enables precise measurement of subtle and transient movements across facial regions ↔ packages/cheese3d/cheese3d/interactive.py, lines 1–84 · score 0.53 · pose tracking, keypoint tracking, tool, Cheese3D, triangulation, training
- [17] § Methods › Analysis of kinematics during anesthesia ↔ paper/fig3-part2-prediction-of-eeg-from-facial-features.ipynb, lines 943–1027 · score 0.53 · fold cross validation, latent, smooth, predicted, EEG, linear
- [18] § Methods › Analysis of kinematics during anesthesia ↔ paper/fig3-part2-prediction-of-eeg-from-facial-features.ipynb, lines 1159–1243 · score 0.52 · latent model, eye height, nose bulge, ear angle, facial features, predict
- [19] § Methods › Analysis of chewing kinematics ↔ paper/fig4-part1-chewing-whole-face-kinematics.ipynb, lines 1178–1217 · score 0.52 · peak cross correlation, phases, mastication, ingestion, mouth area, chewing
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Jupyter notebook · 1,578 lines · 65 KB · MIT · 4 matches
- # %% [markdown]
- # # FIGURE 3 (Feb 2025 submission)
- # %% [markdown]
- # This notebook gathers the code to make all the panels in Figure 3. In order to run this notebook, you need the following anipose projects:
- # 1. 20231013-long-anes-rig2
- # 2. 202408-eeg-emg-all
- # %% [markdown]
- # ## Prep the notebook
- # %% [markdown]
- # ### Load libraries
- # %%
- %load_ext autoreload
- %autoreload 2
- # Update path as if notebook was run from top-level repo directory
- import os
- import sys
- pwd = %pwd
- if pwd.endswith('notebooks'):
- sys.path.insert(0, os.path.abspath('..'))
- new_pwd = os.path.abspath(f"{pwd}/..")
- %cd {new_pwd}
- # %%
- import numpy as np
- import pandas as pd
- import matplotlib.pyplot as plt
- import matplotlib.ticker as ticker
- import seaborn as sns
- import itertools
- import pickle
- import json
- from glob import glob
- from numpy import newaxis as na
- from datetime import datetime
- from pathlib import Path
- from functools import reduce
- import scipy.stats as stats
- from scipy.stats import pearsonr, ttest_rel, wilcoxon
- from scipy.signal import convolve, spectrogram, sosfilt, iirdesign
- from sklearn.preprocessing import PolynomialFeatures
- from sklearn.model_selection import GridSearchCV
- from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
- from matplotlib.colors import ListedColormap
- from datetime import datetime
- from fepipeline.features.landmarks import read_3d_data
- from fepipeline.anatomy import compute_measurements_df
- from labutils.utils import maybe
- from labutils.plotting import (sns_setup,
- landmark_cmap,
- measurements_cmap,
- save_figure)
- # %% [markdown]
- # ### Global variables
- # %%
- sns_setup(palette="colorblind", font="sans-serif")
- OUTPUT_DIR = "./figures/Figure3-EEG"
- os.makedirs(OUTPUT_DIR, exist_ok=True)
- TODAY = datetime.today().date()
- # size in frames of moving average filter window for seeing slow drift in behavior
- FILTER_WINDOW = 1000
- # Start of anesthesia video after injection (in seconds)
- ANES_START_OFFSETS = {
- ("20240822", "B47"): 4 * 60 + 22,
- ("20240822", "B48"): 3 * 60 + 50,
- ("20240912", "B47"): 4 * 60 + 17,
- ("20240912", "B48"): 4 * 60 + 17,
- ("20240913", "B47"): 6 * 60 + 30, #anes video only
- ("20240913", "B48"): 3 * 60 + 33, #anes video only
- ("20240917", "B47"): 4 * 60 + 40,
- ("20240917", "B48"): 4 * 60 + 3,
- ("20240918", "B47"): 3 * 60 + 20,
- ("20240918", "B48"): 3 * 60 + 45,
- ("20240924", "B53"): 3 * 60 + 5, #anes video only
- ("20240925", "B53"): 5 * 60 + 56,
- ("20240926", "B53"): 4 * 60 + 3, #anes video only
- ("20240927", "B53"): 3 * 60 + 0, #anes video only
- }
- REGION_ORDER = ["eye(left)", "eye(right)",
- "ear(left)", "ear(right)",
- "nose", "cheek", "mouth"]
- DATA_CACHE = "measurements-data-cache-2024"
- os.makedirs(DATA_CACHE, exist_ok=True)
- MEAS_DATA_CACHE = os.sep.join([DATA_CACHE, "eeg-slow-drift.pkl"])
- ALL_DATA_CACHE = os.sep.join([DATA_CACHE, "eeg-slow-drift-eeg-meas.pkl"])
- MODEL_RESULTS_CACHE = os.sep.join([DATA_CACHE, "long-anes-slow-drift-results.pkl"])
- # Data cache for time-prediction models using Lasso - Alpha optimization
- # Using facial features
- LASSO_MODEL_FF = os.sep.join([DATA_CACHE, f"{TODAY}-lasso-model-ff.pkl"])
- # Using eeg features
- LASSO_MODEL_EEG = os.sep.join([DATA_CACHE, f"{TODAY}-lasso-model-eeg.pkl"])
- ANIPOSE_BASE = 'anipose-projects/202408-eeg-emg-all'
- COORDINATE_PATHS = {}
- key_cols = ('date', 'mouse', 'condition')
- for p in Path(ANIPOSE_BASE).glob('*/pose-3d/*.csv'):
- date, mouse, *_ = p.name.split('_')
- if date in ["20240816", "20240822"]:
- continue
- condition = 'awake' if 'awake' in p.name else 'anes'
- COORDINATE_PATHS[(date, mouse, condition)] = p
- data_keys = list(COORDINATE_PATHS.keys())
- anes_data_keys = [d for d in data_keys if d[2] != 'awake']
- # %%
- def compute_sample_rate(timestamps):
- start_time = datetime.strptime(timestamps[0], "%H:%M:%S:%f")
- times = np.array([(datetime.strptime(t, "%H:%M:%S:%f") - start_time).total_seconds()
- for t in timestamps])
- return 1 / np.mean(np.diff(times))
- def read_signal(filename, sample_rate):
- df = pd.read_csv(filename, sep="\t", names=["timestamp", "signal"])
- _sample_rate = compute_sample_rate(df["timestamp"].values)
- if abs(sample_rate - _sample_rate) / sample_rate > 0.1:
- return df["signal"].values, _sample_rate
- else:
- return df["signal"].values, sample_rate
- # Remove NaNs at the beginning of the EEG recording and define new recording start
- EEG_STARTS = os.sep.join([DATA_CACHE, 'eeg-anes-start.pkl'])
- if os.path.exists(EEG_STARTS):
- with open(EEG_STARTS, "rb") as fio:
- ANES_START_OFFSETS_EEG = pickle.load(fio)
- else:
- ANES_START_OFFSETS_EEG = {}
- for date, mouse, cond in data_keys:
- folder = glob(f"ephys-data/{date}_*{mouse}*_EEG-EMG-rec_rig2")
- with open(os.sep.join([folder[0], f"{date}_{mouse}_{cond}.align.json"])) as f:
- alignment = json.load(f)
- lag_time = alignment["lag_time"]
- sample_rate = alignment["sample_rate"]
- for name, units in (("eeg", "V"), ("emg", "V"), ("temp", "C")):
- filename = os.sep.join([folder[0], f"{date}_{mouse}_{cond}_{name}.txt"])
- signal, _sample_rate = read_signal(filename, sample_rate)
- # if name in ['emg', 'eeg']:
- signal[np.isnan(signal)] = 0 #added by inm - assign the nan to 0s then save signal from first non-zero
- signal_start = signal.nonzero()[0][0]
- signal = signal[signal_start:]
- if name == 'eeg' and signal_start and cond == 'anes':
- ANES_START_OFFSETS_EEG[date, mouse] = ANES_START_OFFSETS[date, mouse] + int(signal_start/_sample_rate)
- # %% [markdown]
- # ### Define colormaps
- # %%
- _, LANDMARK_CMAP = landmark_cmap()
- _, MEASUREMENT_CMAP = measurements_cmap()
- CONTROL_CMAP = sns.color_palette([sns.color_palette("colorblind")[2],
- sns.color_palette("colorblind")[-3],
- sns.color_palette("colorblind")[3]])
- FREQ_CMAP = "jet"
- # %%
- EEG_CONTROL_CMAP = sns.color_palette([sns.color_palette("colorblind")[0],
- sns.color_palette("colorblind")[7],
- sns.color_palette("colorblind")[0],
- sns.color_palette("colorblind")[0]])
- EEG_CONTROL_CMAP
- # %%
- CONTROL_CMAP_V2 = sns.color_palette([sns.color_palette("colorblind")[3],
- sns.color_palette("colorblind")[-3],
- sns.color_palette("colorblind")[2],
- sns.color_palette("colorblind")[0],
- sns.color_palette("colorblind")[1],
- sns.color_palette("colorblind")[-1],
- sns.color_palette("colorblind")[4],
- sns.color_palette("colorblind")[-2]])
- CONTROL_CMAP_V2
- # %% [markdown]
- # ### Define functions
- # %%
- def z_score(raw_data):
- mean = np.mean(raw_data, 0, keepdims=True)
- sigma = np.std(raw_data, 0, keepdims=True) #1
- sigma[sigma == 0] = 1
- return (raw_data-mean)/sigma
- def un_zscore(raw_data, z_score_data): #Undo z-scoring for plotting as a sanity check - there is no deformation
- mean = np.mean(raw_data, 0, keepdims=True) #0
- sigma = np.std(raw_data, 0, keepdims=True) #1
- sigma[sigma == 0] = 1
- return z_score_data*sigma + mean
- # %%
- def moving_avg_filter(signal, window = 3, stride = 1, truncate = True):
- kernel = np.ones(window) / window
- result = convolve(signal, kernel, mode="same")[window:-window]
- return result[::stride]
- # %%
- test = moving_avg_filter(np.arange(12), window = 1, stride = 2)
- print(test)
- # %%
- def plot_spectrum_helper(dates, mice, conditions, spectrums, lag_times, color,
- min_power = None, max_power = None):
- single = (len(spectrums.values) == 1)
- if single:
- if min_power is None:
- min_power = np.min(spectrums.values[0][-1])
- if max_power is None:
- max_power = np.max(spectrums.values[0][-1])
- else:
- if min_power is None:
- min_power = min(*(np.min(s) for _, _, s in spectrums.values))
- if max_power is None:
- max_power = max(*(np.max(s) for _, _, s in spectrums.values))
- awake = (conditions == "awake")
- # print(awake)
- if sum(awake):
- fs, ts, power = spectrums[awake].values[0]
- awake_end = ts[-1]
- else:
- awake_end = 0
- fs, ts, power = spectrums[~awake].values[0]
- anes_start = ANES_START_OFFSETS[(dates.values[0], mice.values[0])]
- ts = ts + lag_times[~awake].values[0] + anes_start #+ awake_end
- # print(power)
- p = plt.pcolormesh(ts / 60, fs, power,
- vmin=min_power, vmax=max_power, cmap=FREQ_CMAP)
- # %%
- def compute_power_bands(fs, power):
- sub_delta_idx = np.where(fs < 1)[0]
- sub_delta_power = np.mean(power[sub_delta_idx], axis=0)
- delta_idx = np.where((fs >= 1) & (fs <= 4))[0]
- delta_power = np.mean(power[delta_idx], axis=0)
- theta_idx = np.where((fs >= 5) & (fs <= 10))[0]
- theta_power = np.mean(power[theta_idx], axis=0)
- # slow_delta_idx = np.where(fs <= 4)[0]
- # slow_delta_power = np.mean(power[slow_delta_idx], axis=0)
- return sub_delta_power, delta_power, theta_power #, slow_delta_power
- # %%
- def generate_lagged_data(data, lags = 1, bias = True, initial_bias = True):
- assert data.ndim == 2 or data.ndim == 1
- # make the data a matrix
- if data.ndim == 1:
- data = np.expand_dims(data, axis=1)
- # make shifted copies of data
- lagged = np.lib.stride_tricks.sliding_window_view(data, lags, axis=0)
- lagged = np.reshape(lagged, (data.shape[0] - lags + 1, -1))
- # pad in the initial lags
- if lags > 1:
- pad = np.stack([np.pad(data[:(lags - i)], ((i, 0), (0, 0))).T
- for i in range(lags - 1, 0, -1)])
- pad = np.reshape(pad, (lags - 1, -1))
- lagged = np.concatenate([pad, lagged], axis=0)
- # always have initial time point
- if initial_bias:
- lagged = np.concatenate([np.tile(data[0], (lagged.shape[0], 1)), lagged], axis=1)
- # add bias term
- if bias:
- lagged = np.pad(lagged, ((0, 0), (1, 0)), constant_values=1)
- return lagged
- # Uncomment prints for examples:
- x = np.arange(10)
- y = np.stack([x, x + 10], axis=1)
- # print(generate_lagged_data(x, 5))
- # print(generate_lagged_data(y, 3))
- # %%
- def generate_poly_feats(data, degree = 2, interaction = True):
- assert data.ndim == 2 or data.ndim == 1
- # make the data a matrix
- if data.ndim == 1:
- data = np.expand_dims(data, axis=1)
- if interaction == True:
- return PolynomialFeatures(degree).fit_transform(data)
- else:
- polyfeats = np.vstack((np.ones(len(data)), data.T)).T
- for n in (np.arange(1,degree)+1):
- polyfeats = np.hstack((polyfeats, data**n))
- return polyfeats
- # Uncomment prints for examples:
- z = np.stack([x, x + 10, x + 1], axis=1)
- print(generate_lagged_data(x, bias=False))
- print(generate_poly_feats(generate_lagged_data(z, bias=False)))
- print(generate_poly_feats(generate_lagged_data(x, bias=False), 3))
- # %%
- def get_features(measures, degree = 2, lags = 1, interaction = True, initial_bias = True):
- features = generate_lagged_data(measures, lags, bias=False, initial_bias = initial_bias)
- features = generate_poly_feats(features, degree, interaction)
- return features
- # %%
- def plot_predictions(ts, xs, color, label, FILTER_WINDOW = 1000, dash_line = True, ax = None):
- ts = ts.values[0][FILTER_WINDOW:-FILTER_WINDOW]
- xs = moving_avg_filter(xs.values[0], FILTER_WINDOW)
- if ax is None:
- sns.lineplot(x=ts, y=xs, color=color, label=label)
- if dash_line:
- sns.lineplot(x=ts, y=ts, color="black", linestyle="dashed")
- else:
- sns.lineplot(x=ts, y=xs, color=color, label=label, ax = ax)
- if dash_line:
- sns.lineplot(x=ts, y=ts, color="black", linestyle="dashed", ax = ax)
- # %%
- def format_feature_ticks(feature):
- text = feature.get_text()
- if text == "whole-face":
- return "whole\nface"
- if ' ' in text:
- parts = text.split(" ")
- return'\n'.join(parts)
- else:
- parts = text.split(", ")
- return ',\n'.join(parts)
- # %%
- def test_mean_per_run(results, n_runs, feat):
- mean_test = []
- for r in np.arange(n_runs):
- mean_test.append([r, feat, np.mean(results.query("set == 'Test' & run == @r")["RMSE"].values)])
- return pd.DataFrame(mean_test, columns = ["run",
- "features",
- "mean_RMSE"])
- # %% [markdown]
- # ### Load data
- # %%
- if os.path.exists(MEAS_DATA_CACHE):
- meas_df = pd.read_pickle(MEAS_DATA_CACHE)
- else:
- coord_data = {k: read_3d_data(v.parent.parent.as_posix())
- for k, v in COORDINATE_PATHS.items()}
- meas_df = compute_measurements_df(coord_data, key_columns=key_cols)
- meas_df = meas_df.assign(sample_rate=100.0, lag_time=0.0)
- pd.to_pickle(meas_df, MEAS_DATA_CACHE)
- meas_df = meas_df.drop(columns=["measurement_value", "std", "count"])
- meas_df
- # %%
- if os.path.exists(ALL_DATA_CACHE):
- data = pd.read_pickle(ALL_DATA_CACHE)
- else:
- eeg_rows = []
- for date, mouse, cond in data_keys:
- folder = glob(f"ephys-data/{date}_*{mouse}*_EEG-EMG-rec_rig2")
- if len(folder) != 1:
- print(f"Found too many (or no) sources={folder} for {date=}, {mouse=}, {cond=}")
- continue
- with open(os.sep.join([folder[0], f"{date}_{mouse}_{cond}.align.json"])) as f:
- alignment = json.load(f)
- lag_time = alignment["lag_time"]
- sample_rate = alignment["sample_rate"]
- for name, units in (("eeg", "V"), ("emg", "V"), ("temp", "C")):
- filename = os.sep.join([folder[0], f"{date}_{mouse}_{cond}_{name}.txt"])
- signal, _sample_rate = read_signal(filename, sample_rate)
- # if name in ['emg', 'eeg']:
- signal[np.isnan(signal)] = 0 #added by inm - assign the nan to 0s then save signal from first non-zero
- signal_start = signal.nonzero()[0][0]
- signal = signal[signal_start:]
- if name == 'eeg' and signal_start:
- ANES_START_OFFSETS[date, mouse] = ANES_START_OFFSETS[date, mouse] + signal_start/_sample_rate
- eeg_rows.append([date, mouse, cond,
- "ephys", name, units, "ephys",
- signal, _sample_rate, lag_time])
- data = pd.concat([meas_df, pd.DataFrame(eeg_rows, columns=meas_df.columns)])
- pd.to_pickle(data, ALL_DATA_CACHE)
- data
- # %%
- from copy import deepcopy
- def compute_spectrogram(row):
- signal = row["timeseries"]
- # if np.sum(np.isnan(signal))>0:
- # print("signal", np.sum(np.isnan(signal)))
- # signal[np.isnan(signal)] = 0 #added by inm - assign the nan to 0s so that they do not propagate when computing spectrogram - to be improved
- eeg_filt = iirdesign(50, 55, 1, 50, fs=row["sample_rate"], output="sos")
- fs, ts, power = spectrogram(sosfilt(eeg_filt, signal),
- fs=row["sample_rate"],
- nperseg= 5000,#2000, #originally 2048 ; possibly change to 2050 or 2000 (divisible by 10) - number of samples in each fft segment
- # noverlap=512,
- scaling="spectrum",
- mode="magnitude")
- # if np.sum(np.isnan(power)):
- # print("power", np.sum(np.isnan(power))) #nans are not here but appear in the plot
- row = row.copy()
- row["measurement_name"] = "eeg-power"
- row["measurement_units"] = "V^2"
- row["timeseries"] = (fs, ts, power)
- return row
- eeg_df = deepcopy(data.query("measurement_name == 'eeg'")) #brand new copy so that I can pass by object (instead of by reference) and do not alter the original data
- data_v2 = pd.concat([data, eeg_df.apply(compute_spectrogram, axis=1)])
- data_v2
- # %%
- data_and_bands = deepcopy(data_v2)
- eeg_window = 8 #20
- band_names = ['subdelta', 'delta', 'theta']
- for d, m, c in anes_data_keys:
- fs, ts, power = data_and_bands.query("mouse == @m & date == @d & condition == @c & measurement_name == 'eeg-power'")["timeseries"].values[0]
- bands = compute_power_bands(fs, power)
- for i_b, _b in enumerate(bands):
- bands_dict = {'date': d,
- 'mouse': m,
- 'condition': c,
- 'measurement_group': 'ephys',
- 'measurement_name': band_names[i_b],
- 'measurement_units': 'V^2',
- 'measurement_type': 'ephys',
- #Save power band values and actual time together
- 'timeseries': (z_score(moving_avg_filter(_b, window = eeg_window, stride = 1)),
- ts[eeg_window:-eeg_window]),
- 'sample_rate': data_and_bands.query("mouse == @m & date == @d & condition == @c & measurement_name == 'eeg-power'")["sample_rate"].values[0],
- 'lag_time': data_and_bands.query("mouse == @m & date == @d & condition == @c & measurement_name == 'eeg-power'")["sample_rate"].values[0]}
- data_and_bands = pd.concat([data_and_bands, pd.DataFrame.from_records([bands_dict])])
- data_and_bands
- # %%
- print('Example mouse B48, date 20240917, condition anes.')
- print('Number of raw EEG samples: ', data.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg' & condition == 'anes'")["timeseries"].values[0].shape)
- print('Sample rate: ', data.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg' & condition == 'anes'")["sample_rate"].values[0])
- print('Number of power EEG samples: ', data_v2.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg-power' & condition == 'anes'")["timeseries"].values[0][1].shape)
- print('The EEG signal was downsampled by: ',
- data.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg' & condition == 'anes'")["timeseries"].values[0].shape[0]/
- data_v2.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg-power' & condition == 'anes'")["timeseries"].values[0][1].shape[0])
- new_sr = data.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg' & condition == 'anes'")["sample_rate"].values[0]/(data.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg' & condition == 'anes'")["timeseries"].values[0].shape[0]/
- data_v2.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg-power' & condition == 'anes'")["timeseries"].values[0][1].shape[0])
- print('Therefore the new sampling rate is: ', new_sr)
- print('And first three timepoints of EEG power are found at: ', data_v2.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg-power' & condition == 'anes'")["timeseries"].values[0][1][0:3], 'seconds')
- print('\nDownsampling by the spectrum function is more or less constant across mice and sessions:')
- print(data.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg' & condition == 'anes'")["timeseries"].values[0].shape[0]/
- data_v2.query("mouse == 'B48' & date == '20240917' & measurement_name == 'eeg-power' & condition == 'anes'")["timeseries"].values[0][1].shape[0])
- print(data.query("mouse == 'B47' & date == '20240918' & measurement_name == 'eeg' & condition == 'anes'")["timeseries"].values[0].shape[0]/
- data_v2.query("mouse == 'B47' & date == '20240918' & measurement_name == 'eeg-power' & condition == 'anes'")["timeseries"].values[0][1].shape[0])
- # %%
- cam_sr = 100
- eeg_power_sr = {}
- ds_factor = {}
- for date, mouse, _ in anes_data_keys:
- eeg_power_sr[mouse, date] = data_v2.query("mouse == @mouse & date == @date & measurement_name == 'eeg' & condition == 'anes'")["sample_rate"].values[0]/(
- data_v2.query("mouse == @mouse & date == @date & measurement_name == 'eeg' & condition == 'anes'")["timeseries"].values[0].shape[0]/
- data_v2.query("mouse == @mouse & date == @date & measurement_name == 'eeg-power' & condition == 'anes'")["timeseries"].values[0][1].shape[0])
- ds_factor[mouse, date] = int(np.floor(cam_sr / eeg_power_sr[mouse, date])) # camera sample rate / eeg power sample rate
- ds_factor
- # %% [markdown]
- # ## Figure 3c
- # %% [markdown]
- # (notebook: 2024-12-05-long-anes-three-way-predictions-eeg.ipynb)
- # %% [markdown]
- # Overlaid z-scored ear angle, eye height, and nose bulge volume traces from 12 sessions (n = 3 mice, four sessions per mouse. Vertical scale bar indicates one standard deviation).
- # %%
- # f_window = 1000 #1000
- features = ['ear-angle-left', 'eye-height-left', 'nose-bulge-volume']
- fig, axs = plt.subplots(nrows=len(features),
- ncols=1,
- figsize=(3*3, 4*3),
- dpi=200,
- constrained_layout=False)
- meas_summary = {}
- time_summary = {}
- for row, l in enumerate(features):
- meas_summary[l] = []
- time_summary[l] = []
- y_labels = []
- for sess in anes_data_keys:
- mouse = sess[1]
- date = sess[0]
- if sess in anes_data_keys:
- anes_start = np.max(list(ANES_START_OFFSETS.values())) - ANES_START_OFFSETS[date, mouse] #in seconds - Take the same start for all of them
- ys = data_v2.query(
- "mouse == @mouse & "
- "date == @date & "
- "condition == 'anes' & "
- "measurement_name == @l"
- )["timeseries"].values[0]
- anes_end = int(np.floor((45 * 60 - ANES_START_OFFSETS[date, mouse]))) * 100 # Trim at 45 minutes since anesthesia injection
- ys = ys[int(np.floor(anes_start)*100):anes_end]
- time = (np.arange(len(ys))/100 + np.max(list(ANES_START_OFFSETS.values())))/ 60 #Note all will start at 5 min and camera sample rate is 100
- time = time[FILTER_WINDOW:-FILTER_WINDOW]
- # FILTER AND Z SCORED DATA PER MEASUREMENT
- ys = moving_avg_filter(ys, FILTER_WINDOW)
- ys = z_score(ys)
- meas_summary[l].append(ys)
- time_summary[l].append(time)
- sns.lineplot(x = time, y = ys,
- color = CONTROL_CMAP_V2[0], linewidth = 0.5, alpha = 0.5,
- ax = axs[row])
- sns.lineplot(x = time, y = np.mean(meas_summary[l], axis = 0),
- color = CONTROL_CMAP_V2[0], linewidth = 2.5,
- ax = axs[row])
- axs[row].sharex(axs[0])
- axs[row].set_ylabel(('\n').join(l.split('-')), rotation = 0)
- axs[row].yaxis.set_label_coords(-0.1,0.5)
- if row != (len(features)-1):
- axs[row].tick_params("x", bottom=False, labelbottom=False)
- sns.despine(ax=axs[row], bottom=True)
- axs[row].set_xlabel('Time (s)')
- axs[row].set_xticks(np.arange(0, 45, 10))
- sns.despine(ax=axs[row])
- fig.tight_layout()
- fname = "3c-facial-feat-3subset-allmice-10s-avgfilter"
- fig.savefig(os.sep.join([OUTPUT_DIR, f"{fname}.svg"]), bbox_inches="tight")
- # %% [markdown]
- # ## Figure 3d
- # %% [markdown]
- # (notebook: 2024-08-30-eeg-emg-long-anes.ipynb)
- # %% [markdown]
- # Simultaneously recorded Cheese3D features (top, showing moving average over a 10 sec window; vertical scale bars: ear: 2◦, eye: 0.1mm, nose: 0.5mm3), EEG spectrogram (middle, 5sec FFT window), and power of EEG frequency bands (bottom, showing subdelta: 0.2 Hz to 1 Hz, delta: 1 Hz to 4 Hz, and theta: 5 Hz to 10 Hz bands; vertical scale bars indicate one standard deviation) for an example session.
- # %%
- max_power = np.max(data_v2.query("measurement_name == 'eeg-power'").apply(
- lambda x: np.max(x["timeseries"][-1]),
- axis=1
- ).values)
- min_power = np.min(data_v2.query("measurement_name == 'eeg-power'").apply(
- lambda x: np.min(x["timeseries"][-1]),
- axis=1
- ).values)
- def plot_aligned_helper(dates, mice, conditions, style, timeseries, lags, sample_rate, color):
- anes_start = ANES_START_OFFSETS[(dates.values[0], mice.values[0])] #in seconds
- anes_end = -1 #np.floor((45 * 60 - ANES_START_OFFSETS[(dates.values[0], mice.values[0])])) # in seconds - Trim at 45 minutes since anesthesia injection
- awake = (conditions == "awake")
- if style.values[0] == "eeg-power":
- plot_spectrum_helper(dates, mice, conditions, timeseries, lags, color,
- min_power=min_power, max_power=max_power)
- elif style.values[0] in band_names:
- _b = timeseries[~awake].values[0][0] #this has already been filtered and zscored (see cell above)
- ts = timeseries[~awake].values[0][1]
- sns.lineplot(x=(ts[:anes_end]+anes_start)/60,
- y=_b[:anes_end])
- else:
- ys = timeseries[~awake].values[0]
- ys = ys[:anes_end]
- xs = np.arange(len(ys)) / sample_rate[~awake].values[0]
- xs = xs + anes_start #+ awake_end
- sns.lineplot(x=xs[FILTER_WINDOW:-FILTER_WINDOW] / 60,
- y=moving_avg_filter(ys, FILTER_WINDOW),
- color=CONTROL_CMAP[2],
- label="anes")
- measurements = ["ear-angle-left",
- "eye-height-left",
- "nose-bulge-volume",
- "eeg-power",
- "theta",
- "delta",
- "subdelta",
- ]
- for d in set(data_and_bands["date"]):
- if d == '20240822':
- continue
- g = sns.FacetGrid(data_and_bands.query("date == @d & "
- "measurement_name in @measurements"),
- row="measurement_name", col="mouse",
- row_order=measurements,
- sharey=False,
- sharex=False,
- # xlim=(0, 70),
- aspect=2,
- height=2)
- g.map(plot_aligned_helper,
- "date", "mouse", "condition",
- "measurement_name", "timeseries", "lag_time", "sample_rate")
- cbar_ax = g.figure.add_axes([1.015, 0.44, 0.015, 0.12]) #[1.015, 0.19, 0.015, 0.12]
- plt.colorbar(cax=cbar_ax)
- g.figure.suptitle(f"Anesthetized Spectrogram ({d})")
- g.set_xlabels("Time (min)")
- g.set_ylabels("")
- sns.despine(g.figure)
- for ax in g.axes.flat:
- # if ax.get_xbound()[1] > 65:
- # ax.set_xticks(np.arange(0, 71, 10))
- # else:
- ax.set_xticks(np.arange(0, 45, 10))
- ax.set_xlim(0, 45)
- for ax in g.axes[-4, :]: # Limit for the EEG spectrogram
- ax.set_ylim(0, 20)
- for ax in g.axes[:-1, :].flat: # Remove x-ticks except in bottom plot
- ax.set_xticks([])
- sns.despine(ax=ax, bottom=True)
- # for ax in g.axes[-1, :]:
- # axmin = min(ax.get_xlim()[0], ax.get_ylim()[0])
- # axmax = max(ax.get_xlim()[1], ax.get_ylim()[1])
- # ax.plot(np.linspace(axmin, axmax), np.linspace(axmin, axmax), color='black', linestyle="--")
- # ax.set_aspect('equal', anchor = 'SW', adjustable = 'box')
- g.set_titles(template="{col_name} ({row_name})")
- g.tight_layout()
- fname = f"3d-fe-and-eeg-exemplar-{d}"
- # g.figure.savefig(os.sep.join([OUTPUT_DIR, f"{fname}.svg"]), bbox_inches="tight")
- # %% [markdown]
- # ## Figure 3e
- # %% [markdown]
- # (notebook: 2024-12-05-long-anes-three-way-predictions-eeg)
- # %% [markdown]
- # Output from a quadratic model fit across mice predicting time since injection using the initial and current Cheese3D (orange) or EEG (blue) feature values relative to the dotted identity line.
- # %%
- EEG_MODEL_NAME = os.sep.join([DATA_CACHE, "lasso-model-mouse-sess-eeg-220runs-interact-stride-init-bias-45minend-Jan30-2ndfix.pkl"])
- FF_MODEL_NAME = os.sep.join([DATA_CACHE, "lasso-model-mouse-sess-220runs-interact-stride-init-bias-45minend-Jan30-2ndfix.pkl"])
- results_eeg_df_name = 'measurements-data-cache-2024/prediction-eeg-results-220runs-interact-stride-init-bias-45minend-Jan30-2ndfix.pkl'
- results_df_name = 'measurements-data-cache-2024/prediction-results-220runs-interact-stride-init-bias-45minend-Jan30-2ndfix.pkl'
- # %% [markdown]
- # Note that the names above correspond to the files used for the manuscript results. If you would like to use a different model, you will need to change the specified names.
- #
- # Comment the cell below if you would like to run the model code.
- # %%
- # These are the models and predictions used in the manuscript
- # EEG models and results
- if os.path.exists(EEG_MODEL_NAME):
- with open(EEG_MODEL_NAME, "rb") as fio:
- models_eeg = pickle.load(fio) # Model results
- if os.path.exists(results_eeg_df_name):
- results_eeg_df = pd.read_pickle(results_eeg_df_name) # Prediction results
- # Facial feature models
- if os.path.exists(FF_MODEL_NAME):
- with open(FF_MODEL_NAME, "rb") as fio:
- models_ff = pickle.load(fio)
- if os.path.exists(results_eeg_df_name):
- results_df = pd.read_pickle(results_df_name)
- # %% [markdown]
- # ### EEG model
- # %%
- # def run_regression(ts_df, anes_data_keys, features, train_idx, alpha_range, models = None):
- def run_regression(ts_df, anes_data_keys, alpha_range):
- # specify feature parameters
- # 20 filter window in samples ~ 40 seconds if nperseg = 2000;
- # 8 filter window in samples ~ 40 seconds if nperseg = 5000
- # refer to prints about eeg-power sample rate in cell above)
- eeg_window = 8
- # Polynomial features
- degree = 2 #degree of the polynomial
- lag = 1
- # define empty lists to generate data for model
- measures = []
- target = []
- indeces = []
- counter = 0
- for s_idx, dk in enumerate(anes_data_keys):
- d = dk[0]
- mouse = dk[1]
- # Select input features: eeg-power bands
- eegpower = ts_df.query("mouse == @mouse & date == @d & condition == 'anes' & measurement_name == 'eeg-power'")["timeseries"]
- fs, ts, power = eegpower.values[0]
- _bands = compute_power_bands(fs, power) #type(_bands) = tuple
- # Define actual time
- anes_start = ANES_START_OFFSETS_EEG[(d, mouse)]
- anes_end = int(np.floor((45 * 60 - anes_start) * eeg_power_sr[mouse, d])) # Trim at 45 minutes since anesthesia injection
- _time = ts[:anes_end]
- target.append((_time[eeg_window:-eeg_window] + anes_start) / 60) #lag is only for plotting comparisons against facial features?
- # Filter noise
- bands =[]
- for _b in _bands:
- bands.append(moving_avg_filter(_b[:anes_end], window = eeg_window, stride = 1))
- # Z-score and get features
- _measures = np.concatenate((z_score(bands[0][:,na]), #subdelta
- z_score(bands[1][:,na]), #delta
- z_score(bands[2][:,na])), 1) #theta
- _measures = get_features(_measures, degree, lag, interaction = True, initial_bias = True)
- _measures[:,0] = 1
- # Save indeces in a list of arrays corresponding to each session
- indeces.append(np.arange(_measures.shape[0]) + counter)
- counter += _measures.shape[0]
- measures.append(_measures)
- measures = np.concatenate(measures, axis=0)
- time = np.concatenate(target, axis=0)
- cv = []
- # Annotate indeces to separate into validation and train sets
- for k in range(len(indeces)):
- train_indeces = np.concatenate([indeces[i] for i in range(len(indeces)) if i != k])
- test_indeces = indeces[k] #Leave one session out for validation (a.k.a. test within the train set)
- cv.append((train_indeces, test_indeces))
- print(f"Running regression for (subdelta, delta, supdelta)...")
- # fit Lasso model and explore alpha space
- mdl = Lasso(fit_intercept=True)
- param_grid = {'alpha': alpha_range} #key must be the same name that is used in Lasso documentation
- model = GridSearchCV(mdl, param_grid, cv = cv, verbose = 0, n_jobs = 8, scoring = "neg_root_mean_squared_error") # Explore hyper-parameter space (only for alpha in this case)
- model.fit(measures[:,1:], time)
- return model
- # Explore alpha space below:
- alpha_range = np.logspace(-1, 2, 100)
- n_runs = 220 #This number should match the len(all_combos) below
- n_test = 3 #number of test sessions
- test_heldout =[]
- all_combos = list(itertools.combinations(range(len(anes_data_keys)), n_test)) #all possible combinations
- if os.path.exists(EEG_MODEL_NAME):
- print("Manuscript model has been loaded.")
- elif os.path.exists(LASSO_MODEL_EEG):
- print(f"{LASSO_MODEL_EEG} has been loaded.")
- with open(LASSO_MODEL_EEG, "rb") as fio:
- models_eeg = pickle.load(fio) # Model results
- else:
- models_eeg = []
- for r in np.arange(n_runs):
- test_heldout_idx = all_combos[r]
- _test_heldout = [d for idx, d in enumerate(anes_data_keys) if idx in test_heldout_idx]
- test_heldout.append(_test_heldout)
- anes_data_keys_train = [d for d in anes_data_keys if d not in _test_heldout]
- models_eeg.append(run_regression(data_v2, anes_data_keys_train, alpha_range))
- with open(LASSO_MODEL_EEG, "wb") as fio:
- pickle.dump(models_eeg, fio)
- # %%
- def get_results_df(data, model, anes_data_keys, test_heldout, n_run = 0):
- eeg_window = 8
- results = []
- for d in anes_data_keys:
- date = d[0]
- mouse = d[1]
- anes_start = ANES_START_OFFSETS_EEG[(date, mouse)] # in seconds
- anes_end = int(np.floor((45 * 60 - anes_start) * eeg_power_sr[mouse, date])) # Trim at 45 minutes since anesthesia injection
- # Raw eeg/power data
- eegpower = data.query("mouse == @mouse & date == @date & condition == 'anes' & measurement_name == 'eeg-power'")["timeseries"]
- fs, ts, power = eegpower.values[0]
- _bands = compute_power_bands(fs, power) #type(_bands) = tuple
- # Filter eeg power data
- bands =[]
- for _b in _bands:
- bands.append(moving_avg_filter(_b[:anes_end], window = eeg_window, stride = 1))
- # Z-score and get features
- measures = np.concatenate((z_score(bands[0][:,na]), #subdelta
- z_score(bands[1][:,na]), #delta
- z_score(bands[2][:,na])), 1) #theta
- test_ts = get_features(measures, degree = 2, lags = 1, interaction = True, initial_bias = True)
- test_ts[:,0] = 1
- # Define actual time
- time = ts[:anes_end]
- time = (time[eeg_window:-eeg_window] + anes_start) / 60 #lag is only for plotting comparisons against facial features?
- # predict time
- times_hat = model.predict(test_ts[:,1:])
- # Compute RMSE
- rmse = np.sqrt(np.mean((times_hat - time) ** 2))
- # Save results
- set_group = "Test" if (date, mouse, 'anes') in test_heldout else "Train"
- results.append([n_run, mouse, date, set_group, "(subdelta, delta, theta)", times_hat, time, rmse])
- results_df = pd.DataFrame(results, columns = ["run",
- "mouse",
- "date",
- "set",
- "features",
- "predicted time",
- "actual time",
- "RMSE"])
- return results_df
- _results_df = []
- PREDICTION_RESULTS_EEG_DF = os.sep.join([DATA_CACHE, f'{TODAY}-prediction-eeg-results.pkl'])
- if os.path.exists(results_eeg_df_name):
- print("Manuscript prediction results have been loaded.")
- elif os.path.exists(PREDICTION_RESULTS_EEG_DF):
- print(f"{PREDICTION_RESULTS_EEG_DF} prediction results have been loaded.")
- results_eeg_df = pd.read_pickle(PREDICTION_RESULTS_EEG_DF) # Prediction results
- else:
- for r in np.arange(n_runs):
- _results_df.append(get_results_df(data_v2, models_eeg[r], anes_data_keys, test_heldout[r], r))
- results_eeg_df = pd.concat(_results_df)
- pd.to_pickle(pd.DataFrame(results_eeg_df), PREDICTION_RESULTS_EEG_DF)
- # %% [markdown]
- # ### Facial features model
- # %%
- # def run_regression(ts_df, anes_data_keys, features, train_idx, alpha_range, models = None):
- def run_regression(ts_df, anes_data_keys, features, alpha_range):
- # specify feature parameters
- f_window = 1000 #6000 #In samples ; For moving_avg_filter the behavior features
- degree = 2 #3 #of the polynomial
- lag = 1
- # define empty lists to generate data for model
- measures = []
- time = []
- # data query for input features to the model
- meas_name_query = " | ".join(f"measurement_name == '{feature}'" for feature in features)
- indeces = []
- counter = 0
- for s_idx, dk in enumerate(anes_data_keys):
- d = dk[0]
- mouse = dk[1]
- anes_end = int(45 * 60 - ANES_START_OFFSETS[d, mouse])*100 # Trim at 45 minutes since anesthesia injection
- _measures = ts_df.query("mouse == @mouse & date == @d & condition == 'anes' & measurement_group != 'ephys' & "
- "(" + meas_name_query + ")").sort_values("measurement_name")["timeseries"].values
- # Filter noise
- for _ncol, _m in enumerate(_measures):
- _measures[_ncol] = moving_avg_filter(_m[:anes_end], f_window, stride = ds_factor[mouse, d])
- # Z-score
- _measures = z_score(np.stack(_measures, axis=1))
- _measures = get_features(_measures, degree, lag, interaction = True, initial_bias = True)
- _measures[:,0] = 1 #bias should be unaffected by zscoring and thus always 1
- # Save indeces in a list of arrays corresponding to each session
- indeces.append(np.arange(_measures.shape[0]) + counter)
- counter += _measures.shape[0]
- measures.append(_measures)
- # Define actual time
- ys = ts_df.query(
- "mouse == @mouse & "
- "condition == 'anes' & "
- "date == @d & "
- "measurement_name == @features[0]"
- )["timeseries"].values[0]
- _time = (np.arange(len(ys))/100 + ANES_START_OFFSETS[d, mouse])/ 60 #Note camera sample rate is 100
- _time = _time[:anes_end]
- _time = _time[f_window:-f_window]
- time.append(_time[::ds_factor[mouse, d]])
- measures = np.concatenate(measures, axis=0)
- time = np.concatenate(time, axis=0)
- cv = []
- # Annotate indeces to separate into validation and train sets
- for k in range(len(indeces)):
- train_indeces = np.concatenate([indeces[i] for i in range(len(indeces)) if i != k])
- test_indeces = indeces[k] #Leave one out cross-validation (a.k.a. test within the train set)
- cv.append((train_indeces, test_indeces))
- print(f"Running regression for {features}...")
- # fit Lasso model and explore alpha space
- mdl = Lasso(fit_intercept=True)
- param_grid = {'alpha': alpha_range} #key must be the same name that is used in Lasso documentation
- model = GridSearchCV(mdl, param_grid, cv = cv, verbose = 0, n_jobs = 6, scoring = "neg_root_mean_squared_error") # Explore hyper-parameter space (only for alpha in this case)
- model.fit(measures[:,1:], time)
- return model
- # Select facial features as input to the model
- # features = list(set(data_v2.query("measurement_group != 'ephys'")["measurement_name"]))
- features = ['eye-height-left', 'ear-angle-left', 'nose-bulge-volume']
- # Explore alpha space below:
- alpha_range = np.logspace(-1, 2, 100)
- n_runs = 220
- n_test = 3 #number of test sessions
- test_heldout =[]
- all_combos = list(itertools.combinations(range(len(anes_data_keys)), n_test))
- if os.path.exists(FF_MODEL_NAME):
- print("Manuscript model has been loaded.")
- elif os.path.exists(LASSO_MODEL_FF):
- print(f"{LASSO_MODEL_FF} has been loaded.")
- with open(LASSO_MODEL_FF, "rb") as fio:
- models_ff = pickle.load(fio) # Model results
- else:
- models_ff = []
- for r in np.arange(n_runs):
- test_heldout_idx = all_combos[r] #np.random.permutation(len(anes_data_keys))[:n_test]
- _test_heldout = [d for idx, d in enumerate(anes_data_keys) if idx in test_heldout_idx]
- print(test_heldout_idx, _test_heldout)
- test_heldout.append(_test_heldout)
- anes_data_keys_train = [d for d in anes_data_keys if d not in _test_heldout]
- models_ff.append(run_regression(data_v2, anes_data_keys_train, features, alpha_range))
- with open(LASSO_MODEL_FF, "wb") as fio:
- pickle.dump(models_ff, fio)
- # %%
- def get_results_df(data, model, anes_data_keys, test_heldout, features, ds_factor, n_run = 0):
- results = []
- # ANES_END = 10 * 60 * 100
- # These should match measurements used as input features
- meas_name_query = " | ".join(f"measurement_name == '{feature}'" for feature in features)
- for d in anes_data_keys:
- date = d[0]
- mouse = d[1]
- anes_end = int(45 * 60 - ANES_START_OFFSETS[date, mouse]) * 100# Trim at 45 minutes since anesthesia injection
- test_ts = data.query("date == @date & mouse == @mouse & condition == 'anes' & "
- "measurement_group != 'ephys' & "
- "(" + meas_name_query + ")").sort_values("measurement_name")["timeseries"].values
- # Filter noise
- f_window = 1000 #6000 # should be the same as the one used for the regression input features
- for _ncol, _m in enumerate(test_ts):
- test_ts[_ncol] = moving_avg_filter(_m[:anes_end], f_window, stride = ds_factor[mouse, date])
- # Z-score
- test_ts = z_score(np.stack(test_ts, axis=1))
- test_ts = get_features(test_ts, degree = 2, lags = 1, interaction = True, initial_bias = True)
- # test_ts = z_score(test_ts)
- test_ts[:,0] = 1
- # Define actual time
- ys = data.query(
- "mouse == @mouse & "
- "date == @date &"
- "condition == 'anes' & "
- "measurement_name == @features[0]"
- )["timeseries"].values[0]
- time = (np.arange(len(ys))/100 + ANES_START_OFFSETS[date, mouse])/ 60 #Note camera sample rate is 100
- time = time[:anes_end]
- time = time[f_window:-f_window]
- time = time[::ds_factor[mouse, date]]
- # predict time
- # m_idx = [0 if mouse == 'B47' else 1 if mouse == 'B48' else 2]
- # model = models[m_idx[0]]
- times_hat = model.predict(test_ts[:,1:])
- # Compute RMSE
- rmse = np.sqrt(np.mean((times_hat - time) ** 2))
- # save results
- set_group = "Test" if (date, mouse, 'anes') in test_heldout else "Train"
- results.append([n_run, mouse, date, set_group, "whole-face", times_hat, time, rmse])
- results_df = pd.DataFrame(results, columns = ["run",
- "mouse",
- "date",
- "set",
- "features",
- "predicted time",
- "actual time",
- "RMSE"])
- return results_df
- _results_df = []
- PREDICTION_RESULTS_FF_DF = f'{TODAY}-prediction-ff-results.pkl'
- if os.path.exists(results_df_name):
- print("Manuscript model has been loaded.")
- elif os.path.exists(PREDICTION_RESULTS_FF_DF):
- print(f"{PREDICTION_RESULTS_FF_DF} has been loaded.")
- results_df = pd.read_pickle(PREDICTION_RESULTS_FF_DF) # Prediction results
- else:
- for r in np.arange(n_runs):
- _results_df.append(get_results_df(data_v2, models_ff[r], anes_data_keys, test_heldout[r], features, ds_factor, r))
- results_df = pd.concat(_results_df)
- pd.to_pickle(pd.DataFrame(results_df), PREDICTION_RESULTS_FF_DF)
- # %% [markdown]
- # ### figure
- # %%
- ex_mouse = 'B47'
- ex_date = '20240912'
- ex_run = [5, 7] #[face, eeg] #215
- fig, ax = plt.subplots(nrows= 1,
- ncols= 1,
- figsize=(6,6),
- dpi=200,
- constrained_layout=True)
- plot_predictions(results_eeg_df.query("run == @ex_run[1] & mouse == @ex_mouse & date == @ex_date")["actual time"],
- results_eeg_df.query("run == @ex_run[1] & mouse == @ex_mouse & date == @ex_date")["predicted time"],
- FILTER_WINDOW = 10,
- color = EEG_CONTROL_CMAP[0],
- label = 'eeg')
- plot_predictions(results_df.query("run == @ex_run[0] & mouse == @ex_mouse & date == @ex_date")["actual time"],
- results_df.query("run == @ex_run[0] & mouse == @ex_mouse & date == @ex_date")["predicted time"],
- FILTER_WINDOW = 10,
- color = CONTROL_CMAP[2],
- label = 'facial features',
- dash_line = False)
- ax.set(title = f"{ex_mouse} ({ex_date})",
- xlabel = "Actual time (min)", ylabel = "Predicted Time (min)",
- xticks=[0, 10, 20, 30, 40, 50], yticks=[0, 10, 20, 30, 40, 50],
- xlim = [0, 50], ylim = [0, 50])
- ax.set_aspect("equal", "box")
- sns.despine()
- fname = f"3e-eeg-vs-ff-regression-ex-{ex_mouse}-{ex_date}"
- fig.savefig(os.sep.join([OUTPUT_DIR, f"{fname}.svg"]), bbox_inches="tight")
- # %% [markdown]
- # ### statistics
- # %%
- # Save the mean RMSE results into .csv files
- # mean_results_ff_df = test_mean_per_run(results_eeg_df, 220, "(subdelta, delta, theta)")
- # mean_results_eeg_df = test_mean_per_run(results_df, 220, "whole-face)")
- # mean_results_ff_df.to_csv(f'measurements-data-cache/CSVs/mean_results_ff_df_{TODAY}.csv', index=False)
- # mean_results_eeg_df.to_csv(f'measurements-data-cache/CSVs/mean_results_eeg_df_{TODAY}.csv', index=False)
- # %%
- def cv_ttest_corrected(x, y, kfolds, nrepeats, ntrain, ntest):
- diff = x - y
- print(f"Length of the diff ({len(diff)}) should be equal to kfolds * nrepeats ({kfolds * nrepeats})")
- v = np.sum((diff - np.mean(diff))**2) / (len(diff) - 1) # ~= np.std(diff, ddof=1)
- tstat = np.mean(diff) / np.sqrt(v * (1/(kfolds * nrepeats) + ntest/ntrain)) # tstat with correction
- pval = stats.t.sf(np.abs(tstat), nrepeats*kfolds - 1) # pvalue with correction
- return tstat, pval
- # %%
- eeg_mean_results_df = test_mean_per_run(results_eeg_df, n_runs, "(subdelta, delta, theta)")
- face_mean_results_df = test_mean_per_run(results_df, n_runs, "(subdelta, delta, theta)")
- stat = cv_ttest_corrected(face_mean_results_df['mean_RMSE'].values,
- eeg_mean_results_df['mean_RMSE'].values,
- n_runs, 1, 9, 3)
- print(f" tstat = {stat[0]}\n p-value = {stat[1]}")
- # %% [markdown]
- # ## Figure 3f
- # %% [markdown]
- # Root-mean-square error (RMSE) of time prediction where each dot represents the mean test error for one particular model trained on either Cheese3D (orange) or EEG (blue) features.
- # %%
- # Plot the results
- fig, ax = plt.subplots(figsize=(3, 5))
- # Facial features
- n_runs = 220
- sns.stripplot(test_mean_per_run(results_df, n_runs, "whole-face"),
- x="features", y="mean_RMSE",
- hue="features", #date #mouse #run
- palette=CONTROL_CMAP_V2,
- legend = False,
- alpha=0.3, ax=ax)
- sns.violinplot(test_mean_per_run(results_df, n_runs, "whole-face"),
- x="features", y="mean_RMSE",
- hue="features", #date #mouse #run
- palette=CONTROL_CMAP_V2,
- legend = False,
- inner = None,
- alpha=0.6, ax=ax)
- # EEG power bands
- sns.stripplot(test_mean_per_run(results_eeg_df, n_runs, "(subdelta, delta, theta)"),
- x="features", y="mean_RMSE",
- hue="features",
- palette=EEG_CONTROL_CMAP,
- legend = False,
- alpha=0.3, ax=ax)
- sns.violinplot(test_mean_per_run(results_eeg_df, n_runs, "(subdelta, delta, theta)"),
- x="features", y="mean_RMSE",
- hue="features",
- palette=EEG_CONTROL_CMAP,
- legend = False,
- inner = None,
- alpha=0.6, ax=ax)
- ax.set_xlabel("Feature Set")
- ax.set_ylabel("Root Mean Squared Error (min)")
- ax.set_title("Prediction Error Across Feature Sets")
- ax.set_xticks(list(range(len(ax.get_xticklabels()))),
- [format_feature_ticks(f) for f in ax.get_xticklabels()])
- ax.tick_params(axis="x", bottom=False)
- ax.set_ylim(0, None)
- ax.set_yticks(np.arange(0,16,5))
- ax.legend(frameon=False, bbox_to_anchor=(1.0, 0.3))
- sns.despine(fig)
- fname = f"3f-eeg-vs-face-regression-results-summary-violin"
- fig.savefig(os.sep.join([OUTPUT_DIR, f"{fname}.svg"]), bbox_inches="tight")
- # %% [markdown]
- # ## Figure 3g
- # %% [markdown]
- # (notebook: 2024-12-05-long-anes-three-way-predictions-eeg)
- #
- # Overlaid z-scored theta, delta, and sub-delta frequency band power traces from 12 sessions (same as in (c);
- # vertical scale bars indicate one standard deviation)
- # %%
- eeg_window = 8
- spacer = 8 #5
- features = ["subdelta (<1Hz)", "delta [1-4Hz]", "theta [5-10Hz]"]
- scale = 1
- fig, axs = plt.subplots(nrows=len(features),
- ncols=1,
- figsize=(5, 8),
- dpi=150,
- constrained_layout=True)
- meas_summary = {feat: [] for feat in features}
- for sess in anes_data_keys:
- mouse = sess[1]
- date = sess[0]
- eegpower = data_v2.query("mouse == @mouse & date == @date & condition == 'anes' & measurement_name == 'eeg-power'")["timeseries"]
- fs, ts, power = eegpower.values[0]
- bands = compute_power_bands(fs, power) #type(_bands) = tuple
- anes_start = np.max(list(ANES_START_OFFSETS_EEG.values())) - ANES_START_OFFSETS_EEG[date, mouse] #in seconds - Take the same start for all of them
- anes_start = int(np.floor(anes_start) * eeg_power_sr[mouse, date])
- anes_end = 540 #int(np.floor((45 * 60 - anes_start) * eeg_power_sr[mouse, date])) # Trim at 45 minutes since anesthesia injection
- for i_b, _b in enumerate(bands):
- # FILTER AND Z SCORED DATA PER MEASUREMENT
- if i_b > 2:
- continue
- shortened_b = _b[anes_start:anes_start + anes_end]
- time = np.arange(0, len(shortened_b))/eeg_power_sr[mouse, date] + np.max(list(ANES_START_OFFSETS_EEG.values()))
- time = (time[eeg_window:-eeg_window])/60
- # print(time[-1], len(time))
- ys = z_score(moving_avg_filter(shortened_b, window = eeg_window, stride = 1))
- sns.lineplot(x = time, y = ys,
- color = EEG_CONTROL_CMAP[0], linewidth = 0.5, alpha = 0.5,
- ax = axs[i_b])
- meas_summary[features[i_b]].append(ys)
- axs[i_b].sharex(axs[0])
- axs[i_b].set_ylabel(('\n').join(features[i_b].split(' ')), rotation = 0)
- axs[i_b].yaxis.set_label_coords(-0.2,0.5)
- if i_b != len(features)-1:
- axs[i_b].tick_params("x", bottom=False, labelbottom=False)
- sns.despine(ax=axs[i_b], bottom=True)
- # axs[col].plot(time[f_window:-f_window], ys - spacer*spacing, color = MEASUREMENT_CMAP[l])
- # plt.plot(time, ys - spacer*spacing, color = MEASUREMENT_CMAP[l])
- for row, feat in enumerate(features):
- sns.lineplot(x = time, y = np.mean(meas_summary[feat], axis=0),
- color = EEG_CONTROL_CMAP[0], linewidth = 2.5, ax = axs[row])
- sns.despine(ax = axs[2])
- axs[len(features)-1].set_xlabel('Time (s)')
- fig.tight_layout()
- # axs[0].set_yticks(np.arange(-spacer,-(spacer*spacing+1), -spacer)) #Negative signs to invert the order of measurements top-bottom
- # axs[0].set_yticklabels(y_labels);
- file_name = "eeg-band-feat-with-mean"
- # save_figure("rev_figs/", f"2024-{file_name}", fig, formats=["svg"])
- # fig.savefig(f'{file_name}_ZSCORED.pdf')
- # %% [markdown]
- # ## Figure 3a
- # %% [markdown]
- # (notebook: 2023-long-anes-measurements.ipynb)
- # %% [markdown]
- # Example facial movement raster plot during anesthesia with concurrent EEG recording (each vertical line cor- responds to movement above the 99.9-th percentile jitter threshold as shown in Supplementary Figure 4c for a given 10 ms time window).
- # %% [markdown]
- # ### load data and functions from 2023 cohort
- # %%
- # For faster development, limit data to X mice
- # Set to a high number to render plots for every mouse
- MAX_NUM_MICE = 100
- # Plot every row
- PLOT_SAMPLE = 100
- # Smooth data for visualization
- SMOOTH_WINDOW_SIZE = 25
- # We didn't measure the amount of time between the end of the awake video and the anesthesia injection
- # This adds a constant offset of 5 minutes for every mouse
- AWAKE_END_OFFSET = 5*60
- # Start of anesthesia video after injection (in seconds)
- ANES_START_OFFSETS = {
- 'B6': 120,
- 'B8': 180,
- 'B15': 60,
- 'B20': 31,
- 'B26': 53,
- 'B33': 30,
- }
- STILL_PERIODS = {
- "B6": (38.50, 44.05),
- "B8": (34.30, 39.55),
- "B15": (27.20, 34.15),
- "B20": (34.55, 41.00),
- "B26": (58.20, 63.28),
- "B33": (44.30, 50.05) #This mouse was commented out in the original notebook (2024-04-05-long-anes-jitter.ipynb)
- # "C3": (0, None)
- }
- _, MEASUREMENT_CMAP = measurements_cmap()
- DATA_CACHE = "measurements-data-cache-2023"
- os.makedirs(DATA_CACHE, exist_ok=True)
- COORD_DATA_CACHE = os.sep.join([DATA_CACHE, "long-anes-coords.pkl"])
- MEASURE_DATA_CACHE = os.sep.join([DATA_CACHE, "long-anes-meas.pkl"])
- JITTER_DATA_CACHE = os.sep.join([DATA_CACHE, "long-anes-jitter-meas.pkl"])
- ANIPOSE_BASE = 'anipose-projects/20231013-long-anes-rig2'
- COORDINATE_PATHS = {}
- key_cols = ('mouse', 'source', 'condition')
- for p in Path(ANIPOSE_BASE).glob('*/pose-3d/*.csv'):
- mouse = p.name.split('_')[1]
- source = 'rig2'
- condition = 'awake' if 'awake' in p.name else 'anes'
- COORDINATE_PATHS[(mouse, source, condition)] = p
- data_keys = list(COORDINATE_PATHS.keys())
- data_keys
- # %%
- def build_measjitter_df(meas_df, periods):
- # select only the subset of rows that match the mouse/source/condition pairs
- queries = [(meas_df["mouse"] == mouse) &
- (meas_df["source"] == "rig2") &
- ((meas_df["condition"] == "anes") | (meas_df["condition"] == "dead"))
- for mouse in periods.keys()]
- sub_df = meas_df[reduce(lambda x, y: x | y, queries)]
- jitter_df = sub_df[[*key_cols,
- "measurement_group",
- "measurement_name",
- "measurement_type",
- "timeseries"]]
- jitter_df = jitter_df.rename(columns={"measurement_group": "region"})
- jitter_df.loc[(jitter_df["region"] == 'eye') &
- (jitter_df["measurement_name"].str.contains('left')), "region"] = "eye(left)"
- jitter_df.loc[(jitter_df["region"] == 'eye') &
- (jitter_df["measurement_name"].str.contains('right')), "region"] = "eye(right)"
- jitter_df.loc[(jitter_df["region"] == 'ear') &
- (jitter_df["measurement_name"].str.contains('left')), "region"] = "ear(left)"
- jitter_df.loc[(jitter_df["region"] == 'ear') &
- (jitter_df["measurement_name"].str.contains('right')), "region"] = "ear(right)"
- jitter_df.loc[(jitter_df["region"] == "cheek"), "region"] = "whisker pad"
- jitter_df.loc[(jitter_df["measurement_name"] == "cheek-bulge-volume"), "measurement_name"] = "cheek-bulge-volume"
- jitter_df.loc[(jitter_df["measurement_name"] == "nose-bulge-volume"), "measurement_name"] = "nose-bulge-volume"
- for mouse, period in periods.items():
- start = round(period[0] * 60 * 100) if period[0] is not None else None
- end = round(period[1] * 60 * 100) if period[1] is not None else None
- idx = jitter_df["mouse"] == mouse
- jitter_df.loc[idx, "timeseries"] = jitter_df.loc[idx, "timeseries"].apply(
- lambda x: x[start:end]
- )
- jitter_df["deviations"] = jitter_df.groupby("measurement_name")["timeseries"].transform(
- lambda x: x.apply(lambda xi: xi - xi.mean())
- )
- jitter_df["timeseries_stddev"] = jitter_df.groupby("measurement_name")["deviations"].transform(
- lambda x: x.apply(np.std)
- )
- jitter_df["min_deviation"] = jitter_df.groupby("measurement_name")["deviations"].transform(
- lambda x: x.apply(np.min)
- )
- jitter_df["max_deviation"] = jitter_df.groupby("measurement_name")["deviations"].transform(
- lambda x: x.apply(np.max)
- )
- jitter_df["velocities"] = jitter_df.groupby("measurement_name")["timeseries"].transform(
- lambda x: x.apply(lambda xi: np.abs(np.diff(xi))) * 100
- )
- jitter_df["velocity_mean"] = jitter_df.groupby("measurement_name")["velocities"].transform(
- lambda x: x.apply(np.mean)
- )
- jitter_df["velocity_thresh"] = jitter_df.groupby("measurement_name")["velocities"].transform(
- lambda x: x.apply(lambda xi: np.percentile(xi, 99.9))
- )
- jitter_df["min_velocity"] = jitter_df.groupby("measurement_name")["velocities"].transform(
- lambda x: x.apply(np.min)
- )
- jitter_df["max_velocity"] = jitter_df.groupby("measurement_name")["velocities"].transform(
- lambda x: x.apply(np.max)
- )
- jitter_df["deviations_au"] = jitter_df.groupby("measurement_name")["timeseries"].transform(
- lambda x: x.apply(lambda xi: (xi - xi.mean()) / xi.mean())
- )
- jitter_df["timeseries_stddev_au"] = jitter_df.groupby("measurement_name")["deviations_au"].transform(
- lambda x: x.apply(np.std)
- )
- jitter_df["min_deviation_au"] = jitter_df.groupby("measurement_name")["deviations_au"].transform(
- lambda x: x.apply(np.min)
- )
- jitter_df["max_deviation_au"] = jitter_df.groupby("measurement_name")["deviations_au"].transform(
- lambda x: x.apply(np.max)
- )
- jitter_df["velocities_au"] = jitter_df.groupby("measurement_name")["timeseries"].transform(
- lambda x: x.apply(lambda xi: np.abs(np.diff(xi)) / np.mean(np.abs(np.diff(xi))))
- )
- jitter_df["velocity_mean_au"] = jitter_df.groupby("measurement_name")["velocities_au"].transform(
- lambda x: x.apply(np.mean)
- )
- jitter_df["min_velocity_au"] = jitter_df.groupby("measurement_name")["velocities_au"].transform(
- lambda x: x.apply(np.min)
- )
- jitter_df["max_velocity_au"] = jitter_df.groupby("measurement_name")["velocities_au"].transform(
- lambda x: x.apply(np.max)
- )
- return jitter_df
- # %%
- from scipy.signal import medfilt
- if not os.path.exists(COORD_DATA_CACHE):
- print(f"Pre-filtering coordinate data and storing in {COORD_DATA_CACHE}...")
- coord_data = {k: read_3d_data(v.parent.parent.as_posix(),
- filter_func=medfilt,
- filter_kwargs={'kernel_size': (SMOOTH_WINDOW_SIZE, 1)})
- for k, v in COORDINATE_PATHS.items()}
- with open(COORD_DATA_CACHE, 'wb') as dict_pkl:
- pickle.dump(coord_data, dict_pkl)
- else:
- with open(COORD_DATA_CACHE, 'rb') as dict_pkl:
- coord_data = pickle.load(dict_pkl)
- if not os.path.exists(MEASURE_DATA_CACHE):
- print(f"Pre-computing measurements data and storing in {MEASURE_DATA_CACHE}...")
- meas_df = compute_measurements_df(coord_data)
- meas_df.to_pickle(MEASURE_DATA_CACHE)
- else:
- meas_df = pd.read_pickle(MEASURE_DATA_CACHE)
- if os.path.exists(JITTER_DATA_CACHE):
- jitter_df = pd.read_pickle((JITTER_DATA_CACHE))
- else:
- jitter_df = build_measjitter_df(meas_df, STILL_PERIODS)
- # raise RuntimeError("Jitter results not pre-computed. Run jitter analysis notebook.")
- # %%
- awake_end = {
- mouse: len(df)
- for (mouse, _, condition), df in coord_data.items()
- if condition == 'awake'
- }
- awake_end
- # %%
- from collections import defaultdict
- mice = list(meas_df["mouse"].unique())
- measurement_names = list(meas_df['measurement_name'].unique())
- measurement_group_names = list(meas_df['measurement_group'].unique())
- measurement_groups = defaultdict(set)
- for measurement_name in measurement_names:
- sub_df = meas_df.query("measurement_name == @measurement_name")
- measurement_groups[sub_df['measurement_group'].iloc[0]].add(sub_df['measurement_name'].iloc[0])
- measurement_groups
- # %%
- from labutils.utils import unzip
- def build_timeseries_df(meas_df, data_keys):
- # select only the subset of rows that match the mouse/source/condition pairs
- queries = [(meas_df["mouse"] == mouse) &
- (meas_df["source"] == source) &
- (meas_df["condition"] == condition)
- for mouse, source, condition in data_keys]
- sub_df = meas_df[reduce(lambda x, y: x | y, queries)]
- # compute velocities and pad
- def _process_timeseries(row):
- v = np.abs(np.diff(row["timeseries"]))
- if row["condition"] == "awake":
- p = np.concatenate([row["timeseries"],
- np.zeros(100 * AWAKE_END_OFFSET)])
- v = np.concatenate([[0], v, np.zeros(100 * AWAKE_END_OFFSET)])
- t = np.arange(-len(p), 0)
- else:
- p = np.concatenate([np.zeros(100 * ANES_START_OFFSETS[row["mouse"]]),
- row["timeseries"]])
- v = np.concatenate([np.zeros(100 * ANES_START_OFFSETS[row["mouse"]] + 1), v])
- t = np.arange(len(p))
- return p, v, t
- positions, velocities, frames = unzip(sub_df.apply(_process_timeseries, axis=1).values)
- # create new dataframe for velocities spread out over time
- ts_df = sub_df[[*key_cols, "measurement_name"]].copy()
- ts_df = ts_df.assign(position=positions, velocity=velocities, frames=frames)
- ts_df = ts_df.explode(["position", "velocity", "frames"])
- ts_df = ts_df.pivot(index=[*key_cols, "frames"],
- columns="measurement_name",
- values=["position", "velocity"])
- ts_df.columns = ["/".join(multicol).strip() for multicol in ts_df.columns.values]
- ts_df.reset_index(inplace=True)
- ts_df = ts_df.assign(**{"time (s)": ts_df["frames"] / 100,
- "time (min)": ts_df["frames"] / 100 / 60})
- for name in measurement_names:
- ts_df[f"velocity/{name}-std"] = ts_df.groupby("mouse")[f"velocity/{name}"].transform(
- lambda x: x / x.std()
- )
- ts_df.sort_values(["mouse", "frames"], inplace=True)
- return ts_df
- ts_df = build_timeseries_df(meas_df, data_keys)
- ts_df
- # %% [markdown]
- # ### figure
- # %%
- PLOT_SAMPLE = 1
- time_col = "time (min)"
- plot_meas = ["velocity/ear-angle-left",
- "velocity/ear-angle-right",
- "velocity/eye-area-left",
- "velocity/eye-area-right",
- "velocity/mouth-area",
- "velocity/cheek-bulge-volume",
- "velocity/nose-bulge-volume"]
- cols = [time_col] + plot_meas
- column_order = [time_col] + plot_meas
- fig, axs = plt.subplots(nrows=len(mice),
- # sharex=True,
- figsize=(10, 3 * len(mice)))
- for mouse, ax in zip(filter(lambda x: x != "B33", mice), axs):
- # get plotting subset
- sub_df = ts_df.query('mouse == @mouse')[cols].copy()
- sub_df = sub_df[column_order]
- sub_df.set_index(time_col, inplace=True)
- sub_df = sub_df.iloc[::PLOT_SAMPLE, :]
- # ax = axs[ax_num]
- for i, column in enumerate(plot_meas):
- meas_name = column.split("/")[-1]
- baseline = jitter_df.query("mouse == @mouse & "
- "measurement_name == @meas_name")["velocities"]
- baseline = baseline.values[0] / 100 # convert mm / s -> mm / frame
- thresh = np.percentile(baseline, 99.9)
- # thresh = np.std(baseline)
- print(mouse, meas_name, "threshold =", thresh)
- ticks = sub_df.index[sub_df[column] > thresh]
- # Plot vertical lines for threshold crossing events
- if len(ticks) > 0:
- ax.vlines(ticks, i, i - 1,
- colors=MEASUREMENT_CMAP[meas_name],
- alpha=0.01,
- linewidths=0.5)
- ax.set_title(mouse)
- ax.tick_params(axis="y", left=False)
- ax.set_yticklabels([])
- ax.get_xaxis().set_ticks_position('bottom')
- sns.despine(fig, left=True, top=True, right=True)
- fig.tight_layout()
- fname = f"3a-long-anes-tick-rasters-allmice"
- # fig.savefig(os.sep.join([OUTPUT_DIR, f"{fname}.png"]), bbox_inches="tight")
- # save_figure(OUTPUT_DIR, "long-anes-panel-e-exemplar_ticks", fig, formats=["png"])
- # %% [markdown]
- # ## Figure 3b
- # %% [markdown]
- # (notebook: 2023-long-anes-measurements.ipynb)
- # %% [markdown]
- # Zoom in of movement raster plot from (a) to show the early moments of movement recovery following anesthesia.
- # %%
- fig, axs = plt.subplots(1, len(mice), figsize=(2 * len(mice), 2))
- wakeup_window = {
- "B15": (48, 52),
- "B20": (62, 66),
- "B26": (101, 105),
- "B6": (48, 52),
- "B8": (48, 52)
- }
- for mouse, ax in zip(filter(lambda x: x != "B33", mice), axs):
- sub_df = ts_df.query('mouse == @mouse')[cols].copy()
- sub_df = sub_df[column_order]
- sub_df.set_index(time_col, inplace=True)
- sub_df = sub_df.iloc[::PLOT_SAMPLE, :]
- for i, column in enumerate(plot_meas):
- meas_name = column.split("/")[-1]
- baseline = jitter_df.query("mouse == @mouse & "
- "measurement_name == @meas_name")["velocities"]
- baseline = baseline.values[0] / 100 # convert mm / s -> mm / frame
- thresh = np.percentile(baseline, 99.9)
- # thresh = np.std(baseline)
- print(mouse, meas_name, "threshold =", thresh)
- ticks = sub_df.index[sub_df[column] > thresh]
- # Plot vertical lines for threshold crossing events
- if len(ticks) > 0:
- ax.vlines(ticks, i, i - 1,
- colors=MEASUREMENT_CMAP[meas_name],
- alpha=0.1,
- linewidths=0.5)
- ax.set_xlim(*wakeup_window[mouse])
- ax.set_title(mouse)
- ax.tick_params(axis="y", left=False)
- ax.set_yticklabels([])
- ax.get_xaxis().set_ticks_position('bottom')
- sns.despine(fig, left=True, top=True, right=True)
- fig.tight_layout()
- fname = f"3b-long-anes-tick-rasters-allmice-zoom"
- # fig.savefig(os.sep.join([OUTPUT_DIR, f"{fname}.png"]), bbox_inches="tight")
- # save_figure(OUTPUT_DIR, "long-anes-panel-f-exemplar_ticks_zoom", fig, formats=["png"])
fig3-part1-cheese3d-general-anesthesia-eeg.ipynb at commit 07ef417, under MIT · at the source
Overview
- Cold Spring Harbor Laboratory, Cold Spring Harbor, NY USA
- Dept. of Neuroscience, Stony Brook University, Stony Brook, NY USA
Abstract
Facial expressions and movements, from a subtle and ephemeral grimace to vigorous and rapid chewing, offer direct insights into the moment-to-moment changes of neural and physiological processes. Mice, with discernible facial responses and evolutionarily conserved mammalian facial movement control circuits, provide an ideal model in which to unravel the link between facial movement and underlying states. However, existing frameworks lack the spatial or temporal resolution to sensitively track all movements of the mouse face because of its small and conical form factor. We introduce Cheese3D, a computer vision system that captures high-speed 3D motion of the entire mouse face (including ears, eyes, whisker pad and jaw, covering both sides of the face), using a calibrated six-camera array. The interpretable framework extracts dynamics of anatomically meaningful 3D facial features in absolute world units at sub-mm precision. The precise face-wide motion data generated by Cheese3D provides clear insights, as shown by proof-of-principle experiments predicting anesthetic depth from changing facial patterns, inferring tooth and muscle anatomy from fast ingestion motions across the entire face, measuring minute differences in movements evoked by brainstem stimulation and relating neural activity to spontaneous facial movements, including expressive features only measurable in 3D (for example, angles of ear motion). Cheese3D can serve as a discovery tool that renders subtle mouse facial movements as a highly interpretable readout of otherwise hidden processes.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repositories
Its files are read in the Code ↔ Paper reader above, with 19 matches between paragraphs and lines of code.
Hou-Lab-CSHL/cheese3d
07ef417d37166ad1a272767340827ebe11bd1fe2, 7 July 2026Availability: 1 check, the latest on 30 September 2026: the link answers
- 30 September 2026: the link answers
35 files
- docs/
source/ , Python, 64 linesconf.py - packages/
cheese3d-annotator/ , Python, 1 linecheese3d_annotator/ __init__.py - packages/
cheese3d-annotator/ , Python, 165 linescheese3d_annotator/ data.py - packages/
cheese3d-annotator/ , Python, 386 linescheese3d_annotator/ data_visualizer/ plot.py - packages/
cheese3d-annotator/ , Python, 565 linescheese3d_annotator/ data_visualizer/ qc_video.py - packages/
cheese3d-annotator/ , Python, 504 linescheese3d_annotator/ data_visualizer/ rig_view.py - packages/
cheese3d-annotator/ , Python, 412 linescheese3d_annotator/ data_visualizer/ widget.py - packages/
cheese3d-annotator/ , Python, 683 linescheese3d_annotator/ widget.py - packages/
cheese3d/ , Python, 1 linecheese3d/ __init__.py - packages/
cheese3d/ , Python, 3 linescheese3d/ __main__.py - packages/
cheese3d/ , Python, 322 linescheese3d/ allego_fr.py - packages/
cheese3d/ , Python, 599 lines, 1 matchcheese3d/ anatomy.py - packages/
cheese3d/ , Python, 31 linescheese3d/ backends/ core.py - packages/
cheese3d/ , Python, 253 linescheese3d/ backends/ dlc.py - packages/
cheese3d/ , Python, 317 linescheese3d/ cli.py - packages/
cheese3d/ , Python, 361 linescheese3d/ config.py - packages/
cheese3d/ , Python, 318 linescheese3d/ generate_videos.py - packages/
cheese3d/ , Python, 1,016 lines, 1 matchcheese3d/ interactive.py - packages/
cheese3d/ , Python, 851 lines, 1 matchcheese3d/ project.py - packages/
cheese3d/ , Python, 1 linecheese3d/ synchronize/ __init__.py - packages/
cheese3d/ , Python, 212 linescheese3d/ synchronize/ aligners.py - packages/
cheese3d/ , Python, 336 linescheese3d/ synchronize/ core.py - packages/
cheese3d/ , Python, 254 linescheese3d/ synchronize/ readers.py - packages/
cheese3d/ , Python, 10 linescheese3d/ synchronize/ utils.py - packages/
cheese3d/ , Python, 320 linescheese3d/ utils.py - packages/
cheese3d/ , Python, 80 linestests/ conftest.py - packages/
cheese3d/ , Python, 250 linestests/ test_checkpoint.py - paper/
fig1-cheese3d-accuracy.i , Jupyter, 291 linespynb - paper/
fig2-cheese3d-jitter-ana , Jupyter, 1,153 lines, 1 matchlysis.ipynb - paper/
fig3-part1-cheese3d-gene , Jupyter, 1,578 lines, 4 matchesral-anesthesia-eeg.ipynb - paper/
fig3-part2-prediction-of , Jupyter, 1,717 lines, 4 matches-eeg-from-facial-feature s.ipynb - paper/
fig3-part3-cheese3d-redo , Jupyter, 672 linesse-facial-features.ipynb - paper/
fig4-part1-chewing-whole , Jupyter, 1,220 lines, 2 matches-face-kinematics.ipynb - repository limit reached (2,000 files or 30 MB): the rest is at the source (9 files)
- LICENSE, License, 21 lines
- README.md, Text, 62 lines
Zenodo 18573618
Availability: 1 check, the latest on 30 September 2026: the link answers (HTTP 200)
- 30 September 2026: the link answers (HTTP 200)
11 files
- fig1-cheese3d-accuracy.i
pynb , Jupyter, 291 lines - fig2-cheese3d-jitter-ana
lysis.ipynb , Jupyter, 1,153 lines - fig3-part1-cheese3d-gene
ral-anesthesia-eeg.ipynb , Jupyter, 1,578 lines - fig3-part3-cheese3d-redo
se-facial-features.ipynb , Jupyter, 672 lines - fig5-part2-cheese3d-sync
hronized-electrophysiolo , Jupyter, 1,297 lines, 2 matchesgy.ipynb - fig5-part3-prediction-of
-neural-activity-from-ch , Jupyter, 1,844 lines, 2 matcheseese3d.ipynb - supfig2-cheese3d-vs-scan
-structure.ipynb , Jupyter, 311 lines - supfig3-camera-number-co
mparison.ipynb , Jupyter, 521 lines - supfig5-cheese3d-jitter.
ipynb , Jupyter, 1,038 lines, 1 match - supfig6-cheese3d-vs-face
map-jitter.ipynb , Jupyter, 639 lines - README.md, Text, 19 lines
Code availability
Cheese3D software and example datasets are available at https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 43 scripts, each with its path and the digest of its content;
- 19 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
Datasets cited
- zenodo:18508087, at Zenodo; found in “Data availability”
Data availability
All raw data used for analyses in the paper are publicly available on Zenodo at 10.5281/
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 30 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 11 authors, 5 keywords, 9 MeSH terms, 4 funders, 53 references.
Cite
This paper
Daruwalla, K., Nozal Martin, I., Zhang, L., Naglič, D., Frankel, A., Rasgaitis, C., Zhao, R., Zhang, X., Ahmad, Z., Borniger, J. C., & Hou, X. H. (2026). Cheese3D enables sensitive detection and analysis of whole-face movement in mice. Nature neuroscience, 29(6), 1510-1521. https://
BibTeX
@article{daruwalla2026ch
author = {Daruwalla, Kyle and Nozal Martin, Irene and Zhang, Linghua and Naglič, Diana and Frankel, Andrew and Rasgaitis, Catherine and Zhao, Rubin and Zhang, Xinyan and Ahmad, Zainab and Borniger, Jeremy C and Hou, Xun Helen},
title = {{Cheese3D enables sensitive detection and analysis of whole-face movement in mice}},
journal = {Nature neuroscience},
year = {2026},
month = apr,
volume = {29},
number = {6},
pages = {1510--1521},
publisher = {Nature Portfolio},
issn = {1097-6256},
doi = {10.1038/
url = {https://
pmid = {42045464},
pmcid = {PMC13246446}
}
RIS
TY - JOUR
AU - Daruwalla, Kyle
AU - Nozal Martin, Irene
AU - Zhang, Linghua
AU - Naglič, Diana
AU - Frankel, Andrew
AU - Rasgaitis, Catherine
AU - Zhao, Rubin
AU - Zhang, Xinyan
AU - Ahmad, Zainab
AU - Borniger, Jeremy C
AU - Hou, Xun Helen
TI - Cheese3D enables sensitive detection and analysis of whole-face movement in mice
T2 - Nature neuroscience
J2 - Nat Neurosci
PY - 2026
DA - 2026/
VL - 29
IS - 6
SP - 1510
EP - 1521
SN - 1097-6256
PB - Nature Portfolio
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"type": "article-journal",
"title": "Cheese3D enables sensitive detection and analysis of whole-face movement in mice",
"container-title": "Nature neuroscience",
"author": [
{
"family": "Daruwalla",
"given": "Kyle"
},
{
"family": "Nozal Martin",
"given": "Irene"
},
{
"family": "Zhang",
"given": "Linghua"
},
{
"family": "Naglič",
"given": "Diana"
},
{
"family": "Frankel",
"given": "Andrew"
},
{
"family": "Rasgaitis",
"given": "Catherine"
},
{
"family": "Zhao",
"given": "Rubin"
},
{
"family": "Zhang",
"given": "Xinyan"
},
{
"family": "Ahmad",
"given": "Zainab"
},
{
"family": "Borniger",
"given": "Jeremy C"
},
{
"family": "Hou",
"given": "Xun Helen"
}
],
"container-title-short":
"volume": "29",
"issue": "6",
"page": "1510-1521",
"DOI": "10.1038/
"PMID": "42045464",
"PMCID": "PMC13246446",
"ISSN": "1097-6256",
"publisher": "Nature Portfolio",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
27
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1016/j.celrep.2026.117420 [code]
- Neural population dynamics of direct electrical stimulation of neocortex.Journal: Cell reportsIn common: Open Ephys analysis tools, napari, JAX, 9 other tools, systems, mouse
- [2] doi:10.1038/s41593-026-02232-0 [code]
- Entorhinal cortex represents task-relevant remote locations independently of CA1.Journal: Nature neuroscienceIn common: DeepLabCut, Kilosort, OpenCV, 8 other tools, systems, mouse, 2 references
- [3] doi:10.7554/elife.109717 [code]
- Retrosplenial cortex enables context-dependent goal-directed sensorimotor transformation.Journal: eLifeIn common: Kilosort, imageio, OpenCV, 8 other tools, systems, mouse, 1 reference
- [4] doi: [code]
- Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement controlJournal: eLifeIn common: napari, imageio, OpenCV, 8 other tools, mouse, 1 reference
- [5] doi:10.1038/s41467-026-72152-x [code]
- Centralized brain networks controlling antennal grooming coordination.Journal: Nature communicationsIn common: DeepLabCut, OpenCV, h5py, 6 other tools, systems, 2 references
- [6] doi:10.1038/s41586-026-10679-1 [code]
- Cortical development dynamics across autism spectrum disorder mouse models.Journal: NatureIn common: imageio, OpenCV, scikit-image, 7 other tools, mouse, 1 reference
- [7] doi:10.1038/s41467-026-72057-9 [code]
- Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior.Journal: Nature communicationsIn common: OpenCV, scikit-image, h5py, 6 other tools, systems, 2 references
- [8] doi:10.1016/j.crmeth.2026.101421 [code]
- EthoPy provides an accessible platform for reproducible behavioral neuroscience.Journal: Cell reports methodsIn common: imageio, OpenCV, h5py, 6 other tools, mouse, 2 references
- [9] doi:10.3389/fendo.2026.1828487 [code]
- Castration-induced nigrostriatal deficits are linked to reduced TrkB and loss of mature spines in the dorsal striatum.Journal: Frontiers in endocrinologyIn common: imageio, OpenCV, scikit-image, 7 other tools, mouse, 1 reference
- [10] doi:10.1371/journal.pcbi.1014571 [code]
- SynAPSeg: A novel dataset and image analysis framework for deep learning-based synapse detection and quantification.Journal: PLoS computational biologyIn common: napari, imageio, OpenCV, 7 other tools
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 2 repositories of the authors' code, each at its verified commit and with its license, 43 scripts, and 19 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:8d59f445c9ffa3fe…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
