Skip to content

Commit 6ea427e

Browse files
authored
Merge pull request #1 from int-brain-lab/develop
Improved the packaging and logging.
2 parents c65db0e + 30e9378 commit 6ea427e

18 files changed

+624
-302
lines changed

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Build and Test
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.10"]
15+
16+
steps:
17+
- uses: actions/checkout@v3
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v4
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install package in development mode
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e .
28+
29+
- name: Test imports
30+
run: |
31+
python -c "import ephysatlas"

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ ENV/
130130
env.bak/
131131
venv.bak/
132132
.vscode/
133+
load_venv.sh
133134

134135
# Spyder project settings
135136
.spyderproject
@@ -170,4 +171,10 @@ cython_debug/
170171
# version on PyPI doesn't break anything, then do not submit the pdm.lock file.
171172
pdm.lock
172173

173-
Todos.md
174+
# Configuration files
175+
config.yaml
176+
177+
# Scratchpad
178+
Todos.md
179+
Issues.md
180+
testing.ipynb

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,14 @@ python main.py --config config.yaml
2929

3030
### Configuration File
3131

32-
The `config.yaml` file contains all the necessary parameters for running the analysis. Here's an example configuration:
32+
The configuration is managed through a YAML file. To avoid committing local changes, the actual configuration file (`config.yaml`) is ignored by git. Instead, a template file (`config_template.yaml`) is provided. To use the tool:
3333

