Icon representing a recipe

Recipe: Contact Enforcer 2.0 -- Brow42

created by brow42

Profile


Name
Contact Enforcer 2.0 -- Brow42
ID
49209
Shared with
Public
Parent
Contact Enforcer 1.3 -- Brow42
Children
Created on
June 06, 2014 at 02:32 AM UTC
Updated on
June 06, 2014 at 02:32 AM UTC
Description

Enforces constraints in Contact Map puzzles. For playing whack-a-mole on those contacts. Updated for 2014.

Best for


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

Comments


brow42 Lv 1

Contact Enforcer has been updated for the new-style Contact Map puzzles. If you were using the old recipe, you should replace it with this one immediately. The old recipe will not work at all on the new-style puzzles.

For a description and history of the recipe, please visit the page of the old recipe, linked on the right as PARENT.

Basic gist:

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.

By default, the recipe applies bands, then wiggles, then applies new bands and changes existing ones to satisfy the new contacts. Instead, you can take the results of that wiggle to apply bands to a previous best solution, either best score or best multiplier.

The advanced options page lets you change the threshold for contacts, so that fewer bands are made and only strong contacts are pulled together. By default, however, it pulls all predicted contacts together.

Changes from 1.3 to 2.0:

  • 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.

brow42 Lv 1

If the contact map has too many contacts (like puzzle 907), you can limit banding to just the strongest contacts with the threshold sliders.

If you want to keep the weaker contacts, you can use the new Puller Fraction slider, to randomly pick which contacts to band. Thanks, Bruno.

  • 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.