Recipes, often very long-running recipes, have become an integral part of Foldit for most players.
Foldit players can choose from a wide variety of recipes, from a many different authors. Recipes vary a great deal in how much information they log and the amount of cleanup they do when they are terminated.
This article discusses a number of simple steps that can be taken to standardize most recipes. The goal of these steps is to provide a more useful log, and to leave the protein in a more consistent state at the end of the recipe.
The classic recipe "Rav3n_pl Fuzes" has been reworked using the points suggested here. The changes can be seen in "Rav3n_pl Fuzes v1.5", Foldit recipe 100007.
A new set of functions called "chunquePoint" implements standardized score and time reporting.
What does a model recipe do?
All recipes have their frustrations. Some well-known recipes report lots of information about what they're doing, but make it hard to see the current score. Other recipes may have more concise logging, but may leave unexpected bands or frozen segments if they are canceled. Still other recipes may cause a Foldit client crash, without leaving many clues behind.
A model recipe tries to alleviate some of these problems by paying careful attention to logging, and by using a cleanup routine at the end of the recipe.
The key points for a model recipe:
- Recipes should use the xpcall routine (model 000).
- At the beginning, a recipe should log its:
- name and version, the puzzle name, and the track name (model 010)
- key options, particularly if these are set using a dialog (model 020)
- starting score and the start time (model 030)
- Most recipe have one or more major cycles or loops.
- At the start of a cycle, the recipe should log the score, the total gain since the start of the recipe, and the time at the start of each major loop (model 040).
- Within a cycle, the recipe may log major sections (model 050).
- In a cycle, the recipe should avoid normally logging each detailed step, such as each segment processed. The recipe should report significant gains, including information such as the segment number being worked on, the gain, and the current score (model 060). If there's no gain for a period of time, the recipe may issue a message to show that it's still working normally.
- At the end of each processing cycle, the recipe should log the score, the gain for the cycle, and the time. The recipe should also log the elapsed time and some measure of performance, such as points gained per hour (model 070).
- Some recipes may have nested processing cycles.
- Each inner cycle should have the same logging as the outer cycle.
- For inner cycles, the "total gain" reported may either be from the start of the recipe or the start of the series of inner cycles. For most recipes, reporting total gain from the start of the recipe is the least confusing. For multi-start puzzles, however, reporting the total gain for each start makes more sense.
- The recipe should use a cleanup routine. If the recipe runs to completion, it should call the cleanup routine as its last step (model 090).
- The cleanup routine should again print the recipe name and version, the puzzle name, and the track (model 100).
- The cleanup routine should parse the error parameter to determine whether the recipe was canceled( model 120).
- The cleanup routine should print the line number and error message if an error occurred (model 120).
- The cleanup routine should delete bands, unfreeze segments, reset clashing importance, restore the best score, and so on (model 130).
- The cleanup routine should report the final score, the total gain since the start of the recipe, the elapsed time, and a performance measure such as points per hour (model 150).
- Archiving major achievements in a puzzle note (model 160)
The sections below provide code samples from "Rav3n_pl Fuzes v1.5", Foldit recipe 100007.
Model 000 -- xpcall(), main(), and cleanup()
Recipes which hope to change the score should use the xpcall function. This is especially true for recipes which normally run more than a few minutes.
The xpcall() function takes two parameters, both of which are the names of functions that you define. The first function is the "main" function, which should contain most of the recipe's logic. The second function is a cleanup routine, which gets called if the recipe is canceled or encounters an error in the "main" function.
Unfortunately, some recipes cause a Foldit client crash. The "cleanup" function does not get called if the client crashes.
The xpcall() function should be the last executable line of the recipe:
-- main call xpcall ( Fuzes, cleanup ) --end of script
In Rav3n_pl Fuzes, the "main" function is called "Fuzes":
function Fuzes () ... end
Other recipes may call it "main" or something else.
The "cleanup" function is simply called "cleanup", but again, any name can be used.
The cleanup function has one parameter, an error message:
function cleanup ( error )
As discussed in detail below, model recipes should call the cleanup routine at the end of the "main" routine, but leaving off the error parameter. This allows for consistent cleanup and score reporting without duplicating code.
Model 001 -- standard name and version
Somewhere near the top, the recipe should provide a string containing its name and version. In this example, the string is called "ReVersion". This variable can then be used for logging and dialog titles.
-- -- model 001 -- standard recipe name and version -- Recipe = "Rav3n_pl Fuzes" Version = "1.5" ReVersion = Recipe .. " v." .. Version.
Tip: while it's not a requirement, it's good to define and initialize all your global variables at the beginning of the recipe. In LUA, all variables are global unless you explicitly mark them as "local". This is different than other similar C-family languages, and may cause confusion.
Model 010 -- log recipe name, puzzle name, and track
Before starting work the puzzle should log its name and version, the puzzle name, and the current track. Here the "ReVersion" global variable provides the recipe name and version.
-- -- model 010 - startup - print recipe name, puzzle name, track -- print ( ReVersion ) print ( "Puzzle: " .. puzzle.GetName () ) print ( "Track: " .. ui.GetTrackName () )
If the recipe has a user dialog, it probably makes sense to log this information after the dialog completes.
Model 020 -- recipe parms
The next step is to log at least the key recipe parameters. The parameters here are all global variables that can be adjusted in a user dialog.
-- -- model 020 - startup - print recipe parms -- print ( "options:" ) print ( "minimum gain per cycle = " .. daCapo ) print ( "Wiggle Factor = " .. WF ) if ( useIRFF ) then print ( "max fuzz factor = " .. IRFF ) end print ( "--" )
Model 030 -- starting score
This step introduces the chunquePoint package. The beginChunque function records the starting score and time, returning it in "startChunque", a global variable. If LUA had types, this object would be a "chunque".
The first parameter to beginChunque is the parent chunque. At the start of the recipe, the parent chunque is nil.
The second parameter to beginChunque is the chunque name. This name is logged and also recorded in the chunque. This example uses "Start" for the initial chunque.
The optional third parameter to beginChunque is the custom score function. Here, Score() is an existing function in "Rav3n_pl Fuzes". If the score function is omitted, the function current.GetEnergyScore() is used.
The optional fourth parameter to beginChunque is the pose, and is omitted in these examples. You should specify a custom score function if you specify the pose.
-- -- model 030 - startup - print start time, start score -- startChunque = beginChunque ( nil, "Start", Score )
The beginChunque function issues a log message like this one:
Start, score: 3571.426, 10/13/14 16:12:05
Model 040 -- cycle start score
Rav3n_pl Fuzes repeats a set of fuse operations until the gain falls below the specified threshhold.
At the beginning of each of these fusing cycles, another call to beginChunque logs the score and the time.
In this case, the first parameter is the chunque created at the start of the recipe, "startChunque". The second parameter describes what is happening in this cycle. The third parameter is the custom score function, as before.
-- -- model 040 - start cycle - print cycle #, current score -- local runName = "Fuze run " .. fuzeCount local begChunque = beginChunque ( startChunque, runName, Score )
This produces a log line:
Fuze run 1, score: 3571.426, total gain: 0, 10/13/14 16:12:05
For the first cycle, the log line is redundant to the "start" call.
Model 050 -- log major sections
On each cycle, Rav3n_pl Fuzes tries six different fuse variants.
For clarity, each fuse is introduced with a log message:
-- -- pink fuse -- print ( "Run " .. fuzeCount .. ", Fuze of "..maxrun ) --model 050 - log major sections
Model 060 -- report significant gains
Each cycle of Rav3n_pl Fuzes is fairly short, so it's probably enough to report the gains:
local gGain = Score () - bestScore if gGain > 0 then -- -- model 060 - report significant gains -- if gGain > 0.001 then print ( "Gained another " .. round ( gGain ) .. ", score = " .. round ( Score() ) ) end
Longer-running recipes should also report the score along with the gain.
Model 070 -- cycle end score
The endChunque function reports the gain for either a given cycle or the whole recipe, depending on the starting chunque specified. In this example, "begChunque" is a local variable, created by the beginChunque() function at the beginning of the cycle.
-- -- model 070 - end cycle - print cycle #, time, gain -- endChunque ( begChunque, "end run " .. fuzeCount, Score )
The call to endChunque creates two log lines:
end run 1, score: 7918.597, gain: 4347.17, 10/13/14 16:27:40 end run 1, hours: 0.069, points per hour: 62850.65
Model 090 -- exit via the cleanup routine
The last step in the main() function is to call the cleanup() function. This call to cleanup() does not pass an error parameter, indicating normal completion of the recipe.
-- -- model 090 - exit via the cleanup routine -- cleanup ()
Model 100 -- recipe name plus reason code
The cleanup routine should log the recipe name and version, the puzzle name, and the track.
A reason code has been added to the recipe name, as explained in the following steps. The reason code will be "complete", "cancelled", or "error".
print ( ReVersion .. " " .. reason ) print ( "Puzzle: " .. puzzle.GetName () ) print ( "Track: " .. ui.GetTrackName () )
Model 120 -- civilized error reporting
The first parameter passed to a cleanup routine, "error" in this example, can be parsed to determine whether the routine was canceled or ended due to a scripting error. If "error" is nil, then the script ended normally.
-- -- model 120 - civilized error reporting, -- thanks to Jean-Bob -- local reason local start, stop, line, msg if error == nil then reason = "complete" else start, stop, line, msg = error:find ( ":(%d+):%s()" ) if msg ~= nil then error = error:sub ( msg, #error ) end if error:find ( "Cancelled" ) ~= nil then reason = "cancelled" else reason = "error" end end
For an error, the recipe line number and error message can be reported in a more legible format:
if reason == "error" then print ( "Unexpected error detected" ) print ( "Error line: " .. line ) print ( "Error: \"" .. error .. "\"" ) end
Model 130 -- reset the state
This section depends on which tools the recipe uses. Rav3n_pl Fuzes changes the clashing importance, and makes selections. So the cleanup should reset clashing importance and clear selections. Other recipes may need to remove bands, unfreeze, or attempt to remove cut points.
If the recipe crashed or was canceled (the "error" parameter is not nil), most recipes should restore the best score. Here, the best score has been quick saved in slot 3.
-- -- model 130 - reset clash importance, clear selections, restore structures, etc. -- behavior.SetClashImportance ( 1 ) selection.DeselectAll () if error ~= nil then save.Quickload ( 3 ) end
Model 150 -- final report
Regardless of how the recipe ended, the cleanup routine should attempt to report the final score, gain, and related measures.
If the global "startChunque" is not nil, the inital beginChunque function was called, so it's probably safe to call endChunque.
-- -- model 150 - report the final time, score, gain, elapsed time, and points per hour -- if startChunque ~= nil then endChunque ( startChunque, "Final", Score ) end
The call to endChunque produces two log lines:
Final, score: 7941.985, gain: 4370.558, 10/13/14 16:34:38 Final, hours: 0.185, points per hour: 23553.905
For a normal completion, these lines may again be somewhat redundant with the output from the end of the last cycle.
Model 160 -- archive report
The cleanup routine should archive recipe used and main achievements in a puzzle note.
-- -- model 160 - archive player, recipe, scores -- thaks to pauldunn --
structure.SetNote(note_number,string.format("(%s) %.3f + %s(%i) = %.3f",
The note should remain short in order not to disturb the reading when enabling note on selection interface screen. The next available note can be selected with the following routine:
for seg=structure.GetCount(),1,-1 do
if structure.GetNote(seg)~="" then break end
Example display in a note for segment:
(pauldunn) 3571.426 + Rav3n_pl Fuzes v1.5 (6) = 7941.985
Example display in 3 notes, after 2 compliant recipes, a hand fold (or non compliant recipe) and a last compliant recipe have been run on a puzzle:
[Note 1] (pauldunn) 3571.426 + Rav3n_pl Fuzes v1.5 (6) = 7941.985
[Note 2] (jeff101) 7941.985 + Tvdl enhanced DRW 2.8.1 (3) = 8943.567
[Note 3] (madde) 9526.456 + Rav3n_pl Fuzes v1.5 (5) = 9527.956
This archive is useful for analyzing gaining path to a top solution. It was for example used for a published paper co-autored by Foldit Players in 2016.
The techniques described in this article can easily be incorporated into most recipes written using version 2 of the Foldit LUA implementation (V2 LUA).
The chunquePoint "package" of functions simplifies standardized score reporting, and provides basic performance measurements in terms of elapsed time in hours and points gained per hour. The beginChunque() and endChunque() functions can be used to replace similar logic in many recipes. The goal is to provide a more consistent user experience and to free recipe writers to concentrate on making the score go up.