def main(): description = ('Ingress FieldPlan - Maximize the number of links ' 'and fields, and thus AP, for a collection of ' 'portals in the game Ingress and create a convenient plan ' 'in Google Spreadsheets. Spin-off from Maxfield.') parser = argparse.ArgumentParser( description=description, prog='makePlan.py', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument( '-i', '--iterations', type=int, default=5000, help='Number of iterations to perform. More iterations may improve ' 'results, but will take longer to process.') parser.add_argument( '-m', '--travelmode', default='walking', help='Travel mode (walking, bicycling, driving, transit).') parser.add_argument( '-s', '--sheetid', default=None, required=True, help='The Google Spreadsheet ID with portal definitions.') parser.add_argument( '-n', '--nosave', action='store_true', default=False, help='Do not attempt to save the spreadsheet, just calculate the plan.' ) parser.add_argument( '-p', '--plots', default=None, help='Save step-by-step PNGs of the workplan into this directory.') parser.add_argument( '--plotdpi', default=96, type=int, help='DPI to use for generating plots (try 144 for high-dpi screens)') parser.add_argument('-g', '--gmapskey', default=None, help='Google Maps API key (for better distances)') parser.add_argument('-f', '--faction', default='enl', help='Set to "res" to use resistance colours') parser.add_argument( '-c', '--cooling', default='rhs', help='What kind of heatsinks to assume (hs, rhs, vrhs, none, idkfa)') parser.add_argument( '-u', '--maxmu', action='store_true', default=False, help='Find a plan with highest MU coverage instead of best AP') parser.add_argument( '-t', '--maxtime', default=None, type=int, help='Ignore plans that would take longer than this (in minutes)') parser.add_argument( '--minap', default=None, type=int, help= 'Ignore plans that result in less AP than specified (used with --maxtime)' ) parser.add_argument('--maxcpus', default=mp.cpu_count(), type=int, help='Maximum number of cpus to use') parser.add_argument('-l', '--log', default=None, help='Log file where to log processing info') parser.add_argument('-d', '--debug', action='store_true', default=False, help='Add debug information to the logfile') parser.add_argument('-q', '--quiet', action='store_true', default=False, help='Only output errors to the stdout') parser.add_argument('--no-plan-cache', dest='nocache', action='store_true', default=False, help='Do not load or save plan cache') parser.add_argument('-j', '--jsonmap', default=None, help='Save the resulting map as IITC DrawTools json') # Obsolete options parser.add_argument('-b', '--beginfirst', action='store_true', default=False, help='(Obsolete, use waypoints instead)') parser.add_argument('-r', '--roundtrip', action='store_true', default=False, help='(Obsolete, use waypoints instead)') parser.add_argument('-k', '--maxkeys', type=int, default=None, help='(Obsolete, use --cooling options instead)') args = parser.parse_args() if args.beginfirst or args.roundtrip: parser.error( 'Options -b and -r are obsolete. Use waypoints instead (see README).' ) if args.maxkeys: parser.error('Option -k is obsolete. Use --cooling instead.') if args.iterations < 0: parser.error('Number of extra samples should be positive') if args.plotdpi < 1: parser.error('%s is not a valid screen dpi' % args.plotdpi) if args.faction not in ('enl', 'res'): parser.error('Sorry, I do not know about faction "%s".' % args.faction) logger.setLevel(logging.DEBUG) if args.log: ch = logging.FileHandler(args.log) formatter = logging.Formatter( '{%(module)s:%(funcName)s:%(lineno)s} %(message)s') ch.setFormatter(formatter) if args.debug: ch.setLevel(logging.DEBUG) else: ch.setLevel(logging.INFO) logger.addHandler(ch) ch = logging.StreamHandler() formatter = logging.Formatter('%(message)s') ch.setFormatter(formatter) if args.quiet: ch.setLevel(logging.CRITICAL) else: ch.setLevel(logging.INFO) logger.addHandler(ch) gs = gsheets.setup() portals, waypoints = gsheets.get_portals_from_sheet(gs, args.sheetid) logger.info('Considering %d portals and %s waypoints', len(portals), len(waypoints)) if len(portals) < 3: logger.critical('Must have more than 2 portals!') sys.exit(1) maxfield.populate_graphs(portals, waypoints) maxfield.gen_distance_matrix(args.gmapskey, args.travelmode) if args.maxtime or args.nocache: bestgraph = None bestplan = None else: (bestgraph, bestplan) = maxfield.load_cache(args.travelmode, args.maxmu, args.cooling, args.maxtime) if args.maxmu: beststr = 'm2/min' else: beststr = 'AP/min' beststats = None bestkm = '-.--' bestsqkm = '-.--' bestap = '-----' nicetime = '-:--' bestportals = '-' best = 0 if bestgraph is not None: beststats = maxfield.get_workplan_stats(bestplan) if args.maxmu: best = beststats['sqmpmin'] else: best = beststats['appmin'] logger.info('Finding an efficient plan that maximizes %s', beststr) failcount = 0 seenplans = list() s_best = mp.Value('I', best) s_counter = mp.Value('I', 0) push_maxfield_data(args) ready_queue = mp.Queue(maxsize=10) processes = list() for i in range(args.maxcpus): logger.debug('Starting process %s', i) p = mp.Process(target=queue_job, args=(args, s_best, s_counter, ready_queue)) processes.append(p) p.start() logger.info('Started %s worker processes', len(processes)) logger.info('Ctrl-C to exit and use the latest best plan') try: while True: if failcount > 1000: logger.info('Too many consecutive failures, exiting early.') break if beststats is not None: bestdist = beststats['dist'] bestarea = beststats['area'] bestap = beststats['ap'] bestkm = '%0.2f' % (bestdist / float(1000)) bestsqkm = '%0.2f' % (bestarea / float(1000000)) nicetime = beststats['nicetime'] if args.maxmu: best = beststats['sqmpmin'] else: best = beststats['appmin'] bestportals = bestgraph.order() if maxfield.waypoint_graph is not None: bestportals -= maxfield.waypoint_graph.order() if not args.quiet: sys.stdout.write( '\r(Best: %s km, %s km2, %s portals, %s AP, %s %s, %s): %s/%s ' % (bestkm, bestsqkm, bestportals, bestap, best, beststr, nicetime, s_counter.value, args.iterations)) sys.stdout.flush() if s_counter.value >= args.iterations: break try: success, b, workplan, stats = ready_queue.get_nowait() except queue.Empty: time.sleep(0.1) continue if not success: failcount += 1 continue if workplan in seenplans: # Already seen this plan, so don't consider it again. This is done # to avoid pingpoings for best plan. continue if not args.quiet: if bestkm is not None: sys.stdout.write( '\r( %s km, %s km2, %s portals, %s AP, %s %s, %s) \n' % (bestkm, bestsqkm, bestportals, bestap, best, beststr, nicetime)) beststats = stats bestgraph = b bestplan = workplan if args.maxmu: best = stats['sqmpmin'] else: best = stats['appmin'] failcount = 0 except KeyboardInterrupt: if not args.quiet: print() print('Exiting loop') finally: for p in processes: p.terminate() if not args.quiet: print() if bestplan is None: logger.critical('Could not find a solution for this list of portals.') sys.exit(1) maxfield.active_graph = bestgraph if not args.nocache: maxfield.save_cache(bestgraph, bestplan, args.travelmode, args.maxmu, args.cooling, args.maxtime) if args.plots: animate.make_png_steps(bestgraph, bestplan, args.plots, args.faction, args.plotdpi) if args.jsonmap: animate.make_json(bestgraph, args.jsonmap, args.faction) gsheets.write_workplan(gs, args.sheetid, bestgraph, bestplan, beststats, args.faction, args.travelmode, args.nosave)
def main(): description = ('Ingress FieldPlan - Maximize the number of links ' 'and fields, and thus AP, for a collection of ' 'portals in the game Ingress and create a convenient plan ' 'in Google Spreadsheets. Spin-off from Maxfield.') parser = argparse.ArgumentParser( description=description, prog='makePlan.py', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument( '-i', '--iterations', type=int, default=10000, help='Number of iterations to perform. More iterations may improve ' 'results, but will take longer to process.') parser.add_argument('-k', '--maxkeys', type=int, default=None, help='Limit number of keys required per portal ' '(may result in less efficient plans)') parser.add_argument( '-m', '--travelmode', default='walking', help='Travel mode (walking, bicycling, driving, transit).') parser.add_argument( '-s', '--sheetid', default=None, required=True, help='The Google Spreadsheet ID with portal definitions.') parser.add_argument( '-n', '--nosave', action='store_true', default=False, help='Do not attempt to save the spreadsheet, just calculate the plan.' ) parser.add_argument( '-p', '--plots', default=None, help='Save step-by-step PNGs of the workplan into this directory.') parser.add_argument( '--plotdpi', default=96, type=int, help='DPI to use for generating plots (try 144 for high-dpi screens)') parser.add_argument('-g', '--gmapskey', default=None, help='Google Maps API key (for better distances)') parser.add_argument('-f', '--faction', default='enl', help='Set to "res" to use resistance colours') parser.add_argument('-l', '--log', default=None, help='Log file where to log processing info') parser.add_argument('-d', '--debug', action='store_true', default=False, help='Add debug information to the logfile') parser.add_argument('-q', '--quiet', action='store_true', default=False, help='Only output errors to the stdout') args = parser.parse_args() if args.iterations < 0: parser.error('Number of extra samples should be positive') if args.plotdpi < 1: parser.error('%s is not a valid screen dpi' % args.plotdpi) if args.faction not in ('enl', 'res'): parser.error('Sorry, I do not know about faction "%s".' % args.faction) logger.setLevel(logging.DEBUG) if args.log: ch = logging.FileHandler(args.log) formatter = logging.Formatter( '{%(module)s:%(funcName)s:%(lineno)s} %(message)s') ch.setFormatter(formatter) if args.debug: ch.setLevel(logging.DEBUG) else: ch.setLevel(logging.INFO) logger.addHandler(ch) ch = logging.StreamHandler() formatter = logging.Formatter('%(message)s') ch.setFormatter(formatter) if args.quiet: ch.setLevel(logging.CRITICAL) else: ch.setLevel(logging.INFO) logger.addHandler(ch) gs = gsheets.setup() portals, blockers = gsheets.get_portals_from_sheet(gs, args.sheetid) logger.info('Considering %d portals', len(portals)) if len(portals) < 3: logger.critical('Must have more than 2 portals!') sys.exit(1) if len(portals) > _MAX_PORTALS_: logger.critical('Portal limit is %d', _MAX_PORTALS_) a = maxfield.populateGraph(portals) ab = None if blockers: ab = maxfield.populateGraph(blockers) (bestgraph, bestplan, bestdist) = maxfield.loadCache(a, ab) if bestgraph is None: # Use a copy, because we concat ab to a for blockers distances maxfield.genDistanceMatrix(a.copy(), ab, args.gmapskey, args.travelmode) bestkm = None else: bestkm = bestdist / float(1000) logger.info('Best distance of the plan loaded from cache: %0.2f km', bestkm) counter = 0 if args.maxkeys: logger.info('Finding an efficient plan with max %s keys', args.maxkeys) else: logger.info('Finding an efficient plan') logger.info('Ctrl-C to exit and use the latest best plan') failcount = 0 try: while counter < args.iterations: if failcount >= 100: logger.info('Too many consecutive failures, exiting early.') break b = a.copy() counter += 1 if not args.quiet: if bestkm is not None: sys.stdout.write('\r(%0.2f km best): %s/%s ' % (bestkm, counter, args.iterations)) sys.stdout.flush() failcount += 1 if not maxfield.maxFields(b): logger.debug('Could not find a triangulation') failcount += 1 continue for t in b.triangulation: t.markEdgesWithFields() maxfield.improveEdgeOrder(b) workplan = maxfield.makeWorkPlan(b, ab) if args.maxkeys: # do any of the portals require more than maxkeys sane_key_reqs = True for i in range(len(b.node)): if b.in_degree(i) > args.maxkeys: sane_key_reqs = False break if not sane_key_reqs: failcount += 1 logger.debug('Too many keys required, ignoring plan') continue sane_out_links = True for i in range(len(b.node)): if b.out_degree(i) > 8: sane_out_links = False break if not sane_out_links: failcount += 1 logger.debug('Too many outgoing links, ignoring plan') continue failcount = 0 totaldist = maxfield.getWorkplanDist(b, workplan) if totaldist < bestdist: counter = 0 bestplan = workplan bestgraph = b bestdist = totaldist bestkm = bestdist / float(1000) except KeyboardInterrupt: if not args.quiet: print() print('Exiting loop') if not args.quiet: print() if bestplan is None: logger.critical('Could not find a solution for this list of portals.') sys.exit(1) maxfield.saveCache(bestgraph, ab, bestplan, bestdist) if args.plots: animate.make_png_steps(bestgraph, bestplan, args.plots, args.faction, args.plotdpi) gs = gsheets.setup() gsheets.write_workplan(gs, args.sheetid, bestgraph, bestplan, args.faction, args.travelmode, args.nosave)
def main(): description = ('Ingress FieldPlan - Maximize the number of links ' 'and fields, and thus AP, for a collection of ' 'portals in the game Ingress and create a convenient plan ' 'in Google Spreadsheets. Spin-off from Maxfield.') parser = argparse.ArgumentParser( description=description, prog='makePlan.py', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument( '-i', '--iterations', type=int, default=10000, help='Number of iterations to perform. More iterations may improve ' 'results, but will take longer to process.') parser.add_argument('-k', '--maxkeys', type=int, default=None, help='Limit number of keys required per portal ' '(may result in less efficient plans).') parser.add_argument( '-m', '--travelmode', default='walking', help='Travel mode (walking, bicycling, driving, transit).') parser.add_argument( '-s', '--sheetid', default=None, required=True, help='The Google Spreadsheet ID with portal definitions.') parser.add_argument( '-n', '--nosave', action='store_true', default=False, help='Do not attempt to save the spreadsheet, just calculate the plan.' ) parser.add_argument( '-r', '--roundtrip', action='store_true', default=False, help= 'Make sure the plan starts and ends at the same portal (may be less efficient).' ) parser.add_argument( '-b', '--beginfirst', action='store_true', default=False, help= 'Begin capture with the first portal in the spreadsheet (may be less efficient).' ) parser.add_argument( '-p', '--plots', default=None, help='Save step-by-step PNGs of the workplan into this directory.') parser.add_argument( '--plotdpi', default=96, type=int, help='DPI to use for generating plots (try 144 for high-dpi screens)') parser.add_argument('-g', '--gmapskey', default=None, help='Google Maps API key (for better distances)') parser.add_argument('-f', '--faction', default='enl', help='Set to "res" to use resistance colours') parser.add_argument( '-u', '--maxmu', action='store_true', default=False, help='Find a plan with best MU per distance travelled ratio') parser.add_argument('--maxcpus', default=mp.cpu_count(), type=int, help='Maximum number of cpus to use') parser.add_argument('-l', '--log', default=None, help='Log file where to log processing info') parser.add_argument('-d', '--debug', action='store_true', default=False, help='Add debug information to the logfile') parser.add_argument('-q', '--quiet', action='store_true', default=False, help='Only output errors to the stdout') args = parser.parse_args() if args.iterations < 0: parser.error('Number of extra samples should be positive') if args.plotdpi < 1: parser.error('%s is not a valid screen dpi' % args.plotdpi) if args.faction not in ('enl', 'res'): parser.error('Sorry, I do not know about faction "%s".' % args.faction) logger.setLevel(logging.DEBUG) if args.log: ch = logging.FileHandler(args.log) formatter = logging.Formatter( '{%(module)s:%(funcName)s:%(lineno)s} %(message)s') ch.setFormatter(formatter) if args.debug: ch.setLevel(logging.DEBUG) else: ch.setLevel(logging.INFO) logger.addHandler(ch) ch = logging.StreamHandler() formatter = logging.Formatter('%(message)s') ch.setFormatter(formatter) if args.quiet: ch.setLevel(logging.CRITICAL) else: ch.setLevel(logging.INFO) logger.addHandler(ch) gs = gsheets.setup() portals, blockers = gsheets.get_portals_from_sheet(gs, args.sheetid) logger.info('Considering %d portals', len(portals)) if len(portals) < 3: logger.critical('Must have more than 2 portals!') sys.exit(1) if len(portals) > _MAX_PORTALS_: logger.critical('Portal limit is %d', _MAX_PORTALS_) a = maxfield.populateGraph(portals) ab = None if blockers: ab = maxfield.populateGraph(blockers) # Use a copy, because we concat ab to a for blockers distances maxfield.genDistanceMatrix(a.copy(), ab, args.gmapskey, args.travelmode) (bestgraph, bestplan) = maxfield.loadCache(a, ab, args.travelmode, args.beginfirst, args.roundtrip, args.maxmu) if bestgraph is not None: bestdist = maxfield.getWorkplanDist(bestgraph, bestplan) bestarea = maxfield.getWorkplanArea(bestgraph, bestplan) bestmudist = int(bestarea / bestdist) bestkm = bestdist / float(1000) bestsqkm = bestarea / float(1000000) logger.info('Best distance of the plan loaded from cache: %0.2f km', bestkm) logger.info('Best coverage of the plan loaded from cache: %0.2f sqkm', bestsqkm) else: bestkm = None bestsqkm = None bestdist = np.inf bestarea = 0 bestmudist = 0 counter = 0 if args.maxkeys: logger.info('Finding an efficient plan with max %s keys', args.maxkeys) else: logger.info('Finding an efficient plan') failcount = 0 seenplans = list() # set up multiprocessing ready_queue = mp.Queue() processes = list() for i in range(args.maxcpus): logger.debug('Starting process %s', i) p = mp.Process(target=queue_job, args=(a, ready_queue)) processes.append(p) p.start() logger.info('Started %s worker processes', len(processes)) logger.info('Ctrl-C to exit and use the latest best plan') try: while counter < args.iterations: if failcount >= 100: logger.info('Too many consecutive failures, exiting early.') break success, b = ready_queue.get() counter += 1 if not args.quiet: if bestkm is not None: sys.stdout.write( '\r(Best: %0.2f km, %0.2f sqkm, %s sqm/m, %s actions): %s/%s ' % (bestkm, bestsqkm, bestmudist, len(bestplan), counter, args.iterations)) sys.stdout.flush() if not success: failcount += 1 continue workplan = maxfield.makeWorkPlan(b, ab, args.roundtrip, args.beginfirst) if args.maxkeys: # do any of the portals require more than maxkeys sane_key_reqs = True for i in range(len(b.node)): if b.in_degree(i) > args.maxkeys: sane_key_reqs = False break if not sane_key_reqs: failcount += 1 logger.debug('Too many keys required, ignoring plan') continue sane_out_links = True for i in range(len(b.node)): if b.out_degree(i) > 8: sane_out_links = False break if not sane_out_links: failcount += 1 logger.debug('Too many outgoing links, ignoring plan') continue failcount = 0 totaldist = maxfield.getWorkplanDist(b, workplan) totalarea = maxfield.getWorkplanArea(b, workplan) mudist = int(totalarea / totaldist) newbest = False if args.maxmu: # choose a plan that gives us most MU captured per distance of travel if mudist > bestmudist: newbest = True else: # We want: # - the shorter plan, or # - the plan with a similar length that requires fewer actions, or # - the plan with a similar length that has higher mu per distance of travel # - we have not yet considered this plan if ((bestdist - totaldist > 80 or (len(workplan) < len(bestplan) and totaldist - bestdist <= 80) or (mudist > bestmudist and totaldist - bestdist <= 80)) and workplan not in seenplans): newbest = True if newbest: counter = 0 bestplan = workplan seenplans.append(workplan) bestgraph = b bestdist = totaldist bestarea = totalarea bestkm = bestdist / float(1000) bestsqkm = bestarea / float(1000000) bestmudist = mudist except KeyboardInterrupt: if not args.quiet: print() print('Exiting loop') finally: for p in processes: p.terminate() if not args.quiet: print() if bestplan is None: logger.critical('Could not find a solution for this list of portals.') sys.exit(1) maxfield.saveCache(bestgraph, ab, bestplan, args.travelmode, args.beginfirst, args.roundtrip, args.maxmu) if args.plots: animate.make_png_steps(bestgraph, bestplan, args.plots, args.faction, args.plotdpi) gsheets.write_workplan(gs, args.sheetid, bestgraph, bestplan, args.faction, args.travelmode, args.nosave, args.roundtrip, args.maxmu)