0
\$\begingroup\$

Intro

I won't specify the problem here, but instead, provide the link to the full implementation.

Code

io.github.coderodde.finance.lce.support.DefaultEquilibrialDebtCutFinder.java:

package io.github.coderodde.finance.lce.support;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static io.github.coderodde.finance.lce.Utils.checkTimeAssignment;
import static io.github.coderodde.finance.lce.Utils.epsilonEquals;
import io.github.coderodde.finance.lce.model.Contract;
import io.github.coderodde.finance.lce.model.DebtCutAssignment;
import io.github.coderodde.finance.lce.model.EquilibrialDebtCutFinder;
import io.github.coderodde.finance.lce.model.Graph;
import io.github.coderodde.finance.lce.model.Node;
import io.github.coderodde.finance.lce.model.TimeAssignment;
import org.apache.commons.math3.optim.OptimizationData;
import org.apache.commons.math3.optim.PointValuePair;
import org.apache.commons.math3.optim.linear.LinearConstraint;
import org.apache.commons.math3.optim.linear.LinearConstraintSet;
import org.apache.commons.math3.optim.linear.LinearObjectiveFunction;
import org.apache.commons.math3.optim.linear.NonNegativeConstraint;
import org.apache.commons.math3.optim.linear.SimplexSolver;
import org.apache.commons.math3.optim.linear.Relationship;

/**
 * This class implements the default equilibrial debt cut finder, which relies
 * on serial simplex method for optimizing the debt cuts.
 * 
 * @author Rodion Efremov
 * @version 1.618
 */
