Product assembly / disassembly optimization

I have a store containing items. Each element is either a component (which is atomic) or a product consisting of various components (but never from two or more of the same components).

Now that I want to get the product from the store, there are various scenarios:

  • The repository contains the required product number.
  • The repository contains components from which I can assemble the product.
  • The repository contains products that share components with the desired product. I can disassemble them and collect the necessary element.
  • Any combination of the above.

Below you can see my code so far ( getAssemblyPath ). It finds a way to assemble the required item, if possible, but it does not optimize the assembly path.

I want to optimize the path in two ways:

  • First, select the path that takes the least number of build / disassemble actions.
  • Secondly, if there are different paths, select the path that leaves the least amount of disassembled components in the repository.

Now I completely lose how to do this optimization (I'm not even sure if this is a question for SO or for Maths).

How to change getAssemblyPath to fit my optimization requirements?

My code is:

 #! /usr/bin/python class Component: def __init__ (self, name): self.__name = name def __repr__ (self): return 'Component {}'.format (self.__name) class Product: def __init__ (self, name, components): self.__name = name self.__components = components @property def components (self): return self.__components def __repr__ (self): return 'Product {}'.format (self.__name) class Store: def __init__ (self): self.__items = {} def __iadd__ (self, item): item, count = item if not item in self.__items: self.__items [item] = 0 self.__items [item] += count return self @property def items (self): return (item for item in self.__items.items () ) @property def products (self): return ( (item, count) for item, count in self.__items.items () if isinstance (item, Product) ) @property def components (self): return ( (item, count) for item, count in self.__items.items () if isinstance (item, Component) ) def getAssemblyPath (self, product, count): if product in self.__items: take = min (count, self.__items [product] ) print ('Take {} of {}'.format (take, product) ) count -= take if not count: return components = dict ( (comp, count) for comp in product.components) for comp, count in self.components: if comp not in components: continue take = min (count, components [comp] ) print ('Take {} of {}'.format (take, comp) ) components [comp] -= take if not components [comp]: del components [comp] if not components: return for prod, count in self.products: if prod == product: continue shared = set (prod.components) & set (components.keys () ) dis = min (max (components [comp] for comp in shared), count) print ('Disassemble {} of {}.'.format (dis, prod) ) for comp in shared: print ('Take {} of {}.'.format (dis, comp) ) components [comp] -= take if not components [comp]: del components [comp] if not components: return print ('Missing components:') for comp, count in components.items (): print ('{} of {}.'.format (count, comp) ) c1 = Component ('alpha') c2 = Component ('bravo') c3 = Component ('charlie') c4 = Component ('delta') p1 = Product ('A', [c1, c2] ) p2 = Product ('B', [c1, c2, c3] ) p3 = Product ('C', [c1, c3, c4] ) store = Store () store += (c2, 100) store += (c4, 100) store += (p1, 100) store += (p2, 100) store += (p3, 10) store.getAssemblyPath (p3, 20) 

It is output:

 Take 10 of Product C Take 10 of Component delta Disassemble 10 of Product A. Take 10 of Component alpha. Disassemble 10 of Product B. Take 10 of Component charlie. 

Which works, but it unnecessarily parses product A, since product B contains both the necessary alpha and charlie components.

-

EDIT:

Answering the most reasonable questions of Blckknght:

When you say you want the "least number of assembly / disassembly actions", do you mean the least number of elements or the least number of different products?

An "asm / disasm action" is the action of assembling or disassembling a single product, regardless of how many components are involved. I am looking for the least number of subjects affected, whether they are clear or not.

That is, it is better to parse 20 product A than to parse 10 product A and another 5 product B?

The latter is closer to optimal.

In addition, you say that you want to avoid leaving many components behind, but in your current code all parsed components that are not used by the requested Product are lost. Is this intentional (i.e. do you want to discard other components), or is it a mistake?

The getAssemblyPath method determines only the path to get items. This does not apply to a real store. In no case does it assign self.__items . Think of it as a function that sets an order for a store about what it should do in the future (in the interim) in order to get the required quantity of the required item from its store.

-

EDIT 2:

The first obvious (or at least obvious to me) way to solve this problem is to find the first of these products that share the maximum number of components with the desired product, since you get the more necessary components from each disassembly. But, unfortunately, this does not necessarily provide the optimal path. Take for example:

Product A, consisting of components α, β, γ, δ, ε and ζ.

Product B, consisting of components α, β, η, δ, ε and θ.

Product C, consisting of components α, β, γ, ι, κ and λ.

Product D, consisting of the components μ, ν, ξ, δ, ε, and ζ.

We have 0 from A, 100 from B, 100 C and 100 D. We require 10 from A. Now, if we first look at the products that share most of the components with A, we will find B. We take 10 out of B, getting 10 each of α, β, δ and ε. But then we need to parse 10 from C (get γ) and 10 from D (get z). It will be 40 actions (30 disassemblies and 10 assemblies). But the best way would be to parse 10 C and 10 D (30 actions, 20 disassemblies and 10 assemblies).

-

EDIT 3:

You do not need to publish python code to win the award. Just explain the algorithm to me and show that it really gives the optimal path or one of the optimal ones if there are several.

+8
optimization python
source share
3 answers

This is how I would solve this problem. I wanted to write code for this, but I don’t think I have the time.

Recursively, you can find the best solution. Create a data structure that represents the state of the part store and the current query. Now, for each piece you need, make a series of recursive calls that try different ways to fill out the order. The key is that when you try to fill out an order, you get some of the work done, so the recursive call is now a slightly simpler version of the same problem.

Here is an example based on your example. We need to fill out orders for product 3 (p3), which consists of components c1, c3 and c4. Our order is for 20 p3, and we have 10 p3 in stock, so we trivially fill the order for the first 10 p3. Now our order is 10 of p3, but we can consider it as the order of 10 of c1, 10 of c3 and 10 c4. For the first recursive call, we parse p1 and fill in the order for one c1 and add additional c2 to the storage; therefore this recursive call is for 9 of c1, 10 of c3 and 10 c4, with updated store availability. For the second recursive call, we will parse p2 and fill out the order for c1 and c4 and add additional c2 to the repository; therefore, this recursive call is for 9 of c1, 10 of c3, and 9 c4 with updated storage availability.

As each call reduces the problem, the recursive series of calls is terminated. Recursive calls should return a cost metric that either signals that the call could not find a solution, or signals how much the solution found is worth; the function selects the best solution, choosing the solution with the lowest cost.

I'm not sure, but you could speed it up by notifying you of calls. Python has a really great built-in version in the 3.x series, functools.lru_cache() ; since you marked your question as "Python 3.2", this is available to you.

What is memoization and how can I use it in Python?

Recollection works by recognizing that the function is already being called with the same arguments and simply returning the same solution as before. Thus, these are arguments for matching the cache. If the arguments include non-essential data (for example, how many c2 components are in storage), then unmounting is less likely to work. But if we assume that we have products p1 and p9, and p9 contains components c1 and c9, then for our purposes, disassembling one of p1 or one of p9 should be equivalent: they have the same disassembly cost, and both of them produce what we need component (c1) and one that we do not need (c2 or c9). Therefore, if we correctly receive the arguments of the recursive call, memoization may simply return an instant response when we move on to p9, and this can save a lot of time.

Hmm, now that I think about it, we probably can't use functools.lru_cache() , but we can just memoize ourselves. We can create a solution cache: matching a dictionary with values ​​and building tuples in which there are only those arguments that we want to cache. Then in our function, the first thing we do is check the cache solutions, and if this call is equivalent to a cached solution, just return it.

EDIT: Here is the code that I have written so far. I didn’t finish debugging it so that it probably wouldn’t give the right answer yet (I’m not sure because it takes a lot of time and I didn’t let it finish the job). This version goes through dictionaries that will not work well with my memoizing ideas, but I would like to get a simple version and then worry about speeding it up.

In addition, this code combines the products and adds them to the repository as components, so the final solution will first say something like “Separate 10 products A” and then say “Take 20 component alpha” or something else. In other words, the component count can be considered high, because it does not distinguish between the components that were already in the store and the components that were placed there by disassembling the products.

Now I do not have time and I will not work on it for a while, sorry.

 #!/usr/bin/python3 class Component: def __init__ (self, name): self.__name = name #def __repr__ (self): return 'Component {}'.format (self.__name) def __repr__ (self): return 'C_{}'.format (self.__name) class Product: def __init__ (self, name, components): self.__name = name self.__components = components @property def components (self): return self.__components #def __repr__ (self): return 'Product {}'.format (self.__name) def __repr__ (self): return 'P_{}'.format (self.__name) class Store: def __init__ (self): self.__items = {} def __iadd__ (self, item): item, count = item if not item in self.__items: self.__items [item] = 0 self.__items [item] += count return self @property def items (self): return (item for item in self.__items.items () ) @property def products (self): return ( (item, count) for item, count in self.__items.items () if isinstance (item, Product) ) @property def components (self): return ( (item, count) for item, count in self.__items.items () if isinstance (item, Component) ) def get_assembly_path (self, product, count): store = self.__items.copy() if product in store: take = min (count, store [product] ) s_trivial = ('Take {} of {}'.format (take, product) ) count -= take if not count: print(s_trivial) return dict_decr(store, product, take) product not in store order = {item:count for item in product.components} cost, solution = solver(order, store) if cost is None: print("No solution.") return print("Solution:") print(s_trivial) for item, count in solution.items(): if isinstance(item, Component): print ('Take {} of {}'.format (count, item) ) else: assert isinstance(item, Product) print ('Disassemble {} of {}'.format (count, item) ) def getAssemblyPath (self, product, count): if product in self.__items: take = min (count, self.__items [product] ) print ('Take {} of {}'.format (take, product) ) count -= take if not count: return components = dict ( (comp, count) for comp in product.components) for comp, count in self.components: if comp not in components: continue take = min (count, components [comp] ) print ('Take {} of {}'.format (take, comp) ) components [comp] -= take if not components [comp]: del components [comp] if not components: return for prod, count in self.products: if prod == product: continue shared = set (prod.components) & set (components.keys () ) dis = min (max (components [comp] for comp in shared), count) print ('Disassemble {} of {}.'.format (dis, prod) ) for comp in shared: print ('Take {} of {}.'.format (dis, comp) ) components [comp] -= take if not components [comp]: del components [comp] if not components: return print ('Missing components:') for comp, count in components.items (): print ('{} of {}.'.format (count, comp) ) def str_d(d): lst = list(d.items()) lst.sort(key=str) return "{" + ", ".join("{}:{}".format(k, v) for (k, v) in lst) + "}" def dict_incr(d, key, n): if key not in d: d[key] = n else: d[key] += n def dict_decr(d, key, n): assert d[key] >= n d[key] -= n if d[key] == 0: del(d[key]) def solver(order, store): """ order is a dict mapping component:count store is a dict mapping item:count returns a tuple: (cost, solution) cost is a cost metric estimating the expense of the solution solution is a dict that maps item:count (how to fill the order) """ print("DEBUG: solver: {} {}".format(str_d(order), str_d(store))) if not order: solution = {} cost = 0 return (cost, solution) solutions = [] for item in store: if not isinstance(item, Component): continue print("...considering: {}".format(item)) if not item in order: continue else: o = order.copy() s = store.copy() dict_decr(o, item, 1) dict_decr(s, item, 1) if not o: # we have found a solution! Return it solution = {} solution[item] = 1 cost = 1 print("BASIS: solver: {} {} / {} {}".format(str_d(order), str_d(store), cost, str_d(solution))) return (cost, solution) else: cost, solution = solver(o, s) if cost is None: continue # this was a dead end dict_incr(solution, item, 1) cost += 1 solutions.append((cost, solution)) for item in store: if not isinstance(item, Product): continue print("...Product components: {} {}".format(item, item.components)) assert isinstance(item, Product) if any(c in order for c in item.components): print("...disassembling: {}".format(item)) o = order.copy() s = store.copy() dict_decr(s, item, 1) for c in item.components: dict_incr(s, c, 1) cost, solution = solver(o, s) if cost is None: continue # this was a dead end cost += 1 # cost of disassembly solutions.append((cost, solution)) else: print("DEBUG: ignoring {}".format(item)) if not solutions: print("DEBUG: *dead end*") return (None, None) print("DEBUG: finding min of: {}".format(solutions)) return min(solutions) c1 = Component ('alpha') c2 = Component ('bravo') c3 = Component ('charlie') c4 = Component ('delta') p1 = Product ('A', [c1, c2] ) p2 = Product ('B', [c1, c2, c3] ) p3 = Product ('C', [c1, c3, c4] ) store = Store () store += (c2, 100) store += (c4, 100) store += (p1, 100) store += (p2, 100) store += (p3, 10) #store.getAssemblyPath (p3, 20) store.get_assembly_path(p3, 20) 
+3
source share
  • Optimal path for N products <=> optimal path for one product.

Indeed, if we need to optimally collect N products of X, after we optimally (using the current stock) collect one product, the question will be how to optimally assemble (N-1) product X using the remaining stock.

=> Therefore, it is enough to provide an optimal assembly algorithm for ONE product X at a time.

  • Suppose we need components x1, .. xn for the product (here we include only components that are not available as components in stock)

For each xk component, find all products that have this component. We get a list of products for each component - products A1 (1), .., A1 (i1) have component x1, products A (1), .., A (i2) have component x2, etc. (Some products may appear on multiple lists of lists A1, A2, ..., An).

If any of the lists is empty, there is no solution.

We need a minimal set of products, so a product from this set is contained in each of the lists. The simplest, but not computationally effective solution is brute force - try all the sets and choose the minimum:

  • Take the union A1, ..., An - call it A (include only unique products in the union).

but. Take one product from A if it is contained in all A1, .., An - we only need one disassembly (this product). b. Try all combinations of two products from A if any combination (a1, a2) satisfies the condition that either a1 or a2 is contained in each of the lists A1, ..., An is a solution.

...

there is probably a solution at depth n - one component from each of the lists A1, ..., An. If we have not found a solution before, this is the best solution.

Now we only need to think about a better strategy, and then check the brute force, which, I think, is possible - I need to think about it, but this brute force approach will surely find a strictly optimal solution.


EDIT:

A more accurate solution is to sort the lists by length. Then, checking the set of products K for the solution - you only need to check all possible combinations of 1 element from each list from the first lists of K, if there is no solution - there is no minimum set of depths K that solves the problem. This type of check will also be calculated not so bad - maybe it can work ????

+1
source share

I think that the key point here is to determine the potential costs for each purchase case, so that the right combination of procurement optimally minimizes the cost function. (Then it just comes down to the backpack problem)

What follows is probably not optimal, but here is an example of what I mean:

1. Any product that is a final product "spends" its actual value (in currency).

2. Any component or product that can be assembled into the final product (taking into account other individual products / components), but does not require that its exposure cost it a real price (in currency) plus a small tax (tbd).

3. Any component or product that can facilitate the assembly of the final product, but requires exposure, costs its price in foreign currency plus a small assembly tax for the final product and another small tax for each assembly required. (perhaps the same value as the collective tax?).

Note: these "taxes" will apply to all by-products that occupy the same case.

... etc. for other possible cases

Then find all possible combinations of components and products available on the storefront that can be assembled into the final product. Put these “assembly lists” in the list with the cost list defined by your cost function. After that, start creating as many of the first (cheapest) “list of assemblies” as possible (by checking to see if all the assemblies in the list are still available in the store, that is, you have already used them for the previous assembly). Once you can’t create more of this case, drag it out of the list. Repeat until all finished products are created.

Note. Each time you “assemble” the final product, you need to decrease the global counter for each product in the current “build list”.

I hope this helps the discussions move in the right direction. Good luck

0
source share

All Articles