holoscript 6.0.4__tar.gz → 6.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {holoscript-6.0.4 → holoscript-6.0.5}/PKG-INFO +15 -2
- {holoscript-6.0.4 → holoscript-6.0.5}/holoscript/__init__.py +1 -1
- holoscript-6.0.5/holoscript/bridges/__init__.py +11 -0
- holoscript-6.0.5/holoscript/bridges/alphafold.py +345 -0
- holoscript-6.0.5/holoscript/bridges/medical.py +236 -0
- holoscript-6.0.5/holoscript/bridges/narupa.py +376 -0
- holoscript-6.0.5/holoscript/bridges/radio_astronomy.py +45 -0
- holoscript-6.0.5/holoscript/bridges/robotics.py +77 -0
- holoscript-6.0.5/holoscript/bridges/scientific.py +213 -0
- {holoscript-6.0.4 → holoscript-6.0.5}/holoscript.egg-info/PKG-INFO +15 -2
- holoscript-6.0.5/holoscript.egg-info/SOURCES.txt +16 -0
- holoscript-6.0.5/holoscript.egg-info/requires.txt +23 -0
- {holoscript-6.0.4 → holoscript-6.0.5}/pyproject.toml +21 -2
- holoscript-6.0.4/holoscript.egg-info/SOURCES.txt +0 -9
- holoscript-6.0.4/holoscript.egg-info/requires.txt +0 -4
- {holoscript-6.0.4 → holoscript-6.0.5}/README.md +0 -0
- {holoscript-6.0.4 → holoscript-6.0.5}/holoscript.egg-info/dependency_links.txt +0 -0
- {holoscript-6.0.4 → holoscript-6.0.5}/holoscript.egg-info/top_level.txt +0 -0
- {holoscript-6.0.4 → holoscript-6.0.5}/setup.cfg +0 -0
- {holoscript-6.0.4 → holoscript-6.0.5}/tests/test_smoke.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: holoscript
|
|
3
|
-
Version: 6.0.
|
|
4
|
-
Summary: Python bindings for HoloScript parsing and
|
|
3
|
+
Version: 6.0.5
|
|
4
|
+
Summary: Python bindings for HoloScript — parsing, validation, and domain bridges for scientific computing
|
|
5
5
|
Author: Brian X Base Team
|
|
6
6
|
License: MIT
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -19,6 +19,19 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
Provides-Extra: dev
|
|
20
20
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
21
21
|
Requires-Dist: mypy>=1.11.0; extra == "dev"
|
|
22
|
+
Provides-Extra: medical
|
|
23
|
+
Requires-Dist: pydicom>=2.4.0; extra == "medical"
|
|
24
|
+
Requires-Dist: numpy>=1.24.0; extra == "medical"
|
|
25
|
+
Provides-Extra: alphafold
|
|
26
|
+
Requires-Dist: requests>=2.31.0; extra == "alphafold"
|
|
27
|
+
Provides-Extra: astronomy
|
|
28
|
+
Requires-Dist: numpy>=1.24.0; extra == "astronomy"
|
|
29
|
+
Provides-Extra: robotics
|
|
30
|
+
Requires-Dist: roslibpy>=1.6.0; extra == "robotics"
|
|
31
|
+
Provides-Extra: scientific
|
|
32
|
+
Requires-Dist: numpy>=1.24.0; extra == "scientific"
|
|
33
|
+
Provides-Extra: all
|
|
34
|
+
Requires-Dist: holoscript[alphafold,astronomy,medical,robotics,scientific]; extra == "all"
|
|
22
35
|
|
|
23
36
|
# holoscript (Python bindings)
|
|
24
37
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HoloScript Domain Bridges — Python integrations for scientific computing.
|
|
3
|
+
|
|
4
|
+
Install domain extras:
|
|
5
|
+
pip install holoscript[medical] # DICOM imaging
|
|
6
|
+
pip install holoscript[alphafold] # Protein structure prediction
|
|
7
|
+
pip install holoscript[astronomy] # Radio astronomy / synchrotron
|
|
8
|
+
pip install holoscript[robotics] # ROS2 integration
|
|
9
|
+
pip install holoscript[scientific] # AutoDock molecular docking
|
|
10
|
+
pip install holoscript[all] # Everything
|
|
11
|
+
"""
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AlphaFold Bridge for HoloScript
|
|
4
|
+
Protein structure prediction using AlphaFold3 API or ColabFold
|
|
5
|
+
|
|
6
|
+
Requirements:
|
|
7
|
+
pip install requests
|
|
8
|
+
|
|
9
|
+
Optional (for local prediction):
|
|
10
|
+
pip install colabfold
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from alphafold_bridge import AlphaFoldBridge
|
|
14
|
+
|
|
15
|
+
bridge = AlphaFoldBridge(api_key='your_api_key')
|
|
16
|
+
result = bridge.predict_structure({
|
|
17
|
+
'sequence': 'MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEKAVQVKVK',
|
|
18
|
+
'job_name': 'my_protein',
|
|
19
|
+
})
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
import os
|
|
24
|
+
import json
|
|
25
|
+
import time
|
|
26
|
+
from typing import Dict, List, Any, Optional
|
|
27
|
+
import requests
|
|
28
|
+
|
|
29
|
+
class AlphaFoldBridge:
|
|
30
|
+
"""
|
|
31
|
+
AlphaFold bridge for protein structure prediction
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
35
|
+
"""
|
|
36
|
+
Initialize AlphaFold bridge
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
api_key: AlphaFold3 API key (optional, for API mode)
|
|
40
|
+
"""
|
|
41
|
+
self.api_key = api_key or os.getenv('ALPHAFOLD_API_KEY')
|
|
42
|
+
self.api_base_url = 'https://api.alphafoldserver.com/v1'
|
|
43
|
+
self.colabfold_available = False
|
|
44
|
+
self._check_dependencies()
|
|
45
|
+
|
|
46
|
+
def _check_dependencies(self):
|
|
47
|
+
"""Check if ColabFold is installed (for local prediction)"""
|
|
48
|
+
try:
|
|
49
|
+
import colabfold
|
|
50
|
+
self.colabfold_available = True
|
|
51
|
+
print("✓ ColabFold is available for local prediction", file=sys.stderr)
|
|
52
|
+
except ImportError:
|
|
53
|
+
self.colabfold_available = False
|
|
54
|
+
print("⚠ ColabFold not installed. API mode only.", file=sys.stderr)
|
|
55
|
+
|
|
56
|
+
def predict_structure(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Predict protein structure using AlphaFold
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
config: Prediction configuration
|
|
62
|
+
- sequence: str - Amino acid sequence (single-letter codes)
|
|
63
|
+
- job_name: str - Name for this prediction job
|
|
64
|
+
- mode: str - 'api' or 'local' (default: 'api')
|
|
65
|
+
- num_models: int - Number of models to generate (default: 5)
|
|
66
|
+
- use_templates: bool - Use template structures (default: False)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dictionary with prediction results:
|
|
70
|
+
- status: 'success' or 'failed'
|
|
71
|
+
- pdb_data: PDB file contents (if successful)
|
|
72
|
+
- confidence_scores: pLDDT scores per residue
|
|
73
|
+
- mean_plddt: Mean confidence score
|
|
74
|
+
- pae_data: Predicted Aligned Error matrix (optional)
|
|
75
|
+
- job_id: Job ID for tracking (API mode)
|
|
76
|
+
"""
|
|
77
|
+
sequence = config['sequence']
|
|
78
|
+
job_name = config.get('job_name', 'holoscript_prediction')
|
|
79
|
+
mode = config.get('mode', 'api')
|
|
80
|
+
num_models = config.get('num_models', 5)
|
|
81
|
+
use_templates = config.get('use_templates', False)
|
|
82
|
+
|
|
83
|
+
if mode == 'api':
|
|
84
|
+
return self._predict_via_api(sequence, job_name, num_models, use_templates)
|
|
85
|
+
elif mode == 'local':
|
|
86
|
+
return self._predict_via_colabfold(sequence, job_name, num_models)
|
|
87
|
+
else:
|
|
88
|
+
return {
|
|
89
|
+
'error': f'Unknown prediction mode: {mode}',
|
|
90
|
+
'status': 'failed',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def _predict_via_api(
|
|
94
|
+
self,
|
|
95
|
+
sequence: str,
|
|
96
|
+
job_name: str,
|
|
97
|
+
num_models: int,
|
|
98
|
+
use_templates: bool
|
|
99
|
+
) -> Dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Predict structure via AlphaFold3 API
|
|
102
|
+
|
|
103
|
+
Note: This is a stub - actual API integration requires:
|
|
104
|
+
1. AlphaFold3 API access (currently limited beta)
|
|
105
|
+
2. API authentication token
|
|
106
|
+
3. Polling for job completion
|
|
107
|
+
"""
|
|
108
|
+
if not self.api_key:
|
|
109
|
+
return {
|
|
110
|
+
'error': 'AlphaFold API key not set. Set ALPHAFOLD_API_KEY environment variable.',
|
|
111
|
+
'status': 'failed',
|
|
112
|
+
'message': 'API mode requires authentication',
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Submit prediction job
|
|
117
|
+
headers = {
|
|
118
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
payload = {
|
|
123
|
+
'sequences': [{'sequence': sequence}],
|
|
124
|
+
'modelPreset': 'monomer' if num_models == 1 else 'multimer',
|
|
125
|
+
'numPredictions': num_models,
|
|
126
|
+
'useTemplates': use_templates,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Note: This is a placeholder URL - actual API endpoint may differ
|
|
130
|
+
submit_url = f'{self.api_base_url}/predict'
|
|
131
|
+
|
|
132
|
+
print(f"Submitting prediction job: {job_name}", file=sys.stderr)
|
|
133
|
+
response = requests.post(submit_url, json=payload, headers=headers, timeout=30)
|
|
134
|
+
|
|
135
|
+
if response.status_code == 202:
|
|
136
|
+
# Job accepted, poll for results
|
|
137
|
+
job_data = response.json()
|
|
138
|
+
job_id = job_data.get('jobId')
|
|
139
|
+
|
|
140
|
+
print(f"Job ID: {job_id}. Polling for completion...", file=sys.stderr)
|
|
141
|
+
return self._poll_job_status(job_id, headers)
|
|
142
|
+
else:
|
|
143
|
+
return {
|
|
144
|
+
'error': f'API request failed: {response.status_code}',
|
|
145
|
+
'status': 'failed',
|
|
146
|
+
'message': response.text,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
return {
|
|
151
|
+
'error': str(e),
|
|
152
|
+
'status': 'failed',
|
|
153
|
+
'message': f'AlphaFold API prediction failed: {str(e)}',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def _poll_job_status(self, job_id: str, headers: Dict[str, str]) -> Dict[str, Any]:
|
|
157
|
+
"""
|
|
158
|
+
Poll AlphaFold API job status until completion
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
job_id: Job ID from submission
|
|
162
|
+
headers: Request headers with auth
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Prediction results
|
|
166
|
+
"""
|
|
167
|
+
status_url = f'{self.api_base_url}/jobs/{job_id}'
|
|
168
|
+
max_polls = 60 # Max 10 minutes (60 * 10 seconds)
|
|
169
|
+
poll_interval = 10 # seconds
|
|
170
|
+
|
|
171
|
+
for attempt in range(max_polls):
|
|
172
|
+
try:
|
|
173
|
+
response = requests.get(status_url, headers=headers, timeout=30)
|
|
174
|
+
if response.status_code == 200:
|
|
175
|
+
job_data = response.json()
|
|
176
|
+
status = job_data.get('status')
|
|
177
|
+
|
|
178
|
+
if status == 'completed':
|
|
179
|
+
# Download results
|
|
180
|
+
pdb_url = job_data.get('pdbUrl')
|
|
181
|
+
confidence_url = job_data.get('confidenceUrl')
|
|
182
|
+
|
|
183
|
+
pdb_data = requests.get(pdb_url, timeout=30).text
|
|
184
|
+
confidence_data = requests.get(confidence_url, timeout=30).json()
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
'status': 'success',
|
|
188
|
+
'job_id': job_id,
|
|
189
|
+
'pdb_data': pdb_data,
|
|
190
|
+
'confidence_scores': confidence_data.get('plddt', []),
|
|
191
|
+
'mean_plddt': confidence_data.get('meanPlddt', 0.0),
|
|
192
|
+
'pae_data': confidence_data.get('pae', None),
|
|
193
|
+
}
|
|
194
|
+
elif status == 'failed':
|
|
195
|
+
return {
|
|
196
|
+
'error': 'Prediction job failed',
|
|
197
|
+
'status': 'failed',
|
|
198
|
+
'job_id': job_id,
|
|
199
|
+
}
|
|
200
|
+
else:
|
|
201
|
+
print(f"Job status: {status}. Waiting...", file=sys.stderr)
|
|
202
|
+
time.sleep(poll_interval)
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print(f"Polling error: {e}. Retrying...", file=sys.stderr)
|
|
206
|
+
time.sleep(poll_interval)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
'error': 'Job timeout after 10 minutes',
|
|
210
|
+
'status': 'failed',
|
|
211
|
+
'job_id': job_id,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def _predict_via_colabfold(
|
|
215
|
+
self,
|
|
216
|
+
sequence: str,
|
|
217
|
+
job_name: str,
|
|
218
|
+
num_models: int
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
"""
|
|
221
|
+
Predict structure via local ColabFold installation
|
|
222
|
+
|
|
223
|
+
Note: Requires ColabFold + GPU for reasonable performance
|
|
224
|
+
"""
|
|
225
|
+
if not self.colabfold_available:
|
|
226
|
+
return {
|
|
227
|
+
'error': 'ColabFold not installed',
|
|
228
|
+
'status': 'failed',
|
|
229
|
+
'message': 'Install ColabFold: pip install colabfold',
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
# Note: This is a simplified stub
|
|
234
|
+
# Full implementation requires:
|
|
235
|
+
# 1. Write sequence to FASTA file
|
|
236
|
+
# 2. Run colabfold_batch command
|
|
237
|
+
# 3. Parse output PDB and confidence JSON
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
'error': 'Local ColabFold prediction not yet implemented',
|
|
241
|
+
'status': 'failed',
|
|
242
|
+
'message': 'Use API mode for now: mode="api"',
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
return {
|
|
247
|
+
'error': str(e),
|
|
248
|
+
'status': 'failed',
|
|
249
|
+
'message': f'ColabFold prediction failed: {str(e)}',
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
def predict_multimer(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
253
|
+
"""
|
|
254
|
+
Predict protein complex structure (AlphaFold-Multimer)
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
config: Multimer prediction configuration
|
|
258
|
+
- sequences: List[str] - Multiple protein sequences
|
|
259
|
+
- stoichiometry: List[int] - Copy numbers (e.g., [2, 1] for A2B)
|
|
260
|
+
- job_name: str - Name for this prediction
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Prediction results (same format as predict_structure)
|
|
264
|
+
"""
|
|
265
|
+
sequences = config['sequences']
|
|
266
|
+
stoichiometry = config.get('stoichiometry', [1] * len(sequences))
|
|
267
|
+
job_name = config.get('job_name', 'multimer_prediction')
|
|
268
|
+
|
|
269
|
+
if not self.api_key:
|
|
270
|
+
return {
|
|
271
|
+
'error': 'Multimer prediction requires API access',
|
|
272
|
+
'status': 'failed',
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
headers = {
|
|
277
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
278
|
+
'Content-Type': 'application/json',
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Build multimer payload
|
|
282
|
+
chains = []
|
|
283
|
+
for i, seq in enumerate(sequences):
|
|
284
|
+
for _ in range(stoichiometry[i]):
|
|
285
|
+
chains.append({'sequence': seq})
|
|
286
|
+
|
|
287
|
+
payload = {
|
|
288
|
+
'sequences': chains,
|
|
289
|
+
'modelPreset': 'multimer',
|
|
290
|
+
'numPredictions': 5,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
submit_url = f'{self.api_base_url}/predict'
|
|
294
|
+
response = requests.post(submit_url, json=payload, headers=headers, timeout=30)
|
|
295
|
+
|
|
296
|
+
if response.status_code == 202:
|
|
297
|
+
job_data = response.json()
|
|
298
|
+
job_id = job_data.get('jobId')
|
|
299
|
+
return self._poll_job_status(job_id, headers)
|
|
300
|
+
else:
|
|
301
|
+
return {
|
|
302
|
+
'error': f'Multimer API request failed: {response.status_code}',
|
|
303
|
+
'status': 'failed',
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
return {
|
|
308
|
+
'error': str(e),
|
|
309
|
+
'status': 'failed',
|
|
310
|
+
'message': f'Multimer prediction failed: {str(e)}',
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
def get_status(self) -> Dict[str, Any]:
|
|
314
|
+
"""
|
|
315
|
+
Get AlphaFold bridge status
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Status dictionary
|
|
319
|
+
"""
|
|
320
|
+
return {
|
|
321
|
+
'api_available': bool(self.api_key),
|
|
322
|
+
'colabfold_available': self.colabfold_available,
|
|
323
|
+
'python_version': sys.version,
|
|
324
|
+
'module': 'alphafold_bridge',
|
|
325
|
+
'version': '1.0.0',
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# Test mode
|
|
330
|
+
if __name__ == '__main__':
|
|
331
|
+
bridge = AlphaFoldBridge()
|
|
332
|
+
|
|
333
|
+
# Test status
|
|
334
|
+
status = bridge.get_status()
|
|
335
|
+
print("AlphaFold Bridge Status:")
|
|
336
|
+
print(f" API Available: {status['api_available']}")
|
|
337
|
+
print(f" ColabFold Available: {status['colabfold_available']}")
|
|
338
|
+
print(f" Python: {status['python_version']}")
|
|
339
|
+
|
|
340
|
+
if not status['api_available']:
|
|
341
|
+
print("\n⚠ To use AlphaFold API, set environment variable:")
|
|
342
|
+
print(" export ALPHAFOLD_API_KEY=your_api_key")
|
|
343
|
+
if not status['colabfold_available']:
|
|
344
|
+
print("\n⚠ To use local prediction, install ColabFold:")
|
|
345
|
+
print(" pip install colabfold")
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DICOM Medical Imaging Bridge for HoloScript
|
|
3
|
+
Provides DICOM loading, windowing, and 3D volume extraction via JSON-RPC
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, List, Tuple
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import pydicom
|
|
13
|
+
import numpy as np
|
|
14
|
+
except ImportError:
|
|
15
|
+
print("ERROR: Missing dependencies. Install: pip install pydicom numpy", file=sys.stderr)
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DICOMBridge:
|
|
20
|
+
"""Python bridge for DICOM medical imaging operations"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.current_dataset = None
|
|
24
|
+
|
|
25
|
+
def load_dicom(self, file_path: str) -> Dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Load DICOM file and return metadata
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
file_path: Path to DICOM file (.dcm)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
DICOM metadata dictionary
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
self.current_dataset = pydicom.dcmread(file_path)
|
|
37
|
+
ds = self.current_dataset
|
|
38
|
+
|
|
39
|
+
metadata = {
|
|
40
|
+
'success': True,
|
|
41
|
+
'patientName': str(ds.get('PatientName', 'Unknown')),
|
|
42
|
+
'patientID': str(ds.get('PatientID', 'Unknown')),
|
|
43
|
+
'studyDate': str(ds.get('StudyDate', '')),
|
|
44
|
+
'modality': str(ds.get('Modality', 'Unknown')),
|
|
45
|
+
'sliceThickness': float(ds.get('SliceThickness', 1.0)),
|
|
46
|
+
'pixelSpacing': [
|
|
47
|
+
float(ds.PixelSpacing[0]) if hasattr(ds, 'PixelSpacing') else 1.0,
|
|
48
|
+
float(ds.PixelSpacing[1]) if hasattr(ds, 'PixelSpacing') else 1.0,
|
|
49
|
+
],
|
|
50
|
+
'rows': int(ds.Rows),
|
|
51
|
+
'columns': int(ds.Columns),
|
|
52
|
+
'windowCenter': float(ds.get('WindowCenter', 40)) if hasattr(ds, 'WindowCenter') else None,
|
|
53
|
+
'windowWidth': float(ds.get('WindowWidth', 400)) if hasattr(ds, 'WindowWidth') else None,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if hasattr(ds, 'NumberOfFrames'):
|
|
57
|
+
metadata['numberOfFrames'] = int(ds.NumberOfFrames)
|
|
58
|
+
|
|
59
|
+
return metadata
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
return {'success': False, 'error': str(e)}
|
|
63
|
+
|
|
64
|
+
def apply_window_level(self, center: float, width: float) -> Dict[str, Any]:
|
|
65
|
+
"""
|
|
66
|
+
Apply window/level to current DICOM dataset
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
center: Window center (Hounsfield units)
|
|
70
|
+
width: Window width
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Windowed image data as base64-encoded PNG
|
|
74
|
+
"""
|
|
75
|
+
if self.current_dataset is None:
|
|
76
|
+
return {'success': False, 'error': 'No DICOM loaded'}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Get pixel array
|
|
80
|
+
pixels = self.current_dataset.pixel_array
|
|
81
|
+
|
|
82
|
+
# Prevent division by zero for extreme contrast
|
|
83
|
+
width = max(1.0, float(width))
|
|
84
|
+
|
|
85
|
+
# Apply window/level transformation
|
|
86
|
+
lower = center - (width / 2)
|
|
87
|
+
upper = center + (width / 2)
|
|
88
|
+
windowed = np.clip(pixels, lower, upper)
|
|
89
|
+
|
|
90
|
+
# Normalize to 0-255 safely
|
|
91
|
+
window_range = upper - lower
|
|
92
|
+
windowed = ((windowed - lower) / window_range * 255.0)
|
|
93
|
+
|
|
94
|
+
# Ensure it is properly clipped before casting to uint8
|
|
95
|
+
windowed = np.clip(windowed, 0, 255).astype(np.uint8)
|
|
96
|
+
|
|
97
|
+
# Convert to list for JSON serialization
|
|
98
|
+
return {
|
|
99
|
+
'success': True,
|
|
100
|
+
'width': int(self.current_dataset.Columns),
|
|
101
|
+
'height': int(self.current_dataset.Rows),
|
|
102
|
+
'data': windowed.flatten().tolist(), # 1D array
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return {'success': False, 'error': str(e)}
|
|
107
|
+
|
|
108
|
+
def extract_3d_volume(self, series_path: str) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Extract 3D volume from DICOM series (multiple slice files)
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
series_path: Directory containing DICOM series
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
3D volume data with dimensions and spacing
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
series_dir = Path(series_path)
|
|
120
|
+
if not series_dir.is_dir():
|
|
121
|
+
return {'success': False, 'error': f'Not a directory: {series_path}'}
|
|
122
|
+
|
|
123
|
+
# Load all DICOM files in directory
|
|
124
|
+
dicom_files = sorted(series_dir.glob('*.dcm'))
|
|
125
|
+
if not dicom_files:
|
|
126
|
+
return {'success': False, 'error': 'No DICOM files found'}
|
|
127
|
+
|
|
128
|
+
# Read first file for metadata
|
|
129
|
+
first_ds = pydicom.dcmread(dicom_files[0])
|
|
130
|
+
rows = first_ds.Rows
|
|
131
|
+
columns = first_ds.Columns
|
|
132
|
+
num_slices = len(dicom_files)
|
|
133
|
+
|
|
134
|
+
# Get spacing
|
|
135
|
+
pixel_spacing = [float(x) for x in first_ds.PixelSpacing]
|
|
136
|
+
slice_thickness = float(first_ds.get('SliceThickness', 1.0))
|
|
137
|
+
|
|
138
|
+
# Allocate volume
|
|
139
|
+
volume = np.zeros((num_slices, rows, columns), dtype=np.int16)
|
|
140
|
+
|
|
141
|
+
# Load all slices
|
|
142
|
+
for i, file_path in enumerate(dicom_files):
|
|
143
|
+
ds = pydicom.dcmread(file_path)
|
|
144
|
+
volume[i] = ds.pixel_array
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
'success': True,
|
|
148
|
+
'dimensions': [num_slices, rows, columns],
|
|
149
|
+
'spacing': [slice_thickness, pixel_spacing[0], pixel_spacing[1]],
|
|
150
|
+
'origin': [0, 0, 0], # Can extract from ImagePositionPatient if available
|
|
151
|
+
'min': int(volume.min()),
|
|
152
|
+
'max': int(volume.max()),
|
|
153
|
+
# Note: Returning full volume data would be too large for JSON
|
|
154
|
+
# In production, save to file or stream binary data
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
return {'success': False, 'error': str(e)}
|
|
159
|
+
|
|
160
|
+
def dicom_to_mesh(self, threshold: float, smoothing: bool = True) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Convert current DICOM to 3D mesh using marching cubes
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
threshold: Hounsfield threshold for surface extraction
|
|
166
|
+
smoothing: Apply mesh smoothing
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Mesh vertices and faces
|
|
170
|
+
|
|
171
|
+
Note: Requires scikit-image for marching cubes algorithm
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
from skimage import measure
|
|
175
|
+
except ImportError:
|
|
176
|
+
return {
|
|
177
|
+
'success': False,
|
|
178
|
+
'error': 'scikit-image required: pip install scikit-image'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if self.current_dataset is None:
|
|
182
|
+
return {'success': False, 'error': 'No DICOM loaded'}
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
# For single-slice DICOM, this would need 3D volume
|
|
186
|
+
# Simplified example for demo
|
|
187
|
+
pixels = self.current_dataset.pixel_array
|
|
188
|
+
|
|
189
|
+
# Marching cubes requires 3D volume
|
|
190
|
+
# In practice, use extract_3d_volume first
|
|
191
|
+
return {
|
|
192
|
+
'success': False,
|
|
193
|
+
'error': 'Mesh extraction requires 3D volume (DICOM series)'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return {'success': False, 'error': str(e)}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# JSON-RPC server (simple stdin/stdout protocol)
|
|
201
|
+
def main():
|
|
202
|
+
bridge = DICOMBridge()
|
|
203
|
+
|
|
204
|
+
print("DICOM Bridge Ready", file=sys.stderr)
|
|
205
|
+
|
|
206
|
+
for line in sys.stdin:
|
|
207
|
+
try:
|
|
208
|
+
request = json.loads(line.strip())
|
|
209
|
+
method = request.get('method')
|
|
210
|
+
params = request.get('params', {})
|
|
211
|
+
|
|
212
|
+
if method == 'loadDICOM':
|
|
213
|
+
result = bridge.load_dicom(params['filePath'])
|
|
214
|
+
elif method == 'applyWindowLevel':
|
|
215
|
+
result = bridge.apply_window_level(params['center'], params['width'])
|
|
216
|
+
elif method == 'extract3DVolume':
|
|
217
|
+
result = bridge.extract3d_volume(params['seriesPath'])
|
|
218
|
+
elif method == 'dicomToMesh':
|
|
219
|
+
result = bridge.dicom_to_mesh(
|
|
220
|
+
params['threshold'],
|
|
221
|
+
params.get('smoothing', True)
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
result = {'success': False, 'error': f'Unknown method: {method}'}
|
|
225
|
+
|
|
226
|
+
print(json.dumps(result))
|
|
227
|
+
sys.stdout.flush()
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
error_result = {'success': False, 'error': str(e)}
|
|
231
|
+
print(json.dumps(error_result))
|
|
232
|
+
sys.stdout.flush()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == '__main__':
|
|
236
|
+
main()
|