public final strictfp class DefaultEquilibrialDebtCutFinder 
implements EquilibrialDebtCutFinder {

    /**
     * This is a sentinel value to denote the fact that no solution found.
     */
    public static final DebtCutAssignment NO_SOLUTION =
            new DebtCutAssignment(Double.NEGATIVE_INFINITY);
    
    /**
     * The graph this finder is working on.
     */
    private Graph graph;
    
    /**
     * The time assignment object for <code>graph</code>.
     */
    private TimeAssignment timeAssignment;
    
    /**
     * The time at which to attain equilibrium.
     */
    private double equilibriumTime;
    
    /**
     * This map maps contract to unique (column) indices.
     */
    private final Map<Contract, Integer> mci = new HashMap<>();
    
    /**
     * This is the inverse map of <code>mci</code>.
     */
    private final Map<Integer, Contract> mcii = new HashMap<>();
    
    /**
     * This map maps a column index to the appearance index of an independent
     * variable.
     */
    private final Map<Integer, Integer> mivi = new HashMap<>();
    
    /**
     * This map is the inverse of <code>mivi</code>; i.e., it maps appearance
     * index to the column index.
     */
    private final Map<Integer, Integer> mivii = new HashMap<>();
    
    /**
     * Maps a contract to the node it was issued to.
     */
    private final Map<Contract, Node> c2n = new HashMap<>();
    
    /**
     * The duration of matrix reduction.
     */
    private long matrixReductionDuration;
    
    /**
     * The duration of minimization the cuts.
     */
    private long minimizationDuration;
    
    /**
     * The rank of the underlying matrix.
     */
    private int rank;
    
    /**
     * The amount of variables.
     */
    private int variableAmount;
    
    /** 
     * The matrix for current graph.
     */
    private Matrix m;
    
    /**
     * Computes the equilibrial debt cuts.
     * 
     * @param graph           the graph to work on.
     * @param timeAssignment  a map mapping each node <i>u</i> to the moment at 
     *                        which <i>u</i> is ready to pay its debt cuts.
     * @param equilibriumTime the time point at which the graph is supposed to 
     *                        be in equilibrium.
     * 
     * @return a map mapping each contract to its debt cut leading
     * to the global equilibrium.
     */
    @Override
    public DebtCutAssignment compute(Graph graph, 
                                     TimeAssignment timeAssignment,
                                     double equilibriumTime) {
        checkTimeAssignment(graph, timeAssignment);
        this.graph = graph;
        this.timeAssignment = timeAssignment;
        this.equilibriumTime = equilibriumTime;
        this.buildMaps();
        
        this.m = loadMatrix();
        this.variableAmount = m.getColumnAmount() - 1;
        
        long ta = System.currentTimeMillis();
        rank = m.reduceToReducedRowEchelonForm();
        long tb = System.currentTimeMillis();
        this.matrixReductionDuration = tb - ta;
        
        if (m.hasSolution() == false) {
            / This should not happen.
            return NO_SOLUTION;
        }
        
        OptimizationData[] lp = convertMatrixToLinearProgram(m);
        ta = System.currentTimeMillis();
        PointValuePair pvp = new SimplexSolver().optimize(lp);
        tb = System.currentTimeMillis();
        minimizationDuration = tb - ta;
        return extractDebtCuts(pvp);
    }
    
    /**
     * Returns the time spent on reducing the matrix. The return value makes
     * sense only after at least one run of this finder.
     * 
     * @return the time spent on reducing the matrix.
     */
    @Override
    public long getMatrixReductionTime() {
        return this.matrixReductionDuration;
    }
    
    /**
     * Returns the time spent on minimizing the debt cuts.
     * 
     * @return the time spent on minimizing the debt cuts.
     */
    @Override
    public long getMinimizationTime() {
        return minimizationDuration;
    }
    
    /**
     * Extracts the debt cuts from the solution of a linear program.
     * 
     * @param pvp the solution vector.
     * 
     * @return debt cut assignment object.
     */
    private DebtCutAssignment extractDebtCuts(PointValuePair pvp) {
        DebtCutAssignment dca = 
                new DebtCutAssignment(equilibriumTime);
        
        / Process independent variables. mivii maps appearance index to
        / column index.
        for (Map.Entry<Integer, Integer> e : this.mivii.entrySet()) {
            / e.key ~ appearance index,
            / e.value ~ column index
            dca.put(this.mcii.get(e.getValue()), pvp.getPointRef()[e.getKey()]);
        }
        
        / Process dependent variables.
        for (int r = 0; r != rank; ++r) {
            double cut = m.get(variableAmount, r);
            int leadingEntryIndex = -1;
            
            for (int x = r; x != variableAmount; ++x) {
                if (leadingEntryIndex == -1) {
                    if (epsilonEquals(m.get(x, r), 1)) {
                        leadingEntryIndex = x;
                    }
                } else {
                    if (epsilonEquals(m.get(x, r), 0) == false) {
                        cut -= pvp.getPointRef()[this.mivi.get(x)] * m.get(x, r);
                    }
                }
            }
            
            if (epsilonEquals(cut, 0)) {
                cut = 0;
            }
            
            dca.put(this.mcii.get(leadingEntryIndex), cut);
        }
        
        return dca;
    }
    
    /**
     * Converts the matrix to a linear program.
     * 
     * @param m the matrix to extract from.
     * 
     * @return a linear program.
     */
    private OptimizationData[] convertMatrixToLinearProgram(Matrix m) {
        OptimizationData[] od = getLPData(m);
        NonNegativeConstraint nnc = new NonNegativeConstraint(true);
        return new OptimizationData[]{od[0], od[1], nnc};
    }
    
    /**
     * Extracts objective function from matrix <code>m</code>.
     * 
     * @param m the matrix to extract from.
     * 
     * @return an objective function.
     */
    private OptimizationData[] getLPData(Matrix m) {
        int index = 0;
        
        this.mivi.clear();
        this.mivii.clear();
        
        for (int r = 0; r != rank; ++r) {
            boolean leadingEntryFound = false;
            
            for (int c = r; c != variableAmount; ++c) {
                if (leadingEntryFound == false) {
                    if (epsilonEquals(m.get(c, r), 1)) {
                        leadingEntryFound = true;
                    }
                } else {
                    if (epsilonEquals(m.get(c, r), 0) == false) {
                        boolean unique = true;
                        
                        for (int rr = r - 1; rr >= 0; --rr) {
                            if (epsilonEquals(m.get(c, rr), 0) == false) {
                                unique = false;
                                break;
                            }
                        }
                        
                        if (unique) {
                            mivi.put(c, index);
                            mivii.put(index++, c);
                        }
                    }
                }
            }
        }
        
        / The length of the objective function is exactly the amount of
        / independent variables.
        double[] coefficients = new double[mivi.size()];
        List<LinearConstraint> constraintList = 
                new ArrayList<>(2 * variableAmount);
        
        / Create constraints for dependent variables.
        for (int y = 0; y != rank; ++y) {
            int leadingEntryIndex = -1;
            double[] constraintCoefficients = new double[mivi.size()];

            for (int x = y; x != variableAmount; ++x) {
                if (epsilonEquals(m.get(x, y), 0)) {
                    continue;
                }

                if (leadingEntryIndex == -1 
                        && epsilonEquals(m.get(x, y), 1)) {
                    leadingEntryIndex = x;
                } else {
                    int i = mivi.get(x);
                    coefficients[i] -= m.get(x, y);
                    constraintCoefficients[i] = -m.get(x, y);
                }
            }
            
            constraintList.add(new LinearConstraint(
                                   constraintCoefficients,
                                   Relationship.GEQ,
                                   -m.get(variableAmount, y)));
            
            double[] constraintCoefficients2 = constraintCoefficients.clone();
            Contract contract = mcii.get(leadingEntryIndex);
            Node node = c2n.get(contract);
            double duration = timeAssignment.get(node, contract) - 
                              contract.getTimestamp();
            
            constraintList.add(new LinearConstraint(
                                   constraintCoefficients2,
                                   Relationship.LEQ,
                                   contract.evaluate(duration) -
                                   m.get(variableAmount, y)));
        }
        
        / Create constraints for independent variables.
        for (Integer i : mivi.keySet()) {
            double[] constraintCoefficients = new double[mivi.size()];
            
            constraintCoefficients[mivi.get(i)] = 1.0;
            
            Contract c = mcii.get(i);
            Node node = c2n.get(c);
            
            constraintList.add(new LinearConstraint(
                                   constraintCoefficients,
                                   Relationship.LEQ,
                                   c.evaluate(timeAssignment.get(node, c) -
                                              c.getTimestamp())));
            
            coefficients[mivi.get(i)] += 1.0;
        }
       
        / Build the objective function.
        LinearObjectiveFunction of = 
                new LinearObjectiveFunction(coefficients, 0);
        
        return new OptimizationData[]{
            new LinearConstraintSet(constraintList), 
            of
        };
    }
    
    /**
     * Builds some of the maps.
     */
    private void buildMaps() {
        this.mci.clear();
        this.mcii.clear();
        this.c2n.clear();
        
        int index = 0;
            
        for (Node node : graph.getNodes()) {
            for (Contract contract : node.getOutgoingContracts()) {
                this.mci.put(contract, index);
                this.mcii.put(index++, contract);
            }
        }
        
        for (Node node : graph.getNodes()) {
            for (Contract c : node.getIncomingContracts()) {
                this.c2n.put(c, node);
            }
        }
    }
    
    /**
     * Loads the matrix.
     * 
     * @return a set up matrix. 
     */
    private Matrix loadMatrix() {
        / +1 because the result matrix is augmented.
        int COLS = graph.getContractAmount() + 1;
        int ROWS = graph.size();
        
        double[][] matrix = new double[ROWS][COLS];
        
        for (Node node : graph.getNodes()) {
            for (Node debtor : node.getDebtors()) {
                for (Contract contract : node.getContractsTo(debtor)) {
                    contract.setTimestamp( 
                             contract.getTimestamp() - 
                             contract.getShiftCorrection(
                                     timeAssignment.get(debtor, contract)));
                }
            }
        }
        
        int row = 0;
        
        / Load the constant entries.
        for (Node node : graph.getNodes()) {
            matrix[row++][COLS - 1] = computeConstantEntry(node);
        }
        
        row = 0;
        
        / Load everything else.
        for (Node node : graph.getNodes()) {
            loadRow(node, matrix[row++]);
        }
        
        return new Matrix(matrix);
    }
    
    /**
     * Loads a row for node <code>node</code>.
     * 
     * @param node the node whose row to fill up.
     * @param row  the row to fill.
     */
    private void loadRow(Node node, double[] row) {
        for (Node debtor : node.getDebtors()) {
            for (Contract c : node.getContractsTo(debtor)) {
                row[mci.get(c)] += 
                        c.getGrowthFactor(equilibriumTime -
                                          timeAssignment.get(debtor, c));
            }
        }
        
        for (Contract c : node.getIncomingContracts()) {
            row[mci.get(c)] -=
                        c.getGrowthFactor(equilibriumTime -
                                          timeAssignment.get(node, c));
        }
    }
    
    /**
     * Computes the constant entry of the matrix' row corresponding to
     * <code>node</code>
     * 
     * @param node the node.
     * 
     * @return the constant entry belonging the node <code>node</code>.
     */
    private double computeConstantEntry(Node node) {
        double sum = 0;
        Contract tmp;
        
        for (Node debtor : node.getDebtors()) {
            for (Contract contract : node.getContractsTo(debtor)) {
                tmp = contract.clone();
                tmp.setPrincipal(contract.
                                 evaluate(timeAssignment.get(debtor, contract) - 
                                          contract.getTimestamp()));
                
                sum += tmp.evaluate(equilibriumTime - 
                                    timeAssignment.get(debtor, contract));
            }
        }
        
        for (Node lender : node.getLenders()) {
            for (Contract contract : lender.getContractsTo(node)) {
                tmp = contract.clone();
                tmp.setPrincipal(contract.
                                 evaluate(timeAssignment.get(node, contract) -
                                          contract.getTimestamp()));
                
                sum -= tmp.evaluate(equilibriumTime - 
                                    timeAssignment.get(node, contract));
                
            }
        }
        
        return sum;
    }
}

