1+ # This file lets you drive SciPy optimizers through SciML's Optimization.jl API.
12module OptimizationSciPy
23
34using Reexport
45@reexport using Optimization
56using Optimization. SciMLBase
67using PythonCall
78
9+ # We keep a handle to the actual Python SciPy module here.
810const scipy = PythonCall. pynew ()
911
1012function __init__ ()
1113 PythonCall. pycopy! (scipy, pyimport (" scipy" ))
1214end
1315
16+ # Make sure whatever we got back is a plain Julia Vector{T}.
1417function ensure_julia_array (x, :: Type{T} = Float64) where T
1518 x isa Vector{T} && return x
1619 return convert (Vector{T}, x isa Py ? pyconvert (Vector, x) : x)
1720end
1821
22+ # Pull a human-readable message out of the SciPy result object.
1923function safe_get_message (result)
2024 pyhasattr (result, " message" ) || return " Optimization completed"
2125 msg = result. message
@@ -28,6 +32,7 @@ function safe_get_message(result)
2832 return string (pytypeof (msg))
2933end
3034
35+ # Squash any kind of numeric object down to a Julia Float64.
3136function safe_to_float (x)
3237 x isa Float64 && return x
3338 x isa Number && return Float64 (x)
@@ -44,6 +49,7 @@ function safe_to_float(x)
4449 error (" Cannot convert object to Float64: $(typeof (x)) " )
4550end
4651
52+ # Gather timing / iteration counts and wrap them in OptimizationStats.
4753function extract_stats (result, time_elapsed)
4854 stats_dict = Dict {Symbol, Any} (
4955 :iterations => 0 ,
@@ -69,6 +75,7 @@ function extract_stats(result, time_elapsed)
6975 return Optimization. OptimizationStats (; stats_dict... )
7076end
7177
78+ # Map SciPy status integers onto SciML ReturnCode symbols.
7279function scipy_status_to_retcode (status:: Int , success:: Bool )
7380 if success
7481 return SciMLBase. ReturnCode. Success
@@ -90,6 +97,7 @@ function scipy_status_to_retcode(status::Int, success::Bool)
9097 end
9198end
9299
100+ # Tiny structs that tag which SciPy algorithm the user picked.
93101abstract type ScipyOptimizer end
94102
95103struct ScipyMinimize <: ScipyOptimizer
@@ -400,19 +408,30 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
400408 end
401409 cons_jac = _cons_jac
402410 end
411+ # user-controlled NonlinearConstraint extras
412+ keep_feasible_flag = get (cache. solver_args, :keep_feasible , false )
413+ jac_sparsity = get (cache. solver_args, :jac_sparsity , nothing )
403414 nlc = scipy. optimize. NonlinearConstraint (
404- _cons_func,
405- lcons,
406- ucons,
407- jac = cons_jac,
408- keep_feasible = false ,
409- finite_diff_rel_step = get (cache. solver_args, :cons_tol , 1e-8 )
415+ _cons_func,
416+ lcons,
417+ ucons;
418+ jac = cons_jac,
419+ keep_feasible = keep_feasible_flag,
420+ finite_diff_rel_step = get (cache. solver_args, :cons_tol , 1e-8 ),
421+ finite_diff_jac_sparsity = jac_sparsity,
410422 )
411423 constraints = pylist ([nlc])
412424 end
413425 elseif ! isnothing (cache. f. cons)
414426 throw (ArgumentError (" Method $(cache. opt. method) does not support constraints. Use SLSQP, trust-constr, COBYLA, or COBYQA instead." ))
415427 end
428+ # allow users to specify a Hessian update strategy (e.g. "BFGS", "SR1")
429+ if cache. opt. method == " trust-constr"
430+ hess_update = get (cache. solver_args, :hess_update , nothing )
431+ if hess_update != = nothing
432+ hess = hess_update
433+ end
434+ end
416435 t0 = time ()
417436 result = nothing
418437 try
@@ -444,8 +463,12 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
444463 throw (ErrorException (" Optimization failed to return a result" ))
445464 end
446465 t1 = time ()
447- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
448- minimum = safe_to_float (result. fun)
466+ if pyis (result. x, pybuiltins. None)
467+ minimizer = fill (NaN , length (cache. u0))
468+ else
469+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
470+ end
471+ minimum = pyis (result. fun, pybuiltins. None) ? NaN : safe_to_float (result. fun)
449472 py_success = pyconvert (Bool, pybool (result. success))
450473 py_message = safe_get_message (result)
451474 status = 0
@@ -519,8 +542,12 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
519542 throw (ErrorException (" Optimization failed to return a result" ))
520543 end
521544 t1 = time ()
522- minimizer = [safe_to_float (result. x)]
523- minimum = safe_to_float (result. fun)
545+ if pyis (result. x, pybuiltins. None)
546+ minimizer = [NaN ]
547+ else
548+ minimizer = [safe_to_float (result. x)]
549+ end
550+ minimum = pyis (result. fun, pybuiltins. None) ? NaN : safe_to_float (result. fun)
524551 py_success = pyconvert (Bool, pybool (result. success))
525552 if cache. sense === Optimization. MaxSense
526553 minimum = - minimum
@@ -591,7 +618,11 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
591618 throw (ErrorException (" Optimization failed to return a result" ))
592619 end
593620 t1 = time ()
594- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
621+ if pyis (result. x, pybuiltins. None)
622+ minimizer = fill (NaN , length (cache. u0))
623+ else
624+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
625+ end
595626 minimum = safe_to_float (result. cost)
596627 py_success = pyconvert (Bool, pybool (result. success))
597628 py_message = safe_get_message (result)
@@ -693,9 +724,16 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
693724 throw (ErrorException (" Root finding failed to return a result" ))
694725 end
695726 t1 = time ()
696- minimizer = [safe_to_float (result. root)]
697- root_julia = safe_to_float (result. root)
698- minimum = abs (_func (root_julia))
727+ if pyis (result. root, pybuiltins. None)
728+ minimizer = [NaN ]
729+ root_julia = NaN
730+ minimum = NaN
731+ else
732+ val = safe_to_float (result. root)
733+ minimizer = [val]
734+ root_julia = val
735+ minimum = abs (_func (root_julia))
736+ end
699737 converged = pyhasattr (result, " converged" ) ? pyconvert (Bool, pybool (result. converged)) : abs (_func (root_julia)) < 1e-10
700738 retcode = converged ? SciMLBase. ReturnCode. Success : SciMLBase. ReturnCode. Failure
701739 stats_dict = Dict {Symbol, Any} (:time => t1 - t0)
@@ -764,7 +802,11 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
764802 throw (ErrorException (" Root finding failed to return a result" ))
765803 end
766804 t1 = time ()
767- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
805+ if pyis (result. x, pybuiltins. None)
806+ minimizer = fill (NaN , length (cache. u0))
807+ else
808+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
809+ end
768810 fun_val = pyconvert (Vector{Float64}, result. fun)
769811 minimum = sum (abs2, fun_val)
770812 py_success = pyconvert (Bool, pybool (result. success))
@@ -812,12 +854,16 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
812854 end
813855 bounds = pylist (bounds_list)
814856 end
815- A_ub = nothing
816- b_ub = nothing
817- A_eq = nothing
818- b_eq = nothing
819- if ! isnothing (cache. f. cons) && ! isnothing (cache. lcons)
820- @warn " Linear programming constraints implementation is simplified. For proper usage, provide constraints in standard LP form."
857+ # Allow users to pass constraint matrices via solver kwargs
858+ A_ub = get (cache. solver_args, :A_ub , nothing )
859+ b_ub = get (cache. solver_args, :b_ub , nothing )
860+ A_eq = get (cache. solver_args, :A_eq , nothing )
861+ b_eq = get (cache. solver_args, :b_eq , nothing )
862+ if ! (isnothing (A_ub) == isnothing (b_ub))
863+ throw (ArgumentError (" Both A_ub and b_ub must be provided together" ))
864+ end
865+ if ! (isnothing (A_eq) == isnothing (b_eq))
866+ throw (ArgumentError (" Both A_eq and b_eq must be provided together" ))
821867 end
822868 maxiters = Optimization. _check_and_convert_maxiters (cache. solver_args. maxiters)
823869 options = nothing
@@ -849,8 +895,12 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
849895 throw (ErrorException (" Linear programming failed to return a result" ))
850896 end
851897 t1 = time ()
852- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
853- minimum = safe_to_float (result. fun)
898+ if pyis (result. x, pybuiltins. None)
899+ minimizer = fill (NaN , length (cache. u0))
900+ else
901+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
902+ end
903+ minimum = pyis (result. fun, pybuiltins. None) ? NaN : safe_to_float (result. fun)
854904 py_success = pyconvert (Bool, pybool (result. success))
855905 py_message = safe_get_message (result)
856906 if cache. sense === Optimization. MaxSense
@@ -897,10 +947,17 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
897947 ub = ub_new
898948 end
899949 bounds = scipy. optimize. Bounds (lb, ub)
900- integrality = nothing
950+ integrality = get (cache. solver_args, :integrality , nothing )
951+ A = get (cache. solver_args, :A , nothing )
952+ lb_con = get (cache. solver_args, :lb_con , nothing )
953+ ub_con = get (cache. solver_args, :ub_con , nothing )
901954 constraints = nothing
902- if ! isnothing (cache. f. cons) && ! isnothing (cache. lcons)
903- @warn " MILP constraints implementation is simplified. For proper usage, provide constraints in standard MILP form."
955+ if ! (isnothing (A) && isnothing (lb_con) && isnothing (ub_con))
956+ if any (isnothing .((A, lb_con, ub_con)))
957+ throw (ArgumentError (" A, lb_con, and ub_con must all be provided for linear constraints" ))
958+ end
959+ keep_feasible_flag = get (cache. solver_args, :keep_feasible , false )
960+ constraints = scipy. optimize. LinearConstraint (A, lb_con, ub_con, keep_feasible = keep_feasible_flag)
904961 end
905962 t0 = time ()
906963 result = nothing
@@ -924,8 +981,12 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
924981 throw (ErrorException (" MILP failed to return a result" ))
925982 end
926983 t1 = time ()
927- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
928- minimum = safe_to_float (result. fun)
984+ if pyis (result. x, pybuiltins. None)
985+ minimizer = fill (NaN , length (cache. u0))
986+ else
987+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
988+ end
989+ minimum = pyis (result. fun, pybuiltins. None) ? NaN : safe_to_float (result. fun)
929990 py_success = pyconvert (Bool, pybool (result. success))
930991 py_message = safe_get_message (result)
931992 if cache. sense === Optimization. MaxSense
@@ -985,7 +1046,11 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
9851046 throw (ErrorException (" Optimization failed to return a result" ))
9861047 end
9871048 t1 = time ()
988- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1049+ if pyis (result. x, pybuiltins. None)
1050+ minimizer = fill (NaN , length (cache. u0))
1051+ else
1052+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1053+ end
9891054 minimum = safe_to_float (result. fun)
9901055 py_success = pyconvert (Bool, pybool (result. success))
9911056 py_message = safe_get_message (result)
@@ -1039,7 +1104,11 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
10391104 throw (ErrorException (" Optimization failed to return a result" ))
10401105 end
10411106 t1 = time ()
1042- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1107+ if pyis (result. x, pybuiltins. None)
1108+ minimizer = fill (NaN , length (cache. u0))
1109+ else
1110+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1111+ end
10431112 minimum = safe_to_float (result. fun)
10441113 lowest_result = result. lowest_optimization_result
10451114 py_success = pyconvert (Bool, pybool (lowest_result. success))
@@ -1100,7 +1169,11 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
11001169 throw (ErrorException (" Optimization failed to return a result" ))
11011170 end
11021171 t1 = time ()
1103- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1172+ if pyis (result. x, pybuiltins. None)
1173+ minimizer = fill (NaN , length (cache. u0))
1174+ else
1175+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1176+ end
11041177 minimum = safe_to_float (result. fun)
11051178 py_success = pyconvert (Bool, pybool (result. success))
11061179 py_message = safe_get_message (result)
@@ -1190,7 +1263,11 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
11901263 throw (ErrorException (" Optimization failed to return a result" ))
11911264 end
11921265 t1 = time ()
1193- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1266+ if pyis (result. x, pybuiltins. None)
1267+ minimizer = fill (NaN , length (cache. u0))
1268+ else
1269+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1270+ end
11941271 minimum = safe_to_float (result. fun)
11951272 py_success = pyconvert (Bool, pybool (result. success))
11961273 py_message = safe_get_message (result)
@@ -1246,7 +1323,11 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
12461323 throw (ErrorException (" Optimization failed to return a result" ))
12471324 end
12481325 t1 = time ()
1249- minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1326+ if pyis (result. x, pybuiltins. None)
1327+ minimizer = fill (NaN , length (cache. u0))
1328+ else
1329+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result. x)
1330+ end
12501331 minimum = safe_to_float (result. fun)
12511332 py_success = pyconvert (Bool, pybool (result. success))
12521333 py_message = safe_get_message (result)
@@ -1300,7 +1381,11 @@ function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C})
13001381 throw (ErrorException (" Optimization failed to return a result" ))
13011382 end
13021383 t1 = time ()
1303- minimizer = pyconvert (Vector{eltype (cache. u0)}, result[0 ])
1384+ if pyis (result[0 ], pybuiltins. None)
1385+ minimizer = fill (NaN , length (cache. u0))
1386+ else
1387+ minimizer = pyconvert (Vector{eltype (cache. u0)}, result[0 ])
1388+ end
13041389 minimum = safe_to_float (result[1 ])
13051390 if cache. sense === Optimization. MaxSense
13061391 minimum = - minimum
@@ -1347,6 +1432,7 @@ export ScipyMinimize, ScipyNelderMead, ScipyPowell, ScipyCG, ScipyBFGS, ScipyNew
13471432 ScipyDifferentialEvolution, ScipyBasinhopping, ScipyDualAnnealing,
13481433 ScipyShgo, ScipyDirect, ScipyBrute
13491434
1435+ # Wrap the user's Julia objective so it matches what SciPy expects.
13501436function _create_loss (cache; vector_output:: Bool = false )
13511437 maxtime = get (cache. solver_args, :maxtime , nothing )
13521438 start_time = ! isnothing (maxtime) ? time () : 0.0
@@ -1391,7 +1477,13 @@ function _create_loss(cache; vector_output::Bool = false)
13911477 end
13921478end
13931479
1394- const _DEFAULT_EXCLUDE = (:maxiters , :maxtime , :abstol , :reltol , :callback , :progress , :cons_tol )
1480+ # These solver-args are handled specially elsewhere, so we skip them here.
1481+ const _DEFAULT_EXCLUDE = (
1482+ :maxiters , :maxtime , :abstol , :reltol , :callback , :progress , :cons_tol ,
1483+ :jac_sparsity , :keep_feasible , :hess_update
1484+ )
1485+
1486+ # Moving the remaining kwargs into a Dict that we pass straight to SciPy.
13951487function _merge_solver_kwargs! (dest:: AbstractDict , solver_args; exclude = _DEFAULT_EXCLUDE)
13961488 if isa (solver_args, NamedTuple)
13971489 for (k, v) in pairs (solver_args)
0 commit comments