Repository files navigation

Overview

This repository provides all relevant code used in the manuscript titled "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Algorithm", written by authors Jaleesa Houle, Austin Lopez, Kevin Christie, Gaurav Kumar, David Stupski, Aditya Nair, and Floris van Breugel.

The preprint is available on bioRxiv at:

https://www.biorxiv.org/content/10.64898/2026.07.21.739673v1

Downloading the Data

All data necessary to run the analyses and generate the figures can be found on Dryad:

https://doi.org/10.5061/dryad.x0k6djhwc

(Note: data will be made public upon publication.)

Download each zip archive from Dryad and unzip into the Data/ directory. See Data/download_from_dryad_and_unzip_here.txt for the full expected directory structure after unzipping.

Setting up the environment

The figure notebooks were developed and validated on Python 3.11. Follow the steps below to create an environment, install everything, and run the repo.

1. Create and activate an environment

Using conda (recommended, matches how the repo was validated):

conda create -n splitflow python=3.11 -y
conda activate splitflow

Or using a plain virtual environment:

python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate

2. Install the Python dependencies and the splitflow package

pip install -r requirements.txt # all Python dependencies (incl. the private lab repos)
pip install -e .# installs the local splitflow package (the Code/ directory)

requirements.txt pins the dependencies that need care:

  • pandas < 3.0 (required). pandas 3.0 makes Copy-on-Write mandatory, which returns read-only arrays from DataFrame.values; the unifying-algorithm alignment code performs in-place array operations and fails with "output array is read-only" under pandas ≥ 3.0.
  • Private lab repositoriesalign_course_direction_analysis, braid_tools (imported as braid_analysis), FlyPlotLib (fly_plot_lib), and FlyStat (flystat) are installed directly from git (pinned URLs in requirements.txt); you need access to those repositories.

Once installed, notebooks import the shared utilities from the package, e.g.:

importsplitflow.kinematicsaskinematicsfromsplitflow.set_zorder_functionsimport*fromsplitflow.affine_math_and_plot_helperimport*

3. Install Gurobi + an academic license (required for the unifying algorithm)

The unifying-algorithm fit is a mixed-integer quadratic program solved with cvxpy's GUROBI backend. Gurobi is commercial but free for academics; the bundled "restricted" license is too small for these problems, so a full (named-user academic) license is required.

  1. gurobipy is already installed by requirements.txt (or pip install gurobipy).
  2. Create a free account and request a Named-User Academic License at https://www.gurobi.com/academia/academic-program-and-licenses/ .
  3. From the Gurobi "License Center", copy the grbgetkey command for your license and run it while on your university network/VPN:
    grbgetkey <your-license-key>
    This writes a gurobi.lic file (default ~/gurobi.lic).
  4. Verify it works:
    python -c "import gurobipy; gurobipy.Model(); print('Gurobi OK')"
    If your license is not in the default location, point Gurobi at it with export GRB_LICENSE_FILE=/path/to/gurobi.lic.

4. Install a LaTeX distribution (for a few notebooks)

A working system LaTeX is required for the notebooks that set matplotlib text.usetex=True (fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation). Install MacTeX (macOS) or TeX Live (Linux/Windows) system-wide and ensure latex is on your PATH.

5. Get the data and run

Download the data from Dryad into Data/ (see the "Downloading the Data" section above), then launch Jupyter from the repo root with the environment active:

jupyter lab # or: jupyter notebook

