changeset 20:6ab1a261520a draft default tip

planemo upload for repository https://github.com/usegalaxy-au/tools-au commit c3a90eb12ada44d477541baa4dd6182be29cd554-dirty
author galaxy-australia
date Sun, 28 Jul 2024 20:09:55 +0000
parents 2f7702fd0a4c
children
files alphafold.html alphafold.xml macro_output.xml macro_test_output.xml scripts/alphafold.html scripts/outputs.py
diffstat 6 files changed, 826 insertions(+), 702 deletions(-) [+]
line wrap: on
line diff
--- a/alphafold.html	Wed May 08 06:26:55 2024 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,656 +0,0 @@
-<!DOCTYPE html>
-<html lang="en" dir="ltr">
-
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-
-    <title> Alphafold structure prediction </title>
-
-    <link rel="preconnect" href="https://fonts.googleapis.com">
-    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-    <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.0/chroma.min.js" integrity="sha512-yocoLferfPbcwpCMr8v/B0AB4SWpJlouBwgE0D3ZHaiP1nuu5djZclFEIj9znuqghaZ3tdCMRrreLoM8km+jIQ==" crossorigin="anonymous"></script>
-
-    <style type="text/css">
-      * {
-        margin: 0;
-        padding: 0;
-      }
-      html, body {
-        width: 100%;
-        font-size: 1rem;
-      }
-      body {
-        font-family: 'Ubuntu', sans-serif;
-      }
-      canvas {
-        background-color: white;
-      }
-      h1, h2, h3, h4, h5, h6 {
-        color: dodgerblue;
-        text-align: center;
-        font-weight: lighter;
-      }
-      h1 {
-        margin: 2rem;
-        font-size: 3rem;
-      }
-      h2 {
-        font-size: 2rem;
-        margin-top: 1rem;
-        margin-bottom: .5rem;
-      }
-      button.btn {
-        color: #ccc;
-        margin: 1rem;
-        padding: .5rem;
-        font-size: 1rem;
-        min-width: 4rem;
-        border: none;
-        border-radius: .5rem;
-        background-color: grey;
-        transition-duration: 0.25s;
-        cursor: pointer;
-      }
-      button.btn.selected {
-        color: #eee;
-        background-color: dodgerblue;
-      }
-      button.btn.green {
-        color: #eee;
-        background-color: #10941f;
-      }
-      button.btn:focus {
-        outline: none;
-        color: inherit;
-      }
-      button.btn:hover {
-        color: white;
-        box-shadow: 0 0 10px dodgerblue;
-      }
-      button.btn.green:hover {
-        color: white;
-        box-shadow: 0 0 10px limegreen;
-      }
-      .main {
-        min-height: 90vh;
-        position: relative;
-      }
-      .flex {
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        padding: 1rem;
-      }
-      .col {
-        flex-direction: column;
-        flex-grow: 0;
-      }
-      .controls {
-        padding-bottom: 10vh;
-      }
-      .box {
-        padding: .5rem 1rem;
-        margin: .5rem auto;
-        width: fit-content;
-        border-radius: 1rem;
-      }
-      .mono {
-        font-family: monospace;
-        color: #555;
-        background-color: #ddd;
-        padding: .25rem;
-        border-radius: .25rem;
-      }
-      .space-1 {
-        line-height: 1.2;
-      }
-      .space-2 {
-        line-height: 1.5;
-      }
-      .relative {
-        position: relative;
-      }
-      .legend {
-        max-width: 350px;
-      }
-      .legend .scale {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-      }
-      .legend .color {
-        width: 150px;
-        height: 30px;
-        justify-content: space-between;
-        background: linear-gradient(
-          90deg,
-          rgba(255,55,0,1)   0%,
-          rgba(216,224,6,1)  33%,
-          rgba(34,213,238,1) 66%,
-          rgba(3,30,148,1)   100%
-          );
-      }
-      .legend .ticks {
-        margin-top: -1rem;
-        width: 180px;
-        justify-content: space-between;
-      }
-      #ngl-root-parent {
-        width: 40vw;
-        height: 30vw;
-        margin: auto;
-        position: relative;
-      }
-      #ngl-root {
-        width: 40vw;
-        height: 30vw;
-        border-radius: 15px;
-        border: 1px solid grey;
-      }
-      #ngl-nothing {
-        position: absolute;
-        top: 0;
-        left: 0;
-        display: none;
-        text-align: center;
-        width: 40vw;
-        height: 30vw;
-        padding-top: 12vw;
-      }
-      #ngl-loading {
-        position: absolute;
-        top: 0;
-        left: 0;
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        width: 800px;
-        height: 600px;
-        width: 40vw;
-        height: 30vw;
-      }
-      #ngl-loading svg {
-        width: 30%;
-        height: 30%;
-        width: 10vw;
-        height: 10vw;
-      }
-
-      /* Responsive */
-      @media (max-width: 1400px) {
-        :root {
-          font-size: 10pt;
-        }
-        button.btn {
-          margin: .5rem;
-          padding: .25rem;
-        }
-        .box {
-          padding: .5rem;
-          margin: .5rem auto;
-        }
-        .legend {
-          max-width: 200px;
-        }
-        .help-text {
-          font-size: 0.8rem;
-        }
-        .mono {
-          padding: .25rem .5rem;
-        }
-      }
-      @media (max-width: 1000px) {
-        :root {
-          font-size: 8pt;
-        }
-      }
-      @media (max-width: 800px) {
-        :root {
-          font-size: 6pt;
-        }
-      }
-    </style>
-
-    <script src="https://cdn.rawgit.com/arose/ngl/v2.0.0-dev.37/dist/ngl.js"></script>
-  </head>
-
-
-  <body>
-    <h1> Alphafold structure prediction </h1>
-
-    <div class="main flex">
-      <div class="col relative">
-        <div id="ngl-root-parent">
-
-          <div id="ngl-root"></div>
-
-          <div id="ngl-nothing">
-            Select a representation to display
-          </div>
-
-          <div id="ngl-loading">
-            <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
-              <g transform="rotate(0 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(30 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(60 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(90 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(120 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(150 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(180 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(210 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(240 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(270 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(300 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
-                </rect>
-              </g><g transform="rotate(330 50 50)">
-                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
-                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
-                </rect>
-              </g>
-            </svg>
-          </div>
-        </div>
-
-        <div class="flex">
-          <div class="box space-1">
-            <p>
-              <span class="mono">Scroll up/down</span>
-              to zoom in and out
-            </p>
-            <p>
-              <span class="mono">Click + drag</span>
-              to rotate the structure
-            </p>
-            <p>
-              <span class="mono">CTRL + click + drag</span>
-              to move the structure
-            </p>
-            <p>
-              <span class="mono">Click</span>
-              an atom to bring it into focus
-            </p>
-          </div>
-
-          <div class="box legend">
-            <div class="scale">
-              <div class="color"></div>
-              <div class="flex ticks">
-                <div>&lt;50</div>
-                <div>70</div>
-                <div>90+</div>
-              </div>
-            </div>
-
-            <div>
-              <p class="text-center">
-                <small>
-                Alphafold produces a
-                <a href="https://alphafold.ebi.ac.uk/faq#faq-5" target="_blank">
-                  per-residue confidence score (pLDDT)
-                </a>
-                between 0 and 100. Some regions below 50 pLDDT may be
-                unstructured in isolation.
-              </small>
-              </p>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <div class="flex col controls">
-        <div class="box text-center">
-          <h3> Select model </h3>
-          <p>The top-ranked structures predicted by Alphafold</p>
-          <div>
-            <button class="btn selected" id="btn-ranked_0" onclick="setModel(0);">
-              Ranked 0
-            </button>
-
-            <button class="btn" id="btn-ranked_1" onclick="setModel(1);">
-              Ranked 1
-            </button>
-
-            <button class="btn" id="btn-ranked_2" onclick="setModel(2);">
-              Ranked 2
-            </button>
-
-            <button class="btn" id="btn-ranked_3" onclick="setModel(3);">
-              Ranked 3
-            </button>
-
-            <button class="btn" id="btn-ranked_4" onclick="setModel(4);">
-              Ranked 4
-            </button>
-          </div>
-        </div>
-
-        <div class="box text-center">
-          <h3> Toggle representations </h3>
-          <div>
-            <button class="btn selected" id="btn-cartoon" onclick="toggleModelRepresentation('cartoon');">
-              Cartoon
-            </button>
-
-            <button class="btn" id="btn-ball-stick" onclick="toggleModelRepresentation('ball+stick');">
-              Ball + stick
-            </button>
-
-            <button class="btn" id="btn-surface" onclick="toggleModelRepresentation('surface');">
-              Surface
-            </button>
-
-            <button class="btn" id="btn-backbone" onclick="toggleModelRepresentation('backbone');">
-              Backbone
-            </button>
-          </div>
-        </div>
-
-        <div class="box text-center">
-          <h3> Actions </h3>
-          <div>
-            <button class="btn selected" id="btn-toggle-spin" onclick="toggleSpin();">
-              Toggle spin
-            </button>
-
-            <button class="btn" id="btn-toggle-dark" onclick="toggleDark();">
-              Dark mode
-            </button>
-          </div>
-        </div>
-
-        <div class="box text-center">
-          <h3> Download </h3>
-          <div>
-            <button class="btn green" onclick="downloadPng();">
-              Snapshot
-            </button>
-
-            <button class="btn green" onclick="downloadPdb();">
-              PDB
-            </button>
-          </div>
-        </div>
-      </div>
-    </div>
-  </body>
-
-
-  <script type="text/javascript">
-
-    // Render NGLviewer for PDB files
-
-    // State management has been implemented with vanilla Js but could have used
-    // Vue - it's a fairly simple use case so a global 'state' object works fine
-    // without complicating things too much.
-
-
-    // Define a custom color scheme to represent model confidence consistently
-    // across different representations
-    // ------------------------------------------------------------------------
-    const colorScale = chroma.scale([
-      'red', 'yellow', 'green', 'cyan', 'blue'
-    ]).mode('lab').domain([0, 0.9]);
-
-    const confidenceScheme = NGL.ColormakerRegistry.addScheme(function (params) {
-      this.atomColor = function (atom) {
-        // Actually model confidence (pLDDT)
-        const c = atom.bfactor;
-        const BREAK_RED = 40;   // Below this is just plain red
-        let range, r, g, b;
-
-        if (c < BREAK_RED) {
-          return 0xFF0000;
-        }
-        const p = (c - BREAK_RED) / (100 - BREAK_RED)
-        return eval(colorScale(p).hex().replace('#', '0x'));
-      };
-    });
-
-    // NGL color schemes https://nglviewer.org/ngl/api/manual/usage/coloring.html
-    const COLORSCHEME = confidenceScheme;  //'bfactor'
-
-    const MODELS = [
-      'ranked_0.pdb',
-      'ranked_1.pdb',
-      'ranked_2.pdb',
-      'ranked_3.pdb',
-      'ranked_4.pdb',
-    ]
-
-    const REPRESENTATIONS = [
-      'cartoon',
-      'ball+stick',
-      'surface',
-      'backbone',
-    ]
-
-    const DEFAULT_REPRESENTATION = REPRESENTATIONS[0];
-    const MAX_CLICK_INTERVAL_MS = 500;  // For debouncing model clicks
-
-    let stage;
-    let nonceSetModel;
-
-    let state = {
-      model: 0,
-      modelObject: null,
-      representations: {},
-      colorScheme: 'bfactor',
-      darkMode: false,
-      loading: 1,
-      spin: true,
-    }
-
-    const uri = (i) => MODELS[i];
-    // Switch to this function to return sample model URI (local dev)
-    // const uri = (i) => `https://raw.githubusercontent.com/neoformit/alphafold-galaxy/main/data/${MODELS[i]}`;
-
-    document.addEventListener("DOMContentLoaded", async function () {
-      // Can set debug for development if NGL is being... funny
-      // NGL.setDebug(true)
-
-      // Create NGL Stage object
-      stage = new NGL.Stage("ngl-root", { backgroundColor: 'white' });
-
-      // Handle window resizing
-      window.addEventListener("resize",  () => stage.handleResize());
-
-      loadModel();
-      while (true) {
-        if (!state.loading) {
-          // Reload page if NGL failed to display. Weird occassional bug.
-          const canvas = document.querySelector('#ngl-root canvas');
-          canvas.height < 50 && window.reload();
-          break
-        }
-        await new Promise(resolve => setTimeout(resolve, 500));
-      }
-    });
-
-    // Models ------------------------------------------------------------------
-
-    const setModel = (ix) => {
-      state.model = ix;
-      stage.removeComponent(state.modelObject);
-      setLoading(1);
-
-      // Debounce rapid model clicking with a nonce
-      nonceSetModel = new Object();
-      const localNonce = nonceSetModel;
-      setTimeout( () => {
-        if (localNonce === nonceSetModel) {
-          // The user has stopped clicking, hurray...
-          loadModel().then(updateButtons);
-        }
-      }, MAX_CLICK_INTERVAL_MS);
-    }
-
-    const loadModel = () => {
-      reps = Object.keys(state.representations);
-      if (reps.length) {
-        state.representations = {};
-      } else {
-        reps = [DEFAULT_REPRESENTATION];
-      }
-
-      // Load PDB entry
-      return stage.loadFile(uri(state.model)).then( (o) => {
-        state.modelObject = o;
-        reps.forEach( (r) => addModelRepresentation(r) );
-        stage.setSpin(state.spin);
-        o.autoView();
-        setLoading(0);
-      })
-    }
-
-    // Representations ---------------------------------------------------------
-
-    const toggleModelRepresentation = (rep) => {
-      rep in state.representations ?
-        removeModelRepresentation(rep)
-        : addModelRepresentation(rep)
-    }
-
-    const addModelRepresentation = (rep) => {
-      state.representations[rep] =
-        state.modelObject.addRepresentation(rep, {colorScheme: COLORSCHEME});
-      updateButtons();
-    }
-
-    const removeModelRepresentation = (rep) => {
-      o = state.representations[rep];
-      state.modelObject.removeRepresentation(o);
-      delete state.representations[rep];
-      updateButtons();
-    }
-
-    const clearModelRepresentations = () => {
-      state.modelObject && state.modelObject.removeAllRepresentations();
-      state.representations = {};
-    }
-
-    // Actions -----------------------------------------------------------------
-
-    const toggleDark = () => {
-      state.darkMode = !state.darkMode;
-      stage.setParameters({
-        backgroundColor: state.darkMode ? 'black' : 'white',
-      });
-      const btn = document.querySelector('#btn-toggle-dark');
-      btn && btn.classList.toggle('selected');
-    }
-
-    const setLoading = (state) => {
-      document.getElementById('ngl-loading')
-        .style.display = state ? 'flex' : 'none';
-      state.loading = state;
-    }
-
-    const toggleSpin = () => {
-      stage.toggleSpin();
-      const btn = document.querySelector('#btn-toggle-spin');
-      btn && btn.classList.toggle('selected');
-      state.spin = !state.spin;
-    }
-
-    const downloadPng = () => {
-      const params = {
-        factor: 3,
-        antialias: true,
-      }
-      stage.makeImage(params).then( (blob) => {
-        const name = MODELS[state.model].replace('.pdb', '.png');
-        const url = URL.createObjectURL(blob);
-        makeDownload(url, name);
-      })
-    }
-
-    const downloadPdb = () => {
-      const url = uri(state.model);
-      const name = `alphafold_${MODELS[state.model]}`;
-      makeDownload(url, name);
-    }
-
-    const makeDownload = (url, name) => {
-      // Will not work with cross-origin urls (i.e. during development)
-      console.log(`Creating file download for ${name}, href ${url}`);
-      const saveLink = document.createElement('a');
-      saveLink.href = url;
-      saveLink.download = name;
-      document.body.appendChild(saveLink);
-      saveLink.dispatchEvent(
-        new MouseEvent('click', {
-          bubbles: true,
-          cancelable: true,
-          view: window
-        })
-      );
-      document.body.removeChild(saveLink);
-    }
-
-    const updateButtons = () => {
-      MODELS.forEach( (name, i) => {
-        const id = `#btn-${name.replace('.pdb', '')}`;
-        const btn = document.querySelector(id);
-        if (!btn) return
-        i == state.model ?
-          btn.classList.add('selected')
-          : btn.classList.remove('selected');
-      })
-
-      REPRESENTATIONS.forEach( (name) => {
-        const id = `#btn-${name}`.replace('+', '-');
-        const btn = document.querySelector(id);
-        if (!btn) return
-        if (name in state.representations) {
-          btn.classList.add('selected')
-        } else {
-          btn.classList.remove('selected');
-        }
-      });
-
-      // Show "Nothing to display" if no representations are selected
-      document.querySelector('#ngl-nothing').style.display =
-        Object.keys(state.representations).length ?
-        'none'
-        : 'block';
-    }
-
-  </script>
-
-</html>
--- a/alphafold.xml	Wed May 08 06:26:55 2024 +0000
+++ b/alphafold.xml	Sun Jul 28 20:09:55 2024 +0000
@@ -1,9 +1,9 @@
-<tool id="alphafold" name="Alphafold 2" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="20.01">
+<tool id="alphafold" name="Alphafold 2" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="22.05">
     <description> - AI-guided 3D structural prediction of proteins</description>
     <macros>
