Skip to content

Commit e40a7b7

Browse files
Enhance documentation and implement quality checks for parameter uniqueness and sparsity analysis
- Updated global development instructions to include output structure and reproducibility goals. - Modified example configuration for DSI Studio to correct command path. - Added functions for computing sparsity and sparsity scores in analysis_monolith.py. - Expanded test suite to include regression tests for sparsity scoring and CLI behavior. - Centralized quality checks into a unified script with subcommands for quick checks and uniqueness analysis. - Updated install script to support optional experimental requirements installation. - Refactored quick_quality_check.py and verify_parameter_uniqueness.py to delegate functionality to quality_checks.py.
1 parent 361efa6 commit e40a7b7

8 files changed

Lines changed: 538 additions & 317 deletions

File tree

.github/instructions/global.md.instructions.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Global Development Instructions
22

3+
# Purpose
4+
- takes preprocessed fib files
5+
- generates connectivity matrices using DSI Studio
6+
- uses a wide spectrum of parameters
7+
- outputs matrices in a structured directory format
8+
- ensures reproducibility and consistency across runs
9+
- weight each parameter combination to get a qa score
10+
- use cross-validation to find optimal parameter sets
11+
- give user BEST parameter sets for their data (THAT'S THE GOAL!)
12+
313
## Package Management
414
- **All packages MUST be installed via `install.sh`** - never install packages manually
515
- **All installations MUST occur within the virtual environment** - no global installations

dsi_studio_tools/example_config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"comment": "Example configuration for connectivity matrix extraction",
33

4-
"dsi_studio_cmd": "/data/local/software/dsi-studio/dsi_studio",
4+
"dsi_studio_cmd": "/Applications/dsi_studio.app/Contents/MacOS/dsi_studio",
55

66
"atlases": [
77
"ATAG_basal_ganglia",

experimental/monolith_refactor/analysis_monolith.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import sys
1818
import json
1919
from pathlib import Path
20+
import numpy as np
2021

2122
HAS_DEPS = True
2223
try:
@@ -28,6 +29,39 @@
2829
HAS_DEPS = False
2930

3031

32+
def compute_sparsity_from_matrix(matrix: np.ndarray) -> float:
33+
"""Compute network sparsity as proportion of zero off-diagonal elements.
34+
35+
Returns a float in [0,1].
36+
"""
37+
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
38+
raise ValueError("matrix must be square")
39+
n = matrix.shape[0]
40+
mask = ~np.eye(n, dtype=bool)
41+
off = matrix[mask]
42+
zeros = np.sum(off == 0)
43+
return float(zeros) / float(off.size)
44+
45+
46+
def compute_sparsity_score_generic(sparsity_values: np.ndarray, min_sparsity: float = 0.05, max_sparsity: float = 0.4) -> np.ndarray:
47+
"""Compute sparsity quality scores as in MetricOptimizer.compute_sparsity_score.
48+
49+
Uses the same heuristic: values near center of [min,max] get score ~1, outside range penalized.
50+
"""
51+
sparsity_values = np.array(sparsity_values, dtype=float)
52+
min_s, max_s = float(min_sparsity), float(max_sparsity)
53+
optimal = (min_s + max_s) / 2.0
54+
width = max_s - min_s
55+
distance = np.abs(sparsity_values - optimal)
56+
# Avoid division by zero
57+
denom = (width / 2.0) if width != 0 else 1.0
58+
normalized = distance / denom
59+
scores = np.maximum(0.0, 1.0 - normalized)
60+
out_of_range = (sparsity_values < min_s) | (sparsity_values > max_s)
61+
scores[out_of_range] = scores[out_of_range] * 0.1
62+
return scores
63+
64+
3165
def do_score(args: argparse.Namespace) -> int:
3266
print("\n[monolith] SCORE\u001b[0m")
3367
print(f"Input: {args.input}")

experimental/monolith_refactor/tests/test_smoke.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,159 @@ def test_monolith_dry_run_compare():
1717
res = subprocess.run(cmd, capture_output=True, text=True)
1818
print(res.stdout)
1919
assert res.returncode == 0
20+
21+
22+
def test_sparsity_score_regression():
23+
# Load sample adjacency
24+
import numpy as _np
25+
from pathlib import Path as _P
26+
csv = _P(__file__).resolve().parents[1] / 'data' / 'sample_adj.csv'
27+
mat = _np.loadtxt(str(csv), delimiter=',')
28+
# Compute sparsity using POC helper via subprocess import
29+
script = _P(__file__).resolve().parents[1] / 'analysis_monolith.py'
30+
# Direct import from the script to call helper
31+
import importlib.util as _spec
32+
spec = _spec.spec_from_file_location('analysis_monolith', str(script))
33+
mod = _spec.module_from_spec(spec)
34+
spec.loader.exec_module(mod)
35+
sparsity = mod.compute_sparsity_from_matrix(mat)
36+
assert abs(sparsity - 0.3333333333333333) < 1e-6
37+
# If MetricOptimizer available, compare scores
38+
try:
39+
from scripts.metric_optimizer import MetricOptimizer
40+
mo = MetricOptimizer()
41+
# MetricOptimizer expects arrays; compute score using its method
42+
svals = _np.array([sparsity])
43+
mo_scores = mo.compute_sparsity_score(svals)
44+
poc_scores = mod.compute_sparsity_score_generic(svals)
45+
assert abs(float(mo_scores[0]) - float(poc_scores[0])) < 1e-6
46+
except Exception:
47+
# If MetricOptimizer isn't available in imports, at least verify poc scoring produces expected shape
48+
svals = _np.array([sparsity])
49+
poc_scores = mod.compute_sparsity_score_generic(svals)
50+
assert poc_scores.shape == (1,)
51+
52+
53+
def test_cli_prints_help_on_no_args():
54+
script = Path(__file__).resolve().parents[1] / 'analysis_monolith.py'
55+
# Running with no args should print help and exit 0
56+
res = subprocess.run([sys.executable, str(script)], capture_output=True, text=True)
57+
print(res.stdout)
58+
assert res.returncode == 0
59+
assert ('usage' in res.stdout.lower()) or ('experimental monolith' in res.stdout.lower()) or ('analysis_monolith' in res.stdout)
60+
61+
62+
import pytest
63+
64+
65+
@pytest.mark.parametrize('vals', [
66+
[0.01, 0.1, 0.2, 0.3],
67+
[0.05, 0.15, 0.35, 0.5],
68+
[0.0, 0.4, 0.6]
69+
])
70+
def test_vectorized_sparsity_scores(vals):
71+
"""Compare vectorized sparsity scoring between POC helper and MetricOptimizer when available."""
72+
import numpy as _np
73+
from importlib import util as _spec
74+
script = Path(__file__).resolve().parents[1] / 'analysis_monolith.py'
75+
spec = _spec.spec_from_file_location('analysis_monolith', str(script))
76+
mod = _spec.module_from_spec(spec)
77+
spec.loader.exec_module(mod)
78+
arr = _np.array(vals, dtype=float)
79+
poc = mod.compute_sparsity_score_generic(arr)
80+
try:
81+
from scripts.metric_optimizer import MetricOptimizer
82+
mo = MetricOptimizer()
83+
mo_scores = mo.compute_sparsity_score(arr)
84+
# Allow small numerical tolerance
85+
assert _np.allclose(_np.asarray(poc, dtype=float), _np.asarray(mo_scores, dtype=float), atol=1e-6)
86+
except Exception:
87+
# If MetricOptimizer not importable, ensure POC returns sensible numbers in [0,1]
88+
assert _np.all((poc >= 0.0) & (poc <= 1.0))
89+
90+
91+
def test_matrix_sparsity_and_score_various():
92+
"""Generate small matrices with controlled sparsity and check scoring behaviour."""
93+
import numpy as _np
94+
from importlib import util as _spec
95+
script = Path(__file__).resolve().parents[1] / 'analysis_monolith.py'
96+
spec = _spec.spec_from_file_location('analysis_monolith', str(script))
97+
mod = _spec.module_from_spec(spec)
98+
spec.loader.exec_module(mod)
99+
100+
# Create matrices 3x3 with varying off-diagonal zeros
101+
mats = []
102+
# fully connected (no zeros off-diag)
103+
mats.append(_np.array([[0,1,1],[1,0,1],[1,1,0]]))
104+
# one zero off-diag
105+
mats.append(_np.array([[0,1,0],[1,0,1],[0,1,0]]))
106+
# many zeros (sparse)
107+
mats.append(_np.array([[0,0,0],[0,0,1],[0,1,0]]))
108+
109+
sparsities = [round(mod.compute_sparsity_from_matrix(m), 6) for m in mats]
110+
# Basic sanity: sparsity values should be between 0 and 1 and increase with more zeros
111+
assert all(0.0 <= s <= 1.0 for s in sparsities)
112+
assert sparsities[0] < sparsities[1] < sparsities[2]
113+
114+
# Check scoring produces array same length
115+
arr = _np.array(sparsities)
116+
scores = mod.compute_sparsity_score_generic(arr)
117+
assert scores.shape == arr.shape
118+
119+
120+
def test_regression_grid_sparsity_scores():
121+
"""Compare scoring vectors over a grid of sparsity values and synthetic matrices."""
122+
import numpy as _np
123+
from importlib import util as _spec
124+
script = Path(__file__).resolve().parents[1] / 'analysis_monolith.py'
125+
spec = _spec.spec_from_file_location('analysis_monolith', str(script))
126+
mod = _spec.module_from_spec(spec)
127+
spec.loader.exec_module(mod)
128+
129+
# Generate grid of sparsity values
130+
grid = _np.linspace(0.0, 1.0, 21)
131+
poc_scores = mod.compute_sparsity_score_generic(grid)
132+
try:
133+
from scripts.metric_optimizer import MetricOptimizer
134+
mo = MetricOptimizer()
135+
mo_scores = mo.compute_sparsity_score(grid)
136+
# Compare full vectors
137+
assert _np.allclose(_np.asarray(poc_scores, dtype=float), _np.asarray(mo_scores, dtype=float), atol=1e-6)
138+
except Exception:
139+
# If MetricOptimizer not importable, check shape and range
140+
assert poc_scores.shape == grid.shape
141+
assert _np.all((poc_scores >= 0.0) & (poc_scores <= 1.0))
142+
143+
def test_regression_synthetic_matrices():
144+
"""Create a set of synthetic matrices, compute sparsity and compare scoring vectors."""
145+
import numpy as _np
146+
from importlib import util as _spec
147+
script = Path(__file__).resolve().parents[1] / 'analysis_monolith.py'
148+
spec = _spec.spec_from_file_location('analysis_monolith', str(script))
149+
mod = _spec.module_from_spec(spec)
150+
spec.loader.exec_module(mod)
151+
152+
# Create synthetic matrices of size 4x4 with varying sparsity
153+
mats = []
154+
# Fully connected
155+
mats.append(_np.ones((4,4)) - _np.eye(4))
156+
# Half zeros off-diag
157+
m = _np.ones((4,4)) - _np.eye(4)
158+
m[0,1] = m[1,2] = m[2,3] = m[3,0] = 0
159+
mats.append(m)
160+
# Mostly zeros
161+
m = _np.zeros((4,4))
162+
m[0,3] = m[1,2] = m[2,1] = m[3,0] = 1
163+
mats.append(m)
164+
165+
sparsities = [_np.round(mod.compute_sparsity_from_matrix(mat), 6) for mat in mats]
166+
arr = _np.array(sparsities)
167+
poc_scores = mod.compute_sparsity_score_generic(arr)
168+
try:
169+
from scripts.metric_optimizer import MetricOptimizer
170+
mo = MetricOptimizer()
171+
mo_scores = mo.compute_sparsity_score(arr)
172+
assert _np.allclose(_np.asarray(poc_scores, dtype=float), _np.asarray(mo_scores, dtype=float), atol=1e-6)
173+
except Exception:
174+
assert poc_scores.shape == arr.shape
175+
assert _np.all((poc_scores >= 0.0) & (poc_scores <= 1.0))

install.sh

100644100755
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,22 @@ echo -e "${YELLOW}📋 To deactivate the environment:${NC}"
119119
echo " deactivate"
120120
echo ""
121121
echo -e "${GREEN}🚀 Environment ready for braingraph pipeline!${NC}"
122+
123+
# Optional experimental extras
124+
# Install experimental dev requirements when INSTALL_EXPERIMENTAL=1 is set.
125+
# This keeps all installs centralized in install.sh as requested by global instructions.
126+
if [ "${INSTALL_EXPERIMENTAL:-0}" = "1" ]; then
127+
echo -e "\n${BLUE}🧪 Installing experimental extras...${NC}"
128+
if [ -f "experimental/monolith_refactor/requirements.txt" ]; then
129+
echo -e "${BLUE}🔧 Installing experimental requirements from experimental/monolith_refactor/requirements.txt${NC}"
130+
# Ensure pip exists in the venv (some venvs may not have pip bootstrapped)
131+
python -m ensurepip --upgrade || true
132+
python -m pip install --upgrade pip setuptools wheel || true
133+
# Best-effort: use python -m pip inside venv
134+
python -m pip install -r experimental/monolith_refactor/requirements.txt || {
135+
echo -e "${YELLOW}⚠️ Failed to install experimental requirements. Continuing without them.${NC}"
136+
}
137+
else
138+
echo -e "${YELLOW}⚠️ No experimental requirements file found; skipping.${NC}"
139+
fi
140+
fi

0 commit comments

Comments
 (0)