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
Version 1.1 Dec 18 2014 LociOiling
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 Apr 19 2015 LociOiling
* Allowed mutation of locked segments.
Version 1.3 2015/05/16 LociOiling
* loop
Version 1.4 2015/05/16 LociOiling
* shuffle order of segments each loop
Version 1.5 2015/06/10 LociOiling
* add minimum gain setting
Version 1.6 2015/06/12 LociOiling
* add "polar only" option for "ekqdhrtsn"
* add model recipe stuff
* violently restructure
Version 1.7 2015/06/22 LociOiling
* add "y" to "polar only" option
Version 2.0 2015/07/01 LociOiling
* remove timing logic
Version 2.1 2016/09/02 - 2017/09/03 LociOiling
* add option for "design rules", currently:
+ No CYS residues anywhere
+ No GLY, ALA residues in sheets
+ No GLY, ALA, SER, THR in helices
* add more complete printing of options
Version 2.2 2017/12/07 LociOiling
* add ligand option, restrict to sphere around ligand
* add standard Timo ligand logic, changed NSeg to segCnt2
Version 2.3 2019/09/27-2020/12/10 LociOiling
* add dialog for fine-tuning design rules
* revived 2020: add metric support (migrate chunquePoint to CPT)
* never released
Version 2.4 2021/01/10, 2022/07/07 LociOiling
* selection options
* update structure rules
* update to latest Ident
Version 2.5 2022/07/08 LociOiling
* fix the too tall menu thing
]]--
--
-- globals section
--
--
Recipe = "Mutate No Wiggle"
Version = "2.5"
ReVersion = Recipe .. " " .. Version
-- different classes of AAs
charged = "rkdeh"
philic = "qnst" -- excluding charged
phobic = "cmailvpg" -- excluding rings
rings = "fwy"
polars = "ekqdhrtsny" -- polars only
all_aa = charged .. philic .. phobic .. rings
--
-- string things
--
-- phobic - string containing hydrophobic AAs
-- philic - string containing hydrophilic AAs
--
-- deny - string containing disallowed AAs
-- preserve - string containing AAs to be preserved (not changed)
-- aas - string containing valid AAs according to dialog
--
deny = ""
preserve = ""
aas = ""
--
-- able tables
--
Phobes = {
c = { "cysteine", },
m = { "methionine", },
a = { "alanine", },
i = { "isoleucine", },
l = { "leucine", },
v = { "valine", },
p = { "proline", },
g = { "glycine", },
f = { "phenylalanine", },
w = { "tryptophan", },
y = { "tyrosine", },
}
Phils = {
q = { "glutamine", },
n = { "asparagine", },
s = { "serine", },
t = { "threonine", },
r = { "arginine", },
k = { "lysine", },
d = { "aspartate", },
e = { "glutamate", },
h = { "histidine", },
}
Deny = {}
Preserve = {}
AAs = {}
--
-- fed up with all these rules
--
--
-- Illegal is a list of all possible banned AAs,
-- needed to make sure the AAs come out in a
-- predictable order in the dialog
--
Illegal = { "c", "p", "g", "a", "s", "t", }
LoopRule = {
c = false,
p = true,
}
HelixRule = {
c = false,
p = true,
g = true,
a = true,
s = true,
t = true,
}
SheetRule = {
c = false,
p = true,
g = true,
a = true,
}
--
-- AANames is a table containing the user-friendly versions of each AA
--
AANames = {
w = { "tryptophan", "TRP", },
g = { "glycine", "GLY", },
a = { "alanine", "ALA", },
c = { "cysteine", "CYS", },
v = { "valine", "VAL", },
l = { "leucine", "LEU", },
i = { "isoleucine", "ILE", },
m = { "methionine", "MET", },
p = { "proline", "PRO", },
f = { "phenylalanine", "PHE", },
y = { "tyrosine", "TYR", },
r = { "arginine", "ARG", },
s = { "serine", "SER", },
t = { "threonine", "THR", },
n = { "asparagine", "ASN", },
d = { "aspartate", "ASP", },
q = { "glutamine", "GLN", },
e = { "glutamate", "GLU", },
h = { "histidine", "HIS", },
k = { "lysine", "LYS", },
}
options = {
desrules = true,
cok = true,
pok = true,
gok = true,
aok = true,
keepc = false,
keepp = false,
keepg = false,
khydro = false,
charged = true,
rings = true,
polar = false,
custom = "",
range = "",
removeaa = false,
locked = true,
repeats = 3,
maxbad = 2,
mingain = 2,
pause = 1000,
delay = 0.1,
debug = false,
}
--
-- timekeeping
--
start_time = os.time()
n_mutations = 0
speedcheck_n = 100 -- every 100 mutations (5 segments?)
speedcheck_s = 300 -- every 5 minutes
lastspeedcheck_s = start_time
lastspeedcheck_n = 0
--
-- ligand and sphere
--
segCnt = 0
segCnt2 = 0
esfera = 8.5 -- sphere size
ligmode = false -- process only segments within ligand sphere
--
-- tables of primary and secondary structure
--
originalAA = {}
originalSS = {}
selecteds = 0
frozens = 0
--
-- score reporting
--
startChunque = nil -- initial chunquePoint
--
-- end of globals section
--
function round ( x )--cut all afer 3-rd place
return x - x % 0.001
end
CPT = { -- CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--
--[[
CPT package version 0.8
For a given section of code, the CPT package tracks the time used
and the gain produced. The package provides two routines, CPT:start
and CPT:stop, to be used at the start and end of the code section.
Calls may also be nested. A second call to begin in effect creates a new
nested level.
A remix recipe is a typical example of nesting chunque point calls. The recipe
would call CPT:start somewhere near the start.
Such a recipe might remix multiple lengths. The recipe would call
CPT:start at the start of each length.
Within a length, the recipe might remix multiple ranges of segments. For each
range, the recipe would call CPT:start at the start, and CPT:stop
at the end of the remix process. The CPT:stop would report the time and gain
for that range of segments.
When all ranges of segments are remixed, the length is complete, and the recipe
would call CPT:stop. The CPT:stop would report the time and gain
for the entire length.
When all lengths are complete, the recipe would call CPT:stop one last time,
reporting the time and gain for the entire recipe.
Each CPT:start returns a "chunque", a table containing information such as
the starting time and score. The chunque is used to link calls. For example, the
chunque returned by a CPT:start call should be used on the corresponding
CPT:stop call to report results.
For nested calls, the chunque from one CPT:start call is passed to the
nested CPT:start. In the remix example, to chunque from the initial
CPT:start call is passed to the CPT:start calls for each length.
The chunques from the length-level beginChunques are in turn passed to the
beginChunques for each range of segments.
For long-running recipes, the package also prints status reports at
intervals of an hour or more. These reports show the total time elapsed
and the gain since the start of the recipe. The first CPT:start call
sets the internal "root chunque" used to calculate the reports.
See this wiki page for an explanation of how this package is constructed:
https://foldit.fandom.com/wiki/Lua_packaging_for_Foldit
Each chunque contains the following fields:
prefix - text identifying the chunque, output in messages
score - score returned by the score function
bonus - bonus of chunque, from score function
runDate - date of chunque, from os.date
runTime - time of chunque, from os.time
version history
---------------
0.5 - LociOiling - 2019/11/03 - 2019/12/08
* rename to CPT (was chunquePoint)
* align with new packaging strategy
* add internal documentation
* eliminate CSV output option
* include bonus detection and tracking
* convert chunques to named fields
* remove runClock (os.clock), never used
* eliminate pose parm, always use "current"
* change external function names to "start" and "stop"
* eliminate printChunque, add fmtGain for gain formatting
* eliminate second line of start message
* include gain or loss for each condition (filter) in status reports
0.6 - LociOiling - 2019/12/11
* fixed format of condition reporting in CPT:stop
0.7 - LociOiling - 2020/02/18
* handle "MoveCountLimit" and similar binary conditions/filters
0.8a- LociOiling - 2020/11/11
* track metrics
* include metric bonus in overall score
]]--
segCnt = 0, -- segment count
repInterval = 60 * 60, -- seconds between status reports
lastStatusRep = 0, -- time of last status report
rootChunque = nil, -- chunque to use as basis for status reports
filters = false, -- whether filters active
fnames = {}, -- filter names
metrics = false, -- whether metrics active
mnames = {}, -- metric names
density = false, -- whether this is a density puzzle
--
-- start - start tracking time, points, and bonus
--
-- the start and stop functions are the external interface
--
-- parameters:
--
-- startChunque - if nil, set the internal rootChunque
-- if not nil, nested call, report the gain
-- prefix - text to identify the chunque
-- scoreFunc - (optional) function to use for score checking
--
-- calls self:newChunque to create a new chunque
--
-- returns the new chunque
--
start = function ( self, startChunque, prefix, scoreFunc )
--
-- initialize if startChunque omitted
--
if startChunque == nil then
self.segCnt = structure.GetCount ()
--
-- determine if filters apply
--
local fnames = filter.GetNames ()
if #fnames > 0 then
self.filters = true
self.fnames = fnames
end
--
-- determine if metrics apply
--
local mnames = metric.GetNames ()
if #mnames > 0 then
self.metrics = true
self.mnames = mnames
end
--
-- check a few segments to determine if this is an ED puzzle
--
if current.GetSegmentEnergySubscore ( 1, "Density" ) ~= 0
or current.GetSegmentEnergySubscore ( self.segCnt / 2, "Density" ) ~= 0
or current.GetSegmentEnergySubscore ( self.segCnt, "Density" ) ~= 0 then
self.density = true
end
end
--
-- create the chunque
--
local chunque = self:newChunque ( prefix, scoreFunc )
--
-- set the root chunque if needed
--
if startChunque == nil then
self.rootChunque = chunque
self.lastStatusRep = os.time () -- last status message
end
local sscr = self:fmtScore ( chunque )
if startChunque == nil then -- no gain yet
print ( chunque.prefix
.. ", score: " ..
sscr
.. ", " ..
chunque.runDate
)
else -- report gain relative to start chunque
local gstr, hours, pph = self:fmtGain ( startChunque, chunque )
print ( chunque.prefix
.. ", score: " ..
sscr
.. ", total gain: " ..
gstr
.. ", " ..
chunque.runDate
)
end
return chunque
end,
--
-- stop - stop tracking time, points, and bonus, then report result
--
-- startChunque - the chunque returned by the corresponding CPT:start call
-- prefix - text to identify the chunque
-- scoreFunc - (optional) function to use for score checking
--
stop = function ( self, startChunque, prefix, scoreFunc )
local chunque = self:newChunque ( prefix, scoreFunc )
self:chunqueGain ( startChunque, chunque )
--
-- if print overall progress at hourly interval
--
local Clocktime = os.difftime ( os.time (), self.lastStatusRep )
if Clocktime > self.repInterval then
self:chunqueReport ( self.rootChunque, chunque, "--- " ) -- bonus/density since start
self.lastStatusRep = os.time ()
else -- report just bonus changes in this chunque
self:prtExtra ( startChunque, chunque, prefix .. ", " )
end
end,
--
-- scorePlus - determine score and bonus from conditions/filters,
-- and also metrics,
-- used if user doesnt' provide a score function
--
-- turn filters on,
-- use current.GetEnergyScore to determine score,
-- filter.GetBonusTotal to determine bonus for conditions,
-- and metric.GetBonusTotal to determine bonus for metrics
-- restore previous status of filters
--
scorePlus = function ( self )
local filts = behavior.GetFiltersDisabled () -- remember setting
behavior.SetFiltersDisabled ( false ) -- always enable filters
local score = current.GetEnergyScore ()
local bonus = filter.GetBonusTotal ()
if bonus == nil then
bonus = 0
end
local mbonus = metric.GetBonusTotal ()
if mbonus == nil then
mbonus = 0
end
behavior.SetFiltersDisabled ( filts ) -- restore setting
--
-- include metric bonus in overall score
--
score = score + mbonus
return score, bonus, mbonus
end,
--
-- get filter scores
--
getFilters = function ( self, fnames )
local filttab = {}
for ii = 1, #fnames do
local hasbonus = filter.HasBonus ( fnames [ ii ] )
local score = nil
if hasbonus then
score = filter.GetBonus ( fnames [ ii ] )
else
score = nil
end
local satisfied = filter.ConditionSatisfied ( fnames [ ii ] )
filttab [ #filttab + 1 ] =
{ name = fnames [ ii ], hasbonus = hasbonus, score = score, satisfied = satisfied, }
end
return filttab
end,
--
-- get metric scores
--
getMetrics = function ( self, mnames )
local metrtab = {}
for ii = 1, #mnames do
local score = nil
score = metric.GetBonus ( mnames [ ii ] )
metrtab [ #metrtab + 1 ] =
{ name = mnames [ ii ], score = score, }
end
return metrtab
end,
--
-- get total density scores across all segments
--
getDensity = function ( self )
local density = 0
for ii = 1, self.segCnt do
density = density + current.GetSegmentEnergySubscore ( ii, "Density" )
end
return density
end,
--
-- newChunque - create a new chunque
--
-- used internally by CPT:start and CPT:stop
--
newChunque = function ( self, prefix, scoreFunc )
if prefix == nil then
prefix = ""
end
if scoreFunc == nil then
scoreFunc = self.scorePlus
end
local score = 0
local bonus = 0
local mbonus = 0
score, bonus, mbonus = scoreFunc ()
if score == nil then
score = 0
end
if bonus == nil then
bonus = 0
end
if mbonus == nil then
mbonus = 0
end
local runDate
local runTime
runDate = os.date ()
runTime = os.time ()
local ch = { prefix = prefix, score = score, bonus = bonus, mbonus = mbonus, runDate = runDate, runTime = runTime }
if self.filters then
ch.filters = self:getFilters ( self.fnames )
end
if self.metrics then
ch.metrics = self:getMetrics ( self.mnames )
end
if self.density then
ch.density = self:getDensity ()
end
return ch
end,
--
-- fmtScore - return formatted score, showing bonus, metrics, and ED, if applicable
--
fmtScore = function ( self, chunque )
local scout = self:round ( chunque.score )
if self.filters then
scout = scout .. " (Bonus: " .. self:round ( chunque.bonus ) .. ")"
end
if self.metrics then
scout = scout .. " (Metrics: " .. self:round ( chunque.mbonus ) .. ")"
end
if self.density then
scout = scout .. " (ED: " .. self:round ( chunque.density ) .. ")"
end
return scout
end,
--
-- fmtGain - return formatted gain, hours, and points per hour
--
fmtGain = function ( self, startChunque, stopChunque )
local Clocktime = os.difftime ( stopChunque.runTime , startChunque.runTime )
local gain = self:round ( stopChunque.score - startChunque.score )
local bgain = self:round ( stopChunque.bonus - startChunque.bonus )
local mgain = self:round ( stopChunque.mbonus - startChunque.mbonus )
local hours = Clocktime / ( 60 * 60 )
if hours == 0 then
hours = 0.0036 -- nothing happens in no time at all
end
local pph = gain / hours
local gstr = gain
if self.filters then
local bgstr = ""
if bgain > 0 then
bgstr = "+" .. bgain
elseif bgain == 0 then
bgstr = "0"
else
bgstr = tostring ( bgain )
end
gstr = gstr .. " (Bonus: " .. bgstr .. ")"
end
if self.metrics then
local mgstr = ""
if mgain > 0 then
mgstr = "+" .. mgain
elseif mgain == 0 then
mgstr = "0"
else
mgstr = tostring ( mgain )
end
gstr = gstr .. " (Metrics: " .. mgstr .. ")"
end
if self.density then
local dgain = self:round ( stopChunque.density - startChunque.density )
local dgstr = ""
if dgain > 0 then
dgstr = "+" .. dgain
elseif dgain == 0 then
dgstr = "0"
else
dgstr = tostring ( dgain )
end
gstr = gstr .. " (ED: " .. dgstr .. ")"
end
return gstr, hours, pph
end,
--
-- chunqueGain - print gain and time for a chunque
--
-- used internally
--
chunqueGain = function ( self, startChunque, stopChunque )
local sstr = self:fmtScore ( stopChunque )
local gstr, hours, pph =
self:fmtGain ( startChunque, stopChunque )
print ( stopChunque.prefix
.. ", score: " ..
sstr
.. ", gain: " ..
gstr
.. ", " ..
stopChunque.runDate
)
print ( stopChunque.prefix
.. ", hours: " ..
self:round ( hours )
.. ", points per hour: " ..
self:round ( pph )
)
end,
--
-- chunqueReport - issue periodic progress report
--
-- used internally, similar to chunqueGain
--
chunqueReport = function ( self, startChunque, stopChunque, prefix )
local gstr, hours, pph =
self:fmtGain ( startChunque, stopChunque )
local statuz =
"--- status: total hours = "
.. self:round ( hours ) ..
", gain = "
.. gstr ..
", points per hour = "
.. self:round ( pph )
if self.lastPPH ~= nil then
statuz = statuz
.. ", change = " ..
self:round ( pph - self.lastPPH )
.. " points per hour"
end
self.lastPPH = pph
print ( statuz )
self:prtExtra ( startChunque, stopChunque, "--- " )
end,
prtExtra = function ( self, startChunque, stopChunque, prefix )
if prefix == nil then
prefix = ""
end
--
-- detailed report on filters
--
if self.filters then
local chg = 0
local condstr = ""
for ii = 1, #stopChunque.filters do
if stopChunque.filters [ ii ].hasbonus then
local delta = stopChunque.filters [ ii ].score - startChunque.filters [ ii ].score
if delta ~= 0 then
chg = chg + 1
condstr = condstr .. prefix ..
"condition \'"
.. stopChunque.filters [ ii ].name ..
"\', bonus = "
.. self:round ( stopChunque.filters [ ii ].score ) ..
", change = "
.. self:round ( delta ) ..
"\n"
end
else
if stopChunque.filters [ ii ].satisfied ~= startChunque.filters [ ii ].satisfied then
chg = chg + 1
condstr = condstr .. prefix ..
"condition \'"
.. stopChunque.filters [ ii ].name ..
"\', satisfied = "
.. tostring ( stopChunque.filters [ ii ].satisfied ) ..
", previously satisfied = "
.. tostring ( startChunque.filters [ ii ].satisfied ) ..
"\n"
end
end
end
if chg > 0 then
print ( condstr )
end
end
--
-- detailed report on metrics
--
if self.metrics then
local chg = 0
local condstr = ""
for ii = 1, #stopChunque.metrics do
local delta = stopChunque.metrics [ ii ].score - startChunque.metrics [ ii ].score
if delta ~= 0 then
chg = chg + 1
condstr = condstr .. prefix ..
"metric \'"
.. stopChunque.metrics [ ii ].name ..
"\', bonus = "
.. self:round ( stopChunque.metrics [ ii ].score ) ..
", change = "
.. self:round ( delta ) ..
"\n"
end
end
if chg > 0 then
print ( condstr )
end
end
--
-- overall change in density already reported, nothing to add here
--
end,
round = function ( self, ii )
return ii - ii % 0.001
end,
} -- CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--CPT--
function Menu()
local d = dialog.CreateDialog ( ReVersion )
d [ "desrules" ] = dialog.AddCheckbox ( "Enforce design rules", options.desrules )
d [ "l2a" ] = dialog.AddLabel ( "If selected, Foldit design rules apply," )
d [ "l2c" ] = dialog.AddLabel ( "See Rules for details." )
d [ "locked" ] = dialog.AddCheckbox ( "Include locked", options.locked )
d [ "repeats" ] = dialog.AddSlider ( "Repeats", options.repeats, 1, 10, 0 )
d [ "maxbad" ] = dialog.AddSlider ( "Max reps no gain", options.maxbad, 1, 10, 0 )
d [ "mingain" ] = dialog.AddSlider ( "Min gain per rep", options.mingain, 1, 25, 0 )
d [ "debug" ] = dialog.AddCheckbox ( "Verbose output", options.debug )
d.l01 = dialog.AddLabel ( "GUI Saver options" )
d.l02 = dialog.AddLabel ( "after specified mutations, a brief pause" )
d [ "pause" ] = dialog.AddSlider ( "mutations", options.pause, 0, 1000, 0 )
d [ "delay" ] = dialog.AddSlider ( "delay length", options.delay, 0.01, 0.25, 2 )
d [ "ok" ] = dialog.AddButton ( "OK", 1 )
d [ "cancel" ] = dialog.AddButton ( "Cancel", 0 )
d [ "aaopts" ] = dialog.AddButton ( "AAs", 6 )
d [ "rules" ] = dialog.AddButton ( "Rules", 5 )
d [ "where" ] = dialog.AddButton ( "Select", 2 )
if segCnt ~= segCnt2 then
d [ "ligand" ] = dialog.AddButton ( "Ligand", 3 )
end
d [ "help" ] = dialog.AddButton ( "About", 4 )
local rc = dialog.Show(d)
for u,v in pairs(options) do
--
-- using scoping so I don't have to pass any variable names
--
-- pcall used because this method would throw errors for
-- the label entries, which don't have a .value
--
pcall ( function () options [ u ] = d [ u ].value end )
end
return rc
end
--
-- Begin New Dialog Library Functions
--
--
-- dialog.AddLabels - 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
--
-- dialog.CreateMessageBox - 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
--
-- dialog.ShowMessageBox - Display a dialog box
--
function dialog.ShowMessageBox(msg,title,buttontext1,buttontext0,buttontext2)
return dialog.Show(dialog.CreateMessageBox(msg,title,buttontext1,buttontext0,buttontext2))
end
--
-- ErrorMessage: 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 New Dialog Library Functions
--
function Rules ()
local d = dialog.CreateDialog ( ReVersion )
d [ "over0" ] = dialog.AddLabel ( "Foldit design rules" )
d [ "over1" ] = dialog.AddLabel ( "see the SS Design objective for the current puzzle" )
d [ "over9" ] = dialog.AddLabel ( "" )
d [ "L_lab" ] = dialog.AddLabel ( "Loop rules:" )
for ii = 1, #Illegal do
local kk = Illegal [ ii ]
local vv = LoopRule [ kk ]
if vv ~= nil then
d [ "L_" .. kk ] = dialog.AddCheckbox ( AANames [ kk ] [ 1 ] .. " (" .. kk .. ")", vv )
end
end
d [ "E_lab" ] = dialog.AddLabel ( "Sheet rules:" )
for ii = 1, #Illegal do
local kk = Illegal [ ii ]
local vv = SheetRule [ kk ]
if vv ~= nil then
d [ "E_" .. kk ] = dialog.AddCheckbox ( AANames [ kk ] [ 1 ] .. " (" .. kk .. ")", vv )
end
end
d [ "H_lab" ] = dialog.AddLabel ( "Helix rules:" )
for ii = 1, #Illegal do
local kk = Illegal [ ii ]
local vv = HelixRule [ kk ]
if vv ~= nil then
d [ "H_" .. kk ] = dialog.AddCheckbox ( AANames [ kk ] [ 1 ] .. " (" .. kk .. ")", vv )
end
end
d [ "capt1" ] = dialog.AddLabel ( "" )
d [ "capt2" ] = dialog.AddLabel ( "checked means allowed, unchecked not allowed" )
d.OK = dialog.AddButton ( "OK", 1 )
d.Cancel = dialog.AddButton ( "Cancel", 0 )
local rc = dialog.Show ( d )
if rc == 1 then
for kk, vv in pairs ( LoopRule ) do
LoopRule [ kk ] = d [ "L_" .. kk ].value
end
for kk, vv in pairs ( SheetRule ) do
SheetRule [ kk ] = d [ "E_" .. kk ].value
end
for kk, vv in pairs ( HelixRule ) do
HelixRule [ kk ] = d [ "H_" .. kk ].value
end
end
return 2
end
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."
}, ReVersion, "Back" )
if rc == 0 then
return 0
end -- cancel script
return 3
end
--
-- Turn selection or frozen into ranges and range string
--
function Where ()
local d = dialog.CreateDialog ( ReVersion )
dialog.AddLabels ( d,
{ "Have you selected segments to mutate with",
"the selection or freeze tool?"
} )
d.selz = dialog.AddLabel ( selecteds .. " segments selected" )
d.frzz = dialog.AddLabel ( frozens .. " segments frozen" )
d.select = dialog.AddButton ( "Selection", 1 )
d.freeze = dialog.AddButton ( "Freeze", 2 )
d.nope = dialog.AddButton ( "Nope", 3 )
-- 0 is window close (cancel script)
local rc = dialog.Show ( d )
if rc == 0 or rc == 3 then
return rc
end
local tmp = { selection.IsSelected, freeze.IsFrozen }
UserSelect = tmp [ rc ] -- pretty name because it shows up in the GUI
local appending = false
local range
local list = {}
for ii = 1, segCnt2 do
if UserSelect ( ii ) then
if appending then
range [ 2 ] = ii
else
range = { ii, ii }
list [ #list + 1 ] = range
appending = true
end
else
if appending then
appending = false
end
end
end
for ii = 1, #list do
if list [ ii ] [ 1 ] == list [ ii ] [ 2 ] then
list [ ii ] = tostring ( list [ ii ] [ 1 ] )
else
list [ ii ] = tostring ( list [ ii ] [ 1 ] ) .. "-" .. tostring ( list [ ii ] [ 2 ] )
end
end
options.range = table.concat ( list, " " )
return 2
end
--
-- special ligand mode
--
function Ligand ()
local d = dialog.CreateDialog ( ReVersion )
d.ligandseg = dialog.AddLabel ( "Ligand segment: " .. segCnt )
d.ligandmode = dialog.AddLabel ( "Mutate within sphere around the ligand" )
d.esfera = dialog.AddSlider ( "Sphere size", esfera, 2, 15, 1 )
d.ligand = dialog.AddButton ( "OK", 1 )
d.cancel = dialog.AddButton ( "Cancel", 0 )
-- 0 is window close (cancel script)
local rc = dialog.Show ( d )
if rc == 0 then
ligmode = false
return rc
end
ligmode = true
esfera = d.esfera.value
local slist = {}
for ii = 1, segCnt2 do
if structure.GetDistance ( ii, segCnt ) <= esfera then
slist [ #slist + 1 ] = true
else
slist [ #slist + 1 ] = false
end
end
local appending = false
local range
local list = {}
for ii = 1, #slist do
if slist [ ii ] then
if appending then
range [ 2 ] = ii
else
range = { ii, ii }
list [ #list + 1 ] = range
appending = true
end
else
if appending then
appending = false
end
end
end
for ii = 1, #list do
if list [ ii ] [ 1 ] == list [ ii ] [ 2 ] then
list [ ii ] = tostring ( list [ ii ] [ 1 ] )
else
list [ ii ] = tostring ( list [ ii ] [ 1 ] ) .. "-" .. tostring ( list [ ii ] [ 2 ] )
end
end
options.range = table.concat ( list, " " )
return 2
end
function AAOpts()
local d = dialog.CreateDialog ( ReVersion )
d [ "l1" ] = dialog.AddLabel ( "Include these as allowed AA" )
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 [ "charged" ] = dialog.AddCheckbox ( "charged AA: rkdeh", options.charged )
d [ "rings" ] = dialog.AddCheckbox ( "rings: fwy",options.rings )
d [ "polar" ] = dialog.AddCheckbox ( "polar: ekqdhrtsny (overrides above)", options.polar )
d [ "l3" ] = dialog.AddLabel ( "Custom AA list (overrides above)" )
d [ "custom" ] = dialog.AddTextbox ( "custom AAs", options.custom )
d [ "l4" ] = dialog.AddLabel ( "Don't change these AA" )
d [ "keepc" ] = dialog.AddCheckbox ( "cysteine", options.keepc )
d [ "keepp" ] = dialog.AddCheckbox ( "proline", options.keepp )
d [ "keepg" ] = dialog.AddCheckbox ( "glycine", options.keepg )
d [ "range" ] = dialog.AddTextbox ( "Where:", options.range )
d [ "khydro" ] = dialog.AddCheckbox ( "Preserve Hydrophobicity", options.khydro )
d [ "removeaa" ] = dialog.AddCheckbox ( "Remove AAs not in either list above (if possible )", options.removeaa)
d [ "ok" ] = dialog.AddButton ( "OK", 1 )
d [ "cancel" ] = dialog.AddButton ( "Cancel", 0 )
local rc = dialog.Show(d)
if rc == 1 then
for u,v in pairs(options) do
--
-- using scoping so I don't have to pass any variable names
--
-- pcall used because this method would throw errors for
-- the label entries, which don't have a .value
--
pcall ( function () options [ u ] = d [ u ].value end )
end
end
return 6
end
--
-- sort by score, also filter by mutability
--
function SortSegments ( segs )
local scores = {}
local keys = {}
for jj = 1,#segs do
local ii = segs [ jj ]
if structure.IsMutable ( ii )
and ( options.locked or not structure.IsLocked ( ii ) ) then
local ss = current.GetSegmentEnergyScore ( ii ) -
current.GetSegmentEnergySubscore ( ii, 'reference' )
scores [ ss ] = ii -- scores map to segments
keys [ #keys + 1 ] = ss -- keys are scores
end
end
table.sort(keys)
for ii = 1, #keys do
keys [ ii ] = scores [ keys [ ii ] ] -- replace score with segment
end
return keys
end
--
-- Turn a range string into a list of ranges (allows negatives, skips non-numeric)
--
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
--
-- Thanks too Rav4pl
--
function ShuffleTable(tab) --randomize order of elements
local cnt=#tab
for i=1,cnt do
local r=math.random(cnt)
tab[i],tab[r]=tab[r],tab[i]
end
return tab
end
--
-- Turn list of ranges into a list of segments
--
function RangeToList(ranges)
if #ranges == 0 then ranges = {{1,segCnt2}} end
local list = {}
local err = ""
for i = 1,#ranges do
local a,b = unpack(ranges[i])
if a < 1 or a > segCnt2 then return list,tostring(a).." is out of range." end
if b < 1 or b > segCnt2 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
function PrintSpeedCheck ( force, msgx )
if msgx == nil then
msgx = ""
end
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 ( msgx .. "mutation rate: "
.. round ( n_mutations / dt ) ..
" mutations/second" )
end
end
function DesignOK ( aa, iSeg, ss )
if ss == "L" then
local dok = LoopRule [ aa ]
if dok ~= nil and not dok then
local errmsg = "\"" .. aa .. "\"" .. "not allowed in loop"
return false, errmsg
end
elseif ss == "E" then
local dok = SheetRule [ aa ]
if dok ~= nil and not dok then
local errmsg = "\"" .. aa .. "\"" .. "not allowed in sheet"
return false, errmsg
end
elseif ss == "H" then
local dok = HelixRule [ aa ]
if dok ~= nil and not dok then
local errmsg = "\"" .. aa .. "\"" .. "not allowed in helix"
return false, errmsg
end
end
return true
end
function MNW ( order )
local gain = 0
local s = 0
local iSeg = 0
local origaa = ""
local sstruct = ""
for ii = 1, #order do
recentbest.Save ()
s = current.GetScore ()
iSeg = order [ ii ]
local segPnt = "segment "
.. iSeg ..
" ("
.. ii ..
"/"
.. #order ..
") "
if options.debug then
print ( "processing " .. segPnt )
end
--
-- string things
--
-- phobic - string containing hydrophobic AAs
-- philic - string containing hydrophilic AAs
--
-- deny - string containing disallowed AAs
-- preserve - string containing AAs to be preserved (not changed)
-- aas - string containing valid AAs according to dialog
--
origaa = originalAA [ iSeg ]
sstruct = originalSS [ iSeg ]
local skipper = nil
if Preserve [ origaa ] ~= nil then
skipper = AANames [ origaa ] [ 1 ] .. " on preserve list"
end
if skipper == nil then
local first_mutation = true -- first mutation this segment
--
-- try each aa in the allowed list
--
for aa, aan in pairs ( AAs ) do
if options.debug then
print ( "trying mutation to " .. AANames [ aa ] [ 1 ] )
end
local skipaa = nil
if Deny [ aa ] ~= nil then
skipaa = "on deny list"
end
if skipaa == nil and options.khydro then
if structure.IsHydrophobic ( iSeg ) then
if Phobes [ aa ] == nil then
skipaa = "not hydrophobic to preserve " .. AANames [ origaa ] [ 1 ]
end
else
if Phils [ aa ] == nil then
skipaa = "not hydrophilic to preserve " .. AANames [ origaa ] [ 1 ]
end
end
end
if skipaa == nil and options.desrules then
local good, errmsg = DesignOK ( aa, iSeg, sstruct )
if not good then
skipaa = errmsg
end
end
--
-- if checks ok, mutate to this AA
--
if skipaa == nil then
structure.SetAminoAcid ( iSeg, aa )
n_mutations = n_mutations + 1
if first_mutation
and options.removeaa == true
and ( Deny [ origaa ] or Deny [ origaa ] == nil ) then
--
-- forget the original score of the unwanted aa, use this one
--
recentbest.Save ()
end
first_mutation = false
else
if options.debug then
print ( segPnt ..
"mutation to "
.. AANames [ aa ] [ 1 ] ..
" skipped, "
.. skipaa )
end
end
--[[
if options.delay > 0
and options.pause > 0
and n_mutations > 0
and n_mutations % options.pause == 0 then
t = os.clock()
repeat
dt = os.clock() - t -- watch out, clock() can wrap to 0
until dt > options.delay or dt < 0
end
PrintSpeedCheck ( false, segPnt )
]]--
end
recentbest.Restore()
gain = current.GetScore () - s
local newaa = structure.GetAminoAcid ( iSeg )
if gain > 0 then
gainstr = round ( gain )
if tonumber ( gainstr ) == 0 then
gainstr = "tiny"
end
print ( segPnt ..
"\'"
.. origaa ..
"\' -> \'"
.. newaa ..
"\', change = "
.. gainstr )
originalAA [ iSeg ] = newaa
end
else
if options.debug then
print ( segPnt .. "skipped, " .. skipper )
end
end
--
-- test: move the speedcheck to the end of the segment
--
if options.delay > 0
and options.pause > 0
and n_mutations > 0
and n_mutations % options.pause == 0 then
t = os.clock()
repeat
dt = os.clock() - t -- watch out, clock() can wrap to 0
until dt > options.delay or dt < 0
end
PrintSpeedCheck ( false, segPnt )
end
end
function GetOptions ()
repeat
rc = Menu()
if rc == 0 then
return rc
end
segs, err = RangeToList ( ParseRange ( options.range ) )
if err ~= "" then
print ( "bad range" )
ErrorMessage ( err, "Range Input" )
rc = -1
end
if options.custom ~= ""
and options.custom:find ( "[^" .. all_aa..all_aa:upper () .. "]" ) then
print ( "bad aa code" )
ErrorMessage ( "Invalid amino acid code specified." )
rc = -1
end
if rc == 2 then
rc = Where ()
end
if rc == 3 then
rc = Ligand ()
end
if rc == 4 then
rc = About ()
end
if rc == 5 then
rc = Rules ()
end
if rc == 6 then
rc = AAOpts ()
end
until rc == 1
return rc
end
function LigandCheck ()
segCnt = structure.GetCount()
segCnt2 = segCnt
while structure.GetSecondaryStructure ( segCnt2 ) == "M" do
segCnt2 = segCnt2 - 1
end
print ( "initial segment count: " .. segCnt )
if segCnt ~= segCnt2 then
options.desrules = false
print ( "ligand found, adjusted segment count: " .. segCnt2 )
end
end
--
-- Ident - print identifying information at beginning and end of recipe
--
-- slugline - first line to print - normally recipe name and version
--
-- v0.2 - LociOiling - 20191118
-- + streamline the format
-- + user.GetGroupName not working, remove
-- v0.3 - LociOiling - 20201012
-- + combine user name and rank in one line
-- + don't report on group at all
--
function Ident ( slugline )
local function round ( ii )
return ii - ii % 0.001
end
print ( slugline )
print ( "Puzzle: " .. puzzle.GetName () .. " (" .. puzzle.GetPuzzleID () .. ")" )
print ( "Track: " .. ui.GetTrackName () )
--[[
user.GetGroupName not working
local gname = user.GetGroupName ()
if gname ~= nil then
gname = " (" .. gname .. ")"
else
gname = ""
end
print ( "User: " .. user.GetPlayerName () .. gname )
]]--
local luser = user.GetPlayerName ()
local scoretype = scoreboard.GetScoreType ()
local scort = ""
if scoretype == 0 then
scort = "soloist"
elseif scoretype == 1 then
scort = "evolver"
elseif scoretype == 2 then
scort = "all hands"
elseif scoretype == 3 then
scort = "no score"
else
scort = "unknown/error"
end
print ( "User: " .. luser .. " (" .. scort .. " #" .. scoreboard.GetRank ( scoretype ) .. ")" )
--[[
--
-- report group rank/score
--
local sGroup = scoreboard.GetGroupScore ()
if sGroup ~= nil then
print ( "Group rank / score: " .. scoreboard.GetGroupRank () .. " / " .. round ( 10 * ( 800 - sGroup ) ) )
end
]]--
end
function Initialize ()
LigandCheck ()
for ii = 1, segCnt do
originalAA [ #originalAA + 1 ] = structure.GetAminoAcid ( ii )
originalSS [ #originalSS + 1 ] = structure.GetSecondaryStructure ( ii )
if selection.IsSelected ( ii ) then
selecteds = selecteds + 1
end
if freeze.IsFrozen ( ii ) then
frozens = frozens + 1
end
end
end
function main ()
Ident ( ReVersion )
Initialize ()
if GetOptions () == 0 then
return
end
print ( "--" )
--
print ( "options:" )
print ( "enforce design rules: " .. tostring ( options.desrules ) )
print ( "preserve hydrophobicity: " .. tostring ( options.khydro ) )
print ( "include locked segments: " .. tostring ( options.locked ) )
if options.custom ~= "" then
print ( "custom AA list specified" )
aas = options.custom
elseif options.polar then
aas = polars
else
aas = philic .. phobic
if options.charged then
aas = aas .. charged
end
if options.rings then
aas = aas .. rings
end
end
print ( "selected " .. aas:len() .. " amino acids: " .. aas )
for jj = 1, aas:len() do
local aa = aas:sub ( jj, jj )
AAs [ aa ] = AANames [ aa ] [ 1 ]
end
if options.debug then
print ( "selected AAs" )
for uu, vv in pairs ( AAs ) do
print ( uu .. " = " .. vv )
end
end
-- okay add in the subtypes, for checking type later
philic = philic .. charged
phobic = phobic .. rings
-- build preserve and deny lists from the checkboxes
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 preserve:len() > 0 then
print ( "preserved amino acids: " .. preserve )
end
for jj = 1, preserve:len() do
local aa = preserve:sub ( jjAANames [ aa ] [ 1 ], jj )
Preserve [ aa ] = AANames [ aa ] [ 1 ]
end
if options.debug and #Preserve > 0 then
print ( "preserved AAs" )
for uu, vv in pairs ( Preserve ) do
print ( uu .. " = " .. vv )
end
end
if not options.cok then
deny = deny .. "c"
end
if not options.pok then
deny = deny .. "p"
end
if not options.gok then
deny = deny .. "g"
end
if not options.aok then
deny = deny .. "a"
end
if deny:len() > 0 then
print ( "denied amino acids:" .. deny )
end
for jj = 1, deny:len() do
local aa = deny:sub ( jj, jj )
Deny [ aa ] = AANames [ aa ] [ 1 ]
end
if options.debug and #Deny > 0 then
print ( "denied AAs" )
for uu, vv in pairs ( Deny ) do
print ( uu .. " = " .. vv )
end
end
if ligmode then
print ( "ligand mode: ligand segment " .. segCnt .. ", sphere size = " .. esfera )
end
if type ( options.range ) == "table" then
if #options.range > 0 then
print ( "segment range = " )
for ii = 1, #options.range do
if type ( options.range [ ii ] ) == "string"
or type ( options.range [ ii ] ) == "number" then
print ( options.range [ ii ] )
elseif type ( options.range [ ii ] ) == "table" then
for jj = 1, #options.range [ ii ] do
if type ( options.range [ jj ] ) == "string"
or type ( options.range [ jj ] ) == "number" then
print ( options.range [ jj ] )
end
end
end
end
end
elseif type ( options.range ) == "string" then
if options.range:len() > 0 then
print ( "segment range = " .. options.range )
end
end
print ( "number of runs: " .. options.repeats )
print ( "max runs with no gain: " .. options.maxbad )
print ( "minimum gain per run: " .. options.mingain )
print ( "verbose output: " .. tostring ( options.debug ) )
if options.delay == 0 or options.pause == 0 then
print ( "No delay between mutations." )
else
print ( "Waiting "
.. options.delay ..
" seconds every "
.. options.pause ..
" mutations" )
print ( "Maximum possible mutation rate: "
.. options.pause / options.delay ..
" mutations/second" )
end
print ( "--" )
start = current.GetScore()
local order = SortSegments ( segs )
startChunque = CPT:start ( nil, "Start" )
local badreps = 0
for ii = 1, options.repeats do
local loopstart = current.GetScore ()
local runName = "MNW run " .. ii
local begChunque = CPT:start ( startChunque, runName )
MNW ( order )
CPT:stop ( begChunque, "end " .. runName )
local loopend = current.GetScore ()
if loopend - loopstart < options.mingain then
badreps = badreps + 1
else
badreps = 0
end
if badreps >= options.maxbad then
break
end
order = ShuffleTable ( order )
end
cleanup ()
end
function cleanup ( errmsg )
if CLEANUPENTRY ~= nil then
return
end
CLEANUPENTRY = true
print ( "---" )
local reason
local start, stop, line, msg
if errmsg == nil then
reason = "complete"
else
--
-- thanks to Bruno K. and Jean-Bob
--
start, stop, line, msg = errmsg:find ( ":(%d+):%s()" )
if msg ~= nil then
errmsg = errmsg:sub ( msg, #errmsg )
end
if errmsg:find ( "Cancelled" ) ~= nil then
reason = "cancelled"
else
reason = "error"
end
end
Ident ( ReVersion .. " " .. reason )
if reason == "error" then
print ( "Unexpected error detected" )
print ( "Error line: " .. line )
print ( "Error: \"" .. errmsg .. "\"" )
end
if errmsg ~= nil then
recentbest.Restore()
end
behavior.SetFiltersDisabled ( false )
if startChunque ~= nil then
CPT:stop ( startChunque, "Final" )
end
PrintSpeedCheck ( true )
end
xpcall ( main, cleanup )