fennol.utils.input_parser
FeNNol Input Parameter Parser
This module provides parsing capabilities for FeNNol input files in .fnl format. It supports hierarchical parameter organization, automatic unit conversion, and various data types including booleans, numbers, strings, and lists.
The main components are:
- InputFile: Dictionary-like container for hierarchical parameters
- parse_input: Parser function for .fnl files
- convert_dict_units: Unit conversion utility for YAML/dict inputs
Supported input formats:
- .fnl: Native FeNNol format with hierarchical sections
- .yaml/.yml: YAML format (processed through convert_dict_units)
Unit conversion:
- Units specified in brackets: dt[fs] = 0.5
- Units specified in braces: gamma{THz} = 10.0
- All units converted to atomic units internally
Boolean representations:
- True: true, yes, .true.
- False: false, no, .false.
1""" 2FeNNol Input Parameter Parser 3 4This module provides parsing capabilities for FeNNol input files in .fnl format. 5It supports hierarchical parameter organization, automatic unit conversion, 6and various data types including booleans, numbers, strings, and lists. 7 8The main components are: 9- InputFile: Dictionary-like container for hierarchical parameters 10- parse_input: Parser function for .fnl files 11- convert_dict_units: Unit conversion utility for YAML/dict inputs 12 13Supported input formats: 14- .fnl: Native FeNNol format with hierarchical sections 15- .yaml/.yml: YAML format (processed through convert_dict_units) 16 17Unit conversion: 18- Units specified in brackets: dt[fs] = 0.5 19- Units specified in braces: gamma{THz} = 10.0 20- All units converted to atomic units internally 21 22Boolean representations: 23- True: true, yes, .true. 24- False: false, no, .false. 25 26""" 27 28import re 29from typing import Dict, Any 30from .atomic_units import au,UnitSystem 31 32_separators = " |,|=|\t|\n" 33_comment_chars = ["#", "!"] 34_true_repr = ["true", "yes", ".true."] 35_false_repr = ["false", "no", ".false."] 36 37 38class InputFile(dict): 39 """ 40 Dictionary-like container for hierarchical input parameters. 41 42 This class extends dict to provide path-based access to nested parameters 43 using '/' as a separator. It supports case-insensitive keys and automatic 44 unit conversion from parameter names with bracket notation. 45 46 Attributes: 47 case_insensitive (bool): Whether keys are case-insensitive (default: True) 48 49 Examples: 50 >>> params = InputFile() 51 >>> params.store("xyz_input/file", "system.xyz") 52 >>> params.get("xyz_input/file") 53 'system.xyz' 54 >>> params["temperature"] = 300.0 55 >>> params.get("temperature") 56 300.0 57 """ 58 case_insensitive = True 59 60 def __init__(self, *args, **kwargs): 61 super(InputFile, self).__init__(*args, **kwargs) 62 if InputFile.case_insensitive: 63 for key in list(self.keys()): 64 dict.__setitem__(self, key.lower(), dict.get(self, key)) 65 for key in list(self.keys()): 66 if isinstance(self[key], dict): 67 dict.__setitem__(self, key, InputFile(**self[key])) 68 69 70 def get(self, path, default=None): 71 if not isinstance(path, str): 72 raise TypeError("Path must be a string") 73 if InputFile.case_insensitive: 74 path = path.lower() 75 keys = path.split("/") 76 val = None 77 for key in keys: 78 if isinstance(val, InputFile): 79 val = val.get(key, default=None) 80 else: 81 val = dict.get(self, key, None) 82 83 if val is None: 84 return default 85 86 return val 87 88 def store(self, path, value): 89 if not isinstance(path, str): 90 raise TypeError("Path must be a string") 91 if isinstance(value, dict): 92 value = InputFile(**value) 93 if InputFile.case_insensitive: 94 path = path.lower() 95 keys = path.split("/") 96 child = self.get(keys[0], default=None) 97 if isinstance(child, InputFile): 98 if len(keys) == 1: 99 print("Warning: overriding a sub-dictionary!") 100 dict.__setitem__(self, keys[0], value) 101 # self[keys[0]] = value 102 return 1 103 else: 104 child.store("/".join(keys[1:]), value) 105 else: 106 if len(keys) == 1: 107 dict.__setitem__(self, keys[0], value) 108 # self[keys[0]] = value 109 return 0 110 else: 111 if child is None: 112 sub_dict = InputFile() 113 sub_dict.store("/".join(keys[1:]), value) 114 dict.__setitem__(self, keys[0], sub_dict) 115 else: 116 print("Error: hit a leaf before the end of path!") 117 return -1 118 119 def __getitem__(self, path): 120 return self.get(path) 121 122 def __setitem__(self, path, value): 123 return self.store(path, value) 124 125 def print(self, tab=""): 126 string = "" 127 for p_id, p_info in self.items(): 128 string += tab + p_id 129 val = self.get(p_id) 130 if isinstance(val, InputFile): 131 string += "{\n" + val.print(tab=tab + " ") + "\n" + tab + "}\n\n" 132 else: 133 string += " = " + str(val) + "\n" 134 return string[:-1] 135 136 def save(self, filename): 137 with open(filename, "w") as f: 138 f.write(self.print()) 139 140 def __str__(self): 141 return self.print() 142 143 144def parse_input(input_file,us:UnitSystem=au): 145 """ 146 Parse a FeNNol input file (.fnl format) into a hierarchical parameter structure. 147 148 The parser supports: 149 - Hierarchical sections using curly braces {} 150 - Comments starting with # or ! 151 - Unit specifications in brackets [unit] => converted to provided UnitSystem (default: atomic units) 152 - Boolean values (yes/no, true/false, .true./.false.) 153 - Numeric values (int/float) 154 - String values 155 - Lists of values 156 157 Parameters: 158 input_file (str): Path to the input file 159 160 Returns: 161 InputFile: Hierarchical dictionary containing parsed parameters 162 163 Example input file:: 164 165 device cuda:0 166 temperature = 300.0 167 dt[fs] = 0.5 168 169 xyz_input{ 170 file system.xyz 171 indexed yes 172 } 173 174 thermostat LGV 175 gamma[THz] = 10.0 176 """ 177 f = open(input_file, "r") 178 struct = InputFile() 179 path = [] 180 for line in f: 181 # remove all after comment character 182 for comment_char in _comment_chars: 183 index = line.find(comment_char) 184 if index >= 0: 185 line = line[:index] 186 187 # split line using defined separators 188 parsed_line = re.split(_separators, line.strip()) 189 190 # remove empty strings 191 parsed_line = [x for x in parsed_line if x] 192 # skip blank lines 193 if not parsed_line: 194 continue 195 # print(parsed_line) 196 197 word0 = parsed_line[0].lower() 198 cat_fields = "".join(parsed_line) 199 # check if beginning of a category 200 if cat_fields.endswith("{"): 201 path.append(cat_fields[:-1]) 202 continue 203 if cat_fields.startswith("&"): 204 path.append(cat_fields[1:]) 205 continue 206 if cat_fields.endswith("{}"): 207 struct.store("/".join("path") + "/" + cat_fields[1:-2], InputFile()) 208 continue 209 210 # print(current_category) 211 # if not path: 212 # print("Error: line not recognized!") 213 # return None 214 # else: #check if end of a category 215 if (cat_fields[0] in "}/") or ("&end" in cat_fields): 216 del path[-1] 217 continue 218 219 word0, unit = _get_unit_from_key(word0,us) 220 val = None 221 if len(parsed_line) == 1: 222 val = True # keyword only => store True 223 elif len(parsed_line) == 2: 224 val = string_to_true_type(parsed_line[1], unit) 225 else: 226 # analyze parsed line 227 val = [] 228 for word in parsed_line[1:]: 229 val.append(string_to_true_type(word, unit)) 230 struct.store("/".join(path + [word0]), val) 231 232 f.close() 233 return struct 234 235 236def string_to_true_type(word, unit=None): 237 if unit is not None: 238 return float(word) / unit 239 240 try: 241 val = int(word) 242 except ValueError: 243 try: 244 val = float(word) 245 except ValueError: 246 if word.lower() in _true_repr: 247 val = True 248 elif word.lower() in _false_repr: 249 val = False 250 else: 251 val = word 252 return val 253 254 255def _get_unit_from_key(word:str,us:UnitSystem): 256 unit_start = max(word.find("{"), word.find("[")) 257 n = len(word) 258 if unit_start < 0: 259 key = word 260 unit = None 261 elif unit_start == 0: 262 print("Error: Field '" + str(word) + "' must not start with '{' or '[' !") 263 raise ValueError 264 else: 265 if word[unit_start] == "{": 266 end_bracket = "}" 267 else: 268 end_bracket = "]" 269 key = word[:unit_start] 270 if word[n - 1] != end_bracket: 271 print("Error: wrong unit specification in field '" + str(word) + "' !") 272 raise ValueError 273 274 if n - unit_start - 2 < 0: 275 unit = 1.0 276 else: 277 unit = us.get_multiplier(word[unit_start + 1 : -1]) 278 # print(key+" unit= "+str(unit)) 279 return key, unit 280 281 282def convert_dict_units(d: Dict[str, Any],us:UnitSystem=au) -> Dict[str, Any]: 283 """ 284 Convert all values in a dictionary from specified units to the provided unit system (atomic units by default). 285 286 This function recursively processes a dictionary and converts any values 287 with unit specifications (indicated by keys containing [unit] or {unit}) 288 to atomic units. The unit specification is removed from the key name. 289 290 Parameters: 291 d (Dict[str, Any]): Dictionary with potentially unit-specified keys 292 293 Returns: 294 Dict[str, Any]: Dictionary with values converted to atomic units 295 296 Examples: 297 >>> d = {"dt[fs]": 0.5, "temperature": 300.0} 298 >>> convert_dict_units(d) 299 {"dt": 20.67..., "temperature": 300.0} 300 """ 301 302 if not isinstance(d, dict): 303 raise TypeError("Input must be a dictionary") 304 if not d: 305 return d 306 if not isinstance(us, UnitSystem): 307 raise TypeError("Unit system must be an instance of UnitSystem") 308 d2 = {} 309 for k,v in d.items(): 310 if isinstance(v, dict): 311 d2[k] = convert_dict_units(v,us) 312 continue 313 key, unit = _get_unit_from_key(k,us) 314 if unit is None: 315 d2[k] = v 316 continue 317 try: 318 if isinstance(v, list): 319 d2[key] = [x / unit for x in v] 320 elif isinstance(v, tuple): 321 d2[key] = tuple(x / unit for x in v) 322 else: 323 d2[key] = v / unit 324 except TypeError: 325 raise ValueError(f"Error: cannot convert value '{v}' of type to atomic units.") 326 except Exception as e: 327 print(f"Error: unexpected error in unit conversion for key '{k}': {e}") 328 raise e 329 330 return d2
39class InputFile(dict): 40 """ 41 Dictionary-like container for hierarchical input parameters. 42 43 This class extends dict to provide path-based access to nested parameters 44 using '/' as a separator. It supports case-insensitive keys and automatic 45 unit conversion from parameter names with bracket notation. 46 47 Attributes: 48 case_insensitive (bool): Whether keys are case-insensitive (default: True) 49 50 Examples: 51 >>> params = InputFile() 52 >>> params.store("xyz_input/file", "system.xyz") 53 >>> params.get("xyz_input/file") 54 'system.xyz' 55 >>> params["temperature"] = 300.0 56 >>> params.get("temperature") 57 300.0 58 """ 59 case_insensitive = True 60 61 def __init__(self, *args, **kwargs): 62 super(InputFile, self).__init__(*args, **kwargs) 63 if InputFile.case_insensitive: 64 for key in list(self.keys()): 65 dict.__setitem__(self, key.lower(), dict.get(self, key)) 66 for key in list(self.keys()): 67 if isinstance(self[key], dict): 68 dict.__setitem__(self, key, InputFile(**self[key])) 69 70 71 def get(self, path, default=None): 72 if not isinstance(path, str): 73 raise TypeError("Path must be a string") 74 if InputFile.case_insensitive: 75 path = path.lower() 76 keys = path.split("/") 77 val = None 78 for key in keys: 79 if isinstance(val, InputFile): 80 val = val.get(key, default=None) 81 else: 82 val = dict.get(self, key, None) 83 84 if val is None: 85 return default 86 87 return val 88 89 def store(self, path, value): 90 if not isinstance(path, str): 91 raise TypeError("Path must be a string") 92 if isinstance(value, dict): 93 value = InputFile(**value) 94 if InputFile.case_insensitive: 95 path = path.lower() 96 keys = path.split("/") 97 child = self.get(keys[0], default=None) 98 if isinstance(child, InputFile): 99 if len(keys) == 1: 100 print("Warning: overriding a sub-dictionary!") 101 dict.__setitem__(self, keys[0], value) 102 # self[keys[0]] = value 103 return 1 104 else: 105 child.store("/".join(keys[1:]), value) 106 else: 107 if len(keys) == 1: 108 dict.__setitem__(self, keys[0], value) 109 # self[keys[0]] = value 110 return 0 111 else: 112 if child is None: 113 sub_dict = InputFile() 114 sub_dict.store("/".join(keys[1:]), value) 115 dict.__setitem__(self, keys[0], sub_dict) 116 else: 117 print("Error: hit a leaf before the end of path!") 118 return -1 119 120 def __getitem__(self, path): 121 return self.get(path) 122 123 def __setitem__(self, path, value): 124 return self.store(path, value) 125 126 def print(self, tab=""): 127 string = "" 128 for p_id, p_info in self.items(): 129 string += tab + p_id 130 val = self.get(p_id) 131 if isinstance(val, InputFile): 132 string += "{\n" + val.print(tab=tab + " ") + "\n" + tab + "}\n\n" 133 else: 134 string += " = " + str(val) + "\n" 135 return string[:-1] 136 137 def save(self, filename): 138 with open(filename, "w") as f: 139 f.write(self.print()) 140 141 def __str__(self): 142 return self.print()
Dictionary-like container for hierarchical input parameters.
This class extends dict to provide path-based access to nested parameters using '/' as a separator. It supports case-insensitive keys and automatic unit conversion from parameter names with bracket notation.
Attributes: case_insensitive (bool): Whether keys are case-insensitive (default: True)
Examples:
params = InputFile() params.store("xyz_input/file", "system.xyz") params.get("xyz_input/file") 'system.xyz' params["temperature"] = 300.0 params.get("temperature") 300.0
71 def get(self, path, default=None): 72 if not isinstance(path, str): 73 raise TypeError("Path must be a string") 74 if InputFile.case_insensitive: 75 path = path.lower() 76 keys = path.split("/") 77 val = None 78 for key in keys: 79 if isinstance(val, InputFile): 80 val = val.get(key, default=None) 81 else: 82 val = dict.get(self, key, None) 83 84 if val is None: 85 return default 86 87 return val
Return the value for key if key is in the dictionary, else default.
89 def store(self, path, value): 90 if not isinstance(path, str): 91 raise TypeError("Path must be a string") 92 if isinstance(value, dict): 93 value = InputFile(**value) 94 if InputFile.case_insensitive: 95 path = path.lower() 96 keys = path.split("/") 97 child = self.get(keys[0], default=None) 98 if isinstance(child, InputFile): 99 if len(keys) == 1: 100 print("Warning: overriding a sub-dictionary!") 101 dict.__setitem__(self, keys[0], value) 102 # self[keys[0]] = value 103 return 1 104 else: 105 child.store("/".join(keys[1:]), value) 106 else: 107 if len(keys) == 1: 108 dict.__setitem__(self, keys[0], value) 109 # self[keys[0]] = value 110 return 0 111 else: 112 if child is None: 113 sub_dict = InputFile() 114 sub_dict.store("/".join(keys[1:]), value) 115 dict.__setitem__(self, keys[0], sub_dict) 116 else: 117 print("Error: hit a leaf before the end of path!") 118 return -1
126 def print(self, tab=""): 127 string = "" 128 for p_id, p_info in self.items(): 129 string += tab + p_id 130 val = self.get(p_id) 131 if isinstance(val, InputFile): 132 string += "{\n" + val.print(tab=tab + " ") + "\n" + tab + "}\n\n" 133 else: 134 string += " = " + str(val) + "\n" 135 return string[:-1]
145def parse_input(input_file,us:UnitSystem=au): 146 """ 147 Parse a FeNNol input file (.fnl format) into a hierarchical parameter structure. 148 149 The parser supports: 150 - Hierarchical sections using curly braces {} 151 - Comments starting with # or ! 152 - Unit specifications in brackets [unit] => converted to provided UnitSystem (default: atomic units) 153 - Boolean values (yes/no, true/false, .true./.false.) 154 - Numeric values (int/float) 155 - String values 156 - Lists of values 157 158 Parameters: 159 input_file (str): Path to the input file 160 161 Returns: 162 InputFile: Hierarchical dictionary containing parsed parameters 163 164 Example input file:: 165 166 device cuda:0 167 temperature = 300.0 168 dt[fs] = 0.5 169 170 xyz_input{ 171 file system.xyz 172 indexed yes 173 } 174 175 thermostat LGV 176 gamma[THz] = 10.0 177 """ 178 f = open(input_file, "r") 179 struct = InputFile() 180 path = [] 181 for line in f: 182 # remove all after comment character 183 for comment_char in _comment_chars: 184 index = line.find(comment_char) 185 if index >= 0: 186 line = line[:index] 187 188 # split line using defined separators 189 parsed_line = re.split(_separators, line.strip()) 190 191 # remove empty strings 192 parsed_line = [x for x in parsed_line if x] 193 # skip blank lines 194 if not parsed_line: 195 continue 196 # print(parsed_line) 197 198 word0 = parsed_line[0].lower() 199 cat_fields = "".join(parsed_line) 200 # check if beginning of a category 201 if cat_fields.endswith("{"): 202 path.append(cat_fields[:-1]) 203 continue 204 if cat_fields.startswith("&"): 205 path.append(cat_fields[1:]) 206 continue 207 if cat_fields.endswith("{}"): 208 struct.store("/".join("path") + "/" + cat_fields[1:-2], InputFile()) 209 continue 210 211 # print(current_category) 212 # if not path: 213 # print("Error: line not recognized!") 214 # return None 215 # else: #check if end of a category 216 if (cat_fields[0] in "}/") or ("&end" in cat_fields): 217 del path[-1] 218 continue 219 220 word0, unit = _get_unit_from_key(word0,us) 221 val = None 222 if len(parsed_line) == 1: 223 val = True # keyword only => store True 224 elif len(parsed_line) == 2: 225 val = string_to_true_type(parsed_line[1], unit) 226 else: 227 # analyze parsed line 228 val = [] 229 for word in parsed_line[1:]: 230 val.append(string_to_true_type(word, unit)) 231 struct.store("/".join(path + [word0]), val) 232 233 f.close() 234 return struct
Parse a FeNNol input file (.fnl format) into a hierarchical parameter structure.
The parser supports:
- Hierarchical sections using curly braces {}
- Comments starting with # or !
- Unit specifications in brackets [unit] => converted to provided UnitSystem (default: atomic units)
- Boolean values (yes/no, true/false, .true./.false.)
- Numeric values (int/float)
- String values
- Lists of values
Parameters: input_file (str): Path to the input file
Returns: InputFile: Hierarchical dictionary containing parsed parameters
Example input file::
device cuda:0
temperature = 300.0
dt[fs] = 0.5
xyz_input{
file system.xyz
indexed yes
}
thermostat LGV
gamma[THz] = 10.0
237def string_to_true_type(word, unit=None): 238 if unit is not None: 239 return float(word) / unit 240 241 try: 242 val = int(word) 243 except ValueError: 244 try: 245 val = float(word) 246 except ValueError: 247 if word.lower() in _true_repr: 248 val = True 249 elif word.lower() in _false_repr: 250 val = False 251 else: 252 val = word 253 return val
283def convert_dict_units(d: Dict[str, Any],us:UnitSystem=au) -> Dict[str, Any]: 284 """ 285 Convert all values in a dictionary from specified units to the provided unit system (atomic units by default). 286 287 This function recursively processes a dictionary and converts any values 288 with unit specifications (indicated by keys containing [unit] or {unit}) 289 to atomic units. The unit specification is removed from the key name. 290 291 Parameters: 292 d (Dict[str, Any]): Dictionary with potentially unit-specified keys 293 294 Returns: 295 Dict[str, Any]: Dictionary with values converted to atomic units 296 297 Examples: 298 >>> d = {"dt[fs]": 0.5, "temperature": 300.0} 299 >>> convert_dict_units(d) 300 {"dt": 20.67..., "temperature": 300.0} 301 """ 302 303 if not isinstance(d, dict): 304 raise TypeError("Input must be a dictionary") 305 if not d: 306 return d 307 if not isinstance(us, UnitSystem): 308 raise TypeError("Unit system must be an instance of UnitSystem") 309 d2 = {} 310 for k,v in d.items(): 311 if isinstance(v, dict): 312 d2[k] = convert_dict_units(v,us) 313 continue 314 key, unit = _get_unit_from_key(k,us) 315 if unit is None: 316 d2[k] = v 317 continue 318 try: 319 if isinstance(v, list): 320 d2[key] = [x / unit for x in v] 321 elif isinstance(v, tuple): 322 d2[key] = tuple(x / unit for x in v) 323 else: 324 d2[key] = v / unit 325 except TypeError: 326 raise ValueError(f"Error: cannot convert value '{v}' of type to atomic units.") 327 except Exception as e: 328 print(f"Error: unexpected error in unit conversion for key '{k}': {e}") 329 raise e 330 331 return d2
Convert all values in a dictionary from specified units to the provided unit system (atomic units by default).
This function recursively processes a dictionary and converts any values with unit specifications (indicated by keys containing [unit] or {unit}) to atomic units. The unit specification is removed from the key name.
Parameters: d (Dict[str, Any]): Dictionary with potentially unit-specified keys
Returns: Dict[str, Any]: Dictionary with values converted to atomic units
Examples:
d = {"dt[fs]": 0.5, "temperature": 300.0} convert_dict_units(d) {"dt": 20.67..., "temperature": 300.0}