Constrained Optimization

Constrained Optimization Problem Lagrange Multipliers The KKT Conditions Next: Duality →

Constrained Optimization Problem

So far we have studied unconstrained minimization of \(\mathcal{L}: \mathbb{R}^d \to \mathbb{R}\). In most applications, however, the parameters must additionally satisfy a collection of equality and inequality constraints. The result is a feasibility region carved out of \(\mathbb{R}^d\).

Definition: Constrained Optimization Problem

Let \(\mathcal{L}: \mathbb{R}^d \to \mathbb{R}\) be the objective, \(g_i: \mathbb{R}^d \to \mathbb{R}\) for \(i \in \mathcal{I}\) inequality constraints, and \(h_j: \mathbb{R}^d \to \mathbb{R}\) for \(j \in \mathcal{E}\) equality constraints. The constrained optimization problem is \[ \boldsymbol{\theta}^* \in \operatorname*{arg\,min}_{\boldsymbol{\theta} \in \mathcal{C}}\, \mathcal{L}(\boldsymbol{\theta}), \] where the feasible set is \[ \mathcal{C} = \{\, \boldsymbol{\theta} \in \mathbb{R}^d : g_i(\boldsymbol{\theta}) \leq 0 \text{ for all } i \in \mathcal{I}, \quad h_j(\boldsymbol{\theta}) = 0 \text{ for all } j \in \mathcal{E} \,\}. \]

Throughout this page \(\mathcal{L}\), the \(g_i\), and the \(h_j\) are assumed continuously differentiable on \(\mathbb{R}^d\). We write \(\mathcal{I} = \{1, \ldots, m\}\) and \(\mathcal{E} = \{1, \ldots, p\}\).

Penalty Method

The penalty method is a natural first attempt to reduce the constrained problem to something we already know how to solve. It replaces the constraints by soft penalties in the objective, \[ \mathcal{L}_\rho(\boldsymbol{\theta}) = \mathcal{L}(\boldsymbol{\theta}) + \rho \sum_{i \in \mathcal{I}} \max\!\bigl(0, g_i(\boldsymbol{\theta})\bigr)^{\!2} + \rho \sum_{j \in \mathcal{E}} h_j(\boldsymbol{\theta})^2, \] where \(\rho \gt 0\) controls the penalty strength. The result is unconstrained and can be minimized by gradient descent.

The drawback is that for finite \(\rho\) the constraints are in general only approximately satisfied, and driving \(\rho \to \infty\) causes the unconstrained problem to become ill-conditioned.

When constraints must be strictly satisfied or high-precision solutions are required, the Lagrangian machinery introduced below provides a principled alternative that reformulates constrained optimality as a system of equations and inequalities (the KKT conditions). In large-scale problems, hybrid approaches such as Augmented Lagrangian methods combine the numerical robustness of penalties with the exactness of Lagrangian multipliers.

Lagrange Multipliers

We build up the general theory in two stages. First, consider the pure equality-constrained problem with a single smooth constraint \(h: \mathbb{R}^d \to \mathbb{R}\), \[ \begin{align*} &\min_{\boldsymbol{\theta}}\, \mathcal{L}(\boldsymbol{\theta}) \quad \text{subject to} \quad h(\boldsymbol{\theta}) = 0, \\\\ &\mathcal{C} = \{\boldsymbol{\theta} \in \mathbb{R}^d : h(\boldsymbol{\theta}) = 0\}. \end{align*} \]

Normal Direction at a Feasible Point

Fix \(\boldsymbol{\theta} \in \mathcal{C}\) and consider a small displacement \(\boldsymbol{\epsilon} \in \mathbb{R}^d\) keeping \(\boldsymbol{\theta} + \boldsymbol{\epsilon} \in \mathcal{C}\). By the first-order Taylor expansion, \[ h(\boldsymbol{\theta} + \boldsymbol{\epsilon}) \approx h(\boldsymbol{\theta}) + \boldsymbol{\epsilon}^\top \nabla h(\boldsymbol{\theta}) = \boldsymbol{\epsilon}^\top \nabla h(\boldsymbol{\theta}), \] using \(h(\boldsymbol{\theta}) = 0\).

