Code
--
--[[
* Contact Map Enforcer
* Original Author: Brow42
* For Quest to the Contact Map
* Version 1.0 Aug. 22 2012
* Creates 3 types of bands:
* Pushers push segments that are in contact but should not be
* Pullers pull segments that should be in contact but are not
* Holders weakly hold distance, and resize each round
* Pushers and Pullers become holders when they are no longer needed.
* Bands are never deleted, but accumulate as holders. Holding can be
* completed disabled.
* Holders may become Pusher/Pullers again when needed.
* Initial bands are not touched.
* Bands can be locked, preventing them from changing in the next
* round, to carry over and mix bands from different rounds.
* Version 1.1 Aug 24 2012 Brow42
* Pusher/Puller bands can get progressively stronger until their
* contact constraint is sastisfied or they hit max str.
* Version 1.2 Aug 22 2012 Brow42
* Apply bands to best score, or best multiplier, or previous wiggle
* Allow bands to remain on cancel
* Allow save of best multiplier in slot 3
* Allow use of recent best for best score
* Cancel now restores to before the wiggle, not remaining in mid-wiggle
* Changed how the band progression slider works
* Version 1.3 Sept. 29 2013 Brow42
* Changed definition of 'contact' from 0.0 to < 1.0
* Only 10% of the non-contacting pushers will be banded.
* User is asked which save to restore (if there was a wiggle)
* Bands are deleted at once if there are no bands to save
* Put new options in a new dialog panel
* clicking dialog (X) will cancel script from any dialog
* Version 2.0 May 26 2014 Brow42
* Reversed sense of heatmap (0 = no contact)
* Changed range on contact sliders to actual max and min (excluding 0)
* Default contact cutoff includes ~ all predicted contacts
* Hid exploration multiplier features
* Bands do not spam the undo stack
* Best Score save was being overwritten by last wiggle. No more.
* Version 2.1 June 5, 2014 Brwo42
* Exits with a message if the puzzle is not a contact map puzzle.
* New slider to only band a fraction of pullers if there are far too many contacts.
--]]
Title = "Contact Enforcer 2.1"
PushPullStrength,HoldStrength = 2.5,0.7 -- band strengths
PushersNoHold = true -- disable unused pushers
NoPushers = false -- no pushers = pulling only
InitialLock = 1 -- initial lock count
ULProb = 1.0 -- prob of losing a lock count per round
BandSidechain = false -- band the c-beta's instead of c-alpha's (usualy atom 5)
BandStrInc = 0 -- band strength progression
ApplyBands = { bestscore = false, bestmult = false, last = true }
SaveBestMultiplier = false
KeepBands = false
PushFraction = 0.1
PullFraction = 1.0
-- constants
NSeg = structure.GetCount()
IsExploration = current.GetExplorationMultiplier() > 0.0
-- The next two values are computed from the puzzle
--ContactHeat = 0.51 -- lower cutoff score for contact
--NoContactHeat = 0.5 -- upper cutoff for non-contact
--ContactCutoffPercentage = 0.9 -- default for ContactHeat as fraction of max heat
ExpandFactor = 1.03 -- band length adjustment from contact cutoff
WiggleDelta = 1 -- when to stop wiggle
ContactDist = 8 -- upper cutoff distance for contact
MinBandStr = 0.1
MaxBandStr = 10
BestScoreSave = 100
BestMultSave = 99
WiggleSave = 98
UsersFavoriteSave = WiggleSave -- set from ApplyBands
seed = (os.time() * 5779) % 100000 -- or set constant for reproducible behavior
-- enums for state and counters
HOLDING = 1
PUSHING = 2
PULLING = 3
LOCKED = 4
CHANGED = 5
UNCHANGED = 6
FORBIDDEN = 7
status = {} -- state of existing bands { lock count, function, band number }
Counters = {}
function Index(i,j) -- for status table
if i > j then i,j = j,i end
return i * NSeg + j
end
function IncCount(what) Counters[what] = Counters[what] + 1 end
-- ========================== Begin Dialog Menus ================
function MainMenu()
local d = dialog.CreateDialog(Title)
d.label = dialog.AddLabel('Strength of bands 0 = disable')
d.pusherstr = dialog.AddSlider('Push/Pull Str',PushPullStrength,MinBandStr,MaxBandStr,2)
d.holderstr = dialog.AddSlider('Hold Str',HoldStrength,0,MaxBandStr,2) -- 0 = disable
d.bandsc = dialog.AddCheckbox('Band Sidechain instead of Backbone',BandSidechain)
d.nohold = dialog.AddCheckbox('Pushers disable instead of hold',PushersNoHold)
d.nopushers = dialog.AddCheckbox('No pushers (only pullers and holders)',NoPushers)
d.label2 = dialog.AddLabel('You can carry over bands for 1 or more rounds')
d.locking = dialog.AddSlider('Locking',InitialLock,0,4,0)
d.rand = dialog.AddSlider('Unlock %/ Round',ULProb*100,0,100,0)
-- d.label3 = dialog.AddLabel('Pushers/Pullers can become progressively stronger')
-- d.label4 = dialog.AddLabel('1 = Next Round, 10 = Ten Rounds, 0 = No Change')
d.prog = dialog.AddSlider('Band Progression',BandStrInc,0,10,1)
if IsExploration then
d.savemult = dialog.AddCheckbox('Save highest multipier in slot 3?',SaveBestMultiplier)
end
d.keep = dialog.AddCheckbox('Keep bands on cancel', KeepBands)
d.label5 = dialog.AddLabel('Apply new bands to:')
d.applyscore = dialog.AddCheckbox('Best Scoring',ApplyBands.bestscore)
if IsExploration then
d.applymult = dialog.AddCheckbox('Best Multiplier',ApplyBands.bestmult)
end
d.applylast = dialog.AddCheckbox('Last Wiggle',ApplyBands.last)
d.start = dialog.AddButton('Begin',1)
d.cancel = dialog.AddButton('Cancel',0)
d.about = dialog.AddButton('About',2)
d.advanced = dialog.AddButton("Advanced",3)
local rc
repeat
rc = dialog.Show(d)
if rc == 0 then return 0 end -- exit immediately
-- parse fields
PushPullStrength = d.pusherstr.value
HoldStrength = d.holderstr.value
PushersNoHold = d.nohold.value
InitialLock = d.locking.value
NoPushers = d.nopushers.value
ULProb = d.rand.value / 100.
BandSidechain = d.bandsc.value
BandStrInc = d.prog.value
ApplyBands.bestscore = d.applyscore.value
ApplyBands.last = d.applylast.value
if IsExploration then
ApplyBands.bestmult = d.applymult.value
SaveBestMultiplier = d.savemult.value
end
KeepBands = d.keep.value
local boxcount = ApplyBands.bestscore and 1 or 0
boxcount = boxcount + ( ApplyBands.bestmult and 1 or 0 )
boxcount = boxcount + ( ApplyBands.last and 1 or 0 )
if HoldStrength < MinBandStr then HoldStrength = 0 end
if rc == 2 then if About() == 0 then return 0 end
elseif rc == 3 then if Advanced() == 0 then return 0 end
else -- no error checking when hitting the about button
if boxcount ~= 1 then
dialog.MessageBox("Check only ONE solution to apply the bands to!","Solution Selection Error","Okay")
rc = -1
elseif ApplyBands.bestscore then UsersFavoriteSave = BestScoreSave
elseif ApplyBands.bestmult then UsersFavoriteSave = BestMultSave
else UsersFavoriteSave = WiggleSave
end
end
until rc == 1
return rc
end
function dialog.MessageBox(msg,title,buttontext1, buttontext0, buttontext2)
title = title or ''
local d = dialog.CreateDialog(title)
if type(msg) == 'string' then d['1'] = dialog.AddLabel(msg)
else for i = 1,#msg do
d[tostring(i)] = dialog.AddLabel(msg[i])
end
end
buttontext1 = buttontext1 or 'Ok'
d.button = dialog.AddButton(buttontext1,1)
if buttontext2 then d.button2 = dialog.AddButton(buttontext2,2) end
if buttontext0 then d.button0 = dialog.AddButton(buttontext0,0) end
return dialog.Show(d)
end
function About()
local msg = {
"Creates 3 types of bands:",
" Pushers push segments that are in contact but",
" should not be",
" Pullers pull segments that should be in contact",
" but are not",
" Holders weakly hold distance, and resize each round",
"Pushers and Pullers become Holders when they are no",
"longer needed. Bands are never deleted, but become",
"as holders. Holding can be completed disabled.",
"Holders may become Pushers/Pullers again when",
"needed. Initial bands are not touched. Script",
"bands are deleted on cancel. Bands can be",
"locked, preventing them from changing type",
"in the next round. Progressive bands get",
"stronger every round until they become Holders."
}
return dialog.MessageBox(msg,"About "..Title,"Back")
end
function Advanced()
local d = dialog.CreateDialog("Advanced")
d.label1 = dialog.AddLabel("These settings are puzzle dependent. (see output)")
d.label2 = dialog.AddLabel("Range of heat values on contact map:")
d.label22 = dialog.AddLabel("(Lower values okay for small puzzles or weak contacts)")
d.slider1 = dialog.AddSlider("Min Contact:", ContactHeat, HeatmapMin, HeatmapMax, 2)
d.slider2 = dialog.AddSlider("Max Non-Ct:", NoContactHeat, HeatmapMin, HeatmapMax, 2)
d.label3 = dialog.AddLabel("Fraction of pure-pushers to make (else too many!):")
d.label32 = dialog.AddLabel(" (pattern is different every script run)")
d.slider3 = dialog.AddSlider("% banded:",PushFraction*100,0,100,0)
d.label4 = dialog.AddLabel("Fraction of pullers (if too many contacts):")
d.slider4 = dialog.AddSlider("% banded:",PullFraction*100,0,100,0) d.okay = dialog.AddButton("Back",1)
local rc
repeat
rc = dialog.Show(d)
if rc == 0 then return 0 end -- user closed box.
if d.slider1.value <= d.slider2.value then
dialog.MessageBox({"The range for contact must be less than","the range for non-contact."},"Error","Okay")
rc = -1
end
until rc == 1
ContactHeat = d.slider1.value -- upper cutoff score for contact
NoContactHeat = d.slider2.value -- lower cutoff for non-contact
PushFraction = d.slider3.value/100.0
PullFraction = d.slider4.value/100.0
return 1
end
function PickSave()
local d = dialog.CreateDialog("Pick Save To Restore")
d.bands = dialog.AddCheckbox("Keep Bands",KeepBands)
d.none = dialog.AddButton("None",0)
d.prev = dialog.AddButton("Prev",1)
-- d.energy = dialog.AddButton("Energy",2)
if IsExploration then
d.mult = dialog.AddButton("Mult",3)
end
d.score = dialog.AddButton("Score",4)
local rc = dialog.Show(d)
if rc == 1 then
save.Quickload(WiggleSave)
print("Restored previous wiggle results")
elseif rc == 3 then
save.Quickload(BestMultSave)
print("Restored best multiplier")
elseif rc == 4 then
save.Quickload(BestScoreSave)
print("Restored best score")
else
print("Keeping final state")
end
KeepBands = d.bands.value
end
-- ========================== End Dialog Menus ================
-- ========================== Begin AA Atom Database ================
fsl = fsl or {} -- make sure fsl namespace exists
fsl.atom = {}
-- Take just these 4 things if all you need is to know if the sidechain starts at 5 or 6 (tail terminal = true)
fsl.atom.atomcount = { -- shorter table just for identifying terminals
a=10, c=11, d=12, e=15, f=20, g=7, h=17, i=19, k=22, l=19,
m=17, n=14, p=15, q=17, r=24, s=11, t=14, v=16, w=24, y=21
}
-- Pass in a segment number of a cysteine
function fsl.atom._IsDisulfideBonded(iSeg)
local s = current.GetSegmentEnergySubscore(iSeg,'disulfides')
return tostring(s) ~= '-0'
end
function fsl.atom._IsTerminalTest(aa,count,disulfide)
local dbvalue = fsl.atom.atomcount[aa]
if dbvalue == nil then error('Bad argument to an fsl.atom function (not an amino acid code)') end
local diff = count - dbvalue
if disulfide then diff = diff + 1 end -- because count is one less because a H was removed
if diff == 0 then return false, false, disulfide
elseif diff == 1 then return false, true, disulfide
elseif diff == 2 then return true, false, disulfide
elseif diff == 3 then return true, true, disulfide
end
error('Strange atom count, report this!')
end
-- ========================== End AA Atom Database ================
function GetSidechainAtom(iSeg)
if structure.GetSecondaryStructure(iSeg) == 'M' then return 0 end
local aa = structure.GetAminoAcid(iSeg)
if aa == 'g' then return 0 end
local count = structure.GetAtomCount(iSeg)
local _,last = fsl.atom._IsTerminalTest(aa,count,fsl.atom._IsDisulfideBonded(iSeg))
return last and 6 or 5
end
-- Interpret the contact map for a pair
-- Return PUSHING, PULLING or HOLDING (i.e. constraint satisfied)
function PickNewState(i,j)
local ShouldContact, ShouldNotContact, IsContact = contactmap.GetHeat(i,j) >= ContactHeat, contactmap.GetHeat(i,j) <= NoContactHeat, contactmap.IsContact(i,j)
local Ambiguous = (ShouldContact and ShouldNotContact) or not (ShouldContact or ShouldNotContact)
if (ShouldContact and IsContact) or not (ShouldContact or IsContact) or Ambiguous then return HOLDING -- all okay
elseif ShouldContact then return PULLING -- too far
end
return PUSHING -- too close
end
-- saves what the state should be for the current structure
-- Creates state entry if a band is needed
-- Determine what band between i,j should be
-- Update state table for band
function SetNewState(i,j)
local index = Index(i,j)
local state = status[index]
-- V 1.1 move lock check to actual mode change code, to allow for same type-different strength push/pull
local newstate = PickNewState(i,j)
-- no existing band band, no band needed, return instead of created useless table entry
if state == nil and (newstate == HOLDING or (newstate == PUSHING and NoPushers)) then return end
if state == nil and ((newstate == PUSHING and math.random() > PushFraction) or (newstate == PULLING and math.random() > PullFraction) ) then
state = { 0, FORBIDDEN, 0, 0, FORBIDDEN }
status[index] = state
return
end
-- create a table entry for new band
if state == nil then
state = { 0, HOLDING, 0, HoldStrength, newstate }
status[index] = state
-- no new band is ever HOLDING so this forces a change count
end
if state[2] == FORBIDDEN then return end
state[5] = newstate
end
-- Do nothing if nothing to be done
-- Update band itself
-- Update counters
function SetBand(i,j)
local index = Index(i,j)
local state = status[index]
-- V 1.1 move lock check to actual mode change code, to allow for same type-different strength push/pull
-- V 1.2 state selection moved to a separate loop
-- no existing band band, no band needed, return
if state == nil then return end
local newstate = state[5]
local iBand = state[3]
if newstate == FORBIDDEN then return end
-- make sure band exists
if iBand == 0 or iBand > band.GetCount() then -- band doesn't exist in this save
local a1,a2 = 0,0
if BandSidechain then
a1 = GetSidechainAtom(i)
a2 = GetSidechainAtom(j)
end
iBand = band.AddBetweenSegments(i,j,a1,a2)
if iBand == 0 then -- we couldn't make the band, forget about it
state[index] = nil
return
end
state[3] = iBand
-- no new band is ever HOLDING so this forces a change count
end
-- update band to new state
IncCount(newstate)
if state[2] == newstate then -- no change
if newstate == HOLDING then -- update HOLDING length
band.SetGoalLength(iBand,band.GetLength(iBand))
else -- v.1.1 else increase push/pull strength
state[4] = math.min(state[4] + BandStrInc,MaxBandStr)
band.SetStrength(iBand,state[4])
end
IncCount(UNCHANGED)
elseif state[1] == 0 then-- changing to/from HOLDING and not LOCKED
state[2] = newstate
IncCount(CHANGED)
state[1] = InitialLock -- lock recently changed bands
if newstate == HOLDING then
-- Sometimes instead of holding length we disable for more motion
if (state[2] == PUSHING and PushersNoHold) or HoldStrength == 0 then
band.Disable(iBand)
else -- Switch from push/pull to a holding band
band.SetStrength(iBand,HoldStrength)
state[4] = HoldStrength
band.SetGoalLength(iBand,band.GetLength(iBand))
end
else -- switch from holding to push/pull
band.SetStrength(iBand,PushPullStrength)
-- band is a little shorter/longer than contact distance
band.SetGoalLength(iBand,ContactDist * (newstate==PUSHING and ExpandFactor or 1/ExpandFactor))
band.Enable(iBand) -- in case it was a disabled holding band
state[4] = PushPullStrength
end
else
IncCount(UNCHANGED) -- we wanted to change but it was locked
end
-- remove a lock level before returning (regardless of newstate)
if state[1] > 0 then -- locked, no changes, decrement lock count and update counters
if ULProb == 1.0 or math.random() < ULProb then state[1] = state[1] - 1 end
IncCount(LOCKED)
end
return
end
-- Wiggle until change is less than delta
function Wiggle(delta)
local sc, diff
repeat
sc = current.GetEnergyScore()
structure.WiggleAll(2)
diff = math.abs(sc-current.GetEnergyScore())
until diff < delta
end
-- print the score with and without multiplier
function ScoreReport()
if IsExploration then
print(string.format(" %12.4f (%12.4f X %5.3f )",
current.GetScore(),current.GetEnergyScore(),current.GetExplorationMultiplier()))
else
print(string.format(" %12.4f", current.GetScore()))
end
end
-- Print out percentiles of histogram of heatmap, and set dialog slider values
function HeatMapSummary()
local np = 5 -- # of percentiles
local heats = {}
for i = 1,NSeg-2 do
for j = i+2,NSeg do
h = contactmap.GetHeat(i,j)
if h > 0 then heats[#heats+1] = h end
end
end
if #heats == 0 then return false end
table.sort(heats)
local nmax = 0
for i = #heats,1,-1 do
if heats[i] < heats[#heats] then break end
nmax = nmax + 1
end
print ("Heat Map Percentiles and # of contacts (bands):")
print (" 0%",heats[1],#heats)
for i = 1,np do
print (tostring(math.floor(i/np*100)).."%",heats[math.floor(i/np*#heats)],math.max(nmax,math.floor((np-i)/np*#heats)))
end
HeatmapMin = math.max(0.0,heats[1] - 0.01) -- lowest upper bound actually
HeatmapMax = heats[#heats]
NoContactHeat = HeatmapMin -- upper cutoff for non-contact
ContactHeat = NoContactHeat + 0.01 --heats[math.floor(#heats*ContactCutoffPercentage)] -- lower cutoff score for contact
return true
end
-- ========================== Being Main ================
HaveWeWiggled = false
print(Title)
print(puzzle.GetName())
print(os.date())
ScoreReport()
print("Peformance Note: Plese close the Contact Map.")
print("It slows things down.")
if HeatMapSummary() == false then
print("Not a contact map puzzle.")
dialog.MessageBox("Not a contact puzzle.",Title,"Quit")
return
end
-- Get options
if MainMenu() == 0 then print("Cancelled.") return end
-- Print Options
tmp = {}
tmp[BestScoreSave] = "best score"
tmp[BestMultSave] = "best multiplier score"
tmp[WiggleSave] = "previous wiggle"
print()
print("Band Strengths Active:",PushPullStrength,"Holding:",HoldStrength)
print("Pushers Disabled:",NoPushers,"Pushers don't hold:",PushersNoHold)
print("Bands Lock for",InitialLock,"rounds, unlocking",ULProb*100,"% each round")
print("Band Sidechain C-beta:",BandSidechain)
print("Band Progression to Max Strength:",BandStrInc,"rounds")
print("Applying new bands to",tmp[UsersFavoriteSave])
print("Keep bands on cancel:",KeepBands)
print("Saving best multiplier score to slot 3:",SaveBestMultiplier)
print("seed:",seed)
print()
print("Resetting Recent Best to currrent")
recentbest.Save()
format = " Holding: %d Pushing: %d Pulling: %d\n Locked: %d Changed: %d Unchanged: %d"
math.randomseed(seed)
round = 1
NBands = band.GetCount() -- for exit cleanup
-- Main Band/Wiggle loop, runs forever
function WorkFunction()
local bestscore = current.GetScore()
local bestmult = current.GetExplorationMultiplier()
if SaveBestMultiplier then
save.Quicksave(BestMultSave)
end
save.Quicksave(BestScoreSave)
NBandsAtStart = band.GetCount()
while true do
print("Round",round)
Counters = {0,0,0,0,0,0}
for i = 1, NSeg do
for j = i+3, NSeg do
SetNewState(i,j) -- compute new state from results of wiggle
end
end
if not ApplyBands.last then
print("Loading",tmp[UsersFavoriteSave])
save.Quickload(UsersFavoriteSave)
end
-- Apply new state to user's save
undo.SetUndo(false)
for i = 1, NSeg do
for j = i+3, NSeg do
SetBand(i,j)
end
end
undo.SetUndo(true)
save.Quicksave(UsersFavoriteSave) -- save the new bands
print(format:format(unpack(Counters)))
-- print("Total1",Counters[1]+Counters[2]+Counters[3],"Total2",Counters[5]+Counters[6])
HaveWeWiggled = true -- for restore at end
Wiggle(WiggleDelta)
structure.ShakeSidechainsAll(1)
Wiggle(WiggleDelta)
ScoreReport()
-- Save the various bests if appropriate
if current.GetScore() > bestscore then
save.Quicksave(BestScoreSave)
bestscore = current.GetScore()
print("Saving Best Score (internally)",bestscore)
end
if IsExploration and current.GetExplorationMultiplier() > bestmult then
save.Quicksave(BestMultSave)
bestmult = current.GetExplorationMultiplier()
if SaveBestMultiplier then
save.Quicksave(3)
print("Saving Best Multiplier (in slot #3)",bestmult)
else
print("Saving Best Multiplier (internally)", bestmult)
end
end
save.Quicksave(WiggleSave) -- always save this
round = round + 1
end
end
-- Routine called on cancel or error
function OnError(err)
if err:find('Cancel') then
print('User Cancel')
-- save.Quickload(WiggleSave)
if HaveWeWiggled then PickSave() end
if not KeepBands then
if NBandsAtStart == 0 then band.DeleteAll() end
while band.GetCount() > NBands do
band.Delete(band.GetCount())
end
end
print("Top Score is in Undo - Restore Recent Best")
if SaveBestMultiplier then
print("Top Multiplier is in Slot 3")
end
else
print('Error: ',err)
end
return err
end
rc,err = xpcall(WorkFunction,OnError)
-- never returns