4

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).

Jelumar
  • 470
  • 1
  • 4
  • 14
  • 2
    Just shooting in the dark without reading the full post. May be something to do with topological sort and/or network flow algorithms. If you could make the post as concise as possible, that would really help (to you as well as the community). – Shridhar R Kulkarni Jan 30 '21 at 12:00
  • I agree, very few people will be reading such long walls of text, albeit it is well written. – GhostCat Jan 30 '21 at 12:01
  • Thx, I will have a look at the mentioned algorithms. Yes it is long and the second half is only describing what I have done to try to solve the problem, so it is not that important. I will add it as information. – Jelumar Jan 30 '21 at 12:11
  • 1
    I'm not sure there's a well-defined answer. E.g., say R8 can't produce enough to meet the needs of R9 and R13. How do we decide to allocate R8's production to R9 and R13? – Dave Jan 30 '21 at 14:55
  • 1
    What Dave asked, but also, would a continuous approximation be of interest? That might simplify the problem. – David Eisenstat Jan 30 '21 at 15:01
  • 1
    This is a research paper on the topic. It may be overkill depending on whether this is a toy or real problem, but it will at least have some relevant terminology. https://www.researchgate.net/publication/316174233_Improving_configuration_of_complex_production_lines_via_simulation-based_optimization/link/5a05dc04a6fdcc65eab14415/download – Dave Jan 30 '21 at 15:13
  • @DavidEisenstat I wonder if an augmenting path approach would work, giving a profit function to all the sink nodes are redefining 'augmenting path' as the set of flows that would increment sink outflow by 1. – Dave Jan 30 '21 at 15:18
  • I agree with @Dave: Without knowing how the R8s will be allocated, we cannot determine how many R9s and R13s will be produced. – Scott - Слава Україні Jan 30 '21 at 15:53
  • @Dave having multiple inputs to a single factory disrupts the theory around augmenting paths, but integer programming would still work. Passing to the linear relaxation would make dealing with the time component easier. – David Eisenstat Jan 30 '21 at 15:57
  • There are two options (when there is not enough of R8) I would see as equaly good for this case, one that half is used for creating R9 and half for R13 and the other, that they both get a quantity determined so that R9 and R13 get produced to the same percentage of the maximum capacity. But yes, that part of the Problem I have not even tried to implement at the moment. – Jelumar Jan 30 '21 at 17:17
  • @Dave I will have a look at the paper. At the moment it is only a theoretical problem, but it could be that it will be a real problem I need to solve at some point. And yes, seeing some of the things written here I see, that I seem to lack some terminally that could be relevant for the Problem. – Jelumar Jan 30 '21 at 17:20
  • @DavidEisenstat the exact amount would be better. In this case the approximation is needed to be so good, that only an error of one digit over a time of some months can be tolerated (this would be an error margin of around 0,05%, if that can be done while simplifying the problem enough it would be great). – Jelumar Jan 30 '21 at 17:51

2 Answers2

2

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,

Andrew Vershinin
  • 1,958
  • 11
  • 16
  • Thanks for your answer, I will have a look at the linked paper in the next days. and look at the Edmonds-Karp algorithm too. – Jelumar Jan 30 '21 at 17:32
  • I don't see yet how the stocks can be taken into account. But ignoring the stock, this looks like a very good way to solve the Problem. – Jelumar Feb 01 '21 at 13:03
1

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
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • Just by reading the code one time it looks to me like this would solve my problem. Thanks a lot, I need a little more time to get through what happens in the Derivate function, but this looks promising! – Jelumar Jan 31 '21 at 17:24
  • @Jelumar oh, Derivative is mostly not what you want. The main takeaway is that only inputs for which there is no stock limit the production, to the extent that they are being produced/delivered. The rest of code is doing this really bad first-come first-serve thing. – David Eisenstat Jan 31 '21 at 17:36
  • I think thins solves my problem, I only need to change the calculation Derivative (and eventually AdvanceTo) so that I can indicate a percentage for each (that determines how much is given to each when there is not enough for each, there I should add a needed_ array to be able to know it) The percentage would be depending on the percentage a resulting goods maximum use from the total maximum use of a source good. But thanks for that example, it is a great starting point! – Jelumar Feb 01 '21 at 10:35
  • With some changes I solved it. Seams for the moment as if I need to consider the order I define Goods, but with some more work I might get that solved too. Thanks a lot for this starting point! – Jelumar Feb 03 '21 at 13:14
  • 1
    @Jelumar you can do a topological sort on the dependency graph. – David Eisenstat Feb 03 '21 at 20:29