-      <token name="@TOOL_VERSION@">2.3.1</token>
+      <token name="@TOOL_VERSION@">2.3.2</token>
       <token name="@TOOL_MINOR_VERSION@">2.3</token>
-      <token name="@VERSION_SUFFIX@">5</token>
+      <token name="@VERSION_SUFFIX@">0</token>
       <import>macro_output.xml</import>
       <import>macro_test_output.xml</import>
     </macros>
@@ -17,12 +17,12 @@
       <xref type="bio.tools">alphafold_2</xref>
     </xrefs>
     <requirements>
-        <container type="docker">neoformit/alphafold:v2.3.1_2</container>
+        <container type="docker">neoformit/alphafold:v2.3.2_0</container>
     </requirements>
     <required_files>
         <include path="scripts/outputs.py" />
         <include path="scripts/validate_fasta.py" />
-        <include path="alphafold.html" />
+        <include path="scripts/alphafold.html" />
     </required_files>
     <command detect_errors="exit_code"><![CDATA[
 
@@ -46,7 +46,7 @@
 && python3 '$__tool_directory__/scripts/validate_fasta.py' input.fasta
 --min_length \${ALPHAFOLD_AA_LENGTH_MIN:-0}
 --max_length \${ALPHAFOLD_AA_LENGTH_MAX:-0}
-#if $model_preset == 'multimer':
+#if $model_preset.selection == 'multimer':
 --multimer
 --max-sequences \${ALPHAFOLD_MAX_SEQUENCES:-10}
 #end if
@@ -60,7 +60,7 @@
 ## Run AlphaFold  -------------------------------------------------------------
 #if os.environ.get('PLANEMO_TESTING'):
     ## Run in testing mode (mocks a successful AlphaFold run by copying outputs)
-    && echo "Creating dummy outputs for model_preset=$model_preset..."
+    && echo "Creating dummy outputs for model_preset=$model_preset.selection..."
     && bash '$__tool_directory__/scripts/mock_alphafold.sh' $model_preset
 #else:
     ## Run AlphaFold
@@ -68,7 +68,7 @@
         --fasta_paths alphafold.fasta
         --output_dir output
         --data_dir \${ALPHAFOLD_DB:-/data}/@TOOL_MINOR_VERSION@/
-        --model_preset=$model_preset
+        --model_preset=$model_preset.selection
 
         ## Set reference database paths
         --uniref90_database_path   \${ALPHAFOLD_DB:-/data}/@TOOL_MINOR_VERSION@/uniref90/uniref90.fasta
@@ -83,21 +83,33 @@
         --small_bfd_database_path  \${ALPHAFOLD_DB:-/data}/@TOOL_MINOR_VERSION@/small_bfd/bfd-first_non_consensus_sequences.fasta
         #end if
 
-        #if $max_template_date:
-        --max_template_date=$max_template_date
+        #if $advanced.max_template_date:
+        --max_template_date=$advanced.max_template_date
         #else
         --max_template_date=\$TODAY
         #end if
 
-        --use_gpu_relax=\${ALPHAFOLD_USE_GPU:-True}  ## introduced in v2.1.2
+        --use_gpu_relax=\${ALPHAFOLD_USE_GPU:-True}
 
-        #if $model_preset == 'multimer':
+        #if $model_preset.selection == 'multimer':
         --pdb_seqres_database_path=\${ALPHAFOLD_DB:-/data}/@TOOL_MINOR_VERSION@/pdb_seqres/pdb_seqres.txt
         --uniprot_database_path=\${ALPHAFOLD_DB:-/data}/@TOOL_MINOR_VERSION@/uniprot/uniprot.fasta
-        --num_multimer_predictions_per_model=1  ## introduced in v2.2.0
+        --num_multimer_predictions_per_model=$model_preset.num_multimer_predictions_per_model
         #else
         --pdb70_database_path \${ALPHAFOLD_DB:-/data}/@TOOL_MINOR_VERSION@/pdb70/pdb70
         #end if
+
+        ## Galaxy-specific options --------------------------------------------
+        ## See https://github.com/neoformit/alphafold/tree/release_2.3.2_galaxy
+        #if $advanced.disable_amber_relax:
+        --disable_amber_relax
+        #end if
+
+        #if $advanced.limit_model_outputs:
+        --output_models=$limit_model_outputs
+        #end if
+        ## End Galaxy-specific options ----------------------------------------
+
 #end if
 
 ## Generate additional outputs ------------------------------------------------
@@ -106,13 +118,13 @@
 $outputs.model_pkls
 $outputs.pae_csv
 $outputs.plots
-#if $model_preset == 'multimer':
+#if $model_preset.selection == 'multimer':
 --multimer
 #end if
 
 ## HTML output
 && mkdir -p '${ html.files_path }'
-&& cp '$__tool_directory__/alphafold.html' '${html}'
+&& cp output/alphafold/extra/alphafold.html '${html}'
 && cp output/alphafold/ranked_*.pdb '${html.files_path}'
 
 ## This is a (hacky) fix for a bug that has appeared in multiple Pulsar servers.
@@ -136,21 +148,6 @@
         </conditional>
 
         <param
-            name="max_template_date"
-            type="text"
-            label="Max template date (yyyy-mm-dd) (optional)"
-            help="The model will reference PDB structures deposited before this date only. Defaults to today's date."
-            optional="true"
-        >
-            <sanitizer>
-                <valid initial="string.digits">
-                    <add value="-" />
-                </valid>
-            </sanitizer>
-            <validator type="regex">[0-9]{4}-[0-9]{2}-[0-9]{2}</validator>
-        </param>
-
-        <param
           name="dbs"
           type="select"
           display="radio"
@@ -162,8 +159,9 @@
           <option value="full">Full database</option>
         </param>
 
+        <conditional name="model_preset">
         <param
-            name="model_preset"
+                name="selection"
             type="select"
             label="Model preset"
             help="Select which prediction model to run. The monomer model is the most accurate for single protein prediction. The multimer model allows prediction of protein complexes."
@@ -176,6 +174,56 @@
                 multimer - model a protein complex (requires multi-sequence FASTA input)
             </option>
         </param>
+            <when value="monomer"></when>
+            <when value="monomer_ptm"></when>
+            <when value="multimer">
+                <param
+                    name="num_multimer_predictions_per_model"
+                    type="integer"
+                    value="5"
+                    label="Multimer predictions per model"
+                    help="How many predictions (each with a different random seed) will be generated per model. E.g. if this is 2 and there are 5 models then there will be 10 predictions per input. For a small drop in accuracy you may wish to run a single seed per model (default 5, max 10)."
+                    min="1"
+                    max="10"
+                />
+            </when>
+        </conditional>
+
+        <section name="advanced" title="Advanced options" expanded="false">
+            <param
+                name="max_template_date"
+                type="text"
+                label="Max template date (yyyy-mm-dd) (optional)"
+                help="The model will reference PDB structures deposited before this date only. Defaults to today's date."
+                optional="true"
+            >
+                <sanitizer>
+                    <valid initial="string.digits">
+                        <add value="-" />
+                    </valid>
+                </sanitizer>
+                <validator type="regex">[0-9]{4}-[0-9]{2}-[0-9]{2}</validator>
+            </param>
+
+            <param
+                name="disable_amber_relax"
+                type="boolean"
+                label="Disable Amber relaxation"
+                value="false"
+                optional="true"
+                help="Amber relaxation can be disabled to speed up processing time. Amber relaxation is used to refine predicted structures by removing stereochemical violations, resulting in more accurate prediction of side-chain geometry. Disabling this option with large proteins may lead to artefacts in the predicted structure. Disabling amber relax will result in the unrelaxed models being collected as PDB outputs."
+            />
+
+            <param
+                name="limit_model_outputs"
+                type="integer"
+                label="Limit model outputs"
+                value="5"
+                help="Limit the number of models to output. The top N models will be output, where N is the value entered here (default 5). Please note that the top-ranking model is not always the correct one, and it is usually recommended to inspect multiple models. Reducing the number of models will result in a slight reduction in run time."
+                min="1"
+                max="5"
+            />
+        </section>
 
         <section name="outputs" title="Optional outputs" expanded="false">
             <param
@@ -228,6 +276,13 @@
                 label="relax_metrics.json"
                 help="A JSON-formatted text file containing relax metrics (mostly remaining violations)."
             />
+            <param
+                name="timings_json"
+                type="boolean"
+                checked="false"
+                label="timings.json"
+                help="A JSON file with timings reported for each phase of the AlphaFold run."
+            />
         </section>
     </inputs>
 
@@ -241,6 +296,7 @@
         <expand macro="output_pae_csv" />
         <expand macro="output_plots" />
         <expand macro="output_relax_json" />
+        <expand macro="output_timings_json" />
     </outputs>
 
     <tests>
@@ -250,7 +306,7 @@
                 <param name="input_mode" value="history"/>
                 <param name="fasta_file" value="test1.fasta"/>
             </conditional>
-            <param name="model_preset" value="monomer"/>
+            <param name="model_preset|selection" value="monomer"/>
             <expand macro="test_output_pdb_models" />
         </test>
 
@@ -260,7 +316,7 @@
                 <param name="input_mode" value="history"/>
                 <param name="fasta_file" value="test1.fasta"/>
             </conditional>
-            <param name="model_preset" value="monomer"/>
+            <param name="model_preset|selection" value="monomer"/>
             <param name="outputs|plots" value="true"/>
             <param name="outputs|confidence_scores" value="true"/>
             <param name="outputs|plddts" value="true"/>
@@ -281,7 +337,7 @@
                 <param name="input_mode" value="history"/>
                 <param name="fasta_file" value="test1.fasta"/>
             </conditional>
-            <param name="model_preset" value="monomer_ptm"/>
+            <param name="model_preset|selection" value="monomer_ptm"/>
             <param name="outputs|plots" value="true"/>
             <param name="outputs|confidence_scores" value="true"/>
             <param name="outputs|plddts" value="true"/>
@@ -303,19 +359,21 @@
                 <param name="input_mode" value="history"/>
                 <param name="fasta_file" value="multimer.fasta"/>
             </conditional>
-            <param name="model_preset" value="multimer"/>
+            <param name="model_preset|selection" value="multimer"/>
             <param name="outputs|plots" value="true"/>
             <param name="outputs|confidence_scores" value="true"/>
             <param name="outputs|plddts" value="true"/>
             <param name="outputs|pae_csv" value="true"/>
             <param name="outputs|model_pkls" value="true"/>
             <param name="outputs|relax_json" value="true"/>
+            <param name="outputs|timings_json" value="true"/>
             <expand macro="test_output_plots_3" />
             <expand macro="test_output_confidence_scores" />
             <expand macro="test_output_plddts" />
             <expand macro="test_output_pdb_models" />
             <expand macro="test_output_pickles" />
             <expand macro="test_output_relax_json" />
+            <expand macro="test_output_timings_json" />
             <expand macro="test_output_pae_csv" />
         </test>
     </tests>
@@ -325,7 +383,7 @@
 
     | AlphaFold v2: AI-guided 3D structural prediction of proteins
     |
-    | **NOTE: this tool packages AlphaFold v2.3.1.**
+    | **NOTE: this tool packages** `a modified branch of AlphaFold v2.3.2. <https://github.com/neoformit/alphafold/tree/release_2.3.2_galaxy>`_
     |
     | This means that the neural network has been trained on PDBs with a release
     | date before 2021-09-30 (the training cutoff was 2018-04-30 until ``v2.3.0``).
@@ -333,12 +391,9 @@
     | Find out more in the technical and release notes:
     |
 
-    - `Release notes for v2.3.1 <https://github.com/deepmind/alphafold/releases/tag/v2.3.1>`_
+    - `Release notes for v2.3.2 <https://github.com/deepmind/alphafold/releases/tag/v2.3.2>`_
     - `Technical notes for v2.3 <https://github.com/deepmind/alphafold/blob/main/docs/technical_note_v2.3.0.md>`_
 
-    | If you want to use AlphaFold trained against an older cutoff date, switch to Galaxy version ``2.1.2`` (which was trained to data up to 2018-04-30).
-    |
-
     **What it does**
 
     *What is AlphaFold?*
@@ -362,6 +417,7 @@
     | You can choose to input either a file from your Galaxy history or paste a sequence into a text box.
     | If you choose the ``multimer`` option, you can supply a FASTA file containing **multiple sequences** to be folded concurrently into a multimer.
     |
+    | For pairwise screening of target-candidate with multimer, you can submit a list of paired protein sequences in batch mode (i.e. two protein sequences in each FASTA file).
     |
 
     **Outputs**
@@ -380,7 +436,7 @@
 
     *PDB files*
 
-    | Five PDB (Protein Data Bank) files are be created, ordered by rank, as predicted by AlphaFold.
+    | PDB (Protein Data Bank) files (5 by default) are be created, ordered by rank, as predicted by AlphaFold. The tool produces 5 models by default, but this can be reduced with the "Limit model outputs" for a reduced run time.
     | These files describe the molecular structures and can be used for downstream analysis. e.g. *in silico* molecular docking.
     | **PLEASE NOTE** that all outputs have been renamed to their respective rank order, including model and model.pkl files.
     |
@@ -421,6 +477,12 @@
     |
     |
 
+    *timings.json (optional)*
+
+    | A JSON-formatted text file containing the timings for each phase of the prediction.
+    |
+    |
+
     **AlphaFold configuration**
 
     | We have configured AlphaFold to run with the parameters suggested by default on `AlphaFold's GitHub <https://github.com/deepmind/alphafold>`_.
--- a/macro_output.xml	Wed May 08 06:26:55 2024 +0000
+++ b/macro_output.xml	Sun Jul 28 20:09:55 2024 +0000
@@ -1,9 +1,17 @@
 <macros>
     <xml name="output_pdb_models">
-        <data name="model5" format="pdb" from_work_dir="output/alphafold/ranked_4.pdb" label="${tool.name} on ${on_string}: PDB ranked 4"/>
-        <data name="model4" format="pdb" from_work_dir="output/alphafold/ranked_3.pdb" label="${tool.name} on ${on_string}: PDB ranked 3"/>
-        <data name="model3" format="pdb" from_work_dir="output/alphafold/ranked_2.pdb" label="${tool.name} on ${on_string}: PDB ranked 2"/>
-        <data name="model2" format="pdb" from_work_dir="output/alphafold/ranked_1.pdb" label="${tool.name} on ${on_string}: PDB ranked 1"/>
+        <data name="model5" format="pdb" from_work_dir="output/alphafold/ranked_4.pdb" label="${tool.name} on ${on_string}: PDB ranked 4">
+            <filter>advanced['limit_model_outputs'] > 4</filter>
+        </data>
+        <data name="model4" format="pdb" from_work_dir="output/alphafold/ranked_3.pdb" label="${tool.name} on ${on_string}: PDB ranked 3">
+            <filter>advanced['limit_model_outputs'] > 3</filter>
+        </data>
+        <data name="model3" format="pdb" from_work_dir="output/alphafold/ranked_2.pdb" label="${tool.name} on ${on_string}: PDB ranked 2">
+            <filter>advanced['limit_model_outputs'] > 2</filter>
+        </data>
+        <data name="model2" format="pdb" from_work_dir="output/alphafold/ranked_1.pdb" label="${tool.name} on ${on_string}: PDB ranked 1">
+            <filter>advanced['limit_model_outputs'] > 1</filter>
+        </data>
         <data name="model1" format="pdb" from_work_dir="output/alphafold/ranked_0.pdb" label="${tool.name} on ${on_string}: PDB ranked 0"/>
     </xml>
 
@@ -16,6 +24,7 @@
         >
             <filter>outputs['pae_csv']</filter>
             <filter>model_preset != "monomer"</filter>
+            <filter>advanced['limit_model_outputs'] > 4</filter>
         </data>
         <data
             name="pae_ranked_3"
@@ -25,6 +34,7 @@
         >
             <filter>outputs['pae_csv']</filter>
             <filter>model_preset != "monomer"</filter>
+            <filter>advanced['limit_model_outputs'] > 3</filter>
         </data>
         <data
             name="pae_ranked_2"
@@ -34,6 +44,7 @@
         >
             <filter>outputs['pae_csv']</filter>
             <filter>model_preset != "monomer"</filter>
+            <filter>advanced['limit_model_outputs'] > 2</filter>
         </data>
         <data
             name="pae_ranked_1"
@@ -43,6 +54,7 @@
         >
             <filter>outputs['pae_csv']</filter>
             <filter>model_preset != "monomer"</filter>
+            <filter>advanced['limit_model_outputs'] > 1</filter>
         </data>
         <data
             name="pae_ranked_0"
@@ -63,6 +75,7 @@
             label="${tool.name} on ${on_string}: ranked_4.pkl"
         >
             <filter>outputs['model_pkls']</filter>
+            <filter>advanced['limit_model_outputs'] > 4</filter>
         </data>
         <data
             name="output_ranked_3_pkl"
@@ -71,6 +84,7 @@
             label="${tool.name} on ${on_string}: ranked_3.pkl"
         >
             <filter>outputs['model_pkls']</filter>
+            <filter>advanced['limit_model_outputs'] > 3</filter>
         </data>
         <data
             name="output_ranked_2_pkl"
@@ -79,6 +93,7 @@
             label="${tool.name} on ${on_string}: ranked_2.pkl"
         >
             <filter>outputs['model_pkls']</filter>
+            <filter>advanced['limit_model_outputs'] > 2</filter>
         </data>
         <data
             name="output_ranked_1_pkl"
@@ -87,6 +102,7 @@
             label="${tool.name} on ${on_string}: ranked_1.pkl"
         >
             <filter>outputs['model_pkls']</filter>
+            <filter>advanced['limit_model_outputs'] > 1</filter>
         </data>
         <data
             name="output_ranked_0_pkl"
@@ -106,6 +122,7 @@
             label="${tool.name} on ${on_string}: pLDDT/PAE plot ranked 4"
         >
             <filter>outputs['plots']</filter>
+            <filter>advanced['limit_model_outputs'] > 4</filter>
         </data>
         <data
             name="plot_ranked_3"
@@ -114,6 +131,7 @@
             label="${tool.name} on ${on_string}: pLDDT/PAE plot ranked 3"
         >
             <filter>outputs['plots']</filter>
+            <filter>advanced['limit_model_outputs'] > 3</filter>
         </data>
         <data
             name="plot_ranked_2"
@@ -122,6 +140,7 @@
             label="${tool.name} on ${on_string}: pLDDT/PAE plot ranked 2"
         >
             <filter>outputs['plots']</filter>
+            <filter>advanced['limit_model_outputs'] > 2</filter>
         </data>
         <data
             name="plot_ranked_1"
@@ -130,6 +149,7 @@
             label="${tool.name} on ${on_string}: pLDDT/PAE plot ranked 1"
         >
             <filter>outputs['plots']</filter>
+            <filter>advanced['limit_model_outputs'] > 1</filter>
         </data>
         <data
             name="plot_ranked_0"
@@ -173,4 +193,15 @@
             <filter>outputs['relax_json']</filter>
         </data>
     </xml>
+
+    <xml name="output_timings_json">
+        <data
+            name="output_timings_json"
+            format="json"
+            from_work_dir="output/alphafold/timings.json"
+            label="${tool.name} on ${on_string}: timings.json"
+        >
+            <filter>outputs['timings_json']</filter>
+        </data>
+    </xml>
 </macros>
--- a/macro_test_output.xml	Wed May 08 06:26:55 2024 +0000
+++ b/macro_test_output.xml	Sun Jul 28 20:09:55 2024 +0000
@@ -26,6 +26,14 @@
         </output>
     </xml>
 
+    <xml name="test_output_timings_json">
+        <output name="output_timings_json">
+            <assert_contents>
+                <has_size min="500" />
+            </assert_contents>
+        </output>
+    </xml>
+
     <xml name="test_output_pdb_models">
         <output name="model1">
             <assert_contents>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/alphafold.html	Sun Jul 28 20:09:55 2024 +0000
@@ -0,0 +1,656 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <title> Alphafold structure prediction </title>
+
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.0/chroma.min.js" integrity="sha512-yocoLferfPbcwpCMr8v/B0AB4SWpJlouBwgE0D3ZHaiP1nuu5djZclFEIj9znuqghaZ3tdCMRrreLoM8km+jIQ==" crossorigin="anonymous"></script>
+
+    <style type="text/css">
+      * {
+        margin: 0;
+        padding: 0;
+      }
+      html, body {
+        width: 100%;
+        font-size: 1rem;
+      }
+      body {
+        font-family: 'Ubuntu', sans-serif;
+      }
+      canvas {
+        background-color: white;
+      }
+      h1, h2, h3, h4, h5, h6 {
+        color: dodgerblue;
+        text-align: center;
+        font-weight: lighter;
+      }
+      h1 {
+        margin: 2rem;
+        font-size: 3rem;
+      }
+      h2 {
+        font-size: 2rem;
+        margin-top: 1rem;
+        margin-bottom: .5rem;
+      }
+      button.btn {
+        color: #ccc;
+        margin: 1rem;
+        padding: .5rem;
+        font-size: 1rem;
+        min-width: 4rem;
+        border: none;
+        border-radius: .5rem;
+        background-color: grey;
+        transition-duration: 0.25s;
+        cursor: pointer;
+      }
+      button.btn.selected {
+        color: #eee;
+        background-color: dodgerblue;
+      }
+      button.btn.green {
+        color: #eee;
+        background-color: #10941f;
+      }
+      button.btn:focus {
+        outline: none;
+        color: inherit;
+      }
+      button.btn:hover {
+        color: white;
+        box-shadow: 0 0 10px dodgerblue;
+      }
+      button.btn.green:hover {
+        color: white;
+        box-shadow: 0 0 10px limegreen;
+      }
+      .main {
+        min-height: 90vh;
+        position: relative;
+      }
+      .flex {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        padding: 1rem;
+      }
+      .col {
+        flex-direction: column;
+        flex-grow: 0;
+      }
+      .controls {
+        padding-bottom: 10vh;
+      }
+      .box {
+        padding: .5rem 1rem;
+        margin: .5rem auto;
+        width: fit-content;
+        border-radius: 1rem;
+      }
+      .mono {
+        font-family: monospace;
+        color: #555;
+        background-color: #ddd;
+        padding: .25rem;
+        border-radius: .25rem;
+      }
+      .space-1 {
+        line-height: 1.2;
+      }
+      .space-2 {
+        line-height: 1.5;
+      }
+      .relative {
+        position: relative;
+      }
+      .legend {
+        max-width: 350px;
+      }
+      .legend .scale {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+      }
+      .legend .color {
+        width: 150px;
+        height: 30px;
+        justify-content: space-between;
+        background: linear-gradient(
+          90deg,
+          rgba(255,55,0,1)   0%,
+          rgba(216,224,6,1)  33%,
+          rgba(34,213,238,1) 66%,
+          rgba(3,30,148,1)   100%
+          );
+      }
+      .legend .ticks {
+        margin-top: -1rem;
+        width: 180px;
+        justify-content: space-between;
+      }
+      #ngl-root-parent {
+        width: 40vw;
+        height: 30vw;
+        margin: auto;
+        position: relative;
+      }
+      #ngl-root {
+        width: 40vw;
+        height: 30vw;
+        border-radius: 15px;
+        border: 1px solid grey;
+      }
+      #ngl-nothing {
+        position: absolute;
+        top: 0;
+        left: 0;
+        display: none;
+        text-align: center;
+        width: 40vw;
+        height: 30vw;
+        padding-top: 12vw;
+      }
+      #ngl-loading {
+        position: absolute;
+        top: 0;
+        left: 0;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 800px;
+        height: 600px;
+        width: 40vw;
+        height: 30vw;
+      }
+      #ngl-loading svg {
+        width: 30%;
+        height: 30%;
+        width: 10vw;
+        height: 10vw;
+      }
+
+      /* Responsive */
+      @media (max-width: 1400px) {
+        :root {
+          font-size: 10pt;
+        }
+        button.btn {
+          margin: .5rem;
+          padding: .25rem;
+        }
+        .box {
+          padding: .5rem;
+          margin: .5rem auto;
+        }
+        .legend {
+          max-width: 200px;
+        }
+        .help-text {
+          font-size: 0.8rem;
+        }
+        .mono {
+          padding: .25rem .5rem;
+        }
+      }
+      @media (max-width: 1000px) {
+        :root {
+          font-size: 8pt;
+        }
+      }
+      @media (max-width: 800px) {
+        :root {
+          font-size: 6pt;
+        }
+      }
+    </style>
+
+    <script src="https://cdn.rawgit.com/arose/ngl/v2.0.0-dev.37/dist/ngl.js"></script>
+  </head>
+
+
+  <body>
+    <h1> Alphafold structure prediction </h1>
+
+    <div class="main flex">
+      <div class="col relative">
+        <div id="ngl-root-parent">
+
+          <div id="ngl-root"></div>
+
+          <div id="ngl-nothing">
+            Select a representation to display
+          </div>
+
+          <div id="ngl-loading">
+            <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
+              <g transform="rotate(0 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(30 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(60 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(90 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(120 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(150 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(180 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(210 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(240 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(270 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(300 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
+                </rect>
+              </g><g transform="rotate(330 50 50)">
+                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
+                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
+                </rect>
+              </g>
+            </svg>
+          </div>
+        </div>
+
+        <div class="flex">
+          <div class="box space-1">
+            <p>
+              <span class="mono">Scroll up/down</span>
+              to zoom in and out
+            </p>
+            <p>
+              <span class="mono">Click + drag</span>
+              to rotate the structure
+            </p>
+            <p>
+              <span class="mono">CTRL + click + drag</span>
+              to move the structure
+            </p>
+            <p>
+              <span class="mono">Click</span>
+              an atom to bring it into focus
+            </p>
+          </div>
+
+          <div class="box legend">
+            <div class="scale">
+              <div class="color"></div>
+              <div class="flex ticks">
+                <div>&lt;50</div>
+                <div>70</div>
+                <div>90+</div>
+              </div>
+            </div>
+
+            <div>
+              <p class="text-center">
+                <small>
+                Alphafold produces a
+                <a href="https://alphafold.ebi.ac.uk/faq#faq-5" target="_blank">
+                  per-residue confidence score (pLDDT)
+                </a>
+                between 0 and 100. Some regions below 50 pLDDT may be
+                unstructured in isolation.
+              </small>
+              </p>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex col controls">
+        <div class="box text-center">
+          <h3> Select model </h3>
+          <p>The top-ranked structures predicted by Alphafold</p>
+          <div>
+            <button class="btn selected" id="btn-ranked_0" onclick="setModel(0);">
+              Ranked 0
+            </button>
+
+            <button class="btn" id="btn-ranked_1" onclick="setModel(1);">
+              Ranked 1
+            </button>
+
+            <button class="btn" id="btn-ranked_2" onclick="setModel(2);">
+              Ranked 2
+            </button>
+
+            <button class="btn" id="btn-ranked_3" onclick="setModel(3);">
+              Ranked 3
+            </button>
+
+            <button class="btn" id="btn-ranked_4" onclick="setModel(4);">
+              Ranked 4
+            </button>
+          </div>
+        </div>
+
+        <div class="box text-center">
+          <h3> Toggle representations </h3>
+          <div>
+            <button class="btn selected" id="btn-cartoon" onclick="toggleModelRepresentation('cartoon');">
+              Cartoon
+            </button>
+
+            <button class="btn" id="btn-ball-stick" onclick="toggleModelRepresentation('ball+stick');">
+              Ball + stick
+            </button>
+
+            <button class="btn" id="btn-surface" onclick="toggleModelRepresentation('surface');">
+              Surface
+            </button>
+
+            <button class="btn" id="btn-backbone" onclick="toggleModelRepresentation('backbone');">
+              Backbone
+            </button>
+          </div>
+        </div>
+
+        <div class="box text-center">
+          <h3> Actions </h3>
+          <div>
+            <button class="btn selected" id="btn-toggle-spin" onclick="toggleSpin();">
+              Toggle spin
+            </button>
+
+            <button class="btn" id="btn-toggle-dark" onclick="toggleDark();">
+              Dark mode
+            </button>
+          </div>
+        </div>
+
+        <div class="box text-center">
+          <h3> Download </h3>
+          <div>
+            <button class="btn green" onclick="downloadPng();">
+              Snapshot
+            </button>
+
+            <button class="btn green" onclick="downloadPdb();">
+              PDB
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+
+
+  <script type="text/javascript">
+
+    // Render NGLviewer for PDB files
+
+    // State management has been implemented with vanilla Js but could have used
+    // Vue - it's a fairly simple use case so a global 'state' object works fine
+    // without complicating things too much.
+
+
+    // Define a custom color scheme to represent model confidence consistently
+    // across different representations
+    // ------------------------------------------------------------------------
+    const colorScale = chroma.scale([
+      'red', 'yellow', 'green', 'cyan', 'blue'
+    ]).mode('lab').domain([0, 0.9]);
+
+    const confidenceScheme = NGL.ColormakerRegistry.addScheme(function (params) {
+      this.atomColor = function (atom) {
+        // Actually model confidence (pLDDT)
+        const c = atom.bfactor;
+        const BREAK_RED = 40;   // Below this is just plain red
+        let range, r, g, b;
+
+        if (c < BREAK_RED) {
+          return 0xFF0000;
+        }
+        const p = (c - BREAK_RED) / (100 - BREAK_RED)
+        return eval(colorScale(p).hex().replace('#', '0x'));
+      };
+    });
+
+    // NGL color schemes https://nglviewer.org/ngl/api/manual/usage/coloring.html
+    const COLORSCHEME = confidenceScheme;  //'bfactor'
+
+    const MODELS = [
+      'ranked_0.pdb',
+      'ranked_1.pdb',
+      'ranked_2.pdb',
+      'ranked_3.pdb',
+      'ranked_4.pdb',
+    ]
+
+    const REPRESENTATIONS = [
+      'cartoon',
+      'ball+stick',
+      'surface',
+      'backbone',
+    ]
+
+    const DEFAULT_REPRESENTATION = REPRESENTATIONS[0];
+    const MAX_CLICK_INTERVAL_MS = 500;  // For debouncing model clicks
+
+    let stage;
+    let nonceSetModel;
+
+    let state = {
+      model: 0,
+      modelObject: null,
+      representations: {},
+      colorScheme: 'bfactor',
+      darkMode: false,
+      loading: 1,
+      spin: true,
+    }
+
+    const uri = (i) => MODELS[i];
+    // Switch to this function to return sample model URI (local dev)
+    // const uri = (i) => `https://raw.githubusercontent.com/neoformit/alphafold-galaxy/main/data/${MODELS[i]}`;
+
+    document.addEventListener("DOMContentLoaded", async function () {
+      // Can set debug for development if NGL is being... funny
+      // NGL.setDebug(true)
+
+      // Create NGL Stage object
+      stage = new NGL.Stage("ngl-root", { backgroundColor: 'white' });
+
+      // Handle window resizing
+      window.addEventListener("resize",  () => stage.handleResize());
+
+      loadModel();
+      while (true) {
+        if (!state.loading) {
+          // Reload page if NGL failed to display. Weird occassional bug.
+          const canvas = document.querySelector('#ngl-root canvas');
+          canvas.height < 50 && window.reload();
+          break
+        }
+        await new Promise(resolve => setTimeout(resolve, 500));
+      }
+    });
+
+    // Models ------------------------------------------------------------------
+
+    const setModel = (ix) => {
+      state.model = ix;
+      stage.removeComponent(state.modelObject);
+      setLoading(1);
+
+      // Debounce rapid model clicking with a nonce
+      nonceSetModel = new Object();
+      const localNonce = nonceSetModel;
+      setTimeout( () => {
+        if (localNonce === nonceSetModel) {
+          // The user has stopped clicking, hurray...
+          loadModel().then(updateButtons);
+        }
+      }, MAX_CLICK_INTERVAL_MS);
+    }
+
+    const loadModel = () => {
+      reps = Object.keys(state.representations);
+      if (reps.length) {
+        state.representations = {};
+      } else {
+        reps = [DEFAULT_REPRESENTATION];
+      }
+
+      // Load PDB entry
+      return stage.loadFile(uri(state.model)).then( (o) => {
+        state.modelObject = o;
+        reps.forEach( (r) => addModelRepresentation(r) );
+        stage.setSpin(state.spin);
+        o.autoView();
+        setLoading(0);
+      })
+    }
+
+    // Representations ---------------------------------------------------------
+
+    const toggleModelRepresentation = (rep) => {
+      rep in state.representations ?
+        removeModelRepresentation(rep)
+        : addModelRepresentation(rep)
+    }
+
+    const addModelRepresentation = (rep) => {
+      state.representations[rep] =
+        state.modelObject.addRepresentation(rep, {colorScheme: COLORSCHEME});
+      updateButtons();
+    }
+
+    const removeModelRepresentation = (rep) => {
+      o = state.representations[rep];
+      state.modelObject.removeRepresentation(o);
+      delete state.representations[rep];
+      updateButtons();
+    }
+
+    const clearModelRepresentations = () => {
+      state.modelObject && state.modelObject.removeAllRepresentations();
+      state.representations = {};
+    }
+
+    // Actions -----------------------------------------------------------------
+
+    const toggleDark = () => {
+      state.darkMode = !state.darkMode;
+      stage.setParameters({
+        backgroundColor: state.darkMode ? 'black' : 'white',
+      });
+      const btn = document.querySelector('#btn-toggle-dark');
+      btn && btn.classList.toggle('selected');
+    }
+
+    const setLoading = (state) => {
+      document.getElementById('ngl-loading')
+        .style.display = state ? 'flex' : 'none';
+      state.loading = state;
+    }
+
+    const toggleSpin = () => {
+      stage.toggleSpin();
+      const btn = document.querySelector('#btn-toggle-spin');
+      btn && btn.classList.toggle('selected');
+      state.spin = !state.spin;
+    }
+
+    const downloadPng = () => {
+      const params = {
+        factor: 3,
+        antialias: true,
+      }
+      stage.makeImage(params).then( (blob) => {
+        const name = MODELS[state.model].replace('.pdb', '.png');
+        const url = URL.createObjectURL(blob);
+        makeDownload(url, name);
+      })
+    }
+
+    const downloadPdb = () => {
+      const url = uri(state.model);
+      const name = `alphafold_${MODELS[state.model]}`;
+      makeDownload(url, name);
+    }
+
+    const makeDownload = (url, name) => {
+      // Will not work with cross-origin urls (i.e. during development)
+      console.log(`Creating file download for ${name}, href ${url}`);
+      const saveLink = document.createElement('a');
+      saveLink.href = url;
+      saveLink.download = name;
+      document.body.appendChild(saveLink);
+      saveLink.dispatchEvent(
+        new MouseEvent('click', {
+          bubbles: true,
+          cancelable: true,
+          view: window
+        })
+      );
+      document.body.removeChild(saveLink);
+    }
+
+    const updateButtons = () => {
+      MODELS.forEach( (name, i) => {
+        const id = `#btn-${name.replace('.pdb', '')}`;
+        const btn = document.querySelector(id);
+        if (!btn) return
+        i == state.model ?
+          btn.classList.add('selected')
+          : btn.classList.remove('selected');
+      })
+
+      REPRESENTATIONS.forEach( (name) => {
+        const id = `#btn-${name}`.replace('+', '-');
+        const btn = document.querySelector(id);
+        if (!btn) return
+        if (name in state.representations) {
+          btn.classList.add('selected')
+        } else {
+          btn.classList.remove('selected');
+        }
+      });
+
+      // Show "Nothing to display" if no representations are selected
+      document.querySelector('#ngl-nothing').style.display =
+        Object.keys(state.representations).length ?
+        'none'
+        : 'block';
+    }
+
+  </script>
+
+</html>
--- a/scripts/outputs.py	Wed May 08 06:26:55 2024 +0000
+++ b/scripts/outputs.py	Sun Jul 28 20:09:55 2024 +0000
@@ -42,6 +42,12 @@
     'multimer': 'iptm+ptm',
 }
 
