gerbmerge/gerbmerge/tilesearch1.py

251 lines
8.7 KiB
Python

#!/usr/bin/env python
"""Search for an optimum tiling using brute force exhaustive search
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import time
import config
import tiling
import gerbmerge
_StartTime = 0.0 # Start time of tiling
_CkpointTime = 0.0 # Next time to print stats
_Placements = 0 # Number of placements attempted
_PossiblePermutations = 0 # Number of different ways of ordering jobs
_Permutations = 0 # Number of different job orderings already computed
_TBestTiling = None # Best tiling so far
_TBestScore = float(sys.maxsize) # Smallest area so far
_PrintStats = 1 # Print statistics every 3 seconds
def printTilingStats():
global _CkpointTime
_CkpointTime = time.time() + 3
if _TBestTiling:
area = _TBestTiling.area()
utilization = _TBestTiling.usedArea() / area * 100.0
else:
area = 999999.0
utilization = 0.0
percent = 100.0*_Permutations/_PossiblePermutations
# add metric support (1/1000 mm vs. 1/100,000 inch)
if config.Config['measurementunits'] == 'inch':
print("\r %5.2f%% complete / %ld/%ld Perm/Place / Smallest area: %.1f sq. in. / Best utilization: %.1f%%" % \
(percent, _Permutations, _Placements, area, utilization), end=' ')
else:
print("\r %5.2f%% complete / %ld/%ld Perm/Place / Smallest area: %.1f sq. mm / Best utilization: %.1f%%" % \
(percent, _Permutations, _Placements, area, utilization), end=' ')
if gerbmerge.GUI is not None:
sys.stdout.flush()
def bestTiling():
return _TBestTiling
def _tile_search1(Jobs, TSoFar, firstAddPoint, cfg=config.Config):
"""This recursive function does the following with an existing tiling TSoFar:
* For each 4-tuple (Xdim,Ydim,job,rjob) in Jobs, the non-rotated 'job' is selected
* For the non-rotated job, the list of valid add-points is found
* For each valid add-point, the job is placed at this point in a new,
cloned tiling.
* The function then calls its recursively with the remaining list of
jobs.
* The rotated job is then selected and the list of valid add-points is
found. Again, for each valid add-point the job is placed there in
a new, cloned tiling.
* Once again, the function calls itself recursively with the remaining
list of jobs.
* The best tiling encountered from all recursive calls is returned.
If TSoFar is None it means this combination of jobs is not tileable.
The side-effect of this function is to set _TBestTiling and _TBestScore
to the best tiling encountered so far. _TBestTiling could be None if
no valid tilings have been found so far.
"""
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PrintStats
if not TSoFar:
return (None, float(sys.maxsize))
if not Jobs:
# Update the best tiling and score. If the new tiling matches
# the best score so far, compare on number of corners, trying to
# minimize them.
score = TSoFar.area()
if score < _TBestScore:
_TBestTiling,_TBestScore = TSoFar,score
elif score == _TBestScore:
if TSoFar.corners() < _TBestTiling.corners():
_TBestTiling,_TBestScore = TSoFar,score
_Placements += 1
if firstAddPoint:
_Permutations += 1
return
xspacing = cfg['xspacing']
yspacing = cfg['yspacing']
minInletSize = tiling.minDimension(Jobs)
TSoFar.removeInlets(minInletSize)
for job_ix in range(len(Jobs)):
# Pop off the next job and construct remaining_jobs, a sub-list
# of Jobs with the job we've just popped off excluded.
Xdim,Ydim,job,rjob = Jobs[job_ix]
remaining_jobs = Jobs[:job_ix]+Jobs[job_ix+1:]
if 0:
print("Level %d (%s)" % (level, job.name))
TSoFar.joblist()
for J in remaining_jobs:
print(J[2].name, ", ", end=' ')
print()
print('-'*75)
# Construct add-points for the non-rotated and rotated job.
# As an optimization, do not construct add-points for the rotated
# job if the job is a square (duh).
addpoints1 = TSoFar.validAddPoints(Xdim+xspacing,Ydim+yspacing) # unrotated job
if Xdim != Ydim:
addpoints2 = TSoFar.validAddPoints(Ydim+xspacing,Xdim+yspacing) # rotated job
else:
addpoints2 = []
# Recursively construct tilings for the non-rotated job and
# update the best-tiling-so-far as we do so.
if addpoints1:
for ix in addpoints1:
# Clone the tiling we're starting with and add the job at this
# add-point.
T = TSoFar.clone()
T.addJob(ix, Xdim+xspacing, Ydim+yspacing, job)
# Recursive call with the remaining jobs and this new tiling. The
# point behind the last parameter is simply so that _Permutations is
# only updated once for each permutation, not once per add-point.
# A permutation is some ordering of jobs (N! choices) and some
# ordering of non-rotated and rotated within that ordering (2**N
# possibilities per ordering).
_tile_search1(remaining_jobs, T, firstAddPoint and ix==addpoints1[0])
elif firstAddPoint:
# Premature prune due to not being able to put this job anywhere. We
# have pruned off 2^M permutations where M is the length of the remaining
# jobs.
_Permutations += 2**len(remaining_jobs)
if addpoints2:
for ix in addpoints2:
# Clone the tiling we're starting with and add the job at this
# add-point. Remember that the job is rotated so swap X and Y
# dimensions.
T = TSoFar.clone()
T.addJob(ix, Ydim+xspacing, Xdim+yspacing, rjob)
# Recursive call with the remaining jobs and this new tiling.
_tile_search1(remaining_jobs, T, firstAddPoint and ix==addpoints2[0])
elif firstAddPoint:
# Premature prune due to not being able to put this job anywhere. We
# have pruned off 2^M permutations where M is the length of the remaining
# jobs.
_Permutations += 2**len(remaining_jobs)
# If we've been at this for 3 seconds, print some status information
if _PrintStats and time.time() > _CkpointTime:
printTilingStats()
# Check for timeout - changed to file config
if (config.Config['searchtimeout'] > 0) and ((time.time() - _StartTime) > config.Config['searchtimeout']):
raise KeyboardInterrupt
gerbmerge.updateGUI("Performing automatic layout...")
# end for each job in job list
def factorial(N):
if (N <= 1): return 1
prod = int(N)
while (N > 2):
N -= 1
prod *= N
return prod
def initialize(printStats=1):
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PossiblePermutations, _PrintStats
_PrintStats = printStats
_Placements = 0
_Permutations = 0
_TBestTiling = None
_TBestScore = float(sys.maxsize)
def tile_search1(Jobs, X, Y):
"""Wrapper around _tile_search1 to handle keyboard interrupt, etc."""
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PossiblePermutations
initialize()
_StartTime = time.time()
_CkpointTime = _StartTime + 3
# There are (2**N)*(N!) possible permutations where N is the number of jobs.
# This is assuming all jobs are unique and each job has a rotation (i.e., is not
# square). Practically, these assumptions make no difference because the software
# currently doesn't optimize for cases of repeated jobs.
_PossiblePermutations = (2**len(Jobs))*factorial(len(Jobs))
#print "Possible permutations:", _PossiblePermutations
print('='*70)
print("Starting placement using exhaustive search.")
print("There are %ld possible permutations..." % _PossiblePermutations, end=' ')
if _PossiblePermutations < 1e4:
print("this'll take no time at all.")
elif _PossiblePermutations < 1e5:
print("surf the web for a few minutes.")
elif _PossiblePermutations < 1e6:
print("take a long lunch.")
elif _PossiblePermutations < 1e7:
print("come back tomorrow.")
else:
print("don't hold your breath.")
print("Press Ctrl-C to stop and use the best placement so far.")
print("Estimated maximum possible utilization is %.1f%%." % (tiling.maxUtilization(Jobs)*100))
try:
_tile_search1(Jobs, tiling.Tiling(X,Y), 1)
printTilingStats()
print()
except KeyboardInterrupt:
printTilingStats()
print()
print("Interrupted.")
computeTime = time.time() - _StartTime
print("Computed %ld placements in %d seconds / %.1f placements/second" % (_Placements, computeTime, _Placements/computeTime))
print('='*70)
return _TBestTiling