Skip to content

Commit d81867a

Browse files
authored
Merge pull request #23 from Brain-Modulation-Lab/enh/group-flexible
enh: group to program and with user interactions
2 parents ca99034 + 7755e09 commit d81867a

10 files changed

Lines changed: 460 additions & 32 deletions

File tree

src/clinical_dbs_annotator/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"date",
5858
"time",
5959
"block_id",
60-
"group_ID",
60+
"program_ID",
6161
"session_ID",
6262
"is_initial",
6363
"scale_name",

src/clinical_dbs_annotator/config/clinical_presets.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
2+
"MDD": [
3+
"MADRS"
4+
],
25
"OCD": [
36
"Y-BOCS",
47
"Y-BOCS-o",
58
"Y-BOCS-c",
69
"MADRS"
710
],
8-
"MDD": [
9-
"MADRS"
10-
],
1111
"PD": [
1212
"UPDRS-III"
1313
]

src/clinical_dbs_annotator/models/session_data.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def write_clinical_scales(
170170
"date": today,
171171
"time": time_str,
172172
"block_id": self.block_id,
173-
"group_ID": group,
173+
"program_ID": group,
174174
"session_ID": self.session_id,
175175
"is_initial": 1, # Clinical scales are from view1, so is_initial = 1
176176
"scale_name": None,
@@ -187,7 +187,7 @@ def write_clinical_scales(
187187
"date": today,
188188
"time": time_str,
189189
"block_id": self.block_id,
190-
"group_ID": group,
190+
"program_ID": group,
191191
"session_ID": self.session_id,
192192
"is_initial": 1, # Clinical scales are from view1, so is_initial = 1
193193
"scale_name": scale.name,
@@ -234,7 +234,7 @@ def write_session_scales(
234234
"date": today,
235235
"time": time_str,
236236
"block_id": self.block_id,
237-
"group_ID": group,
237+
"program_ID": group,
238238
"session_ID": self.session_id,
239239
"is_initial": 0, # Session scales are from view3, so is_initial = 0
240240
"scale_name": None,
@@ -251,7 +251,7 @@ def write_session_scales(
251251
"date": today,
252252
"time": time_str,
253253
"block_id": self.block_id,
254-
"group_ID": group,
254+
"program_ID": group,
255255
"session_ID": self.session_id,
256256
"is_initial": 0, # Session scales are from view3, so is_initial = 0
257257
"scale_name": scale.name,

src/clinical_dbs_annotator/utils/longitudinal_exporter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ def _add_longitudinal_data_table(
587587
display_cols = [c for c in lateral_df.columns if c not in columns_to_exclude]
588588

589589
lateral_cols = ["date", "laterality", "frequency", "anode", "cathode", "amplitude", "pulse_width"]
590-
common_cols = ["group_ID", "scale_name", "scale_value", "notes"]
590+
common_cols = ["program_ID", "scale_name", "scale_value", "notes"]
591591

592592
lateral_cols = [c for c in lateral_cols if c in display_cols]
593593
common_cols = [c for c in common_cols if c in display_cols]
@@ -605,7 +605,7 @@ def _add_longitudinal_data_table(
605605
section = doc.sections[0]
606606
page_w = (section.page_width - section.left_margin - section.right_margin) / 914400
607607
base_w = {
608-
"date": 0.65, "laterality": 0.25, "group_ID": 0.35,
608+
"date": 0.65, "laterality": 0.25, "program_ID": 0.35,
609609
"frequency": 0.45, "anode": 0.45, "cathode": 0.60,
610610
"amplitude": 0.60, "pulse_width": 0.50,
611611
"scale_name": 1.00, "scale_value": 0.55,
@@ -935,7 +935,7 @@ def _create_lateral_table(self, df: pd.DataFrame) -> pd.DataFrame:
935935
"_global_entry_id": entry_id,
936936
"source": source_label,
937937
"date": date_val,
938-
"group_ID": first.get("group_ID", ""),
938+
"program_ID": first.get("program_ID", ""),
939939
"scale_name": combined_sn,
940940
"scale_value": combined_sv,
941941
"notes": first.get("notes", ""),
@@ -1193,7 +1193,7 @@ def _column_header(col: str) -> str:
11931193
"anode": "+", "cathode": "-",
11941194
"amplitude": PLACEHOLDERS.get("amplitude", "Amp"),
11951195
"pulse_width": PLACEHOLDERS.get("pulse_width", "PW"),
1196-
"group_ID": "Grp", "laterality": "",
1196+
"program_ID": "Prog", "laterality": "",
11971197
}
11981198
return m.get(col, col.replace("_", " ").title())
11991199

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Program configuration manager for custom program names.
2+
3+
This module handles loading, saving, and managing custom program names
4+
used in the DBS clinical programming interface.
5+
"""
6+
7+
import json
8+
import sys
9+
from pathlib import Path
10+
11+
12+
class ProgramConfigManager:
13+
"""Manages program name configuration with persistence."""
14+
15+
DEFAULT_PROGRAMS = ["None", "A", "B", "C", "D"]
16+
CONFIG_FILENAME = "program_names.json"
17+
18+
def __init__(self, config_dir: str | None = None):
19+
"""Initialize the program config manager.
20+
21+
Args:
22+
config_dir: Directory for config files. If None, uses logs in app installation directory.
23+
"""
24+
if config_dir is None:
25+
# Default to logs folder in the application installation directory
26+
# For deployed app: C:\Program Files\BML\Clinical DBS Annotator\logs
27+
# For development: uses the source directory
28+
if getattr(sys, 'frozen', False):
29+
# Running as deployed executable (PyInstaller/Nuitka)
30+
app_root = Path(sys.executable).parent
31+
else:
32+
# Running in development mode
33+
app_root = Path(__file__).parent.parent.parent
34+
self.config_dir = app_root / "logs"
35+
else:
36+
self.config_dir = Path(config_dir)
37+
38+
self.config_file = self.config_dir / self.CONFIG_FILENAME
39+
self._custom_programs: list[str] = []
40+
41+
# Ensure config directory exists
42+
self.config_dir.mkdir(parents=True, exist_ok=True)
43+
44+
# Load existing custom programs
45+
self._load_custom_programs()
46+
47+
def _load_custom_programs(self) -> None:
48+
"""Load custom program names from config file."""
49+
if self.config_file.exists():
50+
try:
51+
with open(self.config_file, encoding="utf-8") as f:
52+
data = json.load(f)
53+
self._custom_programs = data.get("custom_programs", [])
54+
except (OSError, json.JSONDecodeError):
55+
self._custom_programs = []
56+
else:
57+
self._custom_programs = []
58+
59+
def save_custom_programs(self, programs: list[str]) -> None:
60+
"""Save custom program names to config file.
61+
62+
Args:
63+
programs: List of custom program names to save.
64+
"""
65+
self._custom_programs = programs
66+
data = {"custom_programs": programs}
67+
try:
68+
with open(self.config_file, "w", encoding="utf-8") as f:
69+
json.dump(data, f, indent=2, ensure_ascii=False)
70+
except OSError:
71+
pass
72+
73+
def get_all_programs(self) -> list[str]:
74+
"""Get all available programs (default + custom).
75+
76+
Returns:
77+
List of program names.
78+
"""
79+
return self.DEFAULT_PROGRAMS + self._custom_programs
80+
81+
def get_custom_programs(self) -> list[str]:
82+
"""Get only custom program names.
83+
84+
Returns:
85+
List of custom program names.
86+
"""
87+
return self._custom_programs.copy()
88+
89+
def add_program(self, program_name: str) -> bool:
90+
"""Add a new custom program name.
91+
92+
Args:
93+
program_name: Name of the program to add.
94+
95+
Returns:
96+
True if added, False if already exists or invalid.
97+
"""
98+
if not program_name or program_name in self.DEFAULT_PROGRAMS or program_name in self._custom_programs:
99+
return False
100+
101+
self._custom_programs.append(program_name)
102+
self.save_custom_programs(self._custom_programs)
103+
return True
104+
105+
def remove_program(self, program_name: str) -> bool:
106+
"""Remove a custom program name.
107+
108+
Args:
109+
program_name: Name of the program to remove.
110+
111+
Returns:
112+
True if removed, False if not found or is a default program.
113+
"""
114+
if program_name in self.DEFAULT_PROGRAMS:
115+
return False
116+
117+
if program_name in self._custom_programs:
118+
self._custom_programs.remove(program_name)
119+
self.save_custom_programs(self._custom_programs)
120+
return True
121+
return False
122+
123+
def update_program(self, old_name: str, new_name: str) -> bool:
124+
"""Update an existing custom program name.
125+
126+
Args:
127+
old_name: Current name of the program.
128+
new_name: New name for the program.
129+
130+
Returns:
131+
True if updated, False if old_name not found or new_name invalid.
132+
"""
133+
if old_name in self.DEFAULT_PROGRAMS:
134+
return False
135+
136+
if not new_name or new_name in self.DEFAULT_PROGRAMS or new_name in self._custom_programs:
137+
return False
138+
139+
if old_name in self._custom_programs:
140+
idx = self._custom_programs.index(old_name)
141+
self._custom_programs[idx] = new_name
142+
self.save_custom_programs(self._custom_programs)
143+
return True
144+
return False
145+
146+
147+
# Singleton instance
148+
_instance: ProgramConfigManager | None = None
149+
150+
151+
def get_program_config_manager() -> ProgramConfigManager:
152+
"""Get the singleton ProgramConfigManager instance.
153+
154+
Returns:
155+
The singleton instance.
156+
"""
157+
global _instance
158+
if _instance is None:
159+
_instance = ProgramConfigManager()
160+
return _instance

src/clinical_dbs_annotator/utils/session_exporter.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -464,12 +464,12 @@ def _create_lateral_table_data(self, df):
464464
right_row['block_id'] = block_id
465465

466466
# Common columns (non-lateral) - use combined scales with internal lines
467-
left_row['group_ID'] = first_row.get('group_ID', '')
467+
left_row['program_ID'] = first_row.get('program_ID', '')
468468
left_row['scale_name'] = combined_scale_name
469469
left_row['scale_value'] = combined_scale_value
470470
left_row['notes'] = first_row.get('notes', '')
471471

472-
right_row['group_ID'] = first_row.get('group_ID', '')
472+
right_row['program_ID'] = first_row.get('program_ID', '')
473473
right_row['scale_name'] = combined_scale_name
474474
right_row['scale_value'] = combined_scale_value
475475
right_row['notes'] = first_row.get('notes', '')
@@ -524,7 +524,7 @@ def _add_session_data_table(self, doc: Document, df_table: pd.DataFrame) -> None
524524
display_columns = [col for col in lateral_df.columns if col not in columns_to_exclude]
525525

526526
lateral_cols = ['laterality', 'frequency', 'anode', 'cathode', 'amplitude', 'pulse_width']
527-
common_cols = ['group_ID', 'scale_name', 'scale_value', 'notes']
527+
common_cols = ['program_ID', 'scale_name', 'scale_value', 'notes']
528528

529529
lateral_cols = [col for col in lateral_cols if col in display_columns]
530530
common_cols = [col for col in common_cols if col in display_columns]
@@ -540,7 +540,7 @@ def _add_session_data_table(self, doc: Document, df_table: pd.DataFrame) -> None
540540

541541
base_in = {
542542
'laterality': 0.30,
543-
'group_ID': 0.40,
543+
'program_ID': 0.40,
544544
'frequency': 0.50,
545545
'anode': 0.45,
546546
'cathode': 0.60,
@@ -935,7 +935,7 @@ def _column_header(self, col: str) -> str:
935935
"cathode": "-",
936936
"amplitude": PLACEHOLDERS.get("amplitude"),
937937
"pulse_width": PLACEHOLDERS.get("pulse_width"),
938-
"group_ID": "grp",
938+
"program_ID": "Prog",
939939
"laterality": "",
940940
}
941941
if col in placeholder_map and placeholder_map[col] is not None:

0 commit comments

Comments
 (0)