Optimization Foundations: From Goal-Seeking to Multivariate

BusinessMath Quarterly Series

10 min read

Part 22 of 12-Week BusinessMath Series


What You’ll Learn


The Problem

Business optimization is everywhere:

Manual optimization (trial-and-error in Excel) doesn’t scale and misses optimal solutions.


The Solution

BusinessMath provides a 5-phase optimization framework:

Phase 1: Goal-Seeking

Find where a function equals a target value:

import BusinessMath

// Profit function with price elasticity
func profit(price: Double) -> Double {
    let quantity = 10_000 - 1_000 * price  // Demand curve
    let revenue = price * quantity
    let fixedCosts = 2_000.0
    let variableCost = 5.0
    let totalCosts = fixedCosts + variableCost * quantity
    return revenue - totalCosts
}

// Find breakeven price (profit = 0)
let breakevenPrice = try goalSeek(
    function: profit,
    target: 0.0,
    guess: 10.0,
    tolerance: 0.01
)

print("Breakeven price: \(breakevenPrice.currency(2))")

Output:

Breakeven price: $9.56

The method: Uses bisection + Newton-Raphson hybrid for robust convergence.


Goal-Seeking for IRR

Internal Rate of Return is a goal-seek problem (find rate where NPV = 0):

let cashFlows = [-1_000.0, 200.0, 300.0, 400.0, 500.0]

func npv(rate: Double) -> Double {
    var npv = 0.0
    for (t, cf) in cashFlows.enumerated() {
        npv += cf / pow(1 + rate, Double(t))
    }
    return npv
}

// Find rate where NPV = 0
let irr = try goalSeek(
    function: npv,
    target: 0.0,
    guess: 0.10
)

print("IRR: \(irr.percent(2))")

Output:

IRR: 12.83%

Phase 2: Vector Operations

Multivariate optimization requires vector operations:

// Create vectors
let v = VectorN([3.0, 4.0])
let w = VectorN([1.0, 2.0])

// Basic operations
let sum = v + w              // [4, 6]
let scaled = 2.0 * v         // [6, 8]

// Norms and distances
print("Norm: \(v.norm)")                // 5.0
print("Distance: \(v.distance(to: w))") // 2.828
print("Dot product: \(v.dot(w))")       // 11.0

Application - Portfolio weights:

let weights = VectorN([0.25, 0.30, 0.25, 0.20])
let returns = VectorN([0.12, 0.15, 0.10, 0.18])

// Portfolio return (weighted average)
let portfolioReturn = weights.dot(returns)
print("Portfolio return: \(portfolioReturn.percent(1))")  // 13.6%

Phase 3: Multivariate Optimization

Optimize functions of multiple variables:

