```#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
***********************************************************************************
opt_tutorial1.py
DAE Tools: pyDAE module, www.daetools.com
***********************************************************************************
DAE Tools is free software; you can redistribute it and/or modify it under the
Foundation. DAE Tools is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with the
DAE Tools software; if not, see <http://www.gnu.org/licenses/>.
************************************************************************************
"""
__doc__ = """
This tutorial introduces IPOPT NLP solver, its setup and options.
"""

import sys
from time import localtime, strftime
from daetools.pyDAE import *
from daetools.solvers.ipopt import pyIPOPT

class modTutorial(daeModel):
def __init__(self, Name, Parent = None, Description = ""):
daeModel.__init__(self, Name, Parent, Description)

self.x1 = daeVariable("x1", no_t, self)
self.x2 = daeVariable("x2", no_t, self)
self.x3 = daeVariable("x3", no_t, self)
self.x4 = daeVariable("x4", no_t, self)

self.dummy = daeVariable("dummy", no_t, self, "A dummy variable to satisfy the condition that there should be at least one-state variable and one equation in a model")

def DeclareEquations(self):
daeModel.DeclareEquations(self)

eq = self.CreateEquation("Dummy")
eq.Residual = self.dummy()

class simTutorial(daeSimulation):
def __init__(self):
daeSimulation.__init__(self)
self.m = modTutorial("opt_tutorial1")
self.m.Description = __doc__

def SetUpParametersAndDomains(self):
pass

def SetUpVariables(self):
self.m.x1.AssignValue(1)
self.m.x2.AssignValue(5)
self.m.x3.AssignValue(5)
self.m.x4.AssignValue(1)

def SetUpOptimization(self):
# Set the objective function (minimization).
# The objective function can be accessed by using ObjectiveFunction property which always returns the 1st obj. function,
# for in general case more than one obj. function. can be defined, so ObjectiveFunctions[0] can be used as well:
#       self.ObjectiveFunctions[0].Residual = ...
# Obviously defining more than one obj. function has no meaning when using opt. software such as Ipopt, Bonmin or Nlopt
# which cannot do the multi-objective optimization. The number of obj. functions can be defined in the function
# optimization.Initialize by using the named argument NumberOfObjectiveFunctions (the default is 1).
# Other obj. functions can be obtained by using ObjectiveFunctions[i] property.
self.ObjectiveFunction.Residual = self.m.x1() * self.m.x4() * (self.m.x1() + self.m.x2() + self.m.x3()) + self.m.x3()

# Set the constraints (inequality, equality)
# Constraints are in the following form:
#  - Inequality: g(i) <= 0
#  - Equality: h(i) = 0
self.c1 = self.CreateInequalityConstraint("Constraint 1") # g(x) >= 25:  25 - x1*x2*x3*x4 <= 0
self.c1.Residual = 25 - self.m.x1() * self.m.x2() * self.m.x3() * self.m.x4()

self.c2 = self.CreateEqualityConstraint("Constraint 2") # h(x) == 40
self.c2.Residual = self.m.x1() * self.m.x1() + self.m.x2() * self.m.x2() + self.m.x3() * self.m.x3() + self.m.x4() * self.m.x4() - 40

# Set the optimization variables, their lower/upper bounds and the starting point
# The optimization variables can be stored and used later to get the optimization results or
# to interact with some 3rd party software.
self.x1 = self.SetContinuousOptimizationVariable(self.m.x1, 1, 5, 2);
self.x2 = self.SetContinuousOptimizationVariable(self.m.x2, 1, 5, 2);
self.x3 = self.SetContinuousOptimizationVariable(self.m.x3, 1, 5, 2);
self.x4 = self.SetContinuousOptimizationVariable(self.m.x4, 1, 5, 2);

def setOptions(nlpsolver):
# 1) Set the options manually
try:
nlpsolver.SetOption('print_level', 0)
nlpsolver.SetOption('tol', 1e-7)

# Print options loaded at pyIPOPT startup and the user set options:
#nlpsolver.PrintOptions()
#nlpsolver.PrintUserOptions()

# ClearOptions can clear all options:
#nlpsolver.ClearOptions()
except Exception as e:
print(str(e))

def run(**kwargs):
simulation = simTutorial()
# Achtung! Achtung! NLP solver options can only be set after optimization.Initialize()
# Otherwise seg. fault occurs for some reasons.
nlpsolver  = pyIPOPT.daeIPOPT()
return daeActivity.optimize(simulation, reportingInterval       = 1,
timeHorizon             = 1,
nlpsolver               = nlpsolver,
nlpsolver_setoptions_fn = setOptions,
reportSensitivities     = True,
**kwargs)

if __name__ == "__main__":
guiRun = False if (len(sys.argv) > 1 and sys.argv[1] == 'console') else True
run(guiRun = guiRun)
```