Skip to content

Commit df8e224

Browse files
authored
✨ add static analysis and test workflow, add dwi-to-conductivity workflow (#2)
* add static analysis and test workflow * add badge * add anisotropic conductivity workflow * add pyright config * restructure package, add license header Co-authored-by: Bryn Lloyd <[email protected]>
1 parent 6840006 commit df8e224

20 files changed

+426
-7
lines changed

.github/workflows/deploy_and_test.yml

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: CI
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [ main ]
7+
paths-ignore:
8+
- "*.md"
9+
- "*.png"
10+
- "*.gif"
11+
- "*.svg"
12+
- "docs/**"
13+
- ".vscode/**"
14+
pull_request:
15+
branches: [ main ]
16+
paths-ignore:
17+
- "*.md"
18+
- "*.png"
19+
- "*.gif"
20+
- "*.svg"
21+
- "docs/**"
22+
- ".vscode/**"
23+
24+
jobs:
25+
build-and-test:
26+
name: python ${{ matrix.python-version }}
27+
runs-on: windows-latest
28+
strategy:
29+
fail-fast: false
30+
max-parallel: 4
31+
matrix:
32+
python-version: ['3.6']
33+
steps:
34+
- name: Checkout
35+
uses: actions/checkout@v2
36+
with:
37+
path: scripts_src
38+
- name: Setup python ${{ matrix.python-version }}
39+
uses: actions/setup-python@v2
40+
with:
41+
python-version: ${{ matrix.python-version }}
42+
- name: Pip cache
43+
uses: actions/cache@v2
44+
with:
45+
path: ~/.cache/pip
46+
key: ${{ runner.os }}-pip-{{ matrix.python-version }}
47+
restore-keys: |
48+
${{ runner.os }}-pip
49+
- name: Pip install
50+
working-directory: scripts_src
51+
run: pip install ".[test]"
52+
- name: Test
53+
working-directory: scripts_src
54+
run: pytest tests
55+
- name: Static analysis
56+
working-directory: scripts_src
57+
run: mypy src tests

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Sim4Life Python Scripts
2+
[![Build Actions Status](https://github.com/dyollb/s4l-scripts/workflows/CI/badge.svg)](https://github.com/dyollb/s4l-scripts/actions)
3+
[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://https://opensource.org/licenses/MIT)
24
This is just a random collection of [Sim4Life](https://zmt.swiss/sim4life/) Python scripts.
35

4-
Sim4Life is a simulation platform for life scrience applications to investigate interaction between man-made devices and living organisms (e.g. active implants, mobile phones, neuro-stimulation, thermal heating, ...).
6+
Sim4Life is a simulation platform for life science applications to investigate interaction between man-made devices and living organisms (e.g. active implants, mobile phones, neuro-stimulation, thermal heating, ...).
57
It features a powerful CAD/surface-based modeling environment and GPU accelerated physics solvers, enabling simulations with extremely complex anatomical models, such as the [Virtual Population](https://itis.swiss/virtual-population/virtual-population/overview/).
68

79

mypy.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ warn_unused_configs = True
66

77
# Thirdparties:
88

9-
[mypy-s4l_v1.*,numpy,XCore,XCoreModeling,XPostProcessor,EmPostPro,Extractors,XRendererUI,win32gui]
9+
[mypy-s4l_v1.*,setuptools,numpy,nibabel,dipy.*,XCore,XCoreModeling,ImageModeling,MeshModeling,XPostProcessor,EmPostPro,Extractors,XRendererUI,win32gui]
1010
ignore_missing_imports = True

pyrightconfig.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"include": [
3+
"src/",
4+
"tests/",
5+
"scripts/"
6+
],
7+
"exclude": [
8+
],
9+
"useLibraryCodeForTypes": true,
10+
"reportPrivateImportUsage": false,
11+
"reportMissingImports": "warning"
12+
}

requirements-test.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mypy==0.920
2+
pytest==6.2.5

requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dipy==1.4.1
2+
nibabel==3.2.1
3+
numpy==1.19.5
4+
scipy==1.5.4

setup.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from setuptools import setup, find_packages
4+
5+
6+
install_requirements = [i.strip() for i in open("requirements.txt").readlines()]
7+
extra_requirements = [i.strip() for i in open("requirements-test.txt").readlines()]
8+
9+
with open("README.md") as f:
10+
readme = f.read()
11+
12+
with open("LICENSE") as f:
13+
license = f.read()
14+
15+
setup(
16+
name="s4l_scripts",
17+
version="0.1.0",
18+
description="Collection of Sim4Life scripts",
19+
long_description=readme,
20+
author="Bryn Lloyd",
21+
author_email="[email protected]",
22+
url="https://github.com/dyollb/s4l-scripts.git",
23+
license=license,
24+
install_requires=install_requirements,
25+
extras_require={"test": extra_requirements},
26+
packages=find_packages(where="src"),
27+
package_dir={
28+
"": "src",
29+
},
30+
)
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Subject-specific anatomical model with anisotropic conductivity from diffusion weighted imaging
2+
This folder contains scripts to create a subject-specific head model with inhomogeneous anisotropic conductivity distribution in Sim4Life, e.g. for transcranial electric/magnetic stimulation simulations. The workflow demonstrates how to reconstruct diffusion weighted images to diffusion tensor fields compatible with Sim4Life.
3+
4+
The file [main.py](main.py) implements the top-level workflow. The example downloads a demo dataset from the [Stanford digital repository](https://purl.stanford.edu/ng782rw8378) using the open-source package [dipy](https://dipy.org/). The key steps include:
5+
6+
- download the data
7+
- reconstruct diffusion tensors and convert to Sim4Life ordering
8+
- import diffusion tensors and evaluate conductivity based on [Tuch et al.](https://www.pnas.org/content/98/20/11697.short) in Sim4Life
9+
- visualize largest eigenvector of conductivity tensors
10+
- convert label infos to a Sim4Life-compatible tissue list format
11+
- import labels and extract surface model in Sim4Life
12+
- setup Ohmic Quasi-Stationary simulation with inhom. anisotropic conductivity
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright (c) 2021 The Foundation for Research on Information Technologies in Society (IT'IS).
2+
#
3+
# This file is part of s4l-scripts
4+
# (see https://github.com/dyollb/s4l-scripts).
5+
#
6+
# This software is released under the MIT License.
7+
# https://opensource.org/licenses/MIT
8+
9+
from pathlib import Path
10+
from typing import Dict, List
11+
import shutil
12+
13+
from dipy.data import (
14+
fetch_stanford_hardi,
15+
fetch_stanford_labels,
16+
fetch_stanford_t1,
17+
get_fnames,
18+
)
19+
20+
21+
def download_small_dwi_data(download_dir: Path) -> Dict[str, List[Path]]:
22+
"""Download small test dwi data, no t1 or labels are provided
23+
24+
Args:
25+
download_dir: specify folder where data is copied
26+
"""
27+
download_dir.mkdir(exist_ok=True)
28+
files = []
29+
for f in get_fnames(name="small_25"):
30+
files.append(Path(download_dir) / Path(f).name)
31+
shutil.copyfile(Path(f), files[-1])
32+
return {"dwi": files}
33+
34+
35+
def download_stanford_data(download_dir: Path) -> Dict[str, List[Path]]:
36+
"""Download standford dwi data, t1 image and labelfield
37+
38+
Args:
39+
download_dir: specify folder where data is copied
40+
"""
41+
42+
def _fetch_and_copy(fetch_fun):
43+
files, dipy_dir = fetch_fun()
44+
for f in files.keys():
45+
shutil.copyfile(Path(dipy_dir) / f, Path(download_dir) / f)
46+
return [download_dir / f for f in files.keys()]
47+
48+
download_dir.mkdir(exist_ok=True)
49+
dwi_files = _fetch_and_copy(fetch_stanford_hardi)
50+
t1_files = _fetch_and_copy(fetch_stanford_t1)
51+
label_files = _fetch_and_copy(fetch_stanford_labels)
52+
return {"dwi": dwi_files, "t1": t1_files, "labels": label_files}
53+
54+
55+
if __name__ == "__main__":
56+
output_dir = Path("/Users/lloyd/Models/StandfordData")
57+
files = download_stanford_data(download_dir=output_dir)
58+
59+
for f in output_dir.glob("*.*"):
60+
print(f)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright (c) 2021 The Foundation for Research on Information Technologies in Society (IT'IS).
2+
#
3+
# This file is part of s4l-scripts
4+
# (see https://github.com/dyollb/s4l-scripts).
5+
#
6+
# This software is released under the MIT License.
7+
# https://opensource.org/licenses/MIT
8+
9+
import numpy as np
10+
from pathlib import Path
11+
from typing import Dict
12+
13+
14+
def load_stanford_label_info(file_path: Path) -> Dict[int, str]:
15+
"""Load tissue names and labels from stanford example dataset
16+
17+
Args:
18+
file_path: label info file to parse
19+
20+
The file is assumed to by in CSV format with 3 columns, where the first row is a header, e.g.
21+
22+
new label, freesurfer label, freesurfer name
23+
1, 2, "Left-Cerebral-White-Matter"
24+
1, 41, "Right-Cerebral-White-Matter"
25+
1, 77, "WM-hypointensities"
26+
...
27+
"""
28+
with open(file_path, "r") as f:
29+
lines = f.readlines()
30+
label_info: Dict[int, str] = {}
31+
for line in lines[1:]:
32+
parts = line.split(",")
33+
if len(parts) != 3:
34+
continue
35+
label = int(parts[0].strip())
36+
name = parts[2].strip().strip('"').replace(" ", "_")
37+
if label in label_info:
38+
label_info[label] += f"_{name}"
39+
else:
40+
label_info[label] = name
41+
return label_info
42+
43+
44+
def save_iseg_label_info(label_info: Dict[int, str], file_path: Path):
45+
"""save label info dictionary as iSEG compatible tissue list
46+
47+
Args:
48+
tissue_list: input label info dictionary
49+
file_path: tissue list file
50+
51+
Example file:
52+
V7
53+
N3
54+
C0.00 0.00 1.00 0.50 Bone
55+
C0.00 1.00 0.00 0.50 Fat
56+
C1.00 0.00 0.00 0.50 Skin
57+
"""
58+
max_label = max(label_info.keys())
59+
tissue_names = [f"tissue{i}" for i in range(max_label + 1)]
60+
for id, name in label_info.items():
61+
tissue_names[id] = name
62+
63+
with open(file_path, "w") as f:
64+
print("V7", file=f)
65+
print(f"N{max_label}", file=f)
66+
for i in range(1, max_label):
67+
r, g, b = np.random.rand(), np.random.rand(), np.random.rand()
68+
print(f"C{r:.2f} {g:.2f} {b:.2f} 0.50 {tissue_names[i]}", file=f)
69+
70+
71+
if __name__ == "__main__":
72+
label_info_file = Path("/Users/lloyd/Models/StandfordData") / "label_info.txt"
73+
label_info = load_stanford_label_info(label_info_file)
74+
save_iseg_label_info(label_info, label_info_file.parent / "label_info_iseg.txt")

src/anisotropic_conductivity/main.py

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (c) 2021 The Foundation for Research on Information Technologies in Society (IT'IS).
2+
#
3+
# This file is part of s4l-scripts
4+
# (see https://github.com/dyollb/s4l-scripts).
5+
#
6+
# This software is released under the MIT License.
7+
# https://opensource.org/licenses/MIT
8+
9+
import sys
10+
from pathlib import Path
11+
12+
from .download_data import download_stanford_data
13+
from .load_labels import load_stanford_label_info, save_iseg_label_info
14+
from .reconstruct_diffusion_tensors import reconstruct_diffusion_tensors
15+
16+
17+
def import_in_sim4life(
18+
t1_file: Path, seg_file: Path, tissuelist_file: Path, s4l_tensors_file: Path
19+
):
20+
try:
21+
import s4l_v1 as s4l
22+
import ImageModeling # TODO: make sure this works with s4l
23+
import MeshModeling # TODO: make sure this works with s4l
24+
except ImportError as error:
25+
print("ERROR: Could not import Sim4Life Python modules", sys.exc_info()[0])
26+
return
27+
28+
# not used, but nice to visualize model with MRI
29+
t1_image = ImageModeling.ImportImage(t1_file)
30+
31+
# import label field
32+
labelfield = ImageModeling.ImportImage(
33+
seg_file, as_labelfield=True, tissuelist_path=tissuelist_file
34+
)
35+
36+
# extract surface-based model
37+
surfaces = MeshModeling.ExtractSurface(labelfield, min_edge_length=0.5)
38+
39+
# load dwi data, compute conductivity
40+
# s4l.analysis.import(s4l_tensors_file) ?
41+
42+
43+
def main():
44+
data_dir = Path("/Users/lloyd/Models/StandfordData")
45+
files = download_stanford_data(download_dir=data_dir)
46+
47+
# reconstruct DTI using the dipy package
48+
bvec_file = next(f for f in files["dwi"] if f.name.endswith("bvec"))
49+
s4l_tensors_file = bvec_file.parent / bvec_file.name.replace(".bvec", "_s4l.nii.gz")
50+
reconstruct_diffusion_tensors(bvec_file, s4l_tensors_file)
51+
52+
# convert label infos so we get tissue names in Sim4Life
53+
label_info_file = next(f for f in files["labels"] if f.name.endswith("txt"))
54+
iseg_tissue_list_file = label_info_file.parent / label_info_file.name.replace(
55+
".txt", "_iseg.txt"
56+
)
57+
label_infos = load_stanford_label_info(label_info_file)
58+
save_iseg_label_info(label_infos, iseg_tissue_list_file)
59+
60+
# load image and segmentation data in Sim4Life
61+
labels_file = next(f for f in files["labels"] if f.name.endswith("nii.gz"))
62+
t1_file = files["t1"][0]
63+
64+
assert labels_file.exists()
65+
assert t1_file.exists()
66+
67+
68+
if __name__ == "__main__":
69+
main()

0 commit comments

Comments
 (0)