Since \(h(\boldsymbol{\theta} + \boldsymbol{\epsilon}) = 0\) as well, \(\boldsymbol{\epsilon}^\top \nabla h(\boldsymbol{\theta}) \approx 0\). Displacements that keep us on \(\mathcal{C}\) are therefore (to first order) orthogonal to \(\nabla h(\boldsymbol{\theta})\). Equivalently, \(\nabla h(\boldsymbol{\theta})\) points in the normal direction to the constraint surface.

Definition: Tangent Space (constraint surface)

The tangent space of the constraint surface \(\mathcal{C} = \{h = 0\}\) at \(\boldsymbol{\theta}\) is \[ T_{\boldsymbol{\theta}} \mathcal{C} = \{\boldsymbol{\epsilon} \in \mathbb{R}^d : \nabla h(\boldsymbol{\theta})^\top \boldsymbol{\epsilon} = 0\} = \ker\bigl(\nabla h(\boldsymbol{\theta})^\top\bigr). \] When \(\nabla h(\boldsymbol{\theta}) \neq \mathbf{0}\), this is the first-order approximation of the set of feasible displacements from \(\boldsymbol{\theta}\). A general notion of tangent space that does not depend on a particular defining function is developed on the tangent vectors page, and for a regular level set it agrees with the kernel above by the tangent space of a level set proposition.

The Lagrange Multiplier Rule

Suppose \(\boldsymbol{\theta}^* \in \mathcal{C}\) is a local minimum at which \(\nabla h(\boldsymbol{\theta}^*) \neq \mathbf{0}\). If \(\nabla \mathcal{L}(\boldsymbol{\theta}^*)\) had a tangent component, that is, if it were not orthogonal to \(T_{\boldsymbol{\theta}^*} \mathcal{C}\), then moving a small step along that component inside \(\mathcal{C}\) would strictly decrease \(\mathcal{L}\), contradicting local optimality. Therefore \(\nabla \mathcal{L}(\boldsymbol{\theta}^*)\) is also normal to \(\mathcal{C}\).

A caveat about rigor. The step "move along a tangent direction inside \(\mathcal{C}\)" is geometrically intuitive but not self-justifying. The linearized tangent space \(T_{\boldsymbol{\theta}^*}\mathcal{C}\) is defined by a first-order condition, and it is a non-trivial fact that every tangent vector is actually realized as the velocity of a smooth curve inside \(\mathcal{C}\). This realization follows from the implicit function theorem. For a single constraint the nonsingularity hypothesis of that theorem is that some partial derivative \(\partial h / \partial \theta_\ell\) is nonzero at \(\boldsymbol{\theta}^*\), which after reordering coordinates is exactly \(\nabla h(\boldsymbol{\theta}^*) \neq \mathbf{0}\), and the theorem then presents \(\mathcal{C}\) near \(\boldsymbol{\theta}^*\) as the graph of a smooth function of the remaining \(d - 1\) coordinates.

The intrinsic language for making this argument airtight belongs to the smooth-manifold framework of the later manifold series. That framework defines tangent spaces without reference to a chosen constraint function and proves that every regular level set is a smooth submanifold. On this page we accept this geometric realization without proof and proceed with the formal Lagrange rule.

Assuming \(\nabla h(\boldsymbol{\theta}^*) \neq \mathbf{0}\) (the single-constraint analogue of the LICQ regularity condition introduced below), the normal space is one-dimensional, spanned by \(\nabla h(\boldsymbol{\theta}^*)\), so \(\nabla \mathcal{L}(\boldsymbol{\theta}^*)\) is a scalar multiple of \(\nabla h(\boldsymbol{\theta}^*)\). There exists \(\lambda \in \mathbb{R}\), the Lagrange multiplier, with \[ \nabla \mathcal{L}(\boldsymbol{\theta}^*) + \lambda\, \nabla h(\boldsymbol{\theta}^*) = \mathbf{0}. \tag{LM} \]

Definition: Lagrangian (Equality Constraints)

For equality constraints \(h_j(\boldsymbol{\theta}) = 0\), \(j = 1, \ldots, p\), the Lagrangian is \[ \mathcal{L}\!\left(\boldsymbol{\theta}, \boldsymbol{\lambda}\right) = \mathcal{L}(\boldsymbol{\theta}) + \sum_{j=1}^{p} \lambda_j\, h_j(\boldsymbol{\theta}) = \mathcal{L}(\boldsymbol{\theta}) + \boldsymbol{\lambda}^\top \mathbf{h}(\boldsymbol{\theta}), \] where \(\boldsymbol{\lambda} = (\lambda_1, \ldots, \lambda_p)^\top \in \mathbb{R}^p\) is the vector of Lagrange multipliers and \(\mathbf{h} = (h_1, \ldots, h_p)^\top\). (We overload \(\mathcal{L}\) to refer to both the objective and the Lagrangian. The number of arguments disambiguates.)

