Icon representing a recipe

Recipe: Bravo Ligand Batch Operator v1.0

created by bravosk8erboy

Profile


Name
Bravo Ligand Batch Operator v1.0
ID
109250
Shared with
Public
Parent
None
Children
None
Created on
November 16, 2025 at 23:14 PM UTC
Updated on
December 16, 2025 at 04:31 AM UTC
Description

Best for


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()

Comments


Bruno Kestemont Lv 1

The log is misleading with unrealistic scores indicated.
Suggestion: add something else in the note and quicksave, like the client name, in order to be able to run the system on different clients and libraries.