Open any notebook under FigureGeneration/ and run it top to bottom. Notebooks read their inputs from Data/ via relative paths and must be run from their own directory (Jupyter's default), so no path edits are needed.

Directory structure

├── Code/
│ ├── FlyDataProcessingScript.py # Raw fly data preprocessing pipeline
│ ├── PlotUtilities.py # Shared plotting helpers
│ ├── WindDataProcessing.py # Wind tunnel CFD data processing
│ ├── StochasticAlgorithms.py # Algorithm implementations for Brownian motion, Levy flight, etc.
│ ├── opto_tracking.py # Algorithm implementations for Casting/Circling in CFD
│ ├── kinematics.py # Trajectory kinematics calculations
│ ├── TripletLoss.py # Autoencoder training functions
│ ├── vae_optuna.py # VAE param search code
│ ├── RFClassifier.py # Random forest classifier
│ ├── affine_math_and_plot_helper.py # Affine transform math and plotting for unifying algorithm
│ ├── staircase_approximation.py # Staircase function approximation for unifying algorithm
│ ├── BayesFactorFunctions.py # Bayes factor statistical functions -- used in older version of paper
│ └── FeatureFunctions.py # Trajectory feature extraction -- used in older version of paper
│
│
├── Data/
│ ├── download_from_dryad_and_unzip_here.txt # Download instructions + expected structure
│ │ [download from Dryad and unzip into the directories as shown below]
│ ├── wind_tunnel_data/ # Wind speed measurements (laminar, outdoor, split flow)
│ ├── CFD/
│ │ ├── 2D_PlanarSlices/ # 2D velocity field slices (XY steady/unsteady, XZ)
│ │ ├── 2D_PlumeData/ # 2D odor concentration slices -- used in older version of paper
│ │ └── 3D_Velocity_Fields/ # 3D velocity fields, 20s simulation (6 archives)
│ ├── Experimental_Fly_Data/ # full/raw fly datasets, flat (merged raw_data + preprocessed_hdfs):
│ │ # per-condition trimmed HDFs, merged all-nights HDFs,
│ │ # training laminar/stillair sets, variable-wind & CFD parquets
│ ├── Simulated_Trajectory_Data/ # Casting, circling, and unifying algorithm trajectories
│ ├── Animal_Trajectory_Data/ # Cross-species trajectories (15 species/agent types)
│ ├── Unifying_Algo_Results/
│ │ ├── Main/ # Analysis outputs for main figures (42 parquet files)
│ │ └── Supplemental/ # Supplemental-only outputs (30 parquet files + var_wind_analyzed/):
│ │ # variable-wind, WT-splitflow variants, augmented-sham, df_all_species
│ │ # (results shared with the main figure are read from Main/, not duplicated here)
│ ├── Fig1_Input_Data/ # Data specifically used for Figure 1
│ └── Fig5_Input_Data/ # Data specifically used for Figure 5
│
│
├── FigureGeneration/
│ ├── Main/
│ │ ├── fig_1_math_figure/
│ │ │ ├── A_figure_math_affine_explanation.ipynb
│ │ │ ├── B_figure_math_consolidate_params.ipynb
│ │ │ ├── C_f1_example_trajectories.ipynb
│ │ │ └── misc/
│ │ │ └── basic_plotting_examples/
│ │ │ ├── f1_example_trajectories.ipynb
│ │ │ └── slope_vs_speed_over_length.ipynb
│ │ ├── fig_2_wind_tunnel_CFD_and_sup_fig_1/
│ │ │ ├── fig2_CFD.ipynb
│ │ │ ├── fig2_wind_tunnel_plots.ipynb
│ │ │ └── smokepen_images/
│ │ ├── fig_3_trajectory_characteristics/
│ │ │ └── f3_raw_trajectories.ipynb
│ │ ├── fig_4_autoencoder/
│ │ │ ├── f4_autoencoder.ipynb
│ │ │ ├── triplet_vae_6_classes_70_feats.pth # Trained model weights
│ │ │ └── misc/
│ │ │ └── optuna_hyperparam_search_multiobj_6class.ipynb
│ │ └── fig_5_unifying_algo_analysis/
│ │ ├── analysis_scripts/ # scripts used to generate the data in Unifying_Algo_Results
│ │ │ ├── analyze_NEW_unifying_sims.py
│ │ │ ├── analyze_cfd_sims.py
│ │ │ ├── analyze_laminar.py
│ │ │ ├── analyze_laminar_wt.py
│ │ │ ├── analyze_still.py
│ │ │ ├── analyze_still_wt.py
│ │ │ ├── analyze_unsteady_exp_bottom_on.py
│ │ │ └── analyze_unsteady_exp_top_on.py
│ │ └── generate_figure_5/ # run A→E in order (they populate one shared SVG)
│ │ ├── A_figure_math_explanation.ipynb
│ │ ├── B_figure_example_trajectories.ipynb
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── E_variable_wind_slope_axis_ratio.ipynb
│ │ ├── misc/ # alternate/older panel notebooks (not part of the final figure)
│ │ └── animal_trajectories/
│ │ ├── albatross/
│ │ ├── moth/
│ │ └── shark/
│ └── Supplemental/
│ ├── sup_fig_all_animal_trajectories/
│ │ ├── animal_trajectories_and_alignment.ipynb
│ │ └── animal_data/ # Per-species figure notebooks
│ │ ├── albatross/
│ │ ├── cockroach/
│ │ ├── crabs/
│ │ ├── d_melanogaster/
│ │ ├── d_sechelia/
│ │ ├── eels/
│ │ ├── mosquitoes/
│ │ ├── moths/
│ │ ├── mouse/
│ │ ├── nautical_search/
│ │ ├── nautilus/
│ │ ├── RL_agents/
│ │ ├── sharks/
│ │ ├── terns/
│ │ └── walking_drosophila/
│ ├── sup_fig_altitude_response/
│ │ └── f3_altitude_responses.ipynb
│ ├── sup_fig_extra_real_trajectories/
│ │ └── S3_sample_real_trajectories.ipynb
│ ├── sup_fig_sorted_trajectories/
│ │ ├── supp_All_Trajectories_sorted.ipynb
│ │ └── supplemental_autoencoder_altitude_colored.ipynb
│ ├── sup_fig_synthetic_trajectories/
│ │ └── S5_sample_simulated_trajectories.ipynb
│ ├── sup_fig_tunnel_methods/
│ │ ├── CFD_plots.ipynb
│ │ └── wind_tunnel_plot_maker.ipynb
│ ├── sup_fig_unifying_analysis/
│ │ └── generate_supplementary_figure/
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── make_figure_align_course_directions.py
│ │ ├── make_figure_unifying_algo_results_slope_axis.py
│ │ └── run_all_figures.sh
│ └── sup_fig_vertical_split/
│ └── S1_verticalsplit.ipynb
│
│
├── TrajectorySimulations/
│ ├── generate_unifying_algo_trajectories/
│ │ ├── generate_unifying_trajectories.ipynb # Step-by-step walkthrough
│ │ ├── generate_unifying_trajectories.py # Script version
│ │ └── plot_trajectories.ipynb
│ └── generating_synthetic_test_trajectory_data/
│ ├── Casting_Circling_CFD_trajectory_generation.ipynb # Step-by-step walkthrough
│ ├── cfd_casting_circling_sims.py # Script version
│ ├── stochastic_trajectories.ipynb # Step-by-step walkthrough for all other algorithms
│ └── param_sweep/ │
│
├── LICENSE
└── README.md

Running the simulations

Notebooks are provided for a step-by-step walkthrough of trajectory generation:

  • Unifying algorithm:TrajectorySimulations/generate_unifying_algo_trajectories/generate_unifying_trajectories.ipynb
  • Casting/circling (CFD-informed):TrajectorySimulations/generating_synthetic_test_trajectory_data/Casting_Circling_CFD_trajectory_generation.ipynb
  • Stochastic trajectories:TrajectorySimulations/generating_synthetic_test_trajectory_data/stochastic_trajectories.ipynb

Note: simulations using 3D CFD velocity fields can take a while to run. The CFD 3D data archives (3D_vel_1.zip3D_vel_6.zip, ~4 GB each) are only required if re-running these simulations from scratch.

Generating the figures

Each figure is generated by a Jupyter notebook in FigureGeneration/. Notebooks read their inputs from Data/ via relative paths (e.g. ../../../Data/...), so they must be run from their own directory — which is Jupyter's default. No config.py or path edits are needed; helper modules import either from the installed splitflow package or from .py files that sit alongside the notebook (e.g. set_zorder_functions.py, unifying_algo_analysis_helper.py).

Several notebooks build a composite figurefirst SVG: they read a layout template (e.g. unifying_analysis.svg) and write the populated figure back. For generate_figure_5/, run A → B → C → D → E in order — each populates a different layer of the same shared SVG.

The pre-existing figure outputs shipped with the repo are kept under an output_figs/ subfolder in each figure directory; a fresh run writes its output alongside with a _test suffix so the committed figures are never overwritten.

Figure 5 / Supplemental unifying analysis read pre-computed unifying-algorithm results from Data/Unifying_Algo_Results/ (Main/ for the main figure, Supplemental/ for the supplemental figures), so the figure notebooks run without any preprocessing. To regenerate those results from the raw trajectory data, run the scripts in FigureGeneration/Main/fig_5_unifying_algo_analysis/analysis_scripts/ (each reads from Data/Experimental_Fly_Data/ and writes to Data/Unifying_Algo_Results/).

Known environment requirements / caveats

  • Gurobi academic license.fig_5/generate_figure_5/A and B fit the unifying-algorithm MIQP live and need a full Gurobi academic license; the free "restricted" license is too small and fails with SolverError. The other Figure 5 notebooks (C, D, E) read pre-computed results and do not need it.
  • LaTeX.fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation set matplotlib text.usetex=True and require a working system LaTeX (MacTeX/TeX Live).
  • pandas < 3.0 is required (see the setup section above).

About

Analysis and modeling code associated with the paper "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Framework".

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Overview

This repository provides all relevant code used in the manuscript titled "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Algorithm", written by authors Jaleesa Houle, Austin Lopez, Kevin Christie, Gaurav Kumar, David Stupski, Aditya Nair, and Floris van Breugel.

The preprint is available on bioRxiv at:

https://www.biorxiv.org/content/10.64898/2026.07.21.739673v1

Downloading the Data

All data necessary to run the analyses and generate the figures can be found on Dryad:

https://doi.org/10.5061/dryad.x0k6djhwc

(Note: data will be made public upon publication.)

Download each zip archive from Dryad and unzip into the Data/ directory. See Data/download_from_dryad_and_unzip_here.txt for the full expected directory structure after unzipping.

Setting up the environment

The figure notebooks were developed and validated on Python 3.11. Follow the steps below to create an environment, install everything, and run the repo.

1. Create and activate an environment

Using conda (recommended, matches how the repo was validated):

conda create -n splitflow python=3.11 -y
conda activate splitflow

Or using a plain virtual environment:

python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate

2. Install the Python dependencies and the splitflow package

pip install -r requirements.txt # all Python dependencies (incl. the private lab repos)
pip install -e .# installs the local splitflow package (the Code/ directory)

requirements.txt pins the dependencies that need care:

  • pandas < 3.0 (required). pandas 3.0 makes Copy-on-Write mandatory, which returns read-only arrays from DataFrame.values; the unifying-algorithm alignment code performs in-place array operations and fails with "output array is read-only" under pandas ≥ 3.0.
  • Private lab repositoriesalign_course_direction_analysis, braid_tools (imported as braid_analysis), FlyPlotLib (fly_plot_lib), and FlyStat (flystat) are installed directly from git (pinned URLs in requirements.txt); you need access to those repositories.

Once installed, notebooks import the shared utilities from the package, e.g.:

importsplitflow.kinematicsaskinematicsfromsplitflow.set_zorder_functionsimport*fromsplitflow.affine_math_and_plot_helperimport*

3. Install Gurobi + an academic license (required for the unifying algorithm)

The unifying-algorithm fit is a mixed-integer quadratic program solved with cvxpy's GUROBI backend. Gurobi is commercial but free for academics; the bundled "restricted" license is too small for these problems, so a full (named-user academic) license is required.

  1. gurobipy is already installed by requirements.txt (or pip install gurobipy).
  2. Create a free account and request a Named-User Academic License at https://www.gurobi.com/academia/academic-program-and-licenses/ .
  3. From the Gurobi "License Center", copy the grbgetkey command for your license and run it while on your university network/VPN:
    grbgetkey <your-license-key>
    This writes a gurobi.lic file (default ~/gurobi.lic).
  4. Verify it works:
    python -c "import gurobipy; gurobipy.Model(); print('Gurobi OK')"
    If your license is not in the default location, point Gurobi at it with export GRB_LICENSE_FILE=/path/to/gurobi.lic.

4. Install a LaTeX distribution (for a few notebooks)

A working system LaTeX is required for the notebooks that set matplotlib text.usetex=True (fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation). Install MacTeX (macOS) or TeX Live (Linux/Windows) system-wide and ensure latex is on your PATH.

5. Get the data and run

Download the data from Dryad into Data/ (see the "Downloading the Data" section above), then launch Jupyter from the repo root with the environment active:

jupyter lab # or: jupyter notebook

Open any notebook under FigureGeneration/ and run it top to bottom. Notebooks read their inputs from Data/ via relative paths and must be run from their own directory (Jupyter's default), so no path edits are needed.

Directory structure

├── Code/
│ ├── FlyDataProcessingScript.py # Raw fly data preprocessing pipeline
│ ├── PlotUtilities.py # Shared plotting helpers
│ ├── WindDataProcessing.py # Wind tunnel CFD data processing
│ ├── StochasticAlgorithms.py # Algorithm implementations for Brownian motion, Levy flight, etc.
│ ├── opto_tracking.py # Algorithm implementations for Casting/Circling in CFD
│ ├── kinematics.py # Trajectory kinematics calculations
│ ├── TripletLoss.py # Autoencoder training functions
│ ├── vae_optuna.py # VAE param search code
│ ├── RFClassifier.py # Random forest classifier
│ ├── affine_math_and_plot_helper.py # Affine transform math and plotting for unifying algorithm
│ ├── staircase_approximation.py # Staircase function approximation for unifying algorithm
│ ├── BayesFactorFunctions.py # Bayes factor statistical functions -- used in older version of paper
│ └── FeatureFunctions.py # Trajectory feature extraction -- used in older version of paper
│
│
├── Data/
│ ├── download_from_dryad_and_unzip_here.txt # Download instructions + expected structure
│ │ [download from Dryad and unzip into the directories as shown below]
│ ├── wind_tunnel_data/ # Wind speed measurements (laminar, outdoor, split flow)
│ ├── CFD/
│ │ ├── 2D_PlanarSlices/ # 2D velocity field slices (XY steady/unsteady, XZ)
│ │ ├── 2D_PlumeData/ # 2D odor concentration slices -- used in older version of paper
│ │ └── 3D_Velocity_Fields/ # 3D velocity fields, 20s simulation (6 archives)
│ ├── Experimental_Fly_Data/ # full/raw fly datasets, flat (merged raw_data + preprocessed_hdfs):
│ │ # per-condition trimmed HDFs, merged all-nights HDFs,
│ │ # training laminar/stillair sets, variable-wind & CFD parquets
│ ├── Simulated_Trajectory_Data/ # Casting, circling, and unifying algorithm trajectories
│ ├── Animal_Trajectory_Data/ # Cross-species trajectories (15 species/agent types)
│ ├── Unifying_Algo_Results/
│ │ ├── Main/ # Analysis outputs for main figures (42 parquet files)
│ │ └── Supplemental/ # Supplemental-only outputs (30 parquet files + var_wind_analyzed/):
│ │ # variable-wind, WT-splitflow variants, augmented-sham, df_all_species
│ │ # (results shared with the main figure are read from Main/, not duplicated here)
│ ├── Fig1_Input_Data/ # Data specifically used for Figure 1
│ └── Fig5_Input_Data/ # Data specifically used for Figure 5
│
│
├── FigureGeneration/
│ ├── Main/
│ │ ├── fig_1_math_figure/
│ │ │ ├── A_figure_math_affine_explanation.ipynb
│ │ │ ├── B_figure_math_consolidate_params.ipynb
│ │ │ ├── C_f1_example_trajectories.ipynb
│ │ │ └── misc/
│ │ │ └── basic_plotting_examples/
│ │ │ ├── f1_example_trajectories.ipynb
│ │ │ └── slope_vs_speed_over_length.ipynb
│ │ ├── fig_2_wind_tunnel_CFD_and_sup_fig_1/
│ │ │ ├── fig2_CFD.ipynb
│ │ │ ├── fig2_wind_tunnel_plots.ipynb
│ │ │ └── smokepen_images/
│ │ ├── fig_3_trajectory_characteristics/
│ │ │ └── f3_raw_trajectories.ipynb
│ │ ├── fig_4_autoencoder/
│ │ │ ├── f4_autoencoder.ipynb
│ │ │ ├── triplet_vae_6_classes_70_feats.pth # Trained model weights
│ │ │ └── misc/
│ │ │ └── optuna_hyperparam_search_multiobj_6class.ipynb
│ │ └── fig_5_unifying_algo_analysis/
│ │ ├── analysis_scripts/ # scripts used to generate the data in Unifying_Algo_Results
│ │ │ ├── analyze_NEW_unifying_sims.py
│ │ │ ├── analyze_cfd_sims.py
│ │ │ ├── analyze_laminar.py
│ │ │ ├── analyze_laminar_wt.py
│ │ │ ├── analyze_still.py
│ │ │ ├── analyze_still_wt.py
│ │ │ ├── analyze_unsteady_exp_bottom_on.py
│ │ │ └── analyze_unsteady_exp_top_on.py
│ │ └── generate_figure_5/ # run A→E in order (they populate one shared SVG)
│ │ ├── A_figure_math_explanation.ipynb
│ │ ├── B_figure_example_trajectories.ipynb
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── E_variable_wind_slope_axis_ratio.ipynb
│ │ ├── misc/ # alternate/older panel notebooks (not part of the final figure)
│ │ └── animal_trajectories/
│ │ ├── albatross/
│ │ ├── moth/
│ │ └── shark/
│ └── Supplemental/
│ ├── sup_fig_all_animal_trajectories/
│ │ ├── animal_trajectories_and_alignment.ipynb
│ │ └── animal_data/ # Per-species figure notebooks
│ │ ├── albatross/
│ │ ├── cockroach/
│ │ ├── crabs/
│ │ ├── d_melanogaster/
│ │ ├── d_sechelia/
│ │ ├── eels/
│ │ ├── mosquitoes/
│ │ ├── moths/
│ │ ├── mouse/
│ │ ├── nautical_search/
│ │ ├── nautilus/
│ │ ├── RL_agents/
│ │ ├── sharks/
│ │ ├── terns/
│ │ └── walking_drosophila/
│ ├── sup_fig_altitude_response/
│ │ └── f3_altitude_responses.ipynb
│ ├── sup_fig_extra_real_trajectories/
│ │ └── S3_sample_real_trajectories.ipynb
│ ├── sup_fig_sorted_trajectories/
│ │ ├── supp_All_Trajectories_sorted.ipynb
│ │ └── supplemental_autoencoder_altitude_colored.ipynb
│ ├── sup_fig_synthetic_trajectories/
│ │ └── S5_sample_simulated_trajectories.ipynb
│ ├── sup_fig_tunnel_methods/
│ │ ├── CFD_plots.ipynb
│ │ └── wind_tunnel_plot_maker.ipynb
│ ├── sup_fig_unifying_analysis/
│ │ └── generate_supplementary_figure/
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── make_figure_align_course_directions.py
│ │ ├── make_figure_unifying_algo_results_slope_axis.py
│ │ └── run_all_figures.sh
│ └── sup_fig_vertical_split/
│ └── S1_verticalsplit.ipynb
│
│
├── TrajectorySimulations/
│ ├── generate_unifying_algo_trajectories/
│ │ ├── generate_unifying_trajectories.ipynb # Step-by-step walkthrough
│ │ ├── generate_unifying_trajectories.py # Script version
│ │ └── plot_trajectories.ipynb
│ └── generating_synthetic_test_trajectory_data/
│ ├── Casting_Circling_CFD_trajectory_generation.ipynb # Step-by-step walkthrough
│ ├── cfd_casting_circling_sims.py # Script version
│ ├── stochastic_trajectories.ipynb # Step-by-step walkthrough for all other algorithms
│ └── param_sweep/ │
│
├── LICENSE
└── README.md

Running the simulations

Notebooks are provided for a step-by-step walkthrough of trajectory generation:

  • Unifying algorithm:TrajectorySimulations/generate_unifying_algo_trajectories/generate_unifying_trajectories.ipynb
  • Casting/circling (CFD-informed):TrajectorySimulations/generating_synthetic_test_trajectory_data/Casting_Circling_CFD_trajectory_generation.ipynb
  • Stochastic trajectories:TrajectorySimulations/generating_synthetic_test_trajectory_data/stochastic_trajectories.ipynb

Note: simulations using 3D CFD velocity fields can take a while to run. The CFD 3D data archives (3D_vel_1.zip3D_vel_6.zip, ~4 GB each) are only required if re-running these simulations from scratch.

Generating the figures

Each figure is generated by a Jupyter notebook in FigureGeneration/. Notebooks read their inputs from Data/ via relative paths (e.g. ../../../Data/...), so they must be run from their own directory — which is Jupyter's default. No config.py or path edits are needed; helper modules import either from the installed splitflow package or from .py files that sit alongside the notebook (e.g. set_zorder_functions.py, unifying_algo_analysis_helper.py).

Several notebooks build a composite figurefirst SVG: they read a layout template (e.g. unifying_analysis.svg) and write the populated figure back. For generate_figure_5/, run A → B → C → D → E in order — each populates a different layer of the same shared SVG.

The pre-existing figure outputs shipped with the repo are kept under an output_figs/ subfolder in each figure directory; a fresh run writes its output alongside with a _test suffix so the committed figures are never overwritten.

Figure 5 / Supplemental unifying analysis read pre-computed unifying-algorithm results from Data/Unifying_Algo_Results/ (Main/ for the main figure, Supplemental/ for the supplemental figures), so the figure notebooks run without any preprocessing. To regenerate those results from the raw trajectory data, run the scripts in FigureGeneration/Main/fig_5_unifying_algo_analysis/analysis_scripts/ (each reads from Data/Experimental_Fly_Data/ and writes to Data/Unifying_Algo_Results/).

Known environment requirements / caveats

  • Gurobi academic license.fig_5/generate_figure_5/A and B fit the unifying-algorithm MIQP live and need a full Gurobi academic license; the free "restricted" license is too small and fails with SolverError. The other Figure 5 notebooks (C, D, E) read pre-computed results and do not need it.
  • LaTeX.fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation set matplotlib text.usetex=True and require a working system LaTeX (MacTeX/TeX Live).
  • pandas < 3.0 is required (see the setup section above).

About

Analysis and modeling code associated with the paper "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Framework".

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Overview

This repository provides all relevant code used in the manuscript titled "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Algorithm", written by authors Jaleesa Houle, Austin Lopez, Kevin Christie, Gaurav Kumar, David Stupski, Aditya Nair, and Floris van Breugel.

The preprint is available on bioRxiv at:

https://www.biorxiv.org/content/10.64898/2026.07.21.739673v1

Downloading the Data

All data necessary to run the analyses and generate the figures can be found on Dryad:

https://doi.org/10.5061/dryad.x0k6djhwc

(Note: data will be made public upon publication.)

Download each zip archive from Dryad and unzip into the Data/ directory. See Data/download_from_dryad_and_unzip_here.txt for the full expected directory structure after unzipping.

Setting up the environment

The figure notebooks were developed and validated on Python 3.11. Follow the steps below to create an environment, install everything, and run the repo.

1. Create and activate an environment

Using conda (recommended, matches how the repo was validated):

conda create -n splitflow python=3.11 -y
conda activate splitflow

Or using a plain virtual environment:

python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate

2. Install the Python dependencies and the splitflow package

pip install -r requirements.txt # all Python dependencies (incl. the private lab repos)
pip install -e .# installs the local splitflow package (the Code/ directory)

requirements.txt pins the dependencies that need care:

  • pandas < 3.0 (required). pandas 3.0 makes Copy-on-Write mandatory, which returns read-only arrays from DataFrame.values; the unifying-algorithm alignment code performs in-place array operations and fails with "output array is read-only" under pandas ≥ 3.0.
  • Private lab repositoriesalign_course_direction_analysis, braid_tools (imported as braid_analysis), FlyPlotLib (fly_plot_lib), and FlyStat (flystat) are installed directly from git (pinned URLs in requirements.txt); you need access to those repositories.

Once installed, notebooks import the shared utilities from the package, e.g.:

importsplitflow.kinematicsaskinematicsfromsplitflow.set_zorder_functionsimport*fromsplitflow.affine_math_and_plot_helperimport*

3. Install Gurobi + an academic license (required for the unifying algorithm)

The unifying-algorithm fit is a mixed-integer quadratic program solved with cvxpy's GUROBI backend. Gurobi is commercial but free for academics; the bundled "restricted" license is too small for these problems, so a full (named-user academic) license is required.

  1. gurobipy is already installed by requirements.txt (or pip install gurobipy).
  2. Create a free account and request a Named-User Academic License at https://www.gurobi.com/academia/academic-program-and-licenses/ .
  3. From the Gurobi "License Center", copy the grbgetkey command for your license and run it while on your university network/VPN:
    grbgetkey <your-license-key>
    This writes a gurobi.lic file (default ~/gurobi.lic).
  4. Verify it works:
    python -c "import gurobipy; gurobipy.Model(); print('Gurobi OK')"
    If your license is not in the default location, point Gurobi at it with export GRB_LICENSE_FILE=/path/to/gurobi.lic.

4. Install a LaTeX distribution (for a few notebooks)

A working system LaTeX is required for the notebooks that set matplotlib text.usetex=True (fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation). Install MacTeX (macOS) or TeX Live (Linux/Windows) system-wide and ensure latex is on your PATH.

5. Get the data and run

Download the data from Dryad into Data/ (see the "Downloading the Data" section above), then launch Jupyter from the repo root with the environment active:

jupyter lab # or: jupyter notebook

Open any notebook under FigureGeneration/ and run it top to bottom. Notebooks read their inputs from Data/ via relative paths and must be run from their own directory (Jupyter's default), so no path edits are needed.

Directory structure

├── Code/
│ ├── FlyDataProcessingScript.py # Raw fly data preprocessing pipeline
│ ├── PlotUtilities.py # Shared plotting helpers
│ ├── WindDataProcessing.py # Wind tunnel CFD data processing
│ ├── StochasticAlgorithms.py # Algorithm implementations for Brownian motion, Levy flight, etc.
│ ├── opto_tracking.py # Algorithm implementations for Casting/Circling in CFD
│ ├── kinematics.py # Trajectory kinematics calculations
│ ├── TripletLoss.py # Autoencoder training functions
│ ├── vae_optuna.py # VAE param search code
│ ├── RFClassifier.py # Random forest classifier
│ ├── affine_math_and_plot_helper.py # Affine transform math and plotting for unifying algorithm
│ ├── staircase_approximation.py # Staircase function approximation for unifying algorithm
│ ├── BayesFactorFunctions.py # Bayes factor statistical functions -- used in older version of paper
│ └── FeatureFunctions.py # Trajectory feature extraction -- used in older version of paper
│
│
├── Data/
│ ├── download_from_dryad_and_unzip_here.txt # Download instructions + expected structure
│ │ [download from Dryad and unzip into the directories as shown below]
│ ├── wind_tunnel_data/ # Wind speed measurements (laminar, outdoor, split flow)
│ ├── CFD/
│ │ ├── 2D_PlanarSlices/ # 2D velocity field slices (XY steady/unsteady, XZ)
│ │ ├── 2D_PlumeData/ # 2D odor concentration slices -- used in older version of paper
│ │ └── 3D_Velocity_Fields/ # 3D velocity fields, 20s simulation (6 archives)
│ ├── Experimental_Fly_Data/ # full/raw fly datasets, flat (merged raw_data + preprocessed_hdfs):
│ │ # per-condition trimmed HDFs, merged all-nights HDFs,
│ │ # training laminar/stillair sets, variable-wind & CFD parquets
│ ├── Simulated_Trajectory_Data/ # Casting, circling, and unifying algorithm trajectories
│ ├── Animal_Trajectory_Data/ # Cross-species trajectories (15 species/agent types)
│ ├── Unifying_Algo_Results/
│ │ ├── Main/ # Analysis outputs for main figures (42 parquet files)
│ │ └── Supplemental/ # Supplemental-only outputs (30 parquet files + var_wind_analyzed/):
│ │ # variable-wind, WT-splitflow variants, augmented-sham, df_all_species
│ │ # (results shared with the main figure are read from Main/, not duplicated here)
│ ├── Fig1_Input_Data/ # Data specifically used for Figure 1
│ └── Fig5_Input_Data/ # Data specifically used for Figure 5
│
│
├── FigureGeneration/
│ ├── Main/
│ │ ├── fig_1_math_figure/
│ │ │ ├── A_figure_math_affine_explanation.ipynb
│ │ │ ├── B_figure_math_consolidate_params.ipynb
│ │ │ ├── C_f1_example_trajectories.ipynb
│ │ │ └── misc/
│ │ │ └── basic_plotting_examples/
│ │ │ ├── f1_example_trajectories.ipynb
│ │ │ └── slope_vs_speed_over_length.ipynb
│ │ ├── fig_2_wind_tunnel_CFD_and_sup_fig_1/
│ │ │ ├── fig2_CFD.ipynb
│ │ │ ├── fig2_wind_tunnel_plots.ipynb
│ │ │ └── smokepen_images/
│ │ ├── fig_3_trajectory_characteristics/
│ │ │ └── f3_raw_trajectories.ipynb
│ │ ├── fig_4_autoencoder/
│ │ │ ├── f4_autoencoder.ipynb
│ │ │ ├── triplet_vae_6_classes_70_feats.pth # Trained model weights
│ │ │ └── misc/
│ │ │ └── optuna_hyperparam_search_multiobj_6class.ipynb
│ │ └── fig_5_unifying_algo_analysis/
│ │ ├── analysis_scripts/ # scripts used to generate the data in Unifying_Algo_Results
│ │ │ ├── analyze_NEW_unifying_sims.py
│ │ │ ├── analyze_cfd_sims.py
│ │ │ ├── analyze_laminar.py
│ │ │ ├── analyze_laminar_wt.py
│ │ │ ├── analyze_still.py
│ │ │ ├── analyze_still_wt.py
│ │ │ ├── analyze_unsteady_exp_bottom_on.py
│ │ │ └── analyze_unsteady_exp_top_on.py
│ │ └── generate_figure_5/ # run A→E in order (they populate one shared SVG)
│ │ ├── A_figure_math_explanation.ipynb
│ │ ├── B_figure_example_trajectories.ipynb
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── E_variable_wind_slope_axis_ratio.ipynb
│ │ ├── misc/ # alternate/older panel notebooks (not part of the final figure)
│ │ └── animal_trajectories/
│ │ ├── albatross/
│ │ ├── moth/
│ │ └── shark/
│ └── Supplemental/
│ ├── sup_fig_all_animal_trajectories/
│ │ ├── animal_trajectories_and_alignment.ipynb
│ │ └── animal_data/ # Per-species figure notebooks
│ │ ├── albatross/
│ │ ├── cockroach/
│ │ ├── crabs/
│ │ ├── d_melanogaster/
│ │ ├── d_sechelia/
│ │ ├── eels/
│ │ ├── mosquitoes/
│ │ ├── moths/
│ │ ├── mouse/
│ │ ├── nautical_search/
│ │ ├── nautilus/
│ │ ├── RL_agents/
│ │ ├── sharks/
│ │ ├── terns/
│ │ └── walking_drosophila/
│ ├── sup_fig_altitude_response/
│ │ └── f3_altitude_responses.ipynb
│ ├── sup_fig_extra_real_trajectories/
│ │ └── S3_sample_real_trajectories.ipynb
│ ├── sup_fig_sorted_trajectories/
│ │ ├── supp_All_Trajectories_sorted.ipynb
│ │ └── supplemental_autoencoder_altitude_colored.ipynb
│ ├── sup_fig_synthetic_trajectories/
│ │ └── S5_sample_simulated_trajectories.ipynb
│ ├── sup_fig_tunnel_methods/
│ │ ├── CFD_plots.ipynb
│ │ └── wind_tunnel_plot_maker.ipynb
│ ├── sup_fig_unifying_analysis/
│ │ └── generate_supplementary_figure/
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── make_figure_align_course_directions.py
│ │ ├── make_figure_unifying_algo_results_slope_axis.py
│ │ └── run_all_figures.sh
│ └── sup_fig_vertical_split/
│ └── S1_verticalsplit.ipynb
│
│
├── TrajectorySimulations/
│ ├── generate_unifying_algo_trajectories/
│ │ ├── generate_unifying_trajectories.ipynb # Step-by-step walkthrough
│ │ ├── generate_unifying_trajectories.py # Script version
│ │ └── plot_trajectories.ipynb
│ └── generating_synthetic_test_trajectory_data/
│ ├── Casting_Circling_CFD_trajectory_generation.ipynb # Step-by-step walkthrough
│ ├── cfd_casting_circling_sims.py # Script version
│ ├── stochastic_trajectories.ipynb # Step-by-step walkthrough for all other algorithms
│ └── param_sweep/ │
│
├── LICENSE
└── README.md

Running the simulations

Notebooks are provided for a step-by-step walkthrough of trajectory generation:

  • Unifying algorithm:TrajectorySimulations/generate_unifying_algo_trajectories/generate_unifying_trajectories.ipynb
  • Casting/circling (CFD-informed):TrajectorySimulations/generating_synthetic_test_trajectory_data/Casting_Circling_CFD_trajectory_generation.ipynb
  • Stochastic trajectories:TrajectorySimulations/generating_synthetic_test_trajectory_data/stochastic_trajectories.ipynb

Note: simulations using 3D CFD velocity fields can take a while to run. The CFD 3D data archives (3D_vel_1.zip3D_vel_6.zip, ~4 GB each) are only required if re-running these simulations from scratch.

Generating the figures

Each figure is generated by a Jupyter notebook in FigureGeneration/. Notebooks read their inputs from Data/ via relative paths (e.g. ../../../Data/...), so they must be run from their own directory — which is Jupyter's default. No config.py or path edits are needed; helper modules import either from the installed splitflow package or from .py files that sit alongside the notebook (e.g. set_zorder_functions.py, unifying_algo_analysis_helper.py).

Several notebooks build a composite figurefirst SVG: they read a layout template (e.g. unifying_analysis.svg) and write the populated figure back. For generate_figure_5/, run A → B → C → D → E in order — each populates a different layer of the same shared SVG.

The pre-existing figure outputs shipped with the repo are kept under an output_figs/ subfolder in each figure directory; a fresh run writes its output alongside with a _test suffix so the committed figures are never overwritten.

Figure 5 / Supplemental unifying analysis read pre-computed unifying-algorithm results from Data/Unifying_Algo_Results/ (Main/ for the main figure, Supplemental/ for the supplemental figures), so the figure notebooks run without any preprocessing. To regenerate those results from the raw trajectory data, run the scripts in FigureGeneration/Main/fig_5_unifying_algo_analysis/analysis_scripts/ (each reads from Data/Experimental_Fly_Data/ and writes to Data/Unifying_Algo_Results/).

Known environment requirements / caveats

  • Gurobi academic license.fig_5/generate_figure_5/A and B fit the unifying-algorithm MIQP live and need a full Gurobi academic license; the free "restricted" license is too small and fails with SolverError. The other Figure 5 notebooks (C, D, E) read pre-computed results and do not need it.
  • LaTeX.fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation set matplotlib text.usetex=True and require a working system LaTeX (MacTeX/TeX Live).
  • pandas < 3.0 is required (see the setup section above).

About

Analysis and modeling code associated with the paper "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Framework".

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Overview

This repository provides all relevant code used in the manuscript titled "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Algorithm", written by authors Jaleesa Houle, Austin Lopez, Kevin Christie, Gaurav Kumar, David Stupski, Aditya Nair, and Floris van Breugel.

The preprint is available on bioRxiv at:

https://www.biorxiv.org/content/10.64898/2026.07.21.739673v1

Downloading the Data

All data necessary to run the analyses and generate the figures can be found on Dryad:

https://doi.org/10.5061/dryad.x0k6djhwc

(Note: data will be made public upon publication.)

Download each zip archive from Dryad and unzip into the Data/ directory. See Data/download_from_dryad_and_unzip_here.txt for the full expected directory structure after unzipping.

Setting up the environment

The figure notebooks were developed and validated on Python 3.11. Follow the steps below to create an environment, install everything, and run the repo.

1. Create and activate an environment

Using conda (recommended, matches how the repo was validated):

conda create -n splitflow python=3.11 -y
conda activate splitflow

Or using a plain virtual environment:

python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate

2. Install the Python dependencies and the splitflow package

pip install -r requirements.txt # all Python dependencies (incl. the private lab repos)
pip install -e .# installs the local splitflow package (the Code/ directory)

requirements.txt pins the dependencies that need care:

  • pandas < 3.0 (required). pandas 3.0 makes Copy-on-Write mandatory, which returns read-only arrays from DataFrame.values; the unifying-algorithm alignment code performs in-place array operations and fails with "output array is read-only" under pandas ≥ 3.0.
  • Private lab repositoriesalign_course_direction_analysis, braid_tools (imported as braid_analysis), FlyPlotLib (fly_plot_lib), and FlyStat (flystat) are installed directly from git (pinned URLs in requirements.txt); you need access to those repositories.

Once installed, notebooks import the shared utilities from the package, e.g.:

importsplitflow.kinematicsaskinematicsfromsplitflow.set_zorder_functionsimport*fromsplitflow.affine_math_and_plot_helperimport*

3. Install Gurobi + an academic license (required for the unifying algorithm)

The unifying-algorithm fit is a mixed-integer quadratic program solved with cvxpy's GUROBI backend. Gurobi is commercial but free for academics; the bundled "restricted" license is too small for these problems, so a full (named-user academic) license is required.

  1. gurobipy is already installed by requirements.txt (or pip install gurobipy).
  2. Create a free account and request a Named-User Academic License at https://www.gurobi.com/academia/academic-program-and-licenses/ .
  3. From the Gurobi "License Center", copy the grbgetkey command for your license and run it while on your university network/VPN:
    grbgetkey <your-license-key>
    This writes a gurobi.lic file (default ~/gurobi.lic).
  4. Verify it works:
    python -c "import gurobipy; gurobipy.Model(); print('Gurobi OK')"
    If your license is not in the default location, point Gurobi at it with export GRB_LICENSE_FILE=/path/to/gurobi.lic.

4. Install a LaTeX distribution (for a few notebooks)

A working system LaTeX is required for the notebooks that set matplotlib text.usetex=True (fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation). Install MacTeX (macOS) or TeX Live (Linux/Windows) system-wide and ensure latex is on your PATH.

5. Get the data and run

Download the data from Dryad into Data/ (see the "Downloading the Data" section above), then launch Jupyter from the repo root with the environment active:

jupyter lab # or: jupyter notebook

Open any notebook under FigureGeneration/ and run it top to bottom. Notebooks read their inputs from Data/ via relative paths and must be run from their own directory (Jupyter's default), so no path edits are needed.

Directory structure

├── Code/
│ ├── FlyDataProcessingScript.py # Raw fly data preprocessing pipeline
│ ├── PlotUtilities.py # Shared plotting helpers
│ ├── WindDataProcessing.py # Wind tunnel CFD data processing
│ ├── StochasticAlgorithms.py # Algorithm implementations for Brownian motion, Levy flight, etc.
│ ├── opto_tracking.py # Algorithm implementations for Casting/Circling in CFD
│ ├── kinematics.py # Trajectory kinematics calculations
│ ├── TripletLoss.py # Autoencoder training functions
│ ├── vae_optuna.py # VAE param search code
│ ├── RFClassifier.py # Random forest classifier
│ ├── affine_math_and_plot_helper.py # Affine transform math and plotting for unifying algorithm
│ ├── staircase_approximation.py # Staircase function approximation for unifying algorithm
│ ├── BayesFactorFunctions.py # Bayes factor statistical functions -- used in older version of paper
│ └── FeatureFunctions.py # Trajectory feature extraction -- used in older version of paper
│
│
├── Data/
│ ├── download_from_dryad_and_unzip_here.txt # Download instructions + expected structure
│ │ [download from Dryad and unzip into the directories as shown below]
│ ├── wind_tunnel_data/ # Wind speed measurements (laminar, outdoor, split flow)
│ ├── CFD/
│ │ ├── 2D_PlanarSlices/ # 2D velocity field slices (XY steady/unsteady, XZ)
│ │ ├── 2D_PlumeData/ # 2D odor concentration slices -- used in older version of paper
│ │ └── 3D_Velocity_Fields/ # 3D velocity fields, 20s simulation (6 archives)
│ ├── Experimental_Fly_Data/ # full/raw fly datasets, flat (merged raw_data + preprocessed_hdfs):
│ │ # per-condition trimmed HDFs, merged all-nights HDFs,
│ │ # training laminar/stillair sets, variable-wind & CFD parquets
│ ├── Simulated_Trajectory_Data/ # Casting, circling, and unifying algorithm trajectories
│ ├── Animal_Trajectory_Data/ # Cross-species trajectories (15 species/agent types)
│ ├── Unifying_Algo_Results/
│ │ ├── Main/ # Analysis outputs for main figures (42 parquet files)
│ │ └── Supplemental/ # Supplemental-only outputs (30 parquet files + var_wind_analyzed/):
│ │ # variable-wind, WT-splitflow variants, augmented-sham, df_all_species
│ │ # (results shared with the main figure are read from Main/, not duplicated here)
│ ├── Fig1_Input_Data/ # Data specifically used for Figure 1
│ └── Fig5_Input_Data/ # Data specifically used for Figure 5
│
│
├── FigureGeneration/
│ ├── Main/
│ │ ├── fig_1_math_figure/
│ │ │ ├── A_figure_math_affine_explanation.ipynb
│ │ │ ├── B_figure_math_consolidate_params.ipynb
│ │ │ ├── C_f1_example_trajectories.ipynb
│ │ │ └── misc/
│ │ │ └── basic_plotting_examples/
│ │ │ ├── f1_example_trajectories.ipynb
│ │ │ └── slope_vs_speed_over_length.ipynb
│ │ ├── fig_2_wind_tunnel_CFD_and_sup_fig_1/
│ │ │ ├── fig2_CFD.ipynb
│ │ │ ├── fig2_wind_tunnel_plots.ipynb
│ │ │ └── smokepen_images/
│ │ ├── fig_3_trajectory_characteristics/
│ │ │ └── f3_raw_trajectories.ipynb
│ │ ├── fig_4_autoencoder/
│ │ │ ├── f4_autoencoder.ipynb
│ │ │ ├── triplet_vae_6_classes_70_feats.pth # Trained model weights
│ │ │ └── misc/
│ │ │ └── optuna_hyperparam_search_multiobj_6class.ipynb
│ │ └── fig_5_unifying_algo_analysis/
│ │ ├── analysis_scripts/ # scripts used to generate the data in Unifying_Algo_Results
│ │ │ ├── analyze_NEW_unifying_sims.py
│ │ │ ├── analyze_cfd_sims.py
│ │ │ ├── analyze_laminar.py
│ │ │ ├── analyze_laminar_wt.py
│ │ │ ├── analyze_still.py
│ │ │ ├── analyze_still_wt.py
│ │ │ ├── analyze_unsteady_exp_bottom_on.py
│ │ │ └── analyze_unsteady_exp_top_on.py
│ │ └── generate_figure_5/ # run A→E in order (they populate one shared SVG)
│ │ ├── A_figure_math_explanation.ipynb
│ │ ├── B_figure_example_trajectories.ipynb
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── E_variable_wind_slope_axis_ratio.ipynb
│ │ ├── misc/ # alternate/older panel notebooks (not part of the final figure)
│ │ └── animal_trajectories/
│ │ ├── albatross/
│ │ ├── moth/
│ │ └── shark/
│ └── Supplemental/
│ ├── sup_fig_all_animal_trajectories/
│ │ ├── animal_trajectories_and_alignment.ipynb
│ │ └── animal_data/ # Per-species figure notebooks
│ │ ├── albatross/
│ │ ├── cockroach/
│ │ ├── crabs/
│ │ ├── d_melanogaster/
│ │ ├── d_sechelia/
│ │ ├── eels/
│ │ ├── mosquitoes/
│ │ ├── moths/
│ │ ├── mouse/
│ │ ├── nautical_search/
│ │ ├── nautilus/
│ │ ├── RL_agents/
│ │ ├── sharks/
│ │ ├── terns/
│ │ └── walking_drosophila/
│ ├── sup_fig_altitude_response/
│ │ └── f3_altitude_responses.ipynb
│ ├── sup_fig_extra_real_trajectories/
│ │ └── S3_sample_real_trajectories.ipynb
│ ├── sup_fig_sorted_trajectories/
│ │ ├── supp_All_Trajectories_sorted.ipynb
│ │ └── supplemental_autoencoder_altitude_colored.ipynb
│ ├── sup_fig_synthetic_trajectories/
│ │ └── S5_sample_simulated_trajectories.ipynb
│ ├── sup_fig_tunnel_methods/
│ │ ├── CFD_plots.ipynb
│ │ └── wind_tunnel_plot_maker.ipynb
│ ├── sup_fig_unifying_analysis/
│ │ └── generate_supplementary_figure/
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── make_figure_align_course_directions.py
│ │ ├── make_figure_unifying_algo_results_slope_axis.py
│ │ └── run_all_figures.sh
│ └── sup_fig_vertical_split/
│ └── S1_verticalsplit.ipynb
│
│
├── TrajectorySimulations/
│ ├── generate_unifying_algo_trajectories/
│ │ ├── generate_unifying_trajectories.ipynb # Step-by-step walkthrough
│ │ ├── generate_unifying_trajectories.py # Script version
│ │ └── plot_trajectories.ipynb
│ └── generating_synthetic_test_trajectory_data/
│ ├── Casting_Circling_CFD_trajectory_generation.ipynb # Step-by-step walkthrough
│ ├── cfd_casting_circling_sims.py # Script version
│ ├── stochastic_trajectories.ipynb # Step-by-step walkthrough for all other algorithms
│ └── param_sweep/ │
│
├── LICENSE
└── README.md

Running the simulations

Notebooks are provided for a step-by-step walkthrough of trajectory generation:

  • Unifying algorithm:TrajectorySimulations/generate_unifying_algo_trajectories/generate_unifying_trajectories.ipynb
  • Casting/circling (CFD-informed):TrajectorySimulations/generating_synthetic_test_trajectory_data/Casting_Circling_CFD_trajectory_generation.ipynb
  • Stochastic trajectories:TrajectorySimulations/generating_synthetic_test_trajectory_data/stochastic_trajectories.ipynb

Note: simulations using 3D CFD velocity fields can take a while to run. The CFD 3D data archives (3D_vel_1.zip3D_vel_6.zip, ~4 GB each) are only required if re-running these simulations from scratch.

Generating the figures

Each figure is generated by a Jupyter notebook in FigureGeneration/. Notebooks read their inputs from Data/ via relative paths (e.g. ../../../Data/...), so they must be run from their own directory — which is Jupyter's default. No config.py or path edits are needed; helper modules import either from the installed splitflow package or from .py files that sit alongside the notebook (e.g. set_zorder_functions.py, unifying_algo_analysis_helper.py).

Several notebooks build a composite figurefirst SVG: they read a layout template (e.g. unifying_analysis.svg) and write the populated figure back. For generate_figure_5/, run A → B → C → D → E in order — each populates a different layer of the same shared SVG.

The pre-existing figure outputs shipped with the repo are kept under an output_figs/ subfolder in each figure directory; a fresh run writes its output alongside with a _test suffix so the committed figures are never overwritten.

Figure 5 / Supplemental unifying analysis read pre-computed unifying-algorithm results from Data/Unifying_Algo_Results/ (Main/ for the main figure, Supplemental/ for the supplemental figures), so the figure notebooks run without any preprocessing. To regenerate those results from the raw trajectory data, run the scripts in FigureGeneration/Main/fig_5_unifying_algo_analysis/analysis_scripts/ (each reads from Data/Experimental_Fly_Data/ and writes to Data/Unifying_Algo_Results/).

Known environment requirements / caveats

  • Gurobi academic license.fig_5/generate_figure_5/A and B fit the unifying-algorithm MIQP live and need a full Gurobi academic license; the free "restricted" license is too small and fails with SolverError. The other Figure 5 notebooks (C, D, E) read pre-computed results and do not need it.
  • LaTeX.fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation set matplotlib text.usetex=True and require a working system LaTeX (MacTeX/TeX Live).
  • pandas < 3.0 is required (see the setup section above).

About

Analysis and modeling code associated with the paper "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Framework".

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Overview

This repository provides all relevant code used in the manuscript titled "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Algorithm", written by authors Jaleesa Houle, Austin Lopez, Kevin Christie, Gaurav Kumar, David Stupski, Aditya Nair, and Floris van Breugel.

The preprint is available on bioRxiv at:

https://www.biorxiv.org/content/10.64898/2026.07.21.739673v1

Downloading the Data

All data necessary to run the analyses and generate the figures can be found on Dryad:

https://doi.org/10.5061/dryad.x0k6djhwc

(Note: data will be made public upon publication.)

Download each zip archive from Dryad and unzip into the Data/ directory. See Data/download_from_dryad_and_unzip_here.txt for the full expected directory structure after unzipping.

Setting up the environment

The figure notebooks were developed and validated on Python 3.11. Follow the steps below to create an environment, install everything, and run the repo.

1. Create and activate an environment

Using conda (recommended, matches how the repo was validated):

conda create -n splitflow python=3.11 -y
conda activate splitflow

Or using a plain virtual environment:

python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate

2. Install the Python dependencies and the splitflow package

pip install -r requirements.txt # all Python dependencies (incl. the private lab repos)
pip install -e .# installs the local splitflow package (the Code/ directory)

requirements.txt pins the dependencies that need care:

  • pandas < 3.0 (required). pandas 3.0 makes Copy-on-Write mandatory, which returns read-only arrays from DataFrame.values; the unifying-algorithm alignment code performs in-place array operations and fails with "output array is read-only" under pandas ≥ 3.0.
  • Private lab repositoriesalign_course_direction_analysis, braid_tools (imported as braid_analysis), FlyPlotLib (fly_plot_lib), and FlyStat (flystat) are installed directly from git (pinned URLs in requirements.txt); you need access to those repositories.

Once installed, notebooks import the shared utilities from the package, e.g.:

importsplitflow.kinematicsaskinematicsfromsplitflow.set_zorder_functionsimport*fromsplitflow.affine_math_and_plot_helperimport*

3. Install Gurobi + an academic license (required for the unifying algorithm)

The unifying-algorithm fit is a mixed-integer quadratic program solved with cvxpy's GUROBI backend. Gurobi is commercial but free for academics; the bundled "restricted" license is too small for these problems, so a full (named-user academic) license is required.

  1. gurobipy is already installed by requirements.txt (or pip install gurobipy).
  2. Create a free account and request a Named-User Academic License at https://www.gurobi.com/academia/academic-program-and-licenses/ .
  3. From the Gurobi "License Center", copy the grbgetkey command for your license and run it while on your university network/VPN:
    grbgetkey <your-license-key>
    This writes a gurobi.lic file (default ~/gurobi.lic).
  4. Verify it works:
    python -c "import gurobipy; gurobipy.Model(); print('Gurobi OK')"
    If your license is not in the default location, point Gurobi at it with export GRB_LICENSE_FILE=/path/to/gurobi.lic.

4. Install a LaTeX distribution (for a few notebooks)

A working system LaTeX is required for the notebooks that set matplotlib text.usetex=True (fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation). Install MacTeX (macOS) or TeX Live (Linux/Windows) system-wide and ensure latex is on your PATH.

5. Get the data and run

Download the data from Dryad into Data/ (see the "Downloading the Data" section above), then launch Jupyter from the repo root with the environment active:

jupyter lab # or: jupyter notebook

Open any notebook under FigureGeneration/ and run it top to bottom. Notebooks read their inputs from Data/ via relative paths and must be run from their own directory (Jupyter's default), so no path edits are needed.

Directory structure

├── Code/
│ ├── FlyDataProcessingScript.py # Raw fly data preprocessing pipeline
│ ├── PlotUtilities.py # Shared plotting helpers
│ ├── WindDataProcessing.py # Wind tunnel CFD data processing
│ ├── StochasticAlgorithms.py # Algorithm implementations for Brownian motion, Levy flight, etc.
│ ├── opto_tracking.py # Algorithm implementations for Casting/Circling in CFD
│ ├── kinematics.py # Trajectory kinematics calculations
│ ├── TripletLoss.py # Autoencoder training functions
│ ├── vae_optuna.py # VAE param search code
│ ├── RFClassifier.py # Random forest classifier
│ ├── affine_math_and_plot_helper.py # Affine transform math and plotting for unifying algorithm
│ ├── staircase_approximation.py # Staircase function approximation for unifying algorithm
│ ├── BayesFactorFunctions.py # Bayes factor statistical functions -- used in older version of paper
│ └── FeatureFunctions.py # Trajectory feature extraction -- used in older version of paper
│
│
├── Data/
│ ├── download_from_dryad_and_unzip_here.txt # Download instructions + expected structure
│ │ [download from Dryad and unzip into the directories as shown below]
│ ├── wind_tunnel_data/ # Wind speed measurements (laminar, outdoor, split flow)
│ ├── CFD/
│ │ ├── 2D_PlanarSlices/ # 2D velocity field slices (XY steady/unsteady, XZ)
│ │ ├── 2D_PlumeData/ # 2D odor concentration slices -- used in older version of paper
│ │ └── 3D_Velocity_Fields/ # 3D velocity fields, 20s simulation (6 archives)
│ ├── Experimental_Fly_Data/ # full/raw fly datasets, flat (merged raw_data + preprocessed_hdfs):
│ │ # per-condition trimmed HDFs, merged all-nights HDFs,
│ │ # training laminar/stillair sets, variable-wind & CFD parquets
│ ├── Simulated_Trajectory_Data/ # Casting, circling, and unifying algorithm trajectories
│ ├── Animal_Trajectory_Data/ # Cross-species trajectories (15 species/agent types)
│ ├── Unifying_Algo_Results/
│ │ ├── Main/ # Analysis outputs for main figures (42 parquet files)
│ │ └── Supplemental/ # Supplemental-only outputs (30 parquet files + var_wind_analyzed/):
│ │ # variable-wind, WT-splitflow variants, augmented-sham, df_all_species
│ │ # (results shared with the main figure are read from Main/, not duplicated here)
│ ├── Fig1_Input_Data/ # Data specifically used for Figure 1
│ └── Fig5_Input_Data/ # Data specifically used for Figure 5
│
│
├── FigureGeneration/
│ ├── Main/
│ │ ├── fig_1_math_figure/
│ │ │ ├── A_figure_math_affine_explanation.ipynb
│ │ │ ├── B_figure_math_consolidate_params.ipynb
│ │ │ ├── C_f1_example_trajectories.ipynb
│ │ │ └── misc/
│ │ │ └── basic_plotting_examples/
│ │ │ ├── f1_example_trajectories.ipynb
│ │ │ └── slope_vs_speed_over_length.ipynb
│ │ ├── fig_2_wind_tunnel_CFD_and_sup_fig_1/
│ │ │ ├── fig2_CFD.ipynb
│ │ │ ├── fig2_wind_tunnel_plots.ipynb
│ │ │ └── smokepen_images/
│ │ ├── fig_3_trajectory_characteristics/
│ │ │ └── f3_raw_trajectories.ipynb
│ │ ├── fig_4_autoencoder/
│ │ │ ├── f4_autoencoder.ipynb
│ │ │ ├── triplet_vae_6_classes_70_feats.pth # Trained model weights
│ │ │ └── misc/
│ │ │ └── optuna_hyperparam_search_multiobj_6class.ipynb
│ │ └── fig_5_unifying_algo_analysis/
│ │ ├── analysis_scripts/ # scripts used to generate the data in Unifying_Algo_Results
│ │ │ ├── analyze_NEW_unifying_sims.py
│ │ │ ├── analyze_cfd_sims.py
│ │ │ ├── analyze_laminar.py
│ │ │ ├── analyze_laminar_wt.py
│ │ │ ├── analyze_still.py
│ │ │ ├── analyze_still_wt.py
│ │ │ ├── analyze_unsteady_exp_bottom_on.py
│ │ │ └── analyze_unsteady_exp_top_on.py
│ │ └── generate_figure_5/ # run A→E in order (they populate one shared SVG)
│ │ ├── A_figure_math_explanation.ipynb
│ │ ├── B_figure_example_trajectories.ipynb
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── E_variable_wind_slope_axis_ratio.ipynb
│ │ ├── misc/ # alternate/older panel notebooks (not part of the final figure)
│ │ └── animal_trajectories/
│ │ ├── albatross/
│ │ ├── moth/
│ │ └── shark/
│ └── Supplemental/
│ ├── sup_fig_all_animal_trajectories/
│ │ ├── animal_trajectories_and_alignment.ipynb
│ │ └── animal_data/ # Per-species figure notebooks
│ │ ├── albatross/
│ │ ├── cockroach/
│ │ ├── crabs/
│ │ ├── d_melanogaster/
│ │ ├── d_sechelia/
│ │ ├── eels/
│ │ ├── mosquitoes/
│ │ ├── moths/
│ │ ├── mouse/
│ │ ├── nautical_search/
│ │ ├── nautilus/
│ │ ├── RL_agents/
│ │ ├── sharks/
│ │ ├── terns/
│ │ └── walking_drosophila/
│ ├── sup_fig_altitude_response/
│ │ └── f3_altitude_responses.ipynb
│ ├── sup_fig_extra_real_trajectories/
│ │ └── S3_sample_real_trajectories.ipynb
│ ├── sup_fig_sorted_trajectories/
│ │ ├── supp_All_Trajectories_sorted.ipynb
│ │ └── supplemental_autoencoder_altitude_colored.ipynb
│ ├── sup_fig_synthetic_trajectories/
│ │ └── S5_sample_simulated_trajectories.ipynb
│ ├── sup_fig_tunnel_methods/
│ │ ├── CFD_plots.ipynb
│ │ └── wind_tunnel_plot_maker.ipynb
│ ├── sup_fig_unifying_analysis/
│ │ └── generate_supplementary_figure/
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── make_figure_align_course_directions.py
│ │ ├── make_figure_unifying_algo_results_slope_axis.py
│ │ └── run_all_figures.sh
│ └── sup_fig_vertical_split/
│ └── S1_verticalsplit.ipynb
│
│
├── TrajectorySimulations/
│ ├── generate_unifying_algo_trajectories/
│ │ ├── generate_unifying_trajectories.ipynb # Step-by-step walkthrough
│ │ ├── generate_unifying_trajectories.py # Script version
│ │ └── plot_trajectories.ipynb
│ └── generating_synthetic_test_trajectory_data/
│ ├── Casting_Circling_CFD_trajectory_generation.ipynb # Step-by-step walkthrough
│ ├── cfd_casting_circling_sims.py # Script version
│ ├── stochastic_trajectories.ipynb # Step-by-step walkthrough for all other algorithms
│ └── param_sweep/ │
│
├── LICENSE
└── README.md

Running the simulations

Notebooks are provided for a step-by-step walkthrough of trajectory generation:

  • Unifying algorithm:TrajectorySimulations/generate_unifying_algo_trajectories/generate_unifying_trajectories.ipynb
  • Casting/circling (CFD-informed):TrajectorySimulations/generating_synthetic_test_trajectory_data/Casting_Circling_CFD_trajectory_generation.ipynb
  • Stochastic trajectories:TrajectorySimulations/generating_synthetic_test_trajectory_data/stochastic_trajectories.ipynb

Note: simulations using 3D CFD velocity fields can take a while to run. The CFD 3D data archives (3D_vel_1.zip3D_vel_6.zip, ~4 GB each) are only required if re-running these simulations from scratch.

Generating the figures

Each figure is generated by a Jupyter notebook in FigureGeneration/. Notebooks read their inputs from Data/ via relative paths (e.g. ../../../Data/...), so they must be run from their own directory — which is Jupyter's default. No config.py or path edits are needed; helper modules import either from the installed splitflow package or from .py files that sit alongside the notebook (e.g. set_zorder_functions.py, unifying_algo_analysis_helper.py).

Several notebooks build a composite figurefirst SVG: they read a layout template (e.g. unifying_analysis.svg) and write the populated figure back. For generate_figure_5/, run A → B → C → D → E in order — each populates a different layer of the same shared SVG.

The pre-existing figure outputs shipped with the repo are kept under an output_figs/ subfolder in each figure directory; a fresh run writes its output alongside with a _test suffix so the committed figures are never overwritten.

Figure 5 / Supplemental unifying analysis read pre-computed unifying-algorithm results from Data/Unifying_Algo_Results/ (Main/ for the main figure, Supplemental/ for the supplemental figures), so the figure notebooks run without any preprocessing. To regenerate those results from the raw trajectory data, run the scripts in FigureGeneration/Main/fig_5_unifying_algo_analysis/analysis_scripts/ (each reads from Data/Experimental_Fly_Data/ and writes to Data/Unifying_Algo_Results/).

Known environment requirements / caveats

  • Gurobi academic license.fig_5/generate_figure_5/A and B fit the unifying-algorithm MIQP live and need a full Gurobi academic license; the free "restricted" license is too small and fails with SolverError. The other Figure 5 notebooks (C, D, E) read pre-computed results and do not need it.
  • LaTeX.fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation set matplotlib text.usetex=True and require a working system LaTeX (MacTeX/TeX Live).
  • pandas < 3.0 is required (see the setup section above).

About

Analysis and modeling code associated with the paper "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Framework".

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Overview

This repository provides all relevant code used in the manuscript titled "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Algorithm", written by authors Jaleesa Houle, Austin Lopez, Kevin Christie, Gaurav Kumar, David Stupski, Aditya Nair, and Floris van Breugel.

The preprint is available on bioRxiv at:

https://www.biorxiv.org/content/10.64898/2026.07.21.739673v1

Downloading the Data

All data necessary to run the analyses and generate the figures can be found on Dryad:

https://doi.org/10.5061/dryad.x0k6djhwc

(Note: data will be made public upon publication.)

Download each zip archive from Dryad and unzip into the Data/ directory. See Data/download_from_dryad_and_unzip_here.txt for the full expected directory structure after unzipping.

Setting up the environment

The figure notebooks were developed and validated on Python 3.11. Follow the steps below to create an environment, install everything, and run the repo.

1. Create and activate an environment

Using conda (recommended, matches how the repo was validated):

conda create -n splitflow python=3.11 -y
conda activate splitflow

Or using a plain virtual environment:

python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate

2. Install the Python dependencies and the splitflow package

pip install -r requirements.txt # all Python dependencies (incl. the private lab repos)
pip install -e .# installs the local splitflow package (the Code/ directory)

requirements.txt pins the dependencies that need care:

  • pandas < 3.0 (required). pandas 3.0 makes Copy-on-Write mandatory, which returns read-only arrays from DataFrame.values; the unifying-algorithm alignment code performs in-place array operations and fails with "output array is read-only" under pandas ≥ 3.0.
  • Private lab repositoriesalign_course_direction_analysis, braid_tools (imported as braid_analysis), FlyPlotLib (fly_plot_lib), and FlyStat (flystat) are installed directly from git (pinned URLs in requirements.txt); you need access to those repositories.

Once installed, notebooks import the shared utilities from the package, e.g.:

importsplitflow.kinematicsaskinematicsfromsplitflow.set_zorder_functionsimport*fromsplitflow.affine_math_and_plot_helperimport*

3. Install Gurobi + an academic license (required for the unifying algorithm)

The unifying-algorithm fit is a mixed-integer quadratic program solved with cvxpy's GUROBI backend. Gurobi is commercial but free for academics; the bundled "restricted" license is too small for these problems, so a full (named-user academic) license is required.

  1. gurobipy is already installed by requirements.txt (or pip install gurobipy).
  2. Create a free account and request a Named-User Academic License at https://www.gurobi.com/academia/academic-program-and-licenses/ .
  3. From the Gurobi "License Center", copy the grbgetkey command for your license and run it while on your university network/VPN:
    grbgetkey <your-license-key>
    This writes a gurobi.lic file (default ~/gurobi.lic).
  4. Verify it works:
    python -c "import gurobipy; gurobipy.Model(); print('Gurobi OK')"
    If your license is not in the default location, point Gurobi at it with export GRB_LICENSE_FILE=/path/to/gurobi.lic.

4. Install a LaTeX distribution (for a few notebooks)

A working system LaTeX is required for the notebooks that set matplotlib text.usetex=True (fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation). Install MacTeX (macOS) or TeX Live (Linux/Windows) system-wide and ensure latex is on your PATH.

5. Get the data and run

Download the data from Dryad into Data/ (see the "Downloading the Data" section above), then launch Jupyter from the repo root with the environment active:

jupyter lab # or: jupyter notebook

Open any notebook under FigureGeneration/ and run it top to bottom. Notebooks read their inputs from Data/ via relative paths and must be run from their own directory (Jupyter's default), so no path edits are needed.

Directory structure

├── Code/
│ ├── FlyDataProcessingScript.py # Raw fly data preprocessing pipeline
│ ├── PlotUtilities.py # Shared plotting helpers
│ ├── WindDataProcessing.py # Wind tunnel CFD data processing
│ ├── StochasticAlgorithms.py # Algorithm implementations for Brownian motion, Levy flight, etc.
│ ├── opto_tracking.py # Algorithm implementations for Casting/Circling in CFD
│ ├── kinematics.py # Trajectory kinematics calculations
│ ├── TripletLoss.py # Autoencoder training functions
│ ├── vae_optuna.py # VAE param search code
│ ├── RFClassifier.py # Random forest classifier
│ ├── affine_math_and_plot_helper.py # Affine transform math and plotting for unifying algorithm
│ ├── staircase_approximation.py # Staircase function approximation for unifying algorithm
│ ├── BayesFactorFunctions.py # Bayes factor statistical functions -- used in older version of paper
│ └── FeatureFunctions.py # Trajectory feature extraction -- used in older version of paper
│
│
├── Data/
│ ├── download_from_dryad_and_unzip_here.txt # Download instructions + expected structure
│ │ [download from Dryad and unzip into the directories as shown below]
│ ├── wind_tunnel_data/ # Wind speed measurements (laminar, outdoor, split flow)
│ ├── CFD/
│ │ ├── 2D_PlanarSlices/ # 2D velocity field slices (XY steady/unsteady, XZ)
│ │ ├── 2D_PlumeData/ # 2D odor concentration slices -- used in older version of paper
│ │ └── 3D_Velocity_Fields/ # 3D velocity fields, 20s simulation (6 archives)
│ ├── Experimental_Fly_Data/ # full/raw fly datasets, flat (merged raw_data + preprocessed_hdfs):
│ │ # per-condition trimmed HDFs, merged all-nights HDFs,
│ │ # training laminar/stillair sets, variable-wind & CFD parquets
│ ├── Simulated_Trajectory_Data/ # Casting, circling, and unifying algorithm trajectories
│ ├── Animal_Trajectory_Data/ # Cross-species trajectories (15 species/agent types)
│ ├── Unifying_Algo_Results/
│ │ ├── Main/ # Analysis outputs for main figures (42 parquet files)
│ │ └── Supplemental/ # Supplemental-only outputs (30 parquet files + var_wind_analyzed/):
│ │ # variable-wind, WT-splitflow variants, augmented-sham, df_all_species
│ │ # (results shared with the main figure are read from Main/, not duplicated here)
│ ├── Fig1_Input_Data/ # Data specifically used for Figure 1
│ └── Fig5_Input_Data/ # Data specifically used for Figure 5
│
│
├── FigureGeneration/
│ ├── Main/
│ │ ├── fig_1_math_figure/
│ │ │ ├── A_figure_math_affine_explanation.ipynb
│ │ │ ├── B_figure_math_consolidate_params.ipynb
│ │ │ ├── C_f1_example_trajectories.ipynb
│ │ │ └── misc/
│ │ │ └── basic_plotting_examples/
│ │ │ ├── f1_example_trajectories.ipynb
│ │ │ └── slope_vs_speed_over_length.ipynb
│ │ ├── fig_2_wind_tunnel_CFD_and_sup_fig_1/
│ │ │ ├── fig2_CFD.ipynb
│ │ │ ├── fig2_wind_tunnel_plots.ipynb
│ │ │ └── smokepen_images/
│ │ ├── fig_3_trajectory_characteristics/
│ │ │ └── f3_raw_trajectories.ipynb
│ │ ├── fig_4_autoencoder/
│ │ │ ├── f4_autoencoder.ipynb
│ │ │ ├── triplet_vae_6_classes_70_feats.pth # Trained model weights
│ │ │ └── misc/
│ │ │ └── optuna_hyperparam_search_multiobj_6class.ipynb
│ │ └── fig_5_unifying_algo_analysis/
│ │ ├── analysis_scripts/ # scripts used to generate the data in Unifying_Algo_Results
│ │ │ ├── analyze_NEW_unifying_sims.py
│ │ │ ├── analyze_cfd_sims.py
│ │ │ ├── analyze_laminar.py
│ │ │ ├── analyze_laminar_wt.py
│ │ │ ├── analyze_still.py
│ │ │ ├── analyze_still_wt.py
│ │ │ ├── analyze_unsteady_exp_bottom_on.py
│ │ │ └── analyze_unsteady_exp_top_on.py
│ │ └── generate_figure_5/ # run A→E in order (they populate one shared SVG)
│ │ ├── A_figure_math_explanation.ipynb
│ │ ├── B_figure_example_trajectories.ipynb
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── E_variable_wind_slope_axis_ratio.ipynb
│ │ ├── misc/ # alternate/older panel notebooks (not part of the final figure)
│ │ └── animal_trajectories/
│ │ ├── albatross/
│ │ ├── moth/
│ │ └── shark/
│ └── Supplemental/
│ ├── sup_fig_all_animal_trajectories/
│ │ ├── animal_trajectories_and_alignment.ipynb
│ │ └── animal_data/ # Per-species figure notebooks
│ │ ├── albatross/
│ │ ├── cockroach/
│ │ ├── crabs/
│ │ ├── d_melanogaster/
│ │ ├── d_sechelia/
│ │ ├── eels/
│ │ ├── mosquitoes/
│ │ ├── moths/
│ │ ├── mouse/
│ │ ├── nautical_search/
│ │ ├── nautilus/
│ │ ├── RL_agents/
│ │ ├── sharks/
│ │ ├── terns/
│ │ └── walking_drosophila/
│ ├── sup_fig_altitude_response/
│ │ └── f3_altitude_responses.ipynb
│ ├── sup_fig_extra_real_trajectories/
│ │ └── S3_sample_real_trajectories.ipynb
│ ├── sup_fig_sorted_trajectories/
│ │ ├── supp_All_Trajectories_sorted.ipynb
│ │ └── supplemental_autoencoder_altitude_colored.ipynb
│ ├── sup_fig_synthetic_trajectories/
│ │ └── S5_sample_simulated_trajectories.ipynb
│ ├── sup_fig_tunnel_methods/
│ │ ├── CFD_plots.ipynb
│ │ └── wind_tunnel_plot_maker.ipynb
│ ├── sup_fig_unifying_analysis/
│ │ └── generate_supplementary_figure/
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── make_figure_align_course_directions.py
│ │ ├── make_figure_unifying_algo_results_slope_axis.py
│ │ └── run_all_figures.sh
│ └── sup_fig_vertical_split/
│ └── S1_verticalsplit.ipynb
│
│
├── TrajectorySimulations/
│ ├── generate_unifying_algo_trajectories/
│ │ ├── generate_unifying_trajectories.ipynb # Step-by-step walkthrough
│ │ ├── generate_unifying_trajectories.py # Script version
│ │ └── plot_trajectories.ipynb
│ └── generating_synthetic_test_trajectory_data/
│ ├── Casting_Circling_CFD_trajectory_generation.ipynb # Step-by-step walkthrough
│ ├── cfd_casting_circling_sims.py # Script version
│ ├── stochastic_trajectories.ipynb # Step-by-step walkthrough for all other algorithms
│ └── param_sweep/ │
│
├── LICENSE
└── README.md

Running the simulations

Notebooks are provided for a step-by-step walkthrough of trajectory generation:

  • Unifying algorithm:TrajectorySimulations/generate_unifying_algo_trajectories/generate_unifying_trajectories.ipynb
  • Casting/circling (CFD-informed):TrajectorySimulations/generating_synthetic_test_trajectory_data/Casting_Circling_CFD_trajectory_generation.ipynb
  • Stochastic trajectories:TrajectorySimulations/generating_synthetic_test_trajectory_data/stochastic_trajectories.ipynb

Note: simulations using 3D CFD velocity fields can take a while to run. The CFD 3D data archives (3D_vel_1.zip3D_vel_6.zip, ~4 GB each) are only required if re-running these simulations from scratch.

Generating the figures

Each figure is generated by a Jupyter notebook in FigureGeneration/. Notebooks read their inputs from Data/ via relative paths (e.g. ../../../Data/...), so they must be run from their own directory — which is Jupyter's default. No config.py or path edits are needed; helper modules import either from the installed splitflow package or from .py files that sit alongside the notebook (e.g. set_zorder_functions.py, unifying_algo_analysis_helper.py).

Several notebooks build a composite figurefirst SVG: they read a layout template (e.g. unifying_analysis.svg) and write the populated figure back. For generate_figure_5/, run A → B → C → D → E in order — each populates a different layer of the same shared SVG.

The pre-existing figure outputs shipped with the repo are kept under an output_figs/ subfolder in each figure directory; a fresh run writes its output alongside with a _test suffix so the committed figures are never overwritten.

Figure 5 / Supplemental unifying analysis read pre-computed unifying-algorithm results from Data/Unifying_Algo_Results/ (Main/ for the main figure, Supplemental/ for the supplemental figures), so the figure notebooks run without any preprocessing. To regenerate those results from the raw trajectory data, run the scripts in FigureGeneration/Main/fig_5_unifying_algo_analysis/analysis_scripts/ (each reads from Data/Experimental_Fly_Data/ and writes to Data/Unifying_Algo_Results/).

Known environment requirements / caveats

  • Gurobi academic license.fig_5/generate_figure_5/A and B fit the unifying-algorithm MIQP live and need a full Gurobi academic license; the free "restricted" license is too small and fails with SolverError. The other Figure 5 notebooks (C, D, E) read pre-computed results and do not need it.
  • LaTeX.fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation set matplotlib text.usetex=True and require a working system LaTeX (MacTeX/TeX Live).
  • pandas < 3.0 is required (see the setup section above).

About

Analysis and modeling code associated with the paper "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Framework".

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Overview

This repository provides all relevant code used in the manuscript titled "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Algorithm", written by authors Jaleesa Houle, Austin Lopez, Kevin Christie, Gaurav Kumar, David Stupski, Aditya Nair, and Floris van Breugel.

The preprint is available on bioRxiv at:

https://www.biorxiv.org/content/10.64898/2026.07.21.739673v1

Downloading the Data

All data necessary to run the analyses and generate the figures can be found on Dryad:

https://doi.org/10.5061/dryad.x0k6djhwc

(Note: data will be made public upon publication.)

Download each zip archive from Dryad and unzip into the Data/ directory. See Data/download_from_dryad_and_unzip_here.txt for the full expected directory structure after unzipping.

Setting up the environment

The figure notebooks were developed and validated on Python 3.11. Follow the steps below to create an environment, install everything, and run the repo.

1. Create and activate an environment

Using conda (recommended, matches how the repo was validated):

conda create -n splitflow python=3.11 -y
conda activate splitflow

Or using a plain virtual environment:

python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate

2. Install the Python dependencies and the splitflow package

pip install -r requirements.txt # all Python dependencies (incl. the private lab repos)
pip install -e .# installs the local splitflow package (the Code/ directory)

requirements.txt pins the dependencies that need care:

  • pandas < 3.0 (required). pandas 3.0 makes Copy-on-Write mandatory, which returns read-only arrays from DataFrame.values; the unifying-algorithm alignment code performs in-place array operations and fails with "output array is read-only" under pandas ≥ 3.0.
  • Private lab repositoriesalign_course_direction_analysis, braid_tools (imported as braid_analysis), FlyPlotLib (fly_plot_lib), and FlyStat (flystat) are installed directly from git (pinned URLs in requirements.txt); you need access to those repositories.

Once installed, notebooks import the shared utilities from the package, e.g.:

importsplitflow.kinematicsaskinematicsfromsplitflow.set_zorder_functionsimport*fromsplitflow.affine_math_and_plot_helperimport*

3. Install Gurobi + an academic license (required for the unifying algorithm)

The unifying-algorithm fit is a mixed-integer quadratic program solved with cvxpy's GUROBI backend. Gurobi is commercial but free for academics; the bundled "restricted" license is too small for these problems, so a full (named-user academic) license is required.

  1. gurobipy is already installed by requirements.txt (or pip install gurobipy).
  2. Create a free account and request a Named-User Academic License at https://www.gurobi.com/academia/academic-program-and-licenses/ .
  3. From the Gurobi "License Center", copy the grbgetkey command for your license and run it while on your university network/VPN:
    grbgetkey <your-license-key>
    This writes a gurobi.lic file (default ~/gurobi.lic).
  4. Verify it works:
    python -c "import gurobipy; gurobipy.Model(); print('Gurobi OK')"
    If your license is not in the default location, point Gurobi at it with export GRB_LICENSE_FILE=/path/to/gurobi.lic.

4. Install a LaTeX distribution (for a few notebooks)

A working system LaTeX is required for the notebooks that set matplotlib text.usetex=True (fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation). Install MacTeX (macOS) or TeX Live (Linux/Windows) system-wide and ensure latex is on your PATH.

5. Get the data and run

Download the data from Dryad into Data/ (see the "Downloading the Data" section above), then launch Jupyter from the repo root with the environment active:

jupyter lab # or: jupyter notebook

Open any notebook under FigureGeneration/ and run it top to bottom. Notebooks read their inputs from Data/ via relative paths and must be run from their own directory (Jupyter's default), so no path edits are needed.

Directory structure

├── Code/
│ ├── FlyDataProcessingScript.py # Raw fly data preprocessing pipeline
│ ├── PlotUtilities.py # Shared plotting helpers
│ ├── WindDataProcessing.py # Wind tunnel CFD data processing
│ ├── StochasticAlgorithms.py # Algorithm implementations for Brownian motion, Levy flight, etc.
│ ├── opto_tracking.py # Algorithm implementations for Casting/Circling in CFD
│ ├── kinematics.py # Trajectory kinematics calculations
│ ├── TripletLoss.py # Autoencoder training functions
│ ├── vae_optuna.py # VAE param search code
│ ├── RFClassifier.py # Random forest classifier
│ ├── affine_math_and_plot_helper.py # Affine transform math and plotting for unifying algorithm
│ ├── staircase_approximation.py # Staircase function approximation for unifying algorithm
│ ├── BayesFactorFunctions.py # Bayes factor statistical functions -- used in older version of paper
│ └── FeatureFunctions.py # Trajectory feature extraction -- used in older version of paper
│
│
├── Data/
│ ├── download_from_dryad_and_unzip_here.txt # Download instructions + expected structure
│ │ [download from Dryad and unzip into the directories as shown below]
│ ├── wind_tunnel_data/ # Wind speed measurements (laminar, outdoor, split flow)
│ ├── CFD/
│ │ ├── 2D_PlanarSlices/ # 2D velocity field slices (XY steady/unsteady, XZ)
│ │ ├── 2D_PlumeData/ # 2D odor concentration slices -- used in older version of paper
│ │ └── 3D_Velocity_Fields/ # 3D velocity fields, 20s simulation (6 archives)
│ ├── Experimental_Fly_Data/ # full/raw fly datasets, flat (merged raw_data + preprocessed_hdfs):
│ │ # per-condition trimmed HDFs, merged all-nights HDFs,
│ │ # training laminar/stillair sets, variable-wind & CFD parquets
│ ├── Simulated_Trajectory_Data/ # Casting, circling, and unifying algorithm trajectories
│ ├── Animal_Trajectory_Data/ # Cross-species trajectories (15 species/agent types)
│ ├── Unifying_Algo_Results/
│ │ ├── Main/ # Analysis outputs for main figures (42 parquet files)
│ │ └── Supplemental/ # Supplemental-only outputs (30 parquet files + var_wind_analyzed/):
│ │ # variable-wind, WT-splitflow variants, augmented-sham, df_all_species
│ │ # (results shared with the main figure are read from Main/, not duplicated here)
│ ├── Fig1_Input_Data/ # Data specifically used for Figure 1
│ └── Fig5_Input_Data/ # Data specifically used for Figure 5
│
│
├── FigureGeneration/
│ ├── Main/
│ │ ├── fig_1_math_figure/
│ │ │ ├── A_figure_math_affine_explanation.ipynb
│ │ │ ├── B_figure_math_consolidate_params.ipynb
│ │ │ ├── C_f1_example_trajectories.ipynb
│ │ │ └── misc/
│ │ │ └── basic_plotting_examples/
│ │ │ ├── f1_example_trajectories.ipynb
│ │ │ └── slope_vs_speed_over_length.ipynb
│ │ ├── fig_2_wind_tunnel_CFD_and_sup_fig_1/
│ │ │ ├── fig2_CFD.ipynb
│ │ │ ├── fig2_wind_tunnel_plots.ipynb
│ │ │ └── smokepen_images/
│ │ ├── fig_3_trajectory_characteristics/
│ │ │ └── f3_raw_trajectories.ipynb
│ │ ├── fig_4_autoencoder/
│ │ │ ├── f4_autoencoder.ipynb
│ │ │ ├── triplet_vae_6_classes_70_feats.pth # Trained model weights
│ │ │ └── misc/
│ │ │ └── optuna_hyperparam_search_multiobj_6class.ipynb
│ │ └── fig_5_unifying_algo_analysis/
│ │ ├── analysis_scripts/ # scripts used to generate the data in Unifying_Algo_Results
│ │ │ ├── analyze_NEW_unifying_sims.py
│ │ │ ├── analyze_cfd_sims.py
│ │ │ ├── analyze_laminar.py
│ │ │ ├── analyze_laminar_wt.py
│ │ │ ├── analyze_still.py
│ │ │ ├── analyze_still_wt.py
│ │ │ ├── analyze_unsteady_exp_bottom_on.py
│ │ │ └── analyze_unsteady_exp_top_on.py
│ │ └── generate_figure_5/ # run A→E in order (they populate one shared SVG)
│ │ ├── A_figure_math_explanation.ipynb
│ │ ├── B_figure_example_trajectories.ipynb
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── E_variable_wind_slope_axis_ratio.ipynb
│ │ ├── misc/ # alternate/older panel notebooks (not part of the final figure)
│ │ └── animal_trajectories/
│ │ ├── albatross/
│ │ ├── moth/
│ │ └── shark/
│ └── Supplemental/
│ ├── sup_fig_all_animal_trajectories/
│ │ ├── animal_trajectories_and_alignment.ipynb
│ │ └── animal_data/ # Per-species figure notebooks
│ │ ├── albatross/
│ │ ├── cockroach/
│ │ ├── crabs/
│ │ ├── d_melanogaster/
│ │ ├── d_sechelia/
│ │ ├── eels/
│ │ ├── mosquitoes/
│ │ ├── moths/
│ │ ├── mouse/
│ │ ├── nautical_search/
│ │ ├── nautilus/
│ │ ├── RL_agents/
│ │ ├── sharks/
│ │ ├── terns/
│ │ └── walking_drosophila/
│ ├── sup_fig_altitude_response/
│ │ └── f3_altitude_responses.ipynb
│ ├── sup_fig_extra_real_trajectories/
│ │ └── S3_sample_real_trajectories.ipynb
│ ├── sup_fig_sorted_trajectories/
│ │ ├── supp_All_Trajectories_sorted.ipynb
│ │ └── supplemental_autoencoder_altitude_colored.ipynb
│ ├── sup_fig_synthetic_trajectories/
│ │ └── S5_sample_simulated_trajectories.ipynb
│ ├── sup_fig_tunnel_methods/
│ │ ├── CFD_plots.ipynb
│ │ └── wind_tunnel_plot_maker.ipynb
│ ├── sup_fig_unifying_analysis/
│ │ └── generate_supplementary_figure/
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── make_figure_align_course_directions.py
│ │ ├── make_figure_unifying_algo_results_slope_axis.py
│ │ └── run_all_figures.sh
│ └── sup_fig_vertical_split/
│ └── S1_verticalsplit.ipynb
│
│
├── TrajectorySimulations/
│ ├── generate_unifying_algo_trajectories/
│ │ ├── generate_unifying_trajectories.ipynb # Step-by-step walkthrough
│ │ ├── generate_unifying_trajectories.py # Script version
│ │ └── plot_trajectories.ipynb
│ └── generating_synthetic_test_trajectory_data/
│ ├── Casting_Circling_CFD_trajectory_generation.ipynb # Step-by-step walkthrough
│ ├── cfd_casting_circling_sims.py # Script version
│ ├── stochastic_trajectories.ipynb # Step-by-step walkthrough for all other algorithms
│ └── param_sweep/ │
│
├── LICENSE
└── README.md

Running the simulations

Notebooks are provided for a step-by-step walkthrough of trajectory generation:

  • Unifying algorithm:TrajectorySimulations/generate_unifying_algo_trajectories/generate_unifying_trajectories.ipynb
  • Casting/circling (CFD-informed):TrajectorySimulations/generating_synthetic_test_trajectory_data/Casting_Circling_CFD_trajectory_generation.ipynb
  • Stochastic trajectories:TrajectorySimulations/generating_synthetic_test_trajectory_data/stochastic_trajectories.ipynb

Note: simulations using 3D CFD velocity fields can take a while to run. The CFD 3D data archives (3D_vel_1.zip3D_vel_6.zip, ~4 GB each) are only required if re-running these simulations from scratch.

Generating the figures

Each figure is generated by a Jupyter notebook in FigureGeneration/. Notebooks read their inputs from Data/ via relative paths (e.g. ../../../Data/...), so they must be run from their own directory — which is Jupyter's default. No config.py or path edits are needed; helper modules import either from the installed splitflow package or from .py files that sit alongside the notebook (e.g. set_zorder_functions.py, unifying_algo_analysis_helper.py).

Several notebooks build a composite figurefirst SVG: they read a layout template (e.g. unifying_analysis.svg) and write the populated figure back. For generate_figure_5/, run A → B → C → D → E in order — each populates a different layer of the same shared SVG.

The pre-existing figure outputs shipped with the repo are kept under an output_figs/ subfolder in each figure directory; a fresh run writes its output alongside with a _test suffix so the committed figures are never overwritten.

Figure 5 / Supplemental unifying analysis read pre-computed unifying-algorithm results from Data/Unifying_Algo_Results/ (Main/ for the main figure, Supplemental/ for the supplemental figures), so the figure notebooks run without any preprocessing. To regenerate those results from the raw trajectory data, run the scripts in FigureGeneration/Main/fig_5_unifying_algo_analysis/analysis_scripts/ (each reads from Data/Experimental_Fly_Data/ and writes to Data/Unifying_Algo_Results/).

Known environment requirements / caveats

  • Gurobi academic license.fig_5/generate_figure_5/A and B fit the unifying-algorithm MIQP live and need a full Gurobi academic license; the free "restricted" license is too small and fails with SolverError. The other Figure 5 notebooks (C, D, E) read pre-computed results and do not need it.
  • LaTeX.fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation set matplotlib text.usetex=True and require a working system LaTeX (MacTeX/TeX Live).
  • pandas < 3.0 is required (see the setup section above).

About

Analysis and modeling code associated with the paper "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Framework".

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Overview

This repository provides all relevant code used in the manuscript titled "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Algorithm", written by authors Jaleesa Houle, Austin Lopez, Kevin Christie, Gaurav Kumar, David Stupski, Aditya Nair, and Floris van Breugel.

The preprint is available on bioRxiv at:

https://www.biorxiv.org/content/10.64898/2026.07.21.739673v1

Downloading the Data

All data necessary to run the analyses and generate the figures can be found on Dryad:

https://doi.org/10.5061/dryad.x0k6djhwc

(Note: data will be made public upon publication.)

Download each zip archive from Dryad and unzip into the Data/ directory. See Data/download_from_dryad_and_unzip_here.txt for the full expected directory structure after unzipping.

Setting up the environment

The figure notebooks were developed and validated on Python 3.11. Follow the steps below to create an environment, install everything, and run the repo.

1. Create and activate an environment

Using conda (recommended, matches how the repo was validated):

conda create -n splitflow python=3.11 -y
conda activate splitflow

Or using a plain virtual environment:

python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate

2. Install the Python dependencies and the splitflow package

pip install -r requirements.txt # all Python dependencies (incl. the private lab repos)
pip install -e .# installs the local splitflow package (the Code/ directory)

requirements.txt pins the dependencies that need care:

  • pandas < 3.0 (required). pandas 3.0 makes Copy-on-Write mandatory, which returns read-only arrays from DataFrame.values; the unifying-algorithm alignment code performs in-place array operations and fails with "output array is read-only" under pandas ≥ 3.0.
  • Private lab repositoriesalign_course_direction_analysis, braid_tools (imported as braid_analysis), FlyPlotLib (fly_plot_lib), and FlyStat (flystat) are installed directly from git (pinned URLs in requirements.txt); you need access to those repositories.

Once installed, notebooks import the shared utilities from the package, e.g.:

importsplitflow.kinematicsaskinematicsfromsplitflow.set_zorder_functionsimport*fromsplitflow.affine_math_and_plot_helperimport*

3. Install Gurobi + an academic license (required for the unifying algorithm)

The unifying-algorithm fit is a mixed-integer quadratic program solved with cvxpy's GUROBI backend. Gurobi is commercial but free for academics; the bundled "restricted" license is too small for these problems, so a full (named-user academic) license is required.

  1. gurobipy is already installed by requirements.txt (or pip install gurobipy).
  2. Create a free account and request a Named-User Academic License at https://www.gurobi.com/academia/academic-program-and-licenses/ .
  3. From the Gurobi "License Center", copy the grbgetkey command for your license and run it while on your university network/VPN:
    grbgetkey <your-license-key>
    This writes a gurobi.lic file (default ~/gurobi.lic).
  4. Verify it works:
    python -c "import gurobipy; gurobipy.Model(); print('Gurobi OK')"
    If your license is not in the default location, point Gurobi at it with export GRB_LICENSE_FILE=/path/to/gurobi.lic.

4. Install a LaTeX distribution (for a few notebooks)

A working system LaTeX is required for the notebooks that set matplotlib text.usetex=True (fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation). Install MacTeX (macOS) or TeX Live (Linux/Windows) system-wide and ensure latex is on your PATH.

5. Get the data and run

Download the data from Dryad into Data/ (see the "Downloading the Data" section above), then launch Jupyter from the repo root with the environment active:

jupyter lab # or: jupyter notebook

Open any notebook under FigureGeneration/ and run it top to bottom. Notebooks read their inputs from Data/ via relative paths and must be run from their own directory (Jupyter's default), so no path edits are needed.

Directory structure

├── Code/
│ ├── FlyDataProcessingScript.py # Raw fly data preprocessing pipeline
│ ├── PlotUtilities.py # Shared plotting helpers
│ ├── WindDataProcessing.py # Wind tunnel CFD data processing
│ ├── StochasticAlgorithms.py # Algorithm implementations for Brownian motion, Levy flight, etc.
│ ├── opto_tracking.py # Algorithm implementations for Casting/Circling in CFD
│ ├── kinematics.py # Trajectory kinematics calculations
│ ├── TripletLoss.py # Autoencoder training functions
│ ├── vae_optuna.py # VAE param search code
│ ├── RFClassifier.py # Random forest classifier
│ ├── affine_math_and_plot_helper.py # Affine transform math and plotting for unifying algorithm
│ ├── staircase_approximation.py # Staircase function approximation for unifying algorithm
│ ├── BayesFactorFunctions.py # Bayes factor statistical functions -- used in older version of paper
│ └── FeatureFunctions.py # Trajectory feature extraction -- used in older version of paper
│
│
├── Data/
│ ├── download_from_dryad_and_unzip_here.txt # Download instructions + expected structure
│ │ [download from Dryad and unzip into the directories as shown below]
│ ├── wind_tunnel_data/ # Wind speed measurements (laminar, outdoor, split flow)
│ ├── CFD/
│ │ ├── 2D_PlanarSlices/ # 2D velocity field slices (XY steady/unsteady, XZ)
│ │ ├── 2D_PlumeData/ # 2D odor concentration slices -- used in older version of paper
│ │ └── 3D_Velocity_Fields/ # 3D velocity fields, 20s simulation (6 archives)
│ ├── Experimental_Fly_Data/ # full/raw fly datasets, flat (merged raw_data + preprocessed_hdfs):
│ │ # per-condition trimmed HDFs, merged all-nights HDFs,
│ │ # training laminar/stillair sets, variable-wind & CFD parquets
│ ├── Simulated_Trajectory_Data/ # Casting, circling, and unifying algorithm trajectories
│ ├── Animal_Trajectory_Data/ # Cross-species trajectories (15 species/agent types)
│ ├── Unifying_Algo_Results/
│ │ ├── Main/ # Analysis outputs for main figures (42 parquet files)
│ │ └── Supplemental/ # Supplemental-only outputs (30 parquet files + var_wind_analyzed/):
│ │ # variable-wind, WT-splitflow variants, augmented-sham, df_all_species
│ │ # (results shared with the main figure are read from Main/, not duplicated here)
│ ├── Fig1_Input_Data/ # Data specifically used for Figure 1
│ └── Fig5_Input_Data/ # Data specifically used for Figure 5
│
│
├── FigureGeneration/
│ ├── Main/
│ │ ├── fig_1_math_figure/
│ │ │ ├── A_figure_math_affine_explanation.ipynb
│ │ │ ├── B_figure_math_consolidate_params.ipynb
│ │ │ ├── C_f1_example_trajectories.ipynb
│ │ │ └── misc/
│ │ │ └── basic_plotting_examples/
│ │ │ ├── f1_example_trajectories.ipynb
│ │ │ └── slope_vs_speed_over_length.ipynb
│ │ ├── fig_2_wind_tunnel_CFD_and_sup_fig_1/
│ │ │ ├── fig2_CFD.ipynb
│ │ │ ├── fig2_wind_tunnel_plots.ipynb
│ │ │ └── smokepen_images/
│ │ ├── fig_3_trajectory_characteristics/
│ │ │ └── f3_raw_trajectories.ipynb
│ │ ├── fig_4_autoencoder/
│ │ │ ├── f4_autoencoder.ipynb
│ │ │ ├── triplet_vae_6_classes_70_feats.pth # Trained model weights
│ │ │ └── misc/
│ │ │ └── optuna_hyperparam_search_multiobj_6class.ipynb
│ │ └── fig_5_unifying_algo_analysis/
│ │ ├── analysis_scripts/ # scripts used to generate the data in Unifying_Algo_Results
│ │ │ ├── analyze_NEW_unifying_sims.py
│ │ │ ├── analyze_cfd_sims.py
│ │ │ ├── analyze_laminar.py
│ │ │ ├── analyze_laminar_wt.py
│ │ │ ├── analyze_still.py
│ │ │ ├── analyze_still_wt.py
│ │ │ ├── analyze_unsteady_exp_bottom_on.py
│ │ │ └── analyze_unsteady_exp_top_on.py
│ │ └── generate_figure_5/ # run A→E in order (they populate one shared SVG)
│ │ ├── A_figure_math_explanation.ipynb
│ │ ├── B_figure_example_trajectories.ipynb
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── E_variable_wind_slope_axis_ratio.ipynb
│ │ ├── misc/ # alternate/older panel notebooks (not part of the final figure)
│ │ └── animal_trajectories/
│ │ ├── albatross/
│ │ ├── moth/
│ │ └── shark/
│ └── Supplemental/
│ ├── sup_fig_all_animal_trajectories/
│ │ ├── animal_trajectories_and_alignment.ipynb
│ │ └── animal_data/ # Per-species figure notebooks
│ │ ├── albatross/
│ │ ├── cockroach/
│ │ ├── crabs/
│ │ ├── d_melanogaster/
│ │ ├── d_sechelia/
│ │ ├── eels/
│ │ ├── mosquitoes/
│ │ ├── moths/
│ │ ├── mouse/
│ │ ├── nautical_search/
│ │ ├── nautilus/
│ │ ├── RL_agents/
│ │ ├── sharks/
│ │ ├── terns/
│ │ └── walking_drosophila/
│ ├── sup_fig_altitude_response/
│ │ └── f3_altitude_responses.ipynb
│ ├── sup_fig_extra_real_trajectories/
│ │ └── S3_sample_real_trajectories.ipynb
│ ├── sup_fig_sorted_trajectories/
│ │ ├── supp_All_Trajectories_sorted.ipynb
│ │ └── supplemental_autoencoder_altitude_colored.ipynb
│ ├── sup_fig_synthetic_trajectories/
│ │ └── S5_sample_simulated_trajectories.ipynb
│ ├── sup_fig_tunnel_methods/
│ │ ├── CFD_plots.ipynb
│ │ └── wind_tunnel_plot_maker.ipynb
│ ├── sup_fig_unifying_analysis/
│ │ └── generate_supplementary_figure/
│ │ ├── C_figure_align_course_directions.ipynb
│ │ ├── D_figure_unifying_algo_results_slope_axis.ipynb
│ │ ├── make_figure_align_course_directions.py
│ │ ├── make_figure_unifying_algo_results_slope_axis.py
│ │ └── run_all_figures.sh
│ └── sup_fig_vertical_split/
│ └── S1_verticalsplit.ipynb
│
│
├── TrajectorySimulations/
│ ├── generate_unifying_algo_trajectories/
│ │ ├── generate_unifying_trajectories.ipynb # Step-by-step walkthrough
│ │ ├── generate_unifying_trajectories.py # Script version
│ │ └── plot_trajectories.ipynb
│ └── generating_synthetic_test_trajectory_data/
│ ├── Casting_Circling_CFD_trajectory_generation.ipynb # Step-by-step walkthrough
│ ├── cfd_casting_circling_sims.py # Script version
│ ├── stochastic_trajectories.ipynb # Step-by-step walkthrough for all other algorithms
│ └── param_sweep/ │
│
├── LICENSE
└── README.md

Running the simulations

Notebooks are provided for a step-by-step walkthrough of trajectory generation:

  • Unifying algorithm:TrajectorySimulations/generate_unifying_algo_trajectories/generate_unifying_trajectories.ipynb
  • Casting/circling (CFD-informed):TrajectorySimulations/generating_synthetic_test_trajectory_data/Casting_Circling_CFD_trajectory_generation.ipynb
  • Stochastic trajectories:TrajectorySimulations/generating_synthetic_test_trajectory_data/stochastic_trajectories.ipynb

Note: simulations using 3D CFD velocity fields can take a while to run. The CFD 3D data archives (3D_vel_1.zip3D_vel_6.zip, ~4 GB each) are only required if re-running these simulations from scratch.

Generating the figures

Each figure is generated by a Jupyter notebook in FigureGeneration/. Notebooks read their inputs from Data/ via relative paths (e.g. ../../../Data/...), so they must be run from their own directory — which is Jupyter's default. No config.py or path edits are needed; helper modules import either from the installed splitflow package or from .py files that sit alongside the notebook (e.g. set_zorder_functions.py, unifying_algo_analysis_helper.py).

Several notebooks build a composite figurefirst SVG: they read a layout template (e.g. unifying_analysis.svg) and write the populated figure back. For generate_figure_5/, run A → B → C → D → E in order — each populates a different layer of the same shared SVG.

The pre-existing figure outputs shipped with the repo are kept under an output_figs/ subfolder in each figure directory; a fresh run writes its output alongside with a _test suffix so the committed figures are never overwritten.

Figure 5 / Supplemental unifying analysis read pre-computed unifying-algorithm results from Data/Unifying_Algo_Results/ (Main/ for the main figure, Supplemental/ for the supplemental figures), so the figure notebooks run without any preprocessing. To regenerate those results from the raw trajectory data, run the scripts in FigureGeneration/Main/fig_5_unifying_algo_analysis/analysis_scripts/ (each reads from Data/Experimental_Fly_Data/ and writes to Data/Unifying_Algo_Results/).

Known environment requirements / caveats

  • Gurobi academic license.fig_5/generate_figure_5/A and B fit the unifying-algorithm MIQP live and need a full Gurobi academic license; the free "restricted" license is too small and fails with SolverError. The other Figure 5 notebooks (C, D, E) read pre-computed results and do not need it.
  • LaTeX.fig_1/B_figure_math_consolidate_params and Supplemental/unifying_simulation/plot_simulation set matplotlib text.usetex=True and require a working system LaTeX (MacTeX/TeX Live).
  • pandas < 3.0 is required (see the setup section above).

About

Analysis and modeling code associated with the paper "Olfactory Search Behavior Across Flow Regimes Supports a Unifying Framework".

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages