3dmol / 3Dmol.js

WebGL accelerated JavaScript molecular graphics library
https://3dmol.org/
Other
794 stars 194 forks source link

[BUG] WRONG BONDS BEING DRAWN #776

Closed JavierSanchez-Utges closed 6 months ago

JavierSanchez-Utges commented 6 months ago

Describe the bug

I think bonds are being drawn between atoms that should not be connected. For example, the O atoms from HOH and MN of the Mn ions are being linked to the B7Q (blue), K0I (red), and W77 (cyan).

To Reproduce

<title>3DMol.js experiment</title>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script src="https://3Dmol.org/build/3Dmol-min.js"></script>     
<script src="https://3Dmol.org/build/3Dmol.ui-min.js"></script> 
<div id="container-01" class="mol-container"></div>

<style>
  .mol-container {
    width: 50%;
    height: 75%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    border: px solid black; /* Adds a black border */
    box-sizing: border-box; /* Ensures that the border is included in the element's total width and height */
  }
</style>

<script>
  // Define a function for applying styles and visualizations
  function applyVisualizationStyles(viewer) {
    viewer.setViewStyle({style:"outline", width:0.0625, color:"black"});
    viewer.setStyle({hetflag: false}, {cartoon: {hidden: false, style: 'oval', color: 'white', arrows: true,}});
    viewer.setStyle({hetflag: true}, {stick: {hidden: true,}}); 
    viewer.setStyle({resn: 'B7Q'}, {stick: {color: "blue", hidden: false}});
    viewer.setStyle({resn: 'K0I'}, {stick: {color: "red",hidden: false}});
    viewer.setStyle({resn: 'HOH'}, {stick: {color: "green", hidden: false}});
    viewer.setStyle({resn: 'MN'}, {stick: {color: "orange", hidden: false}});
    viewer.setStyle({resn: 'W77'}, {stick: {color: "cyan", hidden: false}});
    viewer.setStyle({resn: 'PO4'}, {stick: {color: "magenta",  hidden: false}});
    viewer.setStyle({resn: 'DMS'}, {stick: {color: "yellow",  hidden: false}});
    viewer.zoomTo(); 
    viewer.render(); 
  }

  let element = document.querySelector('#container-01');
  let config = { backgroundColor: 'white', keepH: false };
  let viewer = $3Dmol.createViewer(element, config);
  let pdbUris = [
    '/static/data/Q9UGL1/5fpu_A_trans.cif',
    '/static/data/Q9UGL1/5fz1_A_trans.cif',
    '/static/data/Q9UGL1/6ej1_A_trans.cif',
  ];
  let loadedCount = 0; // Counter for loaded structures
  let models = [];
  let modelOrder = {}; // creating dictionary to save the order in which files get loaded
  pdbUris.forEach(pdbUri => {
    jQuery.ajax(pdbUri, {
      success: function(data) {
        let model = viewer.addModel(data, "cif"); // Load data
        console.log("Loaded model", pdbUri, model.getID(), loadedCount);
        modelOrder[pdbUri] = model.getID();
        models.push(model);
        loadedCount++; // Increment counter
        if (loadedCount === pdbUris.length) { // All structures are loaded, apply styles
          console.log("All structures loaded");
          console.log("Model order", modelOrder);
          applyVisualizationStyles(viewer)
        }
      },
      error: function(hdr, status, err) {
        console.error("Failed to load PDB " + pdbUri + ": " + err);
        // Error handling, potentially increment loadedCount or handle differently
      },
    });
  });
  </script>

Expected behaviour

Only atoms forming part of the same molecule are connected by bonds drawn in the viewer. In this case, O atoms of HOH and Mn of MN are not connected to the other ligands.