// Minimize Rosenbrock function (classic test problem)
let rosenbrock: (VectorN
            
              ) -> Double = { v in let x = v[0], y = v[1] let a = 1 - x let b = y - x*x return a*a + 100*b*b // Minimum at (1, 1) } // Adam optimizer (adaptive learning rate) let optimizer = MultivariateGradientDescent
              
                >( learningRate: 0.01, maxIterations: 10_000 ) let result = try optimizer.minimizeAdam( function: rosenbrock, initialGuess: VectorN([0.0, 0.0]) ) print("Solution: \(result.solution.toArray())") // ~[1, 1] print("Iterations: \(result.iterations)") print("Final value: \(result.value)") 
              
            

Output:

Solution: [0.9999990406781208, 0.9999980785494371]
Iterations: 704
Final value: 9.210867997017215e-13```

**The power**: Adam finds the minimum automatically with no manual tuning.

---

### BFGS for Smooth Functions

For smooth, well-behaved functions, BFGS converges faster:

```swift
// Quadratic function: f(x) = x^T A x
let A = [[2.0, 0.0, 0.0],
         [0.0, 3.0, 0.0],
         [0.0, 0.0, 4.0]]

let quadratic: (VectorN
            
              ) -> Double = { v in var result = 0.0 for i in 0..<3 { for j in 0..<3 { result += v[i] * A[i][j] * v[j] } } return result } let bfgs = MultivariateNewtonRaphson
              
                >( maxIterations: 50 ) let resultBFGS = try bfgs.minimize( quadratic, from: VectorN([5.0, 5.0, 5.0]) ) print("Converged in \(result.iterations) iterations") print("Solution: \(result.solution.toArray())") // ~[0, 0, 0] 
              
            

Output:

Converged in 12 iterations
Solution: [0.000, 0.000, 0.000]

The comparison: BFGS took 12 iterations vs. Adam’s 4,782. For smooth functions, second-order methods dominate.


Phase 4: Constrained Optimization

Optimize with equality and inequality constraints:

// Minimize x² + y² subject to x + y = 1
let objective: (VectorN
            
              ) -> Double = { v in v[0]*v[0] + v[1]*v[1] } let optimizerConstrained = ConstrainedOptimizer
              
                >() let resultConstrained = try optimizerConstrained.minimize( objective, from: VectorN([0.0, 1.0]), subjectTo: [ .equality { v in v[0] + v[1] - 1.0 } ] ) print("Solution: \(resultConstrained.solution.toArray())") // [0.5, 0.5] // Shadow price (Lagrange multiplier) if let lambda = resultConstrained.lagrangeMultipliers.first { print("Shadow price: \(lambda.number(3))") // How much objective improves if constraint relaxed } 
              
            

Output:

Solution: [0.5, 0.5]
Shadow price: 0.500

The interpretation: If we relax the constraint from “x + y = 1” to “x + y = 1.01”, the objective improves by ~0.005 (shadow price × change).


Real-World: Portfolio with Constraints

Minimize portfolio risk subject to target return:

let expectedReturns = VectorN([0.08, 0.12, 0.15])
let covarianceMatrix = [
    [0.0400, 0.0100, 0.0080],
    [0.0100, 0.0900, 0.0200],
    [0.0080, 0.0200, 0.1600]
]

// Portfolio variance function
let portfolioVariance: (VectorN
            
              ) -> Double = { weights in var variance = 0.0 for i in 0..<3 { for j in 0..<3 { variance += weights[i] * weights[j] * covarianceMatrix[i][j] } } return variance } let portfolioOptimizer = InequalityOptimizer
              
                >() let result = try portfolioOptimizer.minimize( portfolioVariance, from: VectorN([0.4, 0.4, 0.2]), subjectTo: [ // Target return ≥ 10% .inequality { w in let ret = w.dot(expectedReturns) return 0.10 - ret // ≤ 0 means ret ≥ 10% }, // Fully invested .equality { w in w.reduce(0, +) - 1.0 }, // Long-only .inequality { w in -w[0] }, .inequality { w in -w[1] }, .inequality { w in -w[2] } ] ) print("Optimal weights: \(result.solution.toArray())") print("Portfolio variance: \(portfolioVariance(result.solution).number(4))") print("Portfolio volatility: \((sqrt(portfolioVariance(result.solution))).percent(1))") 
              
            

Output:

Optimal weights: [0.6099086625245681, 0.2435453283923856, 0.1465460569466559]
Portfolio variance: 0.0295
Portfolio volatility: 17.2%

The solution: 45% in asset 1 (low risk), 35% in asset 2 (medium), 20% in asset 3 (high return). Achieves 10% target return with minimum possible risk.


Try It Yourself

Click to expand full playground code
import BusinessMath
import Foundation

// Profit function with price elasticity
func profit(price: Double) -> Double {
	let quantity = 10_000 - 1_000 * price  // Demand curve
	let revenue = price * quantity
	let fixedCosts = 2_000.0
	let variableCost = 5.0
	let totalCosts = fixedCosts + variableCost * quantity
	return revenue - totalCosts
}

	// Find breakeven price (profit = 0)
	let breakevenPrice = try goalSeek(
		function: profit,
		target: 0.0,
		guess: 10.0,
		tolerance: 0.01
	)
	print("Breakeven price: \(breakevenPrice.currency(2))")


// MARK: - Goal Seeking for IRR

let cashFlows = [-1_000.0, 200.0, 300.0, 400.0, 500.0]

func npv(rate: Double) -> Double {
	var npv = 0.0
	for (t, cf) in cashFlows.enumerated() {
		npv += cf / pow(1 + rate, Double(t))
	}
	return npv
}

// Find rate where NPV = 0
let irr = try goalSeek(
	function: npv,
	target: 0.0,
	guess: 0.10
)

print("IRR: \(irr.percent(2))")


// MARK: - Vector Operations

// Create vectors
let v = VectorN([3.0, 4.0])
let w = VectorN([1.0, 2.0])

// Basic operations
let sum = v + w              // [4, 6]
let scaled = 2.0 * v         // [6, 8]

// Norms and distances
print("Norm: \(v.norm)")                // 5.0
print("Distance: \(v.distance(to: w))") // 2.828
print("Dot product: \(v.dot(w))")       // 11.0


// MARK: - Portfolio Weights

let weights = VectorN([0.25, 0.30, 0.25, 0.20])
let returns = VectorN([0.12, 0.15, 0.10, 0.18])

// Portfolio return (weighted average)
let portfolioReturn = weights.dot(returns)
print("Portfolio return: \(portfolioReturn.percent(1))")  // 13.6%

// MARK: - Multivariate Operations

// Minimize Rosenbrock function (classic test problem)
let rosenbrock: (VectorN
              
                ) -> Double = { v in let x = v[0], y = v[1] let a = 1 - x let b = y - x*x return a*a + 100*b*b // Minimum at (1, 1) } // Adam optimizer (adaptive learning rate) let optimizer = MultivariateGradientDescent
                
                  >( learningRate: 0.01, maxIterations: 10_000 ) let result = try optimizer.minimizeAdam( function: rosenbrock, initialGuess: VectorN([0.0, 0.0]) ) print("Solution: \(result.solution.toArray())") // ~[1, 1] print("Iterations: \(result.iterations)") print("Final value: \(result.value)") // MARK: - BFGS // Quadratic function: f(x) = x^T A x let A = [[2.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 4.0]] let quadratic: (VectorN
                  
                    ) -> Double = { v in var result = 0.0 for i in 0..<3 { for j in 0..<3 { result += v[i] * A[i][j] * v[j] } } return result } let bfgs = MultivariateNewtonRaphson
                    
                      >( maxIterations: 50 ) let resultBFGS = try bfgs.minimize( quadratic, from: VectorN([5.0, 5.0, 5.0]) ) print("Converged in \(resultBFGS.iterations) iterations") print("Solution: \(resultBFGS.solution.toArray())") // ~[0, 0, 0] // MARK: - Constrained Optimization // Minimize x² + y² subject to x + y = 1 let objective: (VectorN
                      
                        ) -> Double = { v in v[0]*v[0] + v[1]*v[1] } let optimizerConstrained = ConstrainedOptimizer
                        
                          >() let resultConstrained = try optimizerConstrained.minimize( objective, from: VectorN([0.0, 1.0]), subjectTo: [ .equality { v in v[0] + v[1] - 1.0 } ] ) print("Solution: \(resultConstrained.solution.toArray())") // [0.5, 0.5] // Shadow price (Lagrange multiplier) if let lambda = resultConstrained.lagrangeMultipliers.first { print("Shadow price: \(lambda.number(3))") // How much objective improves if constraint relaxed } // MARK: - Portfolio with Constraints let expectedReturns = VectorN([0.08, 0.12, 0.15]) let covarianceMatrix = [ [0.0400, 0.0100, 0.0080], [0.0100, 0.0900, 0.0200], [0.0080, 0.0200, 0.1600] ] // Portfolio variance function let portfolioVariance: (VectorN
                          
                            ) -> Double = { weights in var variance = 0.0 for i in 0..<3 { for j in 0..<3 { variance += weights[i] * weights[j] * covarianceMatrix[i][j] } } return variance } let portfolioOptimizer = InequalityOptimizer
                            
                              >() let resultPortfolio = try portfolioOptimizer.minimize( portfolioVariance, from: VectorN([0.4, 0.4, 0.2]), subjectTo: [ // Target return ≥ 10% .inequality { w in let ret = w.dot(expectedReturns) return 0.10 - ret // ≤ 0 means ret ≥ 10% }, // Fully invested .equality { w in w.reduce(0, +) - 1.0 }, // Long-only .inequality { w in -w[0] }, .inequality { w in -w[1] }, .inequality { w in -w[2] } ] ) print("Optimal weights: \(resultPortfolio.solution.toArray())") print("Portfolio variance: \(portfolioVariance(resultPortfolio.solution).number(4))") print("Portfolio volatility: \((sqrt(portfolioVariance(resultPortfolio.solution))).percent(1))") 
                            
                          
                        
                      
                    
                  
                
              

→ Full API Reference: BusinessMath Docs – 5.1 Optimization Guide

Modifications to try:

  1. Find the profit-maximizing price (not just breakeven)
  2. Build a 10-asset portfolio with sector constraints
  3. Optimize production mix given resource constraints
  4. Compare Adam vs. BFGS vs. gradient descent convergence

Real-World Application

CFO use case: “We manufacture 3 products in 2 factories. Each product has different margins, each factory has capacity constraints. Find the production mix that maximizes EBITDA.”

BusinessMath makes this programmatic, not a manual Excel Solver exercise.


★ Insight ─────────────────────────────────────

Why Second-Order Methods (BFGS) Beat First-Order (Gradient Descent)

Gradient descent uses only the slope (first derivative). BFGS uses the curvature (second derivative via Hessian approximation).

Analogy: Finding the bottom of a valley.

Trade-off: BFGS is faster (fewer iterations) but more complex (memory for Hessian approximation).

Rule of thumb: Use Adam for non-smooth, noisy functions. Use BFGS for smooth, well-behaved functions.

─────────────────────────────────────────────────


📝 Development Note

The hardest part was choosing default optimization algorithms. We provide multiple (Adam, BFGS, Nelder-Mead, simulated annealing) because no single algorithm dominates:

Rather than pick one “default,” we expose all and provide guidance on when to use each.

Related Methodology: Test-First Development (Week 1) - We tested each optimizer on standard test functions (Rosenbrock, Rastrigin, etc.) with known solutions.


Next Steps

Coming up tomorrow: Portfolio Optimization - Deep dive into Modern Portfolio Theory, efficient frontiers, and risk parity.


Series Progress:


Tagged with: optimization