'''
Copyright 2013 Mark Dredze. All rights reserved.
This software is released under the 2-clause BSD license.
Mark Dredze, mdredze@cs.jhu.edu
'''
# Input- a whitelist for each area, a list of submissions per area, csv file with reviewer signups
# The area must add people to their whitelist that they approve of by using my other script
# Merge reviewer duplicates on input

# Algorithm:
# Find choices for every reviewer that respect the whitelist.
# Find every reviewer with only a single choice overall and assign them to their area
# assume a default of X papers by reviewer
# keep track of the number of reviewers assigned to each area and the number needed by each area
# for reviwers with 2 choices
# round robin between areas- for each area, find a reviewer who wants that area as #1, pick them
# Keep selecting area until it is full
# If no first round, then pick a second choice reviewer
# Once all areas are full, continue assignment but put an area in the round robin list propto the number of reviewers they need
# Start with an assumption of 3 * number of papers / load (4) but load is adjustable
from acl_check_reviewers import selectAreaName

import sys, os, csv, re, glob, random

class CsvLoader:
	def __init__(self, filename):
		self.__loadFile(filename)
		
	def __mapColumns(self, entry):
		self.names_to_columns = {}
		self.columns_to_names = {}
		for ii, name in enumerate(entry):
			self.names_to_columns[name.lower()] = ii
			self.columns_to_names[ii] = name.lower()

	def getColumn(self, entry, column):
		column_id = self.names_to_columns[column]
		if column_id >= len(entry):
			return ''
		return entry[column_id]
	
	def getColumnNames(self):
		return self.names_to_columns.keys()
		
	def __loadFile(self, filename):
		file = open(filename)
		reader = csv.reader(file)
		
		self.contents = []
		
		for ii, entry in enumerate(reader):
			if ii == 0:
				self.__mapColumns(entry)
				continue
			line = {}
			for jj in range(len(entry)):
				line[self.columns_to_names[jj]] = entry[jj]
			self.contents.append(line)

		file.close()
	
	def __iter__(self):
		return self.contents.__iter__()
	
	def __len__(self):
		return len(self.contents)
		
