def test_flat_model(self): m = ConcreteModel() m.T = ContinuousSet(bounds=(0, 1)) m.x = Var() m.y = Var([1, 2]) m.a = Var(m.T) m.b = Var(m.T, [1, 2]) m.c = Var([3, 4], m.T) regular, time = flatten_dae_variables(m, m.T) regular_id = set(id(_) for _ in regular) self.assertEqual(len(regular), 3) self.assertIn(id(m.x), regular_id) self.assertIn(id(m.y[1]), regular_id) self.assertIn(id(m.y[2]), regular_id) # Output for debugging #for v in time: # v.pprint() # for _ in v.values(): # print" -> ", _.name ref_data = { self._hashRef(Reference(m.a[:])), self._hashRef(Reference(m.b[:, 1])), self._hashRef(Reference(m.b[:, 2])), self._hashRef(Reference(m.c[3, :])), self._hashRef(Reference(m.c[4, :])), } self.assertEqual(len(time), len(ref_data)) for ref in time: self.assertIn(self._hashRef(ref), ref_data)
def test_2level_model(self): m = ConcreteModel() m.T = ContinuousSet(bounds=(0,1)) @m.Block([1,2],m.T) def B(b, i, t): @b.Block(list(range(2*i, 2*i+2))) def bb(bb, j): bb.y = Var([10,11]) b.x = Var(list(range(2*i, 2*i+2))) regular, time = flatten_dae_variables(m, m.T) self.assertEqual(len(regular), 0) # Output for debugging #for v in time: # v.pprint() # for _ in v.values(): # print" -> ", _.name ref_data = { self._hashRef(Reference(m.B[1,:].x[2])), self._hashRef(Reference(m.B[1,:].x[3])), self._hashRef(Reference(m.B[2,:].x[4])), self._hashRef(Reference(m.B[2,:].x[5])), self._hashRef(Reference(m.B[1,:].bb[2].y[10])), self._hashRef(Reference(m.B[1,:].bb[2].y[11])), self._hashRef(Reference(m.B[1,:].bb[3].y[10])), self._hashRef(Reference(m.B[1,:].bb[3].y[11])), self._hashRef(Reference(m.B[2,:].bb[4].y[10])), self._hashRef(Reference(m.B[2,:].bb[4].y[11])), self._hashRef(Reference(m.B[2,:].bb[5].y[10])), self._hashRef(Reference(m.B[2,:].bb[5].y[11])), } self.assertEqual(len(time), len(ref_data)) for ref in time: self.assertIn(self._hashRef(ref), ref_data)
def test_initialize_by_element_in_range(): mod = make_model(horizon=2, ntfe=20) assert degrees_of_freedom(mod) == 0 scalar_vars, dae_vars = flatten_dae_variables(mod.fs, mod.fs.time) diff_vars = [ Reference(mod.fs.cstr.control_volume.energy_holdup[:, 'aq']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'S']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'E']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'C']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'P']) ] initialize_by_element_in_range(mod.fs, mod.fs.time, 0, 1, solver=solver, dae_vars=dae_vars, time_linking_variables=diff_vars, outlvl=idaeslog.DEBUG, solve_initial_conditions=True) assert degrees_of_freedom(mod.fs) == 0 assert mod.fs.cstr.outlet.conc_mol[1, 'S'].value == approx(10.189, abs=1e-3) assert mod.fs.cstr.outlet.conc_mol[1, 'C'].value == approx(0.4275, abs=1e-4) assert mod.fs.cstr.outlet.conc_mol[1, 'E'].value == approx(0.0541, abs=1e-4) assert mod.fs.cstr.outlet.conc_mol[1, 'P'].value == approx(0.3503, abs=1e-4) initialize_by_element_in_range(mod.fs, mod.fs.time, 1, 2, solver=solver, dae_vars=dae_vars, outlvl=idaeslog.DEBUG) assert degrees_of_freedom(mod.fs) == 0 for con in activated_equalities_generator(mod.fs): assert value(con.body) - value(con.upper) < 1e-5 assert mod.fs.cstr.outlet.conc_mol[2, 'S'].value == approx(11.263, abs=1e-3) assert mod.fs.cstr.outlet.conc_mol[2, 'C'].value == approx(0.4809, abs=1e-4) assert mod.fs.cstr.outlet.conc_mol[2, 'E'].value == approx(0.0538, abs=1e-4) assert mod.fs.cstr.outlet.conc_mol[2, 'P'].value == approx(0.4372, abs=1e-4)
def test_find_slices_in_model(): # Define m1 m1 = ConcreteModel() m1.time = Set(initialize=[1, 2, 3, 4, 5]) m1.v1 = Var(m1.time, initialize=1) @m1.Block(m1.time) def blk(b, t): b.v2 = Var(initialize=1) # Define m2 m2 = ConcreteModel() m2.time = Set(initialize=[1, 2, 3, 4, 5]) m2.v1 = Var(m2.time, initialize=2) @m2.Block(m2.time) def blk(b, t): b.v2 = Var(initialize=2) ### scalar_vars_1, dae_vars_1 = flatten_dae_variables(m1, m1.time) scalar_vars_2, dae_vars_2 = flatten_dae_variables(m2, m2.time) t0_tgt = m1.time.first() group = NMPCVarGroup(dae_vars_1, m1.time) categ = VariableCategory.ALGEBRAIC locator = ComponentMap([(var[t0_tgt], NMPCVarLocator(categ, group, i)) for i, var in enumerate(dae_vars_1)]) tgt_slices = find_slices_in_model(m1, m1.time, m2, m2.time, locator, dae_vars_2) dae_var_set_1 = ComponentSet(dae_vars_1) assert len(dae_var_set_1) == len(tgt_slices) assert len(tgt_slices) == len(dae_vars_2) for i, _slice in enumerate(tgt_slices): assert dae_vars_2[i].name == _slice.name assert _slice in dae_var_set_1
def test_copy_values(): # Define m1 m1 = ConcreteModel() m1.time = Set(initialize=[1, 2, 3, 4, 5]) m1.v1 = Var(m1.time, initialize=1) @m1.Block(m1.time) def blk(b, t): b.v2 = Var(initialize=1) # Define m2 m2 = ConcreteModel() m2.time = Set(initialize=[1, 2, 3, 4, 5]) m2.v1 = Var(m2.time, initialize=2) @m2.Block(m2.time) def blk(b, t): b.v2 = Var(initialize=2) ### scalar_vars_1, dae_vars_1 = flatten_dae_variables(m1, m1.time) scalar_vars_2, dae_vars_2 = flatten_dae_variables(m2, m2.time) m2.v1[2].set_value(5) m2.blk[2].v2.set_value(5) copy_values_at_time(dae_vars_1, dae_vars_2, 1, 2) for t in m1.time: if t != 1: assert m1.v1[t].value == 1 assert m1.blk[t].v2.value == 1 else: assert m1.v1[t].value == 5 assert m1.blk[t].v2.value == 5
def test_2dim_set(self): m = ConcreteModel() m.time = ContinuousSet(bounds=(0, 1)) m.v = Var(m.time, [('a', 1), ('b', 2)]) scalar, dae = flatten_dae_variables(m, m.time) self.assertEqual(len(scalar), 0) ref_data = { self._hashRef(Reference(m.v[:, 'a', 1])), self._hashRef(Reference(m.v[:, 'b', 2])), } self.assertEqual(len(dae), len(ref_data)) for ref in dae: self.assertIn(self._hashRef(ref), ref_data)
def test_indexed_block(self): m = ConcreteModel() m.time = ContinuousSet(bounds=(0,1)) m.comp = Set(initialize=['a', 'b']) def bb_rule(bb, t): bb.dae_var = Var() def b_rule(b, c): b.bb = Block(m.time, rule=bb_rule) m.b = Block(m.comp, rule=b_rule) scalar, dae = flatten_dae_variables(m, m.time) self.assertEqual(len(scalar), 0) ref_data = { self._hashRef(Reference(m.b['a'].bb[:].dae_var)), self._hashRef(Reference(m.b['b'].bb[:].dae_var)), } self.assertEqual(len(dae), len(ref_data)) for ref in dae: self.assertIn(self._hashRef(ref), ref_data)
def initialize_by_element_in_range(model, time, t_start, t_end, time_linking_vars=[], dae_vars=[], max_linking_range=0, **kwargs): """Function for solving a square model, time element-by-time element, between specified start and end times. Args: model : Flowsheet model to solve t_start : Beginning of timespan over which to solve t_end : End of timespan over which to solve Kwargs: solver : Solver option used to solve portions of the square model outlvl : idaes.logger output level """ # TODO: How to handle config arguments here? Should this function # be moved to be a method of NMPC? Have a module-level config? # CONFIG, KWARGS: handle these kwargs through config solver = kwargs.pop('solver', SolverFactory('ipopt')) outlvl = kwargs.pop('outlvl', idaeslog.NOTSET) init_log = idaeslog.getInitLogger('nmpc', outlvl) solver_log = idaeslog.getSolveLogger('nmpc', outlvl) solve_initial_conditions = kwargs.pop('solve_initial_conditions', False) #TODO: Move to docstring # Variables that will be fixed for time points outside the finite element # when constraints for a finite element are activated. # For a "normal" process, these should just be differential variables # (and maybe derivative variables). For a process with a (PID) controller, # these should also include variables used by the controller. # If these variables are not specified, # Timespan over which these variables will be fixed, counting backwards # from the first time point in the finite element (which will always be # fixed) # Should I specify max_linking_range as an integer number of finite # elements, an integer number of time points, or a float in actual time # units? Go with latter for now. # TODO: Should I fix scalar vars? Intuition is that they should already # be fixed. #time = model.time assert t_start in time.get_finite_elements() assert t_end in time.get_finite_elements() assert degrees_of_freedom(model) == 0 #dae_vars = kwargs.pop('dae_vars', []) if not dae_vars: scalar_vars, dae_vars = flatten_dae_variables(model, time) for var in scalar_vars: var.fix() deactivate_constraints_unindexed_by(model, time) ncp = time.get_discretization_info()['ncp'] fe_in_range = [i for i, fe in enumerate(time.get_finite_elements()) if fe >= t_start and fe <= t_end] t_in_range = [t for t in time if t >= t_start and t <= t_end] fe_in_range.pop(0) n_fe_in_range = len(fe_in_range) was_originally_active = get_activity_dict(model) was_originally_fixed = get_fixed_dict(model) # Deactivate model if not solve_initial_conditions: time_list = [t for t in time] deactivated = deactivate_model_at(model, time, time_list, outlvl=idaeslog.ERROR) else: time_list = [t for t in time if t != time.first()] deactivated = deactivate_model_at(model, time, time_list, outlvl=idaeslog.ERROR) assert degrees_of_freedom(model) == 0 with idaeslog.solver_log(solver_log, level=idaeslog.DEBUG) as slc: results = solver.solve(model, tee=slc.tee) if results.solver.termination_condition == TerminationCondition.optimal: pass else: raise ValueError deactivated[time.first()] = deactivate_model_at(model, time, time.first(), outlvl=idaeslog.ERROR)[time.first()] # "Integration" loop for i in fe_in_range: t_prev = time[(i-1)*ncp+1] fe = [time[k] for k in range((i-1)*ncp+2, i*ncp+2)] con_list = [] for t in fe: # These will be fixed vars in constraints at t # Probably not necessary to record at what t # they occur for comp in deactivated[t]: if was_originally_active[id(comp)]: comp.activate() if not time_linking_vars: if isinstance(comp, _ConstraintData): con_list.append(comp) elif isinstance(comp, _BlockData): # Active here should be independent of whether block # was active con_list.extend( list(comp.component_data_objects(Constraint, active=True))) if not time_linking_vars: fixed_vars = [] for con in con_list: for var in identify_variables(con.expr, include_fixed=False): # use var_locator/ComponentMap to get index somehow t_idx = get_implicit_index_of_set(var, time) if t_idx is None: assert not is_in_block_indexed_by(var, time) continue if t_idx <= t_prev: fixed_vars.append(var) var.fix() else: fixed_vars = [] time_range = [t for t in time if t_prev - t <= max_linking_range and t <= t_prev] time_range = [t_prev] for _slice in time_linking_vars: for t in time_range: #if not _slice[t].fixed: _slice[t].fix() fixed_vars.append(_slice[t]) # Here I assume that the only variables that can appear in # constraints at a different (later) time index are derivatives # and differential variables (they do so in the discretization # equations) and that they only participate at t_prev. # # This is not the case for, say, PID controllers, in which case # I should pass in a list of "complicating variables," then fix # them at all time points outside the finite element. # # Alternative solution is to identify_variables in each constraint # that is activated and fix those belonging to a previous finite # element. (Should not encounter variables belonging to a future # finite element.) # ^ This option is easier, less efficient # # In either case need to record whether variable was previously fixed # so I know if I should unfix it or not. for t in fe: for _slice in dae_vars: if not _slice[t].fixed: # Fixed DAE variables are time-dependent disturbances, # whose values should not be altered by this function. _slice[t].set_value(_slice[t_prev].value) assert degrees_of_freedom(model) == 0 with idaeslog.solver_log(solver_log, level=idaeslog.DEBUG) as slc: results = solver.solve(model, tee=slc.tee) if results.solver.termination_condition == TerminationCondition.optimal: pass else: raise ValueError for t in fe: for comp in deactivated[t]: comp.deactivate() for var in fixed_vars: if not was_originally_fixed[id(var)]: var.unfix() for t in time: for comp in deactivated[t]: if was_originally_active[id(comp)]: comp.activate() assert degrees_of_freedom(model) == 0
def test_add_noise_at_time(): mod = make_model(horizon=2, ntfe=20) time = mod.fs.time t0 = time.first() assert degrees_of_freedom(mod) == 0 scalar_vars, dae_vars = flatten_dae_variables(mod.fs, time) diff_vars = [ Reference(mod.fs.cstr.control_volume.energy_holdup[:, 'aq']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'S']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'E']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'C']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'P']) ] for t in time: diff_vars[0][t].setlb(290) diff_vars[0][t].setub(310) for i in range(1, 5): diff_vars[i][t].setlb(0) diff_vars[i][t].setub(1) # Pretend this is mole fraction... assert diff_vars[0][0].value == 300 for i in range(1, 5): assert diff_vars[i][0].value == 0.001 copy_values_at_time(diff_vars, diff_vars, [t for t in time if t != t0], t0) for seed in [4, 8, 15, 16, 23, 42]: random.seed(seed) weights = [10, 0.001, 0.001, 0.001, 0.001] nom_vals = add_noise_at_time(diff_vars, 0, weights=weights) assert nom_vals[0][0] == 300 assert diff_vars[0][0].value != 300 assert diff_vars[0][0].value == approx(300, abs=2) for i in range(1, 5): assert nom_vals[0][i] == 0.001 # ^ nom_vals indexed by time, then var-index. This is confusing, # might need to change (or only accept one time point at a time) assert diff_vars[i][0].value != 0.001 assert diff_vars[i][0].value == approx(0.001, abs=2e-4) # Within four standard deviations should be a safe check for i in range(0, 5): diff_vars[i][0].set_value(nom_vals[0][i]) # Reset and try again with new seed # Try providing function for uniform random rand_fcn = random.uniform random_arg_dict = { 'range_list': [(295, 305), (0.001, 0.01), (0.001, 0.01), (0.001, 0.01), (0.001, 0.01)] } def args_fcn(i, val, **kwargs): # args_fcn expects arguments like this range_list = kwargs.pop('range_list', None) return range_list[i] nom_vals = add_noise_at_time(diff_vars, 0.5, random_function=rand_fcn, args_function=args_fcn, random_arg_dict=random_arg_dict) assert nom_vals[0.5][0] == 300 assert diff_vars[0][0.5].value != 300 assert 295 <= diff_vars[0][0.5].value <= 305 for i in range(1, 5): assert nom_vals[0.5][i] == 0.001 assert diff_vars[i][0.5].value != 0.001 assert 0.001 <= diff_vars[i][0.5].value <= 0.01 # Try to get some bound violations random_arg_dict = { 'range_list': [(295, 305), (1, 2), (1, 2), (1, 2), (1, 2)] } nom_vals = add_noise_at_time(diff_vars, 1, random_function=rand_fcn, args_function=args_fcn, random_arg_dict=random_arg_dict, bound_strategy='push', bound_push=0.01) for i in range(1, 5): assert diff_vars[i][1].value == 0.99 random.seed(123) with pytest.raises(ValueError) as exc_test: # Large weights - one of these lower bounds should fail... nom_vals = add_noise_at_time(diff_vars, 1.5, bound_strategy='discard', discard_limit=0, weights=[1, 1, 1, 1, 1], sig_0=0.05) @pytest.mark.unit def test_get_violated_bounds_at_time(): m = ConcreteModel() m.time = Set(initialize=[1, 2, 3]) m.v = Var(m.time, ['a', 'b', 'c'], initialize=5) varlist = [ Reference(m.v[:, 'a']), Reference(m.v[:, 'b']), Reference(m.v[:, 'c']) ] group = NMPCVarGroup(varlist, m.time) group.set_lb(0, 0) group.set_lb(1, 6) group.set_lb(2, 0) group.set_ub(0, 4) group.set_ub(1, 10) group.set_ub(2, 10) violated = get_violated_bounds_at_time(group, [1, 2, 3], tolerance=1e-8) violated_set = ComponentSet(violated) for t in m.time: assert m.v[t, 'a'] in violated_set assert m.v[t, 'b'] in violated_set violated = get_violated_bounds_at_time(group, 2, tolerance=1e-8) violated_set = ComponentSet(violated) assert m.v[2, 'a'] in violated_set assert m.v[2, 'b'] in violated_set
def categorize_variables(model, initial_inputs): """Creates lists of time-only-slices of the different types of variables in a model, given knowledge of which are inputs. These lists are added as attributes to the model's namespace. Possible variable categories are: - INPUT --- Those specified by the user to be inputs - DERIVATIVE --- Those declared as Pyomo DerivativeVars, whose "state variable" is not fixed, except possibly as an initial condition - DIFFERENTIAL --- Those referenced as the "state variable" by an unfixed (except possibly as an initial condition) DerivativeVar - FIXED --- Those that are fixed at non-initial time points. These are typically disturbances, design variables, or uncertain parameters. - ALGEBRAIC --- Unfixed, time-indexed variables that are neither inputs nor referenced by an unfixed derivative. - SCALAR --- Variables unindexed by time. These could be variables that refer to a specific point in time (initial or final conditions), averages over time, or truly time-independent variables like diameter. Args: model : Model whose variables will be flattened and categorized initial_inputs : List of VarData objects that are input variables at the initial time point """ namespace = getattr(model, DynamicBase.get_namespace_name()) time = namespace.get_time() t0 = time.first() t1 = time.get_finite_elements()[1] deriv_vars = [] diff_vars = [] input_vars = [] alg_vars = [] fixed_vars = [] ic_vars = [] # Create list of time-only-slices of time indexed variables # (And list of VarData objects for scalar variables) scalar_vars, dae_vars = flatten_dae_variables(model, time) dae_map = ComponentMap([(v[t0], v) for v in dae_vars]) t0_vardata = list(dae_map.keys()) namespace.dae_vars = list(dae_map.values()) namespace.scalar_vars = \ NMPCVarGroup( list(ComponentMap([(v, v) for v in scalar_vars]).values()), index_set=None, is_scalar=True) namespace.n_scalar_vars = \ namespace.scalar_vars.n_vars input_set = ComponentSet(initial_inputs) updated_input_set = ComponentSet(initial_inputs) # Iterate over initial vardata, popping from dae map when an input, # derivative, or differential var is found. for var0 in t0_vardata: if var0 in updated_input_set: input_set.remove(var0) time_slice = dae_map.pop(var0) input_vars.append(time_slice) parent = var0.parent_component() if not isinstance(parent, DerivativeVar): continue if not time in ComponentSet(parent.get_continuousset_list()): continue index0 = var0.index() var1 = dae_map[var0][t1] index1 = var1.index() state = parent.get_state_var() if state[index1].fixed: # Assume state var is fixed everywhere, so derivative # 'isn't really' a derivative. # Should be safe to remove state from dae_map here state_slice = dae_map.pop(state[index0]) fixed_vars.append(state_slice) continue if state[index0] in input_set: # If differential variable is an input, then this DerivativeVar # is 'not really a derivative' continue deriv_slice = dae_map.pop(var0) if var1.fixed: # Assume derivative has been fixed everywhere. # Add to list of fixed variables, and don't remove its state variable. fixed_vars.append(deriv_slice) elif var0.fixed: # In this case the derivative has been used as an initial condition. # Still want to include it in the list of derivatives. ic_vars.append(deriv_slice) state_slice = dae_map.pop(state[index0]) if state[index0].fixed: ic_vars.append(state_slice) deriv_vars.append(deriv_slice) diff_vars.append(state_slice) else: # Neither is fixed. This should be the most common case. state_slice = dae_map.pop(state[index0]) if state[index0].fixed: ic_vars.append(state_slice) deriv_vars.append(deriv_slice) diff_vars.append(state_slice) if not updated_input_set: raise RuntimeError('Not all inputs could be found') assert len(deriv_vars) == len(diff_vars) for var0, time_slice in dae_map.items(): var1 = time_slice[t1] # If the variable is still in the list of time-indexed vars, # it must either be fixed (not a var) or be an algebraic var if var1.fixed: fixed_vars.append(time_slice) else: if var0.fixed: ic_vars.append(time_slice) alg_vars.append(time_slice) namespace.deriv_vars = NMPCVarGroup(deriv_vars, time) namespace.diff_vars = NMPCVarGroup(diff_vars, time) namespace.n_diff_vars = len(diff_vars) namespace.n_deriv_vars = len(deriv_vars) assert (namespace.n_diff_vars == namespace.n_deriv_vars) # ic_vars will not be stored as a NMPCVarGroup - don't want to store # all the info twice namespace.ic_vars = ic_vars namespace.n_ic_vars = len(ic_vars) #assert model.n_dv == len(ic_vars) # Would like this to be true, but accurately detecting differential # variables that are not implicitly fixed (by fixing some input) # is difficult # Also, a categorization can have no input vars and still be # valid for MHE namespace.input_vars = NMPCVarGroup(input_vars, time) namespace.n_input_vars = len(input_vars) namespace.alg_vars = NMPCVarGroup(alg_vars, time) namespace.n_alg_vars = len(alg_vars) namespace.fixed_vars = NMPCVarGroup(fixed_vars, time) namespace.n_fixed_vars = len(fixed_vars) namespace.variables_categorized = True