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.
Files changed (30) hide show
  1. molecularnodes-2.11.0/LICENSE +21 -0
  2. molecularnodes-2.11.0/PKG-INFO +15 -0
  3. molecularnodes-2.11.0/README.md +58 -0
  4. molecularnodes-2.11.0/molecularnodes/__init__.py +48 -0
  5. molecularnodes-2.11.0/molecularnodes/assembly/__init__.py +50 -0
  6. molecularnodes-2.11.0/molecularnodes/assembly/cif.py +162 -0
  7. molecularnodes-2.11.0/molecularnodes/assembly/mesh.py +91 -0
  8. molecularnodes-2.11.0/molecularnodes/assembly/mmtf.py +55 -0
  9. molecularnodes-2.11.0/molecularnodes/assembly/pdb.py +134 -0
  10. molecularnodes-2.11.0/molecularnodes/assets/MN_data_file.blend +0 -0
  11. molecularnodes-2.11.0/molecularnodes/assets/template/MolecularNodes/startup.blend +0 -0
  12. molecularnodes-2.11.0/molecularnodes/assets/template/MolecularNodes.zip +0 -0
  13. molecularnodes-2.11.0/molecularnodes/auto_load.py +157 -0
  14. molecularnodes-2.11.0/molecularnodes/coll.py +69 -0
  15. molecularnodes-2.11.0/molecularnodes/color.py +9 -0
  16. molecularnodes-2.11.0/molecularnodes/data.py +515 -0
  17. molecularnodes-2.11.0/molecularnodes/density.py +265 -0
  18. molecularnodes-2.11.0/molecularnodes/esmfold.py +142 -0
  19. molecularnodes-2.11.0/molecularnodes/load.py +736 -0
  20. molecularnodes-2.11.0/molecularnodes/md.py +284 -0
  21. molecularnodes-2.11.0/molecularnodes/mda.py +921 -0
  22. molecularnodes-2.11.0/molecularnodes/nodes.py +925 -0
  23. molecularnodes-2.11.0/molecularnodes/obj.py +141 -0
  24. molecularnodes-2.11.0/molecularnodes/pkg.py +406 -0
  25. molecularnodes-2.11.0/molecularnodes/pref.py +68 -0
  26. molecularnodes-2.11.0/molecularnodes/requirements.txt +5 -0
  27. molecularnodes-2.11.0/molecularnodes/star.py +153 -0
  28. molecularnodes-2.11.0/molecularnodes/ui.py +764 -0
  29. molecularnodes-2.11.0/molecularnodes/utils.py +112 -0
  30. 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
+ ![Documentation Building](https://github.com/bradyajohnston/molecularnodes/actions/workflows/docs.yml/badge.svg) ![Running Tests](https://github.com/bradyajohnston/molecularnodes/actions/workflows/tests.yml/badge.svg) [![codecov](https://codecov.io/gh/BradyAJohnston/MolecularNodes/branch/main/graph/badge.svg?token=ZB2SJFY8FU)](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
+ [![image](https://user-images.githubusercontent.com/36021261/205629018-a6722f88-505e-4cb6-a641-8d423aa26963.png)](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/485261976.svg)](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&amp;emoji=&amp;slug=bradyajohnston&amp;button_colour=eabae1&amp;font_colour=000000&amp;font_family=Poppins&amp;outline_colour=000000&amp;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