Initializing help system before first use

Capital budgeting - Using multi-objective optimization


Type: Capital budgeting
Rating: 2 (easy-medium)
Description: Capital budgeting example, solved using three multi-objective approaches:
  • Lexicographic approach, solving first to minimize capital expended and second to maximize return
  • Blended approach, solving a weighted combination of both objectives simultaneously
  • Lexicographic approach, with the objective priorities reversed
File(s): CapitalBudgeting.cs, CapitalBudgeting.csproj, CapBgt2l.cs, CapBgt2l.csproj


CapitalBudgeting.cs
// (c) 2023-2025 Fair Isaac Corporation

using System;
using Optimizer;
using Optimizer.Objects;
using static Optimizer.Objects.Utils;

namespace XpressExamples
{
    /// <summary>Capital budgeting example, solved using three multi-objective approaches</summary>
    /// <remarks>
    /// <list type='number'>
    /// <item><description>
    ///   Lexicographic approach, solving first to minimize capital expended
    ///   and second to maximize return
    /// </description></item>
    /// <item><description>
    ///   Blended approach, solving a weighted combination of both objectives
    ///   simultaneously
    /// </description></item>
    /// <item><description>
    ///   Lexicographic approach, with the objective priorities reversed
    /// </description></item>
    /// </list>
    class CapitalBudgeting : IDisposable
    {
        /// Required capital for each project
        static readonly double[] CAPITAL = { 104000, 53000, 29000, 187000, 98000, 32000, 75000, 200000 };
        /// Required personnel for each project
        static readonly double[] PERSONNEL = { 22, 12, 7, 36, 24, 10, 20, 41 };
        /// Expected return of each project
        static readonly double[] RETURN = { 124000, 74000, 42000, 188000, 108000, 56000, 88000, 225000 };
        /// Target capital to invest
        static readonly double CAPITAL_TARGET = 478000;
        /// Target return on investment
        static readonly double ROI_TARGET = 550000;
        /// Available personnel
        static readonly int PERSONNEL_MAX = 106;

        private readonly XpressProblem prob;

        /// Binary decision variables indicating which projects will be implemented
        private Variable[] select;

        /// Constraint for the number of personnel available
        private Inequality personnel;

        /// Primary objective: minimize capital expended
        private LinExpression capital;

        /// Secondary objective: maximize return on investment
        private LinExpression roi;

        public CapitalBudgeting()
        {
            prob = new XpressProblem();
        }

        public void Dispose()
        {
            prob.Dispose();
        }

        public static void Main(string[] args)
        {
            using (CapitalBudgeting capBudget = new CapitalBudgeting())
            {
                capBudget.Solve();
            }
        }

        public void Solve()
        {
            // Binary decision variables indicating which projects will be implemented
            select = prob.AddVariables(CAPITAL.Length)
                .WithType(ColumnType.Binary)
                .WithName("select_{0}")
                .ToArray();

            // Constraint: at least 3 projects must be implemented
            prob.AddConstraint(Sum(select) >= 3);

            // Constraint for the number of personnel available
            personnel = prob.AddConstraint(ScalarProduct(select, PERSONNEL) <= PERSONNEL_MAX);

            // Primary objective: minimize capital expended
            capital = ScalarProduct(select, CAPITAL);
            // The first objective has priority=1 (weight=1 by default)
            prob.SetObjective(capital, ObjSense.Minimize);
            prob.SetObjIntControl(0, ObjControl.Priority, 1);

            // Secondary objective: maximize return on investment
            roi = ScalarProduct(select, RETURN);
            // The second objective has weight=-1 to maximize this expression, and priority=0
            // to cause this objective to be solved in a second optimization
            prob.AddObjective(roi, 0, -1);

            prob.Optimize();
            PrintSolution("Higher priority for 'Minimize Capital' objective:");

            // Now set the same priority for both objectives. This will result in a single solve
            // using the weighted sum of the two objectives.
            prob.SetObjIntControl(1, ObjControl.Priority, 1);
            prob.Optimize();
            PrintSolution("Equal priority for objectives:");

            // Finally, give the first objective a lower priority
            prob.SetObjIntControl(0, ObjControl.Priority, 0);
            prob.Optimize();
            PrintSolution("Higher priority for 'Maximize Return' objective:");
        }

        /// <summary>
        /// Prints the current solution to the problem.
        /// </summary>
        /// <param name="title">a title to print before the solution</param>
        private void PrintSolution(String title)
        {
            Console.WriteLine(title);
            if (prob.SolveStatus != SolveStatus.Completed ||
                prob.SolStatus != SolStatus.Optimal)
            {
                Console.WriteLine("  Problem not solved");
            }
            else
            {
                Console.WriteLine($"  Objectives: {prob.Objectives}, solved objectives: {prob.SolvedObjs}");

                double[] sol = prob.GetSolution();
                double capitalUsed = capital.Evaluate(sol);
                double roiGained = roi.Evaluate(sol);
                if (capitalUsed > CAPITAL_TARGET)
                {
                    Console.WriteLine("  Unable to meet Capital target");
                }
                if (roiGained < ROI_TARGET)
                {
                    Console.WriteLine("  Unable to meet Return target");
                }

                Console.Write("  Projects undertaken:");
                for (int p = 0; p < select.Length; p++)
                {
                    if (select[p].GetSolution() >= 0.99)
                    {
                        Console.Write($" {p}");
                    }
                }
                Console.WriteLine();

                Console.WriteLine($"  Used capital: ${capitalUsed}," +
                    (CAPITAL_TARGET >= capitalUsed ? " unused: $" : " overused: $") +
                    Math.Abs(CAPITAL_TARGET - capitalUsed));
                Console.WriteLine($"  Total return: ${roiGained}");
                Console.WriteLine($"  Unused personnel: {(int)personnel.GetSlack()} persons");
            }
        }
    }
}

