Icon representing a recipe

Recipe: Complex Remix v1.0

created by Serca

Profile


Name
Complex Remix v1.0
ID
109182
Shared with
Public
Parent
Complex_Remix v0.9
Children
None
Created on
September 18, 2025 at 17:19 PM UTC
Updated on
September 20, 2025 at 17:03 PM UTC
Description

Remixes multiple selections simultaneously. For many selections, lower the Slot Budget.

Best for


Code


--[[ v1.0.2 - bugfix v1.0 - Major code refactoring. - Now it is possible to Remix up to 6 selections v0.9 Complex Remix for 2 selections https://fold.it/recipes/103304 Remix multiple selections simultaneously. It tests all possible relative Remix solutions for all the selections. Then it fuses all of them and finds the best scored solution. Slot Budget is the main setting to limit the total number of fuses. Example: if you Complex Remix two selections and each has 30 remix solutions, in total you need to try 30×30 = 900 fuses. Slot Budget lets you cap this. If you set Slot Budget to 20, each selection will use only 10 solutions (20=10+10), so the total is 10×10 = 100 fuses. For 2+ selections, lower the Slot Budget. The combinatorics grows sky thousands. Example: with four selections and Slot Budget = 16, each selection will test up to 4 solutions, so the total is 4×4×4×4 = 256 fuses. If you don’t lower the Slot Budget, and each of the 4 selections has 20 Remix solutions, the recipe will have to test 160 000 fuses. ]]-- version = " 1.0" script_name = "Complex Remix" function FastFuze() behavior.SetClashImportance(0.05) structure.ShakeSidechainsAll(1) structure.WiggleAll(10) behavior.SetClashImportance(1) structure.WiggleAll(10) end function Fuze() recentbest.Save() behavior.SetClashImportance(0.05) structure.ShakeSidechainsAll(1) structure.WiggleAll(2) behavior.SetClashImportance(0.2) structure.ShakeSidechainsAll(1) structure.WiggleAll(1) behavior.SetClashImportance(1) structure.WiggleAll(1) behavior.SetClashImportance(0.3) structure.ShakeSidechainsAll(1) behavior.SetClashImportance(1) structure.WiggleAll(20) recentbest.Restore() end function ScoreReturn() local x = current.GetEnergyScore() return x - x % 0.01 end DEFAULT_SOLUTIONS_TO_SEARCH = 94 SLOT_BUDGET = 94 MAX_PREPASS_RESULTS = 95 MIN_SELECTION_LENGTH = 3 MAX_SELECTION_LENGTH = 9 MAX_SELECTIONS = 6 DEFAULT_SELECTION_COUNT = 2 reportLevel = 2 slotBudget = DEFAULT_SOLUTIONS_TO_SEARCH proteinLength = 0 selectionStates = {} selectionCount = DEFAULT_SELECTION_COUNT slotBases = {} initScore = ScoreReturn() function Log(level, message, ...) if reportLevel >= level then if select('#', ...) > 0 then print(string.format(message, ...)) else print(message) end end end function Clamp(value, minVal, maxVal) if value < minVal then return minVal end if value > maxVal then return maxVal end return value end function ClampRange(range) local lengthLimit = math.max(proteinLength, 1) local minLen = math.min(MIN_SELECTION_LENGTH, lengthLimit) local maxLen = math.min(MAX_SELECTION_LENGTH, lengthLimit) local startVal = Clamp(math.floor((range.start or 1) + 0.5), 1, lengthLimit) local finishVal = Clamp(math.floor((range.finish or startVal) + 0.5), 1, lengthLimit) if startVal > finishVal then local tmp = startVal startVal = finishVal finishVal = tmp end local length = finishVal - startVal + 1 if length < minLen then local deficit = minLen - length local leftShift = math.floor(deficit / 2) startVal = startVal - leftShift finishVal = finishVal + (deficit - leftShift) if startVal < 1 then local delta = 1 - startVal startVal = 1 finishVal = math.min(lengthLimit, finishVal + delta) end if finishVal > lengthLimit then local delta = finishVal - lengthLimit finishVal = lengthLimit startVal = math.max(1, startVal - delta) end end length = finishVal - startVal + 1 if length < minLen then finishVal = math.min(lengthLimit, startVal + minLen - 1) startVal = math.max(1, finishVal - minLen + 1) end length = finishVal - startVal + 1 if length > maxLen then local center = (startVal + finishVal) / 2 local newStart = math.floor(center - (maxLen - 1) / 2 + 0.5) local newEnd = newStart + maxLen - 1 if newStart < 1 then newStart = 1 newEnd = maxLen end if newEnd > lengthLimit then newEnd = lengthLimit newStart = math.max(1, lengthLimit - maxLen + 1) end startVal = newStart finishVal = newEnd end return { start = startVal, finish = finishVal, length = finishVal - startVal + 1 } end function ScanSelections() local selections = {} local currentStart = nil for i = 1, proteinLength do if selection.IsSelected(i) then if currentStart == nil then currentStart = i end if i == proteinLength then table.insert(selections, { start = currentStart, finish = i }) currentStart = nil end elseif currentStart ~= nil then table.insert(selections, { start = currentStart, finish = i - 1 }) currentStart = nil end end return selections end function BuildDefaultSelections(existing, desiredCount) local defaults = {} local count = math.min(#existing, desiredCount) for i = 1, count do local original = existing[i] local sanitized = ClampRange(original) if reportLevel >= 2 then if sanitized.start ~= original.start or sanitized.finish ~= original.finish then Log(2, "Adjusted selection %d to %d-%d (len %d) during preset", i, sanitized.start, sanitized.finish, sanitized.length) end end defaults[i] = sanitized end local baseLength = math.min(5, MAX_SELECTION_LENGTH) local halfSpan = math.floor((baseLength - 1) / 2) for i = count + 1, desiredCount do local center = math.floor(proteinLength / (desiredCount + 1) * i) center = Clamp(center, 1 + halfSpan, proteinLength - halfSpan) local startVal = center - halfSpan local sanitized = ClampRange({ start = startVal, finish = startVal + baseLength - 1 }) if reportLevel >= 2 then Log(2, "Initialized selection %d to %d-%d (len %d)", i, sanitized.start, sanitized.finish, sanitized.length) end defaults[i] = sanitized end if desiredCount == 0 then return BuildDefaultSelections(existing, DEFAULT_SELECTION_COUNT) end return defaults end function CreateSelectionState(range, index) local sanitized = ClampRange(range) return { index = index, start = sanitized.start, finish = sanitized.finish, length = sanitized.length, currentLength = sanitized.length, initialStart = sanitized.start, initialLength = sanitized.length, center = (sanitized.start + sanitized.finish) / 2, hasReset = false, solutionCount = 0, assignedSlots = 0 } end function ApplySelectionState(state) for pos = state.start, state.finish do selection.Select(pos) end end function ApplySelectionStates() selection.DeselectAll() for _, state in ipairs(selectionStates) do ApplySelectionState(state) end end function SelectSingle(index) selection.DeselectAll() ApplySelectionState(selectionStates[index]) end function AssignLength(state, newLength) newLength = Clamp(newLength, MIN_SELECTION_LENGTH, MAX_SELECTION_LENGTH) local halfSpan = newLength - 1 local leftSpan = math.floor(halfSpan / 2) local center = state.center local startVal = math.floor(center + 0.5) - leftSpan local finishVal = startVal + newLength - 1 local clamped = ClampRange({ start = startVal, finish = finishVal }) state.start = clamped.start state.finish = clamped.finish state.length = clamped.length state.currentLength = clamped.length state.center = (clamped.start + clamped.finish) / 2 end function AdvanceLength(state) local prevLength = state.length local nextLength if prevLength < MAX_SELECTION_LENGTH then nextLength = prevLength + 1 if state.hasReset and nextLength == state.initialLength then return false end else nextLength = MIN_SELECTION_LENGTH if state.hasReset then if nextLength == state.initialLength then return false end else state.hasReset = true if nextLength == state.initialLength then return false end end end AssignLength(state, nextLength) return true end function TryRemixWithLengthAdjustment(index) local state = selectionStates[index] while true do save.Quickload(1) SelectSingle(index) local count = structure.RemixSelected(2, MAX_PREPASS_RESULTS) save.Quickload(1) if count > 0 then state.solutionCount = count Log(3, "Selection %d length %d (%d-%d) produced %d solutions.", index, state.length, state.start, state.finish, count) ApplySelectionStates() save.Quicksave(1) return true end local advanced = AdvanceLength(state) if not advanced then Log(1, "ERROR: Selection %d failed to produce remix solutions after cycling lengths.", index) return false end ApplySelectionStates() save.Quicksave(1) Log(3, "Selection %d produced no solutions; trying length %d (%d-%d).", index, state.length, state.start, state.finish) end end function PreMeasureSolutions() for idx = 1, selectionCount do if not TryRemixWithLengthAdjustment(idx) then return false end end return true end function AllocateSlots(totalBudget) local counts = {} local sum = 0 for idx, state in ipairs(selectionStates) do counts[idx] = state.solutionCount sum = sum + state.solutionCount end local assigned = {} if sum <= totalBudget then for idx, count in ipairs(counts) do assigned[idx] = math.min(count, totalBudget) end return assigned end local base = math.floor(totalBudget / selectionCount) if base < 1 then base = 1 end local used = 0 for idx = 1, selectionCount do assigned[idx] = math.min(counts[idx], base) used = used + assigned[idx] end local leftover = totalBudget - used local order = {} for idx = 1, selectionCount do order[idx] = idx end table.sort(order, function(a, b) if counts[a] == counts[b] then return a < b end return counts[a] > counts[b] end) while leftover > 0 do local distributed = false for _, idx in ipairs(order) do local capacity = counts[idx] - assigned[idx] if capacity > 0 then local add = math.min(capacity, leftover) assigned[idx] = assigned[idx] + add leftover = leftover - add distributed = true if leftover <= 0 then break end end end if not distributed then break end end local totalAssigned = 0 for idx = 1, selectionCount do totalAssigned = totalAssigned + assigned[idx] end if totalAssigned > totalBudget then local excess = totalAssigned - totalBudget table.sort(order, function(a, b) if assigned[a] == assigned[b] then return a < b end return assigned[a] > assigned[b] end) for _, idx in ipairs(order) do if excess <= 0 then break end local reducible = math.min(assigned[idx] - 1, excess) if reducible > 0 then assigned[idx] = assigned[idx] - reducible excess = excess - reducible end end end return assigned end function Ordinal(n) local suffix = "th" local mod10 = n % 10 local mod100 = n % 100 if mod10 == 1 and mod100 ~= 11 then suffix = "st" elseif mod10 == 2 and mod100 ~= 12 then suffix = "nd" elseif mod10 == 3 and mod100 ~= 13 then suffix = "rd" end return string.format("%d%s", n, suffix) end function SummarizeSelections() if reportLevel < 2 then return end if selectionCount == 2 then local a = selectionStates[1] local b = selectionStates[2] print(string.format("Sel1 is %d-%d\t / sel2 is\t%d-%d", a.start, a.finish, b.start, b.finish)) else for idx, state in ipairs(selectionStates) do Log(2, "Selection %d %d - %d", idx, state.start, state.finish) end end end function ExtractRangesFromDialog(dlg) local ranges = {} for idx = 1, selectionCount do local startSlider = dlg["sel" .. idx .. "Start"] local endSlider = dlg["sel" .. idx .. "End"] if startSlider and endSlider then ranges[idx] = ClampRange({ start = startSlider.value, finish = endSlider.value }) else ranges[idx] = ClampRange({ start = 1, finish = MIN_SELECTION_LENGTH }) end end return ranges end function ShowSelectionDialog(existingSelections) local defaults = BuildDefaultSelections(existingSelections, selectionCount) while true do local dlg = dialog.CreateDialog(script_name..version) dlg.slotCount = dialog.AddSlider ("Slot Budget", slotBudget, selectionCount, SLOT_BUDGET, 0) for idx = 1, selectionCount do if idx > 1 then dlg["label" .. idx] = dialog.AddLabel("Selection " .. idx) end dlg["sel" .. idx .. "Start"] = dialog.AddSlider("Selection " .. idx .. " start", defaults[idx].start, 1, proteinLength, 0) dlg["sel" .. idx .. "End"] = dialog.AddSlider("Selection " .. idx .. " end", defaults[idx].finish, 1, proteinLength, 0) end dlg.report = dialog.AddSlider("Report level", reportLevel, 1, 4, 0) dlg.ok = dialog.AddButton("OK", 1) dlg.add = dialog.AddButton("Add Sel", 2) dlg.cancel = dialog.AddButton("Cancel", 0) local result = dialog.Show(dlg) if result == 0 then print("Canceled by user") return nil elseif result == 2 then if selectionCount >= MAX_SELECTIONS then print(string.format("Maximum selections reached (%d).", MAX_SELECTIONS)) else local current = ExtractRangesFromDialog(dlg) selectionCount = selectionCount + 1 defaults = BuildDefaultSelections(current, selectionCount) slotBudget = math.max(slotBudget, selectionCount) end elseif result == 1 then slotBudget = math.floor(dlg.slotCount.value + 0.5) reportLevel = math.floor(dlg.report.value + 0.5) local ranges = {} for idx = 1, selectionCount do ranges[idx] = ClampRange({ start = dlg["sel" .. idx .. "Start"].value, finish = dlg["sel" .. idx .. "End"].value }) end return { slotCount = slotBudget, reportLevel = reportLevel, ranges = ranges } end end end function PrepareSelections(config) selectionStates = {} for idx = 1, selectionCount do selectionStates[idx] = CreateSelectionState(config.ranges[idx], idx) if reportLevel >= 3 then Log(3, "Selection %d prepared as %d-%d (len %d)", idx, selectionStates[idx].start, selectionStates[idx].finish, selectionStates[idx].length) end end ApplySelectionStates() end function ComputeSlotBases() slotBases = {} slotBases[1] = 2 local highest = 1 for level = 1, selectionCount do if level > 1 then slotBases[level] = slotBases[level - 1] + selectionStates[level - 1].assignedSlots end local maxSlot = slotBases[level] + selectionStates[level].assignedSlots - 1 if maxSlot > highest then highest = maxSlot end end if highest > 100 then Log(1, "ERROR: Slot allocation needs quicksave slot %d, which exceeds the limit of 100. Reduce solutions to search.", highest) return false end return true end -- Recursively count exact number of leaf fuzes (sum of childCount -- across all blocks) without performing any fuzing or mutating budgets. -- (Removed) Exact prepass counting functions to speed up execution. function RunRecursiveRemix(level, outerRemaining, path) if outerRemaining == nil then outerRemaining = 1 end if path == nil then path = {} end local state = selectionStates[level] local baseSlot = slotBases[level] local budget = state.assignedSlots SelectSingle(level) local count = structure.RemixSelected(baseSlot, budget) if count == 0 then Log(1, "ERROR: Selection %d returned no remix solutions (budget %d).", level, budget) return 0 end state.solutionCount = count Log(3, "Got\t%d\tsolutions for %s selection", count, Ordinal(level)) if count < budget then Log(2, "Selection %d produced only %d solution(s); budget capped from %d", level, count, budget) end if level == selectionCount then return count end local finalBase = slotBases[level + 1] for i = 0, count - 1 do save.Quickload(baseSlot + i) local nextPath = {} for k, v in ipairs(path) do nextPath[k] = v end nextPath[#nextPath + 1] = i + 1 local childCount = RunRecursiveRemix(level + 1, outerRemaining * (count - i), nextPath) if level == selectionCount - 1 and childCount > 0 then local combinationsLeft = count - i -- total remaining blocks at this and outer levels: -- current outer branch has 'combinationsLeft' blocks left; each other outer branch (outerRemaining-1) -- still has the full 'count' blocks. Each block contains 'childCount' leaf fuzes. local totalBlocksLeft = combinationsLeft + (outerRemaining - 1) * count local totalLeft = totalBlocksLeft * childCount if reportLevel == 2 then -- Use on-the-fly estimate instead of exact prepass count Log(2, string.format("\t%d\t fuzes to go. Best\t%.2f", totalLeft, bestScore)) else if outerRemaining > 1 then -- Example: Have (6 + (3 - 1) * 7) * 23 = 460 fuzes to go Log(3, string.format("( \t%d\t+\t(%d\t-\t1 )\t*\t%d )\t*\t%d\t=\t%d\t fuzes to go", combinationsLeft, outerRemaining, count, childCount, totalLeft)) else Log(3, string.format("\t%d\t*\t%d\t=\t%d\t fuzes to go", combinationsLeft, childCount, totalLeft)) end end -- track best candidate within this fast-fuze block local blockBestSlot = nil local blockBestScore = -999999 for slotIdx = finalBase, finalBase + childCount - 1 do save.Quickload(slotIdx) save.Quicksave(99) FastFuze() save.Quicksave(slotIdx) local currentScore = ScoreReturn() local marker = "" -- update per-block best (will need that later for full fuze); store its unfuzed version into slot 97 if currentScore > blockBestScore then blockBestScore = currentScore blockBestSlot = slotIdx save.Quickload(99) save.Quicksave(97) save.Quickload(slotIdx) marker = "\t*" end if currentScore > bestScore then bestScore = currentScore bestSlot = slotIdx save.Quicksave(98) --saving fuzed version for the best solution found save.Quickload(99) --loading unfuzed version of the best solution save.Quicksave(100)--saving unfuzed version of the best solution marker = marker.." best" end local parts = {} for k, v in ipairs(path) do parts[#parts+1] = tostring(v) end parts[#parts+1] = tostring(i + 1) parts[#parts+1] = tostring(slotIdx - finalBase + 1) local label = table.concat(parts, " - ") Log(2, string.format("Remix\t%s\t: slot\t%d\tscore\t%.2f%s", label, slotIdx, currentScore, marker)) end -- after finishing fast fuzes for this block, run a full fuze on the best candidate using its unfuzed copy in slot 97 if blockBestSlot ~= nil then save.Quickload(97) -- unfuzed best for this block local preFullScore = blockBestScore -- best fast-fuzed score for report Fuze() local fullScore = ScoreReturn() temp = "" if fullScore > bestScore then bestScore = fullScore bestSlot = blockBestSlot save.Quicksave(98) -- save full-fuzed best save.Quickload(97) -- restore unfuzed best for saving save.Quicksave(100) -- save the unfuzed version temp = "\t* best" end Log(2, "Full Fuzed slot %d score %.2f to score %.2f %s", blockBestSlot, preFullScore, fullScore, temp) end -- No exact remaining fuze counter when prepass is disabled end end return count end function ExecuteRemixes() ApplySelectionStates() local resultCount = RunRecursiveRemix(1, 1, {}) return resultCount > 0 end function main() bestScore = -999999 bestSlot = 0 proteinLength = structure.GetCount() local startScore = ScoreReturn() print(string.format("Starting score %.2f", startScore)) save.Quicksave(1) save.Quicksave(100) save.Quicksave(98) local existingSelections = ScanSelections() if #existingSelections == 0 then selectionCount = DEFAULT_SELECTION_COUNT else selectionCount = math.min(math.max(#existingSelections, DEFAULT_SELECTION_COUNT), MAX_SELECTIONS) end local config = ShowSelectionDialog(existingSelections) if not config then return end slotBudget = Clamp(config.slotCount, selectionCount, SLOT_BUDGET) reportLevel = Clamp(config.reportLevel, 1, 4) PrepareSelections(config) save.Quicksave(1) if not PreMeasureSolutions() then return end SummarizeSelections() local allocations = AllocateSlots(slotBudget) for idx, state in ipairs(selectionStates) do local assigned = math.max(1, math.min(state.solutionCount, allocations[idx] or state.solutionCount)) if reportLevel >= 3 and state.assignedSlots ~= 0 and assigned ~= state.assignedSlots then Log(3, "Selection %d slot budget adjusted from %d to %d", idx, state.assignedSlots, assigned) end state.assignedSlots = assigned if state.assignedSlots < state.solutionCount then Log(2, string.format("Selection %d: %d solutions, budget %d (trimmed %d)", idx, state.solutionCount, state.assignedSlots, state.solutionCount - state.assignedSlots)) else Log(2, string.format("Selection %d: %d solutions, budget %d", idx, state.solutionCount, state.assignedSlots)) end end if not ComputeSlotBases() then return end -- Skip exact prepass; progress will use an on-the-fly estimate -- Ensure a clean selection state before the main run ApplySelectionStates() if not ExecuteRemixes() then return end Cleanup("Finished") end function Cleanup(err) print ("Cleanup") undo.SetUndo(true) -- creating undo stack save.Quickload(1) save.Quickload(100) save.Quickload(98) currentScore=ScoreReturn() behavior.SetClashImportance(1) if (currentScore > initScore) then print ("Total gain:", currentScore-initScore) else save.Quickload(1) print("No improve. Restored to "..ScoreReturn()) end if selectionStates ~= nil and #selectionStates > 0 then ApplySelectionStates() if reportLevel ~= nil and reportLevel >= 2 then Log(3, "Restored %d selection(s) on exit", #selectionStates) end end if err ~= nil then print(err) end end xpcall ( main , Cleanup )

Comments