def make_block(self, sample_time=0.5, horizon=1, nfe=2):
     model = make_model(horizon=horizon, nfe=nfe)
     time = model.time
     t0 = time.first()
     inputs = [model.flow_in[t0]]
     measurements = [model.conc[t0, 'A'], model.conc[t0, 'B']]
     scalar_vars, dae_vars = flatten_dae_components(
         model,
         time,
         pyo.Var,
     )
     category_dict = categorize_dae_variables(dae_vars,
                                              time,
                                              inputs,
                                              measurements=measurements)
     dyn_block = DynamicBlock(
         model=model,
         time=time,
         category_dict={None: category_dict},
         #inputs=inputs,
         #measurements=measurements,
     )
     dyn_block.construct()
     dyn_block.set_sample_time(sample_time)
     return dyn_block
    def test_empty_category(self):
        model = make_model(horizon=1, nfe=2)
        time = model.time
        t0 = time.first()
        inputs = [model.flow_in]
        measurements = [
            pyo.Reference(model.conc[:, 'A']),
            pyo.Reference(model.conc[:, 'B']),
        ]
        category_dict = {
            VC.INPUT: inputs,
            VC.MEASUREMENT: measurements,
            VC.ALGEBRAIC: [],
        }
        db = DynamicBlock(
            model=model,
            time=time,
            category_dict={None: category_dict},
        )
        db.construct()
        db.set_sample_time(0.5)

        # Categories with no variables are removed.
        # If they were retained, trying to iterate
        # over, e.g., vectors.algebraic[:, :] would
        # fail due to inconsistent dimension.
        assert VC.ALGEBRAIC not in db.category_dict
    def test_extra_category(self):
        model = make_model(horizon=1, nfe=2)
        time = model.time
        t0 = time.first()
        inputs = [model.flow_in]
        measurements = [
            pyo.Reference(model.conc[:, 'A']),
            pyo.Reference(model.conc[:, 'B']),
        ]
        disturbances = [
            pyo.Reference(model.conc_in[:, 'A']),
            pyo.Reference(model.conc_in[:, 'B']),
        ]
        category_dict = {
            VC.INPUT: inputs,
            VC.MEASUREMENT: measurements,
            VC.DISTURBANCE: disturbances,
        }
        db = DynamicBlock(
            model=model,
            time=time,
            category_dict={None: category_dict},
        )
        db.construct()
        db.set_sample_time(0.5)

        db.vectors.input[:, t0].set_value(1.1)
        db.vectors.measurement[:, t0].set_value(2.2)
        db.vectors.disturbance[:, t0].set_value(3.3)
        assert model.flow_in[t0].value == 1.1
        assert model.conc[t0, 'A'].value == 2.2
        assert model.conc[t0, 'B'].value == 2.2
        assert model.conc_in[t0, 'A'].value == 3.3
        assert model.conc_in[t0, 'B'].value == 3.3
Beispiel #4
0
 def make_block(self, sample_time=0.5, horizon=1, nfe=2):
     model = make_model(horizon=horizon, nfe=nfe)
     time = model.time
     t0 = time.first()
     inputs = [model.flow_in[t0]]
     measurements = [model.conc[t0,'A'], model.conc[t0,'B']]
     dyn_block = DynamicBlock(
             model=model,
             time=time,
             inputs=inputs,
             measurements=measurements,
             )
     dyn_block.construct()
     dyn_block.set_sample_time(sample_time)
     return dyn_block
Beispiel #5
0
    def test_set_sample_time(self):
        model = make_model(horizon=1, nfe=2)
        time = model.time
        t0 = time.first()
        inputs = [model.flow_in[t0]]
        measurements = [model.conc[t0,'A'], model.conc[t0,'B']]
        blk = DynamicBlock(
                model=model,
                time=time,
                inputs=inputs,
                measurements=measurements,
                )
        blk.construct()

        blk.set_sample_time(1.0)
        assert blk.sample_points == [0.0, 1.0]

        blk.set_sample_time(0.5)
        assert blk.sample_points == [0.0, 0.5, 1.0]
    def test_initialize_only_measurement_input(self):
        model = make_model(horizon=1, nfe=2)
        time = model.time
        t0 = time.first()
        inputs = [model.flow_in]
        measurements = [
            pyo.Reference(model.conc[:, 'A']),
            pyo.Reference(model.conc[:, 'B']),
        ]
        category_dict = {
            VC.INPUT: inputs,
            VC.MEASUREMENT: measurements,
        }
        db = DynamicBlock(
            model=model,
            time=time,
            category_dict={None: category_dict},
        )
        db.construct()
        db.set_sample_time(0.5)

        db.mod.flow_in[:].set_value(3.0)
        initialize_t0(db.mod)
        copy_values_forward(db.mod)
        db.mod.flow_in[:].set_value(2.0)

        # Don't need to know any of the special categories to initialize
        # by element. This is only because we have an implicit discretization.
        db.initialize_by_solving_elements(solver)

        t0 = time.first()
        tl = time.last()
        vectors = db.vectors
        assert vectors.input[0, tl].value == 2.0
        assert vectors.measurement[0, tl].value == pytest.approx(
            3.185595567867036)
        assert vectors.measurement[1, tl].value == pytest.approx(
            1.1532474073395755)
        assert model.dcdt[tl, 'A'].value == pytest.approx(0.44321329639889284)
        assert model.dcdt[tl, 'B'].value == pytest.approx(0.8791007531878847)
