Icon representing a recipe

Recipe: Random Squeezes v1.7.1

created by jeff101

Profile


Name
Random Squeezes v1.7.1
ID
45002
Shared with
Public
Parent
None
Children
None
Created on
December 18, 2012 at 19:34 PM UTC
Updated on
December 18, 2012 at 19:34 PM UTC
Description

Wiggling with random clashing importance. To squeeze improvements from any somewhat stable solution. If there is much to gain, tends to reap more than simple wiggling (with ci=1), and then keeps trying. If there is little movement, often there is a sweet spot ci that gets you out of a local minimum, and it is found after many fruitless iterations.
Supports exploration puzzles and mutable sidechains. Preserves secondary structure, bands and orignal CI.
Single-step undo.

Best for


Code


-- Random Squeezes -- 12/17/12 jeff101 adapted Random Squeezes v1.7.0 to 1.7.1 to vary ci less randomly -- last updated 100pm 12/18/12 -- Generic functions and objects function repeat_string(s, n) return n >= .5 and s..repeat_string(s, n-1) or "" end function width_of_char(c) local chars_by_len = { [0.4] = "ijl", [0.468] = "|", [0.5] = " .,;:![]Ift", [0.599] = "-()", [0.6] = "{}r", [0.7] = "*", [0.843] = "^", [0.9] = "Jcksvxyz", [1.05] = "+<>", [1.099] = "FTZ", [1.2] = "&ABEKPSVXY", [1.3] = "CDHNRUw", [1.4] = "GOQ", [1.499] = "Mm", [1.6] = "%", [1.699] = "W", [1.828] = "@", } for len,chars in pairs(chars_by_len) do if chars:find(c, nil, true) then return len end end return 1 end function width_of_string(s) local sum = 0 for i=1,s:len() do sum = sum + width_of_char(s:sub(i,i)) end return sum end alignment_spill = 0 function alignment_reset() alignment_spill = 0 end function align(text, width, left) local fill = (width - width_of_string(text)) / width_of_char(" ") + alignment_spill local spaces = math.max(0, math.floor(fill+.5)) alignment_spill = fill - spaces if alignment == "left" then return text..repeat_string(" ", spaces) elseif alignment == "right" then return repeat_string(" ", spaces)..text else return repeat_string(" ", math.floor(spaces/2))..text..repeat_string(" ", math.ceil(spaces/2)) end end function format(value, style) -- style fields: -- integer digits: number of digits before decimal point -- integer decimals: number of digits after decimal point, default 0 -- boolean left: aligned, default true for text, false for number -- boolean sign: sign even if positive, default false local decimals = style.decimals or 0 local width = (style.sign and width_of_char("+") or 0) + (style.digits+decimals) * width_of_char("0") + (decimals > 0 and width_of_char(".") or 0) if type(value) == "number" then local fmt = "%"..(style.sign and "+" or "").."."..decimals.."f" local text = string.format(fmt, value) return align(text, width, style.left and "left" or "right") elseif value == "-" then return align(value, width, "center") else return align(value or "", width, "left") end end function is_flexible_segment(si) if freeze.IsFrozen(si) then return nil -- might be flexible but frozen by user else freeze.Freeze(si, true, false) if freeze.IsFrozen(si) then freeze.Unfreeze(si, true, false) return true -- flexible else return false -- intrisically frozen (as in ligand puzzles) end end end function has_flexible_backbone() -- If we test every segment until we find one flexible, -- we rapidly flush the undo buffer in ligand puzzles. -- So only test the last segment and make assumptions. local f = is_flexible_segment(structure.GetCount()) if f then -- At least one is flexible, plain and simple. return true elseif f == nil then -- One segment is frozen by user; assume that there are -- other segments around not frozen by user and not -- Intrinsically frozen. return false else -- intrinsically frozen segments tend to be numbered first; -- so if the last one is Intrinsically frozen, assume all are. return false end end function has_mutable_sidechain() for si = 1, structure.GetCount() do if structure.IsMutable(si) then return true end end return false end band_mgr = { bands_enabled={}, num_bands_enabled=0, num_bands_disabled=0 } for bi = 1, band.GetCount() do e = band.IsEnabled(bi) band_mgr.bands_enabled[bi] = e band_mgr.num_bands_enabled = band_mgr.num_bands_enabled + (e and 1 or 0) band_mgr.num_bands_disabled = band_mgr.num_bands_disabled + (e and 0 or 1) end function band_mgr.is_any_enabled() for bi = 1, band.GetCount() do if band.IsEnabled(bi) then return true end end return false end function band_mgr.set_enabled(enabled) if band_mgr.is_any_enabled() ~= enabled then if enabled then band_mgr.restore_bands() else band.DisableAll() end end end function band_mgr.restore_bands() if band_mgr.num_bands_enabled > band_mgr.num_bands_disabled then band.EnableAll() for bi, e in pairs(band_mgr.bands_enabled) do if not e then band.Disable(bi) end end else for bi, e in pairs(band_mgr.bands_enabled) do if e then band.Enable(bi) end end end end -- Constants min_worthy_score = 1e-4 bottom_score = -1e5 slot_mission, slot_dive, slot_wiggle = 1, 2, 3 buckets = 2 time_width = 4 style_iter = { digits=3 } style_ci = { digits=1, decimals=6 } style_depth = { digits=1 } style_em = { digits=1, decimals=5 } style_score = { digits=5, decimals=3 } style_gain = { digits=4, sign=true } -- decimals determined by state of desparation style_relative_score = { digits=5, decimals=4, sign=true } -- Main initial_CI = behavior.GetClashImportance() is_exploration = current.GetExplorationMultiplier() > 0 is_mutable = has_mutable_sidechain() print('Running Random Squeezes v1.7.1 at '..(os.date())..'\n on puzzle '..(puzzle.GetName())..'\n with Puzzle ID '..(puzzle.GetPuzzleID())..', '..(structure.GetCount())..' residues, and '..(band.GetCount())..' bands:\n ') dbox = dialog.CreateDialog("Mode of operation") dbox.minci = dialog.AddSlider("Min CI", 1e-2, 0, 1, 2) dbox.maxpw = dialog.AddSlider("Max D", 1, 1, 9, 0) dbox.labl1 = dialog.AddLabel("D = depth = maximum #iterations of perturbing wiggle\n i.e. wiggle with ci < 1.") dbox.shake = dialog.AddCheckbox((is_mutable and "Mutate" or "Shake").." after substantial gain (otherwise never)", true) if is_exploration then dbox.embound = dialog.AddCheckbox("Reject lower exploration multiplier", true) end if band_mgr.num_bands_enabled > 0 then dbox.bands_down = dialog.AddCheckbox("Respect bands while perturbing", true) dbox.bands_up = dialog.AddCheckbox("Respect bands while recovering", true) end dbox.cibins = dialog.AddCheckbox("Vary ci less randomly", false) dbox.basebin = dialog.AddSlider("Base Bin", 2, 2, 10, 0) dbox.labl2 = dialog.AddLabel("Base Bin = first # of bins when vary ci less randomly.") dbox.verbose = dialog.AddCheckbox("Give more detailed Recipe Output", false) --dbox.model = dialog.AddLabel("Happy with gain:") dbox.mode1 = dialog.AddButton("Normal", 0) dbox.mode2 = dialog.AddButton("Desperate", 3) dbox.cancl = dialog.AddButton("Cancel", -99) style_gain.decimals = dialog.Show(dbox) if style_gain.decimals == -99 then return end if dbox.cibins.value then print("Below will vary ci less randomly than usual.\n ") else print("Below will vary ci the usual way.\n ") end enable_bands_down = band_mgr.num_bands_enabled > 0 and dbox.bands_down.value enable_bands_up = band_mgr.num_bands_enabled > 0 and dbox.bands_up.value minem = is_exploration and dbox.embound.value and current.GetExplorationMultiplier() or 0 initial_best_score = absolutebest.GetScore() score_granting_repeat = 10 ^ -style_gain.decimals score_granting_shake = math.max(10 * score_granting_repeat, 1) initial_score = current.GetScore() bottom_ci = dbox.minci.value initial_time = os.time() iteration = 0 function header() local time = align("Time", time_width) local iter = format("Iter", style_iter) local ci = format("CI", style_ci) local depth = dbox.maxpw.value > 1 and " "..format("D", style_depth) or "" local em = is_exploration and " "..format("Stability", style_em) or "" local gain = format("Gain", style_gain) print(time.." "..iter.." "..ci..depth..em.." "..gain.."Score versus track best") report(0, "-", 0, 0, "") end function report(iteration, ci, depth, gain, tip) local last_reported_time = os.time() local minutes = os.difftime(last_reported_time, initial_time) / 60 local time = align(string.format("%i", minutes/60)..":"..string.format("%02i", minutes%60), time_width) local iter = format(iteration, style_iter) ci = format(ci, style_ci) depth = dbox.maxpw.value > 1 and " "..format(depth, style_depth) or "" if gain ~= 0 then gain = format(gain, style_gain) else gain = format("-", { digits=style_gain.digits, decimals=style_gain.decimals, center=true}) end local score = current.GetScore() local relative_score = score - initial_best_score local em = is_exploration and " "..format(current.GetExplorationMultiplier(), style_em) or "" alignment_reset() print(time.." "..iter.." "..ci..depth..em.." "..format(gain, style_gain).." "..format(trunc3(score), style_score)..format(relative_score, style_relative_score).." "..tip) end dive = {} function dive.start(ci, depth) save.Quicksave(slot_dive) dive.ref_score = current.GetScore() dive.ci = ci dive.depth = depth end function dive.abort() recentbest.Restore() end recentbest.Save() function dive.wiggle_up(bands_enabled) band_mgr.set_enabled(bands_enabled) local score = current.GetScore() --save.Quicksave(slot_wiggle) local min_win = min_worthy_score if score < dive.ref_score then min_win = dive.ref_score - score end local its_per_depth = { 1, 3, 9, 27 } local its_divisor = 32 -- should be less than sum of its_per_depth local depth = 0 local prev_score repeat depth = depth + 1 prev_score = score structure.WiggleAll(its_per_depth[depth], true, true) local score1 = current.GetScore() recentbest.Restore() local score2 = current.GetScore() if score2 > prev_score then score = score2 else score = score1 --save.Quickload(slot_wiggle) band_mgr.set_enabled(bands_enabled) end until depth == #its_per_depth or score < prev_score + min_win * its_per_depth[depth] / its_divisor end function dive.finish() if dive.ref_score ~= nil then local em = current.GetExplorationMultiplier() local gain = current.GetScore() - dive.ref_score local tip if em < minem or gain < 0 then tip = ":(" save.Quickload(slot_dive) elseif gain == 0 then tip = ":|" else tip = ":)" save.Quicksave(slot_dive) minem = em end report(iteration, dive.ci, dive.depth, gain, tip) dive.ref_score = nil return gain end end -- below will show the sequence and secondary structure function showss() local j,aa,ss,j1s,j10s local numstr=" " local aastr="AAs=" local ssstr="SSs=" local tot=structure.GetCount() print('Puzzle has '..tot..' residues with AAs and SSs as below:') for j=1,tot do aa=structure.GetAminoAcid(j) ss=structure.GetSecondaryStructure(j) j10s=math.floor(j/10) j1s=j-10*j10s numstr=(numstr..j1s) aastr=(aastr..aa) ssstr=(ssstr..ss) if j1s==0 and j<tot then numstr=(numstr..' ') aastr=(aastr..' ') -- add blank space after every 10th one ssstr=(ssstr..' ') end -- if j end -- for j print(numstr) print(aastr) print(ssstr) end -- showss() -- below rounds val down to nearest 0.001 function trunc3(val) return val-(val % 0.001) end -- trunc3() -- below rounds to 2 decimal places function round2(numi) local numo=round(numi*100)/100 return numo end -- round2() -- below rounds val to the nearest integer to get res function round(val) local res=math.floor(val) if val-res>=0.5 then res=res+1 end -- if val return res end -- round() -- below returns an array named res of length dim with all elements equal to 0 function zeros(dim) local res = {} local num for num=1, dim do res[num] = 0 end -- for num return res end -- function zeros -- below determines what bin (binval) a certain ci value (valci) lies in function getbin(valci, min_ci, ncibins, pflag) local binval = math.ceil(ncibins * (valci - min_ci) / (1 - min_ci)) -- ideally binval is always from 1 to ncibins, but what if valci==min_ci or valci==1 ? if binval<1 then binval=1 end if binval>ncibins then binval=ncibins end if pflag==1 and dbox.verbose.value then print('ci='..format(valci, style_ci)..' is in bin '..binval..' of '..ncibins..' for ci='..min_ci..' to 1.') end -- if pflag return binval end -- function getbin -- below lists how well present conformation obeys user-supplied bands function listdiff() local nbands, lens, glens, strs, diffs, ison, tots, avgdiff, rmsdiff, btype, mindiff, maxdiff local maxstr, minstr, num, str, diff, diff2, indx, btype, typenams nbands=band.GetCount() lens={} glens={} strs={} diffs={} ison={} tots={} avgdiff={} rmsdiff={} for btype=0,2 do avgdiff[btype]=0 rmsdiff[btype]=0 tots[btype]=0 end -- for btype mindiff={} maxdiff={} maxstr={} minstr={} for num=1,nbands do ison[num]=band.IsEnabled(num) lens[num]=band.GetLength(num) glens[num]=band.GetGoalLength(num) str=band.GetStrength(num) strs[num]=str diff=math.abs(lens[num]-glens[num]) diffs[num]=diff diff2=diff*diff if ison[num]==true then btypes={1,2} -- for enabled bands else btypes={0,2} -- for disabled bands end -- if ison for indx=1,2 do btype=btypes[indx] avgdiff[btype]=avgdiff[btype]+diff rmsdiff[btype]=rmsdiff[btype]+diff2 tots[btype]=tots[btype]+1 if tots[btype]==1 then mindiff[btype]=diff maxdiff[btype]=diff maxstr[btype]=str minstr[btype]=str else if diff>maxdiff[btype] then maxdiff[btype]=diff elseif diff<mindiff[btype] then mindiff[btype]=diff end if str>maxstr[btype] then maxstr[btype]=str elseif str<minstr[btype] then minstr[btype]=str end end -- if tots end -- for indx end -- for num for btype=0,2 do avgdiff[btype]=avgdiff[btype]/tots[btype] rmsdiff[btype]=math.sqrt(rmsdiff[btype]/tots[btype]) end print('For '..nbands..' user-supplied bands and score '..trunc3(current.GetScore())..' we have:') typenams={} typenams[0]='disabled' typenams[1]=' enabled' typenams[2]='any-type' for btype=0,2 do if tots[btype]>0 then print(' '..tots[btype]..' '..typenams[btype]..' bands with diffs='..round2(mindiff[btype])..'-'..round2(maxdiff[btype])..', avgdiff='..round2(avgdiff[btype])..', rmsdiff='..round2(rmsdiff[btype])..', and strengths='..round2(minstr[btype])..'-'..round2(maxstr[btype])..'.') end -- if tots end -- for btype end -- listdiff() math.randomseed(os.time()) save.Quicksave(slot_mission) save.Quicksave(slot_dive) -- save.SaveSecondaryStructure() mission = {} function mission.finalize(why) dive.abort() -- needed on error or cancel band_mgr.restore_bands() -- save.LoadSecondaryStructure() behavior.SetClashImportance(initial_CI) local win = current.GetScore() - initial_score if win > 0 then -- push single step undo solutions save.Quicksave(slot_dive) save.Quickload(slot_mission) save.Quickload(slot_dive) print(why..string.format(". Gained %.6f over initial %.3f", win, trunc3(initial_score))) else print(why..". Sorry, couldn't help...") save.Quickload(slot_mission) -- different if bands were disabled end end function mission.Main() if dbox.verbose.value then showss() print(' ') if band.GetCount()>0 then listdiff() print(' ') end end header() if disable_bands then band.DisableAll() end local last_shaken_score = initial_score local min_ci = bottom_ci local fruitless_iterations = 0 local bucket = 1 local ncibins, cibindone, oldci, noldci, isnewciok, ci, cibin, oldcinum repeat -- start of main loop -- below picks next ci value to use if dbox.cibins.value then -- vary ci less randomly if fruitless_iterations == 0 then ncibins = dbox.basebin.value -- the number of bins to break ci's range into -- above makes ncibins be dbox.basebin.value raised to some power cibindone = zeros(ncibins) -- cibindone is 0 if bin hasn't been done, 1 if it has oldci = {} -- this will list the ci's used since the last gain in score noldci = 0 -- the number of ci's used since last gain in score end -- if fruitless_iterations isnewciok = 0 repeat -- below tries ci at random until it finds one in a range not done yet ci = min_ci + (math.random()) * (1 - min_ci) cibin = getbin(ci, min_ci, ncibins, 0) -- use 0 so won't print if cibindone[cibin] == 0 then -- it found a ci from a bin that hasn't been done yet isnewciok = 1 -- this is 1 if the new ci is ok, 0 if the new ci is not ok cibindone[cibin] = 1 noldci = noldci + 1 oldci[noldci] = ci cibin = getbin(ci, min_ci, ncibins, 1) -- use 1 so it prints if noldci == ncibins then ncibins = ncibins * dbox.basebin.value -- make ncibins be a power of dbox.basebin.value cibindone = zeros(ncibins) for oldcinum = 1, noldci do cibin = getbin(oldci[oldcinum], min_ci, ncibins, 1) -- use 1 so it prints cibindone[cibin] = 1 end -- for oldcinum end -- if noldci end -- if cibindone until isnewciok == 1 else -- use original way bucket = 1 - bucket -- have bucket alternate between 0 and 1 when buckets equals 2 ci = min_ci + (bucket + math.random()) * (1 - min_ci) / buckets end -- if dbox.cibins.value -- above picks next ci value to use local depth = math.random(dbox.maxpw.value) local best_score = current.GetScore() local gain = 0 repeat -- start of inner loop iteration = iteration + 1 dive.start(ci, depth) behavior.SetClashImportance(ci) band_mgr.set_enabled(enable_bands_down) structure.WiggleAll(depth, true, false) behavior.SetClashImportance(1) score = current.GetScore() if score < bottom_score then dive.abort() report(iteration, dive.ci, dive.depth, 0, "ouch!") min_ci = math.max(ci + bottom_ci, ci * 1.01) else dive.wiggle_up(enable_bands_up) gain = dive.finish() local score = current.GetScore() if score >= last_shaken_score + score_granting_shake then if dbox.shake.value then band.DisableAll() if is_mutable then structure.MutateSidechainsAll(1) else structure.ShakeSidechainsAll(1) end band_mgr.restore_bands() end last_shaken_score = current.GetScore() if dbox.shake.value then report("", is_mutable and "mutate" or "shake", 0, last_shaken_score-score, "") end min_ci = math.max(bottom_ci, min_ci * 0.9921875) end -- depth = depth + 1 end until gain < score_granting_repeat -- end of inner loop if current.GetScore() > best_score then fruitless_iterations = 0 if dbox.verbose.value and band.GetCount()>0 then print(' ') listdiff() print(' ') end else fruitless_iterations = fruitless_iterations + 1 end if min_ci == 1 then break end until min_ci == 1 or fruitless_iterations > 999 -- end of main loop mission.finalize("Petered out") end function mission.Catch(what) -- on cancel or error if what:match("Cancelled$") then dive.finish() mission.finalize("Cancelled") what = nil -- value doesn't matter, somehow suppresses further error message else print(what) mission.finalize("Error") end end -- selection.SelectAll() -- structure.SetSecondaryStructureSelected("l") xpcall(mission.Main, mission.Catch)

Comments


jeff101 Lv 1

Random Squeezes v1.7.1 with default options should behave like v1.7.0 did, but v1.7.1 has a new option to vary ci less randomly using bins.

If the initial number of bins is set to 2, one bin is for ci=0-0.5, and the other is for ci=0.5-1. v1.7.1 does one ci from each bin. Once it has done this, it doubles the number of bins to 4, with bins for ci=0-0.25, 0.25-0.5, 0.5-0.75, and 0.75-1. It sees which of these bins have already been used and then picks the next few ci from the remaining bins. When those 4 bins have all been tried once, it doubles the number of bins again to 8, then later to 16, etc., always giving a power of 2. When the score improves, the number of bins gets reset to its initial value.

Other initial numbers of bins can be used. If the initial number of bins is 3, the 3 bins stand for ci=0-0.33, 0.33-0.66, and 0.66-1. Later these become 9 bins, then 27 bins, then 81 bins, etc., tripling the number of bins each time to always give a power of 3.

v1.7.1 also has a verbose mode which lists more things in Recipe Output as it runs.