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
class InputFile(builtins.dict):
 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

case_insensitive = True
def get(self, path, default=None):
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.

def store(self, path, value):
 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
def print(self, tab=''):
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]
def save(self, filename):
137    def save(self, filename):
138        with open(filename, "w") as f:
139            f.write(self.print())
def parse_input( input_file, us: fennol.utils.atomic_units.UnitSystem = <fennol.utils.atomic_units.UnitSystem object>):
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
def string_to_true_type(word, unit=None):
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
def convert_dict_units( d: Dict[str, Any], us: fennol.utils.atomic_units.UnitSystem = <fennol.utils.atomic_units.UnitSystem object>) -> Dict[str, Any]:
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}