CBRAIN Internal Documentation, September 2021
{
"tool-version": "6.0.4",
"name": "oxford_asl",
"author": "Oxford Centre for Functional MRI of the Brain (FMRIB)",
"description": "oxford_asl is part of BASIL",
"command-line": "oxford_asl [INPUT_FILE] [OUTPUT_DIR] [MASK]...",
"schema-version": "0.5",
"container-image": {
"image": "mcin/docker-fsl:latest",
"index": "docker://",
"type": "singularity"
},
"inputs": [ stuff ],
"output-files": [ stuff ],
"custom": { stuff }
}
id | 3 |
name | "Civet" |
cbrain_task_class_name | "CbrainTask::Civet" |
id | 513 |
tool_id | 3 ("Civet") |
bourreau_id | 56 ("Beluga") |
version_name | "2.1.1" |
(config stuff) | (path, env, etc) |
id | 60021 |
tool_config_id | 513 (Tool "Civet" on "Beluga") |
type | CbrainTask::Civet (Ruby class from Tool) |
cluster_jobid | 285283 (SLURM ID on Beluga) |
status | "On CPU" |
Plugins can provide new Userfile models,
new libraries and new task classes.
This allow the CBRAIN code base to be independent
of the code that provides these extended functions.
Plugins can be developed by other developers
and CBRAIN admins can just install them.
But this requires all plugin code to follow certain coding
conventions (APIs) and structure their code in a particular way.
The CBRAIN code base comes with one built-in plugin,
cbrain-plugins-base, which provides some simple
Userfile classes and a few example task classes.
File | Role | Class declaration |
---|---|---|
PLUGIN.../portal/tool_name.rb | Code loaded by Portal | class CbrainTask::ToolName < PortalTask |
PLUGIN.../bourreau/tool_name.rb | Code loaded by Bourreau | class CbrainTask::ToolName < ClusterTask |
PLUGIN.../views/_task_params.html.erb | Code rendered by Portal |
For more info:
%unless defaults.empty? # Default values for some (all?) of <%= name %>'s parameters. Those values # reflect the defaults taken by the tool's developer; feel free to change # them to match your platform's requirements. def self.default_launch_args #:nodoc: super.merge({ % id_width = max_width.(defaults, 'id') + "'".length % defaults.each do |default| <%= ":'%-#{id_width}s => %s," % [ default['id'] + "'", default['default-value'].inspect ] %> % end }) end % endIn YELLOW: code executed at integration time
# (no Ruby statements end up in the generated code)Generated Portal code
# Default values for some (all?) of CivetRerun's parameters. Those values
# reflect the defaults taken by the tool's developer; feel free to change
# them to match your platform's requirements.
def self.default_launch_args #:nodoc:
super.merge({
:'model' => "icbm152nl_09s",
:'template' => "0.50",
:'lsq' => 12,
:'interp' => "trilinear",
:'n3_dist' => 75,
:'pve_advanced' => true,
:'surfreg_model' => "icbm152MCsym",
:'combine_surface' => true,
:'thickness_methods' => "tlaplace",
:'thickness_kernels' => "30",
:'resample_surface' => true,
:'surf_atlas' => "lobes",
})
end
Generated Portal code
<%- classes = [ 'tsk-prm', type.to_s ] classes << 'list' if list classes << 'prm-grp-mbr' if isGroupMember -%> <%% id = '<%= id %>' %> <li class="<%%= id %> <%= classes.join(' ') %>"> <%% <%- if type == :flag -%> # Flag input toggle checkbox.(id, name: id) <%- end -%> # Name/Label label.(id, %q{ <%= param['name'] %> }, (91 lines skipped, I am not kidding) %> </li>In YELLOW: code executed at integration time
<% id = 'combine_surface' %> <li class="<%= id %> tsk-prm flag prm-grp-mbr"> <% # Flag input toggle checkbox.(id, name: id) # Name/Label label.(id, %q{ Combine left/right surfaces }, optional: true, flag: '-combine-surfaces' ) # Description description.(<<-'DESC',true) Combine left/right surfaces DESC %> </li>Sample generated _task_params.html.erb
See for instance, some old integrator templates:
It's located in lib/boutiques_support.rb
irb> desc = BoutiquesSupport::BoutiquesDescriptor.new_from_file "civet_rerun.json"
irb> desc.class
BoutiquesSupport::BoutiquesDescriptor
irb> desc[:name]
"CivetRerun"
irb> desc['name']
"CivetRerun"
irb> desc.name
"CivetRerun"
irb> desc['tool-version']
"2.1.1"
irb> desc.tool_version # note the _ instead of -
"2.1.1"
irb> desc.inputs.class
Array
irb> desc.inputs.first
{"name"=>"Existing CIVET output", "id"=>"civet_in", "description"=>"..."}
irb> desc.inputs.first.class
BoutiquesSupport::Input
This means they make sure all attributes accessed through methods or hash-like [] are are allowed in Boutiques.
# This works:
desc['name'] = 'MyNewName'
# These three will raise an exception: there is no 'email' field in
# the boutiques schema
desc.email # reading as a method
desc.email = 'pierre.rioux@mcgill.ca' # write as method
desc['email'] = 'pierre.rioux@mcgill.ca' # write as hash
In general, the new integration code uses the method access scheme (desc.abc) instead of the hash access scheme (desc['abc'])
Typical plugin files:
plugindir/cbrain_task/hello/portal/hello.rb # Tool 'hello', unrelated, standard integration plugindir/cbrain_task/hello/bourreau/hello.rb plugindir/cbrain_task/hello/views/_task_params.html.erb plugindir/userfiles/mymodel/mymodel.rb # Some unrelated model plugindir/cbrain_task_descriptors/oldtool.json # Tool 'oldtool', using old btq integration plugindir/boutiques_descriptors/mytool.json # Tool 'mytool', new btq integration
Symbolic links to these files are created in a special install directory by the CBRAIN sysadmin, at install time.
In particular, all the boutiques descriptors for the new integration end up linked into this new system subdirectory:
{BrainPortal|Bourreau}/cbrain_plugins/installed-plugins/boutiques_descriptors
For each JSON file:
The next four slides describe each of these steps.
We create, in memory, a new Ruby class that corresponds to the string BoutiquesTask::ToolName configured in the Tool object.
That class is an empty class that inherits everything.
On the portal side, it behaves like this piece of code:
class BoutiquesTask::ToolName < BoutiquesPortalTask
end
On the bourreau side, it's only slightly different:
class BoutiquesTask::ToolName < BoutiquesClusterTask
end
Here's what is responsible for creating these things under the three integrators:
class BoutiquesPortalTask < PortalTask
# This method returns the BoutiquesDescriptor
# directly associated with the ToolConfig for the task
def boutiques_descriptor
self.tool_config.boutiques_descriptor
end
#...
end
Excerpt from app/models/boutiques_portal_task.rb
class BoutiquesTask::ToolName < BoutiquesPortalTask
# Nothing at all in the class!
end
Boot time code for the task
class BoutiquesClusterTask < ClusterTask
# This method returns the BoutiquesDescriptor
# directly associated with the ToolConfig for the task
def boutiques_descriptor
self.tool_config.boutiques_descriptor
end
#...
end
Excerpt from app/models/boutiques_cluster_task.rb
class BoutiquesTask::ToolName < BoutiquesClusterTask
# Again, nothing at all in the class!
end
Boot time code for the task
-rw-r--r-- 1 prioux staff 2305 Oct 4 11:00 _boutiques_group.html.erb -rw-r--r-- 1 prioux staff 4127 Oct 4 11:00 _boutiques_input.html.erb -rw-r--r-- 1 prioux staff 970 Oct 4 11:00 _boutiques_preview.html.erb -rw-r--r-- 1 prioux staff 995 Oct 4 11:00 _description.html.erb -rw-r--r-- 1 prioux staff 2326 Oct 4 11:00 _dropdown.html.erb -rw-r--r-- 1 prioux staff 4602 Oct 4 11:00 _edit_help.html.erb -rw-r--r-- 1 prioux staff 28488 Oct 4 11:00 _form_js.html.erb -rw-r--r-- 1 prioux staff 991 Oct 4 11:00 _group_checkbox.html.erb -rw-r--r-- 1 prioux staff 1622 Oct 4 11:00 _html_input.html.erb -rw-r--r-- 1 prioux staff 1905 Oct 4 11:00 _html_input_list.html.erb -rw-r--r-- 1 prioux staff 1811 Oct 4 11:00 _html_select.html.erb -rw-r--r-- 1 prioux staff 1910 Oct 4 11:00 _input_checkbox.html.erb -rw-r--r-- 1 prioux staff 1219 Oct 4 11:00 _input_label.html.erb -rw-r--r-- 1 prioux staff 1216 Oct 4 11:00 _opt_checkbox.html.erb -rw-r--r-- 1 prioux staff 3085 Oct 4 11:00 _show_params.html.erb -rw-r--r-- 1 prioux staff 2530 Oct 4 11:00 _task_params.html.erb
_task_params.html.erb ↳ _boutiques_input.html.erb # one or many; see below for def ↳ _boutiques_group.html.erb # zero, one, or many ↳ _group_checkbox.html.erb ↳ _boutiques_input.html.erb # one or many ↳ _opt_checkbox.html.erb ↳ _input_label.html.erb ↳ One of: _input_checkbox.html.erb _dropdown.html.erb _html_input.html.erb _html_input_list.html.erb _html_select.html.erb ↳ _description.html.erb ↳ _boutiques_preview.html.erb # just a div ↳ _form_js.html.erb # templated _show_params.html.erb # separate page _edit_help.html.erb # separate overlay
input is a full BoutiquesSupport::Input object.
<%-
name = input.name
flag = input.command_line_flag
opt = input.optional
-%>
<label class="tsk-prm-lbl" for="<%= input.cb_invoke_html_id %>">
<%= name %>
<% if flag.present? %>
(<code class="cmd-flag"><%= flag %></code>)
<% end %>
<% unless opt %>
<span class="required">*</span>
<% end %>
</label>
Main content of _input_label.html.erb
Most of the time, these JS code snippets are small arrays or hashes of things related to the inputs.
The new integrator works like the old integrator in that regard, but the way the substitions are made has been greatly improved for clarity.
The next two slides show the before (old integrator) and after (new integrator).
a = descriptor.inputs
b = descriptor.file_inputs
c = descriptor.required_file_inputs
d = descriptor.list_inputs
These are much more elegant than
b = params.select { |i| i['type'] == 'File' }
c = params.select { |i| i['type'] == 'File' && ! i['optional'] }
d = params.select { |i| i['list'] }
When working on this framework, add new methods as needed!
It is used as a namespace for holding the real classes
for the tools (e.g. BoutiquesTask::ToolName).
It is not, however, ever instantiated.
It doesn't inherit from anything either:
class BoutiquesTask # not a AR model and does not inherit from no nothin'
Revision_info=CbrainFileRevision[__FILE__] #:nodoc:
end
All of cbrain-plugins-base/cbrain_task/boutiques_task/portal/boutiques_task.rb
Importantly, the views subdirectory for that class is where all the partials for the new boutiques integrator are located.
{
"interface_userfile_ids": [
"14465",
"14469"
],
"sinput1": "14465",
"minput1": "14469",
"my_output_name": "outreport",
"option_a": false,
"option_h": false,
"option_upper_h": true,
"_cbrain_output_du_report_out": [
14510
]
}
{
"interface_userfile_ids": [
"14465",
"14469"
],
"invoke": {
"sinput1": "14465",
"minput1": "14469",
"my_output_name": "outreport",
"option_a": false,
"option_h": false,
"option_upper_h": true
},
"_cbrain_output_du_report_out": [
14510
]
}
# Old traditional way
opt_a = task.params[:invoke][:option_a]
task.params[:invoke][:option_upper_h] = true
All BoutiquesTask objects have the helper invoke_params():
# Through the helper 'invoke_params'
opt_a = task.invoke_params[:option_a]
task.invoke_params[:option_upper_h] = true
Within a task object, then we can even get rid of the receiver:
def some_task_method
opt_a = invoke_params[:option_a]
end
class BoutiquesTask::ToolName < BoutiquesPortalTask
end
Internally generated code at boot time (on Portal side)
This class inherits all the basic functionality for providing
a behavior based on a descriptor (here, from BoutiquesPortalTask)
{
"name": "supertool",
"tool-version": "1.0",
(...)
"custom": {
"cbrain:inherits-from-class": "MySuperHandler"
}
}
The boot-time code will then instead create the handler class as:
class BoutiquesTask::ToolName < MySuperHandler
end
Internally generated code at boot time (on Portal side)
def boutiques_descriptor
self.tool_config.boutiques_descriptor
end
def descriptor_for_before_form
self.boutiques_descriptor
end
def descriptor_for_after_form
self.boutiques_descriptor
end
def descriptor_for_final_task_list
self.boutiques_descriptor
end
#etc...
Excerpts from boutiques_portal_task.rb
class MySuperHandler < BoutiquesPortalTask
# Integration with this class removes one input parameter,
# but just during "before_form".
def descriptor_for_before_form
modified_desc = super.dup
modified_desc.inputs.reject! { |input| input.id == 'numcpus' }
modified_desc
end
end
Again, if the superclass has been overriden and it is coded as
a standard task class (e.g. CbrainTask::SuperHandler) within a plugin,
then any partial can be replaced as needed:
boutiques_descriptors/my_tool.json # with cbrain:inherits set to CbrainTask::SuperHandler cbrain_task/super_handler/portal/super_handler.rb cbrain_task/super_handler/bourreau/super_handler.rb cbrain_task/super_handler/view/_input_label.html.erb # custom labels!List of files in an example plugin, with annotations
You only need to redefine the partials that you want, the others will be fetched from the base location.
Most of the time, this is enough. However, an admin, using the
interface, can provide or override a ToolConfig's descriptor by
providing a path to a JSON file.
The attribute can have four 'states':
Automatic, Manual, Overriden or None.
There are two modes in the integrator: simulate and launch.
These correspond to the bosh subcommands of the same names.
The mode is selected in the descriptor in the custom structure:
{
"name": "supertool",
"tool-version": "1.0",
(...)
"custom": {
"cbrain:boutiques_bosh_exec_mode": "simulate",
"cbrain:boutiques_bosh_exec_mode": "launch"
}
}
The default is simulate.
# Executed by the CBRAIN script builder
bosh exec simulate -i invoke_params.json descriptor.json
This bosh command generates a bash command for the tool, and CBRAIN
captures this command and inserts it in the script that will be submitted
for execution.
See the manual for bosh for more information.
# Inserted in the script that will be submitted for execution
bosh exec launch invoke_params.json descriptor.json
For this to succeed, the environement where the script will be
executed must have access to bosh (which is typically not the
case inside containers).
Note that the "container-image" section of the original descriptor is completely removed before being saved in descriptor.json, since CBRAIN handles containerization itself and we don't want an already containerized bosh command to attempt to pull another container.
{
"name": "supertool",
"tool-version": "1.0",
(...)
"custom": {
"cbrain:integrator_modules": {
"ModuleName": { "moduledata": "stuff" },
"InputFixer": { "planetname": "pluto" },
"OutputPatternRenamer": { "outdir": true },
"LicenseFinder": {
"freesurferlic": {
"type": "FreeSurferLicense"
}
}
}
}
}
JSON descriptor with special CBRAIN Ruby modules specified
class BoutiquesTask::ToolName < BoutiquesPortalTask
include OutputPatternRenamer
include InputFixer
include LicenseFinder
end
Internally generated code at boot time (on Portal side)
The original Boutiques Integrator was created by:
Rémi Bernard, Tristan Aumentado-Armstrong
Tristan Glatard, Pierre Rioux
The new Boutiques Integrator was created by:
Pierre Rioux
Project management:
Marc-Étienne Rousseau,
Shawn Brown,
Bryan Caron