Code
--[[
* Mutate No Wiggle -- Brow42
* Original Author: Brow42
* Version 1.0 Sept 6 2014
* Brute-force mutator without the wiggle...for
* puzzles where wiggle is too slow.
* Also has options to restrict which amino acids
* are mutated, and to what. For example, prolines
* and glycines won't be added, and cysteines won't
* be touched.
* Automatically does all mutables from worst to best
* scoring, but range can be set manually, or via
* selection/freeze before you run the script.
* This script uses strings instead of tables for AAs.
*
* Version 1.02 -- Republished under new id
*
* Version 1.1 Oct 31 2014 Brow42
*
* Added pause to allow GUI to function. Added timing info
* Added option to remove unwanted residues (from Googol30)
* Added trap for User Cancel
*
* Version 1.1.1 May 14 2015 Brow42
*
* Allowed mutation of locked segments as requested by LociOling
*
* Version 1.2 May 18 2015 Brow 42
*
* Added options to quickly select only hydrophillics for bonding
* Added more options to select/exclude polar and aliphatic AAs
* Rearranged menus to accomodate more options
* More error checking, such as empty mutate lists
*
* Version 1.21 July 30 2015
*
* Added option to eliminate alanines
*
* Version 1.3 Nov 23 2015
*
* Added option to allow aa's in loops
* Recoded so each SS has it's own allow/deny.
*
* Version 1.3.1 Nov 30 2015
*
* Bug fix to aas array.
*
* Version 1.3.2 Jan 13 2019
* Wbertro
* Added threonine
--]]
NSeg = structure.GetCount()
title = "Brute Force Mutate No-Wiggle 1.3.1"
options = { cok=true, pok=true, gok=true, aok=true, tok=true, keepc=false, keepp=false, keepg = false, keept = false,
keephydro=false, charged=true, rings=true, custom="", range="", removeaa=false,
lockedok=true, philliconly=false, phobiconly=false, phillic=true, phobic=true,
anyloop=false}
-- Gah. All the options have to be in the dialog, so these options are not in the options table
guitext_template = "%0.2f seconds every %d mutations"
pause = 5
delay = 0.1
options["gui"] = guitext_template:format(delay,pause)
-- Menu Button Enums
OKAY=1
CANCEL=0
SELECT=2
ABOUT=3
AASELECT=4
CLEAR=5
ALL=6
REPEAT=-1
function MainMenu()
local d = dialog.CreateDialog(title)
if options.custom == '' then
d["label1"] = dialog.AddLabel("AA Selection shortcuts (or use button below).")
d["philliconly"] = dialog.AddCheckbox("Hydrophilic (blue) only (no phobics).",options.philliconly)
d["phobiconly"] = dialog.AddCheckbox("Hydrophobic (orange) only (no phillics).",options.phobiconly)
else
d["label1"] = dialog.AddLabel("AAs have been selected on the AA Allowed page.")
end
d["lable2"] = dialog.AddLabel("Don't change these AAs:")
d["keepc"] = dialog.AddCheckbox("cysteine",options.keepc)
d["keepp"] = dialog.AddCheckbox("proline",options.keepp)
d["keepg"] = dialog.AddCheckbox("glycine",options.keepg)
d["keept"] = dialog.AddCheckbox("threonine",options.keept)
d["range"] = dialog.AddTextbox("Where:",options.range)
d["removeaa"] = dialog.AddCheckbox("Remove AAs not marked allowed or don't change",options.removeaa)
d["keephydro"] = dialog.AddCheckbox("Preserve Hydrophobicity",options.keephydro)
d["gui"] = dialog.AddTextbox("GUI Saver: Wait",options.gui)
d["ok"] = dialog.AddButton("OK",OKAY)
d["cancel"] = dialog.AddButton("Cancel",CANCEL)
d["where"] = dialog.AddButton("Select",SELECT)
d["aaselect"] = dialog.AddButton("Allow AA",AASELECT)
local rc = dialog.Show(d)
for u,v in pairs(options) do
pcall(function() options[u] = d[u].value end) -- using scoping so I don't have to pass any variable names
end
if options.philliconly and options.phobiconly then
ErrorMessage("Don't check both phillic-only and phobic-only!","AA Selection")
rc = REPEAT
end
segs,err = RangeToList(ParseRange(options.range))
if err ~= "" then ErrorMessage(err,"Range Input") rc = REPEAT end
err = ParseWait(options.gui)
if err then ErrorMessage(err,"GUI Saver Delay Parameters") rc = REPEAT end
-- change how allowed AA panel looks
if options.philliconly then
options.cok = false
options.pok = false
options.gok = false
options.aok = false
options.tok = false
options.phobic = false
options.rings = false
end
if options.phobiconly then
options.charged = false
options.phillic = false
end
return rc, MainMenu
end
function AASelect()
local d = dialog.CreateDialog(title)
if options.custom == '' then
d["label1"] = dialog.AddLabel("Include these as allowed AA (previous page overrides):")
d["cok"] = dialog.AddCheckbox("cysteine",options.cok)
d["pok"] = dialog.AddCheckbox("proline",options.pok)
d["gok"] = dialog.AddCheckbox("glycine",options.gok)
d["aok"] = dialog.AddCheckbox("alanine",options.aok)
d["tok"] = dialog.AddCheckbox("threonine",options.tok)
d["anyloop"] = dialog.AddCheckbox("But allow ala, pro, gly in loops anyways",options.anyloop)
d["charged"] = dialog.AddCheckbox("charged hydrophillics",options.charged)
d["phillic"] = dialog.AddCheckbox("other (polar) hydrophillics",options.phillic)
d["rings"] = dialog.AddCheckbox("rings (aromatic) hydrophobics: phen, tryp, tyr",options.rings)
d["phobic"] = dialog.AddCheckbox("other (aliphatic, branched, not c,p,g) hydrophobics", options.phobic)
d["label2"] = dialog.AddLabel("Or, specify AA (overrides all checkboxes):")
else
dialog.AddLabels(d,{"AAs have been selected below.","Clear to see options again."})
end
d["custom"] = dialog.AddTextbox("AAs",options.custom)
d["ok"] = dialog.AddButton("Accept",REPEAT)
if options.custom ~= '' then
d["clear"] = dialog.AddButton("Clear",CLEAR)
else
d["all"] = dialog.AddButton("Allow All",ALL)
end
d["help"] = dialog.AddButton("About",ABOUT)
local rc = dialog.Show(d)
if rc == CANCEL then return rc end
if rc == CLEAR then
options.custom = ''
return REPEAT, AASelect
end
if rc == ALL then
options.cok = true
options.pok = true
options.gok = true
options.aok = true
options.tok = true
options.charged = true
options.rings = true
options.phillic = true
options.phobic = true
options.phobiconly = false
options.philliconly = false
return REPEAT, MainMenu
end
for u,v in pairs(options) do
pcall(function() options[u] = d[u].value end) -- using scoping so I don't have to pass any variable names
end
if options.custom ~= "" and options.custom:find("[^"..all_aa..all_aa:upper().."]") then
ErrorMessage("You gave something other than one of the 20 AAs.")
return REPEAT, AASelect
end
return rc, MainMenu
end
-- ================================== Begin New Dialog Library Functions
-- Add a wall of text from a table
function dialog.AddLabels(d,msg,nlabels) -- pass in # of existing autolabels
local nlabels = nlabels or #(d._Order or {}) -- default, valid if never delete dialog elements
if type(msg) == 'string' then
msg = { msg }
end
for i = 1,#msg do
d['autolabel'..tostring(i+nlabels)] = dialog.AddLabel(msg[i])
end
end
-- Create but don't display a wall of text and 1 or 2 buttons
function dialog.CreateMessageBox(msg,title,buttontext1,buttontext0,buttontext2)
title = title or ''
local d = dialog.CreateDialog(title)
dialog.AddLabels(d,msg)
buttontext1 = buttontext1 or 'Ok'
d.button = dialog.AddButton(buttontext1,1)
if buttontext2 ~= nil then d.button2 = dialog.AddButton(buttontext2,2) end
if buttontext0 ~= nil then d.button0 = dialog.AddButton(buttontext0,0) end
return d
end
-- Display a dialog box
function dialog.ShowMessageBox(msg,title,buttontext1,buttontext0,buttontext2)
return dialog.Show(dialog.CreateMessageBox(msg,title,buttontext1,buttontext0,buttontext2))
end
-- Display a box AND print to the output
function ErrorMessage(msg,title)
if type(msg) == 'string' then
msg = { msg }
end
for i = 1,#msg do
print(msg[i])
end
return dialog.ShowMessageBox(msg,title)
end
-- ======================================= End Dialog Functions
function About()
local rc = dialog.ShowMessageBox( {
"This recipe mutates all mutables to all amino acids",
"keeping the best score for each segment. It does this",
"from worst score to best score. NO WIGGLE IS DONE.",
"",
"Skipping wiggle allows this to work on puzzles with",
"very slow wiggle, but probably won't find the same",
"combination as one of the regular mutation scripts.",
"",
"You can control which segments are mutated, and to",
"what types of amino acids. In particular, you can",
"prevent prolines and glycines from being added. You",
"can also prevent cysteines from being mutated to save",
"bridges. You can restrict mutations to the same",
"hydrophobicity."
},title,"Back")
if rc == CANCEL then return CANCEL end -- cancel script
return REPEAT, AASelect
end
-- Turn selection or frozen into ranges and range string
RETURN=REPEAT
function Where()
local d = dialog.CreateDialog(title)
dialog.AddLabels(d,
{"Have you selected segments to muate with","the selection or freeze tool?"})
d.select = dialog.AddButton("Selection",1)
d.freeze = dialog.AddButton("Freeze",2)
d.nope = dialog.AddButton("Nope",RETURN)
-- 0 is window close (cancel script)
local rc = dialog.Show(d)
if rc == CANCEL or rc == RETURN then return rc, MainMenu end
local tmp = {selection.IsSelected, freeze.IsFrozen}
UserSelect = tmp[rc] -- pretty name because it shows up in the GUI
local appending = false
local s
local range
local list = {}
for i = 1, NSeg do
s = UserSelect(i)
if s then
if appending then range[2] = i
else
range = {i,i}
list[#list+1] = range
appending = true
end
else
if appending then appending = false end
end
end
for i = 1,#list do
if list[i][1] == list[i][2] then list[i] = tostring(list[i][1])
else list[i] = tostring(list[i][1]).."-"..tostring(list[i][2]) end
end
options.range = table.concat(list," ")
return REPEAT, MainMenu
end
-- sort by score, also filter by mutability
function SortSegments(segs)
local scores = {}
local keys = {}
for j = 1,#segs do
i = segs[j]
ss = structure.GetSecondaryStructure(i):upper()
if structure.IsMutable(i) and (options.lockedok or not structure.IsLocked(i) and
( ss=='E' or ss=='H' or ss=='L') ) then
s = current.GetSegmentEnergyScore(i) - current.GetSegmentEnergySubscore(i,'reference')
scores[s] = i -- scores map to segments
keys[#keys+1] = s -- keys are scores
end
end
table.sort(keys)
for i = 1,#keys do
keys[i] = scores[keys[i]] -- replace score with segment
end
return keys
end
-- Turn a range string into a list of ranges (allows negatives, skips non-numerica)
function ParseRange(str)
local ranges = {}
local index
while true do
local a,b,x = str:find("(%-?%d+)") -- next number
local c,d,y,z = str:find("(%d+)%s*%-%s*(%d+)") -- next range
if a and ( c == nil or a < c ) then
x = tonumber(x)
ranges[#ranges+1] = { x, x }
index = b+1
elseif c then
y = tonumber(y)
z = tonumber(z)
ranges[#ranges+1] = { y, z }
index = d+1
else
break
end
str = str:sub(index+1,str:len())
end
return ranges
end
-- Parse the GUI Saver text options
function ParseWait(str)
local numpat = "(%-?%d*%.?%d*)"
local notnumpat = "[^%d-.]*"
local _,_,a,b = str:find(notnumpat..numpat..notnumpat..numpat)
if options.gui == '' then
pause,delay = 0,0
return
end
options["gui"] = guitext_template:format(delay,pause)
local errstate = a == '' or a == nil or b == '' or b == nil
if errstate ~= false then return
{"To give the GUI a chance to draw, you can wait a",
"certain amount of time after a fixed number of",
"mutations. Just specify either 0 seconds or 0 mutations",
"to go as fast as possible."
}
end
a,b = tonumber(a),tonumber(b) -- doesn't seem like this can crash on any input matching numpat
errstate = a == '' or a == nil or b == '' or b == nil -- Something a little goofy having to check this twice
if errstate ~= false then
return
{"To give the GUI a chance to draw, you can wait a",
"certain amount of time after a fixed number of",
"mutations. Just specify either 0 seconds or 0 mutations",
"to go as fast as possible."
}
end
if a < 0 or b < 0 then
return {"Looks like you entered a negative number.","Zero or greater only please."}
end
if a < 0.01 and b > 0 then
return {"Did I say zero? Actually, 0.01 seconds is the","shortest delay."}
end
-- all good
pause,delay = b,a
options["gui"] = guitext_template:format(delay,pause)
return
end
-- Turn list of ranges into a list of segments
function RangeToList(ranges)
if #ranges == 0 then ranges = {{1,NSeg}} end
local list = {}
local err = ""
for i = 1,#ranges do
local a,b = unpack(ranges[i])
if a < 1 or a > NSeg then return list,tostring(a).." is out of range." end
if b < 1 or b > NSeg then return list,tostring(b).." is out of range." end
for j = a,b do list[j] = true end
end
local list2 = {}
for u,v in pairs(list) do list2[#list2+1] = u end
table.sort(list2)
return list2,err
end
-- =================== Begin Main ==========================
print(title)
print (puzzle.GetName(),current.GetScore())
-- different classes of AAs
charged = "rkdeh"
philic = "qnst" -- excluding charged
phobic = "cmailvpg" -- excluding rings
rings = "fwy"
all_aa = charged..philic..phobic..rings
Menu = MainMenu -- user will see variable name
aas = ''
while aas == '' do
repeat
rc,Menu = Menu()
if rc == CANCEL then print ("User exit.") return end
if rc == SELECT then Menu = Where end
if rc == ABOUT then Menu = About end
if rc == AASELECT then Menu = AASelect end
if rc == CANCEL then return end -- cancels from child dialogs
until rc == OKAY
if options.custom ~= "" then
aas = options.custom
else
if options.phillic and not options.phobiconly then aas = aas..philic end
if options.phobic and not options.philliconly then aas = aas..phobic end
if options.charged and not options.phobiconly then aas = aas..charged end
if options.rings and not options.philliconly then aas = aas..rings end
if not options.cok then aas = aas:gsub('c','') end
if not options.pok then aas = aas:gsub('p','') end
if not options.gok then aas = aas:gsub('g','') end
if not options.aok then aas = aas:gsub('a','') end
if not options.tok then aas = aas:gsub('t','') end
end
if aas == '' then
ErrorMessage("You've excluded all the AAs from your mutation list!","AA Selection")
end
end
aas = { H= aas, E=aas, L=aas }
if options.custom == '' and options.anyloop then
print ("Re-enabling p,g,a for loops if they were disabled in general...")
if not options.pok then aas['L'] = aas['L']..'p' end
if not options.gok then aas['L'] = aas['L']..'g' end
if not options.aok then aas['L'] = aas['L']..'a' end
if not options.tok then aas['L'] = aas['L']..'t' end
end
if delay == 0 or pause == 0 then
print ("No delay between mutations.")
else
print ("Waiting "..options.gui)
print ("Maximum possible mutation rate:",pause/delay,"Mutations/second")
end
print("Allowed AAs:")
print (" Helix:",aas['H']:upper())
print ("Sheet:",aas['E']:upper())
print (" Loop:",aas['L']:upper())
-- okay add in the subtypes, for checking type later
philic = philic..charged
phobic = phobic..rings
-- build preserve list from the checkboxes
preserve = ""
if options.keepc then preserve = preserve.."c" end
if options.keepp then preserve = preserve.."p" end
if options.keepg then preserve = preserve.."g" end
if options.keept then preserve = preserve.."t" end
start = current.GetScore()
order = SortSegments(segs)
start_time = os.time()
n_mutations = 0
speedcheck_n = 100 -- every 100 mutations (5 segments?)
speedcheck_s = 120 -- every 2 minutes
lastspeedcheck_s = start_time
lastspeedcheck_n = 0
function PrintSpeedCheck(force)
local t = os.time() -- probably won't wrap around
local dt = t - start_time
if force or t-lastspeedcheck_s > speedcheck_s or n_mutations-lastspeedcheck_n > speedcheck_n then
lastspeedcheck_s = t
lastspeedcheck_n = n_mutations
print ("Average mutation rate:",n_mutations/dt,"mutations/second")
end
end
function Main()
for i = 1,#order do
recentbest.Save()
s = current.GetScore()
iSeg = order[i] -- Not Local
origaa = structure.GetAminoAcid(iSeg) -- Not Local
if preserve:find(origaa) == nil then -- skip AAs in keep list
first_mutation = true -- first mutation this segment
ss = structure.GetSecondaryStructure(iSeg):upper()
for j =1,aas[ss]:len() do
aa = aas[ss]:sub(j,j)
hydrook = not options.keephydro or
(structure.IsHydrophobic(iSeg) and phobic:find(aa) ~= nil) or
(not structure.IsHydrophobic(iSeg) and philic:find(aa) ~= nil)
if hydrook then -- if correct type, else skip
structure.SetAminoAcid(iSeg,aa)
n_mutations = n_mutations + 1
if first_mutation and options.removeaa == true and aas[ss]:find(origaa) == nil then -- if removing non-allowed AAs
recentbest.Save() -- forget the original score of the unwanted aa, use this one
end
first_mutation = false
end
-- GUI pause here
if delay > 0 and pause > 0 and n_mutations > 0 and n_mutations % pause == 0 then
t = os.clock()
repeat
dt = os.clock() - t -- watch out, clock() can wrap to 0
until dt > delay or dt < 0
end
PrintSpeedCheck(false)
end
recentbest.Restore()
print( i, "of", #order, "Segment #", iSeg, origaa, "->", structure.GetAminoAcid(iSeg), "change", current.GetScore()-s )
else
print( i, "of", #order, "Segment #", iSeg, origaa, "skipped" )
end
end
end
function OnError(errmsg)
if string.find(errmsg,"Cancelled") then
print("User cancel. Loading recent best.")
recentbest.Restore()
else -- it's a real error, not a cancel
print(errmsg)
end
return errmsg
end
rc, err = xpcall(Main,OnError)
if rc == true then
print("Final score",current.GetScore(), current.GetScore()-start)
PrintSpeedCheck(true)
end