# Python equivalent of the ComplexUserFunction.c example in
# examples/nonlinear/c directory.
#
# Define objective and constraint as user functions that return
# derivatives:
#
# Minimize myobj (y,z,v,w)
#   s.t.
#   ball (x,t) <= 730
#   1 <= x <= 2
#   2 <= y <= 3
#   3 <= z <= 4
#   4 <= v <= 5
#   5 <= w <= 6
#
# where
#
# myobj (y,z,v,w) = y**2 + z - v + w**2
# ball  (x,t) = x**2 + t**2
#
# (C) 1983-2025 Fair Isaac Corporation

import xpress as xp


def myobj(y, z, v, w):
    """Return value of the objective function with the derivative w.r.t. all variables passed."""

    return (y**2 + z - v + w**2,  # value of the function
            2*y,  # derivative w.r.t. y
            1,    # derivative w.r.t. z
            -1,   # derivative w.r.t. v
            2*w)  # derivative w.r.t. w


def ball(x, t):
    """Return value of the left-hand side of the constraint with its derivatives."""

    return (x**2 + t**2,
            2*x,
            2*t)


def myobj_noderiv(y, z, v, w):
    """Return objective without derivatives."""

    return y**2 + z - v + w**2  # Value of the function.


def ball_noderiv(x, t):
    """Return left-hand side of constraint without derivatives."""

    return x**2 + t**2


def solve_problem(incder):
    """Construct and solve the problem with or without derivatives.
    :param incder: True for including derivatives, False otherwise
    :return:
    """
    p = xp.problem()

    x = p.addVariable(lb=1, ub=2)
    y = p.addVariable(lb=2, ub=3)
    z = p.addVariable(lb=3, ub=4)
    v = p.addVariable(lb=4, ub=5)
    w = p.addVariable(lb=5, ub=6)

    t = p.addVariable(lb=-xp.infinity)  # Free variable.

    p.setObjective(t)

    if incder:
        p.addConstraint(t == xp.user(myobj, y, z, v, w, derivatives=True))
        p.addConstraint(xp.user(ball, x, t, derivatives=True) <= 730)
        print('Solving problem using derivatives:')
    else:
        p.addConstraint(t == xp.user(myobj_noderiv, y, z, v, w))
        p.addConstraint(xp.user(ball_noderiv, x, t) <= 730)
        print('Solving problem without using derivatives:')

    # With user functions the problem cannot be solved to
    # global optimality, so the control below is not necessary
    # but shown here for completeness. Setting nlpsolver to one
    # ensures the problem is solved by a local solver rather than the
    # global solver.
    p.controls.nlpsolver = xp.constants.NLPSOLVER_LOCAL
    p.optimize()

    print('Problem solved. Objective:', p.attributes.objval, '; solution:', p.getSolution())

if __name__ == '__main__':
    solve_problem(True)
    solve_problem(False)
