molecularnodes 2.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- molecularnodes-2.11.0/LICENSE +21 -0
- molecularnodes-2.11.0/PKG-INFO +15 -0
- molecularnodes-2.11.0/README.md +58 -0
- molecularnodes-2.11.0/molecularnodes/__init__.py +48 -0
- molecularnodes-2.11.0/molecularnodes/assembly/__init__.py +50 -0
- molecularnodes-2.11.0/molecularnodes/assembly/cif.py +162 -0
- molecularnodes-2.11.0/molecularnodes/assembly/mesh.py +91 -0
- molecularnodes-2.11.0/molecularnodes/assembly/mmtf.py +55 -0
- molecularnodes-2.11.0/molecularnodes/assembly/pdb.py +134 -0
- molecularnodes-2.11.0/molecularnodes/assets/MN_data_file.blend +0 -0
- molecularnodes-2.11.0/molecularnodes/assets/template/MolecularNodes/startup.blend +0 -0
- molecularnodes-2.11.0/molecularnodes/assets/template/MolecularNodes.zip +0 -0
- molecularnodes-2.11.0/molecularnodes/auto_load.py +157 -0
- molecularnodes-2.11.0/molecularnodes/coll.py +69 -0
- molecularnodes-2.11.0/molecularnodes/color.py +9 -0
- molecularnodes-2.11.0/molecularnodes/data.py +515 -0
- molecularnodes-2.11.0/molecularnodes/density.py +265 -0
- molecularnodes-2.11.0/molecularnodes/esmfold.py +142 -0
- molecularnodes-2.11.0/molecularnodes/load.py +736 -0
- molecularnodes-2.11.0/molecularnodes/md.py +284 -0
- molecularnodes-2.11.0/molecularnodes/mda.py +921 -0
- molecularnodes-2.11.0/molecularnodes/nodes.py +925 -0
- molecularnodes-2.11.0/molecularnodes/obj.py +141 -0
- molecularnodes-2.11.0/molecularnodes/pkg.py +406 -0
- molecularnodes-2.11.0/molecularnodes/pref.py +68 -0
- molecularnodes-2.11.0/molecularnodes/requirements.txt +5 -0
- molecularnodes-2.11.0/molecularnodes/star.py +153 -0
- molecularnodes-2.11.0/molecularnodes/ui.py +764 -0
- molecularnodes-2.11.0/molecularnodes/utils.py +112 -0
- molecularnodes-2.11.0/pyproject.toml +29 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Brady Johnston
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: molecularnodes
|
|
3
|
+
Version: 2.11.0
|
|
4
|
+
Summary: Toolbox for molecular animations with Blender and Geometry Nodes.
|
|
5
|
+
Author: Brady Johnston
|
|
6
|
+
Author-email: brady.johnston@me.com
|
|
7
|
+
Requires-Python: >=3.10.0,<3.11.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Requires-Dist: MDAnalysis (==2.6.1)
|
|
11
|
+
Requires-Dist: biotite (==0.37.0)
|
|
12
|
+
Requires-Dist: bpy (>=3.5.0,<4.0)
|
|
13
|
+
Requires-Dist: eulerangles
|
|
14
|
+
Requires-Dist: mrcfile
|
|
15
|
+
Requires-Dist: starfile
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# MolecularNodes 🧬🍝💻
|
|
2
|
+
|
|
3
|
+
<a href="https://www.github.com/bradyajohnston/MolecularNodes/releases"><img src="https://img.shields.io/github/v/release/bradyajohnston/molecularnodes" alt="Badge displaying license, which is MIT." style="height:20px"/></a> <a href="https://www.github.com/bradyajohnston/MolecularNodes/releases"><img src="https://img.shields.io/github/downloads/BradyAJohnston/MolecularNodes/total.svg" alt="Repo total downloads count." style="height:20px"/></a> <a href="https://www.buymeacoffee.com/bradyajohnston"><img src="https://img.shields.io/github/license/bradyajohnston/molecularnodes" alt="Badge displaying license, which is MIT." style="height:20px"/></a> <a href="https://www.buymeacoffee.com/bradyajohnston"><img src="https://img.shields.io/github/stars/bradyajohnston/molecularnodes?style=social" alt="Badge displaying count of GitHub stars." style="height:20px"/></a>
|
|
4
|
+
|
|
5
|
+
  [](https://codecov.io/gh/BradyAJohnston/MolecularNodes)
|
|
6
|
+
|
|
7
|
+
<a href="https://pypi.org/project/biotite"><img src="https://img.shields.io/badge/powered%20by-Biotite-orange.svg" alt="Button linking to buymeacoffee.com to leave me a tip as a thank you." style="height:20px"/></a> <a href="https://pypi.org/project/MDAnalysis"><img src="https://img.shields.io/badge/powered%20by-MDAnalysis-orange.svg" alt="Button linking to buymeacoffee.com to leave me a tip as a thank you." style="height:20px"/></a> <a href="https://pypi.org/project/mrcfile"><img src="https://img.shields.io/badge/powered%20by-mrcfile-orange.svg" alt="Button linking to buymeacoffee.com to leave me a tip as a thank you." style="height:20px"/></a>
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## About
|
|
11
|
+
|
|
12
|
+
MolecularNodes enables quick import and visualisation of structural biology data inside of Blender. Blender provides advanced industry-leading visualisation and animation technology, while MolecularNodes provides the interface that allows Blender to understand the unique data formats used in structural biology.
|
|
13
|
+
|
|
14
|
+
The add-on enables creating animations from static crystal structures, styling proteins and other molecules in a variety of highly customisable styles, importing and playing back molecular dynamics trajectories from a wide variety of sources, and even importing of EM density maps.
|
|
15
|
+
|
|
16
|
+
## Examples
|
|
17
|
+
|
|
18
|
+
See examples, tutorials and video projects that use Molecular Nodes in the [documentation page](https://bradyajohnston.github.io/MolecularNodes).
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
See the [installation page](https://bradyajohnston.github.io/MolecularNodes/installation.html) of the documentation, for detailed instructions on how to install the add-on.
|
|
23
|
+
|
|
24
|
+
## Getting Started
|
|
25
|
+
|
|
26
|
+
These tutorials are for earlier versions of the addon. There are some differences in design, but overall the workflow is the same. Watch through the videos to get an overview of how the addon works.
|
|
27
|
+
|
|
28
|
+
[](https://youtu.be/CvmFaRVmZRU)
|
|
29
|
+
|
|
30
|
+
## Contributing
|
|
31
|
+
|
|
32
|
+
If you would like to contribute to the project, please open an issue to discuss potential new features, or comment on an existing issue if you would like to help with fixing it. I welcome any and all potential PRs.
|
|
33
|
+
|
|
34
|
+
To contribute to the project, fork and clone the Molecular Nodes repo to your local machine. I recommend using VS Code and the [Blender VS Code](https://github.com/JacquesLucke/blender_vscode) addon which streamlines the development process.
|
|
35
|
+
|
|
36
|
+
Once installed, you can use the `Blender: Build and Start` command with VS Code open in the addon directory, to start Blender with the addon built and installed. Any changes that are then made to the underlying addon code, can be quickly previewed inside of the running Blender by using the VS Code command `Blender: Reload Addonds`.
|
|
37
|
+
|
|
38
|
+
Once happy with your code, open a pull request to discuss and get it reviewed by others working on the project. Open a draft pull request early, or open an issue to discuss the scope and feasability of potential features.
|
|
39
|
+
|
|
40
|
+
## Citation
|
|
41
|
+
|
|
42
|
+
A paper has not yet been published on the addon, but if you use it in your academic work you can site it from Zenodo:
|
|
43
|
+
|
|
44
|
+
[](https://zenodo.org/badge/latestdoi/485261976)
|
|
45
|
+
|
|
46
|
+
## Thanks
|
|
47
|
+
|
|
48
|
+
A massive thanks to the [Blender Foundation](https://blender.org) which develops Blender as a free and open source program.
|
|
49
|
+
|
|
50
|
+
<img src="https://download.blender.org/branding/blender_logo.png" alt="The Blender logo." style="height:80px"/>
|
|
51
|
+
|
|
52
|
+
## Buy Me a Coffee
|
|
53
|
+
|
|
54
|
+
If you'd like to say thank you, you can buy me a coffee (or 10!) as a thanks for developing the add-on. Many others have already down exactly that. You can also join our Blender.Science discord, where fellow science visualisation enthusiasts and experts hang out and help each other.
|
|
55
|
+
|
|
56
|
+
<a href="https://www.buymeacoffee.com/bradyajohnston"><img src="https://img.buymeacoffee.com/button-api/?text=Buy Me a Coffee&emoji=&slug=bradyajohnston&button_colour=eabae1&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=FFDD00" alt="Button linking to buymeacoffee.com to leave me a tip as a thank you." style="height:80px"/></a>
|
|
57
|
+
|
|
58
|
+
<img src="https://discord.com/api/guilds/940526858800336936/widget.png?style=banner1" style="height:100px !important; !important;"/></a>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# This program is free software; you can redistribute it and/or modify
|
|
2
|
+
# it under the terms of the GNU General Public License as published by
|
|
3
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
4
|
+
# (at your option) any later version.
|
|
5
|
+
#
|
|
6
|
+
# This program is distributed in the hope that it will be useful, but
|
|
7
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
8
|
+
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
9
|
+
# General Public License for more details.
|
|
10
|
+
#
|
|
11
|
+
# You should have received a copy of the GNU General Public License
|
|
12
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
13
|
+
|
|
14
|
+
bl_info = {
|
|
15
|
+
"name" : "molecularnodes",
|
|
16
|
+
"author" : "Brady Johnston",
|
|
17
|
+
"description" : "Toolbox for molecular animations in Blender & Geometry Nodes.",
|
|
18
|
+
"blender" : (3, 5, 0),
|
|
19
|
+
"version" : (2, 11, 0),
|
|
20
|
+
"location" : "Scene Properties -> Molecular Nodes",
|
|
21
|
+
"warning" : "",
|
|
22
|
+
"doc_url" : "https://bradyajohnston.github.io/MolecularNodes/",
|
|
23
|
+
"tracker_url" : "https://github.com/BradyAJohnston/MolecularNodes/issues",
|
|
24
|
+
"category" : "Import"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
from . import auto_load
|
|
28
|
+
from .mda import _rejuvenate_universe, _sync_universe
|
|
29
|
+
from .ui import MN_add_node_menu
|
|
30
|
+
import bpy
|
|
31
|
+
from . import utils
|
|
32
|
+
|
|
33
|
+
auto_load.init()
|
|
34
|
+
|
|
35
|
+
def register():
|
|
36
|
+
auto_load.register()
|
|
37
|
+
bpy.types.NODE_MT_add.append(MN_add_node_menu)
|
|
38
|
+
utils.template_install()
|
|
39
|
+
|
|
40
|
+
def unregister():
|
|
41
|
+
bpy.types.NODE_MT_add.remove(MN_add_node_menu)
|
|
42
|
+
auto_load.unregister()
|
|
43
|
+
bpy.app.handlers.load_post.remove(_rejuvenate_universe)
|
|
44
|
+
bpy.app.handlers.save_pre.remove(_sync_universe)
|
|
45
|
+
|
|
46
|
+
# register won't be called when MN is run as a module
|
|
47
|
+
bpy.app.handlers.load_post.append(_rejuvenate_universe)
|
|
48
|
+
bpy.app.handlers.save_pre.append(_sync_universe)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A subpackage for reading rotation matrices and translation vectors
|
|
3
|
+
for biological assemblies from different file formats.
|
|
4
|
+
|
|
5
|
+
The central functions are `get_transformations_`
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABCMeta, abstractmethod
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AssemblyParser(metaclass=ABCMeta):
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def list_assemblies(self):
|
|
15
|
+
"""
|
|
16
|
+
Return a ``list`` of ``str`` containing the available assembly
|
|
17
|
+
IDs.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def get_transformations(self, assembly_id):
|
|
22
|
+
"""
|
|
23
|
+
Parse the necessary transformations for a given
|
|
24
|
+
assembly ID.
|
|
25
|
+
|
|
26
|
+
Return a ``list`` of transformations for a set of chains
|
|
27
|
+
transformations:
|
|
28
|
+
|
|
29
|
+
transformations on sets of chains for this assembly
|
|
30
|
+
| chain IDs affected by the transformation
|
|
31
|
+
| | 3x3 rotation matrix
|
|
32
|
+
| | | translation vector
|
|
33
|
+
| | | |
|
|
34
|
+
list[tuple[ndarray, ndarray, ndarray]]]
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_assemblies(self):
|
|
39
|
+
"""
|
|
40
|
+
Parse all the transformations for each assembly, returning a dictionary of
|
|
41
|
+
key:value pairs of assembly_id:transformations. The transformations list
|
|
42
|
+
comes from the `get_transformations(assembly_id)` method.
|
|
43
|
+
|
|
44
|
+
Dictionary of all assemblies
|
|
45
|
+
| Assembly ID
|
|
46
|
+
| | List of transformations to create biological assembly.
|
|
47
|
+
| | |
|
|
48
|
+
dict{'1', list[transformations]}
|
|
49
|
+
|
|
50
|
+
"""
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import itertools
|
|
3
|
+
|
|
4
|
+
from . import AssemblyParser
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CIFAssemblyParser(AssemblyParser):
|
|
8
|
+
### Implementation adapted from ``biotite.structure.io.pdbx.convert``
|
|
9
|
+
|
|
10
|
+
def __init__(self, mmtf_file):
|
|
11
|
+
self._file = mmtf_file
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def list_assemblies(self):
|
|
15
|
+
import biotite.structure.io.pdbx as pdbx
|
|
16
|
+
return list(pdbx.list_assemblies(self._file).keys())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_transformations(self, assembly_id):
|
|
20
|
+
import biotite
|
|
21
|
+
assembly_gen_category = self._file.get_category(
|
|
22
|
+
"pdbx_struct_assembly_gen", expect_looped=True
|
|
23
|
+
)
|
|
24
|
+
if assembly_gen_category is None:
|
|
25
|
+
raise biotite.InvalidFileError(
|
|
26
|
+
"File has no 'pdbx_struct_assembly_gen' category"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
struct_oper_category = self._file.get_category(
|
|
30
|
+
"pdbx_struct_oper_list", expect_looped=True
|
|
31
|
+
)
|
|
32
|
+
if struct_oper_category is None:
|
|
33
|
+
raise biotite.InvalidFileError(
|
|
34
|
+
"File has no 'pdbx_struct_oper_list' category"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if assembly_id not in assembly_gen_category["assembly_id"]:
|
|
38
|
+
raise KeyError(f"File has no Assembly ID '{assembly_id}'")
|
|
39
|
+
|
|
40
|
+
# Extract all possible transformations indexed by operation ID
|
|
41
|
+
transformation_dict = _get_transformations(struct_oper_category)
|
|
42
|
+
|
|
43
|
+
# Get necessary transformations and the affected chain IDs
|
|
44
|
+
# NOTE: The chains given here refer to the `label_asym_id` field
|
|
45
|
+
# of the `atom_site` category
|
|
46
|
+
# However, by default `PDBxFile` uses the `auth_asym_id` as
|
|
47
|
+
# chain ID
|
|
48
|
+
transformations = []
|
|
49
|
+
for id, op_expr, asym_id_expr in zip(
|
|
50
|
+
assembly_gen_category["assembly_id"],
|
|
51
|
+
assembly_gen_category["oper_expression"],
|
|
52
|
+
assembly_gen_category["asym_id_list"],
|
|
53
|
+
):
|
|
54
|
+
# Find the operation expressions for given assembly ID
|
|
55
|
+
# We already asserted that the ID is actually present
|
|
56
|
+
if id == assembly_id:
|
|
57
|
+
operations = _parse_operation_expression(op_expr)
|
|
58
|
+
affected_chain_ids = asym_id_expr.split(",")
|
|
59
|
+
for i, operation in enumerate(operations):
|
|
60
|
+
rotations = []
|
|
61
|
+
translations = []
|
|
62
|
+
for op_step in operation:
|
|
63
|
+
rotation, translation = transformation_dict[op_step]
|
|
64
|
+
rotations.append(rotation)
|
|
65
|
+
translations.append(translation)
|
|
66
|
+
total_rotation, total_translation = _chain_transformations(
|
|
67
|
+
rotations, translations
|
|
68
|
+
)
|
|
69
|
+
transformations.append((
|
|
70
|
+
np.array(affected_chain_ids, dtype="U4").tolist(),
|
|
71
|
+
total_rotation.tolist(),
|
|
72
|
+
total_translation.tolist()
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
return transformations
|
|
76
|
+
|
|
77
|
+
def get_assemblies(self):
|
|
78
|
+
assembly_dict = {}
|
|
79
|
+
for assembly_id in self.list_assemblies():
|
|
80
|
+
assembly_dict[assembly_id] = self.get_transformations(assembly_id)
|
|
81
|
+
|
|
82
|
+
return assembly_dict
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _chain_transformations(rotations, translations):
|
|
86
|
+
"""
|
|
87
|
+
Get a total rotation/translation transformation by combining
|
|
88
|
+
multiple rotation/translation transformations.
|
|
89
|
+
This is done by intermediately combining rotation matrices and
|
|
90
|
+
translation vectors into 4x4 matrices in the form
|
|
91
|
+
|
|
92
|
+
|r11 r12 r13 t1|
|
|
93
|
+
|r21 r22 r23 t2|
|
|
94
|
+
|r31 r32 r33 t3|
|
|
95
|
+
|0 0 0 1 |.
|
|
96
|
+
"""
|
|
97
|
+
total_matrix = np.identity(4)
|
|
98
|
+
for rotation, translation in zip(rotations, translations):
|
|
99
|
+
matrix = np.zeros((4,4))
|
|
100
|
+
matrix[:3, :3] = rotation
|
|
101
|
+
matrix[:3, 3] = translation
|
|
102
|
+
matrix[3, 3] = 1
|
|
103
|
+
total_matrix = matrix @ total_matrix
|
|
104
|
+
|
|
105
|
+
return total_matrix[:3, :3], total_matrix[:3, 3]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _get_transformations(struct_oper):
|
|
110
|
+
"""
|
|
111
|
+
Get transformation operation in terms of rotation matrix and
|
|
112
|
+
translation for each operation ID in ``pdbx_struct_oper_list``.
|
|
113
|
+
"""
|
|
114
|
+
transformation_dict = {}
|
|
115
|
+
for index, id in enumerate(struct_oper["id"]):
|
|
116
|
+
rotation_matrix = np.array(
|
|
117
|
+
[
|
|
118
|
+
[
|
|
119
|
+
float(struct_oper[f"matrix[{i}][{j}]"][index])
|
|
120
|
+
for j in (1, 2, 3)
|
|
121
|
+
]
|
|
122
|
+
for i in (1, 2, 3)
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
translation_vector = np.array(
|
|
126
|
+
[float(struct_oper[f"vector[{i}]"][index]) for i in (1, 2, 3)]
|
|
127
|
+
)
|
|
128
|
+
transformation_dict[id] = (rotation_matrix, translation_vector)
|
|
129
|
+
return transformation_dict
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse_operation_expression(expression):
|
|
133
|
+
"""
|
|
134
|
+
Get successive operation steps (IDs) for the given
|
|
135
|
+
``oper_expression``.
|
|
136
|
+
Form the cartesian product, if necessary.
|
|
137
|
+
"""
|
|
138
|
+
# Split groups by parentheses:
|
|
139
|
+
# use the opening parenthesis as delimiter
|
|
140
|
+
# and just remove the closing parenthesis
|
|
141
|
+
expressions_per_step = expression.replace(")", "").split("(")
|
|
142
|
+
expressions_per_step = [e for e in expressions_per_step if len(e) > 0]
|
|
143
|
+
# Important: Operations are applied from right to left
|
|
144
|
+
expressions_per_step.reverse()
|
|
145
|
+
|
|
146
|
+
operations = []
|
|
147
|
+
for expr in expressions_per_step:
|
|
148
|
+
if "-" in expr:
|
|
149
|
+
# Range of operation IDs, they must be integers
|
|
150
|
+
first, last = expr.split("-")
|
|
151
|
+
operations.append(
|
|
152
|
+
[str(id) for id in range(int(first), int(last) + 1)]
|
|
153
|
+
)
|
|
154
|
+
elif "," in expr:
|
|
155
|
+
# List of operation IDs
|
|
156
|
+
operations.append(expr.split(","))
|
|
157
|
+
else:
|
|
158
|
+
# Single operation ID
|
|
159
|
+
operations.append([expr])
|
|
160
|
+
|
|
161
|
+
# Cartesian product of operations
|
|
162
|
+
return list(itertools.product(*operations))
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import bpy
|
|
3
|
+
from .. import obj
|
|
4
|
+
from .. import coll
|
|
5
|
+
|
|
6
|
+
def create_data_object(transforms_dict, name = 'DataObject', world_scale = 0.01):
|
|
7
|
+
obj_data = bpy.data.objects.get(name)
|
|
8
|
+
if obj_data:
|
|
9
|
+
return obj_data
|
|
10
|
+
|
|
11
|
+
transforms_array = get_transforms_from_dict(transforms_dict)
|
|
12
|
+
chain_ids = np.unique(transforms_array['chain_id'], return_inverse = True)[1]
|
|
13
|
+
locations = transforms_array['translation'] * world_scale
|
|
14
|
+
|
|
15
|
+
obj_data = obj.create_object(name, coll.data(), locations)
|
|
16
|
+
obj.add_attribute(obj_data, 'assembly_rotation', transforms_array['rotation'], 'FLOAT_VECTOR', 'POINT')
|
|
17
|
+
obj.add_attribute(obj_data, 'assembly_id', transforms_array['assembly_id'], 'INT', 'POINT')
|
|
18
|
+
obj.add_attribute(obj_data, 'chain_id', chain_ids, 'INT', 'POINT')
|
|
19
|
+
|
|
20
|
+
return obj_data
|
|
21
|
+
|
|
22
|
+
# data types for the np.array that will store per-chain symmetry operations
|
|
23
|
+
dtype = [
|
|
24
|
+
('assembly_id', int),
|
|
25
|
+
('chain_id', 'U10'),
|
|
26
|
+
('rotation', float, 3),
|
|
27
|
+
('translation', float, 3)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def get_transforms_from_dict(transforms_dict):
|
|
31
|
+
|
|
32
|
+
results = None
|
|
33
|
+
for assembly_id, transforms in transforms_dict.items():
|
|
34
|
+
transforms = transforms_from_assemblies(transforms, index = int(assembly_id))
|
|
35
|
+
if results is None:
|
|
36
|
+
results = transforms
|
|
37
|
+
else:
|
|
38
|
+
results = np.concatenate((results, transforms), axis = 0)
|
|
39
|
+
|
|
40
|
+
# currently also using unique to remove duplicates, TODO: look into duplicates
|
|
41
|
+
results = np.unique(results)
|
|
42
|
+
|
|
43
|
+
return results
|
|
44
|
+
|
|
45
|
+
def transforms_from_assemblies(assembly_list, index = 0):
|
|
46
|
+
n_transforms = 0
|
|
47
|
+
for assembly in assembly_list:
|
|
48
|
+
n_transforms += len(assembly[0])
|
|
49
|
+
|
|
50
|
+
results = np.zeros((n_transforms), dtype = dtype)
|
|
51
|
+
|
|
52
|
+
current_transform = 0
|
|
53
|
+
for i, assembly in enumerate(assembly_list):
|
|
54
|
+
n_chains = len(assembly[0])
|
|
55
|
+
|
|
56
|
+
mask = np.array(range(n_chains))
|
|
57
|
+
mask += current_transform
|
|
58
|
+
current_transform += n_chains
|
|
59
|
+
transforms = transform_chains(assembly, index = index)
|
|
60
|
+
results[mask] = transforms
|
|
61
|
+
|
|
62
|
+
return results
|
|
63
|
+
|
|
64
|
+
def transform_chains(assembly, index = 0):
|
|
65
|
+
|
|
66
|
+
chains = assembly[0]
|
|
67
|
+
rotation_matrix = np.array(assembly[1]).reshape((3, 3))
|
|
68
|
+
rotation_euler = rotation_from_matrix(rotation_matrix)
|
|
69
|
+
translation_matrix = np.array(assembly[2])
|
|
70
|
+
|
|
71
|
+
n = len(chains)
|
|
72
|
+
result = np.zeros((n), dtype = dtype)
|
|
73
|
+
|
|
74
|
+
for i, chain in enumerate(chains):
|
|
75
|
+
result[i]['assembly_id'] = index
|
|
76
|
+
result[i]['chain_id'] = chain
|
|
77
|
+
result[i]['rotation'] = rotation_euler
|
|
78
|
+
result[i]['translation'] = translation_matrix
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def rotation_from_matrix(matrix):
|
|
83
|
+
from scipy.spatial.transform import Rotation as R
|
|
84
|
+
import warnings
|
|
85
|
+
|
|
86
|
+
# calculate the euler rotation from the rotation matrix
|
|
87
|
+
# Blender is 'xyz' euler rotations. Internally they use matrices / quaternions, but
|
|
88
|
+
# current interfaces for geometry nodes are just eulers
|
|
89
|
+
with warnings.catch_warnings():
|
|
90
|
+
warnings.simplefilter("ignore")
|
|
91
|
+
return R.from_matrix(matrix).as_euler('xyz')
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from . import AssemblyParser
|
|
3
|
+
|
|
4
|
+
class MMTFAssemblyParser(AssemblyParser):
|
|
5
|
+
### Implementation adapted from ``biotite.structure.io.mmtf.assembly``
|
|
6
|
+
|
|
7
|
+
def __init__(self, mmtf_file):
|
|
8
|
+
self._file = mmtf_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def list_assemblies(self):
|
|
12
|
+
import biotite.structure.io.mmtf as mmtf
|
|
13
|
+
return mmtf.list_assemblies(self._file)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_transformations(self, assembly_id):
|
|
17
|
+
import biotite
|
|
18
|
+
# Find desired assembly
|
|
19
|
+
selected_assembly = None
|
|
20
|
+
if not "bioAssemblyList" in self._file:
|
|
21
|
+
raise biotite.InvalidFileError(
|
|
22
|
+
"File does not contain assembly information "
|
|
23
|
+
"(missing 'bioAssemblyList')"
|
|
24
|
+
)
|
|
25
|
+
for assembly in self._file["bioAssemblyList"]:
|
|
26
|
+
current_assembly_id = assembly["name"]
|
|
27
|
+
transform_list = assembly["transformList"]
|
|
28
|
+
if current_assembly_id == assembly_id:
|
|
29
|
+
selected_assembly = transform_list
|
|
30
|
+
break
|
|
31
|
+
if selected_assembly is None:
|
|
32
|
+
raise KeyError(
|
|
33
|
+
f"The assembly ID '{assembly_id}' is not found"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Parse transformations from assembly
|
|
37
|
+
transformations = []
|
|
38
|
+
for transform in selected_assembly:
|
|
39
|
+
matrix = np.array(transform["matrix"]).reshape(4, 4).copy(order = 'C') # order needs to be 'c' otherwise Blender doesn't like it
|
|
40
|
+
chain_ids = np.array(self._file["chainNameList"], dtype="U4")
|
|
41
|
+
affected_chain_ids = chain_ids[transform["chainIndexList"]]
|
|
42
|
+
transformations.append((
|
|
43
|
+
affected_chain_ids.tolist(),
|
|
44
|
+
matrix[:3, :3].tolist(),
|
|
45
|
+
matrix[:3, 3].tolist()
|
|
46
|
+
))
|
|
47
|
+
|
|
48
|
+
return transformations
|
|
49
|
+
|
|
50
|
+
def get_assemblies(self):
|
|
51
|
+
assembly_dict = {}
|
|
52
|
+
for assembly_id in self.list_assemblies():
|
|
53
|
+
assembly_dict[assembly_id] = self.get_transformations(assembly_id)
|
|
54
|
+
|
|
55
|
+
return assembly_dict
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
# import biotite.structure.io.mmtf as mmtf
|
|
3
|
+
from . import AssemblyParser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PDBAssemblyParser(AssemblyParser):
|
|
7
|
+
### Implementation adapted from ``biotite.structure.io.pdb.file``
|
|
8
|
+
|
|
9
|
+
def __init__(self, pdb_file):
|
|
10
|
+
self._file = pdb_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_assemblies(self):
|
|
14
|
+
return self._file.list_assemblies()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_transformations(self, assembly_id):
|
|
18
|
+
import biotite
|
|
19
|
+
# Get lines containing transformations for assemblies
|
|
20
|
+
remark_lines = self._file.get_remark(350)
|
|
21
|
+
if remark_lines is None:
|
|
22
|
+
raise biotite.InvalidFileError(
|
|
23
|
+
"File does not contain assembly information (REMARK 350)"
|
|
24
|
+
)
|
|
25
|
+
# Get lines corresponding to selected assembly ID
|
|
26
|
+
assembly_start_i = None
|
|
27
|
+
assembly_stop_i = None
|
|
28
|
+
for i, line in enumerate(remark_lines):
|
|
29
|
+
if line.startswith("BIOMOLECULE"):
|
|
30
|
+
current_assembly_id = line[12:].strip()
|
|
31
|
+
if assembly_start_i is not None:
|
|
32
|
+
# Start was already found -> this is the next entry
|
|
33
|
+
# -> this is the stop
|
|
34
|
+
assembly_stop_i = i
|
|
35
|
+
break
|
|
36
|
+
if current_assembly_id == assembly_id:
|
|
37
|
+
assembly_start_i = i
|
|
38
|
+
# In case of the final assembly of the file,
|
|
39
|
+
# the 'stop' is the end of REMARK 350 lines
|
|
40
|
+
assembly_stop_i = len(remark_lines) if assembly_stop_i is None else i
|
|
41
|
+
if assembly_start_i is None:
|
|
42
|
+
raise KeyError(
|
|
43
|
+
f"The assembly ID '{assembly_id}' is not found"
|
|
44
|
+
)
|
|
45
|
+
assembly_lines = remark_lines[assembly_start_i : assembly_stop_i]
|
|
46
|
+
|
|
47
|
+
# Get transformations for a sets of chains
|
|
48
|
+
transformations = []
|
|
49
|
+
chain_set_start_indices = [
|
|
50
|
+
i for i, line in enumerate(assembly_lines)
|
|
51
|
+
if line.startswith("APPLY THE FOLLOWING TO CHAINS")
|
|
52
|
+
]
|
|
53
|
+
# Add exclusive stop at end of records
|
|
54
|
+
chain_set_start_indices.append(len(assembly_lines))
|
|
55
|
+
assembly = None
|
|
56
|
+
for i in range(len(chain_set_start_indices) - 1):
|
|
57
|
+
start = chain_set_start_indices[i]
|
|
58
|
+
stop = chain_set_start_indices[i+1]
|
|
59
|
+
# Read affected chain IDs from the following line(s)
|
|
60
|
+
affected_chain_ids = []
|
|
61
|
+
transform_start = None
|
|
62
|
+
for j, line in enumerate(assembly_lines[start : stop]):
|
|
63
|
+
if line.startswith("APPLY THE FOLLOWING TO CHAINS:") or \
|
|
64
|
+
line.startswith(" AND CHAINS:"):
|
|
65
|
+
affected_chain_ids += [
|
|
66
|
+
chain_id.strip()
|
|
67
|
+
for chain_id in line[30:].split(",")
|
|
68
|
+
]
|
|
69
|
+
else:
|
|
70
|
+
# Chain specification has finished
|
|
71
|
+
# BIOMT lines start directly after chain specification
|
|
72
|
+
transform_start = start + j
|
|
73
|
+
break
|
|
74
|
+
# Parse transformations from BIOMT lines
|
|
75
|
+
if transform_start is None:
|
|
76
|
+
raise biotite.InvalidFileError(
|
|
77
|
+
"No 'BIOMT' records found for chosen assembly"
|
|
78
|
+
)
|
|
79
|
+
rotations, translations = _parse_transformations(
|
|
80
|
+
assembly_lines[transform_start : stop]
|
|
81
|
+
)
|
|
82
|
+
for rotation, translation in zip(rotations, translations):
|
|
83
|
+
transformations.append((
|
|
84
|
+
np.array(affected_chain_ids, dtype="U4").tolist(),
|
|
85
|
+
rotation.tolist(),
|
|
86
|
+
translation.tolist()
|
|
87
|
+
))
|
|
88
|
+
|
|
89
|
+
return transformations
|
|
90
|
+
|
|
91
|
+
def get_assemblies(self):
|
|
92
|
+
assembly_dict = {}
|
|
93
|
+
for assembly_id in self.list_assemblies():
|
|
94
|
+
assembly_dict[assembly_id] = self.get_transformations(assembly_id)
|
|
95
|
+
|
|
96
|
+
return assembly_dict
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_transformations(lines):
|
|
100
|
+
"""
|
|
101
|
+
Parse the rotation and translation transformations from
|
|
102
|
+
*REMARK* 290 or 350.
|
|
103
|
+
Return as array of matrices and vectors respectively
|
|
104
|
+
"""
|
|
105
|
+
import biotite
|
|
106
|
+
# Each transformation requires 3 lines for the (x,y,z) components
|
|
107
|
+
if len(lines) % 3 != 0:
|
|
108
|
+
raise biotite.InvalidFileError("Invalid number of transformation vectors")
|
|
109
|
+
n_transformations = len(lines) // 3
|
|
110
|
+
|
|
111
|
+
rotations = np.zeros((n_transformations, 3, 3), dtype=float)
|
|
112
|
+
translations = np.zeros((n_transformations, 3), dtype=float)
|
|
113
|
+
|
|
114
|
+
transformation_i = 0
|
|
115
|
+
component_i = 0
|
|
116
|
+
for line in lines:
|
|
117
|
+
# The first two elements (component and
|
|
118
|
+
# transformation index) are not used
|
|
119
|
+
transformations = [float(e) for e in line.split()[2:]]
|
|
120
|
+
if len(transformations) != 4:
|
|
121
|
+
raise biotite.InvalidFileError(
|
|
122
|
+
"Invalid number of transformation vector elements"
|
|
123
|
+
)
|
|
124
|
+
rotations[transformation_i, component_i, :] = transformations[:3]
|
|
125
|
+
translations[transformation_i, component_i] = transformations[3]
|
|
126
|
+
|
|
127
|
+
component_i += 1
|
|
128
|
+
if component_i == 3:
|
|
129
|
+
# All (x,y,z) components were parsed
|
|
130
|
+
# -> head to the next transformation
|
|
131
|
+
transformation_i += 1
|
|
132
|
+
component_i = 0
|
|
133
|
+
|
|
134
|
+
return rotations, translations
|
|
Binary file
|
|
Binary file
|