Beispiel #7
0
class NMPCSim(object):
    """
    This is a user-facing class to perform NMPC simulations with Pyomo
    models for both plant and controller. The user must provide the
    models to use for each, along with sets to treat as "time,"
    inputs in the plant model, and measurements in the controller
    model. Its functionality is primarily to ensure that these components
    (as defined by the names relative to the corresponding provided models)
    exist on both models.
    """

    # TODO: pyomo.common.config.add_docstring_list

    def __init__(self,
                 plant_model=None,
                 plant_time_set=None,
                 controller_model=None,
                 controller_time_set=None,
                 inputs_at_t0=None,
                 measurements=None,
                 sample_time=None,
                 **kwargs):
        """
        Measurements must be defined in the controller model.
        Inputs must be defined in the plant model.
        """
        # To find components in a model given a name,
        # modulo the index of some set:
        # i.   slice the component along the set
        # ii.  create a cuid from that slice
        # iii. get a reference to the slice from the cuid on the new model
        # iv.  access the reference at the index you want (optional)
        self.measurement_cuids = [
            ComponentUID(
                slice_component_along_sets(comp, (controller_time_set, )))
            for comp in measurements
        ]
        self.input_cuids = [
            ComponentUID(slice_component_along_sets(comp, (plant_time_set, )))
            for comp in inputs_at_t0
        ]

        p_t0 = plant_time_set.first()
        init_plant_measurements = [
            cuid.find_component_on(plant_model)[p_t0]
            for cuid in self.measurement_cuids
        ]

        self.plant = DynamicBlock(
            model=plant_model,
            time=plant_time_set,
            inputs=inputs_at_t0,
            measurements=init_plant_measurements,
        )
        self.plant.construct()

        # Here we repeat essentially the same "find component"
        # procedure as above.
        c_t0 = controller_time_set.first()
        init_controller_inputs = [
            cuid.find_component_on(controller_model)[c_t0]
            for cuid in self.input_cuids
        ]
        self.controller = ControllerBlock(
            model=controller_model,
            time=controller_time_set,
            inputs=init_controller_inputs,
            measurements=measurements,
        )
        self.controller.construct()

        if sample_time is not None:
            self.controller.set_sample_time(sample_time)
            self.plant.set_sample_time(sample_time)
            self.sample_time = sample_time
