def taylor_series_expansion(expr, diff_mode=differentiate.Modes.reverse_numeric, order=1): """ Generate a taylor series approximation for expr. Parameters ---------- expr: pyomo.core.expr.numeric_expr.ExpressionBase diff_mode: pyomo.core.expr.calculus.derivatives.Modes The method for differentiation. order: The order of the taylor series expansion If order is not 1, then symbolic differentiation must be used (differentiation.Modes.reverse_sybolic or differentiation.Modes.sympy). Returns ------- res: pyomo.core.expr.numeric_expr.ExpressionBase """ if order < 0: raise ValueError( 'Cannot compute taylor series expansion of order {0}'.format( str(order))) if order != 1 and diff_mode is differentiate.Modes.reverse_numeric: logger.warning( 'taylor_series_expansion can only use symbolic differentiation for orders larger than 1' ) diff_mode = differentiate.Modes.reverse_symbolic e_vars = list(identify_variables(expr=expr, include_fixed=False)) res = value(expr) if order >= 1: derivs = differentiate(expr=expr, wrt_list=e_vars, mode=diff_mode) res += sum( value(derivs[i]) * (e_vars[i] - e_vars[i].value) for i in range(len(e_vars))) """ This last bit of code is just for higher order taylor series expansions. The recursive function _loop modifies derivs in place so that derivs becomes a list of lists of lists... However, _loop is also a generator so that we don't have to loop through it twice. _loop yields two lists. The first is a list of indices corresponding to the first k-1 variables that differentiation is being done with respect to. The second is a list of derivatives. Each entry in this list is the derivative with respect to the first k-1 variables and the kth variable, whose index matches the index in _derivs. """ if order >= 2: for n in range(2, order + 1): coef = 1.0 / math.factorial(n) for ndx_list, _derivs in _loop(derivs, e_vars, diff_mode, list()): tmp = coef for ndx in ndx_list: tmp *= (e_vars[ndx] - e_vars[ndx].value) res += tmp * sum( value(_derivs[i]) * (e_vars[i] - e_vars[i].value) for i in range(len(e_vars))) return res
def test_sympy(self): m = pyo.ConcreteModel() m.x = pyo.Var(initialize=0.23) m.y = pyo.Var(initialize=0.88) ddx = differentiate(m.x**2, wrt=m.x, mode='sympy') self.assertTrue(compare_expressions(ddx, 2 * m.x)) self.assertAlmostEqual(ddx(), 0.46) ddy = differentiate(m.x**2, wrt=m.y, mode='sympy') self.assertEqual(ddy, 0) ddx = differentiate(m.x**2, wrt_list=[m.x, m.y], mode='sympy') self.assertIsInstance(ddx, list) self.assertEqual(len(ddx), 2) self.assertTrue(compare_expressions(ddx[0], 2 * m.x)) self.assertAlmostEqual(ddx[0](), 0.46) self.assertEqual(ddx[1], 0)
def generate(self, problem, relaxed_problem, mip_solution, tree, node): if not self._convex_relaxations_map: return [] cuts = [] for relaxation, (rel_expr, rel_vars) in self._convex_relaxations_map.items(): aux_var = relaxation.get_aux_var() rhs_expr = relaxation.get_rhs_expr() side = relaxation.relaxation_side deviation = get_deviation_adjusted_by_side(aux_var, rhs_expr, side) if deviation < self.threshold: continue diff_map = differentiate(rel_expr, wrt_list=rel_vars) cut_expr = pe.value(rel_expr) + sum( diff_map[i] * (v - pe.value(v)) for i, v in enumerate(rel_vars)) relaxation_side = relaxation.relaxation_side if relaxation_side == RelaxationSide.UNDER: cut_ineq = cut_expr <= 0 elif relaxation_side == RelaxationSide.OVER: cut_ineq = cut_expr >= 0 else: assert relaxation_side == RelaxationSide.BOTH cut_ineq = cut_expr <= 0 cuts.append(cut_ineq) return cuts
def test_bad_mode(self): m = pyo.ConcreteModel() m.x = pyo.Var(initialize=0.23) with self.assertRaisesRegex( ValueError, r'Unrecognized differentiation mode: foo\n' r"Expected one of \['sympy', 'reverse_symbolic', " r"'reverse_numeric'\]"): ddx = differentiate(m.x**2, m.x, mode='foo')
def test_reverse_numeric(self): m = pyo.ConcreteModel() m.x = pyo.Var(initialize=0.23) m.y = pyo.Var(initialize=0.88) ddx = differentiate(m.x**2, wrt=m.x, mode='reverse_numeric') self.assertIsInstance(ddx, float) self.assertAlmostEqual(ddx, 0.46) ddy = differentiate(m.x**2, wrt=m.y, mode='reverse_numeric') self.assertEqual(ddy, 0) ddx = differentiate(m.x**2, wrt_list=[m.x, m.y], mode='reverse_numeric') self.assertIsInstance(ddx, list) self.assertEqual(len(ddx), 2) self.assertIsInstance(ddx[0], float) self.assertAlmostEqual(ddx[0], 0.46) self.assertEqual(ddx[1], 0)
def _loop(derivs, e_vars, diff_mode, ndx_list): for ndx, item in enumerate(derivs): ndx_list.append(ndx) if item.__class__ == list: for a, b in _loop(item, e_vars, diff_mode, ndx_list): yield a, b else: _derivs = differentiate(item, wrt_list=e_vars, mode=diff_mode) derivs[ndx] = _derivs yield ndx_list, _derivs ndx_list.pop()
def updateSurrogateModel(self): """ The parameters needed for the surrogate model are the values of: b(w_k) : basis_model_output d(w_k) : truth_model_output grad b(w_k) : grad_basis_model_output grad d(w_k) : grad_truth_model_output """ b = self.data for i, y in b.ef_outputs.items(): b.basis_model_output[i] = value(b.basis_expressions[y]) b.truth_model_output[i] = value(b.truth_models[y]) # Basis functions are Pyomo expressions (in theory) gradBasis = differentiate(b.basis_expressions[y], wrt_list=b.ef_inputs[i]) # These, however, are external functions gradTruth = differentiate(b.truth_models[y], wrt_list=b.ef_inputs[i]) for j, w in enumerate(b.ef_inputs[i]): b.grad_basis_model_output[i, j] = gradBasis[j] b.grad_truth_model_output[i, j] = gradTruth[j] b.value_of_ef_inputs[i, j] = value(w)
def differentiate(expr, wrt=None, wrt_list=None): """Return derivative of expression. This function returns an expression or list of expression objects corresponding to the derivative of the passed expression 'expr' with respect to a variable 'wrt' or list of variables 'wrt_list' Args: expr (Expression): Pyomo expression wrt (Var): Pyomo variable wrt_list (list): list of Pyomo variables Returns: Expression or list of Expression objects """ return diff_core.differentiate(expr=expr, wrt=wrt, wrt_list=wrt_list, mode=diff_core.Modes.sympy)
def dT_expression_exponential(cobj, prop, T, yc, tau=False): """ Create expressions for temperature derivative of CoolProp exponential sum forms with tau Args: cobj: Component object that will contain the parameters prop: name of property parameters are associated with T: temperature to use in expression yc: value of property at critical point tau: whether tau=Tc/T should be included in expression (default=False) Returns: Pyomo expression for temperature derivative of CoolProp exponential sum form with tau """ # y = yc * exp(Tc/T * sum(ni*theta^ti)) # Need d(y)/dT y = expression_exponential(cobj, prop, T, yc, tau) return differentiate(expr=y, wrt=T, mode=Modes.reverse_symbolic)
def test_bad_wrt(self): m = pyo.ConcreteModel() m.x = pyo.Var(initialize=0.23) with self.assertRaisesRegex(ValueError, r'Cannot specify both wrt and wrt_list'): ddx = differentiate(m.x**2, wrt=m.x, wrt_list=[m.x])
def calculate_variable_from_constraint(variable, constraint, eps=1e-8, iterlim=1000, linesearch=True, alpha_min=1e-8): """Calculate the variable value given a specified equality constraint This function calculates the value of the specified variable necessary to make the provided equality constraint feasible (assuming any other variables values are fixed). The method first attempts to solve for the variable value assuming it appears linearly in the constraint. If that doesn't converge the constraint residual, it falls back on Newton's method using exact (symbolic) derivatives. Notes ----- This is an unconstrained solver and is NOT guaranteed to respect the variable bounds or domain. The solver may leave the variable value in an infeasible state (outside the declared bounds or domain bounds). Parameters: ----------- variable: :py:class:`_VarData` The variable to solve for constraint: :py:class:`_ConstraintData` or relational expression or `tuple` The equality constraint to use to solve for the variable value. May be a `ConstraintData` object or any valid argument for ``Constraint(expr=<>)`` (i.e., a relational expression or 2- or 3-tuple) eps: `float` The tolerance to use to determine equality [default=1e-8]. iterlim: `int` The maximum number of iterations if this method has to fall back on using Newton's method. Raises RuntimeError on iteration limit [default=1000] linesearch: `bool` Decides whether or not to use the linesearch (recommended). [default=True] alpha_min: `float` The minimum fractional step to use in the linesearch [default=1e-8]. Returns: -------- None """ # Leverage all the Constraint logic to process the incoming tuple/expression if not isinstance(constraint, _ConstraintData): constraint = Constraint(expr=constraint, name=type(constraint).__name__) constraint.construct() body = constraint.body lower = constraint.lb upper = constraint.ub if lower != upper: raise ValueError("Constraint must be an equality constraint") if variable.value is None: # Note that we use "skip_validation=True" here as well, as the # variable domain may not admit the calculated initial guesses, # and we want to bypass that check. if variable.lb is None: if variable.ub is None: # no variable values, and no lower or upper bound - set # initial value to 0.0 variable.set_value(0, skip_validation=True) else: # no variable value or lower bound - set to 0 or upper # bound whichever is lower variable.set_value(min(0, variable.ub), skip_validation=True) elif variable.ub is None: # no variable value or upper bound - set to 0 or lower # bound, whichever is higher variable.set_value(max(0, variable.lb), skip_validation=True) else: # we have upper and lower bounds if variable.lb <= 0 and variable.ub >= 0: # set the initial value to 0 if bounds bracket 0 variable.set_value(0, skip_validation=True) else: # set the initial value to the midpoint of the bounds variable.set_value((variable.lb + variable.ub) / 2.0, skip_validation=True) # store the initial value to use later if necessary orig_initial_value = variable.value # solve the common case where variable is linear with coefficient of 1.0 x1 = value(variable) # Note: both the direct (linear) calculation and Newton's method # below rely on a numerically valid initial starting point. # While we have strategies for dealing with hitting numerically # invalid (e.g., sqrt(-1)) conditions below, if the initial point is # not valid, we will allow that exception to propagate up try: residual_1 = value(body) except: logger.error( "Encountered an error evaluating the expression at the " "initial guess.\n\tPlease provide a different initial guess.") raise variable.set_value(x1 - (residual_1 - upper), skip_validation=True) residual_2 = value(body, exception=False) # If we encounter an error while evaluating the expression at the # linear intercept calculated assuming the derivative was 1. This # is most commonly due to nonlinear expressions (like sqrt()) # becoming invalid/complex. We will skip the rest of the # "shortcuts" that assume the expression is linear and move directly # to using Newton's method. if residual_2 is not None and type(residual_2) is not complex: # if the variable appears linearly with a coefficient of 1, then we # are done if abs(residual_2 - upper) < eps: # Re-set the variable value to trigger any warnings WRT the # final variable state variable.set_value(variable.value) return # Assume the variable appears linearly and calculate the coefficient x2 = value(variable) slope = float(residual_1 - residual_2) / (x1 - x2) intercept = (residual_1 - upper) - slope * x1 if slope: variable.set_value(-intercept / slope, skip_validation=True) body_val = value(body, exception=False) if body_val is not None and abs(body_val - upper) < eps: # Re-set the variable value to trigger any warnings WRT # the final variable state variable.set_value(variable.value) return # Variable appears nonlinearly; solve using Newton's method # # restore initial value variable.set_value(orig_initial_value, skip_validation=True) expr = body - upper expr_deriv = differentiate(expr, wrt=variable, mode=differentiate.Modes.sympy) if type(expr_deriv) in native_numeric_types and expr_deriv == 0: raise ValueError("Variable derivative == 0, cannot solve for variable") if abs(value(expr_deriv)) < 1e-12: raise RuntimeError( 'Initial value for variable results in a derivative value that is ' 'very close to zero.\n\tPlease provide a different initial guess.') iter_left = iterlim fk = residual_1 - upper while abs(fk) > eps and iter_left: iter_left -= 1 if not iter_left: raise RuntimeError( "Iteration limit (%s) reached; remaining residual = %s" % (iterlim, value(expr))) # compute step xk = value(variable) try: fk = value(expr) if type(fk) is complex: raise ValueError( "Complex numbers are not allowed in Newton's method.") except: # We hit numerical problems with the last step (possible if # the line search is turned off) logger.error( "Newton's method encountered an error evaluating the " "expression.\n\tPlease provide a different initial guess " "or enable the linesearch if you have not.") raise fpk = value(expr_deriv) if abs(fpk) < 1e-12: raise RuntimeError( "Newton's method encountered a derivative that was too " "close to zero.\n\tPlease provide a different initial guess " "or enable the linesearch if you have not.") pk = -fk / fpk alpha = 1.0 xkp1 = xk + alpha * pk variable.set_value(xkp1, skip_validation=True) # perform line search if linesearch: c1 = 0.999 # ensure sufficient progress while alpha > alpha_min: # check if the value at xkp1 has sufficient reduction in # the residual fkp1 = value(expr, exception=False) # HACK for Python3 support, pending resolution of #879 # Issue #879 also pertains to other checks for "complex" # in this method. if type(fkp1) is complex: # We cannot perform computations on complex numbers fkp1 = None if fkp1 is not None and fkp1**2 < c1 * fk**2: # found an alpha value with sufficient reduction # continue to the next step fk = fkp1 break alpha /= 2.0 xkp1 = xk + alpha * pk variable.set_value(xkp1, skip_validation=True) if alpha <= alpha_min: residual = value(expr, exception=False) if residual is None or type(residual) is complex: residual = "{function evaluation error}" raise RuntimeError( "Linesearch iteration limit reached; remaining " "residual = %s." % (residual, )) # # Re-set the variable value to trigger any warnings WRT the final # variable state variable.set_value(variable.value)