# simulated annealing
# Wesley Phoa 1/5/00

from copy import *
from operator import *
from math import *
from whrandom import *

# annealing schedule: reduce temp by this factor each try
sched_const = 0.95

# estimate starting temperature by generating some random scenarios

def start_temp(scenario, objective):
	E0 = objective(scenario)
	Es = []
	for i in range(10):
		idx = randint(0, len(scenario) - 1)
		scenario[idx] = 1 - scenario[idx]
		Es.append(objective(scenario))
		scenario[idx] = 1 - scenario[idx]
	return 2. * max(map(lambda E, E0=E0: abs(E - E0), Es))

# generate a new scenario by taking one Metropolis step
# permit certain event switches to be blocked from toggling

def new_scenario(E, scenario, objective, temp, blocked=[]):
	# assumes that E = objective(scenario)
	new_scen = copy(scenario)
	for i in range(10): # try 10 times to generate a scenario
		idx = randint(0, len(scenario) - 1)
		if not idx in blocked:
			new_scen[idx] = 1 - new_scen[idx]
			Enew = objective(new_scen)
			try:
				if uniform(0., 1.) < exp(-(E - Enew) / temp):
					return new_scen, Enew
				else:
					new_scen[idx] = 1 - new_scen[idx]
			except OverflowError:
				new_scen[idx] = 1 - new_scen[idx]
	return scenario, E

# maximize objective function via simulated annealing

def anneal(scenario, objective, temp, blocked=[], iterations=250):
	E = objective(scenario)
	for i in range(iterations):
		scenario, E = \
			new_scenario(E, scenario, objective, temp, blocked)
		temp = sched_const * temp
	return scenario, E

# generate sorted list of worst case scenarios
# for each event, include at least one scenario in which that event occurs
# and at least one scenario in which that event does not occur

def worst_cases(num_events, objective):
	scenarios = []
	nothing = num_events * [0]

	for i in range(num_events):
		start = copy(nothing)
		start[i] = 1
		E = objective(start)
		temp = start_temp(start, objective)
		new = anneal(start, objective, temp, [i])
		if not new in scenarios:
			scenarios.append(new)

	for i in range(num_events):
		start = copy(nothing)
		E = objective(start)
		temp = start_temp(start, objective)
		new = anneal(start, objective, temp, [i])
		if not new in scenarios:
			scenarios.append(new)

	scenarios.sort(lambda x, y: cmp(y[1], x[1]))
	return scenarios

# sample objective function for testing: 10 events
# NB: if risk is concentrated, worst cases consist of 1 or 2 events
#     if diversified, worst cases consist of many events

sensitivities = [1.2, 2.3, 3.4, 4.3, 3.2, 2.3, 3.4, 4.3, 3.2, 2.1]
shifts        = [100, 50, 200, 50, 200, 50, 100, 50, 100, 50]

sensitivities = 3 * sensitivities
shifts = 3 * shifts

# scales[n-1] = individual shift scaling to apply to an n-event scenario
scales = [100, 78, 66, 58, 53, 48, 45, 41, 38, 36, 33] + range(30, 20, -3)
scales = scales + 50 * [20]
scales = map(lambda x: x/100., scales)

# scenario is a vector of 0s and 1s indicating which events occur
def test(scenario):
	n = reduce(add, scenario)
	return reduce(add, map(lambda switch, sensitivity, shift, \
		scale=scales[n - 1]: switch * sensitivity * shift * scale, \
		scenario, sensitivities, shifts))

if __name__=='__main__':

	import time
	t = time.time()
	worst =  worst_cases(30, test)
	print '%0.2f seconds' % (time.time() - t,)