Beispiel #8
0
def main(plot_switch=False):

    # This tests the same model constructed in the test_nmpc_constructor_1 file
    m_controller = make_model(horizon=3, ntfe=30, ntcp=2, bounds=True)
    sample_time = 0.5
    m_plant = make_model(horizon=sample_time, ntfe=5, ntcp=2)
    time_plant = m_plant.fs.time

    solve_consistent_initial_conditions(m_plant, time_plant, solver)

    #####
    # Flatten and categorize controller model
    #####
    model = m_controller
    time = model.fs.time
    t0 = time.first()
    t1 = time[2]
    scalar_vars, dae_vars = flatten_dae_components(
        model,
        time,
        pyo.Var,
    )
    scalar_cons, dae_cons = flatten_dae_components(
        model,
        time,
        pyo.Constraint,
    )
    inputs = [
        model.fs.mixer.S_inlet.flow_vol,
        model.fs.mixer.E_inlet.flow_vol,
    ]
    measurements = [
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']),
        model.fs.cstr.outlet.temperature,
    ]
    model.fs.cstr.control_volume.material_holdup[:, 'aq', 'Solvent'].fix()
    model.fs.cstr.total_flow_balance.deactivate()
    var_partition, con_partition = categorize_dae_variables_and_constraints(
        model,
        dae_vars,
        dae_cons,
        time,
        input_vars=inputs,
    )
    controller = ControllerBlock(
        model=model,
        time=time,
        measurements=measurements,
        category_dict={None: var_partition},
    )
    controller.construct()

    solve_consistent_initial_conditions(m_controller, time, solver)
    controller.initialize_to_initial_conditions()

    m_controller._dummy_obj = pyo.Objective(expr=0)
    nlp = PyomoNLP(m_controller)
    igraph = IncidenceGraphInterface(nlp)
    m_controller.del_component(m_controller._dummy_obj)
    diff_vars = [var[t1] for var in var_partition[VC.DIFFERENTIAL]]
    alg_vars = [var[t1] for var in var_partition[VC.ALGEBRAIC]]
    deriv_vars = [var[t1] for var in var_partition[VC.DERIVATIVE]]
    diff_eqns = [con[t1] for con in con_partition[CC.DIFFERENTIAL]]
    alg_eqns = [con[t1] for con in con_partition[CC.ALGEBRAIC]]

    # Assemble and factorize "derivative Jacobian"
    dfdz = nlp.extract_submatrix_jacobian(diff_vars, diff_eqns)
    dfdy = nlp.extract_submatrix_jacobian(alg_vars, diff_eqns)
    dgdz = nlp.extract_submatrix_jacobian(diff_vars, alg_eqns)
    dgdy = nlp.extract_submatrix_jacobian(alg_vars, alg_eqns)
    dfdzdot = nlp.extract_submatrix_jacobian(deriv_vars, diff_eqns)
    fact = sps.linalg.splu(dgdy.tocsc())
    dydz = fact.solve(dgdz.toarray())
    deriv_jac = dfdz - dfdy.dot(dydz)
    fact = sps.linalg.splu(dfdzdot.tocsc())
    dzdotdz = -fact.solve(deriv_jac)

    # Use some heuristic on the eigenvalues of the derivative Jacobian
    # to identify fast states.
    w, V = np.linalg.eig(dzdotdz)
    w_max = np.max(np.abs(w))
    fast_modes, = np.where(np.abs(w) > w_max / 2)
    fast_states = []
    for idx in fast_modes:
        evec = V[:, idx]
        _fast_states, _ = np.where(np.abs(evec) > 0.5)
        fast_states.extend(_fast_states)
    fast_states = set(fast_states)

    # Store components necessary for model reduction in a model-
    # independent form.
    fast_state_derivs = [
        pyo.ComponentUID(var_partition[VC.DERIVATIVE][idx].referent,
                         context=model) for idx in fast_states
    ]
    fast_state_diffs = [
        pyo.ComponentUID(var_partition[VC.DIFFERENTIAL][idx].referent,
                         context=model) for idx in fast_states
    ]
    fast_state_discs = [
        pyo.ComponentUID(con_partition[CC.DISCRETIZATION][idx].referent,
                         context=model) for idx in fast_states
    ]

    # Perform pseudo-steady state model reduction on the fast states
    # and re-categorize
    for cuid in fast_state_derivs:
        var = cuid.find_component_on(m_controller)
        var.fix(0.0)
    for cuid in fast_state_diffs:
        var = cuid.find_component_on(m_controller)
        var[t0].unfix()
    for cuid in fast_state_discs:
        con = cuid.find_component_on(m_controller)
        con.deactivate()

    var_partition, con_partition = categorize_dae_variables_and_constraints(
        model,
        dae_vars,
        dae_cons,
        time,
        input_vars=inputs,
    )
    controller.del_component(model)

    # Re-construct controller block with new categorization
    measurements = [
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']),
    ]
    controller = ControllerBlock(
        model=model,
        time=time,
        measurements=measurements,
        category_dict={None: var_partition},
    )
    controller.construct()

    #####
    # Construct dynamic block for plant
    #####
    model = m_plant
    time = model.fs.time
    t0 = time.first()
    t1 = time[2]
    scalar_vars, dae_vars = flatten_dae_components(
        model,
        time,
        pyo.Var,
    )
    scalar_cons, dae_cons = flatten_dae_components(
        model,
        time,
        pyo.Constraint,
    )
    inputs = [
        model.fs.mixer.S_inlet.flow_vol,
        model.fs.mixer.E_inlet.flow_vol,
    ]
    measurements = [
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']),
    ]
    model.fs.cstr.control_volume.material_holdup[:, 'aq', 'Solvent'].fix()
    model.fs.cstr.total_flow_balance.deactivate()

    var_partition, con_partition = categorize_dae_variables_and_constraints(
        model,
        dae_vars,
        dae_cons,
        time,
        input_vars=inputs,
    )
    plant = DynamicBlock(
        model=model,
        time=time,
        measurements=measurements,
        category_dict={None: var_partition},
    )
    plant.construct()

    p_t0 = plant.time.first()
    c_t0 = controller.time.first()
    p_ts = plant.sample_points[1]
    c_ts = controller.sample_points[1]

    controller.set_sample_time(sample_time)
    plant.set_sample_time(sample_time)

    # We now perform the "RTO" calculation: Find the optimal steady state
    # to achieve the following setpoint
    setpoint = [
        (controller.mod.fs.cstr.outlet.conc_mol[0, 'P'], 0.4),
        #(controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 0.01),
        (controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 0.1),
        (controller.mod.fs.cstr.control_volume.energy_holdup[0, 'aq'], 300),
        (controller.mod.fs.mixer.E_inlet.flow_vol[0], 0.1),
        (controller.mod.fs.mixer.S_inlet.flow_vol[0], 2.0),
        (controller.mod.fs.cstr.volume[0], 1.0),
    ]
    setpoint_weights = [
        (controller.mod.fs.cstr.outlet.conc_mol[0, 'P'], 1.),
        (controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 1.),
        (controller.mod.fs.cstr.control_volume.energy_holdup[0, 'aq'], 1.),
        (controller.mod.fs.mixer.E_inlet.flow_vol[0], 1.),
        (controller.mod.fs.mixer.S_inlet.flow_vol[0], 1.),
        (controller.mod.fs.cstr.volume[0], 1.),
    ]

    # Some of the "differential variables" that have been fixed in the
    # model file are different from the measurements listed above. We
    # unfix them here so the RTO solve is not overconstrained.
    # (The RTO solve will only automatically unfix inputs and measurements.)
    controller.mod.fs.cstr.control_volume.material_holdup[0, ...].unfix()
    controller.mod.fs.cstr.control_volume.energy_holdup[0, ...].unfix()
    #controller.mod.fs.cstr.volume[0].unfix()
    controller.mod.fs.cstr.control_volume.material_holdup[0, 'aq',
                                                          'Solvent'].fix()

    controller.add_setpoint_objective(setpoint, setpoint_weights)
    controller.solve_setpoint(solver)

    # Now we are ready to construct the tracking NMPC problem
    tracking_weights = [
        *((v, 1.) for v in controller.vectors.differential[:, 0]),
        *((v, 1.) for v in controller.vectors.input[:, 0]),
    ]

    controller.add_tracking_objective(tracking_weights)

    controller.constrain_control_inputs_piecewise_constant()

    controller.initialize_to_initial_conditions()

    # Solve the first control problem
    controller.vectors.input[...].unfix()
    controller.vectors.input[:, 0].fix()
    solver.solve(controller, tee=True)

    # For a proper NMPC simulation, we must have noise.
    # We do this by treating inputs and measurements as Gaussian random
    # variables with the following variances (and bounds).
    cstr = controller.mod.fs.cstr
    variance = [
        (cstr.outlet.conc_mol[0.0, 'S'], 0.01),
        (cstr.outlet.conc_mol[0.0, 'E'], 0.005),
        (cstr.outlet.conc_mol[0.0, 'C'], 0.01),
        (cstr.outlet.conc_mol[0.0, 'P'], 0.005),
        (cstr.outlet.temperature[0.0], 1.),
        (cstr.volume[0.0], 0.05),
    ]
    controller.set_variance(variance)
    measurement_variance = [
        v.variance for v in controller.MEASUREMENT_BLOCK[:].var
    ]
    measurement_noise_bounds = [(0.0, var[c_t0].ub)
                                for var in controller.MEASUREMENT_BLOCK[:].var]

    mx = plant.mod.fs.mixer
    variance = [
        (mx.S_inlet_state[0.0].flow_vol, 0.02),
        (mx.E_inlet_state[0.0].flow_vol, 0.001),
    ]
    plant.set_variance(variance)
    input_variance = [v.variance for v in plant.INPUT_BLOCK[:].var]
    input_noise_bounds = [(0.0, var[p_t0].ub)
                          for var in plant.INPUT_BLOCK[:].var]

    random.seed(100)

    # Extract inputs from controller and inject them into plant
    inputs = controller.generate_inputs_at_time(c_ts)
    plant.inject_inputs(inputs)

    # This "initialization" really simulates the plant with the new inputs.
    plant.vectors.input[:, :].fix()
    plant.initialize_by_solving_elements(solver)
    plant.vectors.input[:, :].fix()
    solver.solve(plant, tee=True)

    for i in range(1, 11):
        print('\nENTERING NMPC LOOP ITERATION %s\n' % i)
        measured = plant.generate_measurements_at_time(p_ts)
        plant.advance_one_sample()
        plant.initialize_to_initial_conditions()
        measured = apply_noise_with_bounds(
            measured,
            measurement_variance,
            random.gauss,
            measurement_noise_bounds,
        )

        controller.advance_one_sample()
        controller.load_measurements(measured)

        solver.solve(controller, tee=True)

        inputs = controller.generate_inputs_at_time(c_ts)
        inputs = apply_noise_with_bounds(
            inputs,
            input_variance,
            random.gauss,
            input_noise_bounds,
        )
        plant.inject_inputs(inputs)

        plant.initialize_by_solving_elements(solver)
        solver.solve(plant)

    import pdb
    pdb.set_trace()