Code
-- This recipe is designed to work with Ligand Loader.
-- https://fold.it/recipes/109241
-- Keeping the "Segment Information" window for segment index 1 (press tab on segment #1) will display the "AutoSaveSlot=#" info
-- this can be used as a quick reference to see which quick save is currently being processed.
-- it will process all quicksave slots 1..100 and work on all marked with "AutoSaveSlot=<n>"
-- make sure to erase the "AutoSaveSlot=<n>" from quick save slots you don't want to process again.
-- Scans quicksave slots 1..100, selects those marked with "AutoSaveSlot=<n>" in
-- segment 1's note, runs Ligand_Operations() function to process each save slot ligand,
-- and overwrites the slot if improved.
-- Foldit Lua V2 (Lua 5.1)
-- Authur: Bravo + Co-Pilot (sorry about the hard to read code)
local function Ligand_Batch_Operator()
----------------------------------------------------------------------
-- helpers that do not depend on per-slot state (kept local inside Ligand_Batch_Operator)
----------------------------------------------------------------------
-- Normalize/replace the AutoSaveSlot marker inside an existing note string.
local function ensure_marker_in_note(note, slot)
if type(note) ~= "string" then note = "" end
local marker = "AutoSaveSlot=" .. tostring(slot)
if note:match("AutoSaveSlot=%d+") then
-- Replace first occurrence only, preserve other text
return (note:gsub("AutoSaveSlot=%d+", marker, 1))
else
if #note > 0 then
return note .. "\n" .. marker
else
return marker
end
end
end
-- Check if segment 1's note contains "AutoSaveSlot=<slot>"
local function has_matching_marker_for_slot(slot)
local note = structure.GetNote(1) or ""
local n = tonumber(note:match("AutoSaveSlot=(%d+)") or "")
return (n ~= nil) and (n == slot)
end
-- Overwrite the given quicksave slot after ensuring the marker persists in seg 1's note.
local function overwrite_slot_with_marker(slot)
local note_now = structure.GetNote(1) or ""
local fixed = ensure_marker_in_note(note_now, slot)
if fixed ~= note_now then
structure.SetNote(1, fixed) -- keep the marker correct in the saved pose
end
save.Quicksave(slot)
end
----------------------------------------------------------------------
-- Config (easy knobs)
----------------------------------------------------------------------
local THRESHOLD_GAIN = 0.001
local EPS = 1e-9
local WIGGLE_ITERS = 10
local SHAKES_PER_CYCLE = 1
----------------------------------------------------------------------
-- Shared per-slot state (mutated by helpers; helpers do not return values)
----------------------------------------------------------------------
local current_state = nil -- table set by State_Begin; read/mutated by helpers
-- Seed state for a slot.
local function State_Begin(slot)
current_state = {
slot = slot,
start_best = current.GetEnergyScore(),
best = current.GetEnergyScore(),
last_gain = 0.0,
cycles = 0,
stop = false,
improved = false
}
end
-- If current pose beats state.best, quicksave to the slot and update state.
local function Save_If_Better()
local s = current_state
local score_now = current.GetEnergyScore()
if score_now > (s.best + EPS) then
s.best = score_now
overwrite_slot_with_marker(s.slot) -- keeps AutoSaveSlot marker consistent
s.improved = true
end
end
-- One shake+wiggle cycle; records gain and attempts quicksave via Save_If_Better().
-- Uses the same guarded recentbest.Restore() behavior as the original script.
local function Do_Cycle()
local s = current_state
local before = current.GetEnergyScore()
recentbest.Save()
-- Shake & Wiggle
structure.ShakeSidechainsAll(SHAKES_PER_CYCLE)
structure.WiggleAll(WIGGLE_ITERS, true, true)
-- Defensive restore (may fail in some contexts; we ignore failure)
pcall(recentbest.Restore)
local after = current.GetEnergyScore()
s.last_gain = after - before
s.cycles = s.cycles + 1
-- If this cycle landed on a better pose, persist it
Save_If_Better()
end
-- Decide stopping and print final per-slot summary when done.
local function Maybe_Stop_And_Summarize()
local s = current_state
if s.last_gain < THRESHOLD_GAIN then
s.stop = true
local delta = s.best - s.start_best
print(string.format(" Slot %d %s %+0.3f to %.3f (cycles=%d)",
s.slot, (delta > EPS and "improved" or "unchanged"), delta, s.best, s.cycles))
end
end
-- === NEW: per-run options dialog ===
local USER_OPTS = nil
local function BuildOptionsDialog()
local dlg = dialog.CreateDialog("Ligand Batch Options") -- create dialog
dlg.Info = dialog.AddLabel("Select which stages to run for each ligand (slot).")
-- existing stage toggles
dlg.FR1 = dialog.AddCheckbox("Step 1: FastRelaxV2_0", true)
dlg.BR1 = dialog.AddCheckbox("Step 2: Bravo_SWR_v1_0", true)
dlg.RF1 = dialog.AddCheckbox("Step 3: Ligand_Rotamate_Fuze", true)
dlg.BR2 = dialog.AddCheckbox("Step 4: Bravo_SWR_v1_0", true)
dlg.FR2 = dialog.AddCheckbox("Step 5: FastRelaxV2_0", true)
dlg.BR3 = dialog.AddCheckbox("Step 6: Bravo_SWR_v1_0", true)
-- option to save a .solution after each processed slot
dlg.Info2 = dialog.AddLabel("")
dlg.Info3 = dialog.AddLabel("Save solution file after each processed slot?")
dlg.SaveSol = dialog.AddCheckbox("Save?", true)
dlg.OK = dialog.AddButton("Run", 1)
dlg.Cancel = dialog.AddButton("Cancel", 0)
local btn = dialog.Show(dlg)
if btn == 0 then error("Cancelled") end
-- collect values
local opts = {
FR1 = dlg.FR1.value, BR1 = dlg.BR1.value, RF1 = dlg.RF1.value,
BR2 = dlg.BR2.value, FR2 = dlg.FR2.value, BR3 = dlg.BR3.value,
SaveSol = dlg.SaveSol.value -- NEW field
}
print(string.format("Options selected -> FR1:%s BR1:%s RF1:%s BR2:%s FR2:%s BR3:%s SaveSol:%s",
tostring(opts.FR1), tostring(opts.BR1), tostring(opts.RF1),
tostring(opts.BR2), tostring(opts.FR2), tostring(opts.BR3),
tostring(opts.SaveSol))) -- NEW summary print
return opts
end
-- Bravo_SWR_v1_0: Shake–Wiggle–Repeat with safe best tracking
-- Notes:
-- - Tracks and saves only when score improves
-- - Uses both per-step gain threshold and a patience mechanism
-- - Adds max-iteration guard
-- - Fixes formatting and forward declarations
local function Bravo_SWR_v1_0(Ligand_Slot)
-- -----------------------------
-- Configuration
-- -----------------------------
local BEST_SLOT = Ligand_Slot
local CLASH_IMPORTANCE = 1
local SHAKE_CYCLES = 1 -- structure.ShakeSidechainsAll(…)
local WIGGLE_CYCLES_INIT = 1 -- initial cheap wiggle
local WIGGLE_CYCLES_LOOP = 7 -- loop wiggle effort
local GAIN_THRESHOLD = 0.25 -- stop if step gain < threshold
local PATIENCE_STEPS = 5 -- stop after N steps with < threshold gain
local MAX_ITERATIONS = 200 -- hard cap
-- -----------------------------
-- State
-- -----------------------------
local StartScore = current.GetScore()
local BestScore = StartScore
-- Forward declarations for mutual use
local EndingPrint
local Bravo_SWR_v1_0_main
local function quicksave_if_improved(current_score)
if current_score > BestScore then
BestScore = current_score
save.Quicksave(BEST_SLOT)
return true
end
return false
end
local function wiggle_shake_until_done()
behavior.SetClashImportance(CLASH_IMPORTANCE)
local patience = 0
local iter = 0
-- Evaluate baseline and save once
quicksave_if_improved(current.GetScore())
repeat
iter = iter + 1
local before = current.GetScore()
-- One cycle
structure.ShakeSidechainsAll(SHAKE_CYCLES)
structure.WiggleAll(WIGGLE_CYCLES_LOOP)
local after = current.GetScore()
local gain = after - before
-- Logging
print(string.format(" Iter %3d | +%7.3f | %9.3f", iter, gain, after))
-- Save only on real improvement to best
local improved_best = quicksave_if_improved(after)
-- Patience logic based on per-step gain
if gain >= GAIN_THRESHOLD then
patience = 0
else
patience = patience + 1
end
-- Stop if either patience exhausted or iteration cap hit
if patience >= PATIENCE_STEPS then
print(string.format(" Stopping: patience reached (%d steps with gain < %.3f).",
PATIENCE_STEPS, GAIN_THRESHOLD))
break
end
if iter >= MAX_ITERATIONS then
print(string.format(" Stopping: max iterations reached (%d).", MAX_ITERATIONS))
break
end
until (gain <= GAIN_THRESHOLD)
end
function EndingPrint(slot)
local TotalGain = BestScore - StartScore
print(string.format(" Best Score: %.3f | Total Gain: %.3f", BestScore, TotalGain))
return BestScore, TotalGain
end
function Bravo_SWR_v1_0_main()
print(" Shake Wiggle Repeat")
print(string.format(" Starting Score: %.3f", StartScore))
behavior.SetClashImportance(CLASH_IMPORTANCE)
structure.WiggleAll(WIGGLE_CYCLES_INIT)
quicksave_if_improved(current.GetScore())
--print(" Shaking and Wiggling...")
wiggle_shake_until_done()
EndingPrint()
end
local function cleanup(error_string)
print("")
if error_string ~= nil then
if string.find(error_string, "Cancelled") then
print(" User cancel")
else
print(error_string)
end
end
-- Ensure we end on the best pose we actually saved
if current.GetScore() < BestScore then
save.Quickload(BEST_SLOT)
else
-- If we somehow ended higher than BestScore, accept and update
BestScore = current.GetScore()
end
EndingPrint()
return
end
xpcall(Bravo_SWR_v1_0_main, cleanup)
-- load best save before returning to Ligand_Operations
save.Quickload(Ligand_Slot)
end
local function Ligand_Rotamate_Fuze(Ligand_Slot)
-- Config / state
local Best_Save_slot = Ligand_Slot
local gain_threshold = 0.250
-- NOTE: Declared but unused. Remove if not needed.
local Not_ligand_list = { "g","a","v","l","i","m","f","w","p","s","t","c","y","n","q","d","e","k","r","h" }
local loop_count = 0
local segment_index = 1
local i = 1
local structure_get_count = structure.GetCount()
-- Variables that must be shared across functions (make them locals here)
local best_score = 0
local ErrorHandlerCalled = false
-- Forward declarations so references inside Ligand_Rotamate_Fuze_main
-- bind to these locals (and not accidentally to globals).
local Ligand_Rotamate_Fuze_main
local fuze
local out_of_bounds_segment_index
local Amino_Acid_check
local check_score_improvement
local ErrorHandler
function Ligand_Rotamate_Fuze_main()
save.Quicksave(Best_Save_slot)
best_score = current.GetScore()
print(string.format(" Starting Score: %.3f", best_score))
repeat
local repeat_start_best_score = best_score
segment_index = 1
while segment_index <= structure_get_count do
-- Skip non-ligands per your check
while Amino_Acid_check() do
segment_index = segment_index + 1
out_of_bounds_segment_index()
end
selection.DeselectAll()
selection.Select(segment_index)
i = 1
save.Quickload(Best_Save_slot)
-- If your environment supports delays, use that instead:
-- ui.Delay(0.01) -- or wait(0.01)
loop_count = loop_count + 1
local rot_count = rotamer.GetCount(segment_index)
print((" Loop %d: number of rots: %d"):format(loop_count, rot_count))
while i <= rot_count do
rotamer.SetRotamer(segment_index, i)
fuze()
check_score_improvement()
print(" score: " .. current.GetScore())
save.Quickload(Best_Save_slot)
i = i + 1
end
segment_index = segment_index + 1
end
local repeat_end_best_score = best_score
local repeat_gain = repeat_end_best_score - repeat_start_best_score
until (repeat_gain <= gain_threshold)
end
function fuze()
structure.LocalWiggleSelected(12, true, true)
end
function out_of_bounds_segment_index()
-- check for out of bounds segments
if segment_index > structure_get_count then
segment_index = 1
end
end
function Amino_Acid_check()
out_of_bounds_segment_index()
local aa = structure.GetAminoAcid(segment_index)
if aa == "unk" or aa == "" then
return false
else
return true
end
end
function check_score_improvement()
local score = current.GetScore()
if best_score < score then
best_score = score
save.Quicksave(Best_Save_slot)
-- print(" Score improvement: ", best_score, " seg ", segment_index, " rot ", i) --todo remove if below works
print(string.format(" Score improvement: %.3f seg %d rot %d", best_score, segment_index, i))
end
end
-- cancel handling
function ErrorHandler(errstr)
if ErrorHandlerCalled then return end -- prevent recursion
ErrorHandlerCalled = true
print("")
if errstr ~= nil then
if string.find(errstr, "Cancelled") then
print(" User cancel")
else
print(errstr)
end
end
print("")
save.Quickload(Best_Save_slot)
end
xpcall(Ligand_Rotamate_Fuze_main, ErrorHandler)
-- load best save before returning to Ligand_Operations
save.Quickload(Ligand_Slot)
end
-- FastRelax v2 modified to operate on one save slot
-- now operates in local function
local function FastRelaxV2_0(Ligand_Slot)
local Best_Score = Ligand_Slot
-- Forward declarations so references inside FastRelaxV2_0
-- bind to these locals (and not accidentally to globals).
local AdjScore
local Minimize
local ClashScore
local RelTol2AbsTol
local PrintScores
local FastRelax_main
--[[
FastRelax v2
https://fold.it/recipes/108397
Based on the updated Rosetta relax recipe, "MonomerRelax2019"
Maguire JB, Haddox HK, Strickland D, Halabiya SF, Coventry B, et al. (December 2020).
"Perturbing the energy landscape for improved packing during computational protein design.".
Proteins 89(4):436-449. PMID 33249652.
https://europepmc.org/article/PMC/8299543
Converted from v1 Lua by Rav3n_pl's v1 to v2 converter, FolditLuaConverter.
Fast Relax 1.0 - LociOiling -- 2022/09/17
+ based on the classic PNAS paper, "Algorithm discovery by protein folding game players"
+ converted using FolditLuaConverter
+ fixed #NEED EDIT! lines (only two in this case)
+ minimal testing
Fast Relax 2.0 -- Artoria2e5 -- 2023/09/07
+ Use the new algo from https://www.rosettacommons.org/docs/latest/application_documentation/structure_prediction/relax#description-of-algorithm
+ Implement wiggle-with-tolerance, as opposed to fixed iterations
+ (TODO) implement all variants
]]
local cival = 1
local P = print
local function CI(x)
behavior.SetClashImportance(x)
cival = x
end
local S = structure.ShakeSidechainsSelected
local WA = structure.WiggleAll
local energy = true
local minPpi = 2
local shakes = 1
local wiggles = 5
local usebands = false
local bands = {}
local CI_SCHEDULES = {
["MonomerRelax2019"] = { 0.040, 0.051, 0.265, 0.280, 0.559, 0.581, 1 },
["MonomerDesign2019"] = { 0.059, 0.092, 0.280, 0.323, 0.568, 0.633, 1 },
["InterfaceRelax2019"] = { 0.069, 0.080, 0.288, 0.302, 0.573, 0.594, 1 },
["InterfaceDesign2019"] = { 0.079, 0.100, 0.295, 0.323, 0.577, 0.619, 1 },
["Legacy"] = { 0.02, 0.02, 0.25, 0.25, 0.55, 0.55, 1 },
}
local CI_SCHEDULE = CI_SCHEDULES["MonomerRelax2019"]
local has_density = 0
local function BandScale(multiplier)
for ii = 1, #bands do
band.SetStrength(ii, band.GetStrength(ii) * multiplier)
end
end
local function Score()
return energy and current.GetEnergyScore() or current.GetScore()
end
local function round(x)
return x - x % 0.001
end
local function ClashScore()
local sum = 0
local segs = structure.GetCount()
for i = 1, segs do
sum = sum + current.GetSegmentEnergySubscore(i, "Clashing")
end
return sum
end
local function AdjScore()
local ret = Score() - ClashScore() * (1 - cival)
P(" AdjScore (CI ", cival, "): ", ret)
return ret
end
local function RelTol2AbsTol(score, tol)
local expectedScore = structure.GetCount() * (30 + 20 * has_density)
return expectedScore * tol
end
-- Minimizer
-- crude approximation of rosetta minimize "relative tolerance".
-- wiggle until the (adjusted) score improves by less than
-- RelTol2AbsTol(newscore, tol).
--
-- I know the real minimizer knows better, but I can't exactly ask it, can I?
local function Minimize(tol)
-- always do something
local oldscore = AdjScore()
WA(wiggles)
local newscore = AdjScore()
local abstol = RelTol2AbsTol(newscore, tol)
-- now loop
local iter = 1
while math.abs(newscore - oldscore) > abstol and iter < 20 do
oldscore = newscore
WA(math.floor(wiggles))
newscore = AdjScore()
abstol = RelTol2AbsTol(newscore, tol)
iter = iter + 1
end
end
local function PrintScores()
P(" Score: ", Score())
P(" ClashScore: ", ClashScore())
P(" AdjScore (CI " .. tostring(cival) .. "): ", AdjScore())
end
local function FastRelax_main()
local bestScore = Score()
save.Quicksave(Best_Score)
P(" Starting FastRelax, Score: ", round(bestScore))
selection.SelectAll()
recentbest.Save()
repeat
local ss = Score()
if usebands then band.EnableAll() end
CI(CI_SCHEDULE[1])
S(shakes)
CI(CI_SCHEDULE[2])
WA(wiggles)
Minimize(0.01)
if usebands then BandScale(0.5) end
CI(CI_SCHEDULE[3])
S(shakes)
CI(CI_SCHEDULE[4])
Minimize(0.01)
if usebands then BandScale(2)
band.DisableAll() end
CI(CI_SCHEDULE[5])
S(shakes)
CI(CI_SCHEDULE[6])
Minimize(0.01)
CI(CI_SCHEDULE[7])
S(shakes)
Minimize(0.0001)
recentbest.Restore()
-- SaveBest logic inline
local g = Score() - bestScore
if g > 0 then
bestScore = Score()
save.Quicksave(Best_Score)
else
save.Quickload(Best_Score)
end
P(" Loop gain: ", round(g))
until g < minPpi
P(" End score: ", round(Score()))
end
FastRelax_main()
end
----------------------------------------------------------------------
-- Orchestrator (now just calls helpers; no return value)
----------------------------------------------------------------------
local function Ligand_Operations(ligand_quick_save_slot,opts)
-- this is where all the functions that process the ligands are.
-- try and only write local functions with local variables to help reduce memory usage.
-- each recipe listed here should not need the functions or variables from other recipes.
-- you can copy and paste in whole recipes above this Ligand_Operations() function for processing each ligand.
-- Conditionally run each stage based on the options dialog.
if opts.FR1 then FastRelaxV2_0(ligand_quick_save_slot) end
if opts.BR1 then Bravo_SWR_v1_0(ligand_quick_save_slot) end
if opts.RF1 then Ligand_Rotamate_Fuze(ligand_quick_save_slot) end
if opts.BR2 then Bravo_SWR_v1_0(ligand_quick_save_slot) end
if opts.FR2 then FastRelaxV2_0(ligand_quick_save_slot) end
if opts.BR3 then Bravo_SWR_v1_0(ligand_quick_save_slot) end
-- No return value; you already persist via quicksaves.
end
----------------------------------------------------------------------
-- main (counts improvements by reloading the quicksave after operations)
----------------------------------------------------------------------
local function Main()
print("==== Ligand Batch Operator: scanning slots 1..100 ===")
local MIN_SLOT, MAX_SLOT = 1, 100
local total_seen, total_processed, total_improved = 0, 0, 0
-- Ask the user which stages to run (once per whole batch).
USER_OPTS = BuildOptionsDialog()
for slot = MIN_SLOT, MAX_SLOT do
-- Skip empty slots to avoid loading errors.
if not save.QuicksaveEmpty(slot) then
-- Try to load the slot
local loaded_ok = pcall(save.Quickload, slot)
if loaded_ok then
total_seen = total_seen + 1
-- Only process if segment 1's note has "AutoSaveSlot=<slot>"
if has_matching_marker_for_slot(slot) then
total_processed = total_processed + 1
-- Baseline: score before we start (pose currently loaded)
local before_best = current.GetEnergyScore()
print(string.format(" Slot %d | Starting Score: %.3f", slot, before_best))
-- Run the orchestrator (no return values)
Ligand_Operations(slot, USER_OPTS)
-- Reload the slot and get best score
save.Quickload(slot)
local after_best = current.GetEnergyScore()
local slot_gain = after_best - before_best
print(string.format(" Slot %d | Ending Score: %.3f (+%.3f)", slot, after_best, slot_gain))
if after_best > before_best + EPS then
total_improved = total_improved + 1
end
if USER_OPTS.SaveSol then
-- Save present pose into a puzzle*ir_solution file, using seg 1's note as the description
save.SaveSolution(structure.GetNote(1))
end
else
-- Not our ligand/marker: skip
end
else
-- Load failed (e.g., different track) — skip this slot gracefully.
end
end
-- continue loop
end
print(" === Summary ===")
print(string.format(" Checked non-empty slots: %d", total_seen))
print(string.format(" Processed with matching marker: %d", total_processed))
print(string.format(" Slots improved & overwritten: %d", total_improved))
print(" Done.")
end
local function on_error(err)
print("Recipe error: " .. tostring(err))
if debug and debug.traceback then
print(debug.traceback(err, 2))
end
end
-- Run with protected call
xpcall(Main, on_error)
end
-- Entry
Ligand_Batch_Operator()