alphanso 1.0.0__py3-none-any.whl

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.
alphanso/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ # Alphanso package initialization
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from .transport import Transport
6
+ from .data_manager import ensure_data, get_data_dir, is_data_available, DATA_VERSION
7
+
8
+ __all__ = ['Transport', '__version__', 'ensure_data', 'get_data_dir',
9
+ 'is_data_available', 'DATA_VERSION']
alphanso/__main__.py ADDED
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Main entry point for the alphanso package.
4
+
5
+ Usage:
6
+ alphanso <config_path> [output_dir] # backwards-compatible (implicit run)
7
+ alphanso run <config_path> [output_dir] # explicit run
8
+ alphanso download-data [--dir PATH] # pre-download data
9
+ alphanso data-info # show data paths and status
10
+ """
11
+
12
+ import argparse
13
+ import logging
14
+ import os
15
+ import sys
16
+ import yaml
17
+ import numpy as np
18
+ from pathlib import Path
19
+ from typing import Dict, List, Union, Any
20
+ from collections import defaultdict
21
+
22
+ from .transport import Transport
23
+ from .data_manager import ensure_data, get_data_info
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _SUBCOMMANDS = {"run", "download-data", "data-info"}
28
+
29
+
30
+ def read_in(config_path: Union[str, Path]) -> List[Dict[str, Any]]:
31
+ """
32
+ Read configuration from a YAML file or directory of YAML files.
33
+
34
+ Args:
35
+ config_path: Path to YAML file or directory containing YAML files
36
+
37
+ Returns:
38
+ List of configuration dictionaries, each with a 'source' key indicating the source file
39
+
40
+ Raises:
41
+ FileNotFoundError: If config_path doesn't exist
42
+ yaml.YAMLError: If YAML parsing fails
43
+ ValueError: If no valid YAML files found
44
+ """
45
+ config_path = Path(config_path)
46
+
47
+ if not config_path.exists():
48
+ raise FileNotFoundError(
49
+ f"Configuration path does not exist: {config_path}")
50
+
51
+ configs = []
52
+
53
+ if config_path.is_file():
54
+ try:
55
+ with open(config_path, 'r') as f:
56
+ yaml_content = yaml.safe_load(f)
57
+
58
+ if yaml_content is None:
59
+ raise ValueError(f"Empty YAML file: {config_path}")
60
+
61
+ if isinstance(yaml_content, list):
62
+ for i, config in enumerate(yaml_content):
63
+ if isinstance(config, dict):
64
+ config['source'] = f"{config_path.stem}_{i+1}"
65
+ configs.append(config)
66
+ elif isinstance(yaml_content, dict):
67
+ yaml_content['source'] = config_path.stem
68
+ configs.append(yaml_content)
69
+ else:
70
+ raise ValueError(
71
+ f"Invalid YAML content in {config_path}: expected dict or list of dicts")
72
+
73
+ except yaml.YAMLError as e:
74
+ raise yaml.YAMLError(
75
+ f"Failed to parse YAML file {config_path}: {e}")
76
+
77
+ elif config_path.is_dir():
78
+ yaml_files = list(config_path.glob("*.yaml")) + \
79
+ list(config_path.glob("*.yml"))
80
+
81
+ if not yaml_files:
82
+ raise ValueError(
83
+ f"No YAML files found in directory: {config_path}")
84
+
85
+ for yaml_file in sorted(yaml_files):
86
+ try:
87
+ with open(yaml_file, 'r') as f:
88
+ yaml_content = yaml.safe_load(f)
89
+
90
+ if yaml_content is None:
91
+ print(f"Warning: Empty YAML file: {yaml_file}")
92
+ continue
93
+
94
+ if isinstance(yaml_content, list):
95
+ for i, config in enumerate(yaml_content):
96
+ if isinstance(config, dict):
97
+ config['source'] = f"{yaml_file.stem}_{i+1}"
98
+ configs.append(config)
99
+ elif isinstance(yaml_content, dict):
100
+ yaml_content['source'] = yaml_file.stem
101
+ configs.append(yaml_content)
102
+ else:
103
+ print(
104
+ f"Warning: Invalid YAML content in {yaml_file}: expected dict or list of dicts")
105
+ continue
106
+
107
+ except yaml.YAMLError as e:
108
+ print(f"Warning: Failed to parse YAML file {yaml_file}: {e}")
109
+ continue
110
+
111
+ else:
112
+ raise ValueError(
113
+ f"Invalid config path: {config_path} is neither a file nor directory")
114
+
115
+ if not configs:
116
+ raise ValueError(f"No valid configurations found in {config_path}")
117
+
118
+ print(f"Loaded {len(configs)} configuration(s) from {config_path}")
119
+ return configs
120
+
121
+
122
+ def read_out(configs: List[Dict[str, Any]],
123
+ output_dir: Union[str, Path] = None) -> Path:
124
+ """
125
+ Set up output directory structure for the configurations.
126
+
127
+ Args:
128
+ configs: List of configuration dictionaries
129
+ output_dir: Optional output directory path (default: creates alphanso_output/)
130
+
131
+ Returns:
132
+ Path to the created output directory
133
+
134
+ Raises:
135
+ OSError: If output directory creation fails
136
+ """
137
+ if output_dir is None:
138
+ output_dir = Path("alphanso_output")
139
+ else:
140
+ output_dir = Path(output_dir)
141
+
142
+ output_dir.mkdir(parents=True, exist_ok=True)
143
+
144
+ source_groups = defaultdict(list)
145
+ for config in configs:
146
+ source = config.get('source', 'unknown')
147
+ source_groups[source].append(config)
148
+
149
+ for source, config_list in source_groups.items():
150
+ source_dir = output_dir / source
151
+ source_dir.mkdir(exist_ok=True)
152
+
153
+ if len(config_list) == 1:
154
+ output_file = source_dir / "output.yaml"
155
+ with open(output_file, 'w') as f:
156
+ yaml.dump(config_list[0], f, default_flow_style=True, indent=2)
157
+
158
+ if '_result' in config_list[0]:
159
+ results_file = source_dir / "results.yaml"
160
+ with open(results_file, 'w') as f:
161
+ yaml.dump(
162
+ config_list[0]['_result'],
163
+ f,
164
+ default_flow_style=True,
165
+ indent=2)
166
+ else:
167
+ for i, config in enumerate(config_list):
168
+ output_file = source_dir / f"output_{i+1}.yaml"
169
+ with open(output_file, 'w') as f:
170
+ yaml.dump(config, f, default_flow_style=True, indent=2)
171
+
172
+ if '_result' in config:
173
+ results_file = source_dir / f"results_{i+1}.yaml"
174
+ with open(results_file, 'w') as f:
175
+ yaml.dump(
176
+ config['_result'],
177
+ f,
178
+ default_flow_style=True,
179
+ indent=2)
180
+
181
+ print(f"Created output directory structure: {output_dir}")
182
+ return output_dir
183
+
184
+
185
+ def _cmd_run(config_path, output_dir):
186
+ """Execute the run subcommand (calculation)."""
187
+ ensure_data()
188
+
189
+ configs = read_in(config_path)
190
+
191
+ for i, config in enumerate(configs):
192
+ source = config.get('source', f'config_{i+1}')
193
+ config_id = config.get('id', config.get('name', f'config_{i+1}'))
194
+
195
+ print(
196
+ f"\nProcessing configuration {i+1}/{len(configs)}: {config_id}")
197
+
198
+ try:
199
+ required_fields = ['calc_type']
200
+ missing_fields = [
201
+ field for field in required_fields if field not in config]
202
+
203
+ if missing_fields:
204
+ raise ValueError(
205
+ f"Configuration {config_id} missing required fields: {missing_fields}")
206
+
207
+ calc_type = config.get('calc_type')
208
+ if calc_type not in [
209
+ 'beam',
210
+ 'homogeneous',
211
+ 'interface',
212
+ 'sandwich']:
213
+ raise ValueError(
214
+ f"Configuration {config_id} has invalid calc_type: {calc_type}. Must be 'beam', 'homogeneous', 'interface', or 'sandwich'")
215
+
216
+ if calc_type == 'beam':
217
+ if 'beam_energy' not in config and 'beam_intensities' not in config:
218
+ raise ValueError(
219
+ f"Beam calculation {config_id} missing required field: beam_energy or beam_intensities")
220
+ elif calc_type == 'interface':
221
+ if 'source_matdef' not in config or 'target_matdef' not in config:
222
+ raise ValueError(
223
+ f"Interface calculation {config_id} missing required field: source_matdef or target_matdef")
224
+ elif calc_type == 'sandwich':
225
+ required = [
226
+ 'source_matdef',
227
+ 'source_density',
228
+ 'target_matdef']
229
+ missing = [f for f in required if f not in config]
230
+ if missing:
231
+ raise ValueError(
232
+ f"Sandwich calculation {config_id} missing required fields: {missing}")
233
+
234
+ has_layers_list = 'intermediate_layers' in config
235
+ has_single_layer = all(
236
+ k in config for k in [
237
+ 'intermediate_matdef',
238
+ 'intermediate_density',
239
+ 'intermediate_thickness'])
240
+
241
+ if not (has_layers_list or has_single_layer):
242
+ raise ValueError(
243
+ f"Sandwich calculation {config_id} requires either 'intermediate_layers' (list) "
244
+ "or 'intermediate_matdef', 'intermediate_density', 'intermediate_thickness' (single layer)")
245
+
246
+ if has_single_layer and not has_layers_list:
247
+ config['intermediate_layers'] = [{
248
+ 'matdef': config.pop('intermediate_matdef'),
249
+ 'density': config.pop('intermediate_density'),
250
+ 'thickness': config.pop('intermediate_thickness'),
251
+ 'include_targets': config.get('include_intermediate_targets', True)
252
+ }]
253
+
254
+ for j, layer in enumerate(config['intermediate_layers']):
255
+ if 'matdef' not in layer or 'density' not in layer or 'thickness' not in layer:
256
+ raise ValueError(
257
+ f"Sandwich calculation {config_id} layer {j} missing required fields: matdef, density, thickness")
258
+ if layer['thickness'] <= 0:
259
+ raise ValueError(
260
+ f"Sandwich calculation {config_id} layer {j} thickness must be positive, got {layer['thickness']}")
261
+ if layer['density'] <= 0:
262
+ raise ValueError(
263
+ f"Sandwich calculation {config_id} layer {j} density must be positive, got {layer['density']}")
264
+
265
+ result = Transport.calculate(config)
266
+ logger.info(f"Completed {config_id}")
267
+
268
+ config['_result'] = result
269
+
270
+ except Exception as e:
271
+ print(f"Error processing {config_id}: {e}")
272
+ continue
273
+
274
+ output_dir_path = read_out(configs, output_dir)
275
+ print(f"Output saved to: {output_dir_path}")
276
+
277
+
278
+ def _cmd_download_data(target_dir):
279
+ """Execute the download-data subcommand."""
280
+ if target_dir:
281
+ os.environ["ALPHANSO_DATA_DIR"] = target_dir
282
+ data_path = ensure_data()
283
+ print(f"Nuclear data ready at: {data_path}")
284
+
285
+
286
+ def _cmd_data_info():
287
+ """Execute the data-info subcommand."""
288
+ info = get_data_info()
289
+ print("ALPHANSO Data Info")
290
+ print("=" * 40)
291
+ for key, value in info.items():
292
+ print(f" {key}: {value}")
293
+
294
+
295
+ def main():
296
+ """Main entry point for the alphanso package."""
297
+ # Backwards compatibility: if the first positional arg is not a known
298
+ # subcommand, treat the entire invocation as an implicit "run".
299
+ raw_args = sys.argv[1:]
300
+ if raw_args and raw_args[0] not in _SUBCOMMANDS and not raw_args[0].startswith("-"):
301
+ # Implicit run: alphanso config.yaml [output_dir]
302
+ raw_args = ["run"] + raw_args
303
+
304
+ parser = argparse.ArgumentParser(
305
+ description="ALPHANSO: Alpha-Neutron Source calculations",
306
+ formatter_class=argparse.RawDescriptionHelpFormatter,
307
+ epilog="""
308
+ Examples:
309
+ alphanso config.yaml # Run with config (implicit run)
310
+ alphanso run config.yaml # Explicit run
311
+ alphanso run config.yaml results/ # Run with custom output dir
312
+ alphanso download-data # Pre-download nuclear data
313
+ alphanso download-data --dir /custom/path # Download to custom location
314
+ alphanso data-info # Show data paths and status
315
+ """
316
+ )
317
+
318
+ subparsers = parser.add_subparsers(dest="command")
319
+
320
+ # run subcommand
321
+ run_parser = subparsers.add_parser(
322
+ "run", help="Run ALPHANSO calculation(s)")
323
+ run_parser.add_argument(
324
+ "config_path",
325
+ help="Path to YAML config file or directory containing YAML files"
326
+ )
327
+ run_parser.add_argument(
328
+ "output_dir",
329
+ nargs="?",
330
+ help="Output directory (default: alphanso_output/)"
331
+ )
332
+
333
+ # download-data subcommand
334
+ dl_parser = subparsers.add_parser(
335
+ "download-data", help="Download nuclear data files")
336
+ dl_parser.add_argument(
337
+ "--dir",
338
+ dest="target_dir",
339
+ default=None,
340
+ help="Target directory for data download"
341
+ )
342
+
343
+ # data-info subcommand
344
+ subparsers.add_parser(
345
+ "data-info", help="Show nuclear data paths and status")
346
+
347
+ args = parser.parse_args(raw_args)
348
+
349
+ if args.command is None:
350
+ parser.print_help()
351
+ sys.exit(1)
352
+
353
+ try:
354
+ if args.command == "run":
355
+ _cmd_run(args.config_path, args.output_dir)
356
+ elif args.command == "download-data":
357
+ _cmd_download_data(args.target_dir)
358
+ elif args.command == "data-info":
359
+ _cmd_data_info()
360
+ except Exception as e:
361
+ print(f"Fatal error: {e}")
362
+ sys.exit(1)
363
+
364
+
365
+ if __name__ == "__main__":
366
+ main()