CapitalBudgeting.csproj
<?xml version="1.0" encoding="UTF-8" standalone="no"?><Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>

    <IsPackable>false</IsPackable>
    <XpressExampleFiles Condition="'$(XpressExampleFiles)'==''">../../../data</XpressExampleFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="FICO.Xpress.XPRSdn" Version="45.1.1"/> <!-- Version 45.01.01 or later -->
  </ItemGroup>
  
</Project>

CapBgt2l.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Optimizer;
using Optimizer.Objects;
using static Optimizer.Objects.Utils;

namespace XPRSexamples
{
    /// <summary>Capital budgeting problem.</summary>
    /// <remarks>
    /// Illustrates logical conditions, formulation using logic constraints
    /// and indicator constraints.
    /// </remarks>
    internal class CapBgt2l
    {
        /// <summary>A project.</summary>
        public sealed class Project
        {
            /// <summary>The name of this project.</summary>
            public readonly string Name;
            /// <summary>Payout for the project.</summary>
            public readonly double Payout;
            /// <summary>Capital investment for the project.</summary>
            public readonly double Investment;
            /// <summary>Number of personnel required for the project.</summary>
            public readonly int Personnel;

            public Project(String name, double payout, double investment, int personnel)
            {
                this.Name = name;
                this.Payout = payout;
                this.Investment = investment;
                this.Personnel = personnel;
            }

            public override string ToString()
            {
                return Name;
            }
        }

        /** The projects used in this example. */
        private static readonly Project[] projectArray = new Project[] { new Project("Alpha", 124000.0, 104000.0, 22),
            new Project("Beta", 74000.0, 53000.0, 12), new Project("Gamma", 42000.0, 29000.0, 7),
            new Project("Delta", 188000.0, 187000.0, 36), new Project("Epsilon", 108000.0, 98000.0, 24),
            new Project("Zeta", 56000.0, 32000.0, 10), new Project("Eta", 88000.0, 75000.0, 20),
            new Project("Theta", 225000.0, 200000.0, 41) };

        // The resource constraints used in this example.
        const double budget = 478000.0;
        const int workforce = 106;

        public static void Main(string[] args)
        {
            using (XpressProblem prob = new XpressProblem())
            {
                // Output all messages.
                prob.callbacks.AddMessageCallback(DefaultMessageListener.Console);

                /**** VARIABLES ****/

                // Whether to undertake a specific project
                Variable[] x = prob.AddVariables(projectArray.Length)
                        .WithName(i => $"x_{i}")
                        .WithType(ColumnType.Binary)
                        .ToArray();

                // objective function: sum of payouts of all undertaken projects
                Expression totalProfit = Sum(projectArray.Length, i => x[i] * projectArray[i].Payout);

                prob.SetObjective(totalProfit, ObjSense.Maximize);

                // limits on resource availability
                // sum of investments of all undertaken projects should not exceed budget
                Inequality investmentLimit = prob
                        .AddConstraint(Sum(projectArray.Length, i => x[i] * projectArray[i].Investment) <= budget);
                // sum of personnel committed of all undertaken projects should not exceed
                // workforce
                Inequality workforceLimit = prob
                        .AddConstraint(Sum(projectArray.Length, i => x[i] * projectArray[i].Personnel) <= workforce);

                // project alpha can only be done if both gamma and zeta happen
                prob.AddConstraint(x[0] <= x[2]);
                prob.AddConstraint(x[0] <= x[5]);

                // project zeta can only be done if project epsilon happens
                prob.AddConstraint(x[5] <= x[4]);

                // projects alpha and beta as well as gamma and delta can only happen together
                prob.AddConstraint(x[0] == x[1]);
                prob.AddConstraint(x[2] == x[3]);

                // exactly one of those pairs should be invested in, i.e., if project alpha is
                // performed,
                // neither gamma nor delta can be invested in, and if project alpha does not
                // happen, then
                // projects gamma and delta have to be performed
                prob.AddConstraint(x[0].IfThen(x[2] + x[3] <= 0.0));
                prob.AddConstraint(x[0].IfNotThen(x[2] + x[3] == 2.0));

                // Dump the problem to disk so that we can inspect it.
                prob.WriteProb("capbgt2l.lp");

                // Solve
                prob.Optimize();
                if (prob.SolStatus != SolStatus.Optimal && prob.SolStatus != SolStatus.Feasible)
                    throw new Exception($"optimization failed with status {prob.SolStatus}");
                double[] sol = prob.GetSolution();

                // Print the objective
                Console.WriteLine($"Objective: {prob.ObjVal}");

                // Print the interesting slacks
                Console.WriteLine($"Remaining Budget: {investmentLimit.GetSlack()}");
                Console.WriteLine($"Remaining Workers: {Math.Round(workforceLimit.GetSlack())}");

                // Print out the solution
                for (int i = 0; i < projectArray.Length; ++i)
                {
                    if (x[i].GetValue(sol) > 0.5)
                    {
                        Console.WriteLine($"Undertaking project {projectArray[i].ToString()}.");
                    }
                }
            }
        }
    }
}

CapBgt2l.csproj
<?xml version="1.0" encoding="UTF-8" standalone="no"?><Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>

    <IsPackable>false</IsPackable>
    <XpressExampleFiles Condition="'$(XpressExampleFiles)'==''">../../../data</XpressExampleFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="FICO.Xpress.XPRSdn" Version="45.1.1"/> <!-- Version 45.01.01 or later -->
  </ItemGroup>
  
</Project>

© 2001-2025 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.