holoscript 5.3.1__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.
Files changed (40) hide show
  1. holoscript-6.0.5/PKG-INFO +41 -0
  2. holoscript-6.0.5/README.md +6 -0
  3. holoscript-6.0.5/holoscript/__init__.py +53 -0
  4. holoscript-6.0.5/holoscript/bridges/__init__.py +11 -0
  5. holoscript-6.0.5/holoscript/bridges/alphafold.py +345 -0
  6. holoscript-6.0.5/holoscript/bridges/medical.py +236 -0
  7. holoscript-6.0.5/holoscript/bridges/narupa.py +376 -0
  8. holoscript-6.0.5/holoscript/bridges/radio_astronomy.py +45 -0
  9. holoscript-6.0.5/holoscript/bridges/robotics.py +77 -0
  10. holoscript-6.0.5/holoscript/bridges/scientific.py +213 -0
  11. holoscript-6.0.5/holoscript.egg-info/PKG-INFO +41 -0
  12. holoscript-6.0.5/holoscript.egg-info/SOURCES.txt +16 -0
  13. holoscript-6.0.5/holoscript.egg-info/dependency_links.txt +1 -0
  14. holoscript-6.0.5/holoscript.egg-info/requires.txt +23 -0
  15. holoscript-6.0.5/holoscript.egg-info/top_level.txt +1 -0
  16. holoscript-6.0.5/pyproject.toml +64 -0
  17. holoscript-6.0.5/setup.cfg +4 -0
  18. holoscript-6.0.5/tests/test_smoke.py +18 -0
  19. holoscript-5.3.1/.gitignore +0 -263
  20. holoscript-5.3.1/PKG-INFO +0 -117
  21. holoscript-5.3.1/README.md +0 -80
  22. holoscript-5.3.1/examples/holoscript_tutorial.ipynb +0 -452
  23. holoscript-5.3.1/holoscript/__init__.py +0 -66
  24. holoscript-5.3.1/holoscript/client.py +0 -266
  25. holoscript-5.3.1/holoscript/generator.py +0 -417
  26. holoscript-5.3.1/holoscript/parser.py +0 -243
  27. holoscript-5.3.1/holoscript/renderer.py +0 -132
  28. holoscript-5.3.1/holoscript/robotics.py +0 -494
  29. holoscript-5.3.1/holoscript/sharer.py +0 -134
  30. holoscript-5.3.1/holoscript/traits.py +0 -284
  31. holoscript-5.3.1/holoscript/validator.py +0 -244
  32. holoscript-5.3.1/pyproject.toml +0 -68
  33. holoscript-5.3.1/tests/__init__.py +0 -3
  34. holoscript-5.3.1/tests/conftest.py +0 -10
  35. holoscript-5.3.1/tests/test_init.py +0 -65
  36. holoscript-5.3.1/tests/test_parser.py +0 -259
  37. holoscript-5.3.1/tests/test_pipeline_e2e.py +0 -89
  38. holoscript-5.3.1/tests/test_robotics.py +0 -408
  39. holoscript-5.3.1/tests/test_traits.py +0 -185
  40. holoscript-5.3.1/tests/test_validator.py +0 -153
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: holoscript
3
+ Version: 6.0.5
4
+ Summary: Python bindings for HoloScript — parsing, validation, and domain bridges for scientific computing
5
+ Author: Brian X Base Team
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
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"
35
+
36
+ # holoscript (Python bindings)
37
+
38
+ Python bindings for core HoloScript operations.
39
+
40
+ This package keeps a local development version (`6.0.0.dev0`) in source.
41
+ Release versions are injected by CI from git tags (for example `v6.1.0` -> `6.1.0`).
@@ -0,0 +1,6 @@
1
+ # holoscript (Python bindings)
2
+
3
+ Python bindings for core HoloScript operations.
4
+
5
+ This package keeps a local development version (`6.0.0.dev0`) in source.
6
+ Release versions are injected by CI from git tags (for example `v6.1.0` -> `6.1.0`).
@@ -0,0 +1,53 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List
3
+
4
+ # Release version injected by CI from git tag. Dev version for local use.
5
+ __version__ = "6.0.5"
6
+
7
+
8
+ @dataclass
9
+ class ParseResult:
10
+ success: bool
11
+ ast: Dict[str, str]
12
+ errors: List[str] = field(default_factory=list)
13
+ warnings: List[str] = field(default_factory=list)
14
+ format: str = "holo"
15
+
16
+
17
+ @dataclass
18
+ class ValidationResult:
19
+ valid: bool
20
+ errors: List[str] = field(default_factory=list)
21
+ warnings: List[str] = field(default_factory=list)
22
+
23
+
24
+ def parse(code: str) -> ParseResult:
25
+ stripped = code.strip()
26
+ if not stripped:
27
+ return ParseResult(success=False, ast={}, errors=["Input is empty"])
28
+
29
+ return ParseResult(
30
+ success=True,
31
+ ast={"type": "composition", "source": stripped},
32
+ )
33
+
34
+
35
+ def validate(code: str) -> ValidationResult:
36
+ if not code.strip():
37
+ return ValidationResult(valid=False, errors=["Input is empty"])
38
+
39
+ return ValidationResult(valid=True)
40
+
41
+
42
+ def list_traits() -> List[str]:
43
+ return ["@grabbable", "@physics", "@clickable", "@color", "@position"]
44
+
45
+
46
+ __all__ = [
47
+ "__version__",
48
+ "ParseResult",
49
+ "ValidationResult",
50
+ "parse",
51
+ "validate",
52
+ "list_traits",
53
+ ]
@@ -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()