def _parse_range(range_str): """ Parse a range string for the problem into a dict or list of dict if multi-fit. :param range_str: The a string to parse :type range_str: string :return: The ranges in a dictionary with key as the var and value as a list with min and max e.g. {'x': [0, 10]} :rtype: dict """ if not range_str: return {} output_ranges = {} range_str = range_str.strip('{').strip('}') tmp_ranges = range_str.split(',') ranges = [] cur_str = '' for r in tmp_ranges: cur_str += r balanced = True for lb, rb in ['[]', '{}', '()']: if cur_str.count(lb) > cur_str.count(rb): balanced = False elif cur_str.count(lb) < cur_str.count(rb): raise ParsingError( 'Unbalanced brackets in range: {}'.format(r)) if balanced: ranges.append(cur_str) cur_str = '' else: cur_str += ',' for r in ranges: name, val = r.split(':') name = name.strip().strip('"').strip("'").lower() # Strip off brackets and split on comma val = val.strip(' ')[1:-1].split(',') val = [v.strip() for v in val] try: pair = [float(val[0]), float(val[1])] except ValueError: raise ParsingError('Expected floats in range: {}'.format(r)) if pair[0] >= pair[1]: raise ParsingError('Min value must be smaller than max value ' 'in range: {}'.format(r)) output_ranges[name] = pair return output_ranges
def nist_func_definition(function, param_names): """ Processing a function plus different set of starting values as specified in the NIST problem definition file into a callable :param function: function string as defined in a NIST problem definition file :type function: str :param param_names: names of the parameters in the function :type param_names: list :return: callable function :rtype: callable """ function_scipy_format = format_function_scipy(function) # Create a function def for each starting set in startvals if not is_safe(function_scipy_format): raise ParsingError('Error while sanitizing input') # Sanitizing of function_scipy_format is done so exec use is valid # Param_names is sanitized in get_nist_param_names_and_values # pylint: disable=exec-used local_dict = {} global_dict = {'__builtins__': {}, 'np': np} exec("def fitting_function(x, " + ','.join(param_names) + "): return " + function_scipy_format, global_dict, local_dict) return local_dict['fitting_function']
def _parse_starting_values(self, lines): """ Parses the starting values of a NIST file and converts them into an array. :param lines: All lines in the imported nist file :type lines: list of str :return: The starting values used in NIST problem :rtype: list of list of float """ starting_vals = [] for line in lines: if not line.strip() or line.startswith('Residual'): break startval_str = line.split() if not startval_str[0].isalnum(): raise ParsingError('Could not parse starting parameters.') alt_values = self._get_startvals_floats(startval_str) if not starting_vals: starting_vals = [OrderedDict() for _ in alt_values] for i, a in enumerate(alt_values): starting_vals[i][startval_str[0]] = a return starting_vals
def _get_equation_text(self, lines, idxerr, idx): """ Gets the equation text from the NIST file. :param lines: All lines in the imported nist file :type lines: list of str :param idxerr: boolean that points out if there were any problems in finding the equation in the file :type idxerr: bool :param idx: the line at which the parser is at :type idx: int :return: The equation from the NIST file and the new index :rtype: str and int """ # Next non-empty lines are assumed to continue the equation equation_text = '' if idxerr is False: while lines[idx].strip(): equation_text += lines[idx].strip() idx += 1 if not equation_text: raise ParsingError("Could not find the equation!") return equation_text, idx
def _get_data_points(data_file_path): """ Get the data points of the problem from the data file. :param data_file_path: The path to the file to load the points from :type data_file_path: str :return: data points :rtype: np.ndarray """ with open(data_file_path, 'r') as f: data_text = f.readlines() # Find the line where data starts # i.e. the first line with a float on it first_row = 0 # Loop until break statement while True: try: line = data_text[first_row].strip() except IndexError: raise ParsingError('Could not find data points') if line != '': x_val = line.split()[0] try: _ = float(x_val) except ValueError: pass else: break first_row += 1 dim = len(data_text[first_row].split()) data_points = np.zeros((len(data_text) - first_row, dim)) for idx, line in enumerate(data_text[first_row:]): point_text = line.split() # Skip any values that can't be represented try: point = [float(val) for val in point_text] except ValueError: point = [np.nan for val in point_text] data_points[idx, :] = point # Strip all np.nan entries data_points = data_points[~np.isnan(data_points[:, 0]), :] return data_points
def _check_data(count, x, y, e): """ Check that then number of data points for x, y, and errors are consistent. :param count: Expected number of datapoints :type count: int :param x: Number of x data points :type x: int :param y: Number of y data points :type y: int :param e: Number of error data points :type e: int :raises ParsingError: If x, y, or e does not match count. (e can also be 0) """ if x != count: raise ParsingError('Wrong number of x data points. Got {}, ' 'expected {}'.format(x, count)) if y != count: raise ParsingError('Wrong number of y data points. Got {}, ' 'expected {}'.format(y, count)) if e not in (0, count): raise ParsingError('Wrong number of e data points. Got {}, ' 'expected {}'.format(e, count))
def nist_jacobian_definition(jacobian, param_names): """ Processing a Jacobian plus different set of starting values as specified in the NIST problem definition file into a callable :param jacobian: Jacobian string as defined in the data files for the corresponding NIST problem definition file :type jacobian: str :param param_names: names of the parameters in the function :type param_names: list :return: callable function :rtype: callable """ scipy_jacobian = [] for jacobian_lines in jacobian: jacobian_scipy_format = format_function_scipy(jacobian_lines) # Create a function def for each starting set in startvals if not is_safe(jacobian_scipy_format): raise ParsingError('Error while sanitizing Jacobian input') # Checks to see if the value is an integer and if so reformats the # value to be a constant vector. if is_int(jacobian_scipy_format): jacobian_scipy_format += "*(np.ones(x.shape[0]))" scipy_jacobian.append(jacobian_scipy_format) jacobian_format = "-np.array([{}]).T".format(",".join(scipy_jacobian)) new_param_name = "params" for i, name in enumerate(param_names): jacobian_format = jacobian_format.replace( name, "{0}[{1}]".format(new_param_name, i)) # Sanitizing of jacobian_scipy_format is done so exec use is valid # Param_names is sanitized in get_nist_param_names_and_values # pylint: disable=exec-used local_dict = {} global_dict = {'__builtins__': {}, 'np': np} exec( "def jacobian_function(x, " + new_param_name + "): return " + jacobian_format, global_dict, local_dict) return local_dict['jacobian_function']
def _parse_equation(self, eq_text): """ Parses the equation and converts it to the right format. :param eq_text: The equation :type eq_text: str :return: formatted equation :rtype: str """ start_normal = r'\s*y\s*=(.+)' if re.match(start_normal, eq_text): match = re.search(r'y\s*=(.+)\s*\+\s*e', eq_text) equation = match.group(1).strip() else: raise ParsingError("Unrecognized equation syntax when trying to " "parse a NIST equation: " + eq_text) equation = self._convert_nist_to_muparser(equation) return equation
def _get_data_txt(self, lines, idx): """ Gets the data pattern from the NIST problem file. :param lines: All lines in the imported nist file :type lines: list of str :param idx: the line at which the parser is at :type idx: int :return: The data pattern and the new index :rtype: (list of str) and int """ data_text = None data_text = lines[idx:] idx = len(lines) if not data_text: raise ParsingError("Could not find the data!") return data_text, idx
def _get_startvals_floats(self, startval_str): """ Converts the starting values into floats. :param startval_str: Unparsed starting values :type startval_str: list of str :return: Starting values array of floats :rtype: list of float """ # A bit weak/lax parsing, if there is one less column, # assume only one start point if len(startval_str) == 6: alt_values = [float(startval_str[2]), float(startval_str[3])] elif len(startval_str) == 5: alt_values = [float(startval_str[2])] # In the NIST format this can only contain 5 or 6 columns else: raise ParsingError("Failed to parse this line as starting " "values information: {0}".format(startval_str)) return alt_values
def _parse_function(self): """ Get the params from the function as a list of dicts from the data file. :return: Function definition in format: [{name1: value1, name2: value2, ...}, ...] :rtype: list of dict """ # pylint: disable=too-many-branches, too-many-statements function_def = [] for f in self._entries['function'].split(';'): params_dict = OrderedDict() pop_stack = 0 stack = [params_dict] for p in f.split(','): name, val = p.split('=', 1) name = name.strip() val = val.strip() l_count = val.count('(') r_count = val.count(')') if l_count > r_count: # in brackets # should be of the form 'varname=(othervar=3, ...)' # Allow for nested brackets e.g. 'a=(b=(c=(d=1,e=2)))' for _ in range(l_count - r_count): # Cover case where formula mistyped if '=' not in val: raise ParsingError('Unbalanced brackets in ' 'function value: {}'.format(p)) # Must start with brackets if val[0] != '(': raise ParsingError('Bad placement of opening ' 'bracket in function: ' '{}'.format(p)) # Create new dict for this entry and put at top of # working stack new_dict = OrderedDict() stack[-1][name] = new_dict stack.append(new_dict) # Update name and val name, val = val[1:].split('=', 1) elif l_count == r_count: # Check if single item in brackets while '=' in val: if val[0] == '(' and val[-1] == ')': val = val[1:-1] new_dict = OrderedDict() stack[-1][name] = new_dict stack.append(new_dict) name, val = val.split('=', 1) pop_stack += 1 else: raise ParsingError('Function value contains ' 'unexpected "=": {}'.format(p)) elif l_count < r_count: # exiting brackets pop_stack = r_count - l_count # must end with brackets if val[-pop_stack:] != ')' * pop_stack: raise ParsingError('Bad placement of closing bracket ' 'in function: {}'.format(p)) val = val[:-pop_stack] # Parse to an int/float if possible else assume string tmp_val = None for t in [int, float]: if tmp_val is None: try: tmp_val = t(val) except (ValueError, TypeError): pass if tmp_val is not None: val = tmp_val stack[-1][name] = val if pop_stack > 0: if len(stack) <= pop_stack: raise ParsingError('Too many closing brackets in ' 'function: {}'.format(p)) stack = stack[:-pop_stack] pop_stack = 0 if len(stack) != 1: raise ParsingError('Not all brackets are closed in function.') function_def.append(params_dict) return function_def