Code
--[[ General Assembly
This tiny script is designed to Rebuild the entire protein in a complex.
It is specifically intended for rebuilds with no fusing at all or only very brief fusing. The goal is to minimize protein rigidity and avoid mojo by keeping the use of WiggleAll to a minimum.
The script performs well in the early puzzle stages, making the backbone more natural and stable. It helps lock in the protein structure right after manual folding, before switching to standard DRW recipes.
Its approach is to generate a large number of solutions (hundreds or even thousands) and evaluate them using the original scoring system without performing a Fuse.
There are two key settings:
1) “Solutions to search”
The number of Rebuild solutions generated in a single run (one pass of the protein).
The default value is twice the number of protein segments, i.e., 100–300 rebuilds.
2) “Make Fuse” (slider):
Controls how often fusing happens. Specifically, it triggers a Fuse after every X iterations that don’t score points.
This is the easiest way to adjust the depth and speed of the Rebuild.
Have fun!
]]--
version = "1.0"
function ScoreReturn()
x = current.GetEnergyScore()
return x-x%0.001
end
-----------------------------------------------------------------------------------------------init
proteinLength=structure.GetCount ()
-- service slots are 5:
-- slot1 is used as basic one
-- slot100 - as the best unfuzed solution
-- slot98 used to store fuzed version of the highest score solution
-- slot99 used as service slot to manage fuze
-- slot97 used as service slot to store lighly fuzed version of the currently fuzed solution
totalSlotsAvailable = 95
maxRebuildCount= proteinLength * 2
slotsToFuze=12
slotsNoGain = 1
shiftFuze = -1
fuzeAfternoGain = 4
forceChange = -1
shiftNoGain = -1
stopAfter = -1
multipleOverlap = 2
currentFuze = 1
StartRebuild = 13
EndRebuild = 10
reportLevel=3
sphereRadius=8
shakeOnRank=false
convertLoop=true
worstFirst=true
unfuzeFirst=false
fullProtein=true
resetOnGain = false
fuze2=false
--saveLocal=true
remixNotRebuild = false
rarityManagement = false
doFuze = false
endCI=behavior.GetClashImportance()
CI = endCI
energy2BBScoreRatio=2
selectionLength=13
fuzeConfig = {
"{0.05, -7, 0.05,2} {0.25, 3, 1.00,20} {0.05, 3, 0.25,7} {0.25, 3, 0.25,20} {1.00, 3, 1.00,20}", --best score
--"{0.25, 1, 1.00,20} {0.05, 2, 0.05,7} {0.25, 3, 0.05,7} {1.00, -2, 1.00,20} {1.00, 3, 1.00,20}", --best score
"{0.05, -20, 0.05,7} {0.25, 2, 0.25,7} {1.00, 2, 1.00,20}", --fastest
"{0.25, -7, 0.05,7} {0.05, 3, 0.25,20} {1.00, 2, 1.00,20}",
"{1.00, 3, 1.00,7} {0.05, 1, 0.25,2} {1.00, 3, 1.00,20}",
"{0.05, -2, 0.05,7} {1.00, 3, 1.00,7} {0.05, 2, 0.05,7} {0.25, 1, 0.25,2} {1.00, 3, 1.00,20}"
} --fastest
currentFuze = 3
startingAA=1
bestScore=ScoreReturn()
lastScore=-999999
bestFoundScore=-999999
currentBBScore=-999999
medianScore = -999999
medianScore_prev = -999999
rebuildScores = {}
remixBBScores = {}, {}
rebuildScores[1]=bestScore
bestSelectNum=0
remixNum = #rebuildScores
undo.SetUndo(false)
resetProtein=true
splitDirection = "forward"
bestSlot=0
selectionEnd=0
selectionStart=0
selNum=0
roundsWithNoGain = 0
RoundNumber = 1
totalRebuildsDone = 0
currentSlot = 0
activityMap = {}
selDialog=selNum
selectionStartArr={}
selectionEndArr={}
solutionIDs = {}
action="Rebuild"
numPieces = math.floor(proteinLength/13+0.5)
rebuiltResidues = {} --array to monitor changes in residues in all the solutions in slots
startScore=ScoreReturn()
startRoundScore = ScoreReturn()
initScore=ScoreReturn() --the real start score that doesn't changes
print ("+++Starting score "..startScore.." saved to slot 1")
save.Quicksave(1)
save.Quicksave(98)
save.Quicksave(100)
recentbest.Save()
selection.DeselectAll() --Clear All The Selections On The Start for DRW!
save.SaveSecondaryStructure()
-----------------------------------------------------------------------------------------------main-----------------------------------------------------------------------------------------------
function main()
resetRebuiltResidues()
FindSelection()
tempSelNum=selNum
if selNum==0 then selNum=1 end
selDialog=selNum
--Call Dialog with the options
while requestResult~=1 do
requestResult = RequestOptions()
if requestResult == 0 then return end -- cancel button pressed by user
if requestResult == 2 then selDialog=selDialog+1 end
if requestResult == 3 then
SelectLoops()
FindSelection()
--print (selNum,selDialog)
selDialog=selNum
end
if selDialog>5 then selDialog=5 end
end
selNum=math.max(tempSelNum, selDialog, selNum)
--Fuze first if unfuzed solution checkbox is selected
if (useSlot98) then
if reportLevel>2 then print ("Making Fuze for this unfuzed start", startScore) end
FuzeLocal(fuzeConfig[currentFuze])
if reportLevel>1 then print ("Fuzed to", ScoreReturn()) end
if ScoreReturn() > startScore then
save.Quicksave(98)
startScore=ScoreReturn()
startRoundScore = ScoreReturn()
initScore=ScoreReturn()
end
else
save.Quicksave(98)
end
save.Quickload(1)
--Report settings in output window
strOutput=action..": "..maxRebuildCount.." / fuze:"..slotsToFuze..". Range "..StartRebuild.." /"..EndRebuild
if worstFirst then strOutput=strOutput..", worst first" end
if resetOnGain then strOutput=strOutput..", reset" end
if shakeOnRank then strOutput=strOutput..", shake" end
if reportLevel>2 then strOutput=strOutput..". ReportLevel="..reportLevel end
strOutput = strOutput.."\n"
--if useSlot98 then strOutput=strOutput..", useSlot98" end
if fuze2 then strOutput=strOutput..", fuze2" end
if slotsNoGain then strOutput=strOutput..", slotsNoGain="..slotsNoGain end
if forceChange then strOutput=strOutput..", forceChange="..forceChange end
if fuzeAfternoGain then strOutput=strOutput..", fuzeAfternoGain="..fuzeAfternoGain end
if shiftFuze then strOutput=strOutput..", shiftFuze="..shiftFuze end
--if shiftNoGain then strOutput=strOutput..", shiftNoGain="..shiftNoGain end
if stopAfter>0 then strOutput=strOutput..", stopAfter="..stopAfter end
if (reportLevel>1) then print (strOutput) end
print ("-------------------------------------------------")
loopIter=1
while (roundsWithNoGain <= stopAfter) or (stopAfter<0) do --for Infinite execution option
local startTime = os.clock()
loopIter=loopIter + 1
if (reportLevel>1) then print("Iteration", (loopIter-1)..".") end
if (startScore > startRoundScore) then
roundsWithNoGain = 0
ratio_rarity_changed = ratio_rarity --return rarity ratio to initial values
else
if loopIter > 2 then --don't change the roundsWithNoGain when just entered the loop
roundsWithNoGain = roundsWithNoGain + 1
--increase the part of the rare solutions
ratio_rarity_changed = math.min (2, ratio_rarity_changed + 0.2)
if reportLevel>1 then print ("Score "..startScore," roundsWithNoGain "..roundsWithNoGain) end
end
end
--Make a change in settings every roundsWithNoGain rounds that gained zero points
if (loopIter>2) then
-- save Fuzed version of the protein every roundsWithNoGain rounds that gained zero points
if (roundsWithNoGain >= fuzeAfternoGain) and (fuzeAfternoGain>0) then
if reportLevel>1 then print ("NoGain "..roundsWithNoGain.." rounds. fuzeAfternoGain "..fuzeAfternoGain) end
if startScore ~= initScore then
-- if we are stuck then we accept bestslot solution as new and reset all the slots
lastScore = ScoreReturn()
save.Quickload(98)
if ScoreReturn() > lastScore then
save.SaveSolution("defuse slot98 "..ScoreBBReturn())
save.Quicksave(100)
RoundNumber = RoundNumber + 1
resetProtein = true
if reportLevel>1 then print ("Switched to Fuzed version bb ", ScoreBBReturn(), "score ", ScoreReturn()) end
else
save.Quickload(100)
end
end
end
-- shift between two types of fuze
if (roundsWithNoGain >= shiftFuze) and (shiftFuze>0) then
if currentFuze == 2 then
currentFuze = 1
if reportLevel>1 then print ("Switched to Fuze1. (rounds with no gain="..shiftFuze..")") end
else
if reportLevel>1 then print ("Switched to Fuze2. (rounds with no gain="..shiftFuze..")") end
currentFuze = 2
end
end
-- reset every roundsWithNoGain rounds that gained zero points
if (roundsWithNoGain >= forceChange) and (forceChange>-1) then
lastScore = ScoreReturn()
save.Quickload(100)
--save.SaveSolution("drw slotXX "..ScoreBBReturn())
resetProtein = true --reset the slots, so (bestSlot) will be rebuild now.
if reportLevel > 1 then print("Accepting best from previous Round. NoGain " .. roundsWithNoGain .. " rounds. forceChange " .. forceChange) end
if reportLevel > 1 then print("Switched to version bb ", ScoreBBReturn(), " score ", ScoreReturn()) end
end
if (roundsWithNoGain >= slotsNoGain) and (slotsNoGain>=0) then
maxRebuildCount = math.floor(maxRebuildCount * 1.1)
slotsToFuze = math.floor(slotsToFuze * 1.1)
if slotsToFuze > 36 then slotsToFuze = 36 end
if reportLevel>1 then print ("slotsNoGain reached. Increased maxRebuildCount="..maxRebuildCount..", slotsToFuze="..slotsToFuze) end
end
if (roundsWithNoGain >= shiftNoGain) then
selectionLength = selectionLength - 1
if selectionLength < EndRebuild then selectionLength = EndRebuild end
if reportLevel>1 then print ("NoGain "..roundsWithNoGain.." rounds. shiftNoGain "..shiftNoGain..". Length is "..selectionLength) end
--selectionStartArr, selectionEndArr = SplitProteinOnInit(activityMap, numPieces)
--roundsWithNoGain = 0
startingAA = 1
else
--shift selections start/end points to prevent rebuilding the same selections over and over
--current pointer for the selections markup on full rebuild (shift by one every iteration to prevent rebuilding the same pieces again and again)
startingAA=startingAA-1
end
end
--------'REBUILD PROTEIN BY SELECTIONS--------------------------------------------------------------------------
if resetProtein == true then
for k=1, proteinLength do
activityMap[k] = 1
end
else
activityMap=getActivityMap()
printActivity (activityMap)
end
--Split protein on selections in 2 diferent ways depending if that is the first iteration
if loopIter>0 then --first iteration only
selectionStartArr, selectionEndArr = SplitProteinOnInit(numPieces) --create the selections
else
selectionStartArr, selectionEndArr = SplitProteinByActivity(activityMap, numPieces)
end
--Calculate number of rebuilds for each selection and adjust maxRebuildCount fo split between sels as int
--total number of rebuilds remains constant per Iteration, but they are distributed between selection
tryFirstX, numOfRebuilds, adjustedTotalRebuilds = adjustRebuilParameters(maxRebuildCount, selNum)
if reportLevel>1 then
print ("Rebuilds adjusted to ~"..adjustedTotalRebuilds.." or ~" ..numOfRebuilds.." per sel. " ..tryFirstX.." best slots will be tested.")
end
rebuildsDistribution = DistributeRebuildsByActivity(activityMap, selNum * numOfRebuilds)
sortedStartArr, sortedEndArr = SortSelections(selectionStartArr, selectionEndArr) --sort array ascending to rebuild worst segments first
startRoundScore = startScore
if (reportLevel>2) then printSelections() end
-------------------
for m=1, selNum do
selectionStart=sortedStartArr[m] --selectionStartArr[m]
selectionEnd=sortedEndArr[m] --selectionEndArr[m]
SetSelection()
if convertLoop then
save.LoadSecondaryStructure()
structure.SetSecondaryStructureSelected("l")
end
if resetProtein then --make this on first iteration at first selection only, or on solution reset
resetProtein = false
resetRebuiltResidues(switchedTo) --reset the monitoring array too(except switchedTo slot)
resetInheritanceTable(bestSlot)
solutionsFound=1
remixBBScores = {}
rebuildScores = {}
topXinArray = {}
currentSlot = 1
end
if reportLevel>1 then
print((loopIter-1).."."..m.."/"..selNum.." len "..(selectionEnd - selectionStart +1).." segs: "..selectionStart.."-"..selectionEnd..
". R"..RoundNumber) --..". Current ScoreBB "..ScoreBBReturn()
end
if (#remixBBScores<tryFirstX) then --if not enough solutions yet - just rebuild, dont make that complex as in section lower
localRebuildsCounter, successRebuilds = RebuildToSlots(rebuildsDistribution[m])
if reportLevel>1 and reportLevel<4 and localRebuildsCounter>0 then print ("Accepted "..successRebuilds.." solutions") end
if reportLevel > 3 then print ("--------------------") end
else
topXinArray = returnXinOrder(remixBBScores, tryFirstX)
save.Quickload(topXinArray[1])
activityMap=getActivityMap()
if reportLevel > 3 then printActivity(activityMap) end
--now for each of the best X slots try creating best rebuild solutions for current selection
for currSel=1, tryFirstX do
--in case when tryFirstX > existed Rebuild slots, we prevent the recipe crash on the first iteration pass
if currSel > #topXinArray then
if reportLevel>2 then print (#topXinArray.." slots are not enoug. Adding some more ++++") end
topXinArray = returnXinOrder(remixBBScores, tryFirstX) end
currentSlot = topXinArray[currSel] --the number of slot that is now rebuilding
save.Quickload(currentSlot)
successRebuilds = RebuildToSlots(rebuildsDistribution[m])
if reportLevel>2 then print ((loopIter-1).."."..m.."/"..selNum.." "..currSel.."/"..tryFirstX.." ",
"Slot "..currentSlot, " accepted ", successRebuilds, " / "..rebuildsDistribution[m], "bb"..ScoreBBReturn()) end
if reportLevel > 3 then print ("----------------------------------------") end
end
end
activityMap=getActivityMap()
if reportLevel > 3 then printActivity (activityMap) end
end --for selNum
bestScore=-999999
bestSlot=0
bestSelectNum = math.min (slotsToFuze, #remixBBScores)
activityMap=getActivityMap()
if reportLevel > 3 then printActivity(activityMap) end
if reportLevel>1 then print("Total Rebuilds Tested:", totalRebuildsDone, "Ranking best "..bestSelectNum) end
SortByBackbone()
printHistory()
local endTime = os.clock()
executionTime = endTime - startTime
if reportLevel>2 then print (string.format("----------------------%.2f %.2f sec.-------------------", executionTime, executionTime2)) end
executionTime = 0
executionTime3 = 0
executionTime3 = 0
--------'FUZE BEST SOLUTIONS
if #remixBBScores > 0 then
lastSlots = {} --to restore best slots on clean up
shortFuzeScores = {} -- to keep track of scores after short fuze
--Short Fuze a lot of slots and score best
testedSlots = 0
for i, remixBBScore in ipairs(remixBBScores) do
testedSlots = testedSlots + 1
if testedSlots > slotsToFuze then break end
--Test only non-fuzed slots
if remixBBScore.score ~= 0 then
testedSlots = testedSlots - 1
else
table.insert(lastSlots, remixBBScore.id)
save.Quickload(remixBBScore.id)
prefuzeBB = ScoreBBReturn()
FuzeLocal(fuzeConfig[2]) -- short fuze
currentScore = ScoreReturn()
currentBBScore = ScoreBBReturn()
remixBBScore.score = currentScore --update remixBB array values with the fuzed ones
descriminator = currentBBScore - prefuzeBB
UpdateRemixBBDescr(i, descriminator)
updateHistoryArray (remixBBScore.id, currentScore, currentBBScore)
improve = roundX(currentScore - startScore)
textBest = ""
if currentScore > bestScore then
save.Quicksave(97)
save.Quickload(remixBBScore.id) --delete?
bestScore = currentScore
bestSlot = remixBBScore.id
textBest = "*"
end
table.insert(shortFuzeScores, {id = remixBBScore.id, score = currentScore})
if reportLevel > 1 then
print("Fuzed slot "..remixBBScore.id, " from bb "..prefuzeBB.." to bb "..currentBBScore, "score: "..currentScore, "/"..remixBBScore.rank, textBest)
end
if reportLevel > 3 then printRow(remixBBScore.id) end
end
end
-- This section is to fuze best results with some better fuze
if (#shortFuzeScores>0) then
-- Long Fuze sqrt of best short fuzes and find the best one
table.sort(shortFuzeScores, function(a, b) return a.score > b.score end)
local numToLongFuze = math.floor(math.max(1, math.sqrt(slotsToFuze))) --switch min to max
numToLongFuze = math.min(numToLongFuze, #shortFuzeScores) --prevent crash when no unfuzed left
for i = 1, numToLongFuze do
local bestShortFuze = shortFuzeScores[i]
save.Quickload(bestShortFuze.id)
FuzeLocal(fuzeConfig[1]) -- long fuze
currentScore = ScoreReturn()
currentBBScore = ScoreBBReturn()
if currentScore > bestShortFuze.score and false then --disabled
--update remixBB and History array values
local idxx = getRemixBBIDBySlot(bestShortFuze.id)
remixBBScores[idxx].score = currentScore
descriminator = currentBBScore - prefuzeBB --descriminator<0 means we have some disassembly
UpdateRemixBBDescr(idxx, descriminator)
updateHistoryArray (remixBBScore.id, currentScore, currentBBScore)
end
if currentScore > bestScore then
bestScore = currentScore
bestSlot = bestShortFuze.id
save.Quicksave(97)
if reportLevel > 1 then
print("Best slot after long fuze "..bestShortFuze.id, " score: "..currentScore)
end
end
end --for
------------ Manage best slot
save.Quickload(97)
if reportLevel>1 then print ("Fuzed slot "..bestSlot.." bb "..ScoreBBReturn(), "score ",ScoreReturn() ) end
if reportLevel>2 then printScoreStats() end
--print ("Residues that are not rebuilt are shown with 0:")
temp = getRow(bestSlot)
if reportLevel>2 then printRow(bestSlot) end --report what residues had been changed in this slot
lastScore = ScoreReturn()
lastScoreBB = ScoreBBReturn()
if lastScore > bestFoundScore then
--save.SaveSolution("defuze best slot"..bestSlot.." "..lastScore)
if reportLevel>2 then print ("saved best found slot"..bestSlot.." score "..lastScore) end
end
if (ScoreReturn() > startScore) then
save.Quicksave(98)
if reportLevel>1 then print ("Gained "..roundX(ScoreReturn()-startScore).." points. New score is", ScoreReturn()) end
--if reportLevel>1 then print ("Replaced old "..startScore..". Score improved to bb "..ScoreBBReturn(), "score: ", ScoreReturn()) end
--save.SaveSolution("defuse slot"..bestSlot.." bb "..lastScoreBB)
if reportLevel>3 then print ("saved drw slot"..bestSlot.." bb "..lastScoreBB) end
startScore = ScoreReturn()
--resetProtein = true
switchedTo = bestSlot
save.Quickload(bestSlot)
else
save.Quickload(100)
--if reportLevel>2 then print ("Best score "..startScore..". New one is not enough "..ScoreReturn()) end
if reportLevel>2 then print ("No gain, Restoring "..startScore ) end
end
if lastScore > bestFoundScore then
save.Quicksave(100) --save unfuzed version of the best solution found
bestFoundScore = lastScore
end
--print ("Best score "..startScore.." Fuze score /bb "..lastScore.." "..lastScoreBB, "preFuze score /bb "..ScoreReturn(), ScoreBBReturn() )
end --if (#shortFuzeScores>0)
end --if #remixBBScores > 0
if reportLevel>1 then print (string.format("----------------------%.2f sec.-------------------", executionTime3)) end
end --while
print ("Initial version and fuzed one are in undo stack")
Cleanup()
end -- function main()
------------------------------------------------------------------------------------------------Dialog-----------------------------------------------------------------------------------------------
function RequestOptions()
ask=dialog.CreateDialog("Options: Fast Assembly"..version)
ask.maxRebuildCount = dialog.AddSlider("Solutions to search",maxRebuildCount,1,15000,0) --up to 98 slots possible (change 36 to 98 if needed)
ask.slotsToFuze = dialog.AddSlider("Slots to fuze",slotsToFuze,1,36,0)
ask.remixNotRebuild=dialog.AddCheckbox("Remix instead of Rebuild", remixNotRebuild)
--ask.remixNotRebuild.value=false
ask.convertLoop=dialog.AddCheckbox("Convert to Loop", convertLoop)
ask.resetOnGain=dialog.AddCheckbox("Reset slots on Gain", resetOnGain)
ask.numPieces = dialog.AddSlider("numPieces",numPieces,2,math.floor(proteinLength/3),0)
ask.StartRebuild = dialog.AddSlider("Start Length",StartRebuild,2,proteinLength,0)
ask.EndRebuild = dialog.AddSlider("End Length",EndRebuild,2,proteinLength,0)
ask.multipleOverlap = dialog.AddSlider("Overlap",multipleOverlap,0,10,0)
ask.l1 = dialog.AddLabel("Fuze:")
ask.shakeOnRank=dialog.AddCheckbox("Shake solution on Rank stage", shakeOnRank)
ask.worstFirst=dialog.AddCheckbox("Worst First", worstFirst)
ask.doFuze=dialog.AddCheckbox("Store Fuzed", doFuze)
--ask.forceChange=dialog.AddCheckbox("Force Changes (loss)", forceChange)
ask.l2 = dialog.AddLabel("Do after X rounds with no Gain")
ask.slotsNoGain = dialog.AddSlider("Slots increase",slotsNoGain,-1,15,0)
ask.shiftFuze = dialog.AddSlider("Fuze local",shiftFuze,-1,15,0) --: change rebuild length when there are shiftNoGain iterations with no gain
ask.fuzeAfternoGain = dialog.AddSlider("Make Fuze",fuzeAfternoGain,-1,15,0) --: change rebuild length when there are shiftNoGain iterations with no gain
ask.forceChange = dialog.AddSlider("Force change (loss)",forceChange, -1,20,0) --
ask.shiftNoGain = dialog.AddSlider("Rebuild length",shiftNoGain,-1,15,0) --: change rebuild length when there are shiftNoGain iterations with no gain
ask.stopAfter = dialog.AddSlider("Stop After",stopAfter,-1,100,0) --: change rebuild length when there are shiftNoGain iterations with no gain
ask.fuze2=dialog.AddCheckbox("Fuze2", false)
ask.useSlot98=dialog.AddCheckbox("Unfuzed version", false)
ask.reportLevel = dialog.AddSlider("Report detalization", reportLevel,1,5,0)
ask.OK = dialog.AddButton("OK",1)
--ask.addSelections = dialog.AddButton("AddSelection",2)
ask.selectLoops = dialog.AddButton("SelLoops",3)
ask.Cancel = dialog.AddButton("Cancel",0)
returnVal=dialog.Show(ask)
if returnVal > 0 then
if returnVal==1 then maxRebuildCount=ask.maxRebuildCount.value end
remixNotRebuild=ask.remixNotRebuild.value
worstFirst=ask.worstFirst.value
shakeOnRank=ask.shakeOnRank.value
reportLevel=ask.reportLevel.value
resetOnGain=ask.resetOnGain.value
convertLoop = ask.convertLoop.value
slotsToFuze=ask.slotsToFuze.value
forceChange=ask.forceChange.value
doFuze=ask.doFuze.value
fuze2=ask.fuze2.value
stopAfter=ask.stopAfter.value
slotsNoGain=ask.slotsNoGain.value
fuzeAfternoGain=ask.fuzeAfternoGain.value
useSlot98 =ask.useSlot98.value
multipleOverlap = ask.multipleOverlap.value
forceChange=ask.forceChange.value
shiftNoGain=ask.shiftNoGain.value
shiftFuze=ask.shiftFuze.value
numPieces=ask.numPieces.value
StartRebuild=ask.StartRebuild.value
EndRebuild=ask.EndRebuild.value
----fix some vars if needed
if remixNotRebuild then
action="Remix"
if StartRebuild > 9 then
StartRebuild = 9
print ("StartRebuild more than 8 not available for Remix. Setting at 8")
end
if EndRebuild > 9 then
EndRebuild = 9
print ("EndRebuild more than 8 not available for Remix. Setting at 8")
end
end
if StartRebuild < EndRebuild then
temp = StartRebuild
StartRebuild = EndRebuild
EndRebuild = temp
end
selectionLength=StartRebuild
if slotsToFuze > maxRebuildCount then slotsToFuze = maxRebuildCount end
selectionStartArr, selectionEndArr = SplitProteinOnInit(numPieces) --create the selections
-- to add 0.5 to every highest subscore solution (round first decimal "bestSelectNum / 12". 10 is used as the first decimal floor basis)
else
print ("Canceled")
end
return returnVal
end
---------------------------------------------------------------------------------------------Rebuild/Remix---------------------------------------------------------------------------------------------
generation = 0
function RebuildToSlots(Slots2Rebuild)
generation = generation + 1
remixNum=#rebuildScores-1
j=0
rebuildIter=1
repeats_counter=0
undo.SetUndo(false)
localRebuildsCounter = 0
save.Quicksave(99)
if reportLevel>3 then print ("Rebuilding slots:",Slots2Rebuild) end
while (j<Slots2Rebuild) and (rebuildIter<7) do
j=j+1
structure.RebuildSelected(rebuildIter)
isDuplicated = CheckRepeats()
if isDuplicated then
j=j-1
rebuildIter = rebuildIter+1
repeats_counter=repeats_counter+1
else
totalRebuildsDone = totalRebuildsDone + 1
lastScore = ScoreReturn()
if shakeOnRank then TinyFuze() end --makes a small shake before ranking energy score of rebuild (if checkbox was selected).
currentRow = getRow(currentSlot)
currentRow = changeRowValues(currentRow, selectionStart, selectionEnd)
currentScore=ScoreReturn()
currentScoreBB=ScoreBBReturn()
parentID = getRemixBBIDBySlot(currentSlot)
descriminator = 0
if parentID~=0 and parentID~=nil then
descriminator = remixBBScores[parentID].bbDescriminator
end
--Two different scenarios:
--1) when the solution array is empty (first rebuilds just adds values to arrays),
--2) and when it is full (then replace the min value instead of inserting)
if #rebuildScores <= totalSlotsAvailable then
localRebuildsCounter = localRebuildsCounter + 1
solutionsFound = solutionsFound+1
rebuildScores[solutionsFound] = lastScore --saving score before TinyFuze to really check the repeats for next rebuilds
save.Quicksave(solutionsFound)
subScore = GetSolutionSubscores() --saving all the subscores of current solution in an Array to find later the one with the highest value on each subscore
local newEntry = {id = solutionsFound, scoreBB = currentScoreBB, bbDescriminator=descriminator, score = 0, subScores = subScore, generation=generation, rank = 0, rarity = 0}
table.insert(remixBBScores, newEntry)
--printActivity (currentRow)
addRow(currentRow)
addHistoryEntry(solutionsFound, currentSlot, currentScoreBB, currentRow, generation)
if reportLevel>3 then print (j..". Slot", solutionsFound, "backbone", currentScoreBB, "score", currentScore) end
else
--Add every solution to remixBBScores. Then sort and remove the worse one.
newElement= {id = 0, scoreBB=currentScoreBB, bbDescriminator=descriminator,score=0, subScores = GetSolutionSubscores(), generation=generation, rank = 0, rarity = 0}
table.insert(remixBBScores, newElement)
SortByBackbone(rarityManagement) --sort by rank
IdxToRemove = #remixBBScores
minSlotNo = remixBBScores[#remixBBScores].id
-- Do not add too many from one generation, - replace the worst record only among the same generation
if j>tryFirstX then
for i = #remixBBScores, 1, -1 do
if remixBBScores[i].generation == generation then
IdxToRemove = i
minSlotNo = remixBBScores[i].id
--localRebuildsCounter = localRebuildsCounter - 1
break
end
end
end
--if new solution is good enough then get id of the worst
if remixBBScores[IdxToRemove].id ~= 0 then
newElement.id = minSlotNo --it is a reference to remixBBScores last added
if reportLevel>4 then print ("minSlot, oldBB, oldRank, currBB", minSlotNo, IdxToRemove, remixBBScores[#remixBBScores].scoreBB, remixBBScores[#remixBBScores].rank, currentScoreBB) end
localRebuildsCounter = localRebuildsCounter + 1
save.Quicksave(minSlotNo)
rebuildScores[minSlotNo] = lastScore --saving score before TinyFuze to really check the repeats for next rebuilds
removeSlotStats(minSlotNo) --remove worst from history table
addHistoryEntry(minSlotNo, currentSlot, currentScoreBB, currentRow, generation) --add temporary to calc rarity
setRow(currentRow, minSlotNo)
if reportLevel>4 then printRow(minSlotNo) end
if reportLevel>3 then print (j..". Replaced Slot "..minSlotNo.." bb "..currentScoreBB.." score "..currentScore) end
end
table.remove(remixBBScores, IdxToRemove)
end
rebuildIter=1
end
--clean history table periodicaly to mantain the calculation speed
if totalRebuildsDone % 300 == 0 then cleanHistory() end
save.Quickload(99)
end --while
if reportLevel>3 and (repeats_counter > 5) then print ("repeats_counter",repeats_counter) end
undo.SetUndo(true)
return localRebuildsCounter, j
end
executionTime2 = 0
executionTime3 = 0
ratio_scoreBB = 0.5
ratio_score = 1
ratio_subScores = 0.5
ratio_rarity = 0.2
ratio_rarity_changed = ratio_rarity
function SortByBackbone(calcRarity)
local startTime2 = os.clock()
local bestSelectNum = #remixBBScores --rank points value
for i, remixBBScores in ipairs(remixBBScores) do
remixBBScores.rank=0 --clear array
end
--rank best backbone score
table.sort(remixBBScores, function(a,b) return (a.scoreBB + a.bbDescriminator)< (b.scoreBB + b.bbDescriminator) end)
for i, remixBBScores in ipairs(remixBBScores) do
remixBBScores.rank=remixBBScores.rank + i*ratio_scoreBB
end
--rank best energy score
-- Secondary sort by scoreBB if scores are equal (score==0)
table.sort(remixBBScores, function(a, b)
if a.score == b.score then return a.scoreBB < b.scoreBB
else return a.score < b.score end
end)
for i, remixBBScores in ipairs(remixBBScores) do
remixBBScores.rank=remixBBScores.rank + i*ratio_score
end
--rank rarity
if calcRarity~=nil and calcRarity==true then
--fill remixBBScores with rarity values
for i, remixBBScores in ipairs(remixBBScores) do
local historyIdx = historyIDbySlot[remixBBScores.id]
--print (remixBBScores.id, historyIdx)
remixBBScores.rarity = calculateRarityScore(historyIdx)
end
--rank by rarity
table.sort(remixBBScores, function(a,b) return a.rarity < b.rarity end)
for i, remixBBScores in ipairs(remixBBScores) do
remixBBScores.rank=remixBBScores.rank + i*ratio_rarity_changed
end
end
-- rank for solutions that have one of top subscores
highestSolutionIDs, subScoresNumber = GetHighestSolutionIDs()
for _, remixBBScores in ipairs(remixBBScores) do
local solutionID = remixBBScores.id
for _, solutionIDs in pairs(highestSolutionIDs) do
for _, id in ipairs(solutionIDs) do
if id == solutionID then
remixBBScores.rank = remixBBScores.rank + bestSelectNum/subScoresNumber * ratio_subScores
end
end
end
end
table.sort(remixBBScores, function(a,b) return a.rank > b.rank end)
for i, remixBBScores in ipairs(remixBBScores) do
remixBBScores.rank=math.floor(remixBBScores.rank * 10 + 0.5) / 10
end
if (reportLevel>4) and (calcRarity~=nil) then
for scorePart, solutionIDs in pairs(highestSolutionIDs) do
print("Solution ID with highest",scorePart," subscore:", table.concat(solutionIDs, ", "), subScoresNumber)
end
for i, remixBBScores in ipairs(remixBBScores) do
print("Slot",remixBBScores.id,"bb:", remixBBScores.scoreBB, "score", remixBBScores.score)
end
end
local endTime2 = os.clock()
executionTime2 = executionTime2 + endTime2 - startTime2
end
-- Function to get the list of SolutionIDs with the highest subscores for each score part
function GetHighestSolutionIDs()
local highestSolutionIDs = {}
local solutionSubscoresArray = {}
local counterx = 0
for _, entry in ipairs(remixBBScores) do
table.insert(solutionSubscoresArray, {id = entry.id, subScores = entry.subScores})
end
-- Iterate over each score parts
for scorePart, _ in pairs(solutionSubscoresArray[1].subScores) do
local maxSubscore = -999999
local minSubscore = 999999
local maxSolutionIDs = {}
-- Iterate over each solution subscores
for _, subscores in ipairs(solutionSubscoresArray) do
local subscore = subscores.subScores[scorePart]
local solutionID = subscores.id
-- find the max value between all the solutions for secific subscore
if subscore > maxSubscore then
maxSubscore = subscore
maxSolutionIDs = {solutionID}
elseif subscore == maxSubscore then
table.insert(maxSolutionIDs, solutionID)
end
-- Track the minimum subscore to prevent including in highestSolutionIDs array subscores with the same values for all the solutions
if subscore < minSubscore then
minSubscore = subscore
end
end
if maxSubscore ~= minSubscore then
highestSolutionIDs[scorePart] = maxSolutionIDs
counterx = counterx + #maxSolutionIDs
end
end
-- Return the list of SolutionIDs with the highest non-zero subscore for each score part
return highestSolutionIDs, counterx
end
function CheckRepeats()
currentScore=ScoreReturn()
isDuplicate=false
remixNum=#rebuildScores
for k=1, remixNum do
if rebuildScores[k] == currentScore then
isDuplicate=true
if reportLevel>3 then print (currentScore, "is duplicated to already found in slot",k) end
end
end
return isDuplicate
end
--set Descriminator for current element and all its children in remixBBScores array
function UpdateRemixBBDescr(idxRemixBB, descriminator,level)
--print (idxRemixBB, descriminator)
slot = remixBBScores[idxRemixBB].id
if level==nil then level=0 end
if reportLevel>4 then
local indent = string.rep("\t", level)
print(indent..slot, descriminator)
end
remixBBScores[idxRemixBB].bbDescriminator = descriminator
CurrentID = historyIDbySlot[slot]
--change descriminator for non-scored records only
if (history[CurrentID].score == 0) then
for _, offspringID in ipairs(history[CurrentID].offspring) do
if offspringID and history[offspringID] and history[offspringID].slot then
slot = history[offspringID].slot
if (slot ~= 0) then --if the record is active
idxRemixBB = getRemixBBIDBySlot(slot)
if idxRemixBB==nil then print ("-", offspringID, slot, CurrentID) end
UpdateRemixBBDescr(idxRemixBB, descriminator, level+1)
end
else
print("Error: history, offspringID, or slot is invalid")
end
end
end
end
-- Function to get an entry by its ID
function getRemixBBIDBySlot(slot)
if slot == 1 then return 0 end
for idx, remixBBScore in ipairs(remixBBScores) do
if remixBBScore.id == slot then
return idx
end
end
return nil -- Return nil if no entry with the given ID is found
end
function getHistoryIDBySlot(slot)
for idx, info in ipairs(history) do
if info.slot == slot then
return info.id
end
end
return nil -- Return nil if no entry with the given ID is found
end
----------------------------------------------Rebuild parameters
function adjustRebuilParameters(totalRebuildCount, selNum)
local numOfRebuilds
local tryFirstX
-- Calculate numOfRebuilds
if totalRebuildCount / selNum <= 27000 then
-- Case where sqrt(numOfRebuilds) <= 30
numOfRebuilds = (totalRebuildCount / selNum) ^ (2/3)
tryFirstX = math.min(math.sqrt(numOfRebuilds), 30)
else
-- Case where sqrt(numOfRebuilds) > 30
numOfRebuilds = math.floor(totalRebuildCount / (30 * selNum))
tryFirstX = 30
end
tryFirstX, numOfRebuilds = math.floor(tryFirstX + 0.5), math.floor(numOfRebuilds + 0.5)
local recalculatedTotal = tryFirstX * numOfRebuilds * selNum
return tryFirstX, numOfRebuilds, recalculatedTotal
end
--------------------------------------------------Set of service functions to monitor what parts of the protein in the accepted solution where actually rebuilt
--Using 2d array. Number of rows = number of occupied slots
--Number of columns = number residues. Each row contain zero value for the residues that werent changed and 1 if they were rebuild in this solution
-- Function to add a new row with'1' between startIndex and endIndex and 0 elsewhere
function addRow(newRow)
local numColumns = rebuiltResidues.numColumns or 0
-- Create a row with zeroes if newRow is not provided
if newRow == nil then
newRow = {}
for i = 1, numColumns do
table.insert(newRow, 0)
end
end
table.insert(rebuiltResidues, newRow)
end
-- Function to modify the row and increase values between selectionStart and selectionEnd by 1
function changeRowValues(row, selectionStart, selectionEnd)
for j = selectionStart, selectionEnd do
row[j] = row[j] + 1
end
return row
end
-- Function to modify the row and increase values between selectionStart and selectionEnd by 1
-- If 9 is reached, continue with 'a', 'b', ..., 'z', then 'A', 'B', ..., 'Z'
function changeRowValues2(row, selectionStart, selectionEnd)
for j = selectionStart, selectionEnd do
if j >= 1 and j <= #row then
if row[j] == 9 then
row[j] = 'a'
elseif type(row[j]) == 'string' then
if row[j] >= 'a' and row[j] < 'z' then
row[j] = string.char(string.byte(row[j]) + 1)
elseif row[j] == 'z' then
row[j] = 'A'
elseif row[j] >= 'A' and row[j] < 'Z' then
row[j] = string.char(string.byte(row[j]) + 1)
elseif row[j] == 'Z' then
row[j] = 'Z'
end
elseif type(row[j]) == 'number' and row[j] >= 0 and row[j] < 9 then
row[j] = row[j] + 1
end
end
end
return row
end
-- Function to perform a shallow copy of a table (array) and return the_copy and not the_link to the original row from rebuiltResidues[] in getRow()
local function shallowCopy(array)
local copy = {}
for i, v in ipairs(array) do
copy[i] = v
end
return copy
end
-- Function to get a copy of a row
function getRow(rowNumber)
if rebuiltResidues[rowNumber] then
return shallowCopy(rebuiltResidues[rowNumber])
end
end
function setRow(sourceRow, destRowNumber)
if rebuiltResidues[destRowNumber] then
for j = 1, #sourceRow do
rebuiltResidues[destRowNumber][j] = sourceRow[j]
end
end
end
-- Function to print the values of the specified row compactly
function resetRebuiltResidues(rowNumber)
if (reportLevel>1) then print ("Stats reset") end
if rowNumber~=nil and rowNumber~=0 then
currentrow = getRow(rowNumber) --save the row before reseting the table
end
rebuiltResidues = {}
rebuiltResidues.numColumns = proteinLength
addRow() --add and empty row for 'slot1'
if rowNumber~=nil and rowNumber~=0 then
setRow(currentrow, 1)
print (table.concat(rebuiltResidues[1], ""))
end
end
function printRow(rowNumber)
if rebuiltResidues[rowNumber] then
print(table.concat(rebuiltResidues[rowNumber], ""))
end
end
----------------------------------------------------------activityMap stats for the whole rebuild
-- Convertion between chars and number is activity map
function charToValue(c)
local ascii = string.byte(c)
if ascii >= string.byte('0') and ascii <= string.byte('9') then
return ascii - string.byte('0')
elseif ascii >= string.byte('a') and ascii <= string.byte('z') then
return ascii - string.byte('a') + 10
elseif ascii >= string.byte('A') and ascii <= string.byte('Z') then
return ascii - string.byte('A') + 36
end
end
function valueToChar(value)
if value >= 0 and value <= 9 then
return string.char(string.byte('0') + value)
elseif value >= 10 and value <= 35 then
return string.char(string.byte('a') + value - 10)
elseif value >= 36 and value <= 61 then
return string.char(string.byte('A') + value - 36)
else
if (reportLevel>2) then print ("ActivityMap element out of Range!!", value) end
return string.char(string.byte('Z'))
end
end
function normalizeScores(activityScores)
local minScore = math.huge
local maxScore = -math.huge
for _, score in ipairs(activityScores) do
if score < minScore then minScore = score end
if score > maxScore then maxScore = score end
end
local multiplier = 61 --math.min(61, maxScore) -- 61 iz max letter 'Z'
--print ("minScore, maxScore",minScore, maxScore, multiplier)
-- Normalize scores to fit in the range 0 to 61
local normalizedScores = {}
for i, score in ipairs(activityScores) do
normalizedScores[i] = math.floor(((score - minScore) / (maxScore - minScore))*multiplier + 0.5) -- round to the nearest integer
end
return normalizedScores
end
function printActivity(activityMap)
activityMapNormalized = normalizeScores(activityMap)
activityString = scoresToCharString(activityMapNormalized)
if (reportLevel>3) and (selNum>0) then print("Activity map:") end
if (reportLevel>2) and (selNum>0) then print(activityString) end
end
function scoresToCharString(normalizedScores)
local result = {}
for _, score in ipairs(normalizedScores) do
table.insert(result, valueToChar(score))
end
return table.concat(result)
end
-- Function to count top percent results
function getActivityMap()
local numElements = #rebuiltResidues[1]
local numTests = #rebuiltResidues-1
local activityScores = {}
decayFactor = 0.1
-- Initialize activityScores
for i = 1, numElements do
activityScores[i] = 0
end
-- Sum the activity scores weighted by exponential rank
for testIndex = 1, numTests do
local rank = remixBBScores[testIndex].rank
local weight = math.exp(-decayFactor * (rank - 1))
for elementIndex = 1, numElements do
local value = rebuiltResidues[testIndex][elementIndex]
activityScores[elementIndex] = activityScores[elementIndex] + value * weight
end
end
-- Calculate the average weighted score
local maxValue = 0
for elementIndex = 1, numElements do
activityScores[elementIndex] = activityScores[elementIndex] / numTests
if activityScores[elementIndex] > maxValue then maxValue = activityScores[elementIndex] end
end
activityScores = normalizeScores(activityScores)
return activityScores
end
--------------------------------------------------Other service functions--------------------------------------------------
-- Function to analyze scoreBB in the remixBBScores array
function printScoreStats()
if #remixBBScores == 0 then return nil end
local highestScore = remixBBScores[1].scoreBB
local lowestScore = remixBBScores[1].scoreBB
local sum = 0
local scores = {}
for _, entry in ipairs(remixBBScores) do
local scoreBB = entry.scoreBB
if scoreBB > highestScore then highestScore = scoreBB end
if scoreBB < lowestScore then lowestScore = scoreBB end
sum = sum + scoreBB
table.insert(scores, scoreBB)
end
meanScore = sum / #remixBBScores
table.sort(scores)
--local medianScore
if #scores % 2 == 0 then
medianScore = (scores[#scores / 2] + scores[#scores / 2 + 1]) / 2
else
medianScore = scores[math.ceil(#scores / 2)]
end
print("Highest bb "..highestScore.." lowest "..lowestScore..". Mean "..roundX(meanScore, 0).." median "..medianScore)
end
------------------------------------------------------------------------------------------Split protein------------------------------------------------------------------------------------------
--Split protein based on the Activity map. The low acitivty segments are the points where the split is most likely to happen
-- Function to calculate the split points
function calculateSplitPoints(activityMap, numPieces)
local activityScores = {}
for i = 1, #activityMap do
table.insert(activityScores, charToValue(activityMap:sub(i, i)))
end
local splitPoints = {}
local threshold = 20 -- Adjust threshold as needed
local constant = 30 -- Adjust constant as needed
for i = 1, #activityScores - 1 do
local randomFactor = math.random()
local activity = activityScores[i]
--activity is about 0..61. so approximately in 64% of cases the split will be accepted.
if randomFactor * (activity + constant) > threshold then
table.insert(splitPoints, i)
end
end
-- Ensure that we have exactly numPieces - 1 split points
while #splitPoints > numPieces - 1 do
table.remove(splitPoints, math.random(#splitPoints))
end
while #splitPoints < numPieces - 1 do
local candidate = math.random(2, #activityScores - 2)
if not table.contains(splitPoints, candidate) then
table.insert(splitPoints, candidate)
end
end
table.sort(splitPoints)
-- Ensure that each piece has at least 3 elements
for i = 1, #splitPoints do
if i == 1 then
if splitPoints[i] < 3 then
splitPoints[i] = 3
end
else
if splitPoints[i] - splitPoints[i - 1] < 3 then
splitPoints[i] = splitPoints[i - 1] + 3
end
end
end
-- Adjust last split point to ensure the last piece has at least 3 elements
if #activityScores - splitPoints[#splitPoints] < 3 then
splitPoints[#splitPoints] = #activityScores - 3
end
return splitPoints
end
-- Function to split the structure based on split points
function splitStructure(structure, splitPoints)
local pieces = {}
local startIdx = 1
for _, splitPoint in ipairs(splitPoints) do
table.insert(pieces, structure:sub(startIdx, splitPoint))
startIdx = splitPoint + 1
end
table.insert(pieces, structure:sub(startIdx))
return pieces
end
function sortSelections(selectionStartArr, selectionEndArr)
local combined = {}
for i = 1, #selectionStartArr do
table.insert(combined, {start = selectionStartArr[i], finish = selectionEndArr[i]})
end
table.sort(combined, function(a, b)
if a.start == b.start then return a.finish < b.finish
else return a.start < b.start
end
end)
for i = 1, #combined do
selectionStartArr[i] = combined[i].start
selectionEndArr[i] = combined[i].finish
end
end
-- Main function to split protein by selections
function SplitProteinByActivity(activityMap, numPieces)
selectionStartArr = {}
selectionEndArr = {}
for i = 1, multipleOverlap do
local splitPoints = calculateSplitPoints(activityMap, numPieces)
local startIdx = 1
for _, splitPoint in ipairs(splitPoints) do
table.insert(selectionStartArr, startIdx)
table.insert(selectionEndArr, splitPoint)
startIdx = splitPoint + 1
end
table.insert(selectionStartArr, startIdx)
table.insert(selectionEndArr, #activityMap)
end
sortSelections(selectionStartArr, selectionEndArr)
selNum = #selectionStartArr
return selectionStartArr, selectionEndArr
end
--------------------Initial protein split on selections
-- Box-Muller transform to generate normally distributed values to form random selLength
function randomGaussian(mean, stddev)
local u1 = math.random()
local u2 = math.random()
local z0 = math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2)
return z0 * stddev + mean
end
-- Function to generate a random value where the mean is X and 95% of values are within 30% of X
function randomValueWithConfidence(X)
local mean = X
local stddev = 0.15 * X
local value
repeat
value = randomGaussian(mean, stddev)
until value >= 3 -- Ensure the selection length is not less than 3
return math.floor(value)
end
-- Function to split the protein based on random selection lengths and overlap
function SplitProteinOnInit(numPieces)
local totalLength, selectionLength = proteinLength, math.floor(proteinLength / numPieces)
local overlapFraction = 0.4
local selectionStartArr, selectionEndArr, startIdx, direction = {}, {}, 1, 1
-- Determine the direction based on the global variable
if splitDirection == "backward" then
startIdx, direction = totalLength, -1
splitDirection = "forward"
else
splitDirection = "backward"
end
selNum = 0
while (direction == 1 and startIdx <= totalLength) or (direction == -1 and startIdx >= 1) do
selNum = selNum + 1
local currentSelectionLength = randomValueWithConfidence(selectionLength)
local endIdx = startIdx + (currentSelectionLength - 1) * direction
endIdx = math.max(1, math.min(totalLength, endIdx))
if direction == 1 and totalLength - endIdx < 2 then endIdx = totalLength end
if direction == -1 and endIdx < 3 then endIdx = 1 end
table.insert(selectionStartArr, (direction == 1 and startIdx or endIdx))
table.insert(selectionEndArr, (direction == 1 and endIdx or startIdx))
if (direction == 1 and endIdx >= totalLength) or (direction == -1 and endIdx <= 1) then break end
startIdx = endIdx + (direction == 1 and -1 or 1) * math.max(1, math.floor(currentSelectionLength * overlapFraction + 0.5))
end
return selectionStartArr, selectionEndArr
end
--------------------------------------------------------Number of rebuilds
--Calculate the number of rebuilds for each protein depending on Activity map
-- Function to calculate total activity for a selection
function calculateSelectionActivity(activityMap, startIdx, endIdx)
local totalActivity = 0
for i = startIdx, endIdx do
totalActivity = totalActivity + activityMap[i]
end
return totalActivity
end
-- Function to distribute rebuilds based on activity and number of elements
function DistributeRebuildsByActivity(activityMap, numOfRebuilds)
local selectionActivities = {}
local selectionLengths = {}
local totalActivity = 0
local totalLength = 0
-- Calculate activity and length for each selection
for i = 1, #selectionStartArr do
local activity = calculateSelectionActivity(activityMap, selectionStartArr[i], selectionEndArr[i])
local length = selectionEndArr[i] - selectionStartArr[i] + 1
table.insert(selectionActivities, activity)
table.insert(selectionLengths, length)
totalActivity = totalActivity + activity
totalLength = totalLength + length
end
-- Calculate number of rebuilds for each selection based on activity
local activityBasedRebuilds = {}
local distributedActivityRebuilds = 0
local numOfActivityRebuilds = math.floor(numOfRebuilds * 2 / 3) --distribute just 67% of rebuilds
for i = 1, #selectionStartArr do
local proportion = selectionActivities[i] / totalActivity
local rebuilds = math.floor(proportion * numOfActivityRebuilds)
table.insert(activityBasedRebuilds, rebuilds)
distributedActivityRebuilds = distributedActivityRebuilds + rebuilds
--print ("+++++, i, rebuilds, proportion", i, rebuilds, proportion, numOfActivityRebuilds, numOfRebuilds)
end
-- Calculate number of rebuilds for each selection based on length
local lengthBasedRebuilds = {}
local distributedLengthRebuilds = 0
local numOfLengthRebuilds = numOfRebuilds - numOfActivityRebuilds
for i = 1, #selectionStartArr do
local proportion = selectionLengths[i] / totalLength
local rebuilds = math.floor(proportion * numOfLengthRebuilds)
table.insert(lengthBasedRebuilds, rebuilds)
distributedLengthRebuilds = distributedLengthRebuilds + rebuilds
end
-- Combine the two distributions
local rebuildsDistribution = {}
for i = 1, #selectionStartArr do
rebuildsDistribution[i] = activityBasedRebuilds[i] + lengthBasedRebuilds[i]
end
-- Adjust for any rounding errors to ensure the total number of rebuilds equals numOfRebuilds
local distributedRebuilds = distributedActivityRebuilds + distributedLengthRebuilds
local remainingRebuilds = numOfRebuilds - distributedRebuilds
--print(numOfRebuilds .. ":", "Remaining Rebuilds:", remainingRebuilds .. "-" .. distributedRebuilds .. "=" .. distributedRebuilds)
while remainingRebuilds > 0 do
local randomIndex = math.random(1, #rebuildsDistribution)
rebuildsDistribution[randomIndex] = rebuildsDistribution[randomIndex] + 1
remainingRebuilds = remainingRebuilds - 1
end
return rebuildsDistribution
end
--------------------------------------------------------service funtions
-- Define a function to calculate energy score per number of segments for the selection
function CalculateSelectionEnergyPerSegment(start, finish)
local segmentCount = finish - start + 1
local energyScore = 0
for i = start, finish do
--energyScore = energyScore + current.GetSegmentEnergyScore(i)
energyScore = energyScore + GetSegmentBBScore(i)
end
return energyScore / segmentCount
end
-- Define a function to sort selections based on energy score per number of segments
function SortSelections(selectionStartArr, selectionEndArr)
-- Create a table to store selection indices, their corresponding energy scores per segment, start index, and end index
local sortedSelections = {}
-- Calculate and store energy scores per segment for each selection
for i = 1, #selectionStartArr do
local start = selectionStartArr[i]
local finish = selectionEndArr[i]
local energyScorePerSegment = CalculateSelectionEnergyPerSegment(start, finish)
table.insert(sortedSelections, {index = i, start = start, finish = finish, scorePerSegment = energyScorePerSegment})
end
-- Sort the sortedSelections table based on energy scores per segment in ascending/descending order
if worstFirst then
table.sort(sortedSelections, function(a, b) return a.scorePerSegment < b.scorePerSegment end) --ascending
else
table.sort(sortedSelections, function(a, b) return a.scorePerSegment > b.scorePerSegment end) --descending
end
-- Print the start index, end index, energy score per segment of each selection, and the sorted start and end indices
for _, selection in ipairs(sortedSelections) do
if reportLevel>3 then print("Start index:", selection.start, "- End index:", selection.finish, "- Energy", selection.scorePerSegment) end
end
-- Return the sorted arrays
local sortedStartArr = {}
local sortedEndArr = {}
for _, selection in ipairs(sortedSelections) do
table.insert(sortedStartArr, selection.start)
table.insert(sortedEndArr, selection.finish)
end
return sortedStartArr, sortedEndArr
end
--returns the id of the top X entries with the highest scoreBB values from the remixBBScores array:
function returnXinOrder(remixBBScores, X)
SortByBackbone()
-- Sort the array based on scoreBB in descending order
--table.sort(remixBBScores, function(a, b) return a.scoreBB > b.scoreBB end)
local topIds = {}
for i = 1, math.min(X, #remixBBScores) do
table.insert(topIds, remixBBScores[i].id)
end
if reportLevel>3 then print("topIds ".. #topIds..":", table.concat(topIds, " ")) end
return topIds
end
----------------------------------------------------------
function Cleanup(err)
print ("Cleanup")
currentScore=ScoreReturn()
behavior.SetClashImportance(endCI)
undo.SetUndo(true)
save.Quickload(1)
print ("Slot 1 score", ScoreReturn(),"bb",ScoreBBReturn())
if lastSlots~=nil and false then --disabled
for i=1, #lastSlots do
save.Quickload(lastSlots[i])
print ("Slot",lastSlots[i],"score", ScoreReturn(),"bb",ScoreBBReturn())
end
end
save.Quickload(100)
print ("Slot 100 score", ScoreReturn(),"bb",ScoreBBReturn())
save.Quickload(98)
print ("Slot 98 score", ScoreReturn(),"bb",ScoreBBReturn())
if reportLevel>1 then print ("Total Rebuilds Tested:", totalRebuildsDone) end
--if convertLoop then save.LoadSecondaryStructure() end
print (err)
end
-----------------------------------------------------------------------------------------------FUZE----------------------------------------------------------------------------------------------
function makeShake()
behavior.SetClashImportance(1)
structure.ShakeSidechainsAll (1)
end
function TinyFuze()
SelectionSphere()
behavior.SetClashImportance(0.05)
structure.ShakeSidechainsSelected(1)
behavior.SetClashImportance(1*CI)
SetSelection()
end
---
-- Function to parse the input string into a 2D array
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
-- Universal function that runs the logic on parsed input
function FuzeLocal(inputString)
local startTime3 = os.clock()
save.Quicksave(99)
local fuzeConfig = parseInput(inputString)
local score = ScoreReturn()
for idx, triplet in ipairs(fuzeConfig) do
behavior.SetClashImportance(triplet.clashImportance)
if triplet.shakeIter and triplet.shakeIter > 0 then
structure.ShakeSidechainsAll(triplet.shakeIter)
elseif triplet.shakeIter and triplet.shakeIter < 0 then
structure.LocalWiggleAll(-triplet.shakeIter, false, true)
end
behavior.SetClashImportance(triplet.clashImportance2 )
if triplet.wiggleIter and triplet.wiggleIter > 0 then
structure.LocalWiggleAll(triplet.wiggleIter)
elseif triplet.wiggleIter and triplet.wiggleIter < 0 then
structure.LocalWiggleAll(-triplet.wiggleIter, true, false)
end
if triplet.clashImportance2==1 then
if ScoreReturn() > score then
score = ScoreReturn()
save.Quicksave(99)
else
save.Quickload(99)
end
end
end
local endTime3 = os.clock()
executionTime3 = executionTime3 + endTime3 - startTime3
return score -- Return the score
end
---
function FuzeLocalGlobal1delete()
behavior.SetClashImportance(0.05)
structure.ShakeSidechainsAll(1)
structure.LocalWiggleAll(5)
behavior.SetClashImportance(1)
structure.ShakeSidechainsAll(2)
undo.SetUndo(true)
structure.LocalWiggleAll(20)
undo.SetUndo(false)
end
function FuzeLocalGlobal2()
behavior.SetClashImportance(0.05)
structure.ShakeSidechainsAll(1)
structure.WiggleAll(5)
behavior.SetClashImportance(1)
structure.ShakeSidechainsAll(2)
undo.SetUndo(true)
structure.WiggleAll(20)
undo.SetUndo(false)
end
function Fuze_low() --not used
currentScore1=ScoreReturn()
behavior.SetClashImportance(0.05)
structure.ShakeSidechainsAll (1)
structure.WiggleAll (5)
behavior.SetClashImportance(0.05)
structure.WiggleAll (3)
behavior.SetClashImportance(1*CI)
undo.SetUndo(true)
structure.WiggleAll (20)
undo.SetUndo(false)
if reportLevel > 3 then print ("fuze finished at", ScoreReturn()) end
if convertLoop then save.LoadSecondaryStructure() end
end
function Fuze2()
currentScore1=ScoreReturn()
if bestSlot==0 then
bestSlot=99
print ("++++++++++++++++NO BESTSLOT VALUE!!!!!!!!!!!!!")
end
behavior.SetClashImportance(1)
structure.ShakeSidechainsAll (1)
behavior.SetClashImportance(0.05)
structure.WiggleAll (10)
behavior.SetClashImportance(0.05)
structure.ShakeSidechainsAll (2)
behavior.SetClashImportance(1)
structure.WiggleAll (20)
if ScoreReturn() > currentScore1 then
save.Quicksave(bestSlot)
else save.Quickload(bestSlot) end
if reportLevel > 3 then print ("fuze finished at", ScoreReturn()) end
if convertLoop then save.LoadSecondaryStructure() end
end
function Fuze()
currentScore1=ScoreReturn()
save.Quicksave(99)
behavior.SetClashImportance(0.05)
structure.ShakeSidechainsAll (1)
structure.WiggleAll (2)
behavior.SetClashImportance(0.25)
structure.WiggleAll (2, 0, 1)
structure.WiggleAll (2)
behavior.SetClashImportance(1)
structure.ShakeSidechainsAll (1)
undo.SetUndo(true)
structure.WiggleAll (5, 1, 0)
undo.SetUndo(false)
if ScoreReturn() > currentScore1 then
save.Quicksave(99)
currentScore1=ScoreReturn()
end
behavior.SetClashImportance(0.25)
structure.ShakeSidechainsAll (1)
structure.WiggleAll (2)
behavior.SetClashImportance(1)
undo.SetUndo(true)
structure.WiggleAll (20)
undo.SetUndo(false)
if ScoreReturn() > currentScore1 then
save.Quicksave(99)
currentScore1=ScoreReturn()
end
if reportLevel > 2 then print ("fuze finished at", ScoreReturn(), "bb", ScoreBBReturn()) end
if convertLoop then save.LoadSecondaryStructure() end
end
---------------------------------------------------------------General service functions
function roundX(x, digits)
digits = digits or 1
local multiplier = 10 ^ digits
return math.floor(x * multiplier) / multiplier
end
--[[ function returning the max between two numbers --]]
function max(num1, num2)
if (num1 > num2) then result = num1
else result = num2 end
return result
end
-- BackBone-Score is just the general score without clashing. Clashing is usefull to ignore when there is need to rank a lot of the Rebuild solutions very fast without the Fuze.
function ScoreBBReturn()
x = 0
for i=1, proteinLength do
x = x + current.GetSegmentEnergySubscore(i, "Clashing")
end
x = current.GetEnergyScore() - x
return x-x%1 --no decimals
end
-- Create array of the Scores for every Subscore of the puzzle
function GetSolutionSubscores(SolutionID)
local scoreParts = puzzle.GetPuzzleSubscoreNames()
local solutionSubscores = {}
-- Iterate over each score part
for _, scorePart in ipairs(scoreParts) do
local currentSubscore = 0
-- Iterate over each segment and calculate subscore
for segmentIndex = 1, proteinLength do
currentSubscore = currentSubscore + current.GetSegmentEnergySubscore(segmentIndex, scorePart)
end
solutionSubscores[scorePart] = currentSubscore
end
return solutionSubscores
end
function GetSegmentBBScore(i)
return current.GetSegmentEnergyScore(i) - current.GetSegmentEnergySubscore(i, "Clashing")
end
-------------------------------------------------------------------------Selection functions
--selections
function FindSelection()
selNum=0
for k=1, proteinLength do
if selection.IsSelected(k) then
--find for selection start
if k==1 then
selNum=selNum+1
selectionStartArr[selNum]=k
else
if not selection.IsSelected(k-1) then
selNum=selNum+1
selectionStartArr[selNum]=k
end
end
--find the selection end
if k==proteinLength then
selectionEndArr[selNum]=k
else
if not selection.IsSelected(k+1) then selectionEndArr[selNum]=k end
end
end
end
--if no selection, select residues 3-6
if selNum==0 then
selectionStartArr[1]=math.min(5,proteinLength)
selectionEndArr[1]=math.min(13,proteinLength)
end
end
function SetSelection()
selection.DeselectAll()
for k=1, proteinLength do
if (k>=selectionStart) and (k<=selectionEnd) then
selection.Select (k)
end
end
end
function SelectLoops()
selection.DeselectAll()
looplength=0
--select all loops with length >= 3
for k=1, proteinLength do
--print (structure.GetSecondaryStructure(k))
if structure.GetSecondaryStructure(k) == 'L' then
looplength=looplength+1
selection.Select (k)
else
if (looplength>0) and (looplength<3) then --deselect if loop is too short
for j=1, looplength do
selection.Deselect (k-j)
end
end
looplength=0
end
if (k==proteinLength) and (looplength==1) then selection.Deselect (k) end
end
end
function SetAllSelections()
selection.DeselectAll()
for j=1, selNum do
selectionStart=selectionStartArr[j]
selectionEnd=selectionEndArr[j]
if (selectionEnd-selectionStart < 0) then
temp = selectionStart
selectionStart = selectionEnd
selectionEnd = temp
end
if (reportLevel>3) then print ("Setting selection"..j.."/"..selNum..": ", selectionStart.."-"..selectionEnd) end
for k=1, proteinLength do
if (k>=selectionStart) and (k<=selectionEnd) then
selection.Select (k)
end
end
end
end
-- Select everything in radius sphereRadius(=8) near any selected segment.
function SelectionSphere()
--dump selection to array
selectedSegs={}
for k=1, proteinLength do
if selection.IsSelected(k) then
selectedSegs[k]=1
else
selectedSegs[k]=0
end
end
for k=1, proteinLength do
for j=1, proteinLength do
dist_str = structure.GetDistance(k, j)
if (selectedSegs[j] == 1) and (dist_str < sphereRadius) then
selection.Select(k)
end
end
end
end
-- Function to print selections with the number of rebuilds for each
function printSelections()
if (reportLevel>3) then print("Creating "..#rebuildsDistribution.." selections") end
for i = 1, #selectionStartArr do
local rebuilds = rebuildsDistribution[i]
if (reportLevel>2) then print("Selection " .. i .. ": " .. selectionStartArr[i].." - "..selectionEndArr[i]," Number of Rebuilds: " .. rebuilds) end
end
end
----------------------------------------------------------------------------------------Inheritance statistics system---------------------------------------------------------------------------------
history = {}
historySize = 0
uniqueID = 1
historyIDbySlot = {}
historyIDbySlot[1] = 1
lastBestSlot = 0
newMask = {}
for i = 1, proteinLength do table.insert(newMask, 0) end
table.insert(history, {id = 1, slot = 1, parent = nil, offspring = {}, scoreBB = ScoreBBReturn(), score = ScoreReturn(), changeMask = newMask, generation = 0})
--history[currentID] = {id = currentID, slot = slot, parent = parentID, offspring = {}, scoreBB = scoreBB, changeMask = mask or {}}
function addHistoryEntry(slot, parentSlot, scoreBB, mask, generation)
--if we fill the array after the re set then use last bestslot as a parent
if parentSlot == 1 and lastBestSlot ~= 0 then
parentSlot = lastBestSlot
end
uniqueID = uniqueID + 1
historySize = historySize + 1
local currentID = uniqueID
parentID = historyIDbySlot[parentSlot]
historyIDbySlot[slot] = currentID
if not history[currentID] then
history[currentID] = {id = currentID, slot = slot, parent = parentID, offspring = {}, scoreBB = scoreBB, score = 0, changeMask = mask, generation = generation}
end
if parentID then
if parentID == currentID then
if reportLevel > 2 then print("Error: A slot cannot be its own parent. Skipping invalid parent assignment.", parentSlot, parentID) end
history[currentID].parent = nil
else
if not history[parentID] then
history[parentID] = {id = parentID, slot = 0, parent = nil, offspring = {currentID}, scoreBB = nil, score = 0}
end
table.insert(history[parentID].offspring, currentID)
end
end
return currentID
end
function updateHistoryArray (slot, currentScore, currentBBScore)
idx = historyIDbySlot[slot]
--history[idx].scoreBB = currentBBScore
history[idx].score = currentScore
end
function resetInheritanceTable(slot)
--we save bestSlot on reset to use it in addHistoryEntry()
--when filling table with parentSlot==1 (to use ==currentBestSlot instead to save the inheritance)
lastBestSlot = slot
local currentID = historyIDbySlot[slot]
--remove everything except current and 1st ID
for idx, info in pairs(history) do
if idx~= 1 then
history[idx].slot = 0
end
end
end
function removeSlotStats(slot)
local id = historyIDbySlot[slot]
if history[id] then
history[id].slot = 0
end
end
function removeHistoryByID(id)
--first, remove offspring from the parentID record
parentID = history[id].parent
local offspring = history[parentID].offspring
if #offspring > 0 then
table.remove(offspring, #offspring)
end
historySize = historySize -1
history[id] = nil
end
---------------------------------------Calculate Rarity
cumulativeChangeMasks = {} --store mask for each id to optimize speed
relationScores = {} -- store relation scores between pairs of elements
function combineMasks(mask1, mask2)
local combined = {}
for i = 1, #mask1 do
combined[i] = math.max(mask1[i], mask2[i])
end
return combined
end
function calculateRelation(mask1, mask2)
local totalChanges = #mask1
local diffCount = 0
for i = 1, totalChanges do
if mask1[i] ~= mask2[i] then
diffCount = diffCount + 1
end
end
return 1 - (diffCount / totalChanges)
end
local function getCumulativeChangeMask(id)
if cumulativeChangeMasks[id] then
return cumulativeChangeMasks[id]
end
local changeMask = history[id].changeMask or {}
local parentId = history[id].parent
while parentId do
local parentChangeMask = history[parentId].changeMask or {}
changeMask = combineMasks(changeMask, parentChangeMask)
parentId = history[parentId].parent
end
cumulativeChangeMasks[id] = changeMask
return changeMask
end
local function getRelationScore(idA, idB)
local key = idA < idB and idA .. "-" .. idB or idB .. "-" .. idA
if relationScores[key] then
return relationScores[key]
end
local maskA = getCumulativeChangeMask(idA)
local maskB = getCumulativeChangeMask(idB)
local relationScore = calculateRelation(maskA, maskB)
relationScores[key] = relationScore
return relationScore
end
function calculateRarityScore(id)
local totalRelation = 0
local count = 0
for otherID, _ in pairs(history) do
--print (id, otherID, history[otherID].slot)
if (id ~= otherID) and (history[otherID].slot ~= 0) then
local relationScore = getRelationScore(id, otherID)
totalRelation = totalRelation + relationScore
count = count + 1
end
end
if count == 0 then return 1 end
if totalRelation == 0 then return 1 end
local rarityScore = count / totalRelation - 1
if reportLevel > 3 then print("ID:", id, "Total Relation:", totalRelation, "Count:", count, "Average Relation:", averageRelation, "Normalized Relation:", normalizedRelation) end
return rarityScore
end
----------------------------------------------------------
local function getAncestors(id)
local ancestors = {}
local visited = {} -- Track visited nodes to detect cycles
while id do
if visited[id] then
if reportLevel > 2 then print("Cycle detected! Stopping ancestor retrieval to avoid infinite loop.") end
break
end
table.insert(ancestors, id)
visited[id] = true
id = history[id] and history[id].parent or nil
if reportLevel > 4 then print("Retrieving ancestor, current id:", id) end
end
return ancestors
end
function countDescendants(id)
local function countRecursive(currentID)
--print ("currentID",currentID, id)
if not history[currentID] then return 0 end -- Add this check to prevent crashes
local count = 0
if (#history[currentID].offspring > 0) then
for _, offspringID in ipairs(history[currentID].offspring or {}) do
if history[offspringID] then -- Check if offspringID exists in history
if history[offspringID].slot ~= 0 then -- Do not count removed records
count = count + 1
end
-- Continue recursion even if current isn't active, to find active descendants
count = count + countRecursive(offspringID)
else
--print("!") UNCOMMENT TO FIND THE BUG!!!!!!!!!!!!!!!!!!!!!!!!!!!!
end
end
end
return count
end
return countRecursive(id)
end
function calculateDescendantPercentage(id)
local totalDescendants = countDescendants(id)
local totalSlots = 0
for idx, _ in pairs(history) do
if history[idx].slot ~= 0 then
totalSlots = totalSlots + 1
end
end
if totalSlots == 0 then totalSlots = 1 end -- prevent div zero
return (totalDescendants / totalSlots) * 100
end
local function printHistoryTree(id, level)
local info = history[id]
--print (id, level, info.slot)
if info then
local indent = string.rep("\t", level)
if info.slot ~= 0 then
local parentID = info.parent or "None"
local scoreBB = info.scoreBB
--local rarity = calculateBreedRarityScore(id)
local rarity = calculateRarityScore(id)
remixBBID = getRemixBBIDBySlot (info.slot)
if remixBBID == 0 then rarity, rank, descriminator = 0, "", 0
else
--rarity = remixBBScores[remixBBID].rarity
rank = remixBBID
descriminator = remixBBScores[remixBBID].bbDescriminator
descriminator = (descriminator>=0) and string.format("+%.0f",descriminator) or string.format("%.0f",descriminator)
if rank < 10 then rank = tostring (rank).."++"
else rank = tostring (rank) end
end
local descendantPercentage = calculateDescendantPercentage(id)
local descendantText = ""
if descendantPercentage > 0 then descendantText = string.format(", Descendant%%: %.2f", descendantPercentage) end
print(string.format("%s%sSlot %2d: ID: %3d, ParentID: %3s, ScoreBB: %.0f %s, score: %.0f, Gen: %3d Rarity: %.2f%s", rank, indent, info.slot, id, parentID, scoreBB, descriminator, info.score, info.generation, rarity, descendantText ))
-- Sort offspring by scoreBB before printing them
table.sort(info.offspring, function(a, b)
local scoreA = history[a] and history[a].scoreBB or 0
local scoreB = history[b] and history[b].scoreBB or 0
return scoreA > scoreB
end)
for _, offspringID in ipairs(info.offspring) do
printHistoryTree(offspringID, level + 1)
end
else
--if it has some active offsprings, draw an empty line instead and continue recursion
if #info.offspring > 0 then
print(indent..string.rep("- ", 15))
for _, offspringID in ipairs(info.offspring) do
printHistoryTree(offspringID, level + 1)
end
end
end --if info.slot
end --if info
end
-- Clean the history table from the records with no active offsprings
function cleanHistory()
local toRemove = {}
for id, info in pairs(history) do
if (#info.offspring > 0) then
local totalDescendants = countDescendants(id)
if totalDescendants == 0 then
if (info.slot ==0) then
table.insert(toRemove, id)
else
history[id].offspring = {}
end
end
end
end
-- Remove collected IDs
for _, id in ipairs(toRemove) do
history[id] = nil
historySize = historySize -1
end
end
counterx = 0
-- Prints the entire history in a tree structure
function printHistory()
cleanHistory()
print ("Print history tree ("..historySize..") : ")
--search for the first record to start recursion
for id, info in pairs(history) do
if not info.parent then
printHistoryTree(id, 0)
end
end
end
-------------------------------------------------------------
xpcall ( main , Cleanup )