34+
1. Copy the template file to create your local configuration:
35+
```bash
36+
cp config_template.yaml config.yaml
37+
```
38+
39+
2. Edit `config.yaml` with your specific settings:
3440
```yaml
3541
# Required parameters
3642
pid: "5246af08-0730-40f7-83de-29b5d62b9b6d" # Probe ID

config_template.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# config file
2+
pid: "YOUR_PROBE_ID" # Probe ID
3+
t_start: 300.0 # Start time in seconds
4+
duration: 1 # Duration in seconds
5+
6+
# Logging configuration
7+
log_path: "/path/to/logs/ibleatools.log" # Absolute path for log file. If not provided, no file logging will be done.
8+
9+
# Big .cbin files
10+
# ap_file: "/path/to/your/ap/file.cbin"
11+
# lf_file: "/path/to/your/lf/file.cbin"
12+
13+
14+
# Trajectory information for xyz target computation
15+
# traj_dict:
16+
# x: -2243.1 # x coordinate
17+
# y: -1999.8 # y coordinate
18+
# z: -361.0 # z coordinate
19+
# depth: 4000.0 # insertion depth
20+
# theta: 15.0 # insertion angle theta
21+
# phi: 180.0 # insertion angle phi
22+
23+
# Operation mode: 'features' for feature computation only,
24+
# 'inference' for region inference only, or 'both' for both operations
25+
mode: "both"
26+
27+
# Path to save/load features file. If not provided,
28+
# features will be saved as 'features_{pid}.parquet' in the current directory
29+
features_path: "/path/to/your/features.parquet"
30+
31+
# Path to the model directory for region inference
32+
model_path: "/path/to/your/model/directory/"
33+
34+
# Usage: python main.py --config config.yaml

examples/compute_features_raw_NP1.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# %%
2+
import spikeglx
3+
import ephysatlas.feature_computation
4+
5+
6+
file_ap = "/mnt/s1/spikesorting/raw_data/mrsicflogellab/Subjects/SWC_038/2020-07-30/001/raw_ephys_data/probe01/_spikeglx_ephysData_g0_t0.imec1.ap.cbin"
7+
file_lf = "/mnt/s1/spikesorting/raw_data/mrsicflogellab/Subjects/SWC_038/2020-07-30/001/raw_ephys_data/probe01/_spikeglx_ephysData_g0_t0.imec1.lf.cbin"
8+
9+
t0 = 485
10+
11+
sr_ap = spikeglx.Reader(file_ap)
12+
sr_lf = spikeglx.Reader(file_lf)
13+
duration = 3
14+
15+
df_features = ephysatlas.feature_computation.online_feature_computation(
16+
sr_lf, sr_ap, t0, duration, channel_labels=None
17+
)

main.py

Lines changed: 90 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,160 @@
1-
import os
21
import argparse
2+
import random
3+
import string
34
from typing import List, Optional, Dict, Any
4-
from src.feature_computation import compute_features
5-
from src.region_inference import infer_regions
6-
from one.api import ONE
5+
from pathlib import Path
6+
77
import numpy as np
88
import yaml
9-
from pathlib import Path
109
import pandas as pd
11-
from src.logger_config import setup_logger
12-
from src.plots import plot_results
13-
from src import decoding
14-
import random
15-
import string
1610

17-
# Set up logger
18-
logger = setup_logger(__name__)
11+
from iblutil.util import setup_logger
12+
from one.api import ONE
13+
14+
from ephysatlas.feature_computation import compute_features
15+
from ephysatlas.region_inference import infer_regions
16+
from ephysatlas.plots import plot_results
17+
from ephysatlas import decoding
18+
1919

2020
def load_config(config_path: str) -> Dict[str, Any]:
2121
"""Load configuration from YAML file."""
22-
logger.info(f"Loading configuration from {config_path}")
23-
with open(config_path, 'r') as f:
22+
with open(config_path, "r") as f:
2423
return yaml.safe_load(f)
2524

2625

2726
def parse_arguments(args: List[str]) -> argparse.Namespace:
2827
"""Parse command line arguments."""
29-
logger.debug("Parsing command line arguments")
30-
parser = argparse.ArgumentParser(description="Electrophysiology feature computation and region inference")
31-
parser.add_argument("--config", required=True, help="Path to YAML configuration file")
28+
parser = argparse.ArgumentParser(
29+
description="Electrophysiology feature computation and region inference"
30+
)
31+
parser.add_argument(
32+
"--config", required=True, help="Path to YAML configuration file"
33+
)
3234
return parser.parse_args(args)
3335

3436

3537
def get_parameters(args: argparse.Namespace) -> Dict[str, Any]:
3638
"""Get parameters from config file."""
37-
logger.info("Loading configuration from YAML file")
3839
config = load_config(args.config)
39-
40+
4041
# Validate required parameters
41-
if 'pid' in config:
42+
if "pid" in config:
4243
# PID-based configuration
43-
required_params = ['pid', 't_start', 'duration']
44+
required_params = ["pid", "t_start", "duration"]
4445
missing_params = [param for param in required_params if param not in config]
4546
if missing_params:
46-
raise ValueError(f"Missing required parameters in config file: {', '.join(missing_params)}")
47+
raise ValueError(
48+
f"Missing required parameters in config file: {', '.join(missing_params)}"
49+
)
4750
else:
4851
# File-based configuration
49-
required_params = ['ap_file', 'lf_file']
52+
required_params = ["ap_file", "lf_file"]
5053
missing_params = [param for param in required_params if param not in config]
5154
if missing_params:
52-
raise ValueError(f"Missing required parameters in config file: {', '.join(missing_params)}")
53-
55+
raise ValueError(
56+
f"Missing required parameters in config file: {', '.join(missing_params)}"
57+
)
58+
5459
return {
55-
'pid': config.get('pid'),
56-
'ap_file': config.get('ap_file'),
57-
'lf_file': config.get('lf_file'),
58-
't_start': config.get('t_start', 0.0), # Default to 0.0 if not specified
59-
'duration': config.get('duration'), # Default to None if not specified
60-
'mode': config.get('mode', 'both'),
61-
'features_path': config.get('features_path'),
62-
'model_path': config.get('model_path'),
63-
'traj_dict': config.get('traj_dict')
60+
"pid": config.get("pid"),
61+
"ap_file": config.get("ap_file"),
62+
"lf_file": config.get("lf_file"),
63+
"t_start": config.get("t_start", 0.0), # Default to 0.0 if not specified
64+
"duration": config.get("duration"), # Default to None if not specified
65+
"mode": config.get("mode", "both"),
66+
"features_path": config.get("features_path"),
67+
"model_path": config.get("model_path"),
68+
"traj_dict": config.get("traj_dict"),
69+
"log_path": config.get("log_path"), # Get log path from config
6470
}
6571

6672

6773
def main(args: Optional[List[str]] = None) -> int:
6874
"""Main function that can be called with arguments or use command line arguments."""
69-
logger.info("Starting main function")
7075
if args is None:
7176
import sys
77+
7278
args = sys.argv[1:]
73-
79+
7480
# Parse arguments
7581
parsed_args = parse_arguments(args)
76-
82+
7783
# Get parameters from config file
7884
params = get_parameters(parsed_args)
79-
logger.info(f"Processing probe ID: {params['pid']}")
80-
85+
86+
# Set up logger with config path
87+
logger = setup_logger(__name__, file=params.get("log_path"))
88+
logger.info("Starting main function")
89+
8190
# Initialize ONE if using PID
8291
one = ONE()
83-
if params['pid'] is not None:
92+
if params["pid"] is not None:
8493
logger.info("ONE client initialized")
8594
logger.info(f"Processing probe ID: {params['pid']}")
8695
else:
8796
logger.info(f"Processing files: AP={params['ap_file']}, LF={params['lf_file']}")
88-
97+
8998
df_features = None
9099
# Determine features file path
91-
features_path = params.get('features_path')
100+
features_path = params.get("features_path")
92101
if features_path is None:
93-
if params['pid'] is not None:
102+
if params["pid"] is not None:
94103
features_path = Path(f"features_{params['pid']}.parquet")
95104
else:
96105
# Generate 8 character alphanumeric filename
97-
filename = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
106+
filename = "".join(
107+
random.choices(string.ascii_letters + string.digits, k=8)
108+
)
98109
features_path = Path(f"features_{filename}.parquet")
99110
logger.info(f"Generated features filename: {features_path}")
100111
else:
101112
features_path = Path(features_path)
102113
# Ensure the file has .parquet extension
103-
if features_path.suffix != '.parquet':
104-
features_path = features_path.with_suffix('.parquet')
105-
114+
if features_path.suffix != ".parquet":
115+
features_path = features_path.with_suffix(".parquet")
116+
106117
# Compute features if mode is 'features' or 'both'
107-
if params['mode'] in ['features', 'both']:
118+
if params["mode"] in ["features", "both"]:
108119
logger.info("Starting feature computation")
109120
df_features = compute_features(
110-
pid=params.get('pid'),
111-
t_start=params['t_start'],
112-
duration=params['duration'],
121+
pid=params.get("pid"),
122+
t_start=params["t_start"],
123+
duration=params["duration"],
113124
one=one,
114-
ap_file=params.get('ap_file'),
115-
lf_file=params.get('lf_file'),
116-
traj_dict=params.get('traj_dict')
125+
ap_file=params.get("ap_file"),
126+
lf_file=params.get("lf_file"),
127+
traj_dict=params.get("traj_dict"),
117128
)
118129
logger.info(f"Feature computation completed. Shape: {df_features.shape}")
119-
130+
120131
# Save features to parquet file
121132
logger.info(f"Saving features to {features_path}")
122133
df_features.to_parquet(features_path, index=True)
123-
134+
124135
# Infer regions if mode is 'inference' or 'both'
125-
if params['mode'] in ['inference', 'both']:
136+
if params["mode"] in ["inference", "both"]:
126137
logger.info("Starting region inference")
127138
# Get model path from parameters or use default
128-
model_path = params.get('model_path')
139+
model_path = params.get("model_path")
129140
if model_path is None:
130-
model_path = Path("/Users/pranavrai/Downloads/models/2024_W50_Cosmos_voter-snap-pudding/")
141+
model_path = Path(
142+
"/Users/pranavrai/Downloads/models/2024_W50_Cosmos_voter-snap-pudding/"
143+
)
131144
else:
132145
model_path = Path(model_path)
133-
146+
134147
# If df_features is None, load from file
135148
if df_features is None:
136149
# This should only happen in inference mode
137-
assert params['mode'] == 'inference'
150+
assert params["mode"] == "inference"
138151
if not features_path.exists():
139-
raise ValueError(f"Features file not found at {features_path}. Please compute features first.")
152+
raise ValueError(
153+
f"Features file not found at {features_path}. Please compute features first."
154+
)
140155
logger.info(f"Loading features from {features_path}")
141156
df_features = pd.read_parquet(features_path)
142-
143-
157+
144158
predicted_probas, predicted_regions = infer_regions(df_features, model_path)
145159
logger.info(f"Predicted regions shape: {predicted_regions.shape}")
146160
logger.info(f"Prediction probabilities shape: {predicted_probas.shape}")
@@ -149,24 +163,28 @@ def main(args: Optional[List[str]] = None) -> int:
149163
output_dir = features_path.parent
150164
np_probas_path = output_dir / f"probas_{params['pid']}.npy"
151165
np_regions_path = output_dir / f"regions_{params['pid']}.npy"
152-
153-
logger.info(f"Saving prediction probabilities as numpy array to {np_probas_path}")
166+
167+
logger.info(
168+
f"Saving prediction probabilities as numpy array to {np_probas_path}"
169+
)
154170
np.save(np_probas_path, predicted_probas)
155-
171+
156172
logger.info(f"Saving predicted regions as numpy array to {np_regions_path}")
157173
np.save(np_regions_path, predicted_regions)
158174

159-
#Plot the results
160-
#Todo need to have better interface than calling dict_model here just for plotting.
161-
dict_model = decoding.load_model(model_path.joinpath(f'FOLD04'))
175+
# Plot the results
176+
# Todo need to have better interface than calling dict_model here just for plotting.
177+
dict_model = decoding.load_model(model_path.joinpath("FOLD04"))
162178
fig, ax = plot_results(df_features, predicted_probas, dict_model)
163179
import matplotlib.pyplot as plt
180+
164181
plt.savefig(output_dir / f"results_{params['pid']}.png")
165-
182+
166183
return 0
167184

168185

169186
if __name__ == "__main__":
170187
exit_code = main()
171188
import sys
172-
sys.exit(exit_code)
189+
190+
sys.exit(exit_code)

0 commit comments

Comments
 (0)