Algorithm for calculation of future good amounts with complex production lines

Viewed 172

This is a very long post, but only the first half is really relevant.  The second half describes only what I have tried to solve it, but what seemed to me too inefficient (it can perhaps help to get the idea of what I want). The relevant part ends with the line after the bolded question.

I am trying to simulate multiple productions in an imaginary factory to calculate what amount of goods of each type known will be available at the end. There are several different goods types, and they have all a specific maximum production capacity that can only be reached if enough ingredients are available. An example of how the production lines could look is here: An example of production lines

The goods at the bottom all have a known rate at which they are delivered to the factory, so for those, there is nothing to calculate, though this rate can change over time (also, the maximum production capacity can also change at any point in time, e.g., the capacity can be increased by adding workers or more machines).

As shown in the picture, for the other goods there are three things to look at:

  • Some lines produce a good out of a single other one.
  • Some lines produce a good out of two others.
  • Some lines have a good used for creation of more than one new good (see, for example, "R8" in the middle of the illustration).

I have the following information:

  • Maximum production rate of each good (5 produced per hour, for example)
  • for the bottom goods we have the amount delivered to the factory (5 delivered per hour, for example)
  • how much of each is in stock now (so in case there is not enough delivered, if we still have some in stock, we don't need to reduce production)
  • At what times the delivery of a good will change (can happen to any good at the bottom)
  • At what times the maximum production rate of a good will change (can happen to any good not at the bottom)

With this information, I want to calculate the amount of each good at a given time in the future. I would like this to be as efficient as possible, since I need these calculations quite often.

I tried to make an implementation for this in Java, but I have some problems. Instead of fixing them I looked at my algorithm again and figured out it did not look as if it was very efficient anyway, so I wanted to know if someone has already seen or solved this kind of problem?


The way I tried to solve this is following:

  1. I create maximum production (and delivery) intervals for each good using the known information when a production (or delivery) amount changes.
  2. I put all resources at the bottom in a remaining Set and a checked Set (bottom goods are immediately checked ones).
  3. I calculate the actual amount of goods produced for each good: therefore, I take each good in remaining and I check what goods can be produced, if all that can be produced only are made of checked goods I calculate the actual amount produced depending of the maximum rate and the available goods (depending on the production of the things it is made of and the amount in stock if this is less). Additionally, in this step I add production intervals if due to lesser production of a source good (but some in stock at the beginning) the production needs to be reduced. When finished the goods the new ones are made of get removed from the remaining Set and the new ones are added, as well as being added to the checked Set.
  4. Now we have all the actual good productions for each good and we can calculate it. For this we loop over each good and take the actual production and add it up using the interval borders for time. We have now the amount of goods at the wanted time in the future.

Additional info: we cannot do the point 4. without 3. since the actual amount we calculate for a good, can be consumed again for the production of the next one, so we need need this Step in between.

If it helps to understand what I have done (or wanted to do) I add my code (not working). The class is already initialized with the maximum production rate intervals of each produced good currently in production. Since other goods can be in stock, for all goods that are not included we initialize them to with a production of zero and one interval.

public class FactoryGoods {
    
    private long future;
    private long now;
    private Map<String, Integer> availableGoods;
    private Map<String, ArrayList<ProductionInterval>> hourlyGoodIncrease;

    /**
     * 
     * @param future long current time
     * @param now long last time the factory's resources got updates
     * @param availableGoods Map<String,Integer> of the goods in the factory
     * @param hourlyGoodIncrease Map<String,ArrayList<ProductionInterval>> of the intervals of production quantities for the goods
     */
    public factoryGoods(long future, long now, Map<String,Integer> availableGoods, Map<String,ArrayList<ProductionInterval>> hourlyGoodIncrease) {
        this.future = future;
        this.now = now;
        this.availableGoods = availableGoods;
        this.hourlyGoodIncrease = hourlyGoodIncrease;
    }
    
    /**
     * Calculates the resources present in a factory's storage
     * @return a Map of quantities mapped on the String name of the good
     */
    public Map<String,Integer> getResources() {
        // Make sure all goods to have all goods inside the loop, to work on goods,
        // that are produced, but also those which are only in store
        HashMap<String, Boolean> goodChecked = new HashMap<String,Boolean>();
        Set<String> remaining = new HashSet<String>();
        for (Goods good: Goods.values()) {
            String g = good.get();
            if (hourlyGoodIncrease.get(g) == null) {
                ArrayList<ProductionInterval> prods = new ArrayList<ProductionInterval>();
                ProductionInterval start = new ProductionInterval(now, 0);
                prods.add(start);
                hourlyGoodIncrease.put(g, prods);
            }
            if (availableGoods.get(g) == null) {
                availableGoods.put(g, 0);
            }
            if (good.isPrimary()) {
                goodChecked.put(g, true);
            } else {
                goodChecked.put(g, false);
            }
            remaining.add(g);
        }
        
        // As long as goods are remaining to be checked loops over the goods, and
        // recalculates hourly good increases for goods, that have all its sources
        // already calculated
        while (remaining.size() > 0) {
            Set<String> removes = new HashSet<String>();
            
            for (String good: remaining) {
                
                if (goodChecked.get(good)) {
                    Good g = GoodFactory.get(good);
                    Set<String> to = new HashSet<String>();
                    Map<String,Float> from = new HashMap<String,Float>();
                    setUpFromAndToGoods(g, to, from, availableGoods);
                    if (areGoodsAlreadyCalculated(to, goodChecked)) {
                        //remaining.remove(good);
                        removes.add(good);
                    } else {
                        if (areGoodsReadyForCalculation(to, goodChecked)) {
                            // Get all resources we are working on now
                            Map<String,Float> fromDecrease = new HashMap<String,Float>();
                            for (String t: to) {
                                for (String f: GoodFactory.get(t).isMadeFrom().keySet()) {
                                    from.put(f, (float) availableGoods.get(f));
                                }
                            }
                            // Get all interval borders
                            ArrayList<Long> intervalBorders = new ArrayList<Long>();
                            for (String wGood: from.keySet()) {
                                ArrayList<ProductionInterval> intervals = hourlyGoodIncrease.get(wGood);
                                for (ProductionInterval interval: intervals) {
                                    long intervalStart = interval.getStartTime();
                                    if (!intervalBorders.contains(intervalStart)) {
                                        intervalBorders.add(intervalStart);
                                    }
                                }
                            }
                            Collections.sort(intervalBorders);
                            intervalBorders.add(future);
                            for (String f: from.keySet()) {
                                hourlyGoodIncrease.put(f, createNewProductionIntervalls(intervalBorders, hourlyGoodIncrease.get(f)));
                            }
                            // For all intervals
                            int iLast = intervalBorders.size() - 1;
                            for (int i = 0; i < iLast; i++) {
                                long elapsedTime = intervalBorders.get(i + 1) - intervalBorders.get(i);
                                for (String t: to) {
                                    Map<String, Float> source = GoodFactory.get(t).isMadeFrom();
                                    for (String s: source.keySet()) {
                                        Float decrease = fromDecrease.get(s);
                                        fromDecrease.put(s, (decrease != null ? decrease : 0) + source.get(s));
                                    }
                                }
                                // Calculate amount after normal maximum production
                                Set<String> negatives = new HashSet<String>();
                                Map<String,Float> nextFrom = new HashMap<String,Float>();
                                for (String f: from.keySet()) {
                                    float delta = from.get(f) + (hourlyGoodIncrease.get(f).get(i).getHourlyIncrease() - fromDecrease.get(f)) * elapsedTime / (1000 * 60 * 60);
                                    nextFrom.put(f, delta);
                                    if (delta < 0) {
                                        negatives.add(f);
                                    }
                                }
                                // Check if got under zero
                                if (negatives.size() == 0) {
                                    for (String f: from.keySet()) {
                                        float newIncrease = hourlyGoodIncrease.get(f).get(i).getHourlyIncrease() - fromDecrease.get(f);
                                        hourlyGoodIncrease.get(f).get(i).setHourlyIncrease(newIncrease);
                                        from.put(f, nextFrom.get(f));
                                    }
                                } else {
                                    // TODO: handle case when more is used than exists
                                }
                                // Else calculate point where at least one from is zero and add an interval
                                // before its maximum, after needs to be adjusted
                            }
                            
                            // Break to remove all calculated goods from the remaining set and rerun the loop
                            removes = to;
                            break;
                        }
                    }
                }
            }
            for (String remove: removes) {
                remaining.remove(remove);
            }
        }
        
        
        // Final calculation of the goods amounts that are available in the factory
        for (String good: goodChecked.keySet()) {
            ArrayList<ProductionInterval> intervals = hourlyGoodIncrease.get(good);
            intervals.add(new ProductionInterval(future, 0));
            float after = availableGoods.get(good);
            for (int i = 0; i < (intervals.size() - 1); i++) {
                after += intervals.get(i).getHourlyIncrease() * (intervals.get(i + 1).getStartTime() - intervals.get(i).getStartTime()) / (1000 * 60 * 60);
            }
            availableGoods.put(good, (int) after);
        }
        
        return availableGoods;
    }
    
    private static ArrayList<ProductionInterval> createNewProductionIntervalls(ArrayList<Long> intervalBorders, ArrayList<ProductionInterval> hourlyIncreases) {
        System.out.print("intervalBorders\n");
        System.out.print(intervalBorders + "\n");
        
        System.out.print("hourlyIncreases\n");
        System.out.print(hourlyIncreases + "\n");
        
        ArrayList<ProductionInterval> intervalls = new ArrayList<ProductionInterval>();
        int i = 0;
        long iTime = 0;
        long nextTime = 0;
        for (long l: intervalBorders) {
            float increase = 0;
            iTime = hourlyIncreases.get(i).getStartTime();
            if (i + 1 < hourlyIncreases.size()) {
                nextTime = hourlyIncreases.get(i + 1).getStartTime();
            }
            if (l == iTime) {
                increase = hourlyIncreases.get(i).getHourlyIncrease();
            } else if (iTime < l && l < nextTime) {
                increase = hourlyIncreases.get(i).getHourlyIncrease();
            } else if (l == nextTime) {
                increase = hourlyIncreases.get(++i).getHourlyIncrease();
            }
            intervalls.add(new ProductionInterval(l, increase));
        }
        return intervalls;
    }
    
    private static void setUpFromAndToGoods(Good g, Set<String> to, Map<String,Float> from, Map<String,Integer> availableGoods) {
        Set<String> unchecked = g.isUsedToCreate();
        while (unchecked.size() > 0) {
            String good = unchecked.iterator().next();
            unchecked.remove(good);
            to.add(good);
            Set<String> madeFrom = GoodFactory.get(good).isMadeFrom().keySet();
            for (String fromGood: madeFrom) {
                if (!from.containsKey(fromGood)) {
                    from.put(fromGood, (float) availableGoods.get(fromGood));
                    Set<String> additions = GoodFactory.get(fromGood).isUsedToCreate();
                    for (String addition: additions) {
                        if (!to.contains(addition) && !unchecked.contains(addition)) {
                            unchecked.add(addition);
                        }
                    }
                }
            }
        }
    }
    
    private static boolean areGoodsReadyForCalculation(Set<String> toGoods, Map<String,Boolean> goodChecked) {
        for (String t: toGoods) {
            Good toGood = GoodFactory.get(t);
            for (String from: toGood.isMadeFrom().keySet()) {
                if (!goodChecked.get(from)) {
                    return false;
                }
            }
        }
        return true;
    }
    
    private static boolean areGoodsAlreadyCalculated(Set<String> toGoods, Map<String,Boolean> goodChecked) {
        for (String t: toGoods) {
            if (!goodChecked.get(t)) {
                return false;
            }
        }
        return true;
    }

}

public class ProductionInterval {
    
    private long startTime;
    private float hourlyIncrease;
    
    public ProductionInterval(long startTime, float hourlyIncrease) {
        this.setStartTime(startTime);
        this.setHourlyIncrease(hourlyIncrease);
    }

    public float getHourlyIncrease() {
        return hourlyIncrease;
    }

    public void setHourlyIncrease(float hourlyIncrease) {
        this.hourlyIncrease = hourlyIncrease;
    }

    public long getStartTime() {
        return startTime;
    }

    public void setStartTime(long startTime) {
        this.startTime = startTime;
    }
    
    public String toString() {
        return "<starttime=" + this.startTime + ", hourlyIncrease=" + this.hourlyIncrease + ">";
    }

}

Does someone know an algorithm that can solve my problem, or have some ideas how I can change my algorithm so that it gets more efficient? (I know it does not work at all, but with all these loops, I don't think it will be efficient and I would like to know if someone sees something I could make better before I put the work into finishing it).

2 Answers

You can apply a max flow algorithm like the Edmonds-Karp with few modifications, and you need to build the graph to feed to the algo:

  1. Create a node for each good
  2. You need one "source" node and one "sink" node
  3. For each delivered good, create an arc from the source to respective node, with the capacity equal to delivery rate
  4. For each final product, create an arc from its respective node to the sink, with capacity equal to production rate
  5. For each dependency between goods, create an arc between respective nodes with capacity of one.
  6. For each good, create an arc from source to the respective node with capacity equal to amount of the good in stock (for first iteration it's zero)

The results will be the flows from final goods nodes to the sink after the algorithm is finished. For your case, you need two modifications:

  1. When calculating flow at a node, you take the minimum of the flows to it (since you require all dependencies to create a good), and then cap it at this good's maximum production rate for non-delivered goods
  2. You need to account for change of goods in stock - will edit the answer later

Although, this algorithm is offline, which means it's not suited for flows changing over time, it's relatively simple, and if you're not too constrained by performance requirements, it may work - just run the algo again after adjusting the capacities. For online max flow, you can look at this,

Working out my idea of fractional simulation in C++ (sorry). Please see heavily commented code below.

(I know the prioritization in the face of constrained resources isn't what you want. It's not trivial to get a fair implementation of Derivative that produces as much as it can, so I wanted to validate this approach before going down the rabbit hole.)

#include <cassert>
#include <iostream>
#include <limits>
#include <utility>
#include <vector>

// Identifies a type of good in some Factory.
using Good = int;

// Simulates a factory. The simulation is crude, assuming continuous,
// fractional, zero-latency production. Nevertheless it may be accurate enough
// for high production volumes over long periods of time.
class Factory {
public:
  // Declares a new raw material. `delivery_rate` is the amount of this good
  // delivered per hour.
  Good NewRawMaterial(double stock, double delivery_rate) {
    assert(stock >= 0.0);
    assert(delivery_rate >= 0.0);
    return NewGood(stock, delivery_rate, {});
  }

  // Declares a new manufactured good. `max_production_rate` is the max amount
  // of this good produced per hour. Producing one of this good consumes one
  // `input`.
  Good NewManufacturedGood(double stock, double max_production_rate,
                           Good input) {
    assert(stock >= 0.0);
    assert(max_production_rate >= 0.0);
    return NewGood(stock, max_production_rate, {input});
  }

  // Declares a new manufactured good. `max_production_rate` is the max amount
  // of this good produced per hour. Producing one of this good consumes one
  // `input_a` and one `input_b`.
  Good NewManufacturedGood(double stock, double max_production_rate,
                           Good input_a, Good input_b) {
    assert(stock >= 0.0);
    assert(max_production_rate >= 0.0);
    return NewGood(stock, max_production_rate, {input_a, input_b});
  }

  // Returns the number of hours since the start of the simulation.
  double Now() const { return current_time_; }

  // Advances the current time to `time` hours since the start of the
  // simulation.
  void AdvanceTo(double time);

  // Returns the amount of `good` in stock as of the current time.
  double Stock(Good good) const { return stock_[good]; }

  // Sets the delivery rate of `good` to `delivery_rate` as of the current time.
  void SetDeliveryRate(Good good, double delivery_rate) {
    assert(delivery_rate >= 0.0);
    max_production_rate_[good] = delivery_rate;
  }

  // Sets the max production rate of `good` to `max_production_rate` as of the
  // current time.
  void SetMaxProductionRate(Good good, double max_production_rate) {
    assert(max_production_rate >= 0.0);
    max_production_rate_[good] = max_production_rate;
  }

private:
  // Floating-point tolerance.
  static constexpr double kEpsilon = 1e-06;

  // Declares a new good. We handle raw materials as goods with no inputs.
  Good NewGood(double stock, double max_production_rate,
               std::vector<Good> inputs) {
    assert(stock >= 0.0);
    assert(max_production_rate >= 0.0);
    Good good = stock_.size();
    stock_.push_back(stock);
    max_production_rate_.push_back(max_production_rate);
    inputs_.push_back(std::move(inputs));
    return good;
  }

  // Returns the right-hand derivative of stock.
  std::vector<double> Derivative() const;

  // Returns the next time at which a good is newly out of stock, or positive
  // infinity if there is no such time.
  double NextStockOutTime(const std::vector<double> &derivative) const;

  // The current time, in hours since the start of the simulation.
  double current_time_ = 0.0;

  // `stock_[good]` is the amount of `good` in stock at the current time.
  std::vector<double> stock_;

  // `max_production_rate_[good]` is the max production rate of `good` at the
  // current time.
  std::vector<double> max_production_rate_;

  // `inputs_[good]` is the list of goods required to produce `good` (empty for
  // raw materials).
  std::vector<std::vector<Good>> inputs_;

  // Derivative of `stock_`.
  std::vector<double> stock_rate_;
};

void Factory::AdvanceTo(double time) {
  assert(time >= current_time_);
  bool caught_up = false;
  while (!caught_up) {
    auto derivative = Derivative();
    double next_time = NextStockOutTime(derivative);
    if (time <= next_time) {
      next_time = time;
      caught_up = true;
    }
    for (Good good = 0; good < stock_.size(); good++) {
      stock_[good] += (next_time - current_time_) * derivative[good];
    }
    current_time_ = next_time;
  }
}

std::vector<double> Factory::Derivative() const {
  // TODO: this code prioritizes limited supply by the order in which production
  // is declared. You probably want to use linear programming or something.
  std::vector<double> derivative = max_production_rate_;
  for (Good good = 0; good < stock_.size(); good++) {
    for (Good input : inputs_[good]) {
      if (stock_[input] <= kEpsilon) {
        derivative[good] = std::min(derivative[good], derivative[input]);
      }
    }
    for (Good input : inputs_[good]) {
      derivative[input] -= derivative[good];
    }
  }
  return derivative;
}

double Factory::NextStockOutTime(const std::vector<double> &derivative) const {
  double duration = std::numeric_limits<double>::infinity();
  for (Good good = 0; good < stock_.size(); good++) {
    if (stock_[good] > kEpsilon && derivative[good] < -kEpsilon) {
      duration = std::min(duration, stock_[good] / -derivative[good]);
    }
  }
  return current_time_ + duration;
}

int main() {
  Factory factory;
  Good r1 = factory.NewRawMaterial(60.0, 3.0);
  Good r2 = factory.NewRawMaterial(20.0, 1.0);
  Good r3 = factory.NewManufacturedGood(0.0, 2.0, r1);
  Good r4 = factory.NewManufacturedGood(0.0, 1.0, r1, r2);
  auto print_stocks = [&]() {
    std::cout << "t : " << factory.Now() << "\n";
    std::cout << "r1: " << factory.Stock(r1) << "\n";
    std::cout << "r2: " << factory.Stock(r2) << "\n";
    std::cout << "r3: " << factory.Stock(r3) << "\n";
    std::cout << "r4: " << factory.Stock(r4) << "\n";
    std::cout << "\n";
  };
  print_stocks();
  // Everything running smoothly
  factory.AdvanceTo(24.0);
  print_stocks();
  // Uh oh, r1 supply cut off. Stock out at 44 hours.
  factory.SetDeliveryRate(r1, 0.0);
  factory.AdvanceTo(48.0);
  print_stocks();
  // r1 supply at 50%. r3 production prioritized.
  factory.SetDeliveryRate(r1, 1.5);
  factory.AdvanceTo(72.0);
  print_stocks();
  // r1 oversupplied.
  factory.SetDeliveryRate(r1, 4.0);
  factory.AdvanceTo(96.0);
  print_stocks();
}

Output:

t : 0
r1: 60
r2: 20
r3: 0
r4: 0

t : 24
r1: 60
r2: 20
r3: 48
r4: 24

t : 48
r1: 0
r2: 24
r3: 88
r4: 44

t : 72
r1: 0
r2: 48
r3: 124
r4: 44

t : 96
r1: 24
r2: 48
r3: 172
r4: 68
Related