class ACLAssignGreedyReviewers:
	def __init__(self):
		self.want_string = 'Want to review (1st Choices)'
		self.willing_string = 'Willing to review (2nd Choices)'
		self.will_not_string = 'Will not review'
		self.name_field = 'name'
		self.email_field = 'email'
		self.start_account_username = 'start account username'
		pass
	
	
	def loadReviewerInformation(self, reviewer_csv_filename):
		csv_loader = CsvLoader(reviewer_csv_filename)
		
		
		column_names = csv_loader.getColumnNames()
		if 'name (first last)' in column_names:
			self.name_field = 'name (first last)'
		if 'email address' in column_names:
			self.email_field = 'email address'
		
		area_entry_to_name = {}
		for entry in column_names:
			if entry.startswith('areas ['):
				area_name = re.search('areas \[(.+?) \(', entry).group(1)
				area_entry_to_name[entry] = area_name
				
		
		names = set()
		emails = set()
		reviewer_to_area_choices = {}
		emails_to_reviewer_id_dict = {}
		from_reviewer_id_dict = {}
		reviewer_to_load = {}
		
		num_lines = 0
		for entry in csv_loader:
			num_lines += 1
			if len(entry) == 0:
				continue
			name = entry[self.name_field]
			email = entry[self.email_field].lower().strip()
			start_account_username = entry[self.start_account_username]
			
			load_for_reviewer = entry['reduced review load (optional)']
			
			reviewer_id = name.replace(' ', '_') + '_' + email.replace(' ', '_')
			
				
			# Is this a valid email address.
			if '@' not in email or ' ' in email:
				print 'Warning: Invalid email: %s (%s)' % (name, email)
			

			duplicate_reviewer = False
			if reviewer_id in from_reviewer_id_dict or name in names or email in emails:
				if email in emails_to_reviewer_id_dict:
					reviewer_id = emails_to_reviewer_id_dict[email]
					duplicate_reviewer = True
				else:
					print 'Warning: duplicate reviewer name: ', name
			
			try:
				load_for_reviewer = int(load_for_reviewer)
				reviewer_to_load[reviewer_id] = load_for_reviewer
				print 'Registered load limit for %s (%s): %d' % (name, email, load_for_reviewer)
			except:
				pass
			
			names.add(name)
			emails.add(email)
			
			emails_to_reviewer_id_dict[email] = reviewer_id
			tuple = (name, email, start_account_username)
			
			from_reviewer_id_dict[reviewer_id] = tuple
			area_choices = []
	
			for area_entry, area_name in area_entry_to_name.iteritems():
				if area_entry not in entry:
					continue
				choice = entry[area_entry]
				rating = None
				if choice == self.want_string:
					rating = 1
				elif choice == self.willing_string:
					rating = 2
				
				if rating != None:
					area_choices.append((area_name, rating))
			
			if duplicate_reviewer:
				# Merge the reveiwers choices by always taking their higher choice.
				old_area_choices = reviewer_to_area_choices[reviewer_id]
				new_area_choices = {}
				for area, rating in area_choices:
					new_area_choices[area] = rating
				for area, rating in old_area_choices:
					new_area_choices[area] = max(new_area_choices.setdefault(area, 0), rating)
				area_choices = []
				for area, choice in new_area_choices.iteritems():
					area_choices.append((area, choice))
					
			reviewer_to_area_choices[reviewer_id] = area_choices
		
		print 'Number of lines: %d' % num_lines
		print 'Loaded %d/%d reviewers.' % (len(from_reviewer_id_dict), len(reviewer_to_area_choices))
		return reviewer_to_area_choices, emails_to_reviewer_id_dict, from_reviewer_id_dict, reviewer_to_load
	
	def selectReviewerForArea(self, area, reviewers_per_area_lists, used_reviewers):
		while len(reviewers_per_area_lists[area]) > 0:
			reviewer = reviewers_per_area_lists[area].pop(0)
			if reviewer not in used_reviewers:
				return reviewer
		return None
	
	# To handle differences in number of papers, we want to have some areas get multiple
	# people per round so every area fills up at the same time.
	# This methods computes how many assignments are needed to reach that point.
	def computeNumAreaAssignmentPerRound(self, area_to_load, area_to_num_papers, area_to_paper_load, priority_areas):
		area_to_num_assignments_per_round = {}
		# Compute how many reviewers are needed in each area.
		reviewers_needed_per_area = {}
		min = None
		max = None
		for area in area_to_num_papers.keys():
			reviewers_needed_per_area[area] = (area_to_num_papers[area] * area_to_paper_load[area]) / area_to_load[area]

			if min == None or min > reviewers_needed_per_area[area]:
				min = reviewers_needed_per_area[area]
			if max == None or max < reviewers_needed_per_area[area]:
				max = reviewers_needed_per_area[area]
	
		if max / min < 2:
			# The max isn't even twice the min area, so scale things up.
			print 'Error: max is not greater than twice min. Using 1 for everything.'
		# The min area gets one reviewer per round and every other area gets int(area/min)
		for area, num_reviewers in reviewers_needed_per_area.iteritems():
			area_to_num_assignments_per_round[area] = int(num_reviewers / min)
		
		for area in area_to_num_assignments_per_round.keys():
			if priority_areas != None and area in priority_areas:
				area_to_num_assignments_per_round[area] *= self.increase_priority_factor
			
		return area_to_num_assignments_per_round
	
	def assignReviewers(self, reviewers_per_area_lists, \
						reviewer_load_constraint, area_to_load, area_to_num_papers, \
						area_to_num_assignments_per_round, area_to_paper_load, \
						assign_all_whitelist_reviewers_to_area, forced_reviewer_to_area, \
						min_reviewers_per_area):
		# For each area, we have a list of reviewers who selected that area ordered by first choice,
		# the second choice, then by the number of total areas they picked.
		
		# A dict between area name and the reviewers assigned to that area.
		assignments = {}
		
		areas = area_to_num_papers.keys()
		print 'Assigning to %d areas.' % len(areas)
		area_to_num_reviews_assigned = {}
		for area in areas:
			area_to_num_reviews_assigned[area] = 0
		
		# In each pass, find reviewers who have only n selections. This saves the more flexible
		# reviewers for later.
		assignment_made = False
		full_areas = set()
		used_reviewers = set()
		all_areas_full = False
		all_areas_have_been_filled = False
		
		if assign_all_whitelist_reviewers_to_area:
			for area in assign_all_whitelist_reviewers_to_area:
				# Give everyone to this area that they want.
				while True:
					reviewer = self.selectReviewerForArea(area, reviewers_per_area_lists, used_reviewers)
	
					if not reviewer:
						break
					# Assign the reviewer to the area.
					assignments.setdefault(area, set()).add(reviewer)
					used_reviewers.add(reviewer)
					
					# How many reviews did we just assign to this area?
					if reviewer in reviewer_load_constraint:
						load_constraint = reviewer_load_constraint[reviewer]
						# A reviewer cannot exceed the load for an area.
						this_reviewer_load = min(load_constraint, area_to_load[area])
						if this_reviewer_load != area_to_load[area]:
							print 'LOAD LIMIT for %s: %d instead of %d' % (reviewer, this_reviewer_load, area_to_load[area])
						else:
							# This isn't a constraint for this area. Remove it
							del reviewer_load_constraint[reviewer]
					else:
						this_reviewer_load = area_to_load[area]
					area_to_num_reviews_assigned[area] += this_reviewer_load

					
				# Declare this area full.
				full_areas.add(area)
			
		
		# Assign reviewers to an area if they are forced to that area by special request.
		for (reviewer, area) in forced_reviewer_to_area.iteritems():
			assignments.setdefault(area, set()).add(reviewer)
			used_reviewers.add(reviewer)
			
			# How many reviews did we just assign to this area?
			if reviewer in reviewer_load_constraint:
				load_constraint = reviewer_load_constraint[reviewer]
				# A reviewer cannot exceed the load for an area.
				this_reviewer_load = min(load_constraint, area_to_load[area])
				if this_reviewer_load != area_to_load[area]:
					print 'LOAD LIMIT for %s: %d instead of %d' % (reviewer, this_reviewer_load, area_to_load[area])
				else:
					# This isn't a constraint for this area. Remove it
					del reviewer_load_constraint[reviewer]
			else:
				this_reviewer_load = area_to_load[area]
			area_to_num_reviews_assigned[area] += this_reviewer_load
			
			# Does this area need more reviewers?
			if area_to_num_reviews_assigned[area] >= \
				(area_to_num_papers[area] * area_to_paper_load[area]) \
				and not all_areas_full \
				and min_reviewers_per_area <= len(assignments[area]):

				full_areas.add(area)
	
					
					
					
		while True:
			assignment_made = False
			#random.shuffle(areas)
			for area in areas:
				if area in full_areas and not all_areas_full:
					continue
				
					
				for ii in range(0, int(area_to_num_assignments_per_round[area])):
					# Increase until we find a reviewer. or continue if we cannot.
					reviewer = self.selectReviewerForArea(area, reviewers_per_area_lists, used_reviewers)
					if not reviewer:
						continue # We found no valid reviewer.
						
					# Assign the reviewer to the area.
					assignments.setdefault(area, set()).add(reviewer)
					assignment_made = True
					used_reviewers.add(reviewer)
					
					# How many reviews did we just assign to this area?
					if reviewer in reviewer_load_constraint:
						load_constraint = reviewer_load_constraint[reviewer]
						# A reviewer cannot exceed the load for an area.
						this_reviewer_load = min(load_constraint, area_to_load[area])
						if this_reviewer_load != area_to_load[area]:
							#print 'LOAD LIMIT for %s: %d instead of %d' % (reviewer, this_reviewer_load, area_to_load[area])
							pass
						else:
							# This isn't a constraint for this area. Remove it
							del reviewer_load_constraint[reviewer]
					else:
						this_reviewer_load = area_to_load[area]
					area_to_num_reviews_assigned[area] += this_reviewer_load
					
					# Does this area need more reviewers?
					if area_to_num_reviews_assigned[area] >= \
						(area_to_num_papers[area] * area_to_paper_load[area]) \
						and not all_areas_full \
						and min_reviewers_per_area <= len(assignments[area]):
						full_areas.add(area)
						
						break
	
					
				
			# Do we have enough assignments made?
			if len(full_areas) == len(areas) and not all_areas_full:
				all_areas_full = True
				all_areas_have_been_filled = True
			elif not assignment_made and not all_areas_full:
				# The areas didn't just fill up, but no assignments were made.
				# Start making all remaining assignments anyway.
				all_areas_full = True
			elif not assignment_made and all_areas_full:
				# We can no longer make assignments and we've tried to make any assignment.
				break
		
		print 'Assignments finished.'
		if all_areas_have_been_filled:
			print 'All areas full.'
		else:
			print 'Not all areas full.'
			area_list = []
			for area in areas:
				if area not in full_areas:
					area_list.append(area)
			print 'Needs reviewers: ' + '   |   '.join(area_list)
		
		areas.sort()
		for area in areas:
			coverage = area_to_num_reviews_assigned[area]/area_to_paper_load[area] / float(area_to_num_papers[area]) * 100
			prefix = ''
			if coverage < 100:
				prefix='* '
			print '%s%s (Reviewers: %d, Max review capacity: %d, Actual reviews needed: %d, Coverage: %.0f%%)' % (prefix, area, len(assignments[area]), area_to_num_reviews_assigned[area]/area_to_paper_load[area], area_to_num_papers[area], coverage)
				
		return assignments, area_to_num_reviews_assigned
	
	# Create a map between area and reviewer, with reviewers sorted by choice.
	def createAreaReviewerLists(self, reviewer_to_area_choices, area_to_whitelist, accept_all_reviewers=False):
		reviewers_per_area_lists = {}
		area_to_total_possible_reviewers = {}
		for reviewer, area_choices in reviewer_to_area_choices.iteritems():
			for area, rating in area_choices:
				if accept_all_reviewers or (area in area_to_whitelist and reviewer in area_to_whitelist[area]):
					reviewers_per_area_lists.setdefault(area, []).append((rating, reviewer))
				area_to_total_possible_reviewers[area] = area_to_total_possible_reviewers.setdefault(area, 0) + 1
		
		for area, list in reviewers_per_area_lists.iteritems():
			list.sort(reverse=False)
			new_list = []
			for num, reviewer in list:
				new_list.append(reviewer)
			reviewers_per_area_lists[area] = new_list
		
		print 'Accepted reviewers per area (not including forced reviewers).'
		for area in area_to_whitelist:
			percent = float(len(reviewers_per_area_lists[area])) / float(area_to_total_possible_reviewers[area]) * 100
			print '\t%s %d accepted / %d total (%.2f)' % (area, len(reviewers_per_area_lists[area]), area_to_total_possible_reviewers[area], percent)
		return reviewers_per_area_lists

	def getSecondArgument(self, line):
		line = line.strip()

		split_line = line.split('\t')
		if len(split_line) < 2:
			print 'Error on line: %s' % line
		argument = split_line[1]
		
		return argument
		
	def loadWhitelists(self, whitelist_files, emails_to_reviewer_id_dict, forced_reviewer_to_area):
		whitelists = {}
		area_to_load = {}
		area_to_paper_load = {}
		for filename in whitelist_files:
			print 'Loading whitelist: %s' % filename
			file = open(filename)
			lines = file.readlines()
			file.close()
			
			if not lines[0].startswith('#') and not lines[1].startswith('#') and not lines[2].startswith('#'):
				print 'Error in whitelist file. Missing # on first three lines. ', filename
			
			area_name = self.getSecondArgument(lines[0])
			area_to_load[area_name] = int(self.getSecondArgument(lines[1]))
			area_to_paper_load[area_name] = int(self.getSecondArgument(lines[2]))
			
			num_loaded_reviewers = 0 
			whitelists[area_name] = set()
			for line in lines[3:]:
				line = line.strip()
				if line.startswith('#') or line == '':
					continue
				split_line = line.split('\t')
				if len(split_line) != 2:
					print 'Error on line: "%s"' % line
				reviewer_name = split_line[0].strip()
				reviewer_email = split_line[1].strip().lower()
				
				if reviewer_email not in emails_to_reviewer_id_dict:
					print 'Error: whitelist contains unknown reviewer: "%s" "%s"' % (reviewer_name, reviewer_email)
					sys.exit()
				
				if reviewer_name.startswith('*'):
					reviewer_name = reviewer_name[1:]
					print 'Forcing reviewer %s to area %s' % (reviewer_name, area_name)
					reviewer_id = emails_to_reviewer_id_dict[reviewer_email]
					if reviewer_id in forced_reviewer_to_area:
						print 'Error. %s is being forced to multiple areas.' % reviewer_name
						area_list_to_print = [forced_reviewer_to_area[reviewer_id], area_name]
						print '\tAreas: %s' % '|'.join(area_list_to_print)
						sys.exit()
					else:
						forced_reviewer_to_area[emails_to_reviewer_id_dict[reviewer_email]] = area_name
						num_loaded_reviewers += 1
				else:
					whitelists[area_name].add(emails_to_reviewer_id_dict[reviewer_email])
					num_loaded_reviewers += 1
					
			
			print 'Loaded %d reviewers for area %s.' % (num_loaded_reviewers, area_name)

		print 'Processed %d whitelists.' % len(whitelists)
		return whitelists, area_to_load, area_to_paper_load
	
	def getWhitelistFilenames(self, whitelist_files_prefix):
		return glob.glob(whitelist_files_prefix + '*')
	
	def printFinalAssignmentStats(self, output_filename_prefix, assignments, from_reviewer_id_dict, reviewer_load_constraint):
		output = open(output_filename_prefix + '_all_list.csv', 'w')
		
		#output.write('#name\temail\tmax papers to assign\tarea\n')
		output.write('#username\temail\tfirst\tlast\ttrack\tmax papers to assign\n')
		for area_name, reviewers in assignments.iteritems():
			filename = area_name.replace(' ', '_').replace('/', '_').replace('&', '_')
			area_output = open(output_filename_prefix + filename + '.csv', 'w')
			area_output.write('#name\temail\tmax papers to assign\n')
			for reviewer in reviewers:
				reviewer_name, reviewer_email, start_account_username = from_reviewer_id_dict[reviewer]
				load_constraint = ''
				if reviewer in reviewer_load_constraint:
					load_constraint = str(reviewer_load_constraint[reviewer])
					
				firstname, lastname = reviewer_name.rsplit(' ', 1)
				output.write('%s\t%s\t%s\t%s\t%s\t%s\n' % (start_account_username, reviewer_email, firstname, lastname, area_name, load_constraint))
				area_output.write('%s\t%s\t%s\n' % (reviewer_name, reviewer_email, load_constraint))
			area_output.close()
		output.close()
	
		
		
	
	# Process a filename of the following format:
	# areaname \t reviewer load (how many papers per reviewer) \t num submissions
	
	def loadAreaStats(self, area_stats_filename):
		file = open(area_stats_filename)
		area_to_num_papers = {}
		total_submissions = 0
		for line in file:
			line = line.strip()
			if line.startswith('#') or line == '':
				continue
			print line
			area_name, submissions = line.split('\t')

			area_to_num_papers[area_name.lower()] = int(submissions)
			total_submissions += int(submissions)
		file.close()
		
		print 'Total submissions: %d' % total_submissions
		return area_to_num_papers
	
	def computeReviewerStats(self, assignments, reviewer_to_area_choices):
		total_choice_scores = 0
		total_assigned = 0
		rating_counts = [0,0]
		assigned_reviewers = set()
		for area_name, reviewer_list in assignments.iteritems():
			for reviewer in reviewer_list:
				assigned_reviewers.add(reviewer)
				area_choices = reviewer_to_area_choices[reviewer]
				for choice, rating in area_choices:
					if choice == area_name:
						total_choice_scores += rating
						total_assigned += 1
						rating_counts[rating-1] += 1 
						break

		print 'Average choice rating: ' + str(float(total_choice_scores) / float(total_assigned))
		print 'Reviewers with first choice: ' + str(rating_counts[0])
		print 'Reviewers with second choice: ' + str(rating_counts[1])
		print 'Assigned reviewers: ' + str(total_assigned)
		print 'Total reviewers: ' + str(len(reviewer_to_area_choices))
		
		# Who wasn't assigned?
		unassigned_reviewers = set()
		for reviewer in reviewer_to_area_choices.keys():
			if reviewer not in assigned_reviewers:
				unassigned_reviewers.add(reviewer)
		print 'Unassigned reviewers: %s' % ', '.join(unassigned_reviewers)
		
				
	def loadReviewerLoadConstraints(self, reviewer_load_constraints_files, email_to_reviewer_id):
		reviewer_load_constraint = {}
		for filename in reviewer_load_constraints_files:
			print 'Loading constraints from file: %s' % filename
			file = open(filename)
		
			for line in file:
				line = line.strip()
				if line.startswith('#') or line == '':
					continue
					
				email, load = line.split('\t')
				email = email.lower()
				
				if email not in email_to_reviewer_id:
					print 'Error: Loaded a constraint for %s but could not find this reviewer.' % email
				else:
					reviewer_id = email_to_reviewer_id[email]
					reviewer_load_constraint[reviewer_id] = int(load)
				
			file.close()
		
		return reviewer_load_constraint
		
	def run(self):
		# Load the acl signup sheet
		# Load a csv file with whitelists. each file should start with the area name on the first line
		# load a reviewer constraint list.
		# A file containing the number of submissions per area
		# An output file to write the assignments
		# A whitelist file should contain the area name on the first line preceeded by a pound. It then contains the email addresses of each reviewer.
		
		# reviewer_to_area_choices- filtered by whitelists. For each reviewer, a list of tuples with area and rating (1,2)
		# area_to_load- the load per reviewer in each area
		# area_to_num_papers- the number of papers in each area.
		# area_to_num_assignments_per_round = {} # In the round robin assignment, give multiple reviewers to each
		# 		area based on the number of reviewers they have total.
		
		if len(sys.argv) != 5:
			print 'Usage: %s reviewer_csv area_stats_filename whitelist_files_prefix output_filename_prefix' % sys.argv[0]
			sys.exit()
		reviewer_csv = sys.argv[1]
		area_stats_filename = sys.argv[2]
		whitelist_files_prefix = sys.argv[3]
		output_filename_prefix = sys.argv[4]
		#reviewer_load_constraints_prefix = sys.argv[5]
		
		accept_all_reviewers = False		

		# Automatically give this area everyone in its whitelist that is available.
		assign_all_whitelist_reviewers_to_area = None#set(['spoken language processing', 'nlp-enabled technology'])
		
		# These areas receive extra reviewers on each round so they fill up.
		priority_areas = None#set(['spoken language processing', 'nlp-enabled technology', 'multimodal nlp'])
		self.increase_priority_factor = 2
		
		# Small tracks may end up getting a handfull of reviewers. Ensure that there are at least this many
		# reviewers per area
		min_reviewers_per_area = 10
		################################################
		area_to_num_papers = self.loadAreaStats(area_stats_filename)
		
		# Dedpue and normalize reviewer list
		# emails_to_reviewer_id_dict # A dictionary between emails to reviewer ids.
		# from_reviewer_id_dict # A dictionary containing reviewer names and emails (tuple) from reviewer id.
		
		reviewer_to_area_choices, emails_to_reviewer_id_dict, from_reviewer_id_dict, reviewer_load_constraint = self.loadReviewerInformation(reviewer_csv)
		# Load whitelists and normalize reviewers.
		# A dictionary mapping area to a set of reviewer_ids
		
		#reviewer_load_constraints_files = self.getWhitelistFilenames(reviewer_load_constraints_prefix)
		#reviewer_load_constraint = self.loadReviewerLoadConstraints(reviewer_load_constraints_files, emails_to_reviewer_id_dict)
		
		forced_reviewer_to_area = {}
		whitelist_files = self.getWhitelistFilenames(whitelist_files_prefix)
		area_to_whitelist, area_to_load, area_to_paper_load = self.loadWhitelists(whitelist_files, emails_to_reviewer_id_dict, forced_reviewer_to_area)
		# normalize reviewers by unique keys based on email and username so we can match against whitelists
		reviewers_per_area_lists = self.createAreaReviewerLists(reviewer_to_area_choices, area_to_whitelist, accept_all_reviewers=accept_all_reviewers)
		
		# area_to_paper_load- the number of reviewers needed for each paper in each area
		
		area_to_num_assignments_per_round = self.computeNumAreaAssignmentPerRound(area_to_load, area_to_num_papers, area_to_paper_load, priority_areas)
						
						
		assignments, area_to_num_reviews_assigned = \
			self.assignReviewers(reviewers_per_area_lists, reviewer_load_constraint, area_to_load, \
								area_to_num_papers, area_to_num_assignments_per_round, area_to_paper_load, \
								assign_all_whitelist_reviewers_to_area, forced_reviewer_to_area, min_reviewers_per_area)
		
		self.computeReviewerStats(assignments, reviewer_to_area_choices)
		self.printFinalAssignmentStats(output_filename_prefix, assignments, from_reviewer_id_dict, reviewer_load_constraint)

if __name__ == '__main__':
	ACLAssignGreedyReviewers().run()