At a stationary point of the Lagrangian, \(\nabla_{\boldsymbol{\theta}, \boldsymbol{\lambda}}\, \mathcal{L} = \mathbf{0}\), which unpacks to \[ \begin{align*} &\nabla_{\boldsymbol{\theta}}\, \mathcal{L}(\boldsymbol{\theta}) + \sum_{j=1}^p \lambda_j\, \nabla h_j(\boldsymbol{\theta}) = \mathbf{0}, \\\\ &h_j(\boldsymbol{\theta}) = 0 \text{ for all } j. \end{align*} \] Such a point is called a critical point. It simultaneously satisfies primal feasibility and the multi-constraint form of the stationarity condition (LM).

Geometric Intuition

At a constrained optimum \(\boldsymbol{\theta}^*\), we cannot improve the objective by moving along the constraint surface. When the gradients \(\nabla h_j(\boldsymbol{\theta}^*)\) are linearly independent, this forces the gradient of the objective \(\nabla \mathcal{L}(\boldsymbol{\theta}^*)\) to have, to first order, no component tangent to \(\mathcal{C}\). It must lie in the normal space, which is spanned by \(\{\nabla h_j(\boldsymbol{\theta}^*)\}\). The Lagrange multiplier \(\lambda_j\) measures "how much" of the objective gradient is absorbed by constraint \(h_j\), and its sign records the direction in which the optimal value moves as the constraint level \(h_j = 0\) is shifted, a reading made quantitative by the sensitivity interpretation in the next section.

The KKT Conditions

We now add inequality constraints \(g_i(\boldsymbol{\theta}) \leq 0\). A single inequality is handled by extending the Lagrangian with a non-negative multiplier \(\mu \geq 0\): \[ \mathcal{L}(\boldsymbol{\theta}, \mu) = \mathcal{L}(\boldsymbol{\theta}) + \mu\, g(\boldsymbol{\theta}). \] The sign restriction \(\mu \geq 0\) is essential. It distinguishes inequalities from equalities, and encodes that the constraint can only push back against infeasibility, never pull into it. For multiple inequalities, the extension is additive: \[ \mathcal{L}(\boldsymbol{\theta}, \boldsymbol{\mu}) = \mathcal{L}(\boldsymbol{\theta}) + \sum_{i=1}^{m} \mu_i\, g_i(\boldsymbol{\theta}). \]

Definition: Active Set

At a feasible point \(\boldsymbol{\theta} \in \mathcal{C}\), an inequality constraint \(g_i(\boldsymbol{\theta}) \leq 0\) is said to be active if \(g_i(\boldsymbol{\theta}) = 0\). The active set at \(\boldsymbol{\theta}\) is \[ \mathcal{A}(\boldsymbol{\theta}) = \{\, i \in \{1, \ldots, m\} : g_i(\boldsymbol{\theta}) = 0 \,\}. \] Only active inequalities constrain the local geometry at \(\boldsymbol{\theta}\). Inactive ones (\(g_i(\boldsymbol{\theta}) \lt 0\)) have slack and play no local role.

Definition: Lagrangian (General Form)

In the presence of both \(m\) inequality constraints and \(p\) equality constraints the general Lagrangian is \[ \mathcal{L}(\boldsymbol{\theta}, \boldsymbol{\mu}, \boldsymbol{\lambda}) = \mathcal{L}(\boldsymbol{\theta}) + \sum_{i=1}^{m} \mu_i\, g_i(\boldsymbol{\theta}) + \sum_{j=1}^{p} \lambda_j\, h_j(\boldsymbol{\theta}). \]

The primal problem admits the min-max reformulation \[ \min_{\boldsymbol{\theta}} \max_{\boldsymbol{\mu} \geq 0,\, \boldsymbol{\lambda}}\, \mathcal{L}(\boldsymbol{\theta}, \boldsymbol{\mu}, \boldsymbol{\lambda}). \]

The inner maximization over \(\boldsymbol{\mu} \geq 0\) returns \(+\infty\) whenever any \(g_i(\boldsymbol{\theta}) \gt 0\) (infeasibility), and the maximization over unrestricted \(\boldsymbol{\lambda}\) returns \(+\infty\) whenever any \(h_j(\boldsymbol{\theta}) \neq 0\). Feasibility is therefore enforced implicitly through the min-max structure. This formulation anchors the transition to duality, where we will instead study the dual \(\max_{\boldsymbol{\mu} \geq 0, \boldsymbol{\lambda}} \min_{\boldsymbol{\theta}} \mathcal{L}\).

Definition: Linear Independence Constraint Qualification (LICQ)

The linear independence constraint qualification holds at a feasible point \(\boldsymbol{\theta}^*\) if the gradients \[ \{\nabla g_i(\boldsymbol{\theta}^*) : i \in \mathcal{A}(\boldsymbol{\theta}^*)\} \cup \{\nabla h_j(\boldsymbol{\theta}^*) : j = 1, \ldots, p\} \] are linearly independent in \(\mathbb{R}^d\). LICQ is one of several possible constraint qualifications ensuring that the linearized constraint geometry at \(\boldsymbol{\theta}^*\) faithfully captures the nonlinear feasible set.

Theorem: Karush-Kuhn-Tucker (KKT) Conditions

Let \(\mathcal{L}\), the \(g_i\), and the \(h_j\) be continuously differentiable on \(\mathbb{R}^d\), and let \(\boldsymbol{\theta}^*\) be a feasible point. Say that \((\boldsymbol{\theta}^*, \boldsymbol{\mu}^*, \boldsymbol{\lambda}^*)\) satisfies the KKT conditions if all four of the following hold:

  1. Primal feasibility: \[ \begin{align*} &g_i(\boldsymbol{\theta}^*) \leq 0 \text{ for all } i, \\\\ &h_j(\boldsymbol{\theta}^*) = 0 \text{ for all } j. \end{align*} \]
  2. Stationarity: \[ \nabla \mathcal{L}(\boldsymbol{\theta}^*) + \sum_{i=1}^{m} \mu_i^*\, \nabla g_i(\boldsymbol{\theta}^*) + \sum_{j=1}^{p} \lambda_j^*\, \nabla h_j(\boldsymbol{\theta}^*) = \mathbf{0}. \]
  3. Dual feasibility: \[ \mu_i^* \geq 0 \quad \text{for all } i = 1, \ldots, m. \]
  4. Complementary slackness: \[ \mu_i^*\, g_i(\boldsymbol{\theta}^*) = 0 \quad \text{for all } i = 1, \ldots, m, \] equivalently \(\boldsymbol{\mu}^* \odot \mathbf{g}(\boldsymbol{\theta}^*) = \mathbf{0}\), where \(\mathbf{g} = (g_1, \ldots, g_m)^\top\) and \(\odot\) is the componentwise product. For each \(i\), at least one of \(\mu_i^* = 0\) and \(g_i(\boldsymbol{\theta}^*) = 0\) holds. In particular \(\mu_i^* = 0\) whenever the constraint is inactive. Only constraints in the active set \(\mathcal{A}(\boldsymbol{\theta}^*)\) contribute non-trivially.

(i) Necessity. If \(\boldsymbol{\theta}^*\) is a local minimum of the constrained problem and LICQ holds at \(\boldsymbol{\theta}^*\), then there exist multipliers \((\boldsymbol{\mu}^*, \boldsymbol{\lambda}^*)\) such that the KKT conditions hold. (No convexity of \(\mathcal{L}\) or \(g_i\) is required for necessity.)

(ii) Sufficiency under convexity. If \(\mathcal{L}\) and all \(g_i\) are convex, all \(h_j\) are affine, and \((\boldsymbol{\theta}^*, \boldsymbol{\mu}^*, \boldsymbol{\lambda}^*)\) satisfies the KKT conditions, then \(\boldsymbol{\theta}^*\) is a global minimum of the constrained problem.

Proof of (ii):