Critique request

My only concern here is how to refactor the code of the main algorithm so that it is better maintainable and readable?

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Not a full review (yet), but any particular reason why we are doing (inaccurate) IEEE 754 double math on money? \$\endgroup\$ Commented Oct 5 at 18:08
  • \$\begingroup\$ @JannikS. Simply due to the lack of experience. \$\endgroup\$ Commented Oct 7 at 6:50

1 Answer 1

1
\$\begingroup\$

Comparing to false

In more than one place you have code like the following. You should never need to explicitly compare a boolean value to true or false.

        if (m.hasSolution() == false) {
            / This should not happen.
            return NO_SOLUTION;
        }

Better:

        if (!m.hasSolution()) {
            / This should not happen.
            return NO_SOLUTION;
        }

Indentation

Your indentation is sometimes a bit excessive.

            constraintList.add(new LinearConstraint(
                                   constraintCoefficients2,
                                   Relationship.LEQ,
                                   contract.evaluate(duration) -
                                   m.get(variableAmount, y)));

Could be formatted as:

            constraintList.add(
                new LinearConstraint(
                    constraintCoefficients2,
                    Relationship.LEQ,
                    contract.evaluate(duration) -
                    m.get(variableAmount, y)
                )
            );

Reduced nesting

Since nothing is nested inside the else branch except another conditional, this can be written with less nesting. This occurs more than once in your code.

                if (leadingEntryFound == false) {
                    if (epsilonEquals(m.get(c, r), 1)) {
                        leadingEntryFound = true;
                    }
                } else {
                    if (epsilonEquals(m.get(c, r), 0) == false) {
                        boolean unique = true;
                        
                        for (int rr = r - 1; rr >= 0; --rr) {
                            if (epsilonEquals(m.get(c, rr), 0) == false) {
                                unique = false;
                                break;
                            }
                        }
                        
                        if (unique) {
                            mivi.put(c, index);
                            mivii.put(index++, c);
                        }
                    }
                }

Less nesting:

                if (!leadingEntryFound) {
                    if (epsilonEquals(m.get(c, r), 1)) {
                        leadingEntryFound = true;
                    }
                } else if (!epsilonEquals(m.get(c, r), 0)) {
                    boolean unique = true;
                        
                    for (int rr = r - 1; rr >= 0; --rr) {
                        if (!epsilonEquals(m.get(c, rr), 0)) {
                            unique = false;
                            break;
                        }
                    }
                        
                    if (unique) {
                        mivi.put(c, index);
                        mivii.put(index++, c);
                    }
                }

Benchmarking stats

You're comparing the times of single runs of each method. Gather more data and analyze it. Small sample sizes are the bane of statistics.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.