Code
--[[
* Power Shift 1.0.1
* Original Author: Brow42
* Version 1.0 Sep. 19 2012
* This script will help you shift a long part of the
* protein using the tweak tool. First, use either
* selection or freeze to mark all the segments you want
* to shift. The script will turn all of those into
* sheets. It will also add at least 2 loops to both end
* which are required by tweak. It will save the original
* structure in the notes for later.
* After running the script the first time, you can now
* use the tweak tool on the sheets to shift them. After
* you have shifted, run the script again. The script should
* automatically determine how far you shift. It will then
* apply the original secondary structure to their original
* positions in space.
* Version 1.0.1 Sep 23, 2012 brow42
* Adjusted selection so the tweak tool shows up in selection interface
--]]
title = "Power Shift 1.0"
NoteTag = "Shift" -- our data starts with this string
options = {
buffer = 2 -- default loops to put on end of tweak sheet (min 2)
}
nBands = band.GetCount()
nSeg = structure.GetCount()
-- ================================== Begin New Dialog Library Functions
-- Add a wall of text from a table
function dialog.AddLabels(d,msg,nlabels) -- pass in # of existing autolabels
local nlabels = nlabels or #(d._Order or {}) -- default, valid if never delete dialog elements
if type(msg) == 'string' then
msg = { msg }
end
for i = 1,#msg do
d['autolabel'..tostring(i+nlabels)] = dialog.AddLabel(msg[i])
end
end
-- Create but don't display a wall of text and 1 or 2 buttons
function dialog.CreateMessageBox(msg,title,buttontext1,buttontext0)
title = title or ''
local d = dialog.CreateDialog(title)
dialog.AddLabels(d,msg)
buttontext1 = buttontext1 or 'Ok'
d.button = dialog.AddButton(buttontext1,1)
if buttontext0 ~= nil then d.button0 = dialog.AddButton(buttontext0,0) end
return d
end
-- Display a dialog box
function dialog.ShowMessageBox(msg,title,buttontext1,buttontext0)
return dialog.Show(dialog.CreateMessageBox(msg,title,buttontext1,buttontext0))
end
-- Build a multipage dialog, returns table of dialogs
-- pages is a table contain strings or tables of strings
-- title is table or string
function dialog.CreateBook(pages,title,buttontext1,buttontext0)
local dialogs = {}
for i = 1,#pages do
local t = type(title) == "string" and title or title[i]
local d = dialog.CreateMessageBox(pages[i],t,buttontext1,buttontext0)
if i > 1 then d.prev = dialog.AddButton("Prev",2) end
if i < #pages then d.next = dialog.AddButton("Next",3) end
dialogs[#dialogs+1] = d
end
return dialogs
end
-- Display a multipage dialog
function dialog.ShowBook(dialogs,page)
local rc
page = page or 1
repeat
rc = dialog.Show(dialogs[page])
if rc == 2 then page = page - 1 end
if rc == 3 then page = page + 1 end
until rc == 1 or rc == 0
return rc
end
-- ================================== End New Dialog Library Functions
-- ================================== Begin User Segment Selection Code
-- Determine what the user is going to shift by frozen or selection
-- Returns a pair of positive numbers or a negative number
-- Valid inputs are:
-- a single range selected
-- a single range frozen
-- 2 frozen segments
ERROR_NOSEL, ERROR_BOTH, ERROR_MULT_SEL, ERROR_MULT_FREEZE = -1,-2,-3,-4
function GetSelection()
local list1,list2 = {},{}
-- build up table of frozen and selected segments
for i = 1,nSeg do
if selection.IsSelected(i) then list1[#list1+1] = i end
if freeze.IsFrozen(i) then list2[#list2+1] = i end
end
local sel,froz
-- determine if the selection is valid
if #list1 == 0 then sel = 0 -- none selected
elseif list1[#list1]-list1[1] ~= #list1-1 then sel = -1 -- multiple blocks selected
else sel = {list1[1],list1[#list1]} -- valid selection range
end
-- determine if the freezing is valid
if #list2 == 0 then froz = 0 -- none frozen
elseif list2[#list2]-list2[1] ~= #list2-1 and #list2 ~= 2 then froz = -1 -- multiple blocks frozen
else froz = {list2[1],list2[#list2]} -- valid frozen range
end
-- begin listing error conditions
if sel == 0 and froz == 0 then return ERROR_NOSEL end
if sel ~= 0 and froz ~= 0 then return ERROR_BOTH end
-- at this point, exactly one of sel, froz is not 0
if sel == -1 then return ERROR_MULT_SEL end
if froz == -1 then return ERROR_MULT_FREEZE end
-- at this point, the non 0 is a valid range
if sel == 0 then return froz end
return sel
end
-- ================================== End User Segment Selection Code
-- ================================== Begin Recipe Dialogs
-- Return a string indicating segment type
function GetAminoAcidSafe(iSeg)
if structure.GetSecondaryStructure(iSeg) == "M" then return "ligand" end
return structure.GetAminoAcid(iSeg)
end
-- Return a string giving distance to next segment
function GetNeighborDistance(iSeg)
if iSeg >= nSeg or iSeg < 1 then return "0" end
return string.format("%0.5f",structure.GetDistance(iSeg,iSeg+1))
end
helpdialogs = nil
function HelpDialog(page)
if helpdialogs == nil then
helpdialogs = dialog.CreateBook(
{
{"This script will help you shift a long part of the",
"protein using the tweak tool. First, use either",
"selection or freeze to mark all the segments you want",
"to shift. The script will turn all of those into",
"sheets. It will also add at least 2 loops to both ends,",
"which are required by tweak. It will save the original",
"structure in the notes for later (you might want to",
"hide those.)",
"",
"Click Start to begin changing the structure. The shift",
"will work best if you place cutpoints (optional) at",
"least *3* segments from the tweak zone."
},
{"After running the script the first time, you can now",
"use the tweak tool on the sheets to shift them. Don't",
"shift too much, leave some overlap with your original",
"position. After you have shifted, run the script again.",
"The script should automatically determine how far you",
"shifted. If it doesn't, you can set it in the box. If",
"you shifted segments to the positions of lower",
"numbered segments, that's a negative shift.",
"",
"Click Finish to apply the original secondary",
"structure to is original positions (not segments) or",
"Abort to clear all notes and structure."
},
},{
"Power Shift (stage 1)",
"Power Shift (stage 2)"
},"Return")
end
dialog.ShowBook(helpdialogs,page)
end
START,FINISH,CANCEL,HELP,ERROR,ABORT = 1,2,0,4,5,6
function Stage1Dialog()
d = dialog.CreateDialog(title.." Stage 1")
d.label1 = dialog.AddLabel(string.format(
"It seems you have selected %d (%s) - %d (%s)",
range[1],GetAminoAcidSafe(range[1]),
range[2],GetAminoAcidSafe(range[2])
))
dialog.AddLabels(d, "Extra loops are required for shift to function.")
d.buffer = dialog.AddSlider("Extra Loops",options.buffer,2,5,0)
dialog.AddLabels(d,
{"[1) Select Range, then Click Start.",
"2) Tweak sheet segments and shift",
"3) Run recipe, Click Finish.",
"DO NOT ALTER BANDS OR WIGGLE"})
d.start = dialog.AddButton("Start",START)
d.cancel = dialog.AddButton("Cancel",CANCEL)
d.help = dialog.AddButton("Help",HELP)
local rc
repeat
rc = dialog.Show(d)
if rc == HELP then HelpDialog(1) end
until rc ~= HELP
options.buffer = d.buffer.value
return rc
end
function Stage2Dialog()
d = dialog.CreateDialog(title.." Stage 2")
d.label1 = dialog.AddLabel(string.format(
"It seems you are tweaking %d - %d (including buffer)",
options.first,options.last))
if options.shift then
d.label2 = dialog.AddLabel("It seems you have shifted:")
else
d.label2 = dialog.AddLabel("Could not autodetect your shift, please enter:")
end
d.shift = dialog.AddTextbox("Shift +-n:",tostring(options.shift or 0))
dialog.AddLabels(d,
{"Tip: set to 0 to make structure shift with segments!",
"Click CANCEL to Shift more, FINISH to apply structure,",
"or ABORT to clear all notes and exit."})
d.finish = dialog.AddButton("Finish",FINISH)
d.cancel = dialog.AddButton("Cancel",CANCEL)
d.abort = dialog.AddButton("Abort",ABORT)
d.help = dialog.AddButton("Help",HELP)
local rc
repeat
rc = dialog.Show(d)
if rc == HELP then HelpDialog(2)
elseif rc ~= CANCEL and rc ~= ABORT then-- error check input
local rc2,val = pcall( -- pull a single number out of textbox, if possible
function(s) return tonumber(select(3,s:find("(%-?%d+)"))) end,
d.shift.value)
if rc2 == true then
options.shift = val
else
dialog.ShowMessageBox({
"Please specify how many segments you shifted,",
"and direction (negative shifted to lower seg)."}
,"User Entry Error")
rc = ERROR
end
end
until rc ~= HELP and rc ~= ERROR
return rc
end
-- Get the user selection and show error message if invalid
function GetSelectionDialog()
local range = GetSelection()
if type(range)=="number" then
local errormsg = {
{"You need to select the region to shift by selecting",
"(in selection interface) or freezing two segments or",
"a single group (in original interface)."},
"Use only selection or only freeze, not both.",
"You seem to have multiple regions selected.",
"You seem to have multiple regions frozen."
}
local d = dialog.CreateMessageBox(errormsg[-range],"Range Selection Error","Okay","Clear S/F")
d.help = dialog.AddButton("Help",2)
repeat
local rc = dialog.Show(d)
if rc == 0 then
selection.DeselectAll()
freeze.UnfreezeAll()
end
if rc == 2 then HelpDialog() end
until rc ~= 2
return
end
return range
end
-- ================================== End Recipe Dialogs
-- ================================== Begin Note Routines
-- These routines parse a data string independent of pre-existing
-- note contents. Data is stored as "Tag <data>", where Tag is
-- recipe-specific string, the < > are literal delimiters, and
-- data is an arbitrary string that does not contain ">".
--FieldWrite = "%s <%s %s>" -- write our tag, a char, and a number
--FieldRead = "<(.) (.+)>" -- read a char and number
--FieldWrite = "%s <%s>" -- write our tag and a single char
--FieldRead = "<(.)>" -- read a single char
FieldWrite1 = "%s <%s>"
FieldWrite2 = "%s <%s %d>"
FieldRead = "<(.) -(%d*)>" -- char and optional number
-- Regular expressions for parsing our note data
FindPattern = NoteTag -- find our tag
ExtractPattern = NoteTag.." (%b<>)" -- extract the part in <>
ReplacePattern = NoteTag.." %b<>" -- replace the entire data
DeletePattern = " *"..ReplacePattern -- delete data and leading spaces
-- returns true if any recipe notes exist
function IsStage2()
for i = 1,nSeg do
local s = structure.GetNote(i)
if s:find(FindPattern) then return true,i end
end
return false,0
end
-- Add this recipe's data to a segment note
function SetRecipeNote(iSeg,extra)
local s,t
s = structure.GetNote(iSeg)
if extra then
t = string.format(FieldWrite2,NoteTag,structure.GetSecondaryStructure(iSeg),extra) -- our data
else
t = string.format(FieldWrite1,NoteTag,structure.GetSecondaryStructure(iSeg)) -- our data
end
if s:find(FindPattern) then s = s:gsub(ReplacePattern,t) -- replace our data
elseif s == "" then s = t -- new note
else s = string.format("%s %s",s,t) -- append our data
end
structure.SetNote(iSeg,s)
end
-- Get this recipe's data from a segment note, or nil
function GetRecipeNote(iSeg)
local s,t
s = structure.GetNote(iSeg)
_,_, s = s:find(ExtractPattern) -- find our tag and data
if s == nil then return end
_,_,s,t = s:find(FieldRead) -- parse data component
return s,t
end
-- Remove this recipe's data from all the notes
function RemoveRecipeNotes()
for i = 1,nSeg do
local s = structure.GetNote(i)
if s ~= "" then
s = s:gsub(DeletePattern,"") -- remove our data if present and leading space
structure.SetNote(i,s)
end
end
end
-- Extract all the notes into a table
-- Pass in result from IsStage2() to save time
-- Notes must already exist as a continuous block
function GetRecipeNotesBlock(first)
notes = {}
last = nSeg
for i = first,nSeg do
notes[i] = GetRecipeNote(i)
if notes[i] == nil then
last = i - 1
break
end
end
return notes,last
end
-- ================================== End Note Routines
-- Create a 'zero'length band
ZLBLen = 0.0001
function ZeroLengthBand(iSeg)
local s2,s3
if iSeg == 1 then s2,s3 = 2, 3
elseif iSeg == nSeg then s2,s3 = iSeg-1,iSeg-2
else s2,s3 = iSeg-1,iSeg+1
end
local iBand = band.Add(iSeg,s2,s3,ZLBLen,0,0)
band.Disable(iBand)
return iBand
end
-- Sets a region to sheets with loops on either end
function SetSSForTweak(first,start,finish,last)
selection.DeselectAll()
selection.SelectRange(start,finish)
structure.SetSecondaryStructureSelected('E')
selection.DeselectAll()
if first < start then selection.SelectRange(first,start-1) end
if last > finish then selection.SelectRange(finish+1,last) end
structure.SetSecondaryStructureSelected('L')
selection.DeselectAll()
end
-- Apply the original secondary structure, but shifted back to
-- the original positions in space
-- structures is a table of structures indexed by seg number
function RestoreSecondaryStructure(first,last,structures,shift)
shift = shift or 0
selection.SelectRange(first,last)
structure.SetSecondaryStructureSelected('L') -- default loops
function Apply(seg1,seg2,s)
selection.DeselectAll()
selection.SelectRange(seg1,seg2)
structure.SetSecondaryStructureSelected(s)
end
-- Using ranges is more complicated, but much faster due to drawing time
local s,start,n
for i = first,last do
if notes[i+options.shift] then
if s == nil then
s,start,n = notes[i+options.shift], i, 0 -- init range
elseif s == notes[i+options.shift] then
n = n + 1 -- expand range
else
Apply(start,start+n,s)
s,start,n = notes[i+options.shift], i, 0 -- init new range
end -- if s
elseif s then -- if notes
Apply(start,start+n,s)
s = nil
end
end
if s then Apply(start,start+n,s) end
end
-- Find the seg closest to one of the original endpoints
function EstimateShift()
local BandToFirst = nBands-1
local BandToLast = nBands
local threshold = 0.2
local n = options.last-options.first
for i = 0,n do
band.AddToBandEndpoint(i+options.first,BandToFirst)
band.AddToBandEndpoint(n-i+options.first,BandToLast)
local d1,d2 = band.GetLength(band.GetCount()-1), band.GetLength(band.GetCount())
if d1 < threshold then return -i + options.buffer, d1 end
if d2 < threshold then return i - options.buffer, d2 end
end
return nil
end
-- Delete added bands, optionally, the ZLB bands as well
function DeleteAddedBands(zlb)
local keep = nBands -- bands to keep (pre-existing bands)
if zlb then keep = keep - 2 end
if keep <= 0 then band.DeleteAll()
else
while band.GetCount() > keep do band.Delete(band.GetCount()) end
end
end
-- ================================== Begin Main
print(title)
print(puzzle.GetName(),os.date())
stage2,first = IsStage2() -- look for notes with our tag
if not stage2 then -- stage 1
print("Stage 1: Initial setup")
range = GetSelectionDialog()
if not range then return end
rc = Stage1Dialog()
if rc == CANCEL then
print("Cancelled.")
return
end
freeze.UnfreezeAll()
first = math.max(1,range[1]-options.buffer)
last = math.min(nSeg,range[2]+options.buffer)
SetRecipeNote(first,options.buffer) -- save the buffer length into first note
for i = first+1,last do -- save just the structure in the rest of the notes
SetRecipeNote(i)
end
print("Setting secondary structure from",first,"to",last)
print("With sheets from",range[1],"to",range[2])
SetSSForTweak(first,range[1],range[2],last)
print("Adding two bands at",range[1],"and",range[2])
ZeroLengthBand(range[1])
ZeroLengthBand(range[2])
selection.SelectRange(first,last) -- reselect for selection interface
print("Sheets re-selected.")
print("Suggest you create cutpoints BEYOND the buffer.")
print("Also, quicksave before and after tweaking.")
print("Don't wiggle or change bands.")
else -- stage 2
print("Stage 2: Restoring structure")
notes,last = GetRecipeNotesBlock(first)
_,buffer = GetRecipeNote(first)
print("Buffer =",buffer)
options.first = first
options.last = last
if nBands >= 2 then
print("Autodetecting shift....")
options.shift, atom_dist = EstimateShift()
DeleteAddedBands(false)
if options.shift then
print(string.format("Autodetected shift as %d (atom offset %0.3f)",options.shift,atom_dist))
end
auto_bands_exist = true
else
print("Bands missing, no autodetect.")
auto_bands_exist = false
end
rc = Stage2Dialog()
if rc == CANCEL then
print("Cancelled.")
return
end
if rc == ABORT then
print("Aborting...removing added notes, bands, selection, and freeze.")
selection.DeselectAll()
freeze.UnfreezeAll()
RemoveRecipeNotes()
DeleteAddedBands(false)
print("Done.")
return
end
print("User confirms shift =",options.shift)
print("Restoring original secondary structure...")
RestoreSecondaryStructure(first,last,notes,options.shift)
print("Clearing bands, notes, and selection...")
selection.DeselectAll()
DeleteAddedBands(auto_bands_exist)
RemoveRecipeNotes()
print("Done.")
end