Fix any feasible \(\boldsymbol{\theta}\) and write \(\mathbf{v} = \boldsymbol{\theta} - \boldsymbol{\theta}^*\). For a convex differentiable \(\psi: \mathbb{R}^d \to \mathbb{R}\), the function \(\varphi(t) = \psi(\boldsymbol{\theta}^* + t\mathbf{v})\) is convex on \(\mathbb{R}\) (immediate from the definition) and differentiable by the chain rule, with \(\varphi'(0) = \nabla \psi(\boldsymbol{\theta}^*)^\top \mathbf{v}\), so the first-order characterization of convexity applied to \(\varphi\) at the points \(0\) and \(1\) gives \[ \psi(\boldsymbol{\theta}) \geq \psi(\boldsymbol{\theta}^*) + \nabla \psi(\boldsymbol{\theta}^*)^\top \mathbf{v}. \] We use this for \(\psi = \mathcal{L}\) and for \(\psi = g_i\). For the affine \(h_j\) it holds with equality. Then \[ \begin{align*} \mathcal{L}(\boldsymbol{\theta}) &\geq \mathcal{L}(\boldsymbol{\theta}^*) + \nabla \mathcal{L}(\boldsymbol{\theta}^*)^\top \mathbf{v} \\\\ &= \mathcal{L}(\boldsymbol{\theta}^*) - \sum_{i=1}^{m} \mu_i^*\, \nabla g_i(\boldsymbol{\theta}^*)^\top \mathbf{v} - \sum_{j=1}^{p} \lambda_j^*\, \nabla h_j(\boldsymbol{\theta}^*)^\top \mathbf{v} \\\\ &\geq \mathcal{L}(\boldsymbol{\theta}^*) - \sum_{i=1}^{m} \mu_i^* \bigl(g_i(\boldsymbol{\theta}) - g_i(\boldsymbol{\theta}^*)\bigr) - \sum_{j=1}^{p} \lambda_j^* \bigl(h_j(\boldsymbol{\theta}) - h_j(\boldsymbol{\theta}^*)\bigr) \\\\ &= \mathcal{L}(\boldsymbol{\theta}^*) - \sum_{i=1}^{m} \mu_i^*\, g_i(\boldsymbol{\theta}) \\\\ &\geq \mathcal{L}(\boldsymbol{\theta}^*). \end{align*} \] The second line is stationarity. The third uses \(\mu_i^* \geq 0\) together with the first-order inequality for \(g_i\), and the equality for \(h_j\). The fourth uses complementary slackness \(\mu_i^*\, g_i(\boldsymbol{\theta}^*) = 0\) and \(h_j(\boldsymbol{\theta}) = h_j(\boldsymbol{\theta}^*) = 0\). The last uses \(\mu_i^* \geq 0\) and \(g_i(\boldsymbol{\theta}) \leq 0\). Hence \(\boldsymbol{\theta}^*\) is a global minimum.

On (i). The proof of necessity is beyond the scope of this page. It has two ingredients. First, under LICQ the implicit function theorem realizes every direction \(\mathbf{v}\) with \(\nabla g_i(\boldsymbol{\theta}^*)^\top \mathbf{v} \leq 0\) for active \(i\) and \(\nabla h_j(\boldsymbol{\theta}^*)^\top \mathbf{v} = 0\) for all \(j\) as the initial velocity of a feasible curve, generalizing the single-constraint caveat above, so local optimality forces \(\nabla \mathcal{L}(\boldsymbol{\theta}^*)^\top \mathbf{v} \geq 0\) for all such \(\mathbf{v}\). Second, a finite-dimensional separation argument (Farkas' lemma) converts this sign condition into multipliers with \(\mu_i^* \geq 0\) and \(\mu_i^* = 0\) off the active set.

Reading KKT: sign conventions and the role of each condition

The dual-feasibility sign \(\mu_i^* \geq 0\) also carries a quantitative reading. Perturb \(g_i \leq 0\) to \(g_i \leq \varepsilon\) for \(\varepsilon\) near \(0\) (\(\varepsilon \gt 0\) loosens the constraint) and let \(V(\varepsilon)\) denote the resulting optimal value. Under regularity conditions not developed on this page, \(V\) is differentiable at \(0\) with \(V'(0) = -\mu_i^*\), so \(\mu_i^*\) is the rate at which the optimal value falls as the constraint is loosened. This is the sensitivity interpretation of Lagrange multipliers. Under the same regularity conditions, shifting \(h_j = 0\) to \(h_j = \varepsilon\) changes the optimal value at rate \(-\lambda_j^*\), and \(\lambda_j^*\) can have either sign because the level of an equality constraint can be moved in either direction.

KKT is critical in many machine-learning applications such as the support vector machine (SVM). Here we demonstrate the KKT machinery on a simple problem that can be solved by hand.

Worked Example

We minimize \[ f(\mathbf{x}) = (x_1 - 2)^2 + x_2^2 + x_3^2 \] subject to \[ \begin{align*} &g_1(\mathbf{x}) = x_1 + x_2 - 1 \leq 0, \\\\ &g_2(\mathbf{x}) = x_1 + x_3 - 1 \leq 0, \\\\ &h_1(\mathbf{x}) = x_1 - x_2 = 0. \end{align*} \]

The objective is strictly convex, the inequality constraints are affine (hence convex), and the equality constraint is affine. Moreover the gradients \(\nabla g_1 = (1,1,0)^\top\), \(\nabla g_2 = (1,0,1)^\top\), \(\nabla h_1 = (1,-1,0)^\top\) are linearly independent at every feasible point, so LICQ holds. Both the necessity and the sufficiency parts of the KKT theorem apply. A KKT point is therefore the global minimum.

The Lagrangian is \[ \mathcal{L}(\mathbf{x}, \boldsymbol{\mu}, \lambda) = (x_1 - 2)^2 + x_2^2 + x_3^2 + \mu_1 (x_1 + x_2 - 1) + \mu_2 (x_1 + x_3 - 1) + \lambda (x_1 - x_2). \] Stationarity gives \[ \begin{align*} &\tfrac{\partial \mathcal{L}}{\partial x_1} = 2 x_1 - 4 + \mu_1 + \mu_2 + \lambda = 0, \tag{1}\\\\ &\tfrac{\partial \mathcal{L}}{\partial x_2} = 2 x_2 + \mu_1 - \lambda = 0, \tag{2}\\\\ &\tfrac{\partial \mathcal{L}}{\partial x_3} = 2 x_3 + \mu_2 = 0. \tag{3} \end{align*} \]

Dual feasibility requires \(\mu_1, \mu_2 \geq 0\), and complementary slackness gives \[ \begin{align*} &\mu_1 (x_1 + x_2 - 1) = 0, \\\\ &\mu_2 (x_1 + x_3 - 1) = 0. \end{align*} \]

Case analysis. Complementary slackness splits the problem into four cases determined by which inequality multipliers vanish.

Only case (iii) yields a KKT point, so by sufficiency the constrained global minimum is \[ \begin{align*} &\mathbf{x}^* = \bigl(\tfrac{1}{2},\, \tfrac{1}{2},\, 0\bigr)^\top, \\\\ &f(\mathbf{x}^*) = \bigl(\tfrac{1}{2} - 2\bigr)^2 + \bigl(\tfrac{1}{2}\bigr)^2 + 0 = \tfrac{5}{2}. \end{align*} \]

Sample code for an optimization problem with KKT conditions and a simple penalty method is as follows:

                            import numpy as np

                            # Objective function and its gradient
                            def objective(x):
                                return (x[0] - 2) ** 2 + x[1] ** 2 + x[2] ** 2

                            def grad_objective(x):
                                return np.array([2 * (x[0] - 2), 2 * x[1], 2 * x[2]])

                            # Constraints and their gradients
                            def inequality_constraint_1(x):
                                return x[0] + x[1] - 1  # g1

                            def inequality_constraint_2(x):
                                return x[0] + x[2] - 1  # g2

                            def equality_constraint(x):
                                return x[0] - x[1]  # h

                            def grad_inequality_constraint_1():
                                return np.array([1, 1, 0])  # Dg1

                            def grad_inequality_constraint_2():
                                return np.array([1, 0, 1])  # Dg2

                            def grad_equality_constraint():
                                return np.array([1, -1, 0])  # Dh

                            # Simple Penalty Method
                            def penalty_method(rho=0.01, lr=0.05, tol=1e-6, max_iter=2000, rho_growth=1.2, clip_grad=1.0):
                                
                                x = np.array([0.5, 0.5, 0.5])  # Initial point
                                
                                for i in range(max_iter):
                                    # Evaluate constraints with clipping to avoid invalid values
                                    g1 = max(0, inequality_constraint_1(x))  # g1(x)
                                    g2 = max(0, inequality_constraint_2(x))  # g2(x)
                                    h = equality_constraint(x)              # h(x)

                                    # Compute gradients of penalty terms
                                    grad_penalty = (
                                        2 * rho * g1 * grad_inequality_constraint_1() +
                                        2 * rho * g2 * grad_inequality_constraint_2() +
                                        2 * rho * h * grad_equality_constraint()
                                    )

                                    # Gradient of the penalized objective function
                                    grad = grad_objective(x) + grad_penalty

                                    # Gradient clipping to prevent instability
                                    grad = np.clip(grad, -clip_grad, clip_grad)

                                    # Gradient descent step: Learning rate diminishes over iterations
                                    lr_t = lr / (1 + i / 10)  
                                    x -= lr_t * grad
                                
                                    # Check convergence
                                    constraint_violation = max(abs(g1), abs(g2), abs(h))
                                    if np.linalg.norm(grad) < tol and constraint_violation < tol:
                                        print(f"Converged after {i + 1} iterations.")
                                        break

                                    # Increase penalty parameter
                                    if abs(h) > tol or g1 > tol or g2 > tol:
                                        rho = min(rho * rho_growth, 1e6)  # Cap rho at a reasonable level

                                    # Debugging output for monitoring progress
                                    #print(f"Iteration {iteration + 1}: x = {x}, f(x) = {objective(x):.6f}, rho = {rho}")

                                return x, objective(x)

                            # KKT method (with Newton's method)
                            def kkt_method(tol = 1e-8, max_iter = 10):
                                # Initial values of parameters: x1, x2, x3, mu1, mu2, s1, s2, lambda
                                # Note: s1 and s2 are slack variables.
                                parameters = np.array([2.0, 0.0, 0.0, 0.5, 0.5, 0.5, 0.5, 0.5]) 

                                for i in range(max_iter):
                                    # Scaling factors (currently identity; adjust if Jacobian becomes ill-conditioned)
                                    # Example: scale = np.array([10.0, 1.0, 1.0]) if x1 has different magnitude
                                    x = parameters[:3] / np.array([1.0, 1.0, 1.0]) 

                                    mu1, mu2, s1, s2, lambd = parameters[3:] 

                                    # Gradients
                                    grad_f = grad_objective(x)
                                    grad_g1 = grad_inequality_constraint_1()
                                    grad_g2 = grad_inequality_constraint_2()
                                    grad_h = grad_equality_constraint()

                                    # Residuals (KKT conditions)
                                    r_stationarity = grad_f + mu1 * grad_g1 + mu2 * grad_g2 + lambd * grad_h
                                    r_primal_feasibility = np.array([
                                            inequality_constraint_1(x) + s1,  # g1(x) + s1 == 0
                                            inequality_constraint_2(x) + s2,  # g2(x) + s2 == 0
                                            equality_constraint(x)            # h(x) == 0
                                    ])
                                    r_complementarity = np.array([
                                            mu1 * s1,  # mu1*s1 == 0
                                            mu2 * s2   # mu2*s2 == 0
                                    ])

                                    # Residual vector
                                    residuals = np.concatenate([r_stationarity, r_primal_feasibility, r_complementarity])

                                    # Jacobian matrix is 8x8 (8 equations and 8 parameters)
                                    # The initial Jacobian matrix: 
                                    #  x1   x2    x3   mu1  mu2  s1   s2  lambda
                                    # [ 2.   0.   0.   1.   1.   0.   0.   1. ] Stationarity 1
                                    # [ 0.   2.   0.   1.   0.   0.   0.  -1. ] Stationarity 2
                                    # [ 0.   0.   2.   0.   1.   0.   0.   0. ] Stationarity 3
                                    # [ 1.   1.   0.   0.   0.   1.   0.   0. ] Primal feasibility g1 + s1
                                    # [ 1.   0.   1.   0.   0.   0.   1.   0. ] Primal feasibility g2 + s2
                                    # [ 1.  -1.   0.   0.   0.   0.   0.   0. ] Primal feasibility h
                                    # [ 0.   0.   0.   0.5  0.   0.5  0.   0. ] Complementarity 1 mu1 & s1
                                    # [ 0.   0.   0.   0.   0.5  0.   0.5  0. ] Complementarity 2 mu2 & s2
                                    jacobian = np.zeros((8, 8))

                                    # Stationarity (Df + mu1*Dg1 + mu2*Dg2 + lambda*Dh)
                                    jacobian[:3, :3] = 2 * np.eye(3)  # Df(x)
                                    jacobian[:3, 3] = grad_g1  # Dg1(x) 
                                    jacobian[:3, 4] = grad_g2  # Dg2(x) 
                                    jacobian[:3, 7] = grad_h   # Dh(x) 

                                    # Primal feasibility (g1(x) + s1, g2(x) + s2, h(x))
                                    jacobian[3, :3] = grad_g1  # Dg1(x) 
                                    jacobian[4, :3] = grad_g2  # Dg2(x) 
                                    jacobian[5, :3] = grad_h   # Dh(x) 
                                    jacobian[3, 5] = 1         # Slack variable s1
                                    jacobian[4, 6] = 1         # Slack variable s2

                                    # Complementarity  (mu1*s1, mu2*s2)
                                    jacobian[6, 3] = s1     # mu1*s1 
                                    jacobian[6, 5] = mu1    # mu1 
                                    jacobian[7, 4] = s2     # mu2*s2 
                                    jacobian[7, 6] = mu2    # mu2 

                                    print(f"Iteration {i+1}:")
                                    print(f" x = {parameters[:3]}, f(x) = {objective(x):.6f}")
                                    print(f"Multipliers: mu1 = {parameters[3]}, mu2 = {parameters[4]}, lambda = {parameters[7]}\n")
                                    
                                    # Solve the system
                                    try:
                                        delta = np.linalg.solve(jacobian, -residuals)
                                    except np.linalg.LinAlgError:
                                        print("Jacobian is singular OR ill-conditioned.")
                                        break

                                    # Update parameters
                                    parameters += delta

                                    # Enforce non-negativity of inequality multipliers and slack variables
                                    parameters[3] = max(0, parameters[3])   # mu1 >= 0
                                    parameters[4] = max(0, parameters[4])   # mu2 >= 0
                                    parameters[5] = max(0, parameters[5])   # s1 >= 0
                                    parameters[6] = max(0, parameters[6])   # s2 >= 0

                                    # Check Convergence
                                    if np.linalg.norm(residuals) < tol:
                                        break

                                x = parameters[:3]
                                return x, parameters[3], parameters[4], parameters[7], objective(x)

                            if __name__ == "__main__":
                                x_opt, mu1, mu2, lambd, f_opt = kkt_method()
                                print(f"Optimal by KKT multipliers: x = {x_opt}, f(x) = {f_opt}")
                                print(f"Multipliers: mu1 = {mu1}, mu2 = {mu2}, lambda = {lambd}\n")
                                
                                x_penalty, f_penalty = penalty_method()
                                x_relative_error  = np.linalg.norm(np.array(x_penalty) - np.array(x_opt)) / np.linalg.norm(np.array(x_opt))
                                f_relative_error  = np.linalg.norm(np.array(f_penalty) - np.array(f_opt)) / np.linalg.norm(np.array(f_opt))
                                print(f"Optimal by Penalty Method: x = {x_penalty}, f(x) = {f_penalty}")
                                print(f"x_Relative_Error: {x_relative_error :.6f}, f_Relative_Error: {f_relative_error :.6f}")
                            

Implementation note: slack variables vs. active-set tracking

Newton-type KKT solvers work with equations, so they do not directly handle the sign conditions \(g_i(\mathbf{x}) \leq 0\) and \(\mu_i \geq 0\) that accompany the complementary slackness equation \(\mu_i\, g_i(\mathbf{x}) = 0\). Two strategies resolve this:

(1) Slack variables. Introduce \(s_i \geq 0\) and convert each inequality to an equality: \[ g_i(\mathbf{x}) \leq 0 \iff g_i(\mathbf{x}) + s_i = 0 \text{ for some } s_i \geq 0. \] Complementary slackness then becomes \(\mu_i\, s_i = 0\), and every remaining inequality is a simple bound \(\mu_i \geq 0\), \(s_i \geq 0\) on a single variable, which a Newton-type solver can handle by clipping (a simple heuristic, as in the code above) or, more robustly, by a barrier.

(2) Active-set methods. Guess which constraints are active, solve the resulting equality-constrained problem, then check whether the guess is consistent with dual feasibility and primal feasibility. If not, we update the active set and repeat. This approach is faster when the correct active set can be identified cheaply, but it requires more logic for updating the active set dynamically.

Modern interior-point methods use a smoothed version of slack-variable tracking with barrier functions, and are among the standard solvers for convex programs. Such programs are the natural setting for duality, which reformulates the primal problem in a way that reveals structure often invisible in the primal.