I suspect this has to do with assignPDBBonds (https://3dmol.csb.pitt.edu/doc/parsers_utils_assignPDBBonds.ts.html), and assignBonds (https://3dmol.csb.pitt.edu/doc/parsers_utils_assignBonds.ts.html), which seems they use distance, amongst other things to draw the bonds.

I understand the connectivity issue is a tricky one to solve, but, perhaps looking at the resn or resi might fix drawing incorrect bonds between different molecules (that satisfy the distance criteria). Just a suggestion, this simplistic solution might not be adequate, though.

Screenshots (3DMol.js on the left and ChimeraX on the right)

image

Desktop

Additional context

Here the files:

5fpu_A_trans.txt 5fv3_A_trans.txt 6ej1_A_trans.txt

dkoes commented 6 months ago

When you load the files, set the assignBonds parser option to false.

JavierSanchez-Utges commented 6 months ago

But then I am losing ALL bonds, not just the wrong ones, it seems. As I get nothing when I am showing sticks.

Is it on here I have to add it: let model = viewer.addModel(data, "cif", {assignBonds: false,}); // Load data ?

JavierSanchez-Utges commented 6 months ago

It seems bonds does not only consider covalent bonds, but other kinds of interactions? The Mn and Zn atoms here satisfy {not:{bonds:0}}, and so do present bonds. In purple are HETATM that present bonds, and yellow HETATM that do not present any bonds.

Basically what I want to do is select all ions and HOH and show them independently as spheres, and molecules showing covalent bonds, and formed by multiple atoms, such as other ligands, show them with sticks.

Screenshot 2024-03-07 at 17 02 30

Here my code:

<title>3DMol.js experiment</title>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script src="https://3Dmol.org/build/3Dmol-min.js"></script>     
<script src="https://3Dmol.org/build/3Dmol.ui-min.js"></script> 
<div id="container-01" class="mol-container"></div>

<style>
  .mol-container {
    width: 50%;
    height: 75%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    border: px solid black; /* Adds a black border */
    box-sizing: border-box; /* Ensures that the border is included in the element's total width and height */
  }
</style>

<script>
  let element = document.querySelector('#container-01');
  let config = { backgroundColor: 'white', keepH: false };
  let viewer = $3Dmol.createViewer(element, config);
  let pdbUris = [
    '/static/data/Q9UGL1/5fze_updated.cif',
  ];
  let loadedCount = 0; // Counter for loaded structures
  let models = [];
  let modelOrder = {}; // creating dictionary to save the order in which files get loaded
  pdbUris.forEach(pdbUri => {
    jQuery.ajax(pdbUri, {
      success: function(data) {
        let model = viewer.addModel(data, "cif"); // Load data
        console.log("Loaded model", pdbUri, model.getID(), loadedCount);
        modelOrder[pdbUri] = model.getID();
        models.push(model);
        loadedCount++; // Increment counter
        if (loadedCount === pdbUris.length) { // All structures are loaded, apply styles
          console.log("All structures loaded");
          console.log("Model order", modelOrder);

          viewer.setViewStyle({style:"outline", width:0.0625, color:"black"});
          viewer.setStyle({hetflag: false}, {cartoon: {hidden: false, style: 'oval', color: 'white', arrows: true,}});
          viewer.setStyle({hetflag: true}, {stick: {hidden: true}});
          viewer.setStyle({and:[{hetflag: true}, {not:{bonds:0}}]}, {sphere: {hidden: false, color: 'purple', radius: 0.5}});
          viewer.setStyle({and:[{hetflag: true}, {bonds:0}]}, {sphere: {hidden: false, color: 'gold', radius: 0.5}});
          viewer.zoomTo(); 
          viewer.render(); 
        }
      },
      error: function(hdr, status, err) {
        console.error("Failed to load PDB " + pdbUri + ": " + err);
        // Error handling, potentially increment loadedCount or handle differently
      },
    });
  });
  </script>
dkoes commented 6 months ago

A properly constructed cif file will specify the bonds of hetatms. Lacking bond information in the file, the bonds must be inferred using distances (and possibly valency constraints). If you can provide 5fze_updated.cif I can see if there's a way to tweak the heuristics to get a better result, but most likely you will want to select and style ions separately.

JavierSanchez-Utges commented 6 months ago

Here I am using 5FPU_updated as downloaded from PDBe, which was the structure in which the MN ion was incorrectly linked to the K0I ligand molecule.

Here the code:

<title>3DMol.js experiment</title>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script src="https://3Dmol.org/build/3Dmol-min.js"></script>     
<script src="https://3Dmol.org/build/3Dmol.ui-min.js"></script> 
<div id="container-01" class="mol-container"></div>

<style>
  .mol-container {
    width: 50%;
    height: 75%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    border: px solid black; /* Adds a black border */
    box-sizing: border-box; /* Ensures that the border is included in the element's total width and height */
  }
</style>

<script>

  let element = document.querySelector('#container-01');
  let config = { backgroundColor: 'white', keepH: false };
  let viewer = $3Dmol.createViewer(element, config);
  let pdbUris = [
    <!-- '/static/data/Q9UGL1/5fpu_A_trans.cif', -->
    '/static/data/Q9UGL1/5fpu_updated.cif',
    <!-- '/static/data/Q9UGL1/5fv3_A_trans.cif', -->
    <!-- '/static/data/Q9UGL1/6ej1_A_trans.cif', -->
  ];

  let loadedCount = 0; // Counter for loaded structures
  let models = [];
  let modelOrder = {}; // creating dictionary to save the order in which files get loaded
  let modelOrderRev = {};
  pdbUris.forEach(pdbUri => {
    jQuery.ajax(pdbUri, {
      success: function(data) {
        let model = viewer.addModel(data, "cif",); // Load data
        let modelID = model.getID();
        let pdbID = pdbUri.split("/").pop().split("_")[0];
        modelOrder[pdbUri] = modelID;
        modelOrderRev[modelID] = pdbID;
        models.push(model);
        loadedCount++; // Increment counter
        if (loadedCount === pdbUris.length) { // All structures are loaded, apply styles
          console.log("All structures loaded");

          viewer.setViewStyle({style:"outline", width:0.0625, color:"black"});
          viewer.setStyle({hetflag: false}, {cartoon: {hidden: false, style: 'oval', color: 'white', arrows: true,  thickness: 0.25}});
          viewer.setStyle({hetflag: true}, {stick: {hidden: true, radius: 0.25}});
          viewer.addStyle({and:[{hetflag: true}, {not:{resn: "HOH"}}]}, {stick: {hidden: false, color: "blue", radius: 0.25}}); 
          viewer.addStyle({and:[{hetflag: true}, {not:{resn: "HOH"}}]}, {sphere: {hidden: false, color: "red", radius: 0.20}});
          viewer.addStyle({resn: "HOH"}, {sphere: {hidden: false, color: "gold", radius: 0.20}});

          viewer.addStyle({resn: 'MN'}, {stick: {hidden: false, radius: 0.25, color:"orange"}});

          viewer.setHoverable({}, true, 
            function(atom,viewer,event,container) {
              if(!atom.label) {
                atom.label = viewer.addLabel(
                  modelOrderRev[atom.model] + " " + atom.chain + " " + atom.resn + " " + atom.resi + " " + atom.atom,
                  {position: atom, backgroundColor: 'mintcream', fontColor:'black', borderColor: 'black', borderThickness: 2}
                );
              }
            },
            function(atom) {
              if(atom.label) {
                viewer.removeLabel(atom.label);
                delete atom.label;
              }
            }
          );

          viewer.zoomTo(); 
          viewer.render(); 
        }
      },
      error: function(hdr, status, err) {
        console.error("Failed to load PDB " + pdbUri + ": " + err);
        // Error handling, potentially increment loadedCount or handle differently
      },
    });
  });
  </script>

Here the files: 5fpu_A_trans.txt 5fpu_updated.txt

Loading the structure with default settings seems to draw bond between K0I and MN. Using {assignBonds: false} does not draw any bonds at all and nothing can be displayed with sticks.

Screenshot 2024-03-11 at 10 48 53
dkoes commented 6 months ago

The reason there is a bond is that the atoms are within their covalent radius of each other. I was wondering why pymol doesn't draw a bond and it is because there is an unbound_cations option (on by default in pymol) that avoids such bonds. I've added a parser option, unboundCations that, if set, will mimic this behavior. https://3dmol.org/tests/auto/generate_test.cgi?test=unboundcations

JavierSanchez-Utges commented 6 months ago

I see, very interesting. That works nicely now, all the bonds between HOH, MN and the other ligands are gone now. I also really like the touch that the opacity adds to the visualisation.

I found the PyMol code and saw it considers the same cations you implemented: ["Na", "K", "Ca", "Mg", "Mn", "Sr"]. I was wondering then, why is PyMol just implementing these? I get they are some of the most common, but why isn't Zinc included for example. And what about anions? Chemistry is not my area of expertise, and I might be missing something here.

I also had a question about your implementation:

What is the meaning of this line var callback = function() {}; and then calling the function at the end when rendering the new viewer viewer.render(callback);.

dkoes commented 6 months ago

That is something the test harness uses (e.g., when this individual test is run with glcheck on the commandline or in browser at https://3dmol.org/tests/). We need a callback to capture the input and compare it to the reference image (and to know to proceed to the next test).