Results

A guide for querying optimized InfiniteOpt models. See the respective technical manual for more details.

Overview

So far we have covered defining, transforming, and optimizing InfiniteModels. Now comes the point to extract information from our optimized model. This is done following extended versions of JuMPs querying functions in combination with the mapping information stored in the optimizer model. Thus, this page will walk through the use of these result query functions.

Basic Usage

Let's revisit the example from the optimization page to get us started:

julia> using InfiniteOpt, Ipopt;

julia> model = InfiniteModel(Ipopt.Optimizer);

julia> set_optimizer_attribute(model, "print_level", 0);

julia> @infinite_parameter(model, t in [0, 10], num_supports = 10);

julia> @variable(model, y >= 0, Infinite(t));

julia> @variable(model, z >= 0);

julia> @objective(model, Min, 2z);

julia> @constraint(model, c1, z >= y);

julia> @constraint(model, c2, y(0) == 42);

julia> print(model)
Min 2 z
Subject to
 y(t) ≥ 0.0, ∀ t ∈ [0, 10]
 z ≥ 0.0
 c1 : z - y(t) ≥ 0.0, ∀ t ∈ [0, 10]
 y(0) ≥ 0.0
 c2 : y(0) = 42.0

julia> optimize!(model)

Now that the model has been optimized, let's find out what happened. To determine why the optimizer stopped, we can use termination_status to report the corresponding MathOptInterface termination code (possible codes are explained here.

julia> termination_status(model)
LOCALLY_SOLVED::TerminationStatusCode = 4

Here we see that our model was locally solved via Ipopt and that is why it stopped. Furthermore, we can query the primal and dual problem optimalities via primal_status and dual_status, respectively.

julia> primal_status(model)
FEASIBLE_POINT::ResultStatusCode = 1

julia> dual_status(model)
FEASIBLE_POINT::ResultStatusCode = 1

The possible statuses are detailed here. These results are useful in knowing if information can be drawn from the primal and/or dual and what it means. We can also verify that we indeed have answers via has_values which indicates if our model has optimized variable values.

julia> has_values(model)
true

And indeed we do have values.

Now we can query the objective value via objective_value which reports the optimal objective value.

julia> objective_value(model)
83.99999998250514

Great now we can inquire about variable values via value. First, let's retrieve the value of z:

julia> value(z)
41.99999999125257

We get a single value since z is a FiniteVariable and therefore finite. Now let's retrieve the "value" of y(t) which is infinite with respect to t:

julia> value(y)
10-element Vector{Float64}:
 42.0
 20.999999995620495
 20.999999995620495
 20.999999995620495
 20.999999995620495
 20.999999995620495
 20.999999995620495
 20.999999995620495
 20.999999995620495
 20.999999995620495

Notice here we obtain an array of values since these correspond to the transcribed finite (discretized) variables used to solve the problem. We obtain the corresponding support (discretized t) values via supports:

julia> supports(y)
10-element Vector{Tuple}:
 (0.0,)
 (1.11111111111,)
 (2.22222222222,)
 (3.33333333333,)
 (4.44444444444,)
 (5.55555555556,)
 (6.66666666667,)
 (7.77777777778,)
 (8.88888888889,)
 (10.0,)

There is 1-to-1 correspondence between these supports and the values reported above. Note that these are stored in tuples to facilitate multiple infinite parameter dependencies.

Note

The values for an array of variables is obtained via the vectorized call of value following the syntax:

value.(::AbstractArray{<:GeneralVariableRef})

This also holds true for many other methods in InfiniteOpt. For example, adding the dot also vectorizes dual and set_binary.

We can also query the dual of a constraint via dual if a model has duals available as indicated by has_duals:

julia> has_duals(model)
true

julia> dual(c1)
10-element Vector{Float64}:
 1.9999999988666093
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10

c1 is an infinite constraint and thus we obtain the duals of its transcribed versions. The underlying infinite parameter(s) and support values are queried via parameter_refs and supports:

julia> parameter_refs(c1)
(t,)

julia> supports(c1)
10-element Vector{Tuple}:
 (0.0,)
 (1.11111111111,)
 (2.22222222222,)
 (3.33333333333,)
 (4.44444444444,)
 (5.55555555556,)
 (6.66666666667,)
 (7.77777777778,)
 (8.88888888889,)
 (10.0,)

These again all have a 1-to-1 correspondence.

Note

In the case that our variables/constraints depend on multiple infinite parameter it is typically convenient to add the keyword statement ndarray = true when calling any variable/constraint queries (e.g., value and dual). This will reformat the output vector into a n-dimensional array whose dimensions correspond to the supports of the infinite parameters.

Termination Queries

Termination queries are those that question about how the infinite model was solved and what its optimized state entails. Programmatically, such queries on the InfiniteModel are simply routed to its optimizer model.

The commonly used queries include termination_status, primal_status, dual_status, objective_value, result_count, and solve_time. The first four are well exemplified in the Basic Usage section above and are helpful in quickly understanding the optimality status of a given model following the many possible statuses reported by MathOptInterface which are documented here. We use result_count to determine how many solutions are recorded in the optimizer.

julia> result_count(model)
1

This is useful since it informs what results there are which can be specified via the result keyword argument in many methods such as primal_status, dual_status, objective_value, value, dual, and more.

We use solve_time to determine the time in seconds used by the optimizer until it terminated its search.

julia> solve_time(model)
0.004999876022338867

Note that this query might not be supported with all solvers.

The above status queries are designed to report information in a consistent format irrespective of the chosen optimizer. However, raw_status will provide the optimality status verbatim as reported by the optimizer. Thus, following our example with Ipopt we obtain:

julia> raw_status(model)
"Solve_Succeeded"

Also, we obtain the best objective bound via objective_bound which becomes particularly useful solutions that are suboptimal. However, this method is not supported by all optimizers and in this case Ipopt is one such optimizer.

Finally, we get the best dual objective value via dual_objective_value if the optimizer supplies this information which again Ipopt does not.

Variable Queries

Information about the optimized variables is gathered consistently in comparison to typical JuMP models. With InfiniteModels this is done by querying the optimizer model and using its stored variable mappings to return the correct information. Thus, here the queries are extended to work with the specifics of the optimizer model to return the appropriate info.

Note
  1. Like supports the all variable based query methods below also employ the label::Type{AbstractSupportLabel} = PublicLabel keyword argument that by default will return the desired information associated with public supports. The full set (e.g., ones corresponding to internal collocation nodes) is obtained via label = All.
  2. These methods also employ the ndarray::Bool keyword argument that will cause the output to be formatted as a n-dimensional array where the dimensions correspond to the infinite parameter dependencies. For example, if we have an infinite variable y(t, ξ) and we invoke a query method with ndarray = true then we'll get a matrix whose dimensions correspond to the supports of t and ξ, respectively. Also, if ndarray = true then label correspond to the intersection of supports labels in contrast to its default of invoking the union of the labels.

First, we should verify that the optimized model in fact has variable values via has_values. In our example, we have:

julia> has_values(model)
true

So we have values readily available to be extracted.

Now value can be used to query the values as shown above in the Basic Usage section. This works by calling the appropriate map_value defined by the optimizer model. By default this, employs the map_value fallback which uses optimizer_model_variable to do the mapping. Details on how to extend these methods for user-defined optimizer models is explained on the Extensions page.

We also, support call to value that use an expression of variables as input.

Finally, the optimizer index of a variable is queried via optimizer_index which reports back the index of the variable as used in the MathOptInterface backend:

julia> optimizer_index(z)
MathOptInterface.VariableIndex(1)

julia> optimizer_index(y)
10-element Vector{MathOptInterface.VariableIndex}:
 MathOptInterface.VariableIndex(2)
 MathOptInterface.VariableIndex(3)
 MathOptInterface.VariableIndex(4)
 MathOptInterface.VariableIndex(5)
 MathOptInterface.VariableIndex(6)
 MathOptInterface.VariableIndex(7)
 MathOptInterface.VariableIndex(8)
 MathOptInterface.VariableIndex(9)
 MathOptInterface.VariableIndex(10)
 MathOptInterface.VariableIndex(11)

As noted previously, an array is returned for y(t) in accordance with its transcription variables. In similar manner to value, this is enabled by appropriate versions of map_optimizer_index.

Constraint Queries

Like variables, a variety of information can be queried about constraints.

Note
  1. Like supports the all constraint query methods below also employ the label::Type{AbstractSupportLabel} = PublicLabel keyword argument that by default will return the desired information associated with public supports. The full set (e.g., ones corresponding to internal collocation nodes) is obtained via label = All.
  2. These methods also employ the ndarray::Bool keyword argument that will cause the output to be formatted as a n-dimensional array where the dimensions correspond to the infinite parameter dependencies. For example, if we have an infinite constraint that depends on t and ξ) and we invoke a query method with ndarray = true then we'll get a matrix whose dimensions correspond to the supports of t and ξ, respectively. Also, if ndarray = true then label correspond to the intersection of supports labels in contrast to its default of invoking the union of the labels.

First, recall that constraints are stored in the form function-in-set where generally function contains the variables and coefficients and the set contains the relational operator and the constant value. With this understanding, we query the value of a constraint's function via value:

julia> constraint_object(c1).func # show the function expression of c1
z - y(t)

julia> value(c1)
10-element Vector{Float64}:
 -8.747427671096375e-9
 20.999999995632077
 20.999999995632077
 20.999999995632077
 20.999999995632077
 20.999999995632077
 20.999999995632077
 20.999999995632077
 20.999999995632077
 20.999999995632077

Again, we obtain an array of values since c1 is infinite due to its dependence on x(t). Behind the scenes this is implemented via the appropriate extensions of map_value.

Next the optimizer index(es) of the transcribed constraints in the MathOptInterface backend provided via optimizer_index.

julia> optimizer_index(c1)
10-element Vector{MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.GreaterThan{Float64}}}:
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(1)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(2)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(3)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(4)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(5)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(6)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(7)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(8)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(9)
 MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64},MathOptInterface.GreaterThan{Float64}}(10)

Here 10 indices are given in accordance with the transcription constraints. The mapping between these and the original infinite constraints is managed via the appropriate extensions of map_optimizer_index.

Note

optimizer_index does not work for constraints that contain NLPExprs.

We can also query dual information from our constraints if it is available. First, we should verify that dual information is available via has_duals:

julia> has_duals(model)
true

Now we can query the duals via dual.

julia> dual(c1)
10-element Vector{Float64}:
 1.9999999988666093
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10
 1.1930560126841273e-10

Here we obtain the optimal dual values for each transcribed version of c1. This is enabled via the proper extensions of map_dual.

Finally, we query the shadow price of a constraint via shadow_price. This denotes the change in the objective value due to an infinitesimal relaxation of the constraint. For c1 we get:

julia> shadow_price(c1)
10-element Vector{Float64}:
 -1.9999999988666093
 -1.1930560126841273e-10
 -1.1930560126841273e-10
 -1.1930560126841273e-10
 -1.1930560126841273e-10
 -1.1930560126841273e-10
 -1.1930560126841273e-10
 -1.1930560126841273e-10
 -1.1930560126841273e-10
 -1.1930560126841273e-10

This is computed via interrogating the duals and the objective sense.

LP Sensitivity

We also conduct sensitivity analysis for linear problems using lp_sensitivity_report. This will generate a InfOptSensitivityReport which contains mapping to the ranges indicating how much a constraint RHS constant or a objective coefficient can be changed without violating the feasibility of the solution. This is further explained in the JuMP documentation here. Furthermore, this analysis can only be employed for a solver that implements MOI.ConstraintBasisStatus. In our running example up above, Ipopt.jl does not support this A solver like Gurobi.jl does.

julia> report = lp_sensitivity_report(model);

julia> report[c1]
10-element Vector{Tuple{Float64, Float64}}:
 (-42.0, Inf)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)

julia> report[z]
(-2.0, Inf)

Note that like other query methods, an array of ranges will be provided with testing the sensitivity of an infinite constraint RHS in accordance with the discretization scheme. Also, keyword arguments (like ndarray and label) can be invoked when indexing the report:

julia> report[c1, label = All]
10-element Vector{Tuple{Float64, Float64}}:
 (-42.0, Inf)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)
 (-Inf, 42.0)

Other Queries

Any other queries supported by JuMP can be accessed by simply interrogating the optimizer model directly using optimizer_model to access it. For example, we can get the solution summary of the optimizer model:

julia> solution_summary(optimizer_model(model))
* Solver : Ipopt

* Status
  Termination status : LOCALLY_SOLVED
  Primal status      : FEASIBLE_POINT
  Dual status        : FEASIBLE_POINT
  Message from the solver:
  "Solve_Succeeded"

* Candidate solution
  Objective value      : 83.99999998250514

* Work counters
  Solve time (sec)   : 0.01000