+HTML_PATH = Path(__file__).parent / "alphafold.html"
+HTML_OUTPUT_FILENAME = 'alphafold.html'
+HTML_BUTTON_ATTR = 'class="btn" id="btn-ranked_{rank}"'
+HTML_BUTTON_ATTR_DISABLED = (
+    'class="btn disabled" id="btn-ranked_{rank}" disabled')
+
 
 class Settings:
     """Parse and store settings/config."""
@@ -188,7 +194,7 @@
     """Write per-model confidence scores."""
     path = context.settings.workdir / OUTPUTS['model_confidence_scores']
     with open(path, 'w') as f:
-        for rank in range(1, 6):
+        for rank in range(1, len(context.model_pkl_paths) + 1):
             score = ranking.get_plddt_for_rank(rank)
             f.write(f'ranked_{rank - 1}\t{score:.2f}\n')
 
@@ -300,6 +306,22 @@
         plt.savefig(png_path)
 
 
+def template_html(context: ExecutionContext):
+    """Template HTML file.
+
+    Remove buttons that are redundant with limited model outputs.
+    """
+    print("Templating HTML file...")
+    with open(HTML_PATH) as f:
+        html = f.read()
+    for i in range(len(context.model_pkl_paths), 5):
+        btn_id = HTML_BUTTON_ATTR.format(rank=i)
+        btn_attr_disabled = HTML_BUTTON_ATTR_DISABLED.format(rank=i)
+        html = html.replace(btn_id, btn_attr_disabled)
+    with open(context.settings.output_dir / HTML_OUTPUT_FILENAME, 'w') as f:
+        f.write(html)
+
+
 def main():
     """Parse output files and generate additional output files."""
     settings = Settings()
@@ -307,6 +329,7 @@
     ranking = ResultRanking(context)
     write_confidence_scores(ranking, context)
     rekey_relax_metrics(ranking, context)
+    template_html(context)
 
     # Optional outputs
     if settings.output_model_pkls: