Peano
Loading...
Searching...
No Matches
CustomGridGenerator.py
Go to the documentation of this file.
1# This file is part of the ExaHyPE2 project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3
4import os
5import yaml
6import numpy as np
7import re
8
9from ..Tree import Tree
10
11"""
12Custom Grid Generator for Peano Load Balancing API
13
14This module provides functionality to create tree structures from YAML files generated by
15the `generate_tree_yaml()` function. It serves as an input generator that enables reading
16previously saved tree structures for analysis, modification, or further processing.
17
18## Purpose and Context
19
20The CustomGridGenerator is designed to complete the round-trip workflow of the offline
21load balancing API. Instead of starting from scratch or reading complex grid files, this
22module enables:
23
241. **Tree Reconstruction**: Read previously saved tree structures from YAML files
252. **Workflow Continuity**: Continue analysis from previous splitting results
263. **Configuration Management**: Load pre-defined tree configurations for testing
274. **Result Verification**: Re-import trees for validation and comparison studies
28
29## Integration with Load Balancing API
30
31This module integrates with other components of the load balancing API:
32
33- **Tree Class**: Reconstructs Tree objects from YAML data (see Tree.py)
34- **OfflineDecomposition**: Can be integrated into the main script for YAML-based input
35- **SplitTrees**: Provides tree data for further splitting strategy application
36- **TreeVisualizer**: Enables visualization of reconstructed tree structures
37
38## YAML Format Support
39
40The reader supports YAML files generated by `generate_tree_yaml()` with the following structure:
41
42- **Tree Metadata**: Name, dimensions, domain boundaries, and leaf counts
43- **Split Summary**: Distribution statistics across subtrees
44- **Tree Structure**: Complete spatial arrays for each refinement level with (weight, subtree_id) tuples
45
46## Workflow Integration
47
48This module fits into extended offline decomposition workflows:
49
501. **Initial Processing**: Original mesh construction and optimization
512. **YAML Export**: Tree structure saved using `generate_tree_yaml()`
523. **Configuration Storage**: YAML files stored for later use
534. **Tree Reconstruction**: CustomGridGenerator recreates Tree objects
545. **Further Analysis**: Additional splitting, visualization, or modification
556. **Iterative Optimization**: Multiple rounds of analysis and refinement
56
57## Usage Examples
58
59```python
60# Read from existing YAML file
61tree = CustomGridGenerator("MyCustomTree", "path/to/tree_structure.yaml")
62
63# Apply additional splitting
64from strategies.SplitTrees import split_tree
65split_tree(tree, number_of_subtrees=2)
66
67# Compare with original structure
68from visualize.TreeVisualizer import TreeVisualizer
69TreeVisualizer(tree, dimensions=tree._dimension, filename="extended_splits")
70```
71
72## Comparison with Other Input Methods
73
74| Aspect | CustomGridGenerator | RegularGridGenerator | GridPatchFileReader |
75|--------|---------------------|----------------------|-------------------|
76| **Data Source** | YAML configuration files | Artificially generated | Real simulation checkpoints |
77| **Structure** | Previously processed trees | Uniform regular grids | Adaptive mesh refinement |
78| **Flexibility** | User-defined configurations | Predictable patterns | Simulation-dependent |
79| **Use Case** | Configuration management | Testing and benchmarking | Real-world optimization |
80
81## Integration Potential
82
83The function can be integrated into the main `OfflineDecomposition.py` script by adding:
84
85```bash
86# Process existing YAML configuration
87python3 OfflineDecomposition.py -y /path/to/config.yaml -p
88
89# Apply additional splits to loaded configuration
90python3 OfflineDecomposition.py -y /path/to/config.yaml -s 2 -p
91```
92
93## File Format Validation
94
95The function includes comprehensive validation for:
96- YAML syntax and structure
97- Required metadata fields
98- Tuple format consistency
99- Dimension compatibility
100- Level structure integrity
101
102See the load balancing API documentation for complete workflow integration details.
103"""
104
105def parse_tuple_string(tuple_str):
106 """
107 Parse a string representation of a (weight, subtree_id) tuple.
108
109 Supports various formats:
110 - "(1,0)" - standard format
111 - "(1, 0)" - with spaces
112 - "1,0" - without parentheses
113
114 Args:
115 tuple_str (str): String representation of the tuple
116
117 Returns:
118 tuple: (weight, subtree_id) as integers
119
120 Raises:
121 ValueError: If the string cannot be parsed as a valid tuple
122 """
123 # Remove parentheses and whitespace, then split by comma
124 clean_str = tuple_str.strip().strip('()').replace(' ', '')
125
126 try:
127 parts = clean_str.split(',')
128 if len(parts) != 2:
129 raise ValueError(f"Expected 2 values, got {len(parts)}")
130
131 weight = int(parts[0])
132 subtree_id = int(parts[1])
133
134 return (weight, subtree_id)
135
136 except (ValueError, IndexError) as e:
137 raise ValueError(f"Cannot parse tuple from '{tuple_str}': {e}")
138
139def CustomGridGenerator(name, yaml_file_path):
140 """
141 Generate a Tree object by reading from a YAML file created by generate_tree_yaml().
142
143 This function reconstructs the complete tree structure from a YAML file, including
144 all spatial data, metadata, and subtree assignments. It provides a way to reload
145 previously processed trees for further analysis or modification.
146
147 ## YAML Structure Processing
148
149 The function expects YAML files with the structure created by `generate_tree_yaml()`:
150
151 ```yaml
152 title: "Complete Tree Structure from [source]"
153 description: "Spatial representation of tree with (weight, subtree_id) tuples"
154 version: "2.0"
155 tree_info:
156 name: [tree_name]
157 dimension: [2 or 3]
158 max_level: [number_of_levels]
159 domain_offset: [x, y] or [x, y, z]
160 domain_size: [width, height] or [width, height, depth]
161 total_leaf_count: [total_leaves]
162 subtree_count: [number_of_subtrees]
163 split_summary:
164 subtree_distribution: [count_per_subtree]
165 tree_structure:
166 - level: 1
167 size: 3
168 data:
169 0: "(1,0), (1,0), (1,1)"
170 ...
171 ```
172
173 ## Data Reconstruction
174
175 The function rebuilds the tree arrays by:
176 1. Parsing metadata to determine tree structure
177 2. Creating numpy arrays for each refinement level
178 3. Parsing tuple strings to extract (weight, subtree_id) values
179 4. Populating arrays according to spatial indexing
180 5. Updating tree statistics and metadata
181
182 ## Error Handling
183
184 Comprehensive validation includes:
185 - File existence and readability
186 - YAML syntax validation
187 - Required field presence
188 - Data type consistency
189 - Tuple format validation
190 - Dimensional compatibility
191
192 Args:
193 name (str): Descriptive name for the loaded tree (overrides YAML name if desired)
194 yaml_file_path (str): Path to the YAML file containing tree structure
195
196 Returns:
197 Tree: Reconstructed Tree object with complete structure
198
199 Raises:
200 FileNotFoundError: If YAML file doesn't exist or isn't readable
201 yaml.YAMLError: If YAML file has syntax errors
202 KeyError: If required fields are missing from YAML structure
203 ValueError: If data formats are inconsistent or invalid
204
205 Example:
206 ```python
207 # Load a previously saved tree structure
208 tree = CustomGridGenerator(
209 name="ReloadedTree",
210 yaml_file_path="TreeStructure_l4_s3_split.yaml"
211 )
212
213 print(f"Loaded tree: {tree._name}")
214 print(f"Dimensions: {tree._dimension}D")
215 print(f"Levels: {tree._max_level}")
216 print(f"Leaf count: {tree._leaf_count}")
217 print(f"Subtrees: {tree._subtree_count}")
218
219 # Apply additional processing
220 from strategies.SplitTrees import split_tree
221 split_tree(tree, number_of_subtrees=2)
222 ```
223
224 Note:
225 - The function preserves all original spatial relationships and metadata
226 - Tree name can be overridden with the name parameter
227 - Large YAML files may require significant memory during reconstruction
228 - The reconstructed tree is functionally identical to the original
229 """
230
231 print(f"Loading custom tree '{name}' from YAML file: {yaml_file_path}")
232
233 # Validate file existence
234 if not os.path.exists(yaml_file_path):
235 raise FileNotFoundError(f"YAML file not found: {yaml_file_path}")
236
237 if not os.path.isfile(yaml_file_path):
238 raise ValueError(f"Path is not a file: {yaml_file_path}")
239
240 # Load and parse YAML file
241 try:
242 with open(yaml_file_path, 'r') as file:
243 yaml_data = yaml.safe_load(file)
244 print(f"Successfully loaded YAML file")
245 except yaml.YAMLError as e:
246 raise yaml.YAMLError(f"Error parsing YAML file: {e}")
247 except IOError as e:
248 raise IOError(f"Error reading YAML file: {e}")
249
250 # Validate required YAML structure
251 required_fields = ['tree_info', 'tree_structure']
252 for field in required_fields:
253 if field not in yaml_data:
254 raise KeyError(f"Required field '{field}' missing from YAML file")
255
256 # Extract tree metadata
257 tree_info = yaml_data['tree_info']
258 required_tree_fields = ['dimension', 'max_level', 'domain_offset', 'domain_size']
259 for field in required_tree_fields:
260 if field not in tree_info:
261 raise KeyError(f"Required tree_info field '{field}' missing from YAML file")
262
263 dimension = tree_info['dimension']
264 max_level = tree_info['max_level']
265 domain_offset = tree_info['domain_offset']
266 domain_size = tree_info['domain_size']
267
268 # Validate dimension
269 if dimension not in [2, 3]:
270 raise ValueError(f"Invalid dimension: {dimension}. Must be 2 or 3")
271
272 # Validate domain information
273 expected_coords = dimension
274 if len(domain_offset) != expected_coords or len(domain_size) != expected_coords:
275 raise ValueError(f"Domain offset and size must have {expected_coords} coordinates for {dimension}D")
276
277 print(f" Dimension: {dimension}D")
278 print(f" Max level: {max_level}")
279 print(f" Domain offset: {domain_offset}")
280 print(f" Domain size: {domain_size}")
281
282 # Create Tree object
283 peano_tree = Tree(name, dimension, domain_offset, domain_size)
284
285 # Process tree structure levels
286 tree_structure = yaml_data['tree_structure']
287
288 if len(tree_structure) != max_level:
289 raise ValueError(f"Expected {max_level} levels, got {len(tree_structure)} in tree_structure")
290
291 # Initialize arrays for all levels
292 artificial_arrays = []
293
294 for level_data in tree_structure:
295 # Validate level structure
296 if 'level' not in level_data or 'size' not in level_data or 'data' not in level_data:
297 raise KeyError("Each level must have 'level', 'size', and 'data' fields")
298
299 level_num = level_data['level']
300 size = level_data['size']
301 data = level_data['data']
302
303 # Validate level number and size
304 expected_level = len(artificial_arrays) + 1
305 expected_size = 3 ** expected_level
306
307 if level_num != expected_level:
308 print(f"Warning: Level number {level_num} doesn't match expected {expected_level}")
309
310 if size != expected_size:
311 raise ValueError(f"Level {level_num}: expected size {expected_size}, got {size}")
312
313 print(f" Processing level {level_num}: {size}^{dimension} cells")
314
315 # Create array for this level
316 if dimension == 2:
317 array = np.empty((size, size), dtype=object)
318
319 # Parse 2D data
320 for row_idx in range(size):
321 if row_idx not in data:
322 raise KeyError(f"Level {level_num}: missing row {row_idx}")
323
324 row_str = data[row_idx]
325 # Split by ", " to separate complete tuples
326 tuple_strings = [s.strip() for s in row_str.split(', ')]
327
328 if len(tuple_strings) != size:
329 raise ValueError(f"Level {level_num}, row {row_idx}: expected {size} tuples, got {len(tuple_strings)}")
330
331 for col_idx, tuple_str in enumerate(tuple_strings):
332 try:
333 weight, subtree_id = parse_tuple_string(tuple_str)
334 array[row_idx][col_idx] = (weight, subtree_id)
335 except ValueError as e:
336 raise ValueError(f"Level {level_num}, row {row_idx}, col {col_idx}: {e}")
337
338 elif dimension == 3:
339 array = np.empty((size, size, size), dtype=object)
340
341 # Parse 3D data (planes and rows)
342 # YAML structure: data[plane_a][row_b] = "tuple_c0, tuple_c1, tuple_c2"
343 # Array structure: array[a][b][c] = (weight, subtree_id)
344 for plane_idx in range(size):
345 if plane_idx not in data:
346 raise KeyError(f"Level {level_num}: missing plane {plane_idx}")
347
348 plane_data = data[plane_idx]
349 if not isinstance(plane_data, dict):
350 raise ValueError(f"Level {level_num}, plane {plane_idx}: expected dictionary, got {type(plane_data)}")
351
352 for row_idx in range(size):
353 if row_idx not in plane_data:
354 raise KeyError(f"Level {level_num}, plane {plane_idx}: missing row {row_idx}")
355
356 row_str = plane_data[row_idx]
357 # Split by ", " to separate complete tuples
358 tuple_strings = [s.strip() for s in row_str.split(', ')]
359
360 if len(tuple_strings) != size:
361 raise ValueError(f"Level {level_num}, plane {plane_idx}, row {row_idx}: expected {size} tuples, got {len(tuple_strings)}")
362
363 for col_idx, tuple_str in enumerate(tuple_strings):
364 try:
365 weight, subtree_id = parse_tuple_string(tuple_str)
366 # Map YAML indices to array indices: data[a][b] -> array[a][b][c]
367 array[plane_idx][row_idx][col_idx] = (weight, subtree_id)
368 except ValueError as e:
369 raise ValueError(f"Level {level_num}, plane {plane_idx}, row {row_idx}, col {col_idx}: {e}")
370
371 artificial_arrays.append(array)
372
373 # Determine the actual subtree count from the data before populating tree
374 max_subtree_id = 0
375 for array in artificial_arrays:
376 if dimension == 2:
377 for i in range(array.shape[0]):
378 for j in range(array.shape[1]):
379 weight, subtree_id = array[i][j]
380 if weight > 0: # Only count existing cells
381 max_subtree_id = max(max_subtree_id, subtree_id)
382 elif dimension == 3:
383 for i in range(array.shape[0]):
384 for j in range(array.shape[1]):
385 for k in range(array.shape[2]):
386 weight, subtree_id = array[i][j][k]
387 if weight > 0: # Only count existing cells
388 max_subtree_id = max(max_subtree_id, subtree_id)
389
390 # Set the correct subtree count and max level first
391 peano_tree._subtree_count = max_subtree_id + 1
392 peano_tree._max_level = max_level
393
394 # Directly set the tree arrays and update metadata manually
395 # (avoiding read_in_tree_from_artificial which hardcodes subtree_count = 1)
396 peano_tree._tree_arrays = artificial_arrays
397 peano_tree.update_split_pattern()
398
399 if False: # weighted is False for our case
400 peano_tree.update_weight()
401
402 print(f"Custom tree generation completed successfully")
403 print(f" Tree: {peano_tree._name}")
404 print(f" Leaf count: {peano_tree._leaf_count}")
405 print(f" Subtree count: {peano_tree._subtree_count}")
406
407 # Validate against YAML metadata if available
408 if 'total_leaf_count' in tree_info:
409 expected_leaf_count = tree_info['total_leaf_count']
410 if peano_tree._leaf_count != expected_leaf_count:
411 print(f"Warning: Reconstructed leaf count ({peano_tree._leaf_count}) doesn't match YAML metadata ({expected_leaf_count})")
412
413 if 'subtree_count' in tree_info:
414 expected_subtree_count = tree_info['subtree_count']
415 if peano_tree._subtree_count != expected_subtree_count:
416 print(f"Warning: Reconstructed subtree count ({peano_tree._subtree_count}) doesn't match YAML metadata ({expected_subtree_count})")
417
418 return peano_tree
parse_tuple_string(tuple_str)
Parse a string representation of a (weight, subtree_id) tuple.