Skip to content

Commit d56d847

Browse files
committed
Resolve merge conflict in test_genetics.py - keep uncommented test
2 parents 1e278dc + ffd6889 commit d56d847

21 files changed

Lines changed: 216 additions & 42 deletions

.github/ISSUE_TEMPLATE/feature_request.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: Feature request
3-
about: Suggest an idea for BrainSpace
3+
about: Suggest an idea for BrainStat
44
title: ''
55
labels: ''
66
assignees: ''

.github/ISSUE_TEMPLATE/support-request.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: Support request
3-
about: Ask for help running BrainSpace
3+
about: Ask for help running BrainStat
44
title: ''
55
labels: ''
66
assignees: ''
@@ -9,7 +9,7 @@ assignees: ''
99

1010
* **What are you trying to do?**
1111

12-
* **If you've tried running this with BrainSpace code already, how did you do so and what went wrong?**
12+
* **If you've tried running this with BrainStat code already, how did you do so and what went wrong?**
1313

1414
* **Please tell us about your computing environment:**
1515

.github/workflows/development_release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v2
1515

16-
- name: Set up Python 3.8.
16+
- name: Set up Python 3.10.
1717
uses: actions/setup-python@v2
1818
with:
19-
python-version: 3.8
19+
python-version: "3.10"
2020

2121
- name: Install Python BrainStat.
2222
run: |

.github/workflows/python_unittests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
python-version: [3.9, '3.10', '3.11', '3.12']
14+
python-version: ['3.10', '3.11', '3.12']
1515
os: [ubuntu-latest, windows-latest, macos-latest]
1616

1717
runs-on: ${{ matrix.os }}

.github/workflows/tagged_release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v2
1515

16-
- name: Set up Python 3.8.
16+
- name: Set up Python 3.10.
1717
uses: actions/setup-python@v2
1818
with:
19-
python-version: 3.8
19+
python-version: "3.10"
2020

2121
- name: Install MATLAB
2222
uses: matlab-actions/setup-matlab@v0

brainstat/_utils.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def deprecated_func(*args, **kwargs):
150150
def _download_file(
151151
url: str, output_file: Path, overwrite: bool = False, verbose=True
152152
) -> None:
153-
"""Downloads a file.
153+
"""Downloads a file with retry logic for network failures.
154154
155155
Parameters
156156
----------
@@ -163,14 +163,39 @@ def _download_file(
163163
verbose : bool
164164
If true, print a download message, defaults to True.
165165
"""
166+
import time
167+
from http.client import RemoteDisconnected
168+
from urllib.error import URLError
166169

167170
if output_file.exists() and not overwrite:
168171
return
169172

170173
if verbose:
171174
logger.info("Downloading " + str(output_file) + " from " + url + ".")
172-
with urllib.request.urlopen(url) as response, open(output_file, "wb") as out_file:
173-
shutil.copyfileobj(response, out_file)
175+
176+
# Retry logic for intermittent network failures
177+
max_retries = 3
178+
retry_delay = 2 # seconds
179+
180+
for attempt in range(max_retries):
181+
try:
182+
with urllib.request.urlopen(url, timeout=30) as response, open(output_file, "wb") as out_file:
183+
shutil.copyfileobj(response, out_file)
184+
return # Success, exit function
185+
except (RemoteDisconnected, URLError, TimeoutError) as e:
186+
if attempt < max_retries - 1:
187+
logger.warning(
188+
f"Download attempt {attempt + 1}/{max_retries} failed: {e}. "
189+
f"Retrying in {retry_delay} seconds..."
190+
)
191+
time.sleep(retry_delay)
192+
retry_delay *= 2 # Exponential backoff
193+
else:
194+
logger.error(f"Download failed after {max_retries} attempts.")
195+
raise # Re-raise the exception after all retries fail
196+
197+
198+
174199

175200

176201
if __name__ == "__main__":

brainstat/context/genetics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def surface_genetic_expression(
127127
with tempfile.NamedTemporaryFile(suffix=".gii", delete=False) as f:
128128
name = f.name
129129
write_surface(surface, name, otype="gii")
130-
surfaces_gii.append(nib.load(name))
130+
surfaces_gii.append(nib.load(name))
131131
finally:
132132
Path(name).unlink()
133133
else:

brainstat/context/histology.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ def read_histology_profile(
157157

158158
with h5py.File(histology_file, "r") as h5_file:
159159
if template == "fslr32k":
160+
# Known issue #369: The fslr32k data file on the server actually contains
161+
# fs_LR_64k resolution data (64984 vertices) instead of fs_LR_32k (32492 vertices).
162+
# This is a data hosting issue. Users expecting 32k resolution data should be aware
163+
# that they will receive 64k resolution data until this is fixed on the server.
164+
logger.warning(
165+
"Known issue: The fslr32k histology profile data currently contains "
166+
"fs_LR_64k resolution (64984 vertices) instead of fs_LR_32k (32492 vertices). "
167+
"See https://github.com/MICA-MNI/BrainStat/issues/369 for details."
168+
)
160169
profiles = h5_file.get("fs_LR_64k")[...]
161170
else:
162171
profiles = h5_file.get(template)[...]
@@ -196,6 +205,12 @@ def download_histology_profiles(
196205
data_dir.mkdir(parents=True, exist_ok=True)
197206
if template == "fslr32k":
198207
output_file = data_dir / "histology_fslr32k.h5"
208+
# Known issue #369: warn users about data resolution mismatch
209+
logger.warning(
210+
"Note: The fslr32k histology profile data currently contains "
211+
"fs_LR_64k resolution (64984 vertices) instead of fs_LR_32k. "
212+
"See https://github.com/MICA-MNI/BrainStat/issues/369 for details."
213+
)
199214
else:
200215
output_file = data_dir / ("histology_" + template + ".h5")
201216

brainstat/datasets/base.py

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
""" Load external datasets. """
2+
import json
23
import tempfile
34
import zipfile
5+
from collections import namedtuple
46
from pathlib import Path
57
from typing import List, Optional, Tuple, Union
68

@@ -11,9 +13,19 @@
1113
from brainspace.mesh.mesh_operations import combine_surfaces
1214
from brainspace.vtk_interface.wrappers.data_object import BSPolyData
1315
from netneurotools import datasets as nnt_datasets
14-
from netneurotools.civet import read_civet
1516
from nibabel import load as nib_load
1617
from nibabel.freesurfer.io import read_annot, read_geometry
18+
from sklearn.utils import Bunch
19+
20+
try:
21+
from netneurotools.datasets.utils import _get_data_dir, _get_dataset_info
22+
except ImportError:
23+
from netneurotools.datasets.datasets_utils import _get_data_dir, _get_dataset_info
24+
25+
try:
26+
from nilearn.datasets._utils import fetch_files as _fetch_files
27+
except ImportError:
28+
from nilearn.datasets.utils import fetch_files as _fetch_files
1729

1830
from brainstat._utils import (
1931
_download_file,
@@ -24,6 +36,65 @@
2436
from brainstat.mesh.interpolate import _surf2surf
2537

2638

39+
def read_civet(fname: Union[str, Path]) -> Tuple[np.ndarray, np.ndarray]:
40+
"""
41+
Reads a CIVET surface file (MNI OBJ format).
42+
43+
Parameters
44+
----------
45+
fname : str or Path
46+
Path to the CIVET surface file.
47+
48+
Returns
49+
-------
50+
vertices : numpy.ndarray
51+
(N, 3) array of vertex coordinates.
52+
faces : numpy.ndarray
53+
(M, 3) array of face indices.
54+
"""
55+
fname = str(fname)
56+
with open(fname, "rb") as f:
57+
first_char = f.read(1)
58+
59+
if first_char == b"P":
60+
with open(fname, "r") as f:
61+
content = f.read().split()
62+
63+
n_vertices = int(content[6])
64+
65+
idx = 7
66+
# Vertices
67+
vertices = np.array(content[idx : idx + n_vertices * 3], dtype=float).reshape(
68+
-1, 3
69+
)
70+
idx += n_vertices * 3
71+
72+
# Normals
73+
idx += n_vertices * 3
74+
75+
n_triangles = int(content[idx])
76+
idx += 1
77+
78+
color_flag = int(content[idx])
79+
idx += 1
80+
81+
if color_flag == 0:
82+
idx += 4
83+
else:
84+
idx += n_vertices * 4
85+
86+
# Check remaining items to decide if we need to skip
87+
remaining = len(content) - idx
88+
if remaining == n_triangles * 3 + n_triangles:
89+
idx += n_triangles
90+
91+
triangles = np.array(content[idx:], dtype=int).reshape(-1, 3)
92+
93+
return vertices, triangles
94+
else:
95+
raise NotImplementedError("Binary CIVET files are not supported yet.")
96+
97+
2798
def fetch_parcellation(
2899
template: str,
29100
atlas: str,
@@ -147,7 +218,7 @@ def fetch_template_surface(
147218
surfaces_fs = [read_geometry(file) for file in surface_files]
148219
surfaces = [build_polydata(surface[0], surface[1]) for surface in surfaces_fs]
149220
elif template == "fslr32k":
150-
surfaces = [read_surface(file) for file in surface_files]
221+
surfaces = [read_surface(str(file)) for file in surface_files]
151222
else:
152223
surfaces_obj = [read_civet(file) for file in surface_files]
153224
surfaces = [build_polydata(surface[0], surface[1]) for surface in surfaces_obj]
@@ -384,7 +455,7 @@ def _fetch_template_surface_files(
384455

385456
if template == "fslr32k":
386457
layer = layer if layer else "midthickness"
387-
bunch = nnt_datasets.fetch_conte69(data_dir=str(data_dir))
458+
bunch = _fetch_conte69_fixed(data_dir=str(data_dir))
388459
elif template == "civet41k" or template == "civet164k":
389460
layer = layer if layer else "mid"
390461
if layer == "sphere":
@@ -515,3 +586,43 @@ def _fetch_civet_spheres(template: str, data_dir: Path) -> Tuple[str, str]:
515586

516587
# Return two filenames to conform to other left/right hemisphere functions.
517588
return (str(filename), str(filename))
589+
590+
591+
SURFACE = namedtuple('Surface', ('lh', 'rh'))
592+
593+
594+
def _fetch_conte69_fixed(data_dir=None, url=None, resume=True, verbose=1):
595+
"""
596+
Download files for Van Essen et al., 2012 Conte69 template.
597+
Fixed version to handle encoding on Windows.
598+
"""
599+
dataset_name = 'tpl-conte69'
600+
keys = ['midthickness', 'inflated', 'vinflated']
601+
602+
data_dir = _get_data_dir(data_dir=data_dir)
603+
info = _get_dataset_info(dataset_name)
604+
if url is None:
605+
url = info['url']
606+
607+
opts = {
608+
'uncompress': True,
609+
'md5sum': info['md5'],
610+
'move': '{}.tar.gz'.format(dataset_name)
611+
}
612+
613+
filenames = [
614+
'tpl-conte69/tpl-conte69_space-MNI305_variant-fsLR32k_{}.{}.surf.gii'
615+
.format(res, hemi) for res in keys for hemi in ['L', 'R']
616+
] + ['tpl-conte69/template_description.json']
617+
618+
data = _fetch_files(data_dir, files=[(f, url, opts) for f in filenames],
619+
resume=resume, verbose=verbose)
620+
621+
# Fix: Specify encoding='utf-8'
622+
with open(data[-1], 'r', encoding='utf-8') as src:
623+
data[-1] = json.load(src)
624+
625+
# bundle hemispheres together
626+
data = [SURFACE(*data[:-1][i:i + 2]) for i in range(0, 6, 2)] + [data[-1]]
627+
628+
return Bunch(**dict(zip(keys + ['info'], data)))

brainstat/stats/_linear_model.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,9 @@ def _compute_resls(self, Y: np.ndarray) -> Tuple[np.ndarray, dict]:
442442
for j in range(Y.shape[2]):
443443
normr = np.sqrt(self.SSE[((j + 1) * (j + 2) // 2) - 1])
444444
for i in range(Y.shape[0]):
445-
u = Y[i, :, j] / normr
445+
u = np.divide(
446+
Y[i, :, j], normr, out=np.zeros_like(Y[i, :, j]), where=normr != 0
447+
)
446448
resl[:, j] += np.diff(u[edges], axis=1).ravel() ** 2
447449

448450
return resl, mesh_connections

0 commit comments

Comments
 (0)