Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example from documentation doesn't work #145

Closed
ForceBru opened this issue Feb 2, 2022 · 7 comments
Closed

Example from documentation doesn't work #145

ForceBru opened this issue Feb 2, 2022 · 7 comments

Comments

@ForceBru
Copy link

ForceBru commented Feb 2, 2022

Code

Copied verbatim from https://cyipopt.readthedocs.io/en/stable/tutorial.html#algorithmic-differentation:

import jax.numpy as np
from jax import jit, grad, jacrev, jacfwd
from cyipopt import minimize_ipopt

def objective(x):
    return x[0]*x[3]*np.sum(x[:3]) + x[2]

def eq_constraints(x):
    return np.sum(x**2) - 40

def ineq_constrains(x):
    return np.prod(x) - 25

# jit the functions
obj_jit = jit(objective)
con_eq_jit = jit(eq_constraints)
con_ineq_jit = jit(ineq_constrains)

# build the derivatives and jit them
obj_grad = jit(grad(obj_jit))  # objective gradient
obj_hess = jit(jacrev(jacfwd(obj_jit))) # objective hessian
con_eq_jac = jit(jacfwd(con_eq_jit))  # jacobian
con_ineq_jac = jit(jacfwd(con_ineq_jit))  # jacobian
con_eq_hess = jacrev(jacfwd(con_eq_jit)) # hessian
con_eq_hessvp = jit(lambda x, v: con_eq_hess(x) * v[0]) # hessian vector-product
con_ineq_hess = jacrev(jacfwd(con_ineq_jit))  # hessian
con_ineq_hessvp = jit(lambda x, v: con_ineq_hess(x) * v[0]) # hessian vector-product

# constraints
cons = [{'type': 'eq', 'fun': con_eq_jit, 'jac': con_eq_jac, 'hess': con_eq_hess},
    {'type': 'ineq', 'fun': con_ineq_jit, 'jac': con_ineq_jac, 'hess': con_ineq_hess}]

# starting point
x0 = np.array([1, 5, 5, 1])

# variable bounds: 1 <= x[i] <= 5
bnds = [(1, 5) for _ in range(x0.size)]

# executing the solver
res = minimize_ipopt(
    obj_jit, jac=obj_grad, hess=obj_hess, x0=x0, bounds=bnds,
    constraints=cons, options={'disp': 5}
)

BTW, not sure why con_eq_hessvp and con_ineq_hessvp are needed - they don't seem to be used anywhere after definition.

Output

~/test $ python test_cyipopt.py
WARNING:absl:No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit https://github.com/coin-or/Ipopt
******************************************************************************

This is Ipopt version 3.14.4, running with linear solver MUMPS 5.2.1.

Number of nonzeros in equality constraint Jacobian...:        4
Number of nonzeros in inequality constraint Jacobian.:        4
Number of nonzeros in Lagrangian Hessian.............:       10

Total number of variables............................:        4
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        4
                     variables with only upper bounds:        0
Total number of equality constraints.................:        1
Total number of inequality constraints...............:        1
        inequality constraints with only lower bounds:        1
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  1.6109694e+01 1.12e+01 5.28e-01   0.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  1.7252512e+01 7.43e-01 2.47e+01  -0.3 6.58e-01    -  3.31e-01 1.00e+00f  1

Number of Iterations....: 1

                                   (scaled)                 (unscaled)
Objective...............:   1.7252511978149414e+01    1.7252511978149414e+01
Dual infeasibility......:   2.4685358339957610e+01    2.4685358339957610e+01
Constraint violation....:   7.4340057373046875e-01    7.4340057373046875e-01
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   2.8007828268271449e+00    2.8007828268271449e+00
Overall NLP error.......:   2.4685358339957610e+01    2.4685358339957610e+01


Number of objective function evaluations             = 2
Number of objective gradient evaluations             = 2
Number of equality constraint evaluations            = 2
Number of inequality constraint evaluations          = 2
Number of equality constraint Jacobian evaluations   = 2
Number of inequality constraint Jacobian evaluations = 2
Number of Lagrangian Hessian evaluations             = 1
Total seconds in IPOPT                               = 1.248

EXIT: Stopping optimization at current point as requested by user.
Traceback (most recent call last):
  File "/Users/forcebru/test/test_cyipopt.py", line 40, in <module>
    res = minimize_ipopt(
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/cyipopt/scipy_interface.py", line 311, in minimize_ipopt
    x, info = nlp.solve(_x0)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/cyipopt/scipy_interface.py", line 166, in hessian
    H += hessian(x, lagr, *args)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/traceback_util.py", line 165, in reraise_with_filtered_traceback
    return fun(*args, **kwargs)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/api.py", line 1555, in batched_fun
    out_flat = batching.batch(
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/linear_util.py", line 166, in call_wrapped
    ans = self.f(*args, **dict(self.params, **kwargs))
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/api.py", line 2309, in _jvp
    out_primals, out_tangents = ad.jvp(flat_fun).call_wrapped(ps_flat, ts_flat)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/linear_util.py", line 166, in call_wrapped
    ans = self.f(*args, **dict(self.params, **kwargs))
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/traceback_util.py", line 165, in reraise_with_filtered_traceback
    return fun(*args, **kwargs)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/api.py", line 430, in cache_miss
    out_flat = xla.xla_call(
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1681, in bind
    return call_bind(self, fun, *args, **params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1693, in call_bind
    outs = top_trace.process_call(primitive, fun, tracers, params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/ad.py", line 324, in process_call
    result = call_primitive.bind(f_jvp, *primals, *nonzero_tangents, **new_params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1681, in bind
    return call_bind(self, fun, *args, **params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1693, in call_bind
    outs = top_trace.process_call(primitive, fun, tracers, params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/batching.py", line 214, in process_call
    vals_out = call_primitive.bind(f, *vals, **params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1681, in bind
    return call_bind(self, fun, *args, **params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1693, in call_bind
    outs = top_trace.process_call(primitive, fun, tracers, params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/ad.py", line 324, in process_call
    result = call_primitive.bind(f_jvp, *primals, *nonzero_tangents, **new_params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1681, in bind
    return call_bind(self, fun, *args, **params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1693, in call_bind
    outs = top_trace.process_call(primitive, fun, tracers, params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/partial_eval.py", line 206, in process_call
    jaxpr, out_pvals, consts, env_tracers = self.partial_eval(
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/partial_eval.py", line 319, in partial_eval
    out_flat, (out_avals, jaxpr, env) = app(f, *in_consts), aux()
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1681, in bind
    return call_bind(self, fun, *args, **params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 1693, in call_bind
    outs = top_trace.process_call(primitive, fun, tracers, params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/core.py", line 594, in process_call
    return primitive.impl(f, *tracers, **params)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/dispatch.py", line 142, in _xla_call_impl
    compiled_fun = _xla_callable(fun, device, backend, name, donated_invars,
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/linear_util.py", line 272, in memoized_fun
    ans = call(fun, *args)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/dispatch.py", line 169, in _xla_callable_uncached
    return lower_xla_callable(fun, device, backend, name, donated_invars,
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/profiler.py", line 206, in wrapper
    return func(*args, **kwargs)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/dispatch.py", line 197, in lower_xla_callable
    jaxpr, out_avals, consts = pe.trace_to_jaxpr_final(
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/profiler.py", line 206, in wrapper
    return func(*args, **kwargs)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/partial_eval.py", line 1680, in trace_to_jaxpr_final
    jaxpr, out_avals, consts = trace_to_subjaxpr_dynamic(fun, main, in_avals)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/partial_eval.py", line 1657, in trace_to_subjaxpr_dynamic
    ans = fun.call_wrapped(*in_tracers_)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/linear_util.py", line 166, in call_wrapped
    ans = self.f(*args, **dict(self.params, **kwargs))
jax._src.traceback_util.UnfilteredStackTrace: TypeError: eq_constraints() takes 1 positional argument but 2 were given

The stack trace below excludes JAX-internal frames.
The preceding is the original exception that occurred, unmodified.

--------------------

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/forcebru/test/test_cyipopt.py", line 40, in <module>
    res = minimize_ipopt(
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/cyipopt/scipy_interface.py", line 311, in minimize_ipopt
    x, info = nlp.solve(_x0)
  File "cyipopt/cython/ipopt_wrapper.pyx", line 642, in ipopt_wrapper.Problem.solve
  File "cyipopt/cython/ipopt_wrapper.pyx", line 895, in ipopt_wrapper.hessian_cb
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/cyipopt/scipy_interface.py", line 166, in hessian
    H += hessian(x, lagr, *args)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/api.py", line 1257, in jacfun
    y, pullback = _vjp(f_partial, *dyn_args)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/api.py", line 2522, in _vjp
    out_primal, out_vjp = ad.vjp(
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/ad.py", line 116, in vjp
    out_primals, pvals, jaxpr, consts = linearize(traceable, *primals)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/ad.py", line 103, in linearize
    jaxpr, out_pvals, consts = pe.trace_to_jaxpr(jvpfun_flat, in_pvals)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/profiler.py", line 206, in wrapper
    return func(*args, **kwargs)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/interpreters/partial_eval.py", line 522, in trace_to_jaxpr
    jaxpr, (out_pvals, consts, env) = fun.call_wrapped(pvals)
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/linear_util.py", line 166, in call_wrapped
    ans = self.f(*args, **dict(self.params, **kwargs))
  File "/Users/forcebru/opt/miniconda3/envs/pyMLenv/lib/python3.9/site-packages/jax/_src/api.py", line 1179, in jacfun
    y, jac = vmap(pushfwd, out_axes=(None, -1))(_std_basis(dyn_args))
TypeError: eq_constraints() takes 1 positional argument but 2 were given

Versions

cyipopt                   1.1.0            py39h8b0e17a_1    conda-forge
jax                       0.2.28                   pypi_0    pypi
jaxlib                    0.1.76                   pypi_0    pypi
python                    3.9.10          hea1dfa3_2_cpython    conda-forge
  • macOS 10.15.7
  • x86 CPU
@ForceBru
Copy link
Author

ForceBru commented Feb 2, 2022

Okay, replacing con_eq_hess with con_eq_hessvp and similarly for the other constraint works:

cons = [
    {'type': 'eq', 'fun': con_eq_jit, 'jac': con_eq_jac, 'hess': con_eq_hessvp},
    {'type': 'ineq', 'fun': con_ineq_jit, 'jac': con_ineq_jac, 'hess': con_ineq_hessvp}
]

Turns out, computing the Hessian-vector product was indeed necessary. The tutorial code is currently broken anyway because:

  1. It uses con_eq_hess and con_ineq_hess instead of con_eq_hessvp and con_ineq_hessvp respectively, so the code simply doesn't work.
  2. The hessvps are accessed by the key 'hess' in the constraint dictionaries, which doesn't convey the fact that it's actually a Hessian-vector product, not the Hessian.

@moorepants
Copy link
Collaborator

@jhelgert Can you comment on this since you added this example?

@moorepants
Copy link
Collaborator

There are also comments in #144 about issues with this example.

@jhelgert
Copy link
Contributor

jhelgert commented Feb 3, 2022

  1. It uses con_eq_hess and con_ineq_hess instead of con_eq_hessvp and con_ineq_hessvp respectively, so the code simply doesn't work.

Yes, you are right. We need to pass con_eq_hessvp and con_ineq_hessvp since Ipopt needs the Hessian (w.r.t x) of the lagrangian function L of the primal problem min f(x) s.t. g(x) <= 0, h(x) = 0. Here, the Hessian H_L is given by

H_L(x, u, v) = H_f(x) + u_1*H_g1(x) + .... + u_m * H_gm(x) + v_1*H_h1(x) + .... + v_r*H_hr(x)

where H_g1, ..., H_gm and H_h1, ..., H_hr are just the hessians for each equality/inequality constraint.

  1. The hessvps are accessed by the key 'hess' in the constraint dictionaries, which doesn't convey the fact that it's actually a Hessian-vector product, not the Hessian.

I totally agree. However, the scipy interface tries to mimic scipy.optimize.minimize which also uses the 'hess' key, see here. ('trust-constr' is the only algorithm that supports hessians and only accepts NonlinearConstraints objects as constraints, no dict-constraints.)

@ForceBru
Copy link
Author

ForceBru commented Feb 3, 2022

the scipy interface tries to mimic scipy.optimize.minimize which also uses the 'hess' key

I see, but that's not the Hessian, though. Users will attempt to pass functions that compute Hessians (I mean, even the documentation entry does this) and get cryptic errors saying that the function "takes 1 positional argument but 2 were given". However, it's completely unclear why the function would be passed two arguments and what these arguments are.

It's not too hard to debug (I just wrapped the Hessian function in a function that accepts two argument and printed the second argument, which turned out to contain a vector; I then remembered the mysterious unused con_eq_hessvp and deduced that Ipopt wants to call the Hessian-vector product function), but the key 'hess' is really misleading, since the SciPy documentation (same link as in your comment) says:

hess

Method for computing the Hessian matrix.

...but Ipopt expects a function for computing the Hessian-vector product, not the Hessian itself.

@jhelgert
Copy link
Contributor

jhelgert commented Feb 3, 2022

...but Ipopt expects a function for computing the Hessian-vector product, not the Hessian itself.

so does scipy.optimize.minimize's 'trust-constr' algorithm and the NonlinearConstraint objects. Having said that, I personally would prefer a 'hessvp' key (in both cyipopt and scipy.optimize.minimize's NonlinarConstraints).

@moorepants What do you think? Should we change it or keep it similar to scipy?

@moorepants
Copy link
Collaborator

The example in the docs now works:

from jax.config import config

# Enable 64 bit floating point precision
config.update("jax_enable_x64", True)

# We use the CPU instead of GPU und mute all warnings if no GPU/TPU is found.
config.update('jax_platform_name', 'cpu')

import jax.numpy as np
from jax import jit, grad, jacfwd
from cyipopt import minimize_ipopt
def objective(x):
    return x[0]*x[3]*np.sum(x[:3]) + x[2]

def eq_constraints(x):
    return np.sum(x**2) - 40

def ineq_constrains(x):
    return np.prod(x) - 25

# jit the functions
obj_jit = jit(objective)
con_eq_jit = jit(eq_constraints)
con_ineq_jit = jit(ineq_constrains)

# build the derivatives and jit them
obj_grad = jit(grad(obj_jit))  # objective gradient
obj_hess = jit(jacrev(jacfwd(obj_jit))) # objective hessian
con_eq_jac = jit(jacfwd(con_eq_jit))  # jacobian
con_ineq_jac = jit(jacfwd(con_ineq_jit))  # jacobian
con_eq_hess = jacrev(jacfwd(con_eq_jit)) # hessian
con_eq_hessvp = jit(lambda x, v: con_eq_hess(x) * v[0]) # hessian vector-product
con_ineq_hess = jacrev(jacfwd(con_ineq_jit))  # hessian
con_ineq_hessvp = jit(lambda x, v: con_ineq_hess(x) * v[0]) # hessian vector-product

# constraints
cons = [
    {'type': 'eq', 'fun': con_eq_jit, 'jac': con_eq_jac, 'hess': con_eq_hessvp},
    {'type': 'ineq', 'fun': con_ineq_jit, 'jac': con_ineq_jac, 'hess': con_ineq_hessvp}
 ]

# starting point
x0 = np.array([1.0, 5.0, 5.0, 1.0])

# variable bounds: 1 <= x[i] <= 5
bnds = [(1, 5) for _ in range(x0.size)]

# executing the solver
res = minimize_ipopt(obj_jit, jac=obj_grad, hess=obj_hess, x0=x0, bounds=bnds,
                  constraints=cons, options={'disp': 5})

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit https://github.com/coin-or/Ipopt
******************************************************************************

This is Ipopt version 3.14.12, running with linear solver MUMPS 5.2.1.

Number of nonzeros in equality constraint Jacobian...:        4
Number of nonzeros in inequality constraint Jacobian.:        4
Number of nonzeros in Lagrangian Hessian.............:       10

Total number of variables............................:        4
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        4
                     variables with only upper bounds:        0
Total number of equality constraints.................:        1
Total number of inequality constraints...............:        1
        inequality constraints with only lower bounds:        1
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  1.6109693e+01 1.12e+01 5.28e-01   0.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  1.7263331e+01 7.42e-01 2.19e+01  -0.3 6.49e-01    -  3.49e-01 1.00e+00f  1
   2  1.7829621e+01 7.24e-03 4.90e+00  -0.3 5.53e-02   2.0 9.98e-01 1.00e+00h  1
   3  1.7353146e+01 3.91e-02 3.91e-01  -1.0 4.09e-01    -  9.96e-01 1.00e+00f  1
   4  1.6950812e+01 1.51e-01 3.89e-02  -1.4 2.79e-01    -  9.68e-01 1.00e+00h  1
   5  1.7002828e+01 2.33e-02 8.59e-03  -2.8 6.35e-02    -  9.69e-01 1.00e+00h  1
   6  1.7013924e+01 3.26e-04 1.47e-04  -4.4 7.99e-03    -  9.98e-01 1.00e+00h  1
   7  1.7014017e+01 5.42e-07 4.41e-07 -10.2 2.08e-04    -  9.99e-01 1.00e+00h  1
   8  1.7014017e+01 9.95e-14 9.21e-14 -11.0 2.10e-07    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 8

                                   (scaled)                 (unscaled)
Objective...............:   1.7014017272774488e+01    1.7014017272774488e+01
Dual infeasibility......:   9.2147180682691379e-14    9.2147180682691379e-14
Constraint violation....:   9.9475983006414026e-14    9.9475983006414026e-14
Variable bound violation:   9.9907189188286338e-09    9.9907189188286338e-09
Complementarity.........:   1.0096675639168746e-11    1.0096675639168746e-11
Overall NLP error.......:   1.0096675639168746e-11    1.0096675639168746e-11


Number of objective function evaluations             = 9
Number of objective gradient evaluations             = 9
Number of equality constraint evaluations            = 9
Number of inequality constraint evaluations          = 9
Number of equality constraint Jacobian evaluations   = 9
Number of inequality constraint Jacobian evaluations = 9
Number of Lagrangian Hessian evaluations             = 8
Total seconds in IPOPT                               = 0.363

EXIT: Optimal Solution Found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants