Icon representing a recipe

Recipe: Barrel Rebuild v1.0

created by Serca

Profile


Name
Barrel Rebuild v1.0
ID
109255
Shared with
Public
Parent
None
Children
Created on
November 20, 2025 at 19:36 PM UTC
Updated on
November 22, 2025 at 16:03 PM UTC
Description

Rebuilds multiple selections simultaneously. Remove all the selections to rebuild beta sheets only.

Best for


Code


--[[ Rebuilds multiple selections simultaneously. Remove all selections before starting this recipe to rebuild beta sheets only. v1.0 - Who would’ve thought Foldit actually supports rebuilding multiple selections at once as a built-in feature? ]] script_name = "Barrel Rebuild " version = "1.0.13" print(script_name .. version) START_SLOT = 2 END_SLOT = 96 BEST_SLOT = 100 -- pre-fuse reference used for rebuild attempts BEST_SHORT_SLOT = 99 -- pre-fuse reference used for short fuse. not used yet FUZED_SLOT = 98 -- fused snapshot of the current best INIT_SLOT = 1 TEMP_SLOT = 97 SOLUTION_COUNT = END_SLOT - START_SLOT + 1 SHORT_FUSE_CONFIG = "{0.05, 1, 0.05,20} {1.00, -7, 1.00,20} {1.00, 1, 1.00,20}" DRAFT_FUSE_CONFIG = "{0.05, -7, 0.05,20}" LONG_FUSE_CONFIG = "{0.05, 1, 0.05,20} {0.25, -7, 0.25,20} {1.00, 2, 1.00,20} {0.05, -7, 0.25,20} {1.00, 1, 1.00,20}" options = { topPercent = 25, rebuildPasses = 6, attemptPerSlot = 40, reportLevel = 2, seedFromFusedEvery = 1, draftFuseEvery = 3 } proteinLength = structure.GetCount() duplicateRegistry = {} solutionSubscoresArray = {} scorePartsCache = nil iterationCounter = 0 bestScore = nil initialScore = nil noGainIterations = 0 baseSegmentScores = nil function ScoreReturn() local x = current.GetEnergyScore() return math.floor(x * 1000) / 1000 end function ScoreBBReturn() local clash = 0 for i = 1, proteinLength do clash = clash + (current.GetSegmentEnergySubscore(i, "Clashing")) end return (current.GetEnergyScore()) - clash end function log(level, fmt, ...) local threshold = options.reportLevel if threshold < level then return end if select("#", ...) > 0 then print(string.format(fmt, ...)) else print(fmt) end end function phaseReport(label) local currentScore = ScoreReturn() log(1, "[Iteration %d] %s | UnfusedScore %.3f | Best %.3f", iterationCounter, label, currentScore, bestScore or currentScore) end function captureDigest() local acc = {} for i = 1, proteinLength do acc[#acc + 1] = string.format("%.2f", current.GetSegmentEnergyScore(i)) end return table.concat(acc, ";") end function acceptFusedBase(reason) log(1, reason or "Accepting fused solution as new base") save.Quickload(FUZED_SLOT) save.Quicksave(BEST_SLOT) save.Quickload(BEST_SLOT) duplicateRegistry[captureDigest()] = true noGainIterations = 0 end function isDraftIteration() local freq = options.draftFuseEvery return freq > 0 and iterationCounter > 0 and (iterationCounter % freq == 0) end function captureSegmentScores() local scores = {} for i = 1, proteinLength do scores[i] = current.GetSegmentEnergyScore(i) end return scores end function segmentScoreMapString(baseScores) if not baseScores or proteinLength == 0 then return "" end local diffs = {} for i = 1, proteinLength do diffs[i] = (current.GetSegmentEnergyScore(i)) - (baseScores[i]) end local sortedDiffs = {} for i = 1, proteinLength do sortedDiffs[i] = diffs[i] end table.sort(sortedDiffs) local thresholds = {} for bucket = 1, 9 do local rank = math.ceil(bucket * proteinLength / 10) if rank < 1 then rank = 1 end if rank > proteinLength then rank = proteinLength end thresholds[bucket] = sortedDiffs[rank] end local builder = {} for i = 1, proteinLength do local delta = diffs[i] local digit = 9 for bucket = 1, 9 do if delta <= thresholds[bucket] then digit = bucket - 1 break end end builder[i] = string.format("%d", digit) end return table.concat(builder) end function ensureSelection() local selected = 0 for i = 1, proteinLength do if selection.IsSelected(i) then selected = selected + 1 end end --error("Barrel Rebuild: please create a selection before starting the script.") if selected < 2 then selection.DeselectAll() createAutoSelections() end end function createAutoSelections() local selected = 0 for i = 1, proteinLength do if structure.GetSecondaryStructure(i) == "E" then selection.Select(i) selected = selected + 1 end end if selected > 1 then log(2,"No selections found. Selecting Beta Sheets.") return end selected = 0 for i = 1, proteinLength do if structure.GetSecondaryStructure(i) == "L" then selection.Select(i) selected = selected + 1 end end if selected > 1 then log(2,"No selections found. Selecting Loops.") return end error("Barrel Rebuild: please create a selection before starting the script.") end function RequestOptions() local ask = dialog.CreateDialog(script_name .. version) ask.topPercent = dialog.AddSlider("ShortFuze %", options.topPercent, 5, 100, 0) ask.draftFuseEvery = dialog.AddSlider("DraftFuseEvery", options.draftFuseEvery, 0, 50, 0) ask.seedFromFusedEvery = dialog.AddSlider("AcceptFusedEvery", options.seedFromFusedEvery, 0, 50, 0) ask.reportLevel = dialog.AddSlider("ReportLevel", options.reportLevel, 1, 4, 0) ask.Cancel = dialog.AddButton("Cancel", 0) ask.OK = dialog.AddButton("Start", 1) local ret = dialog.Show(ask) if ret ~= 1 then error("Cancelled by user") end options.topPercent = math.max(1, math.min(100, ask.topPercent.value)) options.reportLevel = math.max(1, math.floor(ask.reportLevel.value + 0.5)) options.seedFromFusedEvery = math.max(0, math.floor(ask.seedFromFusedEvery.value + 0.5)) options.draftFuseEvery = math.max(0, math.floor(ask.draftFuseEvery.value + 0.5)) end function parseInput(input) local result = {} for quadruplet in input:gmatch("{([^}]+)}") do local values = {} for value in quadruplet:gmatch("[^,%s]+") do table.insert(values, tonumber(value)) end table.insert(result, { clashImportance = values[1], shakeIter = values[2], clashImportance2 = values[3], wiggleIter = values[4] }) end return result end function Fuze2(inputString) recentbest.Save() local fuzeConfig = parseInput(inputString) for _, step in ipairs(fuzeConfig) do behavior.SetClashImportance(step.clashImportance or 1) if step.shakeIter and step.shakeIter > 0 then structure.ShakeSidechainsAll(step.shakeIter) elseif step.shakeIter and step.shakeIter < 0 then structure.WiggleAll(-step.shakeIter, false, true) end behavior.SetClashImportance(step.clashImportance2 or 1) if step.wiggleIter and step.wiggleIter > 0 then structure.WiggleAll(step.wiggleIter) elseif step.wiggleIter and step.wiggleIter < 0 then structure.WiggleAll(-step.wiggleIter, true, false) end end recentbest.Restore() end function GetSolutionSubscores(solutionId) if not scorePartsCache then scorePartsCache = puzzle.GetPuzzleSubscoreNames() or {} end local solutionSubscores = { SolutionID = solutionId } for _, scorePart in ipairs(scorePartsCache) do local sum = 0 for segmentIndex = 1, proteinLength do sum = sum + (current.GetSegmentEnergySubscore(segmentIndex, scorePart) or 0) end solutionSubscores[scorePart] = sum end return solutionSubscores end function GetHighestSolutionIDs(subscoreArray) local highest = {} if not subscoreArray or #subscoreArray == 0 then return highest end for scorePart, _ in pairs(subscoreArray[1]) do if scorePart ~= "SolutionID" then local maxVal = -math.huge local minVal = math.huge local ids = {} for _, row in ipairs(subscoreArray) do local value = row[scorePart] if value then if value > maxVal then maxVal = value ids = { row.SolutionID } elseif value == maxVal then table.insert(ids, row.SolutionID) end if value < minVal then minVal = value end end end if maxVal > minVal then highest[scorePart] = ids end end end return highest end function reportTopCandidates(candidates) if not candidates or #candidates == 0 then log(2, "No ranked candidates to report.") return end if (options.reportLevel or 2) < 2 then return end local shortCount = math.max(1, math.floor(#candidates * options.topPercent / 100 + 0.5)) local limit = math.min(shortCount, #candidates) --local limit = math.min(5, #candidates) log(3, "Top %d candidates:", limit) for i = 1, limit do local rec = candidates[i] log(3, "%2d) Slot %3d | Score %7.3f | BB %7.0f / Rank %6.1f", i, rec.id or 0, rec.score or 0, rec.scoreBB or 0, rec.rank or 0) end end function attemptUniqueSolution(slotIndex) local attempts = 0 local duplicateHits = 0 local success = false local finalScore = nil local finalBB = nil while attempts < options.attemptPerSlot do attempts = attempts + 1 local rebuildIter = ((attempts - 1) % options.rebuildPasses) + 1 save.Quickload(BEST_SLOT) log(4, "[Slot %d] attempt %d (RebuildIter=%d)", slotIndex, attempts, rebuildIter) structure.RebuildSelected(rebuildIter) finalScore = ScoreReturn() finalBB = ScoreBBReturn() local digest = captureDigest() if not duplicateRegistry[digest] then duplicateRegistry[digest] = true save.Quicksave(slotIndex) save.Quickload(BEST_SLOT) success = true break else duplicateHits = duplicateHits + 1 log(4, "[Slot %d] duplicate #%d", slotIndex, duplicateHits) end end if success then log(4, "[Slot %d] stored unique solution after %d attempts (%d duplicates)", slotIndex, attempts, duplicateHits) else log(3, "[Slot %d] attempt cap reached (%d). Keeping last candidate", slotIndex, attempts) save.Quicksave(slotIndex) save.Quickload(BEST_SLOT) end return success, duplicateHits, attempts, finalScore, finalBB end function fillSolutionSlots() undo.SetUndo(false) log(2, "Generating %d solutions (slots %d-%d)", SOLUTION_COUNT, START_SLOT, END_SLOT) local duplicatesRejected = 0 for slotIndex = START_SLOT, END_SLOT do local success, duplicateHits, attempts, slotScore, slotBB = attemptUniqueSolution(slotIndex) duplicatesRejected = duplicatesRejected + duplicateHits log(3, "[Slot %d] BB %.0f | score %.3f | attempts %d", slotIndex, slotScore or 0, slotBB or 0, attempts) end undo.SetUndo(true) log(1, "Solution slots ready: %d candidates, duplicate hits %d", SOLUTION_COUNT, duplicatesRejected) return SOLUTION_COUNT, duplicatesRejected end function collectCandidates(count) log(2, "Loading %d slots for ranking", count) local candidates = {} solutionSubscoresArray = {} for offset = 0, count - 1 do local slot = START_SLOT + offset save.Quickload(slot) local record = { id = slot, score = ScoreReturn(), scoreBB = ScoreBBReturn(), rank = 0 } table.insert(candidates, record) table.insert(solutionSubscoresArray, GetSolutionSubscores(slot)) log(3, "[Slot %d] score %7.3f | BB %7.0f", slot, record.score or 0, record.scoreBB or 0) end save.Quickload(BEST_SLOT) log(2, "Collected %d candidates", #candidates) return candidates end function assignRank(list, field) table.sort(list, function(a, b) if a[field] == b[field] then return a.id < b.id end return (a[field] or -1e18) > (b[field] or -1e18) end) local bonus = #list + 1 for index, rec in ipairs(list) do rec.rank = (rec.rank or 0) + (bonus - index) end end function rankCandidates(candidates) log(2, "Ranking %d candidates", #candidates) for _, rec in ipairs(candidates) do rec.rank = 0 end assignRank(candidates, "score") assignRank(candidates, "scoreBB") local highest = GetHighestSolutionIDs(solutionSubscoresArray) local subscoreBonus = math.floor(#candidates / 12 * 10 + 0.5) / 10 for _, rec in ipairs(candidates) do for _, ids in pairs(highest) do for _, id in ipairs(ids) do if id == rec.id then rec.rank = rec.rank + subscoreBonus break end end end end table.sort(candidates, function(a, b) if a.rank == b.rank then if a.score == b.score then return a.id < b.id end return (a.score or 0) > (b.score or 0) end return (a.rank or 0) > (b.rank or 0) end) reportTopCandidates(candidates) end function updateBestFromCurrent(candidateSlot, startScore) local fusedScore = ScoreReturn() if not bestScore or fusedScore > bestScore then local previous = bestScore or fusedScore bestScore = fusedScore save.Quicksave(FUZED_SLOT) local fusedDigest = captureDigest() duplicateRegistry[fusedDigest] = true save.Quickload(TEMP_SLOT) save.Quicksave(BEST_SLOT) local bestDigest = captureDigest() duplicateRegistry[bestDigest] = true save.Quickload(BEST_SLOT) log(1, "New best slot %d score %.3f (Δ%.3f)", candidateSlot, fusedScore, fusedScore - previous) log(2, "Total gain: %.2f", bestScore - initialScore) else save.Quickload(BEST_SLOT) if startScore then log(3, "Slot %d: %.3f → %.3f (no improvement; best %.3f)", candidateSlot, startScore, fusedScore, bestScore or fusedScore) else log(3, "Slot %d: no improvement (%.3f ≤ %.3f)", candidateSlot, fusedScore, bestScore or fusedScore) end end end function runDraftFusePass(candidates) log(2, "Running draft fuse pass for %d candidates", #candidates) for index, rec in ipairs(candidates) do save.Quickload(rec.id) save.Quicksave(TEMP_SLOT) local before = ScoreReturn() Fuze2(DRAFT_FUSE_CONFIG) local afterDraft = ScoreReturn() log(3, "[Draft %2d/%2d] slot %2d: %6.3f → %6.3f / Rank %6.1f", index, #candidates, rec.id, before, afterDraft , rec.rank or 0) --save.Quicksave(rec.id) updateBestFromCurrent(rec.id, before) end end function runShortFuseCandidate(rec, shortIndex, shortTotal) save.Quickload(rec.id) save.Quicksave(TEMP_SLOT) local startScore = ScoreReturn() Fuze2(SHORT_FUSE_CONFIG) local afterShort = ScoreReturn() log(2, "[Short %2d/%2d] slot %2d: %6.3f → %6.3f / %3.1f", shortIndex, shortTotal, rec.id, startScore, afterShort , rec.rank or 0) --save.Quicksave(rec.id) -- keep short result for potential long fuse updateBestFromCurrent(rec.id, startScore) return afterShort end function runLongFuseCandidate(rec, longIndex, longTotal) save.Quickload(rec.id) save.Quicksave(TEMP_SLOT) local startScore = ScoreReturn() Fuze2(LONG_FUSE_CONFIG) local afterLong = ScoreReturn() log(2, "[Long %2d/%2d] slot %2d: %6.3f → %6.3f / Rank %6.1f", longIndex, longTotal, rec.id, startScore, afterLong , rec.rank or 0) --save.Quicksave(rec.id) updateBestFromCurrent(rec.id, startScore) end function runFuzes(candidates, opts) opts = opts or {} local draftMode = opts.draftMode local total = #candidates if total == 0 then return end local shortCount = math.max(1, math.floor(total * options.topPercent / 100 + 0.5)) local longPercent = math.sqrt(options.topPercent) local longCount = math.max(1, math.floor(total * longPercent / 100 + 0.5)) if longCount > shortCount then longCount = shortCount end if draftMode then local draftLongCount = math.min(longCount, total) log(2, "Draft fuse iteration.", draftLongCount) for i = 1, draftLongCount do runLongFuseCandidate(candidates[i], i, draftLongCount) end log(3, "Fuse complete. Current best: %.3f", bestScore or ScoreReturn()) return end log(2, "Short fuse: %d candidates. Long fuse: %d", shortCount, longCount) local shortResults = {} local maxShort = math.min(shortCount, total) for index = 1, maxShort do local rec = candidates[index] local afterShort = runShortFuseCandidate(rec, index, shortCount) table.insert(shortResults, { rec = rec, shortScore = afterShort, shortIndex = index }) end table.sort(shortResults, function(a, b) return (a.shortScore or -1e18) > (b.shortScore or -1e18) end) for i = 1, math.min(longCount, #shortResults) do local entry = shortResults[i] runLongFuseCandidate(entry.rec, i, longCount) end log(3, "Fuse complete. Current best: %.3f", bestScore) end function main() ensureSelection() RequestOptions() save.Quicksave(INIT_SLOT) save.Quicksave(BEST_SLOT) save.Quicksave(FUZED_SLOT) duplicateRegistry[captureDigest()] = true bestScore = ScoreReturn() initialScore = bestScore baseSegmentScores = captureSegmentScores() while true do iterationCounter = iterationCounter + 1 local bestBeforeIteration = bestScore local draftMode = isDraftIteration() phaseReport("Generating solutions") local _, duplicateHits = fillSolutionSlots() log(3, "Duplicates rejected this cycle: %d", duplicateHits) phaseReport("Ranking") local candidates = collectCandidates(SOLUTION_COUNT) if draftMode then phaseReport("Draft fuse pass") runDraftFusePass(candidates) candidates = collectCandidates(SOLUTION_COUNT) end rankCandidates(candidates) phaseReport(draftMode and "Draft long fuse" or "Fusing") runFuzes(candidates, { draftMode = draftMode }) if bestScore > bestBeforeIteration then local gain = bestScore - bestBeforeIteration noGainIterations = 0 phaseReport(string.format("Gained this iteration +%.3f", gain)) else noGainIterations = noGainIterations + 1 local freq = options.seedFromFusedEvery or 0 if freq > 0 and noGainIterations >= freq then local streak = noGainIterations acceptFusedBase(string.format("No gain for %d iterations — accepting fused solution", streak)) phaseReport(string.format("No gain streak hit %d → fused base accepted", streak)) else if freq > 0 then phaseReport(string.format("No gain (%d/%d until switching to fuzed solution)", noGainIterations, freq)) else phaseReport(string.format("No gain (%d in a row)", noGainIterations)) end end end if baseSegmentScores then local map = segmentScoreMapString(baseSegmentScores) if map ~= "" then log(2, "Segment score change map: \n%s", map) end end end end function cleanup(err) if err and err ~= "" then log(1, "Cleanup: %s", tostring(err)) end save.Quickload(INIT_SLOT) save.Quickload(BEST_SLOT) save.Quickload(FUZED_SLOT) if bestScore and initialScore then log(1, "Net change: %.3f", (bestScore or initialScore) - initialScore) end behavior.SetClashImportance(1) end xpcall(main, cleanup)

Comments