gerbmerge/gerbmerge/tilesearch1.py

244 lines
8.3 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 = 0L # Number of placements attempted
_PossiblePermutations = 0L # Number of different ways of ordering jobs
_Permutations = 0L # Number of different job orderings already computed
_TBestTiling = None # Best tiling so far
_TBestScore = float(sys.maxint) # 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
print "\r %5.2f%% complete / %ld/%ld Perm/Place / Smallest area: %.1f sq. in. / Best utilization: %.1f%%" % \
(percent, _Permutations, _Placements, area, utilization),
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.maxint))
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, ", ",
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 += 2L**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 += 2L**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
if (config.SearchTimeout > 0) and (time.time() - _StartTime > config.SearchTimeout):
raise KeyboardInterrupt
gerbmerge.updateGUI("Performing automatic layout...")
# end for each job in job list
def factorial(N):
if (N <= 1): return 1L
prod = long(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 = 0L
_Permutations = 0L
_TBestTiling = None
_TBestScore = float(sys.maxint)
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 = (2L**len(Jobs))*factorial(len(Jobs))
#print "Possible permutations:", _PossiblePermutations
print '='*70
print "Starting placement using exhaustive search."
print "There are %ld possible permutations..." % _PossiblePermutations,
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