diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c5aee906..90b08155 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1 +1,281 @@ -Details on how to contribute to the project. \ No newline at end of file +# Contributing to Molecular Nodes + +## Table of Contents +- [Contributing to Molecular Nodes](#contributing-to-molecular-nodes) + - [Table of Contents](#table-of-contents) + - [Introduction](#introduction) + - [Molecular Nodes Overview](#molecular-nodes-overview) + - [Getting Started](#getting-started) + - [Understanding Blender Add-ons](#understanding-blender-add-ons) + - [`bpy`](#bpy) + - [Creating a Basic Operator:](#creating-a-basic-operator) + - [Project Structure](#project-structure) + - [Import](#import) + - [Manipulation](#manipulation) + - [Coding Standards](#coding-standards) + - [Submitting Changes](#submitting-changes) + - [Testing](#testing) + - [Python Environments](#python-environments) + - [Running Tests](#running-tests) + - [Writing and Building Docs](#writing-and-building-docs) + - [Getting Help](#getting-help) + +## Introduction +Molecular Nodes is an add-on for the 3D modelling and animation program [Blender](https://blender.org). It enables import of structural biology data formats into Blender, and provides a suite of methods for interacting, animating and visualising this data. + +The structure of Molecular Nodes is likely quite different to other python projects you may be familiar with, and different to other Blender add-ons as well. Some things are done in a particularly _quirky_ way, usually to be usable as an add-on inside of Blender. + +Molecular Nodes is primarily an add-on, and intended to be interacted with through Blender's GUI. There is experimental support for installing and using as a python package from `pypi`. This is still extremely experimental, and again results in a lot of strange quirks as we are using a program intended for use through a GUI, through a script. + + +### Molecular Nodes Overview +There are a couple of distinct areas of Molecular Nodes to be aware of. + +1. Reading, parsing and importing data formats +2. Visualising data through Geometry Nodes + +Most of the 'scripting' side of things is for the first section of parsing the wide variety of structural biology data formats and importing them into Blender. The general idea is that we turn molecular structures into 3D models by turning each atom into a vertex, and each bond into an edge between vertices. Once this data is imported, Blender has a suite of tools for dealing with '3D models', which we can exploit to work on molecular models as well. + +## Getting Started +Unfortunately `.blend` files are binary files to git, so the full repo size can be quite large when downloading the full history (~1GB). It's recommended to clone this repository using `git clone --depth 1` which greatly reduces the size of the download. + +For writing code, I highly recommend using VSCode and the [Blender VS Code](https://github.com/JacquesLucke/blender_vscode) addon which streamlines the development process. It provides a range of commands for building and quickly refreshing the add-on during development, greatly speeding up the process. + +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 Addons`. + +## Development Environment + +To install the required packages for development, I recommend using Poetry, which can install all of the required development packages. + +```py +pip install poetry +poetry install . --with dev +``` + + +## Understanding Blender Add-ons +The general idea with add-ons is that they provide new functionality to Blender, usually by adding new panels with buttons that execute custom python code. Blender ships with it's own Python kernel inside, allowing for essentially any arbitrary Python code to be executed. + +Usually this is done through the creation of [`operators`](https://docs.blender.org/manual/en/latest/interface/operators.html). Think of operators as just Python code that is executed when a button is pressed inside of the GUI. All of the the buttons inside of Blender execute an operator when pressed, which then carries out the desired actions. The operators have support for taking into account the current state of the GUI, where the mouse is, what objects are available etc when executing the code. + +We _can_ execute code without calling an operator, but this has to be done via the Python REPL inside of Blender. To create a useful add-on, we define the code we want to be executed, then create a related operator that can call the code when required. + +Because operators take into account `context` and other aspects of the GUI when executing, they can be difficult to work with at times when trying to script without the GUI, like when trying to use Blender as a package inside of a Jupyter Notebook. To help with this problem, the general design of Molecular Nodes is to create a function which includes all of the code we want, then the associated operator only calls this function with the relevant parameters and does nothing else. That way we can get the same results as the operator while scripting, without having to deal with operators. + +In the example add-on below, we can see the operator class being defined as a subclass of the `bpy.types.Operator` class. It will have a method called `execute(self, context)` which is what is called when a button is pressed in the add-on. We will have access to `context` information (where the mouse cursor is, what viewport we are in etc). You can include as much code as you wish inside of the `execute()` function, but like previously described the design with Molecular Nodes is to define the function elsewhere so it can be called more easily in another script, then have the operator just call the the function and report the success. + +### `bpy` + +In Blender add-on development, `import bpy` is your gateway to the Blender Python API. Anything that you can do via Blender's UI, you can usually achieve via calls to `bpy`. + +```python +import bpy + +bpy.data # Access all of the data blocks inside of Blender +bpy.data.objects # access all of the objects in the scene by name + +cube = bpy.data.objects['Cube'] # get the data block for an object in Blender +cube.data # the data associated with the cube, such as edges, vertices, faces +cube.data.attributes +cube.data.vertices + +bpy.ops # all of the pre-defined operators inside of Blender + +bpy.context # all of the global context values, i.e. different properties set in the UI +bpy.types # the different pre-defined types used through bpy +``` + +`bpy` exposes a wide range of classes and functions, enabling you to perform tasks like creating objects, applying materials, setting animations, and much more, all programmatically. + +For example, `bpy.data` grants access to the data blocks within Blender, such as meshes, materials, and textures, while `bpy.ops` allows you to call operators to perform specific actions, like rendering an image or duplicating an object. + +Until earlier this year, `bpy` was only available when running scripts from inside of Blender, but it is now a `pip` installable package, which helps us with running test suites and for potential integrations with Jupyter Notebooks and other scripting environments. + +### Creating a Basic Operator: + +In Blender, operators are actions that can be triggered by the user or other parts of the code. They can range from simple tasks like moving an object to complex operations like rendering an animation. + +Operators can execute code of any arbitrary length. They can provide additional _context_ in the form of the `context` argument, which is given by Blender depending on where the operator is invoked. If you press a button in one window of Blender, it might do something different compared to a different window of Blender. Most of the operators inside of Molecular Nodes do not change their behaviour. + +The design of Molecular Nodes is mostly to expose all of the functionality inside individual function calls. To download a protein from the PDB, import it to Blender and create starting style, you can use the `mn.load.molecular_rcsb()` function. Inside of the UI for Blender, when the user clicks the Download from PDB button, the operator just calls this function with the inputs taken from the local context, such as starting style and PDB code to download. The operators themselves should not be doing any kind of complex operations, as that functionality won't then be available for use via scripts. + +Below is the minimum required to create an add-on for Blender. We define a custom function, create an operator that executes code (calling the function), we create some UI that displays a button to execute the operator, and we create `register()` and `unregister()` functions to install and uninstall the add-on. + + +```py +import bpy + +def my_function(): + print("hello world!") + +class SimpleOperator(bpy.types.Operator): + bl_idname = "wm.simple_operator" + bl_label = "Simple Operator" + + def execute(self, context): + #code to be executed by the operator goes in the `execute()` function + my_function() + + # operators inside of Blender return `{'FINISHED'}` to signal they have completed + # correctly and Blender can return control of the program back to the user. + # This is why they are useful for UI operations, but less useful for scripting + # other potential returns are 'CANCELLED', 'RUNNING_MODAL', 'PASS_THROUGH' + return {'FINISHED'} + +# define a menu that will appear inside of the Blender's UI +# the layout function `layout.operator()` will take a string name of the operator, +# and create a button in the UI which will execute the operator when the buttons is pressed +def menu_func(self, context): + # you can input either the string for the operator name, or take that + # name from the class itself + self.layout.operator(SimpleOperator.bl_idname) + self.layout.operator("wm.simple_operator") + + +# The `register()` and `unregister()` functions are run whenever Blender loads the +# addon. This occurs the first time the add-on is installed and enabled, and then whenever +# Blender is started while the add-on is enabled. For Blender to be aware of the operator's +# existence, it has to be registered (and unregistered when uninstalled). The same has to +# happen for the UI components +def register(): + bpy.utils.register_class(SimpleOperator) + bpy.types.VIEW3D_MT_mesh.append(menu_func) + +def unregister(): + bpy.utils.unregister_class(SimpleOperator) + bpy.types.VIEW3D_MT_mesh.remove(menu_func) +``` + +The `register()` and `unregister()` functions are two crucial components of a Blender add-on and have to be included. These functions are called when the add-on is enabled or disabled. They register all of the operators, UI elements and other necessary components with Blender when the add-on is enabled, and remove them all when it's disabled to ensure that you don't have a panel showing up for an add-on that isn't being used. + +These functions are called automatically when using Blender via the GUI, but have to be manually called when scripting outside of Blender. + +```py +import molecularnodes as mn +mn.register() +# other code here +``` + + +## Project Structure + +The way that data flows and is handled is unconventional, and likely different +to other python packages that you might have experience with. + +There are two main components to the add-on, split into `Import` and +`Manipulation`. Depending on data format, the `import` is handled by a different python package. For downloading from the wwPDB and importing most local `.pdb` and `.cif` files, `biotite` is used. When importing a molecular dynamics trajectory. + +All import methods result in a Blender object, and then the `Geometry Nodes` system inside of Blender manipulates and styles the imported 3D model. + + +### Import + +Importing is the more traditional aspect of the add-on. With the help of several +python packages such as `biotite`, `MDAnalysis` and others, various molecular +data formats are parsed. + +Once parsed, the data is turned into a 3D mesh, with a vertex for each atom and +an edge for each bond (if information available). Now inside Blender as a +'native' 3D mesh, Geometry Nodes handles all further manipulation of the data, +with additional animations, duplication, selections, and creation of new +geometry in the form of styles. + +Below shows the potential flow of data, showing whether MDAnalysis (MDA), +Blender's python module (bpy) or Geometry Nodes (GN) are responsible for +handling that data. Once the data is parsed into a universe, MDA can select, +filter and do other operations on the topology and the trajectory of the +universe. While MDA can update the object inside of Blender by + +```mermaid +flowchart LR +A{Data File} -->|MDA|B +B(MDA.Universe) -->|bpy|C +B -->|MDA|B +C[Blender Object] -->|GN|D +C -->|GN|C +D -->|GN|C +D[Styled Molecule] --->|GN|D +``` + +### Manipulation + +Manipulation is handled entirely by the Geometry Nodes (GN) system that exists +inside of Blender. Inside of Geometry Nodes, users can create node trees to +modify, animate and style their macromolecular structures, through a range of +pre-made node groups which are included inside of the add-on. + +The nodes take the underlying atomic data, which is stored as a 3D mesh with +each vertex representing an atom, and each edge between those vertices +representing a bond (where applicable). Both the vertices and edges can store +arbitrary attributes, which we use to store the atomic information with the +atoms and bonds. Currently only numeric, boolean and vector attributes are +supported, but in the future strings and more complex attributes will also be +supported. + +Interacting with the nodes via scripting is still quite difficult, the API on +this on Blender's side still needs a lot of improvements. So far the best +approach has been to 'manually' make the node groups inside of Blender, and then +save them and append the pre-made node groups from other `.blend` files to the +current working file. This isn't a fantastic strategy as the `.blend` files are +opaque to `git`, so we just need to rely upon tests for checking if something is +broken. + +## Coding Standards +This project has already gone through several iterations to improve the general code base and the ability for others to contribute. It started as my (@bradyajohnston) first python project, so there is still lots of old code that could do with a refresh. It is slowly being improve to better fit PEP8 standards, but there are no official standards for the project currently. I welcome suggestions and discussion around the topic. + +## Submitting Changes +Please open an issue or PR if you would like to discuss submitting changes. Support for importing more data formats or improving on current import formats are more than welcome. Submitting changes for node groups can be a bit tricky as the node graphs inside of Blender don't work with `git`, so please open an issue or discussion with propose changes. + + +## Testing + +### Python Environments +Blender is _VERY PARTICULAR_ about python versions. Blender 4.1 now uses Python `3.11.X`. Blender 4.0 and some earlier versions use Python `3.10.X`. I recommend using `anaconda` or something similar to manage python environments. They aren't required for building and running the add-on (this is handled by the Python that is shipped inside of Blender), but they are required for running tests. + +```bash +conda create -n mn python==3.11 +conda activate mn +pip install poetry +poetry install --with dev +``` + +### Running Tests +```bash +pytest -v # run with a more verbose output +pytest -v tests/test_load.py # run a single tests file +pytest -v -k centre # pattern match to 'centre' and only run tests with that in the name +``` + +### Tetsing _in_ Blender +We can run the testing suite also inside of the Blender itself. You can call the Blender application and pass in some command line arguments which which include python scripts. We can use those scripts to install testing packages, and run the python testing suite _inside_ of Blender rather than just inside a regular python environment. + +When running the `tests/run.py` everything after the `--` will be passed in as extra commands to the `python -m pytest`. + +```bash +/Applications/Blender.app/Contents/MacOS/Blender -b -P tests/install.py +/Applications/Blender.app/Contents/MacOS/Blender -b -P tests/run.py -- -vv +``` + +Look over other tests to see how we are structuring them. Most of the tests will involve importing data, generating a 3D model and then creating a `snapshot` of the attributes for some subset of vertices from that 3D model. +We could snapshot _all_ of the vertices, but the snapshots are just `.txt` files so the diffs would become very large. +When changing something that _should_ change the output of the snapshots, we end up with _very_ large number of files changed. +Based on the current snapshot method, I can't think of of a better way to handle this. + +## Writing and Building Docs +To build the documentation, [`Quarto`](https://quarto.org) is used. The docs can be built and previewed with the following line which launches a live preview of the docs. + + +```bash +conda activate mn +python docs/build_node_docs.py +quarto preview +``` + +The long-form written documentation is all inside of `docs/`. Documentation is written in markdown (`.md`) or quarto-flavored markdown (`.qmd`) which allows to execute code when building the docs. + +The documentation for individual nodes which are shown [here](https://bradyajohnston.github.io/MolecularNodes/nodes/) are built by running the `docs/build_node_docs.py`, which extracts information from the relevent `.blend` data files inside of `molecularnodes/assets/`. Combining the information for the input / output types & tooltips with the summaries described in `molecularnodes/ui/node_info.py` we can then generate nice HTML documentation for each of the nodes. + +This isn't currently the best implementation for this. I would prefer to just pull from those nodes which are defined in the `.blend` file, but we aren't able to include descriptions for the node groups currently inside of the `.blend`. `node_info.py` is also used for building the add menus as well as the documentation. To update the descriptions of inputs, outputs and data types the nodes themselves need to be updated inside of the `.blend` files. Relevant example videos should be updated when nodes are changed. + +## Getting Help +Please open an issue on the repo to ask questions relating to development or testing. diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 00000000..dd774122 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,32 @@ +name: mypy +on: + push: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: chartboost/ruff-action@v1 + + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.11.7 + cache: pip + + - name: Build docs using blender + run: | + wget -nv https://download.blender.org/release/Blender4.1/blender-4.1.0-linux-x64.tar.xz + tar -xf blender-4.1.0-linux-x64.tar.xz + + blender-4.1.0-linux-x64/blender --version + blender-4.1.0-linux-x64/blender -b --python tests/python.py -- -m pip install poetry + blender-4.1.0-linux-x64/blender -b --python tests/python.py -- -m poetry install --with dev + blender-4.1.0-linux-x64/blender -b --python tests/python.py -- -m mypy . + + diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 00000000..ef06d34a --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,8 @@ +name: Ruff +on: [ push, pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c2b5ba8e..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,261 +0,0 @@ -# Contributing to Molecular Nodes - -## Table of Contents -- [Contributing to Molecular Nodes](#contributing-to-molecular-nodes) - - [Table of Contents](#table-of-contents) - - [Introduction](#introduction) - - [Molecular Nodes Overview](#molecular-nodes-overview) - - [Getting Started](#getting-started) - - [Understanding Blender Add-ons](#understanding-blender-add-ons) - - [`bpy`](#bpy) - - [Creating a Basic Operator:](#creating-a-basic-operator) - - [Project Structure](#project-structure) - - [Import](#import) - - [Manipulation](#manipulation) - - [Coding Standards](#coding-standards) - - [Submitting Changes](#submitting-changes) - - [Testing](#testing) - - [Python Environments](#python-environments) - - [Running Tests](#running-tests) - - [Writing and Building Docs](#writing-and-building-docs) - - [Getting Help](#getting-help) - -## Introduction -Molecular Nodes is an add-on for the 3D modelling and animation program [Blender](https://blender.org). It enables import of structural biology data formats into Blender, and provides a suite of methods for interacting, animating and visualising this data. - -The structure of Molecular Nodes is likely quite different to other python projects you may be familiar with, and different to other Blender add-ons as well. Some things are done in a particularly _quirky_ qay, usually to be usable as an add-on inside of Blender. - -Molecular Nodes is primarily an add-on, and intended to be interacted with through Blender's GUI. There is experimental support for installing and using as a python package from `pypi`. This is still extremely experimental, and again results in a lot of strange quirks as we are using a program intended for use through a GUI, through a script. - -### Molecular Nodes Overview -There are a couple of distinct areas of Molecular Nodes to be aware of. - -1. Reading, parsing and importing data formats -2. Visualising data through Geometry Nodes - -Most of the 'scripting' side of things is for the first section of parsing the wide variety of structural biology data formats and importing them into Blender. The general idea is that we turn molecular structures into 3D models by turning each atom into a vertex, and each bond into an edge between vertices. Once this data is imported, Blender has a suite of tools for dealing with '3D models', which we can exploit to work on molecular models as well. - -## Getting Started -Unfortunately `.blend` files are binary files to git, so the full repo size can be quite large when downloading the full history (~1GB). It's recommended to clone this repository using `git clone --depth 1` which greatly reduces the size of the download. - -For writing code, I highly recommend using VSCode and the [Blender VS Code](https://github.com/JacquesLucke/blender_vscode) addon which streamlines the development process. It provides a range of commands for building and quickly refreshing the add-on during development, greatly speeding up the process. - -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 Addons`. - - -## Understanding Blender Add-ons -The general idea with add-ons is that they provide new functionality to Blender, usually by adding new panels with buttons that execute custom python code. Blender ships with it's own Python kernel inside, allowing for essentially any arbitrary Python code to be executed. - -Usually this is done through the creation of [`operators`](https://docs.blender.org/manual/en/latest/interface/operators.html). Think of operators as just Python code that is executed when a button is pressed inside of the GUI. All of the the buttons inside of Blender execute an operator when pressed, which then carries out the desired actions. The operators have support for taking into account the current state of the GUI, where the mouse is, what objects are available etc when executing the code. - -We _can_ execute code without calling an operator, but this has to be done via the Python REPL inside of Blender. To create a useful add-on, we define the code we want to be executed, then create a related operator that can call the code when required. - -Because operators take into account `context` and other aspects of the GUI when executing, they can be difficult to work with at times when trying to script without the GUI, like when trying to use Blender as a package inside of a Jupyter Notebook. To help with this problem, the general design of Molecular Nodes is to create a function which includes all of the code we want, then the associated operator only calls this function with the relevant parameters and does nothing else. That way we can get the same results as the operator while scripting, without having to deal with operators. - -In the example add-on below, we can see the operator class being defined as a subclass of the `bpy.types.Operator` class. It will have a method called `execute(self, context)` which is what is called when a button is pressed in the add-on. We will have access to `context` information (where the mouse cursor is, what viewport we are in etc). You can include as much code as you wish inside of the `execute()` function, but like previously described the design with Molecular Nodes is to define the function elsewhere so it can be called more easily in another script, then have the operator just call the the function and report the success. - -### `bpy` - -In Blender add-on development, `import bpy` is your gateway to the Blender Python API. Anything that you can do via Blender's UI, you can usually achieve via calls to `bpy`. - -```python -import bpy - -bpy.data # Access all of the data blocks inside of Blender -bpy.data.objects # access all of the objects in the scene by name - -cube = bpy.data.objects['Cube'] # get the data block for an object in Blender -cube.data # the data associated with the cube, such as edges, vertices, faces -cube.data.attributes -cube.data.vertices - -bpy.ops # all of the pre-defined operators inside of Blender - -bpy.context # all of the global context values, i.e. different properties set in the UI -bpy.types # the different pre-defined types used through bpy -``` - -`bpy` exposes a wide range of classes and functions, enabling you to perform tasks like creating objects, applying materials, setting animations, and much more, all programmatically. - -For example, `bpy.data` grants access to the data blocks within Blender, such as meshes, materials, and textures, while `bpy.ops` allows you to call operators to perform specific actions, like rendering an image or duplicating an object. - -Until earlier this year, `bpy` was only available when running scripts from inside of Blender, but it is now a `pip` installable package, which helps us with running test suites and for potential integrations with Jupyter Notebooks and other scripting environments. - -### Creating a Basic Operator: - -In Blender, operators are actions that can be triggered by the user or other parts of the code. They can range from simple tasks like moving an object to complex operations like rendering an animation. - -Operators can execute code of any arbitrary length. They can provide additional _context_ in the form of the `context` argument, which is given by Blender depending on where the operator is invoked. If you press a button in one window of Blender, it might do something different compared to a different window of Blender. Most of the operators inside of Molecular Nodes do not change their behaviour. - -The design of Molecular Nodes is mostly to expose all of the functionality inside individual function calls. To download a protein from the PDB, import it to Blender and create starting style, you can use the `mn.load.molecular_rcsb()` function. Inside of the UI for Blender, when the user clicks the Download from PDB button, the operator just calls this function with the inputs taken from the local context, such as starting style and PDB code to download. The operators themselves should not be doing any kind of complex operations, as that functionality won't then be available for use via scripts. - -Below is the minimum required to create an add-on for Blender. We define a custom function, create an operator that executes code (calling the function), we create some UI that displays a button to execute the operator, and we create `register()` and `unregister()` functions to install and uninstall the add-on. - - -```py -import bpy - -def my_function(): - print("hello world!") - -class SimpleOperator(bpy.types.Operator): - bl_idname = "wm.simple_operator" - bl_label = "Simple Operator" - - def execute(self, context): - #code to be executed by the operator goes in the `execute()` function - my_function() - - # operators inside of Blender return `{'FINISHED'}` to signal they have completed - # correctly and Blender can return control of the program back to the user. - # This is why they are useful for UI operations, but less useful for scripting - # other potential returns are 'CANCELLED', 'RUNNING_MODAL', 'PASS_THROUGH' - return {'FINISHED'} - -# define a menu that will appear inside of the Blender's UI -# the layout function `layout.operator()` will take a string name of the operator, -# and create a button in the UI which will execute the operator when the buttons is pressed -def menu_func(self, context): - # you can input either the string for the operator name, or take that - # name from the class itself - self.layout.operator(SimpleOperator.bl_idname) - self.layout.operator("wm.simple_operator") - - -# The `register()` and `unregister()` functions are run whenever Blender loads the -# addon. This occurs the first time the add-on is installed and enabled, and then whenever -# Blender is started while the add-on is enabled. For Blender to be aware of the operator's -# existence, it has to be registered (and unregistered when uninstalled). The same has to -# happen for the UI components -def register(): - bpy.utils.register_class(SimpleOperator) - bpy.types.VIEW3D_MT_mesh.append(menu_func) - -def unregister(): - bpy.utils.unregister_class(SimpleOperator) - bpy.types.VIEW3D_MT_mesh.remove(menu_func) -``` - -The `register()` and `unregister()` functions are two crucial components of a Blender add-on and have to be included. These functions are called when the add-on is enabled or disabled. They register all of the operators, UI elements and other necessary components with Blender when the add-on is enabled, and remove them all when it's disabled to ensure that you don't have a panel showing up for an add-on that isn't being used. - -These functions are called automatically when using Blender via the GUI, but have to be manually called when scripting outside of Blender. - -```py -import molecularnodes as mn -mn.register() -# other code here -``` - - -## Project Structure - -The way that data flows and is handled is unconventional, and likely different -to other python packages that you might have experience with. - -There are two main components to the add-on, split into `Import` and -`Manipulation`. Depending on data format, the `import` is handled by a different python package. For downloading from the wwPDB and importing most local `.pdb` and `.cif` files, `biotite` is used. When importing a molecular dynamics trajectory. - -All import methods result in a Blender object, and then the `Geometry Nodes` system inside of Blender manipulates and styles the imported 3D model. - - -### Import - -Importing is the more traditional aspect of the add-on. With the help of several -python packages such as `biotite`, `MDAnalysis` and others, various molecular -data formats are parsed. - -Once parsed, the data is turned into a 3D mesh, with a vertex for each atom and -an edge for each bond (if information available). Now inside Blender as a -'native' 3D mesh, Geometry Nodes handles all further manipulation of the data, -with additional animations, duplication, selections, and creation of new -geometry in the form of styles. - -Below shows the potential flow of data, showing whether MDAnalysis (MDA), -Blender's python module (bpy) or Geometry Nodes (GN) are responsible for -handling that data. Once the data is parsed into a universe, MDA can select, -filter and do other operations on the topology and the trajectory of the -universe. While MDA can update the object inside of Blender by - -```mermaid -flowchart LR -A{Data File} -->|MDA|B -B(MDA.Universe) -->|bpy|C -B -->|MDA|B -C[Blender Object] -->|GN|D -C -->|GN|C -D -->|GN|C -D[Styled Molecule] --->|GN|D -``` - -### Manipulation - -Manipulation is handled entirely by the Geometry Nodes (GN) system that exists -inside of Blender. Inside of Geometry Nodes, users can create node trees to -modify, animate and style their macromolecular structures, through a range of -pre-made node groups which are included inside of the add-on. - -The nodes take the underlying atomic data, which is stored as a 3D mesh with -each vertex representing an atom, and each edge between those vertices -representing a bond (where applicable). Both the vertices and edges can store -arbitrary attributes, which we use to store the atomic information with the -atoms and bonds. Currently only numeric, boolean and vector attributes are -supported, but in the future strings and more complex attributes will also be -supported. - -Interacting with the nodes via scripting is still quite difficult, the API on -this on Blender's side still needs a lot of improvements. So far the best -approach has been to 'manually' make the node groups inside of Blender, and then -save them and append the pre-made node groups from other `.blend` files to the -current working file. This isn't a fantastic strategy as the `.blend` files are -opaque to `git`, so we just need to rely upon tests for checking if something is -broken. - -## Coding Standards -This project has already gone through several iterations to improve the general code base and the ability for others to contribute. It started as my (@bradyajohnston) first python project, so there is still lots of old code that could do with a refresh. It is slowly being improve to better fit PEP8 standards, but there are no official standards for the project currently. I welcome suggestions and discussion around the topic. - -## Submitting Changes -Please open an issue or PR if you would like to discuss submitting changes. Support for importing more data formats or improving on current import formats are more than welcome. Submitting changes for node groups can be a bit tricky as the node graphs inside of Blender don't work with `git`, so please open an issue or discussion with propose changes. - - -## Testing - -### Python Environments -Blender is _VERY PARTICULAR_ about python versions. Blender 4.1 now uses Python `3.11.X`. Blender 4.0 and some earlier versions use Python `3.10.X`. I recommend using `anaconda` or something similar to manage python environments. They aren't required for building and running the add-on (this is handled by the Python that is shipped inside of Blender), but they are required for running tests. - -```bash -conda create -n mn python==3.10 -conda activate mn -pip install poetry -poetry install --with dev -``` - -### Running Tests -```bash -pytest -v # run with a more verbose output -pytest -v tests/test_load.py # run a single tests file -pytest -v -k centre # pattern match to 'centre' and only run tests with that in the name -``` - -Look over other tests to see how we are structuring them. Most of the tests will involve importing data, generating a 3D model and then creating a `snapshot` of the attributes for some subset of vertices from that 3D model. -We could snapshot _all_ of the vertices, but the snapshots are just `.txt` files so the diffs would become very large. -When changing something that _should_ change the output of the snapshots, we end up with _very_ large number of files changed. -Based on the current snapshot method, I can't think of of a better way to handle this. - -## Writing and Building Docs -To build the documentation, [`Quarto`](https://quarto.org) is used. The docs can be built and previewed with the following line which launches a live preview of the docs. - - -```bash -conda activate mn -python docs/build_node_docs.py -quarto preview -``` - -The long-form written documentation is all inside of `docs/`. Documentation is written in markdown (`.md`) or quarto-flavored markdown (`.qmd`) which allows to execute code when building the docs. - -The documentation for individual nodes which are shown [here](https://bradyajohnston.github.io/MolecularNodes/nodes/) are built by running the `docs/build_node_docs.py`, which extracts information from the relevent `.blend` data files inside of `molecularnodes/assets/`. Combining the information for the input / output types & tooltips with the summaries described in `molecularnodes/ui/node_info.py` we can then generate nice HTML documentation for each of the nodes. - -This isn't currently the best implementation for this. I would prefer to just pull from those nodes which are defined in the `.blend` file, but we aren't able to include descriptions for the node groups currently inside of the `.blend`. `node_info.py` is also used for building the add menus as well as the documentation. To update the descriptions of inputs, outputs and data types the nodes themselves need to be updated inside of the `.blend` files. Relevant example videos should be updated when nodes are changed. - -## Getting Help -Please open an issue on the repo to ask questions relating to development or testing. diff --git a/build.py b/build.py index ae123f55..f7e37867 100644 --- a/build.py +++ b/build.py @@ -3,21 +3,23 @@ # zips up the template file -def zip_template(): +def zip_template() -> None: # Define the directory and zip file paths - dir_path = 'molecularnodes/assets/template/Molecular Nodes' - zip_file_path = 'molecularnodes/assets/template/Molecular Nodes.zip' + dir_path = "molecularnodes/assets/template/Molecular Nodes" + zip_file_path = "molecularnodes/assets/template/Molecular Nodes.zip" # Create a ZipFile object in write mode - with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED) as zipf: # Walk the directory tree and add files to the zip file for root, dirs, files in os.walk(dir_path): for file in files: # Get the path of the file file_path = os.path.join(root, file) # Add the file to the zip file - zipf.write(file_path, arcname=os.path.relpath( - file_path, start='molecularnodes/assets/template/')) + zipf.write( + file_path, + arcname=os.path.relpath(file_path, start="molecularnodes/assets/template/"), + ) if __name__ == "__main__": diff --git a/docs/build_node_docs.py b/docs/build_node_docs.py index cbb10f35..d7adf22a 100644 --- a/docs/build_node_docs.py +++ b/docs/build_node_docs.py @@ -1,12 +1,14 @@ +import os +import pathlib +import sys + import bpy +import griffe from quartodoc import MdRenderer + import molecularnodes as mn -import griffe -import os -import sys -import pathlib -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) folder = pathlib.Path(__file__).resolve().parent file_output_qmd = os.path.join(folder, "nodes/index.qmd") @@ -28,12 +30,12 @@ def get_values(sockets): default = None if dtype == "Float": default = round(socket.default_value, 2) - elif dtype in ['Geometry', 'Collection', 'Object']: + elif dtype in ["Geometry", "Collection", "Object"]: default = None elif dtype == "Vector": default = [round(x, 2) for x in socket.default_value] elif dtype == "Material": - default = '`MN Default`' + default = "`MN Default`" elif dtype == "Color": default = col_to_rgb_str(socket.default_value) else: @@ -44,21 +46,20 @@ def get_values(sockets): name=socket.name, annotation=dtype, value=default, - description=socket.description + description=socket.description, ) ) return param_list -cat = '' +cat = "" text = griffe.docstrings.dataclasses.DocstringSectionText params = griffe.docstrings.dataclasses.DocstringSectionParameters categories = {} for category, node_list in mn.ui.node_info.menu_items.items(): objects = [] - objects.append( - [text(title=None, value=f"## {mn.blender.nodes.format_node_name(category)}")]) + objects.append([text(title=None, value=f"## {mn.blender.nodes.format_node_name(category)}")]) for item in node_list: if isinstance(item, str): @@ -66,40 +67,34 @@ def get_values(sockets): iter_list = [item] - if item['label'] == "custom": - iter_list = item['values'] + if item["label"] == "custom": + iter_list = item["values"] for entry in iter_list: - name = entry['name'] + name = entry["name"] if name.startswith("mn."): - name = entry['backup'] + name = entry["backup"] entry_list = [] - desc = entry.get('description') - urls = entry.get('video_url') + desc = entry.get("description") + urls = entry.get("video_url") - inputs = params(get_values( - mn.blender.nodes.inputs(bpy.data.node_groups[name]))) - outputs = params(get_values( - mn.blender.nodes.outputs(bpy.data.node_groups[name]))) + inputs = params(get_values(mn.blender.nodes.inputs(bpy.data.node_groups[name]))) + outputs = params(get_values(mn.blender.nodes.outputs(bpy.data.node_groups[name]))) - title = mn.blender.nodes.format_node_name(entry.get('label')) + title = mn.blender.nodes.format_node_name(entry.get("label")) entry_list.append(text(title=None, value=f"### {title}")) if desc: entry_list.append(text(title=None, value=desc)) if urls: if not isinstance(urls, list): urls = [urls] - [ - entry_list.append( - text(title=None, value=f"![]({url}.mp4)") - ) for url in urls - ] + [entry_list.append(text(title=None, value=f"![]({url}.mp4)")) for url in urls] - if len(inputs.as_dict()['value']) > 0: + if len(inputs.as_dict()["value"]) > 0: entry_list.append(text(value="\n#### Inputs")) entry_list.append(inputs) - if len(outputs.as_dict()['value']) > 0: + if len(outputs.as_dict()["value"]) > 0: entry_list.append(text(value="\n#### Outputs")) entry_list.append(outputs) @@ -116,10 +111,10 @@ def get_values(sockets): """ for category, object in categories.items(): - with open(os.path.join(folder, f'nodes/{category}.qmd'), 'w') as file: + with open(os.path.join(folder, f"nodes/{category}.qmd"), "w") as file: file.write(header) for doc in object: - section = '' + section = "" for sec in doc: file.write(ren.render(sec)) file.write("\n\n") diff --git a/docs/install.py b/docs/install.py index e3edf92e..70b309c3 100644 --- a/docs/install.py +++ b/docs/install.py @@ -3,17 +3,13 @@ import os -def main(): - +def main() -> None: python = os.path.realpath(sys.executable) - commands = [ - f'{python} -m pip install .', - f'{python} -m pip install quartodoc' - ] + commands = [f"{python} -m pip install .", f"{python} -m pip install quartodoc"] for command in commands: - subprocess.run(command.split(' ')) + subprocess.run(command.split(" ")) if __name__ == "__main__": diff --git a/molecularnodes/__init__.py b/molecularnodes/__init__.py index fbe07ee6..c5360371 100644 --- a/molecularnodes/__init__.py +++ b/molecularnodes/__init__.py @@ -29,7 +29,7 @@ "warning": "", "doc_url": "https://bradyajohnston.github.io/MolecularNodes/", "tracker_url": "https://github.com/BradyAJohnston/MolecularNodes/issues", - "category": "Import" + "category": "Import", } auto_load.init() @@ -40,8 +40,7 @@ def register(): auto_load.register() bpy.types.NODE_MT_add.append(MN_add_node_menu) - bpy.types.Object.mn = bpy.props.PointerProperty( - type=MolecularNodesObjectProperties) + bpy.types.Object.mn = bpy.props.PointerProperty(type=MolecularNodesObjectProperties) for func in universe_funcs: try: bpy.app.handlers.load_post.append(func) diff --git a/molecularnodes/assets/template/Molecular Nodes.zip b/molecularnodes/assets/template/Molecular Nodes.zip index d38dc396..41c27e62 100644 Binary files a/molecularnodes/assets/template/Molecular Nodes.zip and b/molecularnodes/assets/template/Molecular Nodes.zip differ diff --git a/molecularnodes/auto_load.py b/molecularnodes/auto_load.py index 72254c06..f7ef1088 100644 --- a/molecularnodes/auto_load.py +++ b/molecularnodes/auto_load.py @@ -1,6 +1,4 @@ -import os import bpy -import sys import typing import inspect import pkgutil @@ -18,6 +16,7 @@ modules = None ordered_classes = None + def init(): global modules global ordered_classes @@ -25,6 +24,7 @@ def init(): modules = get_all_submodules(Path(__file__).parent) ordered_classes = get_ordered_classes_to_register(modules) + def register(): for cls in ordered_classes: bpy.utils.register_class(cls) @@ -35,6 +35,7 @@ def register(): if hasattr(module, "register"): module.register() + def unregister(): for cls in reversed(ordered_classes): bpy.utils.unregister_class(cls) @@ -49,13 +50,16 @@ def unregister(): # Import modules ################################################# + def get_all_submodules(directory): return list(iter_submodules(directory, directory.name)) + def iter_submodules(path, package_name): for name in sorted(iter_submodule_names(path)): yield importlib.import_module("." + name, package_name) + def iter_submodule_names(path, root=""): for _, module_name, is_package in pkgutil.iter_modules([str(path)]): if is_package: @@ -69,22 +73,26 @@ def iter_submodule_names(path, root=""): # Find classes to register ################################################# + def get_ordered_classes_to_register(modules): return toposort(get_register_deps_dict(modules)) + def get_register_deps_dict(modules): my_classes = set(iter_my_classes(modules)) - my_classes_by_idname = {cls.bl_idname : cls for cls in my_classes if hasattr(cls, "bl_idname")} + my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")} deps_dict = {} for cls in my_classes: deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname)) return deps_dict + def iter_my_register_deps(cls, my_classes, my_classes_by_idname): yield from iter_my_deps_from_annotations(cls, my_classes) yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname) + def iter_my_deps_from_annotations(cls, my_classes): for value in typing.get_type_hints(cls, {}, {}).values(): dependency = get_dependency_from_annotation(value) @@ -92,6 +100,7 @@ def iter_my_deps_from_annotations(cls, my_classes): if dependency in my_classes: yield dependency + def get_dependency_from_annotation(value): if blender_version >= (2, 93): if isinstance(value, bpy.props._PropertyDeferred): @@ -102,6 +111,7 @@ def get_dependency_from_annotation(value): return value[1]["type"] return None + def iter_my_deps_from_parent_id(cls, my_classes_by_idname): if bpy.types.Panel in cls.__bases__: parent_idname = getattr(cls, "bl_parent_id", None) @@ -110,6 +120,7 @@ def iter_my_deps_from_parent_id(cls, my_classes_by_idname): if parent_cls is not None: yield parent_cls + def iter_my_classes(modules): base_types = get_register_base_types() for cls in get_classes_in_modules(modules): @@ -117,6 +128,7 @@ def iter_my_classes(modules): if not getattr(cls, "is_registered", False): yield cls + def get_classes_in_modules(modules): classes = set() for module in modules: @@ -124,24 +136,38 @@ def get_classes_in_modules(modules): classes.add(cls) return classes + def iter_classes_in_module(module): for value in module.__dict__.values(): if inspect.isclass(value): yield value + def get_register_base_types(): - return set(getattr(bpy.types, name) for name in [ - "Panel", "Operator", "PropertyGroup", - "AddonPreferences", "Header", "Menu", - "Node", "NodeSocket", "NodeTree", - "UIList", "RenderEngine", - "Gizmo", "GizmoGroup", - ]) + return set( + getattr(bpy.types, name) + for name in [ + "Panel", + "Operator", + "PropertyGroup", + "AddonPreferences", + "Header", + "Menu", + "Node", + "NodeSocket", + "NodeTree", + "UIList", + "RenderEngine", + "Gizmo", + "GizmoGroup", + ] + ) # Find order to register to solve dependencies ################################################# + def toposort(deps_dict): sorted_list = [] sorted_values = set() @@ -153,5 +179,5 @@ def toposort(deps_dict): sorted_values.add(value) else: unsorted.append(value) - deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted} + deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted} return sorted_list diff --git a/molecularnodes/blender/bones.py b/molecularnodes/blender/bones.py index d40e8633..85f7c935 100644 --- a/molecularnodes/blender/bones.py +++ b/molecularnodes/blender/bones.py @@ -11,7 +11,7 @@ def clear_armature(object): object.modifiers.remove(mod) -def add_bones(object, name='armature'): +def add_bones(object, name="armature"): # creates bones and assigns correct weights clear_armature(object) @@ -20,27 +20,24 @@ def add_bones(object, name='armature'): armature = create_bones(bone_positions, chain_ids) for i in range(bone_weights.shape[1]): - group = object.vertex_groups.new(name=f'mn_armature_{i}') + group = object.vertex_groups.new(name=f"mn_armature_{i}") vertex_indices = np.where(bone_weights[:, i] == 1)[0] - group.add(vertex_indices.tolist(), 1, 'ADD') + group.add(vertex_indices.tolist(), 1, "ADD") object.select_set(True) armature.select_set(True) bpy.context.view_layer.objects.active = armature - bpy.ops.object.parent_set(type='ARMATURE') + bpy.ops.object.parent_set(type="ARMATURE") bpy.context.view_layer.objects.active = object - bpy.ops.object.modifier_move_to_index( - 'EXEC_DEFAULT', modifier="Armature", index=0) + bpy.ops.object.modifier_move_to_index("EXEC_DEFAULT", modifier="Armature", index=0) return armature def get_bone_positions(object): - positions, atom_name, chain_id, res_id, sec_struct = [ - obj.get_attribute(object, att) - for att in ['position', 'atom_name', 'chain_id', 'res_id', 'sec_struct'] + obj.get_attribute(object, att) for att in ["position", "atom_name", "chain_id", "res_id", "sec_struct"] ] is_alpha_carbon = atom_name == 2 @@ -59,17 +56,16 @@ def get_bone_positions(object): def get_bone_weights(object): - print('hello world') - + print("hello world") -def create_bones(positions, chain_ids, name='armature'): - bpy.ops.object.add(type='ARMATURE', enter_editmode=True) +def create_bones(positions, chain_ids, name="armature"): + bpy.ops.object.add(type="ARMATURE", enter_editmode=True) object = bpy.context.active_object object.name = name coll.armature().objects.link(object) armature = object.data - armature.name = f'{name}_frame' + armature.name = f"{name}_frame" arm_name = armature.name bones = [] # add bones @@ -77,7 +73,7 @@ def create_bones(positions, chain_ids, name='armature'): try: pos_a = position pos_b = positions[i + 1, :] - except: + except IndexError: continue bone_name = f"mn_armature_{i}" @@ -96,7 +92,7 @@ def create_bones(positions, chain_ids, name='armature'): armature.edit_bones.active = armature.edit_bones[bone_a] for bone in [bone_a, bone_b]: armature.edit_bones[bone].select = True - bpy.ops.armature.parent_set(type='CONNECTED') + bpy.ops.armature.parent_set(type="CONNECTED") for bone in [bone_a, bone_b]: armature.edit_bones[bone].select = False bpy.ops.object.editmode_toggle() @@ -105,12 +101,12 @@ def create_bones(positions, chain_ids, name='armature'): class MN_MT_Add_Armature(bpy.types.Operator): - bl_idname = 'mn.add_armature' - bl_label = 'Add Armature' - bl_description = 'Automatically add armature for each amino acid of the structure ' + bl_idname = "mn.add_armature" + bl_label = "Add Armature" + bl_description = "Automatically add armature for each amino acid of the structure " - def execute(self, context): + def execute(self, context: bpy.types.Context): object = context.active_object add_bones(bpy.data.objects[object.name], name=object.name) - return {'FINISHED'} + return {"FINISHED"} diff --git a/molecularnodes/blender/coll.py b/molecularnodes/blender/coll.py index f5359bba..1bd983d0 100644 --- a/molecularnodes/blender/coll.py +++ b/molecularnodes/blender/coll.py @@ -1,20 +1,21 @@ import bpy +from typing import Optional -def mn(): +def mn() -> bpy.types.Collection: """Return the MolecularNodes Collection - The collection called 'MolecularNodes' inside the Blender scene is returned. If the + The collection called 'MolecularNodes' inside the Blender scene is returned. If the collection does not exist first, it is created. """ - coll = bpy.data.collections.get('MolecularNodes') + coll = bpy.data.collections.get("MolecularNodes") if not coll: - coll = bpy.data.collections.new('MolecularNodes') + coll = bpy.data.collections.new("MolecularNodes") bpy.context.scene.collection.children.link(coll) return coll -def armature(name='MN_armature'): +def armature(name: str = "MN_armature") -> bpy.types.Collection: coll = bpy.data.collections.get(name) if not coll: coll = bpy.data.collections.new(name) @@ -22,9 +23,8 @@ def armature(name='MN_armature'): return coll -def data(suffix=""): - """A collection for storing MN related data objects. - """ +def data(suffix: str = "") -> bpy.types.Collection: + """A collection for storing MN related data objects.""" name = f"MN_data{suffix}" collection = bpy.data.collections.get(name) @@ -33,16 +33,16 @@ def data(suffix=""): mn().children.link(collection) # disable the view of the data collection - bpy.context.view_layer.layer_collection.children['MolecularNodes'].children[name].exclude = True + bpy.context.view_layer.layer_collection.children["MolecularNodes"].children[name].exclude = True return collection -def frames(name="", parent=None, suffix="_frames"): +def frames(name: str = "", parent: Optional[bpy.types.Object] = None, suffix: str = "_frames") -> bpy.types.Collection: """Create a Collection for Frames of a Trajectory Args: name (str, optional): Name of the collection for the frames. Defaults to "". - parent (_type_, optional): A blender collection which will become the parent + parent (_type_, optional): A blender collection which will become the parent collection. Defaults to the MolecularNodes collection if None. """ coll_frames = bpy.data.collections.new(name + suffix) @@ -54,7 +54,7 @@ def frames(name="", parent=None, suffix="_frames"): return coll_frames -def cellpack(name="", parent=None, fallback=False): +def cellpack(name: str = "", parent: Optional[bpy.types.Object] = None, fallback: bool = False) -> bpy.types.Collection: full_name = f"cellpack_{name}" coll = bpy.data.collections.get(full_name) if coll and fallback: diff --git a/molecularnodes/blender/node.py b/molecularnodes/blender/node.py deleted file mode 100644 index eaf0ed07..00000000 --- a/molecularnodes/blender/node.py +++ /dev/null @@ -1,37 +0,0 @@ -from abc import ABCMeta -from typing import Optional, Any -import warnings -import time -import numpy as np -import bpy - - -class Node(metaclass=ABCMeta): - def __init__(self, node: bpy.types.Node, chain=[]): - - self.node = node - self.group = node.id_data - self.chain = chain - - @property - def location(self): - return np.array(self.node.location) - - def new(self, name): - "Add a new node to the node group." - try: - return self.group.nodes.new(f'GeometryNode{name}') - except RuntimeError: - return self.group.nodes.new(f'ShaderNode{name}') - - def link(self, name, linkto=0, linkfrom=0): - "Create a new node along in the chain and create a link to it. Return the new node." - new_node = self.new(name) - new_node.location = self.location + np.array((200, 0)) - - self.group.links.new( - self.node.outputs[linkfrom], - new_node.inputs[linkto] - ) - - return Node(new_node, chain=self.chain + [self]) diff --git a/molecularnodes/blender/nodes.py b/molecularnodes/blender/nodes.py index 84f6531f..62e69153 100644 --- a/molecularnodes/blender/nodes.py +++ b/molecularnodes/blender/nodes.py @@ -4,74 +4,81 @@ import math import warnings import itertools +from typing import List, Optional, Union, Dict, Tuple from .. import utils from .. import color from .. import pkg from ..blender import obj socket_types = { - 'BOOLEAN': 'NodeSocketBool', - 'GEOMETRY': 'NodeSocketGeometry', - 'INT': 'NodeSocketInt', - 'MATERIAL': 'NodeSocketMaterial', - 'VECTOR': 'NodeSocketVector', - 'STRING': 'NodeSocketString', - 'VALUE': 'NodeSocketFloat', - 'COLLECTION': 'NodeSocketCollection', - 'TEXTURE': 'NodeSocketTexture', - 'COLOR': 'NodeSocketColor', - 'RGBA': 'NodeSocketColor', - 'IMAGE': 'NodeSocketImage' + "BOOLEAN": "NodeSocketBool", + "GEOMETRY": "NodeSocketGeometry", + "INT": "NodeSocketInt", + "MATERIAL": "NodeSocketMaterial", + "VECTOR": "NodeSocketVector", + "STRING": "NodeSocketString", + "VALUE": "NodeSocketFloat", + "COLLECTION": "NodeSocketCollection", + "TEXTURE": "NodeSocketTexture", + "COLOR": "NodeSocketColor", + "RGBA": "NodeSocketColor", + "IMAGE": "NodeSocketImage", } # current implemented representations styles_mapping = { "presets": "MN_style_presets", - 'preset_1': "MN_style_presets", - 'preset_2': "MN_style_presets", - 'preset_3': "MN_style_presets", - 'preset_4': "MN_style_presets", - 'atoms': 'MN_style_spheres', - 'spheres': 'MN_style_spheres', - 'vdw': 'MN_style_spheres', - 'sphere': 'MN_style_spheres', - 'cartoon': 'MN_style_cartoon', - 'ribbon': 'MN_style_ribbon', - 'surface': 'MN_style_surface', - 'ball_and_stick': 'MN_style_ball_and_stick', - 'ball+stick': 'MN_style_ball_and_stick', - 'oxdna': 'MN_oxdna_style_ribbon', + "preset_1": "MN_style_presets", + "preset_2": "MN_style_presets", + "preset_3": "MN_style_presets", + "preset_4": "MN_style_presets", + "atoms": "MN_style_spheres", + "spheres": "MN_style_spheres", + "vdw": "MN_style_spheres", + "sphere": "MN_style_spheres", + "cartoon": "MN_style_cartoon", + "ribbon": "MN_style_ribbon", + "surface": "MN_style_surface", + "ball_and_stick": "MN_style_ball_and_stick", + "ball+stick": "MN_style_ball_and_stick", + "oxdna": "MN_oxdna_style_ribbon", "density_surface": "MN_density_style_surface", - "density_wire": "MN_density_style_wire" + "density_wire": "MN_density_style_wire", } STYLE_ITEMS = ( - ('presets', 'Presets', 'A pre-made combination of different styles'), + ("presets", "Presets", "A pre-made combination of different styles"), ("spheres", "Spheres", "Space-filling atoms style."), ("surface", "Surface", "Solvent-accsible surface."), ("cartoon", "Cartoon", "Secondary structure cartoons"), ("ribbon", "Ribbon", "Continuous backbone ribbon."), - ("ball_and_stick", "Ball and Stick", "Spheres for atoms, sticks for bonds") + ( + "ball_and_stick", + "Ball and Stick", + "Spheres for atoms, sticks for bonds", + ), ) bpy.types.Scene.MN_import_style = bpy.props.EnumProperty( name="Style", description="Default style for importing molecules.", items=STYLE_ITEMS, - default='spheres' + default="spheres", ) -MN_DATA_FILE = os.path.join(pkg.ADDON_DIR, 'assets', 'MN_data_file.blend') +MN_DATA_FILE = os.path.join(pkg.ADDON_DIR, "assets", "MN_data_file.blend") class NodeGroupCreationError(Exception): - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message super().__init__(self.message) -def inputs(node): +def inputs( + node: bpy.types.GeometryNodeGroup, +) -> Dict[str, bpy.types.NodeSocket]: items = {} for item in node.interface.items_tree: if item.item_type == "SOCKET": @@ -80,7 +87,9 @@ def inputs(node): return items -def outputs(node): +def outputs( + node: bpy.types.GeometryNodeGroup, +) -> Dict[str, bpy.types.NodeSocket]: items = {} for item in node.interface.items_tree: if item.item_type == "SOCKET": @@ -89,51 +98,66 @@ def outputs(node): return items -def get_output_type(node, type="INT"): - for output in node.outputs: - if output.type == type: - return output - - -def set_selection(group, node, selection): +def set_selection( + tree: bpy.types.GeometryNodeTree, + node: bpy.types.GeometryNodeGroup, + selection: bpy.types.GeometryNodeGroup, +) -> bpy.types.GeometryNodeGroup: pos = node.location pos = [pos[0] - 200, pos[1] - 200] selection.location = pos - group.links.new(selection.outputs[0], node.inputs['Selection']) + tree.links.new(selection.outputs[0], node.inputs["Selection"]) return selection -def create_debug_group(name='MolecularNodesDebugGroup'): - group = new_group(name=name, fallback=False) - info = group.nodes.new('GeometryNodeObjectInfo') - group.links.new(info.outputs['Geometry'], - group.nodes['Group Output'].inputs[0]) +def node_tree_debug( + name: str = "MolecularNodesTree", +) -> bpy.types.GeometryNodeTree: + group = new_tree(name=name, fallback=False) + info = group.nodes.new("GeometryNodeObjectInfo") + group.links.new(info.outputs["Geometry"], group.nodes["Group Output"].inputs[0]) return group -def add_selection(group, sel_name, input_list, field='chain_id'): +def add_selection( + group: bpy.types.GeometryNodeGroup, + sel_name: str, + input_list: List[str], + field: str = "chain_id", +) -> bpy.types.Node: style = style_node(group) - sel_node = add_custom(group, custom_iswitch( - name='selection', - iter_list=input_list, - field=field, - dtype='BOOLEAN' - ).name) + sel_node = add_custom( + group, + custom_iswitch( + name="selection", + iter_list=input_list, + field=field, + dtype="BOOLEAN", + ).name, + ) set_selection(group, style, sel_node) return sel_node -def get_output(group): - return group.nodes[bpy.app.translations.pgettext_data("Group Output",)] +def get_output(group: bpy.types.GeometryNodeGroup) -> bpy.types.Node: + return group.nodes[ + bpy.app.translations.pgettext_data( + "Group Output", + ) + ] -def get_input(group): - return group.nodes[bpy.app.translations.pgettext_data("Group Input",)] +def get_input(group: bpy.types.GeometryNodeGroup) -> bpy.types.Node: + return group.nodes[ + bpy.app.translations.pgettext_data( + "Group Input", + ) + ] -def get_mod(object, name='MolecularNodes'): +def get_mod(object: bpy.types.Object, name: str = "MolecularNodes") -> bpy.types.Modifier: node_mod = object.modifiers.get(name) if not node_mod: node_mod = object.modifiers.new(name, "NODES") @@ -141,63 +165,69 @@ def get_mod(object, name='MolecularNodes'): return node_mod -def format_node_name(name): +def format_node_name(name: str) -> str: "Formats a node's name for nicer printing." - return name.strip("MN_").replace("_", " ").title().replace('Dna', 'DNA').replace('Topo ', 'Topology ') + return name.strip("MN_").replace("_", " ").title().replace("Dna", "DNA").replace("Topo ", "Topology ") -def get_nodes_last_output(group): +def get_nodes_last_output( + group: bpy.types.GeometryNodeTree, +) -> Tuple[bpy.types.GeometryNode, bpy.types.GeometryNode]: output = get_output(group) last = output.inputs[0].links[0].from_node return last, output -def previous_node(node): +def previous_node(node: bpy.types.GeometryNode) -> bpy.types.GeometryNode: "Get the node which is the first connection to the first input of this node" prev = node.inputs[0].links[0].from_node return prev -def style_node(group): +def style_node(group: bpy.types.Object) -> bpy.types.GeometryNode: prev = previous_node(get_output(group)) - is_style_node = ("style" in prev.name) + is_style_node = "style" in prev.name while not is_style_node: print(prev.name) prev = previous_node(prev) - is_style_node = ("style" in prev.name) + is_style_node = "style" in prev.name return prev -def get_style_node(object): +def get_style_node(object: bpy.types.Object) -> bpy.types.GeometryNode: "Walk back through the primary node connections until you find the first style node" - group = object.modifiers['MolecularNodes'].node_group + group = object.modifiers["MolecularNodes"].node_group return style_node(group) -def star_node(group): +def star_node(group: bpy.types.GeometryNodeTree) -> bpy.types.GeometryNode: prev = previous_node(get_output(group)) - is_star_node = ("MN_starfile_instances" in prev.name) + is_star_node = "MN_starfile_instances" in prev.name while not is_star_node: prev = previous_node(prev) - is_star_node = ("MN_starfile_instances" in prev.name) + is_star_node = "MN_starfile_instances" in prev.name return prev -def get_star_node(object): +def get_star_node(object: bpy.types.Object) -> bpy.types.GeometryNode: "Walk back through the primary node connections until you find the first style node" - group = object.modifiers['MolecularNodes'].node_group + group = object.modifiers["MolecularNodes"].node_group return star_node(group) -def get_color_node(object): +def get_color_node(object: bpy.types.Object) -> bpy.types.GeometryNode: "Walk back through the primary node connections until you find the first style node" - group = object.modifiers['MolecularNodes'].node_group + group = object.modifiers["MolecularNodes"].node_group for node in group.nodes: if node.name == "MN_color_attribute_random": return node -def insert_last_node(group, node, link_input=True): +def insert_last_node( + group: bpy.types.GeometryNodeTree, + node: bpy.types.GeometryNodeGroup, + link_input: bool = True, +) -> None: last, output = get_nodes_last_output(group) link = group.links.new location = output.location @@ -208,96 +238,93 @@ def insert_last_node(group, node, link_input=True): link(node.outputs[0], output.inputs[0]) -def realize_instances(obj): - group = obj.modifiers['MolecularNodes'].node_group - realize = group.nodes.new('GeometryNodeRealizeInstances') +def realize_instances(obj: bpy.types.Object) -> None: + group = obj.modifiers["MolecularNodes"].node_group + realize = group.nodes.new("GeometryNodeRealizeInstances") insert_last_node(group, realize) -def append(node_name, link=False): +def append(node_name: str, link: bool = False) -> bpy.types.GeometryNodeTree: node = bpy.data.node_groups.get(node_name) with warnings.catch_warnings(): warnings.simplefilter("ignore") if not node or link: bpy.ops.wm.append( - 'EXEC_DEFAULT', - directory=os.path.join(MN_DATA_FILE, 'NodeTree'), + "EXEC_DEFAULT", + directory=os.path.join(MN_DATA_FILE, "NodeTree"), filename=node_name, link=link, - use_recursive=True + use_recursive=True, ) node = bpy.data.node_groups.get(node_name) with warnings.catch_warnings(): warnings.simplefilter("ignore") if not node or link: - node_name_components = node_name.split('_') - if node_name_components[0] == 'MN': - data_file = MN_DATA_FILE[:-6] + '_' + \ - node_name_components[1] + '.blend' + node_name_components = node_name.split("_") + if node_name_components[0] == "MN": + data_file = MN_DATA_FILE[:-6] + "_" + node_name_components[1] + ".blend" bpy.ops.wm.append( - 'EXEC_DEFAULT', - directory=os.path.join(data_file, 'NodeTree'), + "EXEC_DEFAULT", + directory=os.path.join(data_file, "NodeTree"), filename=node_name, link=link, - use_recursive=True + use_recursive=True, ) return bpy.data.node_groups[node_name] -def material_default(): +def material_default() -> bpy.types.Material: """ Append MN Default to the .blend file it it doesn't already exist, and return that material. """ - mat_name = 'MN Default' + mat_name = "MN Default" mat = bpy.data.materials.get(mat_name) if not mat: - print('appending material') + print("appending material") bpy.ops.wm.append( - directory=os.path.join(MN_DATA_FILE, 'Material'), - filename='MN Default', - link=False + directory=os.path.join(MN_DATA_FILE, "Material"), + filename="MN Default", + link=False, ) return bpy.data.materials[mat_name] -def MN_micrograph_material(): +def MN_micrograph_material() -> bpy.types.Material: """ Append MN_micrograph_material to the .blend file it it doesn't already exist, and return that material. """ - mat_name = 'MN_micrograph_material' + mat_name = "MN_micrograph_material" return bpy.data.materials[mat_name] -def new_group(name="Geometry Nodes", geometry=True, fallback=True): +def new_tree(name: str = "Geometry Nodes", geometry: bool = True, fallback: bool = True) -> bpy.types.GeometryNodeTree: group = bpy.data.node_groups.get(name) # if the group already exists, return it and don't create a new one if group and fallback: return group # create a new group for this particular name and do some initial setup - group = bpy.data.node_groups.new(name, 'GeometryNodeTree') - input_node = group.nodes.new('NodeGroupInput') - output_node = group.nodes.new('NodeGroupOutput') + group = bpy.data.node_groups.new(name, "GeometryNodeTree") + input_node = group.nodes.new("NodeGroupInput") + output_node = group.nodes.new("NodeGroupOutput") input_node.location.x = -200 - input_node.width output_node.location.x = 200 if geometry: - group.interface.new_socket( - 'Geometry', in_out='INPUT', socket_type='NodeSocketGeometry') - group.interface.new_socket( - 'Geometry', in_out='OUTPUT', socket_type='NodeSocketGeometry') + group.interface.new_socket("Geometry", in_out="INPUT", socket_type="NodeSocketGeometry") + group.interface.new_socket("Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry") group.links.new(output_node.inputs[0], input_node.outputs[0]) return group -def assign_material(node, material='default'): - material_socket = node.inputs.get('Material') +def assign_material(node: bpy.types.GeometryNode, material: str = "default") -> None: + material_socket = node.inputs.get("Material") if material_socket: if not material: pass @@ -307,26 +334,27 @@ def assign_material(node, material='default'): material_socket.default_value = material -def add_node(node_name, label: str = '', show_options=False, material="default"): +def add_node( + node_name: str, + label: str = "", + show_options: bool = False, + material: str = "default", +) -> None: # intended to be called upon button press in the node tree prev_context = bpy.context.area.type - bpy.context.area.type = 'NODE_EDITOR' + bpy.context.area.type = "NODE_EDITOR" # actually invoke the operator to add a node to the current node tree # use_transform=True ensures it appears where the user's mouse is and is currently # being moved so the user can place it where they wish - bpy.ops.node.add_node( - 'INVOKE_DEFAULT', - type='GeometryNodeGroup', - use_transform=True - ) + bpy.ops.node.add_node("INVOKE_DEFAULT", type="GeometryNodeGroup", use_transform=True) bpy.context.area.type = prev_context node = bpy.context.active_node node.node_tree = bpy.data.node_groups[node_name] node.width = 200.0 node.show_options = show_options - if label == '': + if label == "": node.label = format_node_name(node_name) else: node.label = label @@ -337,16 +365,15 @@ def add_node(node_name, label: str = '', show_options=False, material="default") def add_custom( - group, - name, - location=[0, 0], - width=200, - material="default", - show_options=False, - link=False -): - - node = group.nodes.new('GeometryNodeGroup') + tree: bpy.types.GeometryNodeTree, + name: str, + location: List[float] = [0, 0], + width: int = 200, + material: str = "default", + show_options: bool = False, + link: bool = False, +) -> bpy.types.GeometryNode: + node = tree.nodes.new("GeometryNodeGroup") node.node_tree = append(name, link=link) # if there is an input socket called 'Material', assign it to the base MN material @@ -363,7 +390,7 @@ def add_custom( return node -def change_style_node(object, style): +def change_style_node(object: bpy.types.Object, style: str) -> None: # get the node group that we are working on, to change the specific style node group = get_mod(object).node_group link = group.links.new @@ -386,7 +413,7 @@ def change_style_node(object, style): group.links.remove(output_link) try: - material = node_style.inputs['Material'].default_value + material = node_style.inputs["Material"].default_value except KeyError: material = None # append the new node tree, and swap out the tree that is used for the group @@ -404,13 +431,13 @@ def change_style_node(object, style): if material: try: - node_style.inputs['Material'].default_value = material + node_style.inputs["Material"].default_value = material except KeyError: # the new node doesn't contain a material slot pass -def create_starting_nodes_starfile(object, n_images=1): +def create_starting_nodes_starfile(object: bpy.types.Object, n_images: int = 1) -> None: # ensure there is a geometry nodes modifier called 'MolecularNodes' that is created and applied to the object node_mod = get_mod(object) @@ -419,7 +446,7 @@ def create_starting_nodes_starfile(object, n_images=1): # Make sure the aotmic material is loaded material_default() # create a new GN node group, specific to this particular molecule - group = new_group(node_name) + group = new_tree(node_name) node_mod.node_group = group link = group.links.new @@ -428,21 +455,25 @@ def create_starting_nodes_starfile(object, n_images=1): node_output = get_output(group) node_input.location = [0, 0] node_output.location = [700, 0] - node_star_instances = add_custom(group, 'MN_starfile_instances', [450, 0]) + node_star_instances = add_custom(group, "MN_starfile_instances", [450, 0]) link(node_star_instances.outputs[0], node_output.inputs[0]) link(node_input.outputs[0], node_star_instances.inputs[0]) # Need to manually set Image input to 1, otherwise it will be 0 (even though default is 1) - node_mod['Input_3'] = 1 + node_mod["Input_3"] = 1 -def create_starting_nodes_density(object, threshold=0.8, style='density_surface'): +def create_starting_nodes_density( + object: bpy.types.Object, + threshold: float = 0.8, + style: str = "density_surface", +) -> None: # ensure there is a geometry nodes modifier called 'MolecularNodes' that is created and applied to the object mod = get_mod(object) node_name = f"MN_density_{object.name}" # create a new GN node group, specific to this particular molecule - group = new_group(node_name, fallback=False) + group = new_tree(node_name, fallback=False) link = group.links.new mod.node_group = group @@ -453,13 +484,19 @@ def create_starting_nodes_density(object, threshold=0.8, style='density_surface' node_output.location = [800, 0] node_density = add_custom(group, styles_mapping[style], [400, 0]) - node_density.inputs['Threshold'].default_value = threshold + node_density.inputs["Threshold"].default_value = threshold link(node_input.outputs[0], node_density.inputs[0]) link(node_density.outputs[0], node_output.inputs[0]) -def create_starting_node_tree(object, coll_frames=None, style="spheres", name=None, set_color=True): +def create_starting_node_tree( + object: bpy.types.Object, + coll_frames: Optional[bpy.types.Collection] = None, + style: str = "spheres", + name: Optional[str] = None, + set_color: bool = True, +) -> None: """ Create a starting node tree for the inputted object. @@ -488,7 +525,7 @@ def create_starting_node_tree(object, coll_frames=None, style="spheres", name=No # create a new GN node group, specific to this particular molecule mod = get_mod(object) - group = new_group(name) + group = new_tree(name) link = group.links.new mod.node_group = group @@ -504,15 +541,16 @@ def create_starting_node_tree(object, coll_frames=None, style="spheres", name=No # if requested, setup the nodes for generating colors in the node tree if set_color: - node_color_set = add_custom(group, 'MN_color_set', [200, 0]) - node_color_common = add_custom(group, 'MN_color_common', [-50, -150]) - node_random_color = add_custom( - group, 'MN_color_attribute_random', [-300, -150]) - - link(node_input.outputs['Geometry'], node_color_set.inputs[0]) - link(node_random_color.outputs['Color'], - node_color_common.inputs['Carbon']) - link(node_color_common.outputs[0], node_color_set.inputs['Color']) + node_color_set = add_custom(group, "MN_color_set", [200, 0]) + node_color_common = add_custom(group, "MN_color_common", [-50, -150]) + node_random_color = add_custom(group, "MN_color_attribute_random", [-300, -150]) + + link(node_input.outputs["Geometry"], node_color_set.inputs[0]) + link( + node_random_color.outputs["Color"], + node_color_common.inputs["Carbon"], + ) + link(node_color_common.outputs[0], node_color_set.inputs["Color"]) link(node_color_set.outputs[0], node_style.inputs[0]) to_animate = node_color_set else: @@ -523,30 +561,38 @@ def create_starting_node_tree(object, coll_frames=None, style="spheres", name=No node_output.location = [1100, 0] node_style.location = [800, 0] - node_animate_frames = add_custom(group, 'MN_animate_frames', [500, 0]) - node_animate = add_custom(group, 'MN_animate_value', [500, -300]) + node_animate_frames = add_custom(group, "MN_animate_frames", [500, 0]) + node_animate = add_custom(group, "MN_animate_value", [500, -300]) - node_animate_frames.inputs['Frames'].default_value = coll_frames - node_animate.inputs['To Max'].default_value = len( - coll_frames.objects) - 1 + node_animate_frames.inputs["Frames"].default_value = coll_frames + node_animate.inputs["To Max"].default_value = len(coll_frames.objects) - 1 link(to_animate.outputs[0], node_animate_frames.inputs[0]) link(node_animate_frames.outputs[0], node_style.inputs[0]) - link(node_animate.outputs[0], node_animate_frames.inputs['Frame']) + link(node_animate.outputs[0], node_animate_frames.inputs["Frame"]) -def combine_join_geometry(group, node_list, output='Geometry', join_offset=300): - link = group.links.new +def combine_join_geometry( + tree: bpy.types.GeometryNodeTree, + node_list: List[bpy.types.GeometryNode], + output: str = "Geometry", + join_offset: int = 300, +) -> bpy.types.GeometryNode: + link = tree.links.new max_x = max([node.location[0] for node in node_list]) - node_to_instances = group.nodes.new('GeometryNodeJoinGeometry') + node_to_instances = tree.nodes.new("GeometryNodeJoinGeometry") node_to_instances.location = [int(max_x + join_offset), 0] for node in reversed(node_list): - link(node.outputs[output], node_to_instances.inputs['Geometry']) + link(node.outputs[output], node_to_instances.inputs["Geometry"]) return node_to_instances -def split_geometry_to_instances(name, iter_list=('A', 'B', 'C'), attribute='chain_id'): +def split_geometry_to_instances( + name: str, + iter_list: List[str] = ["A", "B", "C"], + attribute: str = "chain_id", +) -> bpy.types.GeometryNodeTree: """Create a Node to Split Geometry by an Attribute into Instances Splits the inputted geometry into instances, based on an attribute field. By @@ -555,54 +601,46 @@ def split_geometry_to_instances(name, iter_list=('A', 'B', 'C'), attribute='chai define how many times to create the required nodes. """ - group = new_group(name) - node_input = get_input(group) - node_output = get_output(group) + tree = new_tree(name) + node_input = get_input(tree) + node_output = get_output(tree) - named_att = group.nodes.new('GeometryNodeInputNamedAttribute') + named_att = tree.nodes.new("GeometryNodeInputNamedAttribute") named_att.location = [-200, -200] - named_att.data_type = 'INT' + named_att.data_type = "INT" named_att.inputs[0].default_value = attribute - link = group.links.new + link = tree.links.new list_sep = [] for i, chain in enumerate(iter_list): pos = [i % 10, math.floor(i / 10)] - node_split = add_custom(group, '.MN_utils_split_instance') + node_split = add_custom(tree, ".MN_utils_split_instance") node_split.location = [int(250 * pos[0]), int(-300 * pos[1])] - node_split.inputs['Group ID'].default_value = i - link(named_att.outputs['Attribute'], node_split.inputs['Field']) - link(node_input.outputs['Geometry'], node_split.inputs['Geometry']) + node_split.inputs["Group ID"].default_value = i + link(named_att.outputs["Attribute"], node_split.inputs["Field"]) + link(node_input.outputs["Geometry"], node_split.inputs["Geometry"]) list_sep.append(node_split) - node_instance = combine_join_geometry(group, list_sep, 'Instance') + node_instance = combine_join_geometry(tree, list_sep, "Instance") node_output.location = [int(10 * 250 + 400), 0] link(node_instance.outputs[0], node_output.inputs[0]) - return group + return tree -def assembly_initialise(mol: bpy.types.Object): +def assembly_initialise(mol: bpy.types.Object) -> bpy.types.GeometryNodeTree: """ Setup the required data object and nodes for building an assembly. """ - transforms = utils.array_quaternions_from_dict( - mol['biological_assemblies']) - data_object = obj.create_data_object( - array=transforms, - name=f"data_assembly_{mol.name}" - ) - tree_assembly = create_assembly_node_tree( - name=mol.name, - iter_list=mol['chain_ids'], - data_object=data_object - ) + transforms = utils.array_quaternions_from_dict(mol["biological_assemblies"]) + data_object = obj.create_data_object(array=transforms, name=f"data_assembly_{mol.name}") + tree_assembly = create_assembly_node_tree(name=mol.name, iter_list=mol["chain_ids"], data_object=data_object) return tree_assembly -def assembly_insert(mol: bpy.types.Object): +def assembly_insert(mol: bpy.types.Object) -> None: """ Given a molecule, setup the required assembly node and insert it into the node tree. """ @@ -613,92 +651,105 @@ def assembly_insert(mol: bpy.types.Object): insert_last_node(get_mod(mol).node_group, node) -def create_assembly_node_tree(name, iter_list, data_object): - +def create_assembly_node_tree( + name: str, iter_list: List[str], data_object: bpy.types.Object +) -> bpy.types.GeometryNodeTree: node_group_name = f"MN_assembly_{name}" - group = new_group(name=node_group_name) - link = group.links.new + tree = new_tree(name=node_group_name) + link = tree.links.new - n_assemblies = len( - np.unique(obj.get_attribute(data_object, 'assembly_id'))) + n_assemblies = len(np.unique(obj.get_attribute(data_object, "assembly_id"))) node_group_instances = split_geometry_to_instances( name=f".MN_utils_split_{name}", iter_list=iter_list, - attribute='chain_id' + attribute="chain_id", ) - node_group_assembly_instance = append('.MN_assembly_instance_chains') - node_instances = add_custom(group, node_group_instances.name, [0, 0]) - node_assembly = add_custom( - group, node_group_assembly_instance.name, [200, 0]) - node_assembly.inputs['data_object'].default_value = data_object + node_group_assembly_instance = append(".MN_assembly_instance_chains") + node_instances = add_custom(tree, node_group_instances.name, [0, 0]) + node_assembly = add_custom(tree, node_group_assembly_instance.name, [200, 0]) + node_assembly.inputs["data_object"].default_value = data_object - out_sockets = outputs(group) + out_sockets = outputs(tree) out_sockets[list(out_sockets)[0]].name = "Instances" socket_info = ( - {"name": "Rotation", "type": "NodeSocketFloat", - "min": 0, "max": 1, "default": 1}, - {"name": "Translation", "type": "NodeSocketFloat", - "min": 0, "max": 1, "default": 1}, - {"name": "assembly_id", "type": "NodeSocketInt", - "min": 1, "max": n_assemblies, "default": 1} + { + "name": "Rotation", + "type": "NodeSocketFloat", + "min": 0, + "max": 1, + "default": 1, + }, + { + "name": "Translation", + "type": "NodeSocketFloat", + "min": 0, + "max": 1, + "default": 1, + }, + { + "name": "assembly_id", + "type": "NodeSocketInt", + "min": 1, + "max": n_assemblies, + "default": 1, + }, ) for info in socket_info: - socket = group.interface.items_tree.get(info['name']) + socket = tree.interface.items_tree.get(info["name"]) if not socket: - socket = group.interface.new_socket( - info['name'], in_out='INPUT', socket_type=info['type']) - socket.default_value = info['default'] - socket.min_value = info['min'] - socket.max_value = info['max'] + socket = tree.interface.new_socket(info["name"], in_out="INPUT", socket_type=info["type"]) + socket.default_value = info["default"] + socket.min_value = info["min"] + socket.max_value = info["max"] - link(get_input(group).outputs[info['name']], - node_assembly.inputs[info['name']]) + link( + get_input(tree).outputs[info["name"]], + node_assembly.inputs[info["name"]], + ) - get_output(group).location = [400, 0] - link(get_input(group).outputs[0], node_instances.inputs[0]) + get_output(tree).location = [400, 0] + link(get_input(tree).outputs[0], node_instances.inputs[0]) link(node_instances.outputs[0], node_assembly.inputs[0]) - link(node_assembly.outputs[0], get_output(group).inputs[0]) + link(node_assembly.outputs[0], get_output(tree).inputs[0]) - return group + return tree -def add_inverse_selection(group): +def add_inverse_selection(group: bpy.types.GeometryNodeTree) -> None: output = get_output(group) - if 'Inverted' not in output.inputs.keys(): - group.interface.new_socket( - 'Inverted', in_out='OUTPUT', socket_type='NodeSocketBool') + if "Inverted" not in output.inputs.keys(): + group.interface.new_socket("Inverted", in_out="OUTPUT", socket_type="NodeSocketBool") loc = output.location bool_math = group.nodes.new("FunctionNodeBooleanMath") bool_math.location = [loc[0], -100] bool_math.operation = "NOT" - group.links.new( - output.inputs['Selection'].links[0].from_socket, bool_math.inputs[0]) - group.links.new(bool_math.outputs[0], output.inputs['Inverted']) + group.links.new(output.inputs["Selection"].links[0].from_socket, bool_math.inputs[0]) + group.links.new(bool_math.outputs[0], output.inputs["Inverted"]) def custom_iswitch( - name, - iter_list, - field='chain_id', - dtype='BOOLEAN', - default_values=None, - prefix='', - start=0 -): + name: str, + iter_list: List[str], + field: str = "chain_id", + dtype: str = "BOOLEAN", + default_values: Optional[List[Union[str, int, float, bool]]] = None, + prefix: str = "", + start: int = 0, +) -> bpy.types.GeometryNodeTree: """ - Creates a named `Index Switch` node. + Creates a named `Index Switch` node. Wraps an index switch node, giving the group names or each name in the `iter_list`. Uses the given field for the attribute name to use in the index switch, and optionally adds an offset value if the start value is non zero. - If a list of default items is given, then it is recycled to fill the defaults for + If a list of default items is given, then it is recycled to fill the defaults for each created socket in for the node. Parameters @@ -718,7 +769,7 @@ def custom_iswitch( Returns ------- - group : bpy.types.NodeGroup + group : bpy.types.GeometryNodeGroup The created node group. Raises @@ -732,7 +783,7 @@ def custom_iswitch( return group socket_type = socket_types[dtype] - group = new_group(name, geometry=False, fallback=False) + group = new_tree(name, geometry=False, fallback=False) # try creating the node group, otherwise on fail cleanup the created group and # report the error @@ -740,48 +791,36 @@ def custom_iswitch( link = group.links.new node_input = get_input(group) node_output = get_output(group) - node_attr = group.nodes.new('GeometryNodeInputNamedAttribute') - node_attr.data_type = 'INT' + node_attr = group.nodes.new("GeometryNodeInputNamedAttribute") + node_attr.data_type = "INT" node_attr.location = [0, 150] - node_attr.inputs['Name'].default_value = str(field) + node_attr.inputs["Name"].default_value = str(field) - node_iswitch = group.nodes.new('GeometryNodeIndexSwitch') + node_iswitch = group.nodes.new("GeometryNodeIndexSwitch") node_iswitch.data_type = dtype - link(node_attr.outputs['Attribute'], node_iswitch.inputs['Index']) + link(node_attr.outputs["Attribute"], node_iswitch.inputs["Index"]) # if there is as offset to the lookup values (say we want to start looking up # from 100 or 1000 etc) then we add a math node with that offset value if start != 0: - node_math = group.nodes.new('ShaderNodeMath') - node_math.operation = 'ADD' + node_math = group.nodes.new("ShaderNodeMath") + node_math.operation = "ADD" node_math.location = [0, 150] node_attr.location = [0, 300] node_math.inputs[1].default_value = start - link( - node_attr.outputs['Attribute'], - node_math.inputs[0] - ) - link( - node_math.outputs['Value'], - node_iswitch.inputs['Index'] - ) + link(node_attr.outputs["Attribute"], node_math.inputs[0]) + link(node_math.outputs["Value"], node_iswitch.inputs["Index"]) # if there are custom values provided, create a dictionary lookup for those values # to assign to the sockets upon creation. If no default was given and the dtype # is colors, then generate a random pastel color for each value default_lookup = None if default_values: - default_lookup = dict(zip( - iter_list, - itertools.cycle(default_values) - )) - elif dtype == 'RGBA': - default_lookup = dict(zip( - iter_list, - [color.random_rgb() for i in iter_list] - )) + default_lookup = dict(zip(iter_list, itertools.cycle(default_values))) + elif dtype == "RGBA": + default_lookup = dict(zip(iter_list, [color.random_rgb() for i in iter_list])) # for each item in the iter_list, we create a new socket on the interface for this # node group, and link it to the interface on the index switch. The index switch @@ -791,11 +830,7 @@ def custom_iswitch( if i > 1: node_iswitch.index_switch_items.new() - socket = group.interface.new_socket( - name=f'{prefix}{item}', - in_out='INPUT', - socket_type=socket_type - ) + socket = group.interface.new_socket(name=f"{prefix}{item}", in_out="INPUT", socket_type=socket_type) # if a set of default values was given, then use it for setting # the defaults on the created sockets of the node group if default_lookup: @@ -803,17 +838,13 @@ def custom_iswitch( link( node_input.outputs[socket.identifier], - node_iswitch.inputs[str(i)] + node_iswitch.inputs[str(i)], ) - socket_out = group.interface.new_socket( - name='Color', - in_out='OUTPUT', - socket_type=socket_type - ) + socket_out = group.interface.new_socket(name="Color", in_out="OUTPUT", socket_type=socket_type) link( - node_iswitch.outputs['Output'], - node_output.inputs[socket_out.identifier] + node_iswitch.outputs["Output"], + node_output.inputs[socket_out.identifier], ) return group @@ -822,12 +853,10 @@ def custom_iswitch( except Exception as e: node_name = group.name bpy.data.node_groups.remove(group) - raise NodeGroupCreationError( - f'Unable to make node group: {node_name}.\nError: {e}' - ) + raise NodeGroupCreationError(f"Unable to make node group: {node_name}.\nError: {e}") -def resid_multiple_selection(node_name, input_resid_string): +def resid_multiple_selection(node_name: str, input_resid_string: str) -> bpy.types.GeometryNodeTree: """ Returns a node group that takes an integer input and creates a boolean tick box for each item in the input list. Outputs are the selected @@ -839,16 +868,16 @@ def resid_multiple_selection(node_name, input_resid_string): # do a cleanning of input string to allow fuzzy input from users for c in ";/+ .": if c in input_resid_string: - input_resid_string = input_resid_string.replace(c, ',') + input_resid_string = input_resid_string.replace(c, ",") for c in "_=:": if c in input_resid_string: - input_resid_string = input_resid_string.replace(c, '-') + input_resid_string = input_resid_string.replace(c, "-") # print(f'fixed input:{input_resid_string}') # parse input_resid_string into sub selecting string list - sub_list = [item for item in input_resid_string.split(',') if item] + sub_list = [item for item in input_resid_string.split(",") if item] # distance vertical to space all of the created nodes node_sep_dis = -100 @@ -858,76 +887,70 @@ def resid_multiple_selection(node_name, input_resid_string): # create the custom node group data block, where everything will go # also create the required group node input and position it - residue_id_group = bpy.data.node_groups.new(node_name, "GeometryNodeTree") - node_input = residue_id_group.nodes.new("NodeGroupInput") - node_input.location = [0, node_sep_dis * len(sub_list)/2] + tree = bpy.data.node_groups.new(node_name, "GeometryNodeTree") + node_input = tree.nodes.new("NodeGroupInput") + node_input.location = [0, node_sep_dis * len(sub_list) / 2] - group_link = residue_id_group.links.new - new_node = residue_id_group.nodes.new + link = tree.links.new + new_node = tree.nodes.new - prev = None for residue_id_index, residue_id in enumerate(sub_list): - # add an new node of Select Res ID or MN_sek_res_id_range current_node = new_node("GeometryNodeGroup") # add an bool_math block bool_math = new_node("FunctionNodeBooleanMath") - bool_math.location = [400, (residue_id_index+1) * node_sep_dis] + bool_math.location = [400, (residue_id_index + 1) * node_sep_dis] bool_math.operation = "OR" - if '-' in residue_id: + if "-" in residue_id: # set two new inputs - current_node.node_tree = append('MN_select_res_id_range') - [resid_start, resid_end] = residue_id.split('-')[:2] - socket_1 = residue_id_group.interface.new_socket( - 'res_id: Min', in_out='INPUT', socket_type='NodeSocketInt') + current_node.node_tree = append("MN_select_res_id_range") + [resid_start, resid_end] = residue_id.split("-")[:2] + socket_1 = tree.interface.new_socket("res_id: Min", in_out="INPUT", socket_type="NodeSocketInt") socket_1.default_value = int(resid_start) - socket_2 = residue_id_group.interface.new_socket( - 'res_id: Max', in_out='INPUT', socket_type='NodeSocketInt') + socket_2 = tree.interface.new_socket("res_id: Max", in_out="INPUT", socket_type="NodeSocketInt") socket_2.default_value = int(resid_end) # a residue range - group_link( - node_input.outputs[socket_1.identifier], current_node.inputs[0]) - group_link( - node_input.outputs[socket_2.identifier], current_node.inputs[1]) + link(node_input.outputs[socket_1.identifier], current_node.inputs[0]) + link(node_input.outputs[socket_2.identifier], current_node.inputs[1]) else: # create a node - current_node.node_tree = append('MN_select_res_id_single') - socket = residue_id_group.interface.new_socket( - 'res_id', in_out='INPUT', socket_type='NodeSocketInt') + current_node.node_tree = append("MN_select_res_id_single") + socket = tree.interface.new_socket("res_id", in_out="INPUT", socket_type="NodeSocketInt") socket.default_value = int(residue_id) - group_link( - node_input.outputs[socket.identifier], current_node.inputs[0]) + link(node_input.outputs[socket.identifier], current_node.inputs[0]) # set the coordinates - current_node.location = [200, (residue_id_index+1) * node_sep_dis] - if not prev: + current_node.location = [200, (residue_id_index + 1) * node_sep_dis] + if residue_id_index == 0: # link the first residue selection to the first input of its OR block - group_link(current_node.outputs['Selection'], bool_math.inputs[0]) + link(current_node.outputs["Selection"], bool_math.inputs[0]) + prev = bool_math else: # if it is not the first residue selection, link the output to the previous or block - group_link(current_node.outputs['Selection'], prev.inputs[1]) + link(current_node.outputs["Selection"], prev.inputs[1]) # link the ouput of previous OR block to the current OR block - group_link(prev.outputs[0], bool_math.inputs[0]) - prev = bool_math + link(prev.outputs[0], bool_math.inputs[0]) + prev = bool_math # add a output block residue_id_group_out = new_node("NodeGroupOutput") residue_id_group_out.location = [ - 800, (residue_id_index + 1) / 2 * node_sep_dis] - residue_id_group.interface.new_socket( - 'Selection', in_out='OUTPUT', socket_type='NodeSocketBool') - residue_id_group.interface.new_socket( - 'Inverted', in_out='OUTPUT', socket_type='NodeSocketBool') - group_link(prev.outputs[0], residue_id_group_out.inputs['Selection']) + 800, + (residue_id_index + 1) / 2 * node_sep_dis, + ] + tree.interface.new_socket("Selection", in_out="OUTPUT", socket_type="NodeSocketBool") + tree.interface.new_socket("Inverted", in_out="OUTPUT", socket_type="NodeSocketBool") + link(prev.outputs[0], residue_id_group_out.inputs["Selection"]) invert_bool_math = new_node("FunctionNodeBooleanMath") invert_bool_math.location = [ - 600, (residue_id_index+1) / 3 * 2 * node_sep_dis] + 600, + (residue_id_index + 1) / 3 * 2 * node_sep_dis, + ] invert_bool_math.operation = "NOT" - group_link(prev.outputs[0], invert_bool_math.inputs[0]) - group_link(invert_bool_math.outputs[0], - residue_id_group_out.inputs['Inverted']) - return residue_id_group + link(prev.outputs[0], invert_bool_math.inputs[0]) + link(invert_bool_math.outputs[0], residue_id_group_out.inputs["Inverted"]) + return tree diff --git a/molecularnodes/blender/obj.py b/molecularnodes/blender/obj.py index c67d884e..b8630617 100644 --- a/molecularnodes/blender/obj.py +++ b/molecularnodes/blender/obj.py @@ -1,9 +1,11 @@ +from dataclasses import dataclass +from typing import Union, List, Optional, Type +from types import TracebackType +from pathlib import Path import bpy import numpy as np -from . import coll -from . import nodes -from dataclasses import dataclass +from . import coll, nodes @dataclass @@ -13,29 +15,32 @@ class AttributeTypeInfo: width: int -TYPES = {key: AttributeTypeInfo(*values) for key, values in { - 'FLOAT_VECTOR': ('vector', float, 3), - 'FLOAT_COLOR': ('color', float, 4), - 'QUATERNION': ('value', float, 4), - 'INT': ('value', int, 1), - 'FLOAT': ('value', float, 1), - 'INT32_2D': ('value', int, 2), - 'BOOLEAN': ('value', bool, 1) -}.items()} +TYPES = { + key: AttributeTypeInfo(*values) + for key, values in { + "FLOAT_VECTOR": ("vector", float, 3), + "FLOAT_COLOR": ("color", float, 4), + "QUATERNION": ("value", float, 4), + "INT": ("value", int, 1), + "FLOAT": ("value", float, 1), + "INT32_2D": ("value", int, 2), + "BOOLEAN": ("value", bool, 1), + }.items() +} class AttributeMismatchError(Exception): - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message super().__init__(self.message) -def centre(array: np.array): - return np.mean(array, axis=0) +def centre(array: np.array) -> np.ndarray: + return np.array(np.mean(array, axis=0)) -def centre_weighted(array: np.ndarray, weight: np.ndarray): - return np.sum(array * weight.reshape((len(array), 1)), axis=0) / np.sum(weight) +def centre_weighted(array: np.ndarray, weight: np.ndarray) -> np.ndarray: + return np.array(np.sum(array * weight.reshape((len(array), 1)), axis=0) / np.sum(weight)) class ObjectTracker: @@ -51,7 +56,7 @@ class ObjectTracker: Returns a list of new objects that were added to bpy.data.objects while in the context. """ - def __enter__(self): + def __enter__(self) -> "ObjectTracker": """ Store the current objects and their names when entering the context. @@ -63,10 +68,20 @@ def __enter__(self): self.objects = list(bpy.context.scene.objects) return self - def __exit__(self, type, value, traceback): - pass - - def new_objects(self): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: + del self.objects + if exc_type is not None: + print(f"Exception detected: {exc_val}") + print(exc_tb) + return True + return False + + def new_objects(self) -> List[bpy.types.Object]: """ Find new objects that were added to bpy.data.objects while in the context. @@ -85,7 +100,7 @@ def new_objects(self): new_objects.append(bob) return new_objects - def latest(self): + def latest(self) -> bpy.types.Object: """ Get the most recently added object. @@ -100,14 +115,14 @@ def latest(self): def create_object( - vertices: np.ndarray = [], - edges: np.ndarray = [], - faces: np.ndarray = [], - name: str = 'NewObject', - collection: bpy.types.Collection = None + vertices: Union[np.ndarray, List[List[float]], None] = None, + edges: Union[np.ndarray, List[List[int]], None] = None, + faces: Union[np.ndarray, List[List[int]], None] = None, + name: Optional[str] = "NewObject", + collection: Optional[bpy.types.Collection] = None, ) -> bpy.types.Object: """ - Create a new Blender object, initialised with locations for each vertex. + Create a new Blender object, initialised with locations for each vertex. If edges and faces are supplied then these are also created on the mesh. @@ -131,28 +146,43 @@ def create_object( """ mesh = bpy.data.meshes.new(name) - mesh.from_pydata(vertices=vertices, edges=edges, faces=faces) + if edges is None: + edges = [] + if faces is None: + faces = [] + if vertices is None: + vertices = [[0, 0, 0]] + mesh.from_pydata(vertices=vertices, edges=edges, faces=faces) + if name is None: + name = "NewObject" object = bpy.data.objects.new(name, mesh) if not collection: # Add the object to the scene if no collection is specified - collection = bpy.data.collections['Collection'] + collection = bpy.data.collections["Collection"] collection.objects.link(object) - object['type'] = 'molecule' + object["type"] = "molecule" return object +def set_position(bob: bpy.types.Object, positions: np.ndarray) -> None: + "A stripped-back way to set the positions for higher performance." + attribute = bob.data.attributes["position"] + attribute.data.foreach_set("vector", positions.reshape(-1)) + bob.data.vertices[0].co = bob.data.vertices[0].co + + def set_attribute( object: bpy.types.Object, name: str, data: np.ndarray, - type=None, - domain="POINT", - overwrite: bool = True + type: Optional[str] = None, + domain: str = "POINT", + overwrite: bool = True, ) -> bpy.types.Attribute: """ Adds and sets the values of an attribute on the object. @@ -170,7 +200,7 @@ def set_attribute( 'FLOAT_VECTOR', 'FLOAT_COLOR", 'QUATERNION', 'FLOAT', 'INT', 'BOOLEAN' ) domain : str, optional - The domain of the attribute. Defaults to "POINT". Currenlty only ('POINT', 'EDGE', + The domain of the attribute. Defaults to "POINT". Currenlty only ('POINT', 'EDGE', 'FACE') have been tested. overwrite : bool, optional Whether to overwrite an existing attribute with the same name. Defaults to False. @@ -207,13 +237,14 @@ def set_attribute( if len(data) != len(attribute.data): raise AttributeMismatchError( - f"Data length {len(data)}, dimensions {data.shape} does not equal the size of the target domain {domain}, len={len(attribute.data)=}" + f"Data length {len(data)}, dimensions {data.shape} " + f"does not equal the size of the target domain {domain}, " + f"len={len(attribute.data)=}" ) # the 'foreach_set' requires a 1D array, regardless of the shape of the attribute # it also requires the order to be 'c' or blender might crash!! - attribute.data.foreach_set( - TYPES[type].dname, data.reshape(-1).copy(order='c')) + attribute.data.foreach_set(TYPES[type].dname, data.reshape(-1).copy(order="C")) # The updating of data doesn't work 100% of the time (see: # https://projects.blender.org/blender/blender/issues/118507) so this resetting of a @@ -222,14 +253,14 @@ def set_attribute( # is the case For now we will set a single vert to it's own position, which triggers a # proper refresh of the object data. try: - object.data.vertices[0].co = object.data.certices[0].co + object.data.vertices[0].co = object.data.vertices[0].co except AttributeError: object.data.update() return attribute -def get_attribute(object: bpy.types.Object, name='position', evaluate=False) -> np.ndarray: +def get_attribute(object: bpy.types.Object, name: str = "position", evaluate: bool = False) -> np.ndarray: """ Get the attribute data from the object. @@ -251,9 +282,7 @@ def get_attribute(object: bpy.types.Object, name='position', evaluate=False) -> Possible attributes are: {attribute_names=}" ) else: - raise AttributeError( - f"The selected attribute '{name}' does not exist on the mesh." - ) + raise AttributeError(f"The selected attribute '{name}' does not exist on the mesh.") # Get the attribute and some metadata about it from the object att = object.data.attributes[name] @@ -265,7 +294,7 @@ def get_attribute(object: bpy.types.Object, name='position', evaluate=False) -> # we have the initialise the array first with the appropriate length, then we can # fill it with the given data using the 'foreach_get' method which is super fast C++ # internal method - array = np.zeros(n_att * width, dtype=data_type.dtype) + array: np.ndarray = np.zeros(n_att * width, dtype=data_type.dtype) # it is currently not really consistent, but to get the values you need to use one of # the 'value', 'vector', 'color' etc from the types dict. This I could only figure # out through trial and error. I assume this might be changed / improved in the future @@ -278,10 +307,7 @@ def get_attribute(object: bpy.types.Object, name='position', evaluate=False) -> return array -def import_vdb( - file: str, - collection: bpy.types.Collection = None -) -> bpy.types.Object: +def import_vdb(file: Union[Path, str], collection: bpy.types.Collection = None) -> bpy.types.Object: """ Imports a VDB file as a Blender volume object, in the MolecularNodes collection. @@ -311,13 +337,13 @@ def import_vdb( return object -def evaluated(object): +def evaluated(object: bpy.types.Object) -> bpy.types.Object: "Return an object which has the modifiers evaluated." object.update_tag() return object.evaluated_get(bpy.context.evaluated_depsgraph_get()) -def evaluate_using_mesh(object): +def evaluate_using_mesh(object: bpy.types.Object) -> bpy.types.Object: """ Evaluate the object using a debug object. Some objects can't currently have their Geometry Node trees evaluated (such as volumes), so we source the geometry they create @@ -340,24 +366,24 @@ def evaluate_using_mesh(object): # object types can't be currently through the API debug = create_object() mod = nodes.get_mod(debug) - mod.node_group = nodes.create_debug_group() - mod.node_group.nodes['Object Info'].inputs['Object'].default_value = object + mod.node_group = nodes.node_tree_debug() + mod.node_group.nodes["Object Info"].inputs["Object"].default_value = object # need to use 'evaluate' otherwise the modifiers won't be taken into account return evaluated(debug) def create_data_object( - array, - collection=None, - name='DataObject', - world_scale=0.01, - fallback=False -): + array: np.ndarray, + collection: Optional[bpy.types.Collection] = None, + name: str = "DataObject", + world_scale: float = 0.01, + fallback: bool = False, +) -> bpy.types.Object: # still requires a unique call TODO: figure out why # I think this has to do with the bcif instancing extraction array = np.unique(array) - locations = array['translation'] * world_scale + locations = array["translation"] * world_scale if not collection: collection = coll.data() @@ -365,10 +391,10 @@ def create_data_object( object = create_object(locations, collection=collection, name=name) attributes = [ - ('rotation', 'QUATERNION'), - ('assembly_id', 'INT'), - ('chain_id', 'INT'), - ('transform_id', 'INT') + ("rotation", "QUATERNION"), + ("assembly_id", "INT"), + ("chain_id", "INT"), + ("transform_id", "INT"), ] for column, type in attributes: @@ -381,7 +407,6 @@ def create_data_object( if np.issubdtype(data.dtype, str): data = np.unique(data, return_inverse=True)[1] - set_attribute(object, name=column, data=data, - type=type, domain='POINT') + set_attribute(object, name=column, data=data, type=type, domain="POINT") return object diff --git a/molecularnodes/color.py b/molecularnodes/color.py index 2eacf510..1de71eba 100644 --- a/molecularnodes/color.py +++ b/molecularnodes/color.py @@ -1,28 +1,33 @@ import random import colorsys import numpy as np +import numpy.typing as npt +from numpy.typing import NDArray +from typing import List, Dict, Tuple, Any, Iterable -def random_rgb(seed=None): - """Random Pastel RGB values - """ - if seed: - random.seed(seed) +def random_rgb(seed: int = 6) -> NDArray[np.float64]: + """Random Pastel RGB values""" + random.seed(seed) r, g, b = colorsys.hls_to_rgb(random.random(), 0.6, 0.6) - return np.array((r, g, b, 1)) + return np.array((r, g, b, 1.0)) -def color_from_atomic_number(atomic_number: int): +def color_from_atomic_number(atomic_number: int) -> Tuple[int, int, int, int]: r, g, b = list(iupac_colors_rgb.values())[int(atomic_number - 1)] - return np.array((r, g, b, 1)) + return (r, g, b, 1) -def colors_from_elements(atomic_numbers): - colors = np.array(list(map(color_from_atomic_number, atomic_numbers))) +def colors_from_elements( + atomic_numbers: Iterable, +) -> NDArray[np.float64]: + colors = np.array([color_from_atomic_number(x) for x in atomic_numbers]) return colors -def equidistant_colors(some_list): +def equidistant_colors( + some_list: NDArray[np.character], +) -> Dict[str, List[Tuple[int, int, int, int]]]: u = np.unique(some_list) num_colors = len(u) @@ -32,26 +37,27 @@ def equidistant_colors(some_list): colors = [colorsys.hls_to_rgb(hue, 0.6, 0.6) for hue in hues] # Convert RGB to 8-bit integer values - colors = [(int(r * 255), int(g * 255), int(b * 255), 1) - for (r, g, b) in colors] + colors = [ + (int(r * 255), int(g * 255), int(b * 255), int(1)) # type: ignore + for (r, g, b) in colors + ] - return dict(zip(u, colors)) + return dict(zip(u, colors)) # type: ignore -def color_chains_equidistant(chain_ids): +def color_chains_equidistant( + chain_ids: NDArray[Any], +) -> NDArray[np.float32]: color_dict = equidistant_colors(chain_ids) chain_colors = np.array([color_dict[x] for x in chain_ids]) return chain_colors / 255 -def color_chains(atomic_numbers, chain_ids): +def color_chains(atomic_numbers: NDArray[np.int32], chain_ids: NDArray[np.character]) -> NDArray[np.float32]: mask = atomic_numbers == 6 colors = colors_from_elements(atomic_numbers) chain_color_dict = equidistant_colors(chain_ids) - chain_colors = np.array(list(map( - lambda x: chain_color_dict.get(x), - chain_ids - ))) + chain_colors = np.array([chain_color_dict.get(x) for x in chain_ids]) colors[mask] = chain_colors[mask] @@ -59,116 +65,116 @@ def color_chains(atomic_numbers, chain_ids): iupac_colors_rgb = { - "H": (255, 255, 255), # Hydrogen + "H": (255, 255, 255), # Hydrogen "He": (217, 255, 255), # Helium "Li": (204, 128, 255), # Lithium - "Be": (194, 255, 0), # Beryllium - "B": (255, 181, 181), # Boron - "C": (144, 144, 144), # Carbon - "N": (48, 80, 248), # Nitrogen - "O": (255, 13, 13), # Oxygen - "F": (144, 224, 80), # Fluorine + "Be": (194, 255, 0), # Beryllium + "B": (255, 181, 181), # Boron + "C": (144, 144, 144), # Carbon + "N": (48, 80, 248), # Nitrogen + "O": (255, 13, 13), # Oxygen + "F": (144, 224, 80), # Fluorine "Ne": (179, 227, 245), # Neon - "Na": (171, 92, 242), # Sodium - "Mg": (138, 255, 0), # Magnesium + "Na": (171, 92, 242), # Sodium + "Mg": (138, 255, 0), # Magnesium "Al": (191, 166, 166), # Aluminum "Si": (240, 200, 160), # Silicon - "P": (255, 128, 0), # Phosphorus - "S": (255, 255, 48), # Sulfur - "Cl": (31, 240, 31), # Chlorine - "K": (143, 64, 212), # Potassium + "P": (255, 128, 0), # Phosphorus + "S": (255, 255, 48), # Sulfur + "Cl": (31, 240, 31), # Chlorine + "K": (143, 64, 212), # Potassium "Ar": (128, 209, 227), # Argon - "Ca": (61, 255, 0), # Calcium + "Ca": (61, 255, 0), # Calcium "Sc": (230, 230, 230), # Scandium "Ti": (191, 194, 199), # Titanium - "V": (166, 166, 171), # Vanadium + "V": (166, 166, 171), # Vanadium "Cr": (138, 153, 199), # Chromium "Mn": (156, 122, 199), # Manganese - "Fe": (224, 102, 51), # Iron + "Fe": (224, 102, 51), # Iron "Ni": (199, 138, 138), # Nickel "Co": (255, 217, 143), # Cobalt - "Cu": (200, 128, 51), # Copper + "Cu": (200, 128, 51), # Copper "Zn": (125, 128, 176), # Zinc "Ga": (194, 143, 143), # Gallium "Ge": (102, 143, 143), # Germanium "As": (189, 128, 227), # Arsenic - "Se": (255, 161, 0), # Selenium - "Br": (166, 41, 41), # Bromine - "Kr": (92, 184, 209), # Krypton - "Rb": (112, 46, 176), # Rubidium - "Sr": (0, 255, 0), # Strontium - "Y": (148, 255, 255), # Yttrium - "Zr": (148, 224, 224), # Zirconium - "Nb": (115, 194, 201), # Niobium - "Mo": (84, 181, 181), # Molybdenum - "Tc": (59, 158, 158), # Technetium - "Ru": (36, 125, 125), # Ruthenium - "Rh": (10, 125, 140), # Rhodium - "Pd": (0, 105, 133), # Palladium + "Se": (255, 161, 0), # Selenium + "Br": (166, 41, 41), # Bromine + "Kr": (92, 184, 209), # Krypton + "Rb": (112, 46, 176), # Rubidium + "Sr": (0, 255, 0), # Strontium + "Y": (148, 255, 255), # Yttrium + "Zr": (148, 224, 224), # Zirconium + "Nb": (115, 194, 201), # Niobium + "Mo": (84, 181, 181), # Molybdenum + "Tc": (59, 158, 158), # Technetium + "Ru": (36, 125, 125), # Ruthenium + "Rh": (10, 125, 140), # Rhodium + "Pd": (0, 105, 133), # Palladium "Ag": (192, 192, 192), # Silver "Cd": (255, 217, 143), # Cadmium "In": (166, 117, 115), # Indium "Sn": (102, 128, 128), # Tin - "Sb": (158, 99, 181), # Antimony - "Te": (212, 122, 0), # Tellurium - "I": (148, 0, 148), # Iodine - "Xe": (66, 158, 176), # Xenon - "Cs": (87, 23, 143), # Cesium - "Ba": (0, 201, 0), # Barium + "Sb": (158, 99, 181), # Antimony + "Te": (212, 122, 0), # Tellurium + "I": (148, 0, 148), # Iodine + "Xe": (66, 158, 176), # Xenon + "Cs": (87, 23, 143), # Cesium + "Ba": (0, 201, 0), # Barium "La": (112, 212, 255), # Lanthanum "Ce": (255, 255, 199), # Cerium "Pr": (217, 255, 199), # Praseodymium "Nd": (199, 255, 199), # Neodymium "Pm": (163, 255, 199), # Promethium "Sm": (143, 255, 199), # Samarium - "Eu": (97, 255, 199), # Europium - "Gd": (69, 255, 199), # Gadolinium - "Tb": (48, 255, 199), # Terbium - "Dy": (31, 255, 199), # Dysprosium - "Ho": (0, 255, 156), # Holmium - "Er": (0, 230, 117), # Erbium - "Tm": (0, 212, 82), # Thulium - "Yb": (0, 191, 56), # Ytterbium - "Lu": (0, 171, 36), # Lutetium - "Hf": (77, 194, 255), # Hafnium - "Ta": (77, 166, 255), # Tantalum - "W": (33, 148, 214), # Tungsten - "Re": (38, 125, 171), # Rhenium - "Os": (38, 102, 150), # Osmium - "Ir": (23, 84, 135), # Iridium + "Eu": (97, 255, 199), # Europium + "Gd": (69, 255, 199), # Gadolinium + "Tb": (48, 255, 199), # Terbium + "Dy": (31, 255, 199), # Dysprosium + "Ho": (0, 255, 156), # Holmium + "Er": (0, 230, 117), # Erbium + "Tm": (0, 212, 82), # Thulium + "Yb": (0, 191, 56), # Ytterbium + "Lu": (0, 171, 36), # Lutetium + "Hf": (77, 194, 255), # Hafnium + "Ta": (77, 166, 255), # Tantalum + "W": (33, 148, 214), # Tungsten + "Re": (38, 125, 171), # Rhenium + "Os": (38, 102, 150), # Osmium + "Ir": (23, 84, 135), # Iridium "Pt": (208, 208, 224), # Platinum - "Au": (255, 209, 35), # Gold + "Au": (255, 209, 35), # Gold "Hg": (184, 184, 208), # Mercury - "Tl": (166, 84, 77), # Thallium - "Pb": (87, 89, 97), # Lead - "Bi": (158, 79, 181), # Bismuth - "Th": (255, 161, 0), # Thorium - "Pa": (255, 161, 0), # Protactinium - "U": (255, 161, 0), # Uranium - "Np": (255, 161, 0), # Neptunium - "Pu": (255, 161, 0), # Plutonium - "Am": (255, 161, 0), # Americium - "Cm": (255, 161, 0), # Curium - "Bk": (255, 161, 0), # Berkelium - "Cf": (255, 161, 0), # Californium - "Es": (255, 161, 0), # Einsteinium - "Fm": (255, 161, 0), # Fermium - "Md": (255, 161, 0), # Mendelevium - "No": (255, 161, 0), # Nobelium - "Lr": (255, 161, 0), # Lawrencium - "Rf": (204, 0, 89), # Rutherfordium - "Db": (209, 0, 79), # Dubnium - "Sg": (217, 0, 69), # Seaborgium - "Bh": (224, 0, 56), # Bohrium - "Hs": (230, 0, 46), # Hassium - "Mt": (235, 0, 38), # Meitnerium - "Ds": (240, 0, 33), # Darmstadtium - "Rg": (241, 0, 30), # Roentgenium - "Cn": (242, 0, 26), # Copernicium - "Nh": (242, 0, 26), # Nihonium - "Fl": (242, 0, 26), # Flerovium - "Mc": (242, 0, 26), # Moscovium - "Lv": (242, 0, 26), # Livermorium - "Ts": (242, 0, 26), # Tennessine - "Og": (242, 0, 26) # Oganesson + "Tl": (166, 84, 77), # Thallium + "Pb": (87, 89, 97), # Lead + "Bi": (158, 79, 181), # Bismuth + "Th": (255, 161, 0), # Thorium + "Pa": (255, 161, 0), # Protactinium + "U": (255, 161, 0), # Uranium + "Np": (255, 161, 0), # Neptunium + "Pu": (255, 161, 0), # Plutonium + "Am": (255, 161, 0), # Americium + "Cm": (255, 161, 0), # Curium + "Bk": (255, 161, 0), # Berkelium + "Cf": (255, 161, 0), # Californium + "Es": (255, 161, 0), # Einsteinium + "Fm": (255, 161, 0), # Fermium + "Md": (255, 161, 0), # Mendelevium + "No": (255, 161, 0), # Nobelium + "Lr": (255, 161, 0), # Lawrencium + "Rf": (204, 0, 89), # Rutherfordium + "Db": (209, 0, 79), # Dubnium + "Sg": (217, 0, 69), # Seaborgium + "Bh": (224, 0, 56), # Bohrium + "Hs": (230, 0, 46), # Hassium + "Mt": (235, 0, 38), # Meitnerium + "Ds": (240, 0, 33), # Darmstadtium + "Rg": (241, 0, 30), # Roentgenium + "Cn": (242, 0, 26), # Copernicium + "Nh": (242, 0, 26), # Nihonium + "Fl": (242, 0, 26), # Flerovium + "Mc": (242, 0, 26), # Moscovium + "Lv": (242, 0, 26), # Livermorium + "Ts": (242, 0, 26), # Tennessine + "Og": (242, 0, 26), # Oganesson } diff --git a/molecularnodes/data.py b/molecularnodes/data.py index cf361218..f54ebcb1 100644 --- a/molecularnodes/data.py +++ b/molecularnodes/data.py @@ -4,15 +4,15 @@ # of an element to its atomic number etc -# elements dictionary format: +# elements dictionary format: # keys are the element symbol for elements up to Lawrencium (a bit overkill) # values are a subdictionary filled with keys detailing element information -# keys in subdicts: 'atomic_number', 'name', 'vdw_radii', and +# keys in subdicts: 'atomic_number', 'name', 'vdw_radii', and # 'standard_mass' # vdw_radii given in picometres # atomic masses given in daltons -# -# this dict is used to convert element symbol to atomic number, vdw_radii, and +# +# this dict is used to convert element symbol to atomic number, vdw_radii, and # mass values elements = { @@ -20,1227 +20,758 @@ "atomic_number": 1, "vdw_radii": 120, "name": "Hydrogen", - "standard_mass": 1.00794 + "standard_mass": 1.00794, }, "He": { "atomic_number": 2, "vdw_radii": 140, "name": "Helium", - "standard_mass": 4.002602 + "standard_mass": 4.002602, }, "Li": { "atomic_number": 3, "vdw_radii": 182, "name": "Lithium", - "standard_mass": 6.941 + "standard_mass": 6.941, }, "Be": { "atomic_number": 4, "vdw_radii": 153, "name": "Beryllium", - "standard_mass": 9.012182 + "standard_mass": 9.012182, }, "B": { "atomic_number": 5, "vdw_radii": 192, "name": "Boron", - "standard_mass": 10.811 + "standard_mass": 10.811, }, "C": { "atomic_number": 6, "vdw_radii": 170, "name": "Carbon", - "standard_mass": 12.0107 + "standard_mass": 12.0107, }, "N": { "atomic_number": 7, "vdw_radii": 155, "name": "Nitrogen", - "standard_mass": 14.0067 + "standard_mass": 14.0067, }, "O": { "atomic_number": 8, "vdw_radii": 152, "name": "Oxygen", - "standard_mass": 15.9994 + "standard_mass": 15.9994, }, "F": { "atomic_number": 9, "vdw_radii": 147, "name": "Fluorine", - "standard_mass": 18.9984032 + "standard_mass": 18.9984032, }, "Ne": { "atomic_number": 10, "vdw_radii": 154, "name": "Neon", - "standard_mass": 20.1797 + "standard_mass": 20.1797, }, "Na": { "atomic_number": 11, "vdw_radii": 227, "name": "Sodium", - "standard_mass": 22.98977 + "standard_mass": 22.98977, }, "Mg": { "atomic_number": 12, "vdw_radii": 173, "name": "Magnesium", - "standard_mass": 24.305 + "standard_mass": 24.305, }, "Al": { "atomic_number": 13, "vdw_radii": 184, "name": "Aluminium", - "standard_mass": 26.981538 + "standard_mass": 26.981538, }, "Si": { "atomic_number": 14, "vdw_radii": 210, "name": "Silicon", - "standard_mass": 28.0855 + "standard_mass": 28.0855, }, "P": { "atomic_number": 15, "vdw_radii": 180, "name": "Phosphorus", - "standard_mass": 30.973761 + "standard_mass": 30.973761, }, "S": { "atomic_number": 16, "vdw_radii": 180, "name": "Sulfur", - "standard_mass": 32.065 + "standard_mass": 32.065, }, "Cl": { "atomic_number": 17, "vdw_radii": 175, "name": "Chlorine", - "standard_mass": 35.453 + "standard_mass": 35.453, }, "Ar": { "atomic_number": 18, "vdw_radii": 188, "name": "Argon", - "standard_mass": 39.948 + "standard_mass": 39.948, }, "K": { "atomic_number": 19, "vdw_radii": 275, "name": "Potassium", - "standard_mass": 39.0983 + "standard_mass": 39.0983, }, "Ca": { "atomic_number": 20, "vdw_radii": 231, "name": "Calcium", - "standard_mass": 40.078 + "standard_mass": 40.078, }, "Sc": { "atomic_number": 21, "vdw_radii": 211, "name": "Scandium", - "standard_mass": 44.95591 + "standard_mass": 44.95591, }, "Ti": { "atomic_number": 22, "vdw_radii": 147, "name": "Titanium", - "standard_mass": 47.867 + "standard_mass": 47.867, }, "V": { "atomic_number": 23, "vdw_radii": 134, "name": "Vanadium", - "standard_mass": 50.9415 + "standard_mass": 50.9415, }, "Cr": { "atomic_number": 24, "vdw_radii": 128, "name": "Chromium", - "standard_mass": 51.9961 + "standard_mass": 51.9961, }, "Mn": { "atomic_number": 25, "vdw_radii": 127, "name": "Manganese", - "standard_mass": 54.938049 + "standard_mass": 54.938049, }, "Fe": { "atomic_number": 26, "vdw_radii": 126, "name": "Iron", - "standard_mass": 55.845 + "standard_mass": 55.845, }, "Co": { "atomic_number": 27, "vdw_radii": 125, "name": "Cobalt", - "standard_mass": 58.9332 + "standard_mass": 58.9332, }, "Ni": { "atomic_number": 28, "vdw_radii": 163, "name": "Nickel", - "standard_mass": 58.6934 + "standard_mass": 58.6934, }, "Cu": { "atomic_number": 29, "vdw_radii": 140, "name": "Copper", - "standard_mass": 63.546 + "standard_mass": 63.546, }, "Zn": { "atomic_number": 30, "vdw_radii": 139, "name": "Zinc", - "standard_mass": 65.409 + "standard_mass": 65.409, }, "Ga": { "atomic_number": 31, "vdw_radii": 187, "name": "Gallium", - "standard_mass": 69.723 + "standard_mass": 69.723, }, "Ge": { "atomic_number": 32, "vdw_radii": 211, "name": "Germanium", - "standard_mass": 72.64 + "standard_mass": 72.64, }, "As": { "atomic_number": 33, "vdw_radii": 185, "name": "Arsenic", - "standard_mass": 74.9216 + "standard_mass": 74.9216, }, "Se": { "atomic_number": 34, "vdw_radii": 190, "name": "Selenium", - "standard_mass": 78.96 + "standard_mass": 78.96, }, "Br": { "atomic_number": 35, "vdw_radii": 185, "name": "Bromine", - "standard_mass": 79.904 + "standard_mass": 79.904, }, "Kr": { "atomic_number": 36, "vdw_radii": 202, "name": "Krypton", - "standard_mass": 83.798 + "standard_mass": 83.798, }, "Rb": { "atomic_number": 37, "vdw_radii": 303, "name": "Rubidium", - "standard_mass": 85.4678 + "standard_mass": 85.4678, }, "Sr": { "atomic_number": 38, "vdw_radii": 249, "name": "Strontium", - "standard_mass": 87.62 + "standard_mass": 87.62, }, "Y": { "atomic_number": 39, "vdw_radii": 180, "name": "Yttrium", - "standard_mass": 88.90585 + "standard_mass": 88.90585, }, "Zr": { "atomic_number": 40, "vdw_radii": 160, "name": "Zirconium", - "standard_mass": 91.224 + "standard_mass": 91.224, }, "Nb": { "atomic_number": 41, "vdw_radii": 146, "name": "Niobium", - "standard_mass": 92.90638 + "standard_mass": 92.90638, }, "Mo": { "atomic_number": 42, "vdw_radii": 239, "name": "Molybdenum", - "standard_mass": 95.94 + "standard_mass": 95.94, }, "Tc": { "atomic_number": 43, "vdw_radii": 136, "name": "Technetium", - "standard_mass": 98.9062 + "standard_mass": 98.9062, }, "Ru": { "atomic_number": 44, "vdw_radii": 134, "name": "Ruthenium", - "standard_mass": 101.07 + "standard_mass": 101.07, }, "Rh": { "atomic_number": 45, "vdw_radii": 137, "name": "Rhodium", - "standard_mass": 102.9055 + "standard_mass": 102.9055, }, "Pd": { "atomic_number": 46, "vdw_radii": 144, "name": "Palladium", - "standard_mass": 106.42 - }, - "Ag": { - "atomic_number": 47, - "name": "Silver", - "standard_mass": 107.8682 - }, - "Cd": { - "atomic_number": 48, - "name": "Cadmium", - "standard_mass": 112.411 - }, - "In": { - "atomic_number": 49, - "name": "Indium", - "standard_mass": 114.818 - }, - "Sn": { - "atomic_number": 50, - "name": "Tin", - "standard_mass": 118.71 - }, - "Sb": { - "atomic_number": 51, - "name": "Antimony", - "standard_mass": 121.76 - }, - "Te": { - "atomic_number": 52, - "name": "Tellurium", - "standard_mass": 127.6 - }, - "I": { - "atomic_number": 53, - "name": "Iodine", - "standard_mass": 126.90447 - }, - "Xe": { - "atomic_number": 54, - "name": "Xenon", - "standard_mass": 131.293 - }, - "Cs": { - "atomic_number": 55, - "name": "Caesium", - "standard_mass": 132.90545 - }, - "Ba": { - "atomic_number": 56, - "name": "Barium", - "standard_mass": 137.327 - }, - "La": { - "atomic_number": 57, - "name": "Lanthanum", - "standard_mass": 138.9055 - }, - "Ce": { - "atomic_number": 58, - "name": "Cerium", - "standard_mass": 140.116 - }, - "Pr": { - "atomic_number": 59, - "name": "Praseodymium", - "standard_mass": 140.90765 - }, - "Nd": { - "atomic_number": 60, - "name": "Neodymium", - "standard_mass": 144.24 - }, - "Pm": { - "atomic_number": 61, - "name": "Promethium", - "standard_mass": 145 - }, - "Sm": { - "atomic_number": 62, - "name": "Samarium", - "standard_mass": 150.36 - }, - "Eu": { - "atomic_number": 63, - "name": "Europium", - "standard_mass": 151.964 - }, - "Gd": { - "atomic_number": 64, - "name": "Gadolinium", - "standard_mass": 157.25 - }, - "Tb": { - "atomic_number": 65, - "name": "Terbium", - "standard_mass": 158.92534 - }, - "Dy": { - "atomic_number": 66, - "name": "Dysprosium", - "standard_mass": 162.5 - }, - "Ho": { - "atomic_number": 67, - "name": "Holmium", - "standard_mass": 164.93032 - }, - "Er": { - "atomic_number": 68, - "name": "Erbium", - "standard_mass": 167.259 - }, - "Tm": { - "atomic_number": 69, - "name": "Thulium", - "standard_mass": 168.93421 - }, - "Yb": { - "atomic_number": 70, - "name": "Ytterbium", - "standard_mass": 173.04 - }, - "Lu": { - "atomic_number": 71, - "name": "Lutetium", - "standard_mass": 174.967 - }, - "Hf": { - "atomic_number": 72, - "name": "Hafnium", - "standard_mass": 178.49 - }, - "Ta": { - "atomic_number": 73, - "name": "Tantalum", - "standard_mass": 180.9479 - }, - "W": { - "atomic_number": 74, - "name": "Tungsten", - "standard_mass": 183.84 - }, - "Re": { - "atomic_number": 75, - "name": "Rhenium", - "standard_mass": 186.207 - }, - "Os": { - "atomic_number": 76, - "name": "Osmium", - "standard_mass": 190.23 - }, - "Ir": { - "atomic_number": 77, - "name": "Iridium", - "standard_mass": 192.217 - }, - "Pt": { - "atomic_number": 78, - "name": "Platinum", - "standard_mass": 195.078 - }, - "Au": { - "atomic_number": 79, - "name": "Gold", - "standard_mass": 196.96655 - }, - "Hg": { - "atomic_number": 80, - "name": "Mercury", - "standard_mass": 200.59 - }, - "Tl": { - "atomic_number": 81, - "name": "Thallium", - "standard_mass": 204.3833 - }, - "Pb": { - "atomic_number": 82, - "name": "Lead", - "standard_mass": 207.2 - }, - "Bi": { - "atomic_number": 83, - "name": "Bismuth", - "standard_mass": 208.98038 - }, - "Po": { - "atomic_number": 84, - "name": "Polonium", - "standard_mass": 209 - }, - "At": { - "atomic_number": 85, - "name": "Astatine", - "standard_mass": 210 - }, - "Rn": { - "atomic_number": 86, - "name": "Radon", - "standard_mass": 222 - }, - "Fr": { - "atomic_number": 87, - "name": "Francium", - "standard_mass": 223 - }, - "Ra": { - "atomic_number": 88, - "name": "Radium", - "standard_mass": 226.0254 - }, - "Ac": { - "atomic_number": 89, - "name": "Actinium", - "standard_mass": 227 - }, - "Th": { - "atomic_number": 90, - "name": "Thorium", - "standard_mass": 232.0381 - }, - "Pa": { - "atomic_number": 91, - "name": "Protactinium", - "standard_mass": 231.03588 + "standard_mass": 106.42, }, - "U": { - "atomic_number": 92, - "name": "Uranium", - "standard_mass": 238.02891 - }, - "Np": { - "atomic_number": 93, - "name": "Neptunium", - "standard_mass": 237.0482 - }, - "Pu": { - "atomic_number": 94, - "name": "Plutonium", - "standard_mass": 244 - }, - "Am": { - "atomic_number": 95, - "name": "Americium", - "standard_mass": 243 - }, - "Cm": { - "atomic_number": 96, - "name": "Curium", - "standard_mass": 243 - }, - "Bk": { - "atomic_number": 97, - "name": "Berkelium", - "standard_mass": 247 - }, - "Cf": { - "atomic_number": 98, - "name": "Californium", - "standard_mass": 251 - }, - "Es": { - "atomic_number": 99, - "name": "Einsteinium", - "standard_mass": 254 - }, - "Fm": { - "atomic_number": 100, - "name": "Fermium", - "standard_mass": 254 - }, - "Md": { - "atomic_number": 101, - "name": "Mendelevium", - "standard_mass": 258 - }, - "No": { - "atomic_number": 102, - "name": "Nobelium", - "standard_mass": 259 - }, - "Lr": { - "atomic_number": 103, - "name": "Lawrencium", - "standard_mass": 262 - }, - "X": { - "atomic_number": -1, - "name": "Unknown", - "standard_mass": 0 - } + "Ag": {"atomic_number": 47, "name": "Silver", "standard_mass": 107.8682}, + "Cd": {"atomic_number": 48, "name": "Cadmium", "standard_mass": 112.411}, + "In": {"atomic_number": 49, "name": "Indium", "standard_mass": 114.818}, + "Sn": {"atomic_number": 50, "name": "Tin", "standard_mass": 118.71}, + "Sb": {"atomic_number": 51, "name": "Antimony", "standard_mass": 121.76}, + "Te": {"atomic_number": 52, "name": "Tellurium", "standard_mass": 127.6}, + "I": {"atomic_number": 53, "name": "Iodine", "standard_mass": 126.90447}, + "Xe": {"atomic_number": 54, "name": "Xenon", "standard_mass": 131.293}, + "Cs": {"atomic_number": 55, "name": "Caesium", "standard_mass": 132.90545}, + "Ba": {"atomic_number": 56, "name": "Barium", "standard_mass": 137.327}, + "La": {"atomic_number": 57, "name": "Lanthanum", "standard_mass": 138.9055}, + "Ce": {"atomic_number": 58, "name": "Cerium", "standard_mass": 140.116}, + "Pr": {"atomic_number": 59, "name": "Praseodymium", "standard_mass": 140.90765}, + "Nd": {"atomic_number": 60, "name": "Neodymium", "standard_mass": 144.24}, + "Pm": {"atomic_number": 61, "name": "Promethium", "standard_mass": 145}, + "Sm": {"atomic_number": 62, "name": "Samarium", "standard_mass": 150.36}, + "Eu": {"atomic_number": 63, "name": "Europium", "standard_mass": 151.964}, + "Gd": {"atomic_number": 64, "name": "Gadolinium", "standard_mass": 157.25}, + "Tb": {"atomic_number": 65, "name": "Terbium", "standard_mass": 158.92534}, + "Dy": {"atomic_number": 66, "name": "Dysprosium", "standard_mass": 162.5}, + "Ho": {"atomic_number": 67, "name": "Holmium", "standard_mass": 164.93032}, + "Er": {"atomic_number": 68, "name": "Erbium", "standard_mass": 167.259}, + "Tm": {"atomic_number": 69, "name": "Thulium", "standard_mass": 168.93421}, + "Yb": {"atomic_number": 70, "name": "Ytterbium", "standard_mass": 173.04}, + "Lu": {"atomic_number": 71, "name": "Lutetium", "standard_mass": 174.967}, + "Hf": {"atomic_number": 72, "name": "Hafnium", "standard_mass": 178.49}, + "Ta": {"atomic_number": 73, "name": "Tantalum", "standard_mass": 180.9479}, + "W": {"atomic_number": 74, "name": "Tungsten", "standard_mass": 183.84}, + "Re": {"atomic_number": 75, "name": "Rhenium", "standard_mass": 186.207}, + "Os": {"atomic_number": 76, "name": "Osmium", "standard_mass": 190.23}, + "Ir": {"atomic_number": 77, "name": "Iridium", "standard_mass": 192.217}, + "Pt": {"atomic_number": 78, "name": "Platinum", "standard_mass": 195.078}, + "Au": {"atomic_number": 79, "name": "Gold", "standard_mass": 196.96655}, + "Hg": {"atomic_number": 80, "name": "Mercury", "standard_mass": 200.59}, + "Tl": {"atomic_number": 81, "name": "Thallium", "standard_mass": 204.3833}, + "Pb": {"atomic_number": 82, "name": "Lead", "standard_mass": 207.2}, + "Bi": {"atomic_number": 83, "name": "Bismuth", "standard_mass": 208.98038}, + "Po": {"atomic_number": 84, "name": "Polonium", "standard_mass": 209}, + "At": {"atomic_number": 85, "name": "Astatine", "standard_mass": 210}, + "Rn": {"atomic_number": 86, "name": "Radon", "standard_mass": 222}, + "Fr": {"atomic_number": 87, "name": "Francium", "standard_mass": 223}, + "Ra": {"atomic_number": 88, "name": "Radium", "standard_mass": 226.0254}, + "Ac": {"atomic_number": 89, "name": "Actinium", "standard_mass": 227}, + "Th": {"atomic_number": 90, "name": "Thorium", "standard_mass": 232.0381}, + "Pa": {"atomic_number": 91, "name": "Protactinium", "standard_mass": 231.03588}, + "U": {"atomic_number": 92, "name": "Uranium", "standard_mass": 238.02891}, + "Np": {"atomic_number": 93, "name": "Neptunium", "standard_mass": 237.0482}, + "Pu": {"atomic_number": 94, "name": "Plutonium", "standard_mass": 244}, + "Am": {"atomic_number": 95, "name": "Americium", "standard_mass": 243}, + "Cm": {"atomic_number": 96, "name": "Curium", "standard_mass": 243}, + "Bk": {"atomic_number": 97, "name": "Berkelium", "standard_mass": 247}, + "Cf": {"atomic_number": 98, "name": "Californium", "standard_mass": 251}, + "Es": {"atomic_number": 99, "name": "Einsteinium", "standard_mass": 254}, + "Fm": {"atomic_number": 100, "name": "Fermium", "standard_mass": 254}, + "Md": {"atomic_number": 101, "name": "Mendelevium", "standard_mass": 258}, + "No": {"atomic_number": 102, "name": "Nobelium", "standard_mass": 259}, + "Lr": {"atomic_number": 103, "name": "Lawrencium", "standard_mass": 262}, + "X": {"atomic_number": -1, "name": "Unknown", "standard_mass": 0}, } -# elements_by_atomic_number dictionary format: -# keys are integers of the element's atomic number for elements up to +# elements_by_atomic_number dictionary format: +# keys are integers of the element's atomic number for elements up to # Lawrencium (a bit overkill) # values are a subdictionary filled with keys detailing element information -# keys in subdicts: 'element_symbol', 'name', 'standard_mass', and +# keys in subdicts: 'element_symbol', 'name', 'standard_mass', and # 'vdw_radii' # vdw_radii given in picometres # atomic masses given in daltons -# -# this dict is used to back-convert atomic numbers to element symbols, +# +# this dict is used to back-convert atomic numbers to element symbols, # vdw_radii, and mass values elements_by_atomic_number = { - -1: { - "element_symbol": "X", - "name": "Unknown" - }, + -1: {"element_symbol": "X", "name": "Unknown"}, 1: { "element_symbol": "H", "name": "Hydrogen", "standard_mass": 1.00794, - "vdw_radii": 120 + "vdw_radii": 120, }, 2: { "element_symbol": "He", "name": "Helium", "standard_mass": 4.002602, - "vdw_radii": 140 + "vdw_radii": 140, }, 3: { "element_symbol": "Li", "name": "Lithium", "standard_mass": 6.941, - "vdw_radii": 182 + "vdw_radii": 182, }, 4: { "element_symbol": "Be", "name": "Beryllium", "standard_mass": 9.012182, - "vdw_radii": 153 + "vdw_radii": 153, }, 5: { "element_symbol": "B", "name": "Boron", "standard_mass": 10.811, - "vdw_radii": 192 + "vdw_radii": 192, }, 6: { "element_symbol": "C", "name": "Carbon", "standard_mass": 12.0107, - "vdw_radii": 170 + "vdw_radii": 170, }, 7: { "element_symbol": "N", "name": "Nitrogen", "standard_mass": 14.0067, - "vdw_radii": 155 + "vdw_radii": 155, }, 8: { "element_symbol": "O", "name": "Oxygen", "standard_mass": 15.9994, - "vdw_radii": 152 + "vdw_radii": 152, }, 9: { "element_symbol": "F", "name": "Fluorine", "standard_mass": 18.9984032, - "vdw_radii": 147 + "vdw_radii": 147, }, 10: { "element_symbol": "Ne", "name": "Neon", "standard_mass": 20.1797, - "vdw_radii": 154 + "vdw_radii": 154, }, 11: { "element_symbol": "Na", "name": "Sodium", "standard_mass": 22.98977, - "vdw_radii": 227 + "vdw_radii": 227, }, 12: { "element_symbol": "Mg", "name": "Magnesium", "standard_mass": 24.305, - "vdw_radii": 173 + "vdw_radii": 173, }, 13: { "element_symbol": "Al", "name": "Aluminium", "standard_mass": 26.981538, - "vdw_radii": 184 + "vdw_radii": 184, }, 14: { "element_symbol": "Si", "name": "Silicon", "standard_mass": 28.0855, - "vdw_radii": 210 + "vdw_radii": 210, }, 15: { "element_symbol": "P", "name": "Phosphorus", "standard_mass": 30.973761, - "vdw_radii": 180 + "vdw_radii": 180, }, 16: { "element_symbol": "S", "name": "Sulfur", "standard_mass": 32.065, - "vdw_radii": 180 + "vdw_radii": 180, }, 17: { "element_symbol": "Cl", "name": "Chlorine", "standard_mass": 35.453, - "vdw_radii": 175 + "vdw_radii": 175, }, 18: { "element_symbol": "Ar", "name": "Argon", "standard_mass": 39.948, - "vdw_radii": 188 + "vdw_radii": 188, }, 19: { "element_symbol": "K", "name": "Potassium", "standard_mass": 39.0983, - "vdw_radii": 275 + "vdw_radii": 275, }, 20: { "element_symbol": "Ca", "name": "Calcium", "standard_mass": 40.078, - "vdw_radii": 231 + "vdw_radii": 231, }, 21: { "element_symbol": "Sc", "name": "Scandium", "standard_mass": 44.95591, - "vdw_radii": 211 + "vdw_radii": 211, }, 22: { "element_symbol": "Ti", "name": "Titanium", "standard_mass": 47.867, - "vdw_radii": 147 + "vdw_radii": 147, }, 23: { "element_symbol": "V", "name": "Vanadium", "standard_mass": 50.9415, - "vdw_radii": 134 + "vdw_radii": 134, }, 24: { "element_symbol": "Cr", "name": "Chromium", "standard_mass": 51.9961, - "vdw_radii": 128 + "vdw_radii": 128, }, 25: { "element_symbol": "Mn", "name": "Manganese", "standard_mass": 54.938049, - "vdw_radii": 127 + "vdw_radii": 127, }, 26: { "element_symbol": "Fe", "name": "Iron", "standard_mass": 55.845, - "vdw_radii": 126 + "vdw_radii": 126, }, 27: { "element_symbol": "Co", "name": "Cobalt", "standard_mass": 58.9332, - "vdw_radii": 125 + "vdw_radii": 125, }, 28: { "element_symbol": "Ni", "name": "Nickel", "standard_mass": 58.6934, - "vdw_radii": 163 + "vdw_radii": 163, }, 29: { "element_symbol": "Cu", "name": "Copper", "standard_mass": 63.546, - "vdw_radii": 140 + "vdw_radii": 140, }, 30: { "element_symbol": "Zn", "name": "Zinc", "standard_mass": 65.409, - "vdw_radii": 139 + "vdw_radii": 139, }, 31: { "element_symbol": "Ga", "name": "Gallium", "standard_mass": 69.723, - "vdw_radii": 187 + "vdw_radii": 187, }, 32: { "element_symbol": "Ge", "name": "Germanium", "standard_mass": 72.64, - "vdw_radii": 211 + "vdw_radii": 211, }, 33: { "element_symbol": "As", "name": "Arsenic", "standard_mass": 74.9216, - "vdw_radii": 185 + "vdw_radii": 185, }, 34: { "element_symbol": "Se", "name": "Selenium", "standard_mass": 78.96, - "vdw_radii": 190 + "vdw_radii": 190, }, 35: { "element_symbol": "Br", "name": "Bromine", "standard_mass": 79.904, - "vdw_radii": 185 + "vdw_radii": 185, }, 36: { "element_symbol": "Kr", "name": "Krypton", "standard_mass": 83.798, - "vdw_radii": 202 + "vdw_radii": 202, }, 37: { "element_symbol": "Rb", "name": "Rubidium", "standard_mass": 85.4678, - "vdw_radii": 303 + "vdw_radii": 303, }, 38: { "element_symbol": "Sr", "name": "Strontium", "standard_mass": 87.62, - "vdw_radii": 249 + "vdw_radii": 249, }, 39: { "element_symbol": "Y", "name": "Yttrium", "standard_mass": 88.90585, - "vdw_radii": 180 + "vdw_radii": 180, }, 40: { "element_symbol": "Zr", "name": "Zirconium", "standard_mass": 91.224, - "vdw_radii": 160 + "vdw_radii": 160, }, 41: { "element_symbol": "Nb", "name": "Niobium", "standard_mass": 92.90638, - "vdw_radii": 146 + "vdw_radii": 146, }, 42: { "element_symbol": "Mo", "name": "Molybdenum", "standard_mass": 95.94, - "vdw_radii": 239 + "vdw_radii": 239, }, 43: { "element_symbol": "Tc", "name": "Technetium", "standard_mass": 98.9062, - "vdw_radii": 136 + "vdw_radii": 136, }, 44: { "element_symbol": "Ru", "name": "Ruthenium", "standard_mass": 101.07, - "vdw_radii": 134 + "vdw_radii": 134, }, 45: { "element_symbol": "Rh", "name": "Rhodium", "standard_mass": 102.9055, - "vdw_radii": 137 + "vdw_radii": 137, }, 46: { "element_symbol": "Pd", "name": "Palladium", "standard_mass": 106.42, - "vdw_radii": 144 - }, - 47: { - "element_symbol": "Ag", - "name": "Silver", - "standard_mass": 107.8682 - }, - 48: { - "element_symbol": "Cd", - "name": "Cadmium", - "standard_mass": 112.411 - }, - 49: { - "element_symbol": "In", - "name": "Indium", - "standard_mass": 114.818 - }, - 50: { - "element_symbol": "Sn", - "name": "Tin", - "standard_mass": 118.71 - }, - 51: { - "element_symbol": "Sb", - "name": "Antimony", - "standard_mass": 121.76 - }, - 52: { - "element_symbol": "Te", - "name": "Tellurium", - "standard_mass": 127.6 - }, - 53: { - "element_symbol": "I", - "name": "Iodine", - "standard_mass": 126.90447 - }, - 54: { - "element_symbol": "Xe", - "name": "Xenon", - "standard_mass": 131.293 - }, - 55: { - "element_symbol": "Cs", - "name": "Caesium", - "standard_mass": 132.90545 - }, - 56: { - "element_symbol": "Ba", - "name": "Barium", - "standard_mass": 137.327 - }, - 57: { - "element_symbol": "La", - "name": "Lanthanum", - "standard_mass": 138.9055 - }, - 58: { - "element_symbol": "Ce", - "name": "Cerium", - "standard_mass": 140.116 - }, - 59: { - "element_symbol": "Pr", - "name": "Praseodymium", - "standard_mass": 140.90765 - }, - 60: { - "element_symbol": "Nd", - "name": "Neodymium", - "standard_mass": 144.24 - }, - 61: { - "element_symbol": "Pm", - "name": "Promethium", - "standard_mass": 145 - }, - 62: { - "element_symbol": "Sm", - "name": "Samarium", - "standard_mass": 150.36 - }, - 63: { - "element_symbol": "Eu", - "name": "Europium", - "standard_mass": 151.964 - }, - 64: { - "element_symbol": "Gd", - "name": "Gadolinium", - "standard_mass": 157.25 - }, - 65: { - "element_symbol": "Tb", - "name": "Terbium", - "standard_mass": 158.92534 - }, - 66: { - "element_symbol": "Dy", - "name": "Dysprosium", - "standard_mass": 162.5 - }, - 67: { - "element_symbol": "Ho", - "name": "Holmium", - "standard_mass": 164.93032 - }, - 68: { - "element_symbol": "Er", - "name": "Erbium", - "standard_mass": 167.259 - }, - 69: { - "element_symbol": "Tm", - "name": "Thulium", - "standard_mass": 168.93421 - }, - 70: { - "element_symbol": "Yb", - "name": "Ytterbium", - "standard_mass": 173.04 - }, - 71: { - "element_symbol": "Lu", - "name": "Lutetium", - "standard_mass": 174.967 - }, - 72: { - "element_symbol": "Hf", - "name": "Hafnium", - "standard_mass": 178.49 - }, - 73: { - "element_symbol": "Ta", - "name": "Tantalum", - "standard_mass": 180.9479 - }, - 74: { - "element_symbol": "W", - "name": "Tungsten", - "standard_mass": 183.84 - }, - 75: { - "element_symbol": "Re", - "name": "Rhenium", - "standard_mass": 186.207 - }, - 76: { - "element_symbol": "Os", - "name": "Osmium", - "standard_mass": 190.23 - }, - 77: { - "element_symbol": "Ir", - "name": "Iridium", - "standard_mass": 192.217 - }, - 78: { - "element_symbol": "Pt", - "name": "Platinum", - "standard_mass": 195.078 - }, - 79: { - "element_symbol": "Au", - "name": "Gold", - "standard_mass": 196.96655 - }, - 80: { - "element_symbol": "Hg", - "name": "Mercury", - "standard_mass": 200.59 - }, - 81: { - "element_symbol": "Tl", - "name": "Thallium", - "standard_mass": 204.3833 - }, - 82: { - "element_symbol": "Pb", - "name": "Lead", - "standard_mass": 207.2 - }, - 83: { - "element_symbol": "Bi", - "name": "Bismuth", - "standard_mass": 208.98038 - }, - 84: { - "element_symbol": "Po", - "name": "Polonium", - "standard_mass": 209 - }, - 85: { - "element_symbol": "At", - "name": "Astatine", - "standard_mass": 210 - }, - 86: { - "element_symbol": "Rn", - "name": "Radon", - "standard_mass": 222 - }, - 87: { - "element_symbol": "Fr", - "name": "Francium", - "standard_mass": 223 - }, - 88: { - "element_symbol": "Ra", - "name": "Radium", - "standard_mass": 226.0254 - }, - 89: { - "element_symbol": "Ac", - "name": "Actinium", - "standard_mass": 227 - }, - 90: { - "element_symbol": "Th", - "name": "Thorium", - "standard_mass": 232.0381 - }, - 91: { - "element_symbol": "Pa", - "name": "Protactinium", - "standard_mass": 231.03588 - }, - 92: { - "element_symbol": "U", - "name": "Uranium", - "standard_mass": 238.02891 - }, - 93: { - "element_symbol": "Np", - "name": "Neptunium", - "standard_mass": 237.0482 - }, - 94: { - "element_symbol": "Pu", - "name": "Plutonium", - "standard_mass": 244 - }, - 95: { - "element_symbol": "Am", - "name": "Americium", - "standard_mass": 243 - }, - 96: { - "element_symbol": "Cm", - "name": "Curium", - "standard_mass": 243 - }, - 97: { - "element_symbol": "Bk", - "name": "Berkelium", - "standard_mass": 247 - }, - 98: { - "element_symbol": "Cf", - "name": "Californium", - "standard_mass": 251 - }, - 99: { - "element_symbol": "Es", - "name": "Einsteinium", - "standard_mass": 254 - }, - 100: { - "element_symbol": "Fm", - "name": "Fermium", - "standard_mass": 254 - }, - 101: { - "element_symbol": "Md", - "name": "Mendelevium", - "standard_mass": 258 - }, - 102: { - "element_symbol": "No", - "name": "Nobelium", - "standard_mass": 259 - }, - 103: { - "element_symbol": "Lr", - "name": "Lawrencium", - "standard_mass": 262 - } + "vdw_radii": 144, + }, + 47: {"element_symbol": "Ag", "name": "Silver", "standard_mass": 107.8682}, + 48: {"element_symbol": "Cd", "name": "Cadmium", "standard_mass": 112.411}, + 49: {"element_symbol": "In", "name": "Indium", "standard_mass": 114.818}, + 50: {"element_symbol": "Sn", "name": "Tin", "standard_mass": 118.71}, + 51: {"element_symbol": "Sb", "name": "Antimony", "standard_mass": 121.76}, + 52: {"element_symbol": "Te", "name": "Tellurium", "standard_mass": 127.6}, + 53: {"element_symbol": "I", "name": "Iodine", "standard_mass": 126.90447}, + 54: {"element_symbol": "Xe", "name": "Xenon", "standard_mass": 131.293}, + 55: {"element_symbol": "Cs", "name": "Caesium", "standard_mass": 132.90545}, + 56: {"element_symbol": "Ba", "name": "Barium", "standard_mass": 137.327}, + 57: {"element_symbol": "La", "name": "Lanthanum", "standard_mass": 138.9055}, + 58: {"element_symbol": "Ce", "name": "Cerium", "standard_mass": 140.116}, + 59: {"element_symbol": "Pr", "name": "Praseodymium", "standard_mass": 140.90765}, + 60: {"element_symbol": "Nd", "name": "Neodymium", "standard_mass": 144.24}, + 61: {"element_symbol": "Pm", "name": "Promethium", "standard_mass": 145}, + 62: {"element_symbol": "Sm", "name": "Samarium", "standard_mass": 150.36}, + 63: {"element_symbol": "Eu", "name": "Europium", "standard_mass": 151.964}, + 64: {"element_symbol": "Gd", "name": "Gadolinium", "standard_mass": 157.25}, + 65: {"element_symbol": "Tb", "name": "Terbium", "standard_mass": 158.92534}, + 66: {"element_symbol": "Dy", "name": "Dysprosium", "standard_mass": 162.5}, + 67: {"element_symbol": "Ho", "name": "Holmium", "standard_mass": 164.93032}, + 68: {"element_symbol": "Er", "name": "Erbium", "standard_mass": 167.259}, + 69: {"element_symbol": "Tm", "name": "Thulium", "standard_mass": 168.93421}, + 70: {"element_symbol": "Yb", "name": "Ytterbium", "standard_mass": 173.04}, + 71: {"element_symbol": "Lu", "name": "Lutetium", "standard_mass": 174.967}, + 72: {"element_symbol": "Hf", "name": "Hafnium", "standard_mass": 178.49}, + 73: {"element_symbol": "Ta", "name": "Tantalum", "standard_mass": 180.9479}, + 74: {"element_symbol": "W", "name": "Tungsten", "standard_mass": 183.84}, + 75: {"element_symbol": "Re", "name": "Rhenium", "standard_mass": 186.207}, + 76: {"element_symbol": "Os", "name": "Osmium", "standard_mass": 190.23}, + 77: {"element_symbol": "Ir", "name": "Iridium", "standard_mass": 192.217}, + 78: {"element_symbol": "Pt", "name": "Platinum", "standard_mass": 195.078}, + 79: {"element_symbol": "Au", "name": "Gold", "standard_mass": 196.96655}, + 80: {"element_symbol": "Hg", "name": "Mercury", "standard_mass": 200.59}, + 81: {"element_symbol": "Tl", "name": "Thallium", "standard_mass": 204.3833}, + 82: {"element_symbol": "Pb", "name": "Lead", "standard_mass": 207.2}, + 83: {"element_symbol": "Bi", "name": "Bismuth", "standard_mass": 208.98038}, + 84: {"element_symbol": "Po", "name": "Polonium", "standard_mass": 209}, + 85: {"element_symbol": "At", "name": "Astatine", "standard_mass": 210}, + 86: {"element_symbol": "Rn", "name": "Radon", "standard_mass": 222}, + 87: {"element_symbol": "Fr", "name": "Francium", "standard_mass": 223}, + 88: {"element_symbol": "Ra", "name": "Radium", "standard_mass": 226.0254}, + 89: {"element_symbol": "Ac", "name": "Actinium", "standard_mass": 227}, + 90: {"element_symbol": "Th", "name": "Thorium", "standard_mass": 232.0381}, + 91: {"element_symbol": "Pa", "name": "Protactinium", "standard_mass": 231.03588}, + 92: {"element_symbol": "U", "name": "Uranium", "standard_mass": 238.02891}, + 93: {"element_symbol": "Np", "name": "Neptunium", "standard_mass": 237.0482}, + 94: {"element_symbol": "Pu", "name": "Plutonium", "standard_mass": 244}, + 95: {"element_symbol": "Am", "name": "Americium", "standard_mass": 243}, + 96: {"element_symbol": "Cm", "name": "Curium", "standard_mass": 243}, + 97: {"element_symbol": "Bk", "name": "Berkelium", "standard_mass": 247}, + 98: {"element_symbol": "Cf", "name": "Californium", "standard_mass": 251}, + 99: {"element_symbol": "Es", "name": "Einsteinium", "standard_mass": 254}, + 100: {"element_symbol": "Fm", "name": "Fermium", "standard_mass": 254}, + 101: {"element_symbol": "Md", "name": "Mendelevium", "standard_mass": 258}, + 102: {"element_symbol": "No", "name": "Nobelium", "standard_mass": 259}, + 103: {"element_symbol": "Lr", "name": "Lawrencium", "standard_mass": 262}, } # coarse_grain_particles dictionary is currently being used as a backup for # non-standard atom names that were mixed in with the elements dictionary coarse_grain_particles = { - "BB": {"atomic_number": 6, 'vdw_radii': 250, "name": "MartiniBB"}, - "CD": {"atomic_number": 6, 'vdw_radii': 170, "name": "MartiniLipidCarbonD"}, - "D" : {"atomic_number": 6, 'vdw_radii': 170, "name": "Carbon"}, - "GL": {"atomic_number": 8, 'vdw_radii': 180, "name": "MartiniGL"}, - "SC": {"atomic_number": 16, 'vdw_radii': 200, "name": "MartiniSC"}#, + "BB": {"atomic_number": 6, "vdw_radii": 250, "name": "MartiniBB"}, + "CD": {"atomic_number": 6, "vdw_radii": 170, "name": "MartiniLipidCarbonD"}, + "D": {"atomic_number": 6, "vdw_radii": 170, "name": "Carbon"}, + "GL": {"atomic_number": 8, "vdw_radii": 180, "name": "MartiniGL"}, + "SC": {"atomic_number": 16, "vdw_radii": 200, "name": "MartiniSC"}, # , ## just kept since these were entries in the old `elements` dictionary - #"NA": {"atomic_number": 11, 'vdw_radii': 227, "name": "Sodium"}, - #"CL": {"atomic_number": 17, 'vdw_radii': 175, "name": "Chlorine"} - } + # "NA": {"atomic_number": 11, 'vdw_radii': 227, "name": "Sodium"}, + # "CL": {"atomic_number": 17, 'vdw_radii': 175, "name": "Chlorine"} +} residues = { # unknown? Came up in one of the structures, haven't looked into it yet # TODO look into it! - "UNK": {"res_name_num": -1, "res_type": "unknown", "res_type_no": 1}, - + "UNK": {"res_name_num": -1, "res_type": "unknown", "res_type_no": 1}, # TODO implement an attribute that uses the residue types (basic / polar / acid etc) # 20 naturally occurring amino acids - "ALA": {"res_name_num": 0, "res_type": "apolar", "res_type_no": 1}, - "ARG": {"res_name_num": 1, "res_type": "basic", "res_type_no": 1}, - "ASN": {"res_name_num": 2, "res_type": "polar", "res_type_no": 1}, - "ASP": {"res_name_num": 3, "res_type": "acid", "res_type_no": 1}, + "ALA": {"res_name_num": 0, "res_type": "apolar", "res_type_no": 1}, + "ARG": {"res_name_num": 1, "res_type": "basic", "res_type_no": 1}, + "ASN": {"res_name_num": 2, "res_type": "polar", "res_type_no": 1}, + "ASP": {"res_name_num": 3, "res_type": "acid", "res_type_no": 1}, # can act as acid, but mostly polar? - "CYS": {"res_name_num": 4, "res_type": "polar", "res_type_no": 1}, - "GLU": {"res_name_num": 5, "res_type": "acid", "res_type_no": 1}, - "GLN": {"res_name_num": 6, "res_type": "polar", "res_type_no": 1}, - "GLY": {"res_name_num": 7, "res_type": "apolar", "res_type_no": 1}, + "CYS": {"res_name_num": 4, "res_type": "polar", "res_type_no": 1}, + "GLU": {"res_name_num": 5, "res_type": "acid", "res_type_no": 1}, + "GLN": {"res_name_num": 6, "res_type": "polar", "res_type_no": 1}, + "GLY": {"res_name_num": 7, "res_type": "apolar", "res_type_no": 1}, # ambiguous - "HIS": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, - "ILE": {"res_name_num": 9, "res_type": "apolar", "res_type_no": 1}, - "LEU": {"res_name_num": 10, "res_type": "apolar", "res_type_no": 1}, - "LYS": {"res_name_num": 11, "res_type": "basic", "res_type_no": 1}, - "MET": {"res_name_num": 12, "res_type": "apolar", "res_type_no": 1}, + "HIS": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, + "ILE": {"res_name_num": 9, "res_type": "apolar", "res_type_no": 1}, + "LEU": {"res_name_num": 10, "res_type": "apolar", "res_type_no": 1}, + "LYS": {"res_name_num": 11, "res_type": "basic", "res_type_no": 1}, + "MET": {"res_name_num": 12, "res_type": "apolar", "res_type_no": 1}, "PHE": {"res_name_num": 13, "res_type": "aromatic", "res_type_no": 1}, - "PRO": {"res_name_num": 14, "res_type": "apolar", "res_type_no": 1}, - "SER": {"res_name_num": 15, "res_type": "polar", "res_type_no": 1}, - "THR": {"res_name_num": 16, "res_type": "polar", "res_type_no": 1}, + "PRO": {"res_name_num": 14, "res_type": "apolar", "res_type_no": 1}, + "SER": {"res_name_num": 15, "res_type": "polar", "res_type_no": 1}, + "THR": {"res_name_num": 16, "res_type": "polar", "res_type_no": 1}, "TRP": {"res_name_num": 17, "res_type": "aromatic", "res_type_no": 1}, "TYR": {"res_name_num": 18, "res_type": "aromatic", "res_type_no": 1}, - "VAL": {"res_name_num": 19, "res_type": "apolar", "res_type_no": 1}, - + "VAL": {"res_name_num": 19, "res_type": "apolar", "res_type_no": 1}, # modified amino acids, unsure how to deal with but currently will label as the # same res_name number # S-nitrosylation of cysteine "SNC": {"res_name_num": 15, "res_type": "unkown", "res_type_no": 1}, # selenomethionine "MSE": {"res_name_num": 12, "res_type": "unkown", "res_type_no": 1}, - # add conventional AMBER FF residue names with different protonations - "ASH": {"res_name_num": 3, "res_type": "acid", "res_type_no": 1}, - "CYM": {"res_name_num": 4, "res_type": "polar", "res_type_no": 1}, - "CYX": {"res_name_num": 4, "res_type": "polar", "res_type_no": 1}, - "GLH": {"res_name_num": 5, "res_type": "acid", "res_type_no": 1}, - "HID": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, - "HIE": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, - "HIP": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, - "HYP": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, - "LYN": {"res_name_num": 11, "res_type": "basic", "res_type_no": 1}, - + "ASH": {"res_name_num": 3, "res_type": "acid", "res_type_no": 1}, + "CYM": {"res_name_num": 4, "res_type": "polar", "res_type_no": 1}, + "CYX": {"res_name_num": 4, "res_type": "polar", "res_type_no": 1}, + "GLH": {"res_name_num": 5, "res_type": "acid", "res_type_no": 1}, + "HID": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, + "HIE": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, + "HIP": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, + "HYP": {"res_name_num": 8, "res_type": "polar", "res_type_no": 1}, + "LYN": {"res_name_num": 11, "res_type": "basic", "res_type_no": 1}, # nucleic acids # DNA - "DA": {"res_name_num": 30, "res_type": "unkown", "res_type_no": 1}, - "DC": {"res_name_num": 31, "res_type": "unkown", "res_type_no": 1}, - "DG": {"res_name_num": 32, "res_type": "unkown", "res_type_no": 1}, - "DT": {"res_name_num": 33, "res_type": "unkown", "res_type_no": 1}, - - "PST": {"res_name_num": 33, "res_type": "unkown", "res_type_no": 1}, + "DA": {"res_name_num": 30, "res_type": "unkown", "res_type_no": 1}, + "DC": {"res_name_num": 31, "res_type": "unkown", "res_type_no": 1}, + "DG": {"res_name_num": 32, "res_type": "unkown", "res_type_no": 1}, + "DT": {"res_name_num": 33, "res_type": "unkown", "res_type_no": 1}, + "PST": {"res_name_num": 33, "res_type": "unkown", "res_type_no": 1}, # RNA - "A": {"res_name_num": 40, "res_type": "unkown", "res_type_no": 1}, - "C": {"res_name_num": 41, "res_type": "unkown", "res_type_no": 1}, - "G": {"res_name_num": 42, "res_type": "unkown", "res_type_no": 1}, - "U": {"res_name_num": 43, "res_type": "unkown", "res_type_no": 1} - + "A": {"res_name_num": 40, "res_type": "unkown", "res_type_no": 1}, + "C": {"res_name_num": 41, "res_type": "unkown", "res_type_no": 1}, + "G": {"res_name_num": 42, "res_type": "unkown", "res_type_no": 1}, + "U": {"res_name_num": 43, "res_type": "unkown", "res_type_no": 1}, # need to add some sugars and such here as well } @@ -1251,59 +782,53 @@ # rotation point around the alpha carbon is: 2 atom_names = { # backbone atoms - 'N': 1, - 'CA': 2, # rotation point - 'C': 3, - 'O': 4, - + "N": 1, + "CA": 2, # rotation point + "C": 3, + "O": 4, # sidechain atoms - 'CB': 5, # rotation point - 'CG': 6, # rotation point - 'CG1': 7, - 'CG2': 8, - 'OG': 9, - 'OG1': 10, - 'SG': 11, - 'CD': 12, # rotation point - 'CD1': 13, - 'CD2': 14, - 'ND1': 15, - 'ND2': 16, - 'OD1': 17, - 'OD2': 18, - 'SD': 19, - 'CE': 20, # rotation point - 'CE1': 21, - 'CE2': 23, - 'CE3': 24, - 'NE': 25, # rotation point - 'NE1': 26, - 'NE2': 27, - 'OE1': 28, - 'OE2': 29, - 'CH2': 30, - 'NH1': 31, - 'NH2': 32, - 'OH': 33, - 'CZ': 34, - 'CZ2': 35, - 'CZ3': 36, - 'NZ': 37, + "CB": 5, # rotation point + "CG": 6, # rotation point + "CG1": 7, + "CG2": 8, + "OG": 9, + "OG1": 10, + "SG": 11, + "CD": 12, # rotation point + "CD1": 13, + "CD2": 14, + "ND1": 15, + "ND2": 16, + "OD1": 17, + "OD2": 18, + "SD": 19, + "CE": 20, # rotation point + "CE1": 21, + "CE2": 23, + "CE3": 24, + "NE": 25, # rotation point + "NE1": 26, + "NE2": 27, + "OE1": 28, + "OE2": 29, + "CH2": 30, + "NH1": 31, + "NH2": 32, + "OH": 33, + "CZ": 34, + "CZ2": 35, + "CZ3": 36, + "NZ": 37, # terminus of a peptide chain when it ends in an oxygen, maybe move higher? (in the 'backbone') - 'OXT': 38, - - + "OXT": 38, # DNA # currently works for RNA as well, but haven't optimised - # phosphate "P": 50, # connection to the previous ribose - # These two atoms can sometimes have their names written different ways, # this covers both to ensure their names are assigned properly. "O1P": 51, "OP1": 51, - "OP2": 52, "O2P": 52, # ribose @@ -1316,12 +841,10 @@ "C2'": 59, # ring "O2'": 60, "C1'": 61, # ring # connection to the base - # connection point to the base "N1": 62, "N9": 63, "N3": 64, - "C8": 65, "N7": 66, "C5": 67, @@ -1333,14 +856,10 @@ "C4": 71, "O6": 72, "N2": 73, - "N4": 74, "O2": 75, - "O4": 76, - "C7": 77 # another extra carbon? - - + "C7": 77, # another extra carbon? # unsure how to proceed past this point, as atom names are reused inside of # different bases but represent totally different locations like the N9 and N1 atoms # can both be the connecting carbon to the ribose, or a carbon much further into the @@ -1351,116 +870,2277 @@ # atom charges taken from AMBER force field source code atom_charge = { # peptide charges - 'ALA': {'N': -0.4157, 'H': 0.2719, 'CA': 0.0337, 'HA': 0.0823, 'CB': -0.1825, 'HB1': 0.0603, 'HB2': 0.0603, 'HB3': 0.0603, 'C': 0.5973, 'O': -0.5679}, - 'ARG': {'N': -0.3479, 'H': 0.2747, 'CA': -0.2637, 'HA': 0.156, 'CB': -0.0007, 'HB2': 0.0327, 'HB3': 0.0327, 'CG': 0.039, 'HG2': 0.0285, 'HG3': 0.0285, 'CD': 0.0486, 'HD2': 0.0687, 'HD3': 0.0687, 'NE': -0.5295, 'HE': 0.3456, 'CZ': 0.8076, 'NH1': -0.8627, 'HH11': 0.4478, 'HH12': 0.4478, 'NH2': -0.8627, 'HH21': 0.4478, 'HH22': 0.4478, 'C': 0.7341, 'O': -0.5894}, - 'ASH': {'N': -0.4157, 'H': 0.2719, 'CA': 0.0341, 'HA': 0.0864, 'CB': -0.0316, 'HB2': 0.0488, 'HB3': 0.0488, 'CG': 0.6462, 'OD1': -0.5554, 'OD2': -0.6376, 'HD2': 0.4747, 'C': 0.5973, 'O': -0.5679}, - 'ASN': {'N': -0.4157, 'H': 0.2719, 'CA': 0.0143, 'HA': 0.1048, 'CB': -0.2041, 'HB2': 0.0797, 'HB3': 0.0797, 'CG': 0.713, 'OD1': -0.5931, 'ND2': -0.9191, 'HD21': 0.4196, 'HD22': 0.4196, 'C': 0.5973, 'O': -0.5679}, - 'ASP': {'N': -0.5163, 'H': 0.2936, 'CA': 0.0381, 'HA': 0.088, 'CB': -0.0303, 'HB2': -0.0122, 'HB3': -0.0122, 'CG': 0.7994, 'OD1': -0.8014, 'OD2': -0.8014, 'C': 0.5366, 'O': -0.5819}, - 'CYM': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0351, 'HA': 0.0508, 'CB': -0.2413, 'HB3': 0.1122, 'HB2': 0.1122, 'SG': -0.8844, 'C': 0.5973, 'O': -0.5679}, - 'CYS': {'N': -0.4157, 'H': 0.2719, 'CA': 0.0213, 'HA': 0.1124, 'CB': -0.1231, 'HB2': 0.1112, 'HB3': 0.1112, 'SG': -0.3119, 'HG': 0.1933, 'C': 0.5973, 'O': -0.5679}, - 'CYX': {'N': -0.4157, 'H': 0.2719, 'CA': 0.0429, 'HA': 0.0766, 'CB': -0.079, 'HB2': 0.091, 'HB3': 0.091, 'SG': -0.1081, 'C': 0.5973, 'O': -0.5679}, - 'GLH': {'N': -0.4157, 'H': 0.2719, 'CA': 0.0145, 'HA': 0.0779, 'CB': -0.0071, 'HB2': 0.0256, 'HB3': 0.0256, 'CG': -0.0174, 'HG2': 0.043, 'HG3': 0.043, 'CD': 0.6801, 'OE1': -0.5838, 'OE2': -0.6511, 'HE2': 0.4641, 'C': 0.5973, 'O': -0.5679}, - 'GLN': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0031, 'HA': 0.085, 'CB': -0.0036, 'HB2': 0.0171, 'HB3': 0.0171, 'CG': -0.0645, 'HG2': 0.0352, 'HG3': 0.0352, 'CD': 0.6951, 'OE1': -0.6086, 'NE2': -0.9407, 'HE21': 0.4251, 'HE22': 0.4251, 'C': 0.5973, 'O': -0.5679}, - 'GLU': {'N': -0.5163, 'H': 0.2936, 'CA': 0.0397, 'HA': 0.1105, 'CB': 0.056, 'HB2': -0.0173, 'HB3': -0.0173, 'CG': 0.0136, 'HG2': -0.0425, 'HG3': -0.0425, 'CD': 0.8054, 'OE1': -0.8188, 'OE2': -0.8188, 'C': 0.5366, 'O': -0.5819}, - 'GLY': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0252, 'HA2': 0.0698, 'HA3': 0.0698, 'C': 0.5973, 'O': -0.5679}, - 'HID': {'N': -0.4157, 'H': 0.2719, 'CA': 0.0188, 'HA': 0.0881, 'CB': -0.0462, 'HB2': 0.0402, 'HB3': 0.0402, 'CG': -0.0266, 'ND1': -0.3811, 'HD1': 0.3649, 'CE1': 0.2057, 'HE1': 0.1392, 'NE2': -0.5727, 'CD2': 0.1292, 'HD2': 0.1147, 'C': 0.5973, 'O': -0.5679}, - 'HIE': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0581, 'HA': 0.136, 'CB': -0.0074, 'HB2': 0.0367, 'HB3': 0.0367, 'CG': 0.1868, 'ND1': -0.5432, 'CE1': 0.1635, 'HE1': 0.1435, 'NE2': -0.2795, 'HE2': 0.3339, 'CD2': -0.2207, 'HD2': 0.1862, 'C': 0.5973, 'O': -0.5679}, - 'HIP': {'N': -0.3479, 'H': 0.2747, 'CA': -0.1354, 'HA': 0.1212, 'CB': -0.0414, 'HB2': 0.081, 'HB3': 0.081, 'CG': -0.0012, 'ND1': -0.1513, 'HD1': 0.3866, 'CE1': -0.017, 'HE1': 0.2681, 'NE2': -0.1718, 'HE2': 0.3911, 'CD2': -0.1141, 'HD2': 0.2317, 'C': 0.7341, 'O': -0.5894}, - 'ILE': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0597, 'HA': 0.0869, 'CB': 0.1303, 'HB': 0.0187, 'CG2': -0.3204, 'HG21': 0.0882, 'HG22': 0.0882, 'HG23': 0.0882, 'CG1': -0.043, 'HG12': 0.0236, 'HG13': 0.0236, 'CD1': -0.066, 'HD11': 0.0186, 'HD12': 0.0186, 'HD13': 0.0186, 'C': 0.5973, 'O': -0.5679}, - 'LEU': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0518, 'HA': 0.0922, 'CB': -0.1102, 'HB2': 0.0457, 'HB3': 0.0457, 'CG': 0.3531, 'HG': -0.0361, 'CD1': -0.4121, 'HD11': 0.1, 'HD12': 0.1, 'HD13': 0.1, 'CD2': -0.4121, 'HD21': 0.1, 'HD22': 0.1, 'HD23': 0.1, 'C': 0.5973, 'O': -0.5679}, - 'LYN': {'N': -0.4157, 'H': 0.2719, 'CA': -0.07206, 'HA': 0.0994, 'CB': -0.04845, 'HB2': 0.034, 'HB3': 0.034, 'CG': 0.06612, 'HG2': 0.01041, 'HG3': 0.01041, 'CD': -0.03768, 'HD2': 0.01155, 'HD3': 0.01155, 'CE': 0.32604, 'HE2': -0.03358, 'HE3': -0.03358, 'NZ': -1.03581, 'HZ2': 0.38604, 'HZ3': 0.38604, 'C': 0.5973, 'O': -0.5679}, - 'LYS': {'N': -0.3479, 'H': 0.2747, 'CA': -0.24, 'HA': 0.1426, 'CB': -0.0094, 'HB2': 0.0362, 'HB3': 0.0362, 'CG': 0.0187, 'HG2': 0.0103, 'HG3': 0.0103, 'CD': -0.0479, 'HD2': 0.0621, 'HD3': 0.0621, 'CE': -0.0143, 'HE2': 0.1135, 'HE3': 0.1135, 'NZ': -0.3854, 'HZ1': 0.34, 'HZ2': 0.34, 'HZ3': 0.34, 'C': 0.7341, 'O': -0.5894}, - 'MET': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0237, 'HA': 0.088, 'CB': 0.0342, 'HB2': 0.0241, 'HB3': 0.0241, 'CG': 0.0018, 'HG2': 0.044, 'HG3': 0.044, 'SD': -0.2737, 'CE': -0.0536, 'HE1': 0.0684, 'HE2': 0.0684, 'HE3': 0.0684, 'C': 0.5973, 'O': -0.5679}, - 'PHE': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0024, 'HA': 0.0978, 'CB': -0.0343, 'HB2': 0.0295, 'HB3': 0.0295, 'CG': 0.0118, 'CD1': -0.1256, 'HD1': 0.133, 'CE1': -0.1704, 'HE1': 0.143, 'CZ': -0.1072, 'HZ': 0.1297, 'CE2': -0.1704, 'HE2': 0.143, 'CD2': -0.1256, 'HD2': 0.133, 'C': 0.5973, 'O': -0.5679}, - 'PRO': {'N': -0.2548, 'CD': 0.0192, 'HD2': 0.0391, 'HD3': 0.0391, 'CG': 0.0189, 'HG2': 0.0213, 'HG3': 0.0213, 'CB': -0.007, 'HB2': 0.0253, 'HB3': 0.0253, 'CA': -0.0266, 'HA': 0.0641, 'C': 0.5896, 'O': -0.5748}, - 'SER': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0249, 'HA': 0.0843, 'CB': 0.2117, 'HB2': 0.0352, 'HB3': 0.0352, 'OG': -0.6546, 'HG': 0.4275, 'C': 0.5973, 'O': -0.5679}, - 'THR': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0389, 'HA': 0.1007, 'CB': 0.3654, 'HB': 0.0043, 'CG2': -0.2438, 'HG21': 0.0642, 'HG22': 0.0642, 'HG23': 0.0642, 'OG1': -0.6761, 'HG1': 0.4102, 'C': 0.5973, 'O': -0.5679}, - 'TRP': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0275, 'HA': 0.1123, 'CB': -0.005, 'HB2': 0.0339, 'HB3': 0.0339, 'CG': -0.1415, 'CD1': -0.1638, 'HD1': 0.2062, 'NE1': -0.3418, 'HE1': 0.3412, 'CE2': 0.138, 'CZ2': -0.2601, 'HZ2': 0.1572, 'CH2': -0.1134, 'HH2': 0.1417, 'CZ3': -0.1972, 'HZ3': 0.1447, 'CE3': -0.2387, 'HE3': 0.17, 'CD2': 0.1243, 'C': 0.5973, 'O': -0.5679}, - 'TYR': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0014, 'HA': 0.0876, 'CB': -0.0152, 'HB2': 0.0295, 'HB3': 0.0295, 'CG': -0.0011, 'CD1': -0.1906, 'HD1': 0.1699, 'CE1': -0.2341, 'HE1': 0.1656, 'CZ': 0.3226, 'OH': -0.5579, 'HH': 0.3992, 'CE2': -0.2341, 'HE2': 0.1656, 'CD2': -0.1906, 'HD2': 0.1699, 'C': 0.5973, 'O': -0.5679}, - 'VAL': {'N': -0.4157, 'H': 0.2719, 'CA': -0.0875, 'HA': 0.0969, 'CB': 0.2985, 'HB': -0.0297, 'CG1': -0.3192, 'HG11': 0.0791, 'HG12': 0.0791, 'HG13': 0.0791, 'CG2': -0.3192, 'HG21': 0.0791, 'HG22': 0.0791, 'HG23': 0.0791, 'C': 0.5973, 'O': -0.5679}, - + "ALA": { + "N": -0.4157, + "H": 0.2719, + "CA": 0.0337, + "HA": 0.0823, + "CB": -0.1825, + "HB1": 0.0603, + "HB2": 0.0603, + "HB3": 0.0603, + "C": 0.5973, + "O": -0.5679, + }, + "ARG": { + "N": -0.3479, + "H": 0.2747, + "CA": -0.2637, + "HA": 0.156, + "CB": -0.0007, + "HB2": 0.0327, + "HB3": 0.0327, + "CG": 0.039, + "HG2": 0.0285, + "HG3": 0.0285, + "CD": 0.0486, + "HD2": 0.0687, + "HD3": 0.0687, + "NE": -0.5295, + "HE": 0.3456, + "CZ": 0.8076, + "NH1": -0.8627, + "HH11": 0.4478, + "HH12": 0.4478, + "NH2": -0.8627, + "HH21": 0.4478, + "HH22": 0.4478, + "C": 0.7341, + "O": -0.5894, + }, + "ASH": { + "N": -0.4157, + "H": 0.2719, + "CA": 0.0341, + "HA": 0.0864, + "CB": -0.0316, + "HB2": 0.0488, + "HB3": 0.0488, + "CG": 0.6462, + "OD1": -0.5554, + "OD2": -0.6376, + "HD2": 0.4747, + "C": 0.5973, + "O": -0.5679, + }, + "ASN": { + "N": -0.4157, + "H": 0.2719, + "CA": 0.0143, + "HA": 0.1048, + "CB": -0.2041, + "HB2": 0.0797, + "HB3": 0.0797, + "CG": 0.713, + "OD1": -0.5931, + "ND2": -0.9191, + "HD21": 0.4196, + "HD22": 0.4196, + "C": 0.5973, + "O": -0.5679, + }, + "ASP": { + "N": -0.5163, + "H": 0.2936, + "CA": 0.0381, + "HA": 0.088, + "CB": -0.0303, + "HB2": -0.0122, + "HB3": -0.0122, + "CG": 0.7994, + "OD1": -0.8014, + "OD2": -0.8014, + "C": 0.5366, + "O": -0.5819, + }, + "CYM": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0351, + "HA": 0.0508, + "CB": -0.2413, + "HB3": 0.1122, + "HB2": 0.1122, + "SG": -0.8844, + "C": 0.5973, + "O": -0.5679, + }, + "CYS": { + "N": -0.4157, + "H": 0.2719, + "CA": 0.0213, + "HA": 0.1124, + "CB": -0.1231, + "HB2": 0.1112, + "HB3": 0.1112, + "SG": -0.3119, + "HG": 0.1933, + "C": 0.5973, + "O": -0.5679, + }, + "CYX": { + "N": -0.4157, + "H": 0.2719, + "CA": 0.0429, + "HA": 0.0766, + "CB": -0.079, + "HB2": 0.091, + "HB3": 0.091, + "SG": -0.1081, + "C": 0.5973, + "O": -0.5679, + }, + "GLH": { + "N": -0.4157, + "H": 0.2719, + "CA": 0.0145, + "HA": 0.0779, + "CB": -0.0071, + "HB2": 0.0256, + "HB3": 0.0256, + "CG": -0.0174, + "HG2": 0.043, + "HG3": 0.043, + "CD": 0.6801, + "OE1": -0.5838, + "OE2": -0.6511, + "HE2": 0.4641, + "C": 0.5973, + "O": -0.5679, + }, + "GLN": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0031, + "HA": 0.085, + "CB": -0.0036, + "HB2": 0.0171, + "HB3": 0.0171, + "CG": -0.0645, + "HG2": 0.0352, + "HG3": 0.0352, + "CD": 0.6951, + "OE1": -0.6086, + "NE2": -0.9407, + "HE21": 0.4251, + "HE22": 0.4251, + "C": 0.5973, + "O": -0.5679, + }, + "GLU": { + "N": -0.5163, + "H": 0.2936, + "CA": 0.0397, + "HA": 0.1105, + "CB": 0.056, + "HB2": -0.0173, + "HB3": -0.0173, + "CG": 0.0136, + "HG2": -0.0425, + "HG3": -0.0425, + "CD": 0.8054, + "OE1": -0.8188, + "OE2": -0.8188, + "C": 0.5366, + "O": -0.5819, + }, + "GLY": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0252, + "HA2": 0.0698, + "HA3": 0.0698, + "C": 0.5973, + "O": -0.5679, + }, + "HID": { + "N": -0.4157, + "H": 0.2719, + "CA": 0.0188, + "HA": 0.0881, + "CB": -0.0462, + "HB2": 0.0402, + "HB3": 0.0402, + "CG": -0.0266, + "ND1": -0.3811, + "HD1": 0.3649, + "CE1": 0.2057, + "HE1": 0.1392, + "NE2": -0.5727, + "CD2": 0.1292, + "HD2": 0.1147, + "C": 0.5973, + "O": -0.5679, + }, + "HIE": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0581, + "HA": 0.136, + "CB": -0.0074, + "HB2": 0.0367, + "HB3": 0.0367, + "CG": 0.1868, + "ND1": -0.5432, + "CE1": 0.1635, + "HE1": 0.1435, + "NE2": -0.2795, + "HE2": 0.3339, + "CD2": -0.2207, + "HD2": 0.1862, + "C": 0.5973, + "O": -0.5679, + }, + "HIP": { + "N": -0.3479, + "H": 0.2747, + "CA": -0.1354, + "HA": 0.1212, + "CB": -0.0414, + "HB2": 0.081, + "HB3": 0.081, + "CG": -0.0012, + "ND1": -0.1513, + "HD1": 0.3866, + "CE1": -0.017, + "HE1": 0.2681, + "NE2": -0.1718, + "HE2": 0.3911, + "CD2": -0.1141, + "HD2": 0.2317, + "C": 0.7341, + "O": -0.5894, + }, + "ILE": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0597, + "HA": 0.0869, + "CB": 0.1303, + "HB": 0.0187, + "CG2": -0.3204, + "HG21": 0.0882, + "HG22": 0.0882, + "HG23": 0.0882, + "CG1": -0.043, + "HG12": 0.0236, + "HG13": 0.0236, + "CD1": -0.066, + "HD11": 0.0186, + "HD12": 0.0186, + "HD13": 0.0186, + "C": 0.5973, + "O": -0.5679, + }, + "LEU": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0518, + "HA": 0.0922, + "CB": -0.1102, + "HB2": 0.0457, + "HB3": 0.0457, + "CG": 0.3531, + "HG": -0.0361, + "CD1": -0.4121, + "HD11": 0.1, + "HD12": 0.1, + "HD13": 0.1, + "CD2": -0.4121, + "HD21": 0.1, + "HD22": 0.1, + "HD23": 0.1, + "C": 0.5973, + "O": -0.5679, + }, + "LYN": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.07206, + "HA": 0.0994, + "CB": -0.04845, + "HB2": 0.034, + "HB3": 0.034, + "CG": 0.06612, + "HG2": 0.01041, + "HG3": 0.01041, + "CD": -0.03768, + "HD2": 0.01155, + "HD3": 0.01155, + "CE": 0.32604, + "HE2": -0.03358, + "HE3": -0.03358, + "NZ": -1.03581, + "HZ2": 0.38604, + "HZ3": 0.38604, + "C": 0.5973, + "O": -0.5679, + }, + "LYS": { + "N": -0.3479, + "H": 0.2747, + "CA": -0.24, + "HA": 0.1426, + "CB": -0.0094, + "HB2": 0.0362, + "HB3": 0.0362, + "CG": 0.0187, + "HG2": 0.0103, + "HG3": 0.0103, + "CD": -0.0479, + "HD2": 0.0621, + "HD3": 0.0621, + "CE": -0.0143, + "HE2": 0.1135, + "HE3": 0.1135, + "NZ": -0.3854, + "HZ1": 0.34, + "HZ2": 0.34, + "HZ3": 0.34, + "C": 0.7341, + "O": -0.5894, + }, + "MET": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0237, + "HA": 0.088, + "CB": 0.0342, + "HB2": 0.0241, + "HB3": 0.0241, + "CG": 0.0018, + "HG2": 0.044, + "HG3": 0.044, + "SD": -0.2737, + "CE": -0.0536, + "HE1": 0.0684, + "HE2": 0.0684, + "HE3": 0.0684, + "C": 0.5973, + "O": -0.5679, + }, + "PHE": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0024, + "HA": 0.0978, + "CB": -0.0343, + "HB2": 0.0295, + "HB3": 0.0295, + "CG": 0.0118, + "CD1": -0.1256, + "HD1": 0.133, + "CE1": -0.1704, + "HE1": 0.143, + "CZ": -0.1072, + "HZ": 0.1297, + "CE2": -0.1704, + "HE2": 0.143, + "CD2": -0.1256, + "HD2": 0.133, + "C": 0.5973, + "O": -0.5679, + }, + "PRO": { + "N": -0.2548, + "CD": 0.0192, + "HD2": 0.0391, + "HD3": 0.0391, + "CG": 0.0189, + "HG2": 0.0213, + "HG3": 0.0213, + "CB": -0.007, + "HB2": 0.0253, + "HB3": 0.0253, + "CA": -0.0266, + "HA": 0.0641, + "C": 0.5896, + "O": -0.5748, + }, + "SER": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0249, + "HA": 0.0843, + "CB": 0.2117, + "HB2": 0.0352, + "HB3": 0.0352, + "OG": -0.6546, + "HG": 0.4275, + "C": 0.5973, + "O": -0.5679, + }, + "THR": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0389, + "HA": 0.1007, + "CB": 0.3654, + "HB": 0.0043, + "CG2": -0.2438, + "HG21": 0.0642, + "HG22": 0.0642, + "HG23": 0.0642, + "OG1": -0.6761, + "HG1": 0.4102, + "C": 0.5973, + "O": -0.5679, + }, + "TRP": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0275, + "HA": 0.1123, + "CB": -0.005, + "HB2": 0.0339, + "HB3": 0.0339, + "CG": -0.1415, + "CD1": -0.1638, + "HD1": 0.2062, + "NE1": -0.3418, + "HE1": 0.3412, + "CE2": 0.138, + "CZ2": -0.2601, + "HZ2": 0.1572, + "CH2": -0.1134, + "HH2": 0.1417, + "CZ3": -0.1972, + "HZ3": 0.1447, + "CE3": -0.2387, + "HE3": 0.17, + "CD2": 0.1243, + "C": 0.5973, + "O": -0.5679, + }, + "TYR": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0014, + "HA": 0.0876, + "CB": -0.0152, + "HB2": 0.0295, + "HB3": 0.0295, + "CG": -0.0011, + "CD1": -0.1906, + "HD1": 0.1699, + "CE1": -0.2341, + "HE1": 0.1656, + "CZ": 0.3226, + "OH": -0.5579, + "HH": 0.3992, + "CE2": -0.2341, + "HE2": 0.1656, + "CD2": -0.1906, + "HD2": 0.1699, + "C": 0.5973, + "O": -0.5679, + }, + "VAL": { + "N": -0.4157, + "H": 0.2719, + "CA": -0.0875, + "HA": 0.0969, + "CB": 0.2985, + "HB": -0.0297, + "CG1": -0.3192, + "HG11": 0.0791, + "HG12": 0.0791, + "HG13": 0.0791, + "CG2": -0.3192, + "HG21": 0.0791, + "HG22": 0.0791, + "HG23": 0.0791, + "C": 0.5973, + "O": -0.5679, + }, # nucleic acid charges - "DA": {"P": 1.1659, "O1P": -0.7761, "O2P": -0.7761, "O5'": -0.4954, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.0431, "H1'": 0.1838, "N9": -0.0268, "C8": 0.1607, "H8": 0.1877, "N7": -0.6175, "C5": 0.0725, "C6": 0.6897, "N6": -0.9123, "H61": 0.4167, "H62": 0.4167, "N1": -0.7624, "C2": 0.5716, "H2": 0.0598, "N3": -0.7417, "C4": 0.38, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.5232}, - "DA3": {"P": 1.1659, "O1P": -0.7761, "O2P": -0.7761, "O5'": -0.4954, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.0431, "H1'": 0.1838, "N9": -0.0268, "C8": 0.1607, "H8": 0.1877, "N7": -0.6175, "C5": 0.0725, "C6": 0.6897, "N6": -0.9123, "H61": 0.4167, "H62": 0.4167, "N1": -0.7624, "C2": 0.5716, "H2": 0.0598, "N3": -0.7417, "C4": 0.38, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.6549, "H3T": 0.4396}, - "DA5": {"H5T": 0.4422, "O5'": -0.6318, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.0431, "H1'": 0.1838, "N9": -0.0268, "C8": 0.1607, "H8": 0.1877, "N7": -0.6175, "C5": 0.0725, "C6": 0.6897, "N6": -0.9123, "H61": 0.4167, "H62": 0.4167, "N1": -0.7624, "C2": 0.5716, "H2": 0.0598, "N3": -0.7417, "C4": 0.38, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.5232}, - "DAN": {"H5T": 0.4422, "O5'": -0.6318, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.0431, "H1'": 0.1838, "N9": -0.0268, "C8": 0.1607, "H8": 0.1877, "N7": -0.6175, "C5": 0.0725, "C6": 0.6897, "N6": -0.9123, "H61": 0.4167, "H62": 0.4167, "N1": -0.7624, "C2": 0.5716, "H2": 0.0598, "N3": -0.7417, "C4": 0.38, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.6549, "H3T": 0.4396}, - "DC": {"P": 1.1659, "O1P": -0.7761, "O2P": -0.7761, "O5'": -0.4954, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": -0.0116, "H1'": 0.1963, "N1": -0.0339, "C6": -0.0183, "H6": 0.2293, "C5": -0.5222, "H5": 0.1863, "C4": 0.8439, "N4": -0.9773, "H41": 0.4314, "H42": 0.4314, "N3": -0.7748, "C2": 0.7959, "O2": -0.6548, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.5232}, - "DC3": {"P": 1.1659, "O1P": -0.7761, "O2P": -0.7761, "O5'": -0.4954, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": -0.0116, "H1'": 0.1963, "N1": -0.0339, "C6": -0.0183, "H6": 0.2293, "C5": -0.5222, "H5": 0.1863, "C4": 0.8439, "N4": -0.9773, "H41": 0.4314, "H42": 0.4314, "N3": -0.7748, "C2": 0.7959, "O2": -0.6548, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.6549, "H3T": 0.4396}, - "DC5": {"H5T": 0.4422, "O5'": -0.6318, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": -0.0116, "H1'": 0.1963, "N1": -0.0339, "C6": -0.0183, "H6": 0.2293, "C5": -0.5222, "H5": 0.1863, "C4": 0.8439, "N4": -0.9773, "H41": 0.4314, "H42": 0.4314, "N3": -0.7748, "C2": 0.7959, "O2": -0.6548, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.5232}, - "DCN": {"H5T": 0.4422, "O5'": -0.6318, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": -0.0116, "H1'": 0.1963, "N1": -0.0339, "C6": -0.0183, "H6": 0.2293, "C5": -0.5222, "H5": 0.1863, "C4": 0.8439, "N4": -0.9773, "H41": 0.4314, "H42": 0.4314, "N3": -0.7748, "C2": 0.7959, "O2": -0.6548, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.6549, "H3T": 0.4396}, - "DG": {"P": 1.1659, "O1P": -0.7761, "O2P": -0.7761, "O5'": -0.4954, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.0358, "H1'": 0.1746, "N9": 0.0577, "C8": 0.0736, "H8": 0.1997, "N7": -0.5725, "C5": 0.1991, "C6": 0.4918, "O6": -0.5699, "N1": -0.5053, "H1": 0.352, "C2": 0.7432, "N2": -0.923, "H21": 0.4235, "H22": 0.4235, "N3": -0.6636, "C4": 0.1814, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.5232}, - "DG3": {"P": 1.1659, "O1P": -0.7761, "O2P": -0.7761, "O5'": -0.4954, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.0358, "H1'": 0.1746, "N9": 0.0577, "C8": 0.0736, "H8": 0.1997, "N7": -0.5725, "C5": 0.1991, "C6": 0.4918, "O6": -0.5699, "N1": -0.5053, "H1": 0.352, "C2": 0.7432, "N2": -0.923, "H21": 0.4235, "H22": 0.4235, "N3": -0.6636, "C4": 0.1814, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.6549, "H3T": 0.4396}, - "DG5": {"H5T": 0.4422, "O5'": -0.6318, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.0358, "H1'": 0.1746, "N9": 0.0577, "C8": 0.0736, "H8": 0.1997, "N7": -0.5725, "C5": 0.1991, "C6": 0.4918, "O6": -0.5699, "N1": -0.5053, "H1": 0.352, "C2": 0.7432, "N2": -0.923, "H21": 0.4235, "H22": 0.4235, "N3": -0.6636, "C4": 0.1814, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.5232}, - "DGN": {"H5T": 0.4422, "O5'": -0.6318, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.0358, "H1'": 0.1746, "N9": 0.0577, "C8": 0.0736, "H8": 0.1997, "N7": -0.5725, "C5": 0.1991, "C6": 0.4918, "O6": -0.5699, "N1": -0.5053, "H1": 0.352, "C2": 0.7432, "N2": -0.923, "H21": 0.4235, "H22": 0.4235, "N3": -0.6636, "C4": 0.1814, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.6549, "H3T": 0.4396}, - "DT": {"P": 1.1659, "O1P": -0.7761, "O2P": -0.7761, "O5'": -0.4954, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.068, "H1'": 0.1804, "N1": -0.0239, "C6": -0.2209, "H6": 0.2607, "C5": 0.0025, "C7": -0.2269, "H71": 0.077, "H72": 0.077, "H73": 0.077, "C4": 0.5194, "O4": -0.5563, "N3": -0.434, "H3": 0.342, "C2": 0.5677, "O2": -0.5881, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.5232}, - "DT3": {"P": 1.1659, "O1P": -0.7761, "O2P": -0.7761, "O5'": -0.4954, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.068, "H1'": 0.1804, "N1": -0.0239, "C6": -0.2209, "H6": 0.2607, "C5": 0.0025, "C7": -0.2269, "H71": 0.077, "H72": 0.077, "H73": 0.077, "C4": 0.5194, "O4": -0.5563, "N3": -0.434, "H3": 0.342, "C2": 0.5677, "O2": -0.5881, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.6549, "H3T": 0.4396}, - "DT5": {"H5T": 0.4422, "O5'": -0.6318, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.068, "H1'": 0.1804, "N1": -0.0239, "C6": -0.2209, "H6": 0.2607, "C5": 0.0025, "C7": -0.2269, "H71": 0.077, "H72": 0.077, "H73": 0.077, "C4": 0.5194, "O4": -0.5563, "N3": -0.434, "H3": 0.342, "C2": 0.5677, "O2": -0.5881, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.5232}, - "DTN": {"H5T": 0.4422, "O5'": -0.6318, "C5'": -0.0069, "H5'1": 0.0754, "H5'2": 0.0754, "C4'": 0.1629, "H4'": 0.1176, "O4'": -0.3691, "C1'": 0.068, "H1'": 0.1804, "N1": -0.0239, "C6": -0.2209, "H6": 0.2607, "C5": 0.0025, "C7": -0.2269, "H71": 0.077, "H72": 0.077, "H73": 0.077, "C4": 0.5194, "O4": -0.5563, "N3": -0.434, "H3": 0.342, "C2": 0.5677, "O2": -0.5881, "C3'": 0.0713, "H3'": 0.0985, "C2'": -0.0854, "H2'1": 0.0718, "H2'2": 0.0718, "O3'": -0.6549, "H3T": 0.4396}, - "RA": {"P": 1.1662, "O1P": -0.776, "O2P": -0.776, "O5'": -0.4989, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0394, "H1'": 0.2007, "N9": -0.0251, "C8": 0.2006, "H8": 0.1553, "N7": -0.6073, "C5": 0.0515, "C6": 0.7009, "N6": -0.9019, "H61": 0.4115, "H62": 0.4115, "N1": -0.7615, "C2": 0.5875, "H2": 0.0473, "N3": -0.6997, "C4": 0.3053, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.5246}, - "RA3": {"P": 1.1662, "O1P": -0.776, "O2P": -0.776, "O5'": -0.4989, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0394, "H1'": 0.2007, "N9": -0.0251, "C8": 0.2006, "H8": 0.1553, "N7": -0.6073, "C5": 0.0515, "C6": 0.7009, "N6": -0.9019, "H61": 0.4115, "H62": 0.4115, "N1": -0.7615, "C2": 0.5875, "H2": 0.0473, "N3": -0.6997, "C4": 0.3053, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.6541, "H3T": 0.4376}, - "RA5": {"H5T": 0.4295, "O5'": -0.6223, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0394, "H1'": 0.2007, "N9": -0.0251, "C8": 0.2006, "H8": 0.1553, "N7": -0.6073, "C5": 0.0515, "C6": 0.7009, "N6": -0.9019, "H61": 0.4115, "H62": 0.4115, "N1": -0.7615, "C2": 0.5875, "H2": 0.0473, "N3": -0.6997, "C4": 0.3053, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.5246}, - "RAN": {"H5T": 0.4295, "O5'": -0.6223, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0394, "H1'": 0.2007, "N9": -0.0251, "C8": 0.2006, "H8": 0.1553, "N7": -0.6073, "C5": 0.0515, "C6": 0.7009, "N6": -0.9019, "H61": 0.4115, "H62": 0.4115, "N1": -0.7615, "C2": 0.5875, "H2": 0.0473, "N3": -0.6997, "C4": 0.3053, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.6541, "H3T": 0.4376}, - "RC": {"P": 1.1662, "O1P": -0.776, "O2P": -0.776, "O5'": -0.4989, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0066, "H1'": 0.2029, "N1": -0.0484, "C6": 0.0053, "H6": 0.1958, "C5": -0.5215, "H5": 0.1928, "C4": 0.8185, "N4": -0.953, "H41": 0.4234, "H42": 0.4234, "N3": -0.7584, "C2": 0.7538, "O2": -0.6252, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.5246}, - "RC3": {"P": 1.1662, "O1P": -0.776, "O2P": -0.776, "O5'": -0.4989, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0066, "H1'": 0.2029, "N1": -0.0484, "C6": 0.0053, "H6": 0.1958, "C5": -0.5215, "H5": 0.1928, "C4": 0.8185, "N4": -0.953, "H41": 0.4234, "H42": 0.4234, "N3": -0.7584, "C2": 0.7538, "O2": -0.6252, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.6541, "H3T": 0.4376}, - "RC5": {"H5T": 0.4295, "O5'": -0.6223, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0066, "H1'": 0.2029, "N1": -0.0484, "C6": 0.0053, "H6": 0.1958, "C5": -0.5215, "H5": 0.1928, "C4": 0.8185, "N4": -0.953, "H41": 0.4234, "H42": 0.4234, "N3": -0.7584, "C2": 0.7538, "O2": -0.6252, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.5246}, - "RCN": {"H5T": 0.4295, "O5'": -0.6223, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0066, "H1'": 0.2029, "N1": -0.0484, "C6": 0.0053, "H6": 0.1958, "C5": -0.5215, "H5": 0.1928, "C4": 0.8185, "N4": -0.953, "H41": 0.4234, "H42": 0.4234, "N3": -0.7584, "C2": 0.7538, "O2": -0.6252, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.6541, "H3T": 0.4376}, - "RG": {"P": 1.1662, "O1P": -0.776, "O2P": -0.776, "O5'": -0.4989, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0191, "H1'": 0.2006, "N9": 0.0492, "C8": 0.1374, "H8": 0.164, "N7": -0.5709, "C5": 0.1744, "C6": 0.477, "O6": -0.5597, "N1": -0.4787, "H1": 0.3424, "C2": 0.7657, "N2": -0.9672, "H21": 0.4364, "H22": 0.4364, "N3": -0.6323, "C4": 0.1222, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.5246}, - "RG3": {"P": 1.1662, "O1P": -0.776, "O2P": -0.776, "O5'": -0.4989, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0191, "H1'": 0.2006, "N9": 0.0492, "C8": 0.1374, "H8": 0.164, "N7": -0.5709, "C5": 0.1744, "C6": 0.477, "O6": -0.5597, "N1": -0.4787, "H1": 0.3424, "C2": 0.7657, "N2": -0.9672, "H21": 0.4364, "H22": 0.4364, "N3": -0.6323, "C4": 0.1222, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.6541, "H3T": 0.4376}, - "RG5": {"H5T": 0.4295, "O5'": -0.6223, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0191, "H1'": 0.2006, "N9": 0.0492, "C8": 0.1374, "H8": 0.164, "N7": -0.5709, "C5": 0.1744, "C6": 0.477, "O6": -0.5597, "N1": -0.4787, "H1": 0.3424, "C2": 0.7657, "N2": -0.9672, "H21": 0.4364, "H22": 0.4364, "N3": -0.6323, "C4": 0.1222, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.5246}, - "RGN": {"H5T": 0.4295, "O5'": -0.6223, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0191, "H1'": 0.2006, "N9": 0.0492, "C8": 0.1374, "H8": 0.164, "N7": -0.5709, "C5": 0.1744, "C6": 0.477, "O6": -0.5597, "N1": -0.4787, "H1": 0.3424, "C2": 0.7657, "N2": -0.9672, "H21": 0.4364, "H22": 0.4364, "N3": -0.6323, "C4": 0.1222, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.6541, "H3T": 0.4376}, - "RU": {"P": 1.1662, "O1P": -0.776, "O2P": -0.776, "O5'": -0.4989, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0674, "H1'": 0.1824, "N1": 0.0418, "C6": -0.1126, "H6": 0.2188, "C5": -0.3635, "H5": 0.1811, "C4": 0.5952, "O4": -0.5761, "N3": -0.3549, "H3": 0.3154, "C2": 0.4687, "O2": -0.5477, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.5246}, - "RU3": {"P": 1.1662, "O1P": -0.776, "O2P": -0.776, "O5'": -0.4989, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0674, "H1'": 0.1824, "N1": 0.0418, "C6": -0.1126, "H6": 0.2188, "C5": -0.3635, "H5": 0.1811, "C4": 0.5952, "O4": -0.5761, "N3": -0.3549, "H3": 0.3154, "C2": 0.4687, "O2": -0.5477, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.6541, "H3T": 0.4376}, - "RU5": {"H5T": 0.4295, "O5'": -0.6223, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0674, "H1'": 0.1824, "N1": 0.0418, "C6": -0.1126, "H6": 0.2188, "C5": -0.3635, "H5": 0.1811, "C4": 0.5952, "O4": -0.5761, "N3": -0.3549, "H3": 0.3154, "C2": 0.4687, "O2": -0.5477, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.5246}, - "RUN": {"H5T": 0.4295, "O5'": -0.6223, "C5'": 0.0558, "H5'1": 0.0679, "H5'2": 0.0679, "C4'": 0.1065, "H4'": 0.1174, "O4'": -0.3548, "C1'": 0.0674, "H1'": 0.1824, "N1": 0.0418, "C6": -0.1126, "H6": 0.2188, "C5": -0.3635, "H5": 0.1811, "C4": 0.5952, "O4": -0.5761, "N3": -0.3549, "H3": 0.3154, "C2": 0.4687, "O2": -0.5477, "C3'": 0.2022, "H3'": 0.0615, "C2'": 0.067, "H2'1": 0.0972, "O2'": -0.6139, "HO'2": 0.4186, "O3'": -0.6541, "H3T": 0.4376} + "DA": { + "P": 1.1659, + "O1P": -0.7761, + "O2P": -0.7761, + "O5'": -0.4954, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.0431, + "H1'": 0.1838, + "N9": -0.0268, + "C8": 0.1607, + "H8": 0.1877, + "N7": -0.6175, + "C5": 0.0725, + "C6": 0.6897, + "N6": -0.9123, + "H61": 0.4167, + "H62": 0.4167, + "N1": -0.7624, + "C2": 0.5716, + "H2": 0.0598, + "N3": -0.7417, + "C4": 0.38, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.5232, + }, + "DA3": { + "P": 1.1659, + "O1P": -0.7761, + "O2P": -0.7761, + "O5'": -0.4954, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.0431, + "H1'": 0.1838, + "N9": -0.0268, + "C8": 0.1607, + "H8": 0.1877, + "N7": -0.6175, + "C5": 0.0725, + "C6": 0.6897, + "N6": -0.9123, + "H61": 0.4167, + "H62": 0.4167, + "N1": -0.7624, + "C2": 0.5716, + "H2": 0.0598, + "N3": -0.7417, + "C4": 0.38, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.6549, + "H3T": 0.4396, + }, + "DA5": { + "H5T": 0.4422, + "O5'": -0.6318, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.0431, + "H1'": 0.1838, + "N9": -0.0268, + "C8": 0.1607, + "H8": 0.1877, + "N7": -0.6175, + "C5": 0.0725, + "C6": 0.6897, + "N6": -0.9123, + "H61": 0.4167, + "H62": 0.4167, + "N1": -0.7624, + "C2": 0.5716, + "H2": 0.0598, + "N3": -0.7417, + "C4": 0.38, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.5232, + }, + "DAN": { + "H5T": 0.4422, + "O5'": -0.6318, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.0431, + "H1'": 0.1838, + "N9": -0.0268, + "C8": 0.1607, + "H8": 0.1877, + "N7": -0.6175, + "C5": 0.0725, + "C6": 0.6897, + "N6": -0.9123, + "H61": 0.4167, + "H62": 0.4167, + "N1": -0.7624, + "C2": 0.5716, + "H2": 0.0598, + "N3": -0.7417, + "C4": 0.38, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.6549, + "H3T": 0.4396, + }, + "DC": { + "P": 1.1659, + "O1P": -0.7761, + "O2P": -0.7761, + "O5'": -0.4954, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": -0.0116, + "H1'": 0.1963, + "N1": -0.0339, + "C6": -0.0183, + "H6": 0.2293, + "C5": -0.5222, + "H5": 0.1863, + "C4": 0.8439, + "N4": -0.9773, + "H41": 0.4314, + "H42": 0.4314, + "N3": -0.7748, + "C2": 0.7959, + "O2": -0.6548, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.5232, + }, + "DC3": { + "P": 1.1659, + "O1P": -0.7761, + "O2P": -0.7761, + "O5'": -0.4954, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": -0.0116, + "H1'": 0.1963, + "N1": -0.0339, + "C6": -0.0183, + "H6": 0.2293, + "C5": -0.5222, + "H5": 0.1863, + "C4": 0.8439, + "N4": -0.9773, + "H41": 0.4314, + "H42": 0.4314, + "N3": -0.7748, + "C2": 0.7959, + "O2": -0.6548, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.6549, + "H3T": 0.4396, + }, + "DC5": { + "H5T": 0.4422, + "O5'": -0.6318, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": -0.0116, + "H1'": 0.1963, + "N1": -0.0339, + "C6": -0.0183, + "H6": 0.2293, + "C5": -0.5222, + "H5": 0.1863, + "C4": 0.8439, + "N4": -0.9773, + "H41": 0.4314, + "H42": 0.4314, + "N3": -0.7748, + "C2": 0.7959, + "O2": -0.6548, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.5232, + }, + "DCN": { + "H5T": 0.4422, + "O5'": -0.6318, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": -0.0116, + "H1'": 0.1963, + "N1": -0.0339, + "C6": -0.0183, + "H6": 0.2293, + "C5": -0.5222, + "H5": 0.1863, + "C4": 0.8439, + "N4": -0.9773, + "H41": 0.4314, + "H42": 0.4314, + "N3": -0.7748, + "C2": 0.7959, + "O2": -0.6548, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.6549, + "H3T": 0.4396, + }, + "DG": { + "P": 1.1659, + "O1P": -0.7761, + "O2P": -0.7761, + "O5'": -0.4954, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.0358, + "H1'": 0.1746, + "N9": 0.0577, + "C8": 0.0736, + "H8": 0.1997, + "N7": -0.5725, + "C5": 0.1991, + "C6": 0.4918, + "O6": -0.5699, + "N1": -0.5053, + "H1": 0.352, + "C2": 0.7432, + "N2": -0.923, + "H21": 0.4235, + "H22": 0.4235, + "N3": -0.6636, + "C4": 0.1814, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.5232, + }, + "DG3": { + "P": 1.1659, + "O1P": -0.7761, + "O2P": -0.7761, + "O5'": -0.4954, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.0358, + "H1'": 0.1746, + "N9": 0.0577, + "C8": 0.0736, + "H8": 0.1997, + "N7": -0.5725, + "C5": 0.1991, + "C6": 0.4918, + "O6": -0.5699, + "N1": -0.5053, + "H1": 0.352, + "C2": 0.7432, + "N2": -0.923, + "H21": 0.4235, + "H22": 0.4235, + "N3": -0.6636, + "C4": 0.1814, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.6549, + "H3T": 0.4396, + }, + "DG5": { + "H5T": 0.4422, + "O5'": -0.6318, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.0358, + "H1'": 0.1746, + "N9": 0.0577, + "C8": 0.0736, + "H8": 0.1997, + "N7": -0.5725, + "C5": 0.1991, + "C6": 0.4918, + "O6": -0.5699, + "N1": -0.5053, + "H1": 0.352, + "C2": 0.7432, + "N2": -0.923, + "H21": 0.4235, + "H22": 0.4235, + "N3": -0.6636, + "C4": 0.1814, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.5232, + }, + "DGN": { + "H5T": 0.4422, + "O5'": -0.6318, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.0358, + "H1'": 0.1746, + "N9": 0.0577, + "C8": 0.0736, + "H8": 0.1997, + "N7": -0.5725, + "C5": 0.1991, + "C6": 0.4918, + "O6": -0.5699, + "N1": -0.5053, + "H1": 0.352, + "C2": 0.7432, + "N2": -0.923, + "H21": 0.4235, + "H22": 0.4235, + "N3": -0.6636, + "C4": 0.1814, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.6549, + "H3T": 0.4396, + }, + "DT": { + "P": 1.1659, + "O1P": -0.7761, + "O2P": -0.7761, + "O5'": -0.4954, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.068, + "H1'": 0.1804, + "N1": -0.0239, + "C6": -0.2209, + "H6": 0.2607, + "C5": 0.0025, + "C7": -0.2269, + "H71": 0.077, + "H72": 0.077, + "H73": 0.077, + "C4": 0.5194, + "O4": -0.5563, + "N3": -0.434, + "H3": 0.342, + "C2": 0.5677, + "O2": -0.5881, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.5232, + }, + "DT3": { + "P": 1.1659, + "O1P": -0.7761, + "O2P": -0.7761, + "O5'": -0.4954, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.068, + "H1'": 0.1804, + "N1": -0.0239, + "C6": -0.2209, + "H6": 0.2607, + "C5": 0.0025, + "C7": -0.2269, + "H71": 0.077, + "H72": 0.077, + "H73": 0.077, + "C4": 0.5194, + "O4": -0.5563, + "N3": -0.434, + "H3": 0.342, + "C2": 0.5677, + "O2": -0.5881, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.6549, + "H3T": 0.4396, + }, + "DT5": { + "H5T": 0.4422, + "O5'": -0.6318, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.068, + "H1'": 0.1804, + "N1": -0.0239, + "C6": -0.2209, + "H6": 0.2607, + "C5": 0.0025, + "C7": -0.2269, + "H71": 0.077, + "H72": 0.077, + "H73": 0.077, + "C4": 0.5194, + "O4": -0.5563, + "N3": -0.434, + "H3": 0.342, + "C2": 0.5677, + "O2": -0.5881, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.5232, + }, + "DTN": { + "H5T": 0.4422, + "O5'": -0.6318, + "C5'": -0.0069, + "H5'1": 0.0754, + "H5'2": 0.0754, + "C4'": 0.1629, + "H4'": 0.1176, + "O4'": -0.3691, + "C1'": 0.068, + "H1'": 0.1804, + "N1": -0.0239, + "C6": -0.2209, + "H6": 0.2607, + "C5": 0.0025, + "C7": -0.2269, + "H71": 0.077, + "H72": 0.077, + "H73": 0.077, + "C4": 0.5194, + "O4": -0.5563, + "N3": -0.434, + "H3": 0.342, + "C2": 0.5677, + "O2": -0.5881, + "C3'": 0.0713, + "H3'": 0.0985, + "C2'": -0.0854, + "H2'1": 0.0718, + "H2'2": 0.0718, + "O3'": -0.6549, + "H3T": 0.4396, + }, + "RA": { + "P": 1.1662, + "O1P": -0.776, + "O2P": -0.776, + "O5'": -0.4989, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0394, + "H1'": 0.2007, + "N9": -0.0251, + "C8": 0.2006, + "H8": 0.1553, + "N7": -0.6073, + "C5": 0.0515, + "C6": 0.7009, + "N6": -0.9019, + "H61": 0.4115, + "H62": 0.4115, + "N1": -0.7615, + "C2": 0.5875, + "H2": 0.0473, + "N3": -0.6997, + "C4": 0.3053, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.5246, + }, + "RA3": { + "P": 1.1662, + "O1P": -0.776, + "O2P": -0.776, + "O5'": -0.4989, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0394, + "H1'": 0.2007, + "N9": -0.0251, + "C8": 0.2006, + "H8": 0.1553, + "N7": -0.6073, + "C5": 0.0515, + "C6": 0.7009, + "N6": -0.9019, + "H61": 0.4115, + "H62": 0.4115, + "N1": -0.7615, + "C2": 0.5875, + "H2": 0.0473, + "N3": -0.6997, + "C4": 0.3053, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.6541, + "H3T": 0.4376, + }, + "RA5": { + "H5T": 0.4295, + "O5'": -0.6223, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0394, + "H1'": 0.2007, + "N9": -0.0251, + "C8": 0.2006, + "H8": 0.1553, + "N7": -0.6073, + "C5": 0.0515, + "C6": 0.7009, + "N6": -0.9019, + "H61": 0.4115, + "H62": 0.4115, + "N1": -0.7615, + "C2": 0.5875, + "H2": 0.0473, + "N3": -0.6997, + "C4": 0.3053, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.5246, + }, + "RAN": { + "H5T": 0.4295, + "O5'": -0.6223, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0394, + "H1'": 0.2007, + "N9": -0.0251, + "C8": 0.2006, + "H8": 0.1553, + "N7": -0.6073, + "C5": 0.0515, + "C6": 0.7009, + "N6": -0.9019, + "H61": 0.4115, + "H62": 0.4115, + "N1": -0.7615, + "C2": 0.5875, + "H2": 0.0473, + "N3": -0.6997, + "C4": 0.3053, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.6541, + "H3T": 0.4376, + }, + "RC": { + "P": 1.1662, + "O1P": -0.776, + "O2P": -0.776, + "O5'": -0.4989, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0066, + "H1'": 0.2029, + "N1": -0.0484, + "C6": 0.0053, + "H6": 0.1958, + "C5": -0.5215, + "H5": 0.1928, + "C4": 0.8185, + "N4": -0.953, + "H41": 0.4234, + "H42": 0.4234, + "N3": -0.7584, + "C2": 0.7538, + "O2": -0.6252, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.5246, + }, + "RC3": { + "P": 1.1662, + "O1P": -0.776, + "O2P": -0.776, + "O5'": -0.4989, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0066, + "H1'": 0.2029, + "N1": -0.0484, + "C6": 0.0053, + "H6": 0.1958, + "C5": -0.5215, + "H5": 0.1928, + "C4": 0.8185, + "N4": -0.953, + "H41": 0.4234, + "H42": 0.4234, + "N3": -0.7584, + "C2": 0.7538, + "O2": -0.6252, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.6541, + "H3T": 0.4376, + }, + "RC5": { + "H5T": 0.4295, + "O5'": -0.6223, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0066, + "H1'": 0.2029, + "N1": -0.0484, + "C6": 0.0053, + "H6": 0.1958, + "C5": -0.5215, + "H5": 0.1928, + "C4": 0.8185, + "N4": -0.953, + "H41": 0.4234, + "H42": 0.4234, + "N3": -0.7584, + "C2": 0.7538, + "O2": -0.6252, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.5246, + }, + "RCN": { + "H5T": 0.4295, + "O5'": -0.6223, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0066, + "H1'": 0.2029, + "N1": -0.0484, + "C6": 0.0053, + "H6": 0.1958, + "C5": -0.5215, + "H5": 0.1928, + "C4": 0.8185, + "N4": -0.953, + "H41": 0.4234, + "H42": 0.4234, + "N3": -0.7584, + "C2": 0.7538, + "O2": -0.6252, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.6541, + "H3T": 0.4376, + }, + "RG": { + "P": 1.1662, + "O1P": -0.776, + "O2P": -0.776, + "O5'": -0.4989, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0191, + "H1'": 0.2006, + "N9": 0.0492, + "C8": 0.1374, + "H8": 0.164, + "N7": -0.5709, + "C5": 0.1744, + "C6": 0.477, + "O6": -0.5597, + "N1": -0.4787, + "H1": 0.3424, + "C2": 0.7657, + "N2": -0.9672, + "H21": 0.4364, + "H22": 0.4364, + "N3": -0.6323, + "C4": 0.1222, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.5246, + }, + "RG3": { + "P": 1.1662, + "O1P": -0.776, + "O2P": -0.776, + "O5'": -0.4989, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0191, + "H1'": 0.2006, + "N9": 0.0492, + "C8": 0.1374, + "H8": 0.164, + "N7": -0.5709, + "C5": 0.1744, + "C6": 0.477, + "O6": -0.5597, + "N1": -0.4787, + "H1": 0.3424, + "C2": 0.7657, + "N2": -0.9672, + "H21": 0.4364, + "H22": 0.4364, + "N3": -0.6323, + "C4": 0.1222, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.6541, + "H3T": 0.4376, + }, + "RG5": { + "H5T": 0.4295, + "O5'": -0.6223, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0191, + "H1'": 0.2006, + "N9": 0.0492, + "C8": 0.1374, + "H8": 0.164, + "N7": -0.5709, + "C5": 0.1744, + "C6": 0.477, + "O6": -0.5597, + "N1": -0.4787, + "H1": 0.3424, + "C2": 0.7657, + "N2": -0.9672, + "H21": 0.4364, + "H22": 0.4364, + "N3": -0.6323, + "C4": 0.1222, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.5246, + }, + "RGN": { + "H5T": 0.4295, + "O5'": -0.6223, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0191, + "H1'": 0.2006, + "N9": 0.0492, + "C8": 0.1374, + "H8": 0.164, + "N7": -0.5709, + "C5": 0.1744, + "C6": 0.477, + "O6": -0.5597, + "N1": -0.4787, + "H1": 0.3424, + "C2": 0.7657, + "N2": -0.9672, + "H21": 0.4364, + "H22": 0.4364, + "N3": -0.6323, + "C4": 0.1222, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.6541, + "H3T": 0.4376, + }, + "RU": { + "P": 1.1662, + "O1P": -0.776, + "O2P": -0.776, + "O5'": -0.4989, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0674, + "H1'": 0.1824, + "N1": 0.0418, + "C6": -0.1126, + "H6": 0.2188, + "C5": -0.3635, + "H5": 0.1811, + "C4": 0.5952, + "O4": -0.5761, + "N3": -0.3549, + "H3": 0.3154, + "C2": 0.4687, + "O2": -0.5477, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.5246, + }, + "RU3": { + "P": 1.1662, + "O1P": -0.776, + "O2P": -0.776, + "O5'": -0.4989, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0674, + "H1'": 0.1824, + "N1": 0.0418, + "C6": -0.1126, + "H6": 0.2188, + "C5": -0.3635, + "H5": 0.1811, + "C4": 0.5952, + "O4": -0.5761, + "N3": -0.3549, + "H3": 0.3154, + "C2": 0.4687, + "O2": -0.5477, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.6541, + "H3T": 0.4376, + }, + "RU5": { + "H5T": 0.4295, + "O5'": -0.6223, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0674, + "H1'": 0.1824, + "N1": 0.0418, + "C6": -0.1126, + "H6": 0.2188, + "C5": -0.3635, + "H5": 0.1811, + "C4": 0.5952, + "O4": -0.5761, + "N3": -0.3549, + "H3": 0.3154, + "C2": 0.4687, + "O2": -0.5477, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.5246, + }, + "RUN": { + "H5T": 0.4295, + "O5'": -0.6223, + "C5'": 0.0558, + "H5'1": 0.0679, + "H5'2": 0.0679, + "C4'": 0.1065, + "H4'": 0.1174, + "O4'": -0.3548, + "C1'": 0.0674, + "H1'": 0.1824, + "N1": 0.0418, + "C6": -0.1126, + "H6": 0.2188, + "C5": -0.3635, + "H5": 0.1811, + "C4": 0.5952, + "O4": -0.5761, + "N3": -0.3549, + "H3": 0.3154, + "C2": 0.4687, + "O2": -0.5477, + "C3'": 0.2022, + "H3'": 0.0615, + "C2'": 0.067, + "H2'1": 0.0972, + "O2'": -0.6139, + "HO'2": 0.4186, + "O3'": -0.6541, + "H3T": 0.4376, + }, } lipophobicity = { # standard amino acids - 'ALA': {'C': -0.61, 'CA': 0.02, 'CB': 0.62, 'O': -0.58, 'N': -0.49, 'H': -0.5, 'HA': -0.25, 'HB1': 0.0, 'HB2': 0.0, 'HB3': 0.0, 'OXT': 0.49}, - 'ARG': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD': 0.45, 'CG': 0.45, 'CZ': -0.61, 'N': -0.49, 'NE': -0.49, 'NH1': -0.14, 'NH2': -0.69, 'O': -0.58, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HG2': 0.0, 'HG3': 0.0, 'HD2': -0.25, 'HD3': -0.25, 'HE': -0.5, 'HH11': -0.5, 'HH12': -0.5, 'HH21': -0.5, 'HH22': -0.5, '1HH1': -0.5, '2HH1': -0.5, '1HH2': -0.5, '2HH2': -0.5, 'OXT': 0.49}, - 'ASN': {'C': -0.61, 'CA': 0.02, 'CB': 0.02, 'CG': -0.61, 'N': -0.49, 'ND2': -0.14, 'O': -0.58, 'OD1': -0.58, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HD21': -0.5, 'HD22': -0.5, '1HD2': -0.5, '2HD2': -0.5, 'OXT': 0.49}, - 'ASP': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CG': -0.61, 'N': -0.49, 'O': -0.58, 'OD1': -0.58, 'OD2': 0.49, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'OXT': 0.49}, - 'CYS': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'N': -0.49, 'O': -0.58, 'SG': 0.29, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'OXT': 0.49}, - 'GLN': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD': -0.54, 'CG': 0.45, 'N': -0.49, 'NE2': -0.14, 'O': -0.58, 'OE1': -0.58, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HG2': 0.0, 'HG3': 0.0, 'HE21': -0.5, 'HE22': -0.5, '1HE2': -0.5, '2HE2': -0.5, 'OXT': 0.49}, - 'GLU': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD': -0.54, 'CG': 0.45, 'N': -0.49, 'O': -0.58, 'OE1': -0.58, 'OE2': 0.49, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HG2': 0.0, 'HG3': 0.0, 'OXT': 0.49}, - 'GLY': {'C': -0.61, 'CA': 0.45, 'O': -0.58, 'N': -0.57, 'H': -0.5, 'HA': -0.25, 'HA2': 0.0, 'HA3': 0.0, 'OXT': 0.49}, - 'HIS': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD2': 0.31, 'CE1': 0.31, 'CG': 0.1, 'N': -0.49, 'ND1': 0.08, 'NE2': -1.14, 'O': -0.58, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HD1': -0.5, 'HD2': -0.25, 'HE1': -0.25, 'OXT': 0.49}, - 'ILE': {'C': -0.61, 'CA': 0.02, 'CB': 0.02, 'CD': 0.63, 'CD1': 0.63, 'CG1': 0.45, 'CG2': 0.63, 'N': -0.49, 'O': -0.58, 'H': -0.5, 'HA': -0.25, 'HB': 0.0, 'HG12': 0.0, 'HG13': 0.0, 'HG21': 0.0, 'HG22': 0.0, 'HG23': 0.0, 'HD11': 0.0, 'HD12': 0.0, 'HD13': 0.0, '2HG1': 0.0, '3HG1': 0.0, '1HG2': 0.0, '2HG2': 0.0, '3HG2': 0.0, '1HD1': 0.0, '2HD1': 0.0, '3HD1': 0.0, 'OXT': 0.49}, - 'LEU': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD1': 0.63, 'CD2': 0.63, 'CG': 0.02, 'N': -0.49, 'O': -0.58, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HG': 0.0, 'HD11': 0.0, 'HD12': 0.0, 'HD13': 0.0, 'HD21': 0.0, 'HD22': 0.0, 'HD23': 0.0, '1HD1': 0.0, '2HD1': 0.0, '3HD1': 0.0, '1HD2': 0.0, '2HD2': 0.0, '3HD2': 0.0, 'OXT': 0.49}, - 'LYS': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD': 0.45, 'CE': 0.45, 'CG': 0.45, 'N': -0.49, 'NZ': -1.07, 'O': -0.58, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HG2': 0.0, 'HG3': 0.0, 'HD2': 0.0, 'HD3': 0.0, 'HE2': -0.2, 'HE3': -0.2, 'HZ1': -0.5, 'HZ2': -0.5, 'HZ3': -0.5, 'OXT': 0.49, }, - 'MET': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CE': 0.63, 'CG': 0.45, 'N': -0.49, 'O': -0.58, 'SD': -0.30, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HG2': 0.0, 'HG3': 0.0, 'HE1': 0.0, 'HE2': 0.0, 'HE3': -0.5, 'OXT': 0.49, }, - 'PHE': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD1': 0.31, 'CD2': 0.31, 'CE1': 0.31, 'CE2': 0.31, 'CG': 0.1, 'CZ': 0.31, 'N': -0.49, 'O': -0.58, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HD1': 0.0, 'HD2': 0.0, 'HE1': 0.0, 'HE2': 0.0, 'HZ': 0.0, 'OXT': 0.49}, - 'PRO': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD': 0.45, 'CG': 0.45, 'N': -0.92, 'O': -0.58, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HG2': 0.0, 'HG3': 0.0, 'HD2': -0.2, 'HD3': -0.2, 'OXT': 0.49, }, - 'SER': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'N': -0.49, 'O': -0.58, 'OG': -0.99, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HG': 0.0, 'OXT': 0.49, }, - 'THR': {'C': -0.61, 'CA': 0.02, 'CB': 0.02, 'CG2': 0.62, 'N': -0.49, 'O': -0.58, 'OG1': -0.9, 'H': -0.5, 'HA': -0.25, 'HB': 0.0, 'HG1': 0.0, 'HG21': 0.0, 'HG22': 0.0, 'HG23': 0.0, '1HG2': 0.0, '2HG2': 0.0, '3HG2': 0.0, 'OXT': 0.49, }, - 'TRP': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD1': 0.31, 'CD2': 0.25, 'CE2': 0.25, 'CE3': 0.31, 'CG': 0.1, 'CH2': 0.31, 'CZ2': 0.31, 'CZ3': 0.31, 'N': -0.49, 'NE1': 0.08, 'O': -0.58, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HE1': -0.5, 'HD1': -0.2, 'HE3': 0.0, 'HZ2': 0.0, 'HZ3': 0.0, 'HH2': 0.0, 'OXT': 0.49, }, - 'TYR': {'C': -0.61, 'CA': 0.02, 'CB': 0.45, 'CD1': 0.31, 'CD2': 0.31, 'CE1': 0.31, 'CE2': 0.31, 'CG': 0.1, 'CZ': 0.1, 'N': -0.49, 'O': -0.58, 'OH': -0.17, 'H': -0.5, 'HA': -0.25, 'HB2': 0.0, 'HB3': 0.0, 'HD1': 0.0, 'HD2': 0.0, 'HE1': 0.0, 'HE2': 0.0, 'HH': 0.0, 'OXT': 0.49, }, - 'VAL': {'C': -0.61, 'CA': 0.02, 'CB': 0.02, 'CG1': 0.62, 'CG2': 0.62, 'N': -0.49, 'O': -0.58, 'H': -0.5, 'HA': -0.25, 'HB': 0.0, 'HG11': 0.0, 'HG12': 0.0, 'HG13': 0.0, 'HG21': 0.0, 'HG22': 0.0, 'HG23': 0.0, '1HG1': 0.0, '2HG1': 0.0, '3HG1': 0.0, '1HG2': 0.0, '2HG2': 0.0, '3HG2': 0.0, 'OXT': 0.49, }, - + "ALA": { + "C": -0.61, + "CA": 0.02, + "CB": 0.62, + "O": -0.58, + "N": -0.49, + "H": -0.5, + "HA": -0.25, + "HB1": 0.0, + "HB2": 0.0, + "HB3": 0.0, + "OXT": 0.49, + }, + "ARG": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD": 0.45, + "CG": 0.45, + "CZ": -0.61, + "N": -0.49, + "NE": -0.49, + "NH1": -0.14, + "NH2": -0.69, + "O": -0.58, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HG2": 0.0, + "HG3": 0.0, + "HD2": -0.25, + "HD3": -0.25, + "HE": -0.5, + "HH11": -0.5, + "HH12": -0.5, + "HH21": -0.5, + "HH22": -0.5, + "1HH1": -0.5, + "2HH1": -0.5, + "1HH2": -0.5, + "2HH2": -0.5, + "OXT": 0.49, + }, + "ASN": { + "C": -0.61, + "CA": 0.02, + "CB": 0.02, + "CG": -0.61, + "N": -0.49, + "ND2": -0.14, + "O": -0.58, + "OD1": -0.58, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HD21": -0.5, + "HD22": -0.5, + "1HD2": -0.5, + "2HD2": -0.5, + "OXT": 0.49, + }, + "ASP": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CG": -0.61, + "N": -0.49, + "O": -0.58, + "OD1": -0.58, + "OD2": 0.49, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "OXT": 0.49, + }, + "CYS": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "N": -0.49, + "O": -0.58, + "SG": 0.29, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "OXT": 0.49, + }, + "GLN": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD": -0.54, + "CG": 0.45, + "N": -0.49, + "NE2": -0.14, + "O": -0.58, + "OE1": -0.58, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HG2": 0.0, + "HG3": 0.0, + "HE21": -0.5, + "HE22": -0.5, + "1HE2": -0.5, + "2HE2": -0.5, + "OXT": 0.49, + }, + "GLU": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD": -0.54, + "CG": 0.45, + "N": -0.49, + "O": -0.58, + "OE1": -0.58, + "OE2": 0.49, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HG2": 0.0, + "HG3": 0.0, + "OXT": 0.49, + }, + "GLY": { + "C": -0.61, + "CA": 0.45, + "O": -0.58, + "N": -0.57, + "H": -0.5, + "HA": -0.25, + "HA2": 0.0, + "HA3": 0.0, + "OXT": 0.49, + }, + "HIS": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD2": 0.31, + "CE1": 0.31, + "CG": 0.1, + "N": -0.49, + "ND1": 0.08, + "NE2": -1.14, + "O": -0.58, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HD1": -0.5, + "HD2": -0.25, + "HE1": -0.25, + "OXT": 0.49, + }, + "ILE": { + "C": -0.61, + "CA": 0.02, + "CB": 0.02, + "CD": 0.63, + "CD1": 0.63, + "CG1": 0.45, + "CG2": 0.63, + "N": -0.49, + "O": -0.58, + "H": -0.5, + "HA": -0.25, + "HB": 0.0, + "HG12": 0.0, + "HG13": 0.0, + "HG21": 0.0, + "HG22": 0.0, + "HG23": 0.0, + "HD11": 0.0, + "HD12": 0.0, + "HD13": 0.0, + "2HG1": 0.0, + "3HG1": 0.0, + "1HG2": 0.0, + "2HG2": 0.0, + "3HG2": 0.0, + "1HD1": 0.0, + "2HD1": 0.0, + "3HD1": 0.0, + "OXT": 0.49, + }, + "LEU": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD1": 0.63, + "CD2": 0.63, + "CG": 0.02, + "N": -0.49, + "O": -0.58, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HG": 0.0, + "HD11": 0.0, + "HD12": 0.0, + "HD13": 0.0, + "HD21": 0.0, + "HD22": 0.0, + "HD23": 0.0, + "1HD1": 0.0, + "2HD1": 0.0, + "3HD1": 0.0, + "1HD2": 0.0, + "2HD2": 0.0, + "3HD2": 0.0, + "OXT": 0.49, + }, + "LYS": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD": 0.45, + "CE": 0.45, + "CG": 0.45, + "N": -0.49, + "NZ": -1.07, + "O": -0.58, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HG2": 0.0, + "HG3": 0.0, + "HD2": 0.0, + "HD3": 0.0, + "HE2": -0.2, + "HE3": -0.2, + "HZ1": -0.5, + "HZ2": -0.5, + "HZ3": -0.5, + "OXT": 0.49, + }, + "MET": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CE": 0.63, + "CG": 0.45, + "N": -0.49, + "O": -0.58, + "SD": -0.30, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HG2": 0.0, + "HG3": 0.0, + "HE1": 0.0, + "HE2": 0.0, + "HE3": -0.5, + "OXT": 0.49, + }, + "PHE": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD1": 0.31, + "CD2": 0.31, + "CE1": 0.31, + "CE2": 0.31, + "CG": 0.1, + "CZ": 0.31, + "N": -0.49, + "O": -0.58, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HD1": 0.0, + "HD2": 0.0, + "HE1": 0.0, + "HE2": 0.0, + "HZ": 0.0, + "OXT": 0.49, + }, + "PRO": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD": 0.45, + "CG": 0.45, + "N": -0.92, + "O": -0.58, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HG2": 0.0, + "HG3": 0.0, + "HD2": -0.2, + "HD3": -0.2, + "OXT": 0.49, + }, + "SER": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "N": -0.49, + "O": -0.58, + "OG": -0.99, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HG": 0.0, + "OXT": 0.49, + }, + "THR": { + "C": -0.61, + "CA": 0.02, + "CB": 0.02, + "CG2": 0.62, + "N": -0.49, + "O": -0.58, + "OG1": -0.9, + "H": -0.5, + "HA": -0.25, + "HB": 0.0, + "HG1": 0.0, + "HG21": 0.0, + "HG22": 0.0, + "HG23": 0.0, + "1HG2": 0.0, + "2HG2": 0.0, + "3HG2": 0.0, + "OXT": 0.49, + }, + "TRP": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD1": 0.31, + "CD2": 0.25, + "CE2": 0.25, + "CE3": 0.31, + "CG": 0.1, + "CH2": 0.31, + "CZ2": 0.31, + "CZ3": 0.31, + "N": -0.49, + "NE1": 0.08, + "O": -0.58, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HE1": -0.5, + "HD1": -0.2, + "HE3": 0.0, + "HZ2": 0.0, + "HZ3": 0.0, + "HH2": 0.0, + "OXT": 0.49, + }, + "TYR": { + "C": -0.61, + "CA": 0.02, + "CB": 0.45, + "CD1": 0.31, + "CD2": 0.31, + "CE1": 0.31, + "CE2": 0.31, + "CG": 0.1, + "CZ": 0.1, + "N": -0.49, + "O": -0.58, + "OH": -0.17, + "H": -0.5, + "HA": -0.25, + "HB2": 0.0, + "HB3": 0.0, + "HD1": 0.0, + "HD2": 0.0, + "HE1": 0.0, + "HE2": 0.0, + "HH": 0.0, + "OXT": 0.49, + }, + "VAL": { + "C": -0.61, + "CA": 0.02, + "CB": 0.02, + "CG1": 0.62, + "CG2": 0.62, + "N": -0.49, + "O": -0.58, + "H": -0.5, + "HA": -0.25, + "HB": 0.0, + "HG11": 0.0, + "HG12": 0.0, + "HG13": 0.0, + "HG21": 0.0, + "HG22": 0.0, + "HG23": 0.0, + "1HG1": 0.0, + "2HG1": 0.0, + "3HG1": 0.0, + "1HG2": 0.0, + "2HG2": 0.0, + "3HG2": 0.0, + "OXT": 0.49, + }, # other potential residues - 'CA': {'CA': -1.0}, - 'NAG': {'C1': 0.02, 'C2': 0.02, 'C3': 0.02, 'C4': 0.02, 'C5': 0.02, 'C6': 0.31, 'C7': -0.61, 'C8': 0.62, 'O1': -0.92, 'O2': -0.92, 'O3': -0.92, 'O4': -0.92, 'O5': -1.14, 'O6': -0.99, 'O7': -0.58, 'N2': -0.49, 'H2': -0.25, 'HN2': -0.5, }, - 'NDG': {'C1': 0.02, 'C2': 0.02, 'C3': 0.02, 'C4': 0.02, 'C5': 0.02, 'C6': 0.031, 'C7': -0.61, 'C8': 0.62, 'O1L': -0.9, 'O3': -0.92, 'O4': -0.92, 'O': -1.14, 'O6': -0.99, 'O7': -0.58, 'N2': -0.29, }, - 'BMA': {'C1': 0.02, 'C2': 0.02, 'C3': 0.02, 'C4': 0.02, 'C5': 0.02, 'C6': 0.31, 'O1': -0.92, 'O2': -0.92, 'O3': -0.92, 'O4': -0.92, 'O5': -1.14, 'O6': -0.58, }, - 'MAN': {'C1': 0.02, 'C2': 0.02, 'C3': 0.02, 'C4': 0.02, 'C5': 0.02, 'C6': 0.31, 'O1': -0.92, 'O2': -0.92, 'O3': -0.92, 'O4': -0.92, 'O5': -1.14, 'O6': -0.58, }, - 'GAL': {'C1': 0.02, 'C2': 0.02, 'C3': 0.02, 'C4': 0.02, 'C5': 0.02, 'C6': 0.31, 'O1': -0.92, 'O2': -0.92, 'O3': -0.92, 'O4': -0.92, 'O5': -1.14, 'O6': -0.58, }, - 'NAN': {'C1': -0.61, 'C2': 0.02, 'C3': 0.62, 'C4': 0.02, 'C5': 0.02, 'C6': 0.02, 'C7': 0.02, 'C8': 0.02, 'C9': 0.31, 'C10': -0.6, 'C11': 0.62, 'O1A': -0.2, 'O1B': -0.4, 'O2': -0.92, 'O4': -0.92, 'O6': -1.14, 'O7': -0.92, 'O8': -0.92, 'O9': -0.7, 'O10': -0.2, 'N5': -0.49, 'NH5': -0.5, }, - 'DG': {'P': -0.94, 'O1P': -0.7, 'O2P': -0.22, "O5'": -0.5, "C5'": 0.45, "C4'": 0.02, "O4'": -1.14, "C1'": 0.02, "C2'": 0.45, "C3'": 0.02, "O3'": -0.92, 'N9': -1.66, 'C8': 0.31, 'N7': -0.55, 'C5': 0.25, 'C6': 0.1, 'O6': -0.58, 'N1': -0.49, 'C2': 0.1, 'N2': -0.6, 'N3': -0.07, 'C4': -0.25, }, - 'DA': {'P': -0.94, 'O1P': -0.7, 'O2P': -0.22, "O5'": -0.5, "C5'": 0.45, "C4'": 0.02, "O4'": -1.14, "C1'": 0.02, "C2'": 0.45, "C3'": 0.02, "O3'": -0.92, 'N9': -1.66, 'C8': 0.31, 'N7': -0.55, 'C5': 0.25, 'C6': 0.1, 'N6': -0.6, 'N1': -0.49, 'C2': 0.31, 'N2': -0.6, 'N3': -0.07, 'C4': -0.25, }, - 'DC': {'P': -0.94, 'O1P': -0.7, 'O2P': -0.22, "O5'": -0.5, "C5'": 0.45, "C4'": 0.02, "O4'": -1.14, "C1'": 0.02, "C2'": 0.45, "C3'": 0.02, "O3'": -0.92, 'N1': -1.66, 'C2': 0.1, 'O2': -0.58, 'N3': -0.29, 'C4': 0.1, 'N4': -0.6, 'C5': 0.31, 'C6': 0.31}, - 'DT': {'P': -0.94, 'O1P': -0.7, 'O2P': -0.22, "O5'": -0.5, "C5'": 0.45, "C4'": 0.02, "O4'": -1.14, "C1'": 0.02, "C2'": 0.45, "C3'": 0.02, "O3'": -0.92, 'N1': -1.66, 'C2': 0.1, 'O2': -0.58, 'N3': 0.16, 'C4': 0.25, 'O4': -0.58, 'C5': 0.1, 'C6': 0.31, 'C7': 0.45}, - 'G': {'P': -0.94, 'O1P': -0.7, 'O2P': -0.22, "O5'": -0.5, "C5'": 0.45, "C4'": 0.02, "O4'": -1.14, "C1'": 0.02, "C2'": 0.02, "O2'": -0.92, "C3'": 0.02, "O3'": -0.92, 'N9': -1.66, 'C8': 0.31, 'N7': -0.55, 'C5': 0.25, 'C6': 0.1, 'O6': -0.58, 'N1': -0.49, 'C2': 0.1, 'N2': -0.6, 'N3': -0.07, 'C4': -0.25}, - 'A': {'P': -0.94, 'O1P': -0.7, 'O2P': -0.22, "O5'": -0.5, "C5'": 0.45, "C4'": 0.02, "O4'": -1.14, "C1'": 0.02, "C2'": 0.02, "O2'": -0.92, "C3'": 0.02, "O3'": -0.92, 'N9': -1.66, 'C8': 0.31, 'N7': -0.55, 'C5': 0.25, 'C6': 0.1, 'N6': -0.6, 'N1': -0.49, 'C2': 0.31, 'N2': -0.6, 'N3': -0.07, 'C4': -0.25}, - 'C': {'P': -0.94, 'O1P': -0.7, 'O2P': -0.22, "O5'": -0.5, "C5'": 0.45, "C4'": 0.02, "O4'": -1.14, "C1'": 0.02, "C2'": 0.02, "O2'": -0.92, "C3'": 0.02, "O3'": -0.92, 'N1': -1.66, 'C2': 0.1, 'O2': -0.58, 'N3': -0.29, 'C4': 0.1, 'N4': -0.6, 'C5': 0.31, 'C6': 0.31}, - 'U': {'P': -0.94, 'O1P': -0.7, 'O2P': -0.22, "O5'": -0.5, "C5'": 0.45, "C4'": 0.02, "O4'": -1.14, "C1'": 0.02, "C2'": 0.02, "O2'": -0.92, "C3'": 0.02, "O3'": -0.92, 'N1': -1.66, 'C2': 0.1, 'O2': -0.58, 'N3': 0.16, 'C4': 0.25, 'O4': -0.58, 'C5': 0.1, 'C6': 0.31}, + "CA": {"CA": -1.0}, + "NAG": { + "C1": 0.02, + "C2": 0.02, + "C3": 0.02, + "C4": 0.02, + "C5": 0.02, + "C6": 0.31, + "C7": -0.61, + "C8": 0.62, + "O1": -0.92, + "O2": -0.92, + "O3": -0.92, + "O4": -0.92, + "O5": -1.14, + "O6": -0.99, + "O7": -0.58, + "N2": -0.49, + "H2": -0.25, + "HN2": -0.5, + }, + "NDG": { + "C1": 0.02, + "C2": 0.02, + "C3": 0.02, + "C4": 0.02, + "C5": 0.02, + "C6": 0.031, + "C7": -0.61, + "C8": 0.62, + "O1L": -0.9, + "O3": -0.92, + "O4": -0.92, + "O": -1.14, + "O6": -0.99, + "O7": -0.58, + "N2": -0.29, + }, + "BMA": { + "C1": 0.02, + "C2": 0.02, + "C3": 0.02, + "C4": 0.02, + "C5": 0.02, + "C6": 0.31, + "O1": -0.92, + "O2": -0.92, + "O3": -0.92, + "O4": -0.92, + "O5": -1.14, + "O6": -0.58, + }, + "MAN": { + "C1": 0.02, + "C2": 0.02, + "C3": 0.02, + "C4": 0.02, + "C5": 0.02, + "C6": 0.31, + "O1": -0.92, + "O2": -0.92, + "O3": -0.92, + "O4": -0.92, + "O5": -1.14, + "O6": -0.58, + }, + "GAL": { + "C1": 0.02, + "C2": 0.02, + "C3": 0.02, + "C4": 0.02, + "C5": 0.02, + "C6": 0.31, + "O1": -0.92, + "O2": -0.92, + "O3": -0.92, + "O4": -0.92, + "O5": -1.14, + "O6": -0.58, + }, + "NAN": { + "C1": -0.61, + "C2": 0.02, + "C3": 0.62, + "C4": 0.02, + "C5": 0.02, + "C6": 0.02, + "C7": 0.02, + "C8": 0.02, + "C9": 0.31, + "C10": -0.6, + "C11": 0.62, + "O1A": -0.2, + "O1B": -0.4, + "O2": -0.92, + "O4": -0.92, + "O6": -1.14, + "O7": -0.92, + "O8": -0.92, + "O9": -0.7, + "O10": -0.2, + "N5": -0.49, + "NH5": -0.5, + }, + "DG": { + "P": -0.94, + "O1P": -0.7, + "O2P": -0.22, + "O5'": -0.5, + "C5'": 0.45, + "C4'": 0.02, + "O4'": -1.14, + "C1'": 0.02, + "C2'": 0.45, + "C3'": 0.02, + "O3'": -0.92, + "N9": -1.66, + "C8": 0.31, + "N7": -0.55, + "C5": 0.25, + "C6": 0.1, + "O6": -0.58, + "N1": -0.49, + "C2": 0.1, + "N2": -0.6, + "N3": -0.07, + "C4": -0.25, + }, + "DA": { + "P": -0.94, + "O1P": -0.7, + "O2P": -0.22, + "O5'": -0.5, + "C5'": 0.45, + "C4'": 0.02, + "O4'": -1.14, + "C1'": 0.02, + "C2'": 0.45, + "C3'": 0.02, + "O3'": -0.92, + "N9": -1.66, + "C8": 0.31, + "N7": -0.55, + "C5": 0.25, + "C6": 0.1, + "N6": -0.6, + "N1": -0.49, + "C2": 0.31, + "N2": -0.6, + "N3": -0.07, + "C4": -0.25, + }, + "DC": { + "P": -0.94, + "O1P": -0.7, + "O2P": -0.22, + "O5'": -0.5, + "C5'": 0.45, + "C4'": 0.02, + "O4'": -1.14, + "C1'": 0.02, + "C2'": 0.45, + "C3'": 0.02, + "O3'": -0.92, + "N1": -1.66, + "C2": 0.1, + "O2": -0.58, + "N3": -0.29, + "C4": 0.1, + "N4": -0.6, + "C5": 0.31, + "C6": 0.31, + }, + "DT": { + "P": -0.94, + "O1P": -0.7, + "O2P": -0.22, + "O5'": -0.5, + "C5'": 0.45, + "C4'": 0.02, + "O4'": -1.14, + "C1'": 0.02, + "C2'": 0.45, + "C3'": 0.02, + "O3'": -0.92, + "N1": -1.66, + "C2": 0.1, + "O2": -0.58, + "N3": 0.16, + "C4": 0.25, + "O4": -0.58, + "C5": 0.1, + "C6": 0.31, + "C7": 0.45, + }, + "G": { + "P": -0.94, + "O1P": -0.7, + "O2P": -0.22, + "O5'": -0.5, + "C5'": 0.45, + "C4'": 0.02, + "O4'": -1.14, + "C1'": 0.02, + "C2'": 0.02, + "O2'": -0.92, + "C3'": 0.02, + "O3'": -0.92, + "N9": -1.66, + "C8": 0.31, + "N7": -0.55, + "C5": 0.25, + "C6": 0.1, + "O6": -0.58, + "N1": -0.49, + "C2": 0.1, + "N2": -0.6, + "N3": -0.07, + "C4": -0.25, + }, + "A": { + "P": -0.94, + "O1P": -0.7, + "O2P": -0.22, + "O5'": -0.5, + "C5'": 0.45, + "C4'": 0.02, + "O4'": -1.14, + "C1'": 0.02, + "C2'": 0.02, + "O2'": -0.92, + "C3'": 0.02, + "O3'": -0.92, + "N9": -1.66, + "C8": 0.31, + "N7": -0.55, + "C5": 0.25, + "C6": 0.1, + "N6": -0.6, + "N1": -0.49, + "C2": 0.31, + "N2": -0.6, + "N3": -0.07, + "C4": -0.25, + }, + "C": { + "P": -0.94, + "O1P": -0.7, + "O2P": -0.22, + "O5'": -0.5, + "C5'": 0.45, + "C4'": 0.02, + "O4'": -1.14, + "C1'": 0.02, + "C2'": 0.02, + "O2'": -0.92, + "C3'": 0.02, + "O3'": -0.92, + "N1": -1.66, + "C2": 0.1, + "O2": -0.58, + "N3": -0.29, + "C4": 0.1, + "N4": -0.6, + "C5": 0.31, + "C6": 0.31, + }, + "U": { + "P": -0.94, + "O1P": -0.7, + "O2P": -0.22, + "O5'": -0.5, + "C5'": 0.45, + "C4'": 0.02, + "O4'": -1.14, + "C1'": 0.02, + "C2'": 0.02, + "O2'": -0.92, + "C3'": 0.02, + "O3'": -0.92, + "N1": -1.66, + "C2": 0.1, + "O2": -0.58, + "N3": 0.16, + "C4": 0.25, + "O4": -0.58, + "C5": 0.1, + "C6": 0.31, + }, } # taken from biotite code: https://www.biotite-python.org/examples/gallery/structure/glycan_visualization.html # originally adapted from "Mol*" Software # The dictionary maps residue names of saccharides to their common names SACCHARIDE_NAMES = { - res_name: common_name for common_name, res_names in [ + res_name: common_name + for common_name, res_names in [ ("Glc", ["GLC", "BGC", "Z8T", "TRE", "MLR"]), ("Man", ["MAN", "BMA"]), ("Gal", ["GLA", "GAL", "GZL", "GXL", "GIV"]), @@ -1528,123 +3208,620 @@ "All": ("o", "purple"), "Tal": ("o", "lightsteelblue"), "Ido": ("o", "chocolate"), - "GlcNAc": ("s", "royalblue"), "ManNAc": ("s", "forestgreen"), "GalNAc": ("s", "gold"), "GulNAc": ("s", "darkorange"), "AllNAc": ("s", "purple"), "IdoNAc": ("s", "chocolate"), - "GlcN": ("1", "royalblue"), "ManN": ("1", "forestgreen"), "GalN": ("1", "gold"), - "GlcA": ("v", "royalblue"), "ManA": ("v", "forestgreen"), "GalA": ("v", "gold"), "GulA": ("v", "darkorange"), "TalA": ("v", "lightsteelblue"), "IdoA": ("v", "chocolate"), - "Qui": ("^", "royalblue"), "Rha": ("^", "forestgreen"), "6dGul": ("^", "darkorange"), "Fuc": ("^", "crimson"), - "QuiNAc": ("P", "royalblue"), "FucNAc": ("P", "crimson"), - "Oli": ("X", "royalblue"), "Tyv": ("X", "forestgreen"), "Abe": ("X", "darkorange"), "Par": ("X", "pink"), "Dig": ("X", "purple"), - "Ara": ("*", "forestgreen"), "Lyx": ("*", "gold"), "Xyl": ("*", "darkorange"), "Rib": ("*", "pink"), - "Kdn": ("D", "forestgreen"), "Neu5Ac": ("D", "mediumvioletred"), "Neu5Gc": ("D", "turquoise"), - "LDManHep": ("H", "forestgreen"), "Kdo": ("H", "gold"), "DDManHep": ("H", "pink"), "MurNAc": ("H", "purple"), "Mur": ("H", "chocolate"), - "Api": ("p", "royalblue"), "Fru": ("p", "forestgreen"), "Tag": ("p", "gold"), "Sor": ("p", "darkorange"), "Psi": ("p", "pink"), - # Default representation - None: ("h", "black") + None: ("h", "black"), } # Lipid names to match against for the `is_lipid` attribute lipid_names = ( - '23SM', 'CDL1', 'CDL2', 'ABLIPA', 'ABLIPB', 'ADR', 'ADRP', 'ALIN', 'ALINP', - 'APC', 'APPC', 'ARA', 'ARAN', 'ARANP', 'ARAP', 'ASM', 'BCLIPA', 'BCLIPB', 'BCLIPC', - 'BEH', 'BEHP', 'BNSM', 'BSM', 'C6DHPC', 'C7DHPC', 'CER160', 'CER180', 'CER181', - 'CER2', 'CER200', 'CER220', 'CER240', 'CER241', 'CER3E', 'CHAPS', 'CHAPSO', 'CHL1', - 'CHM1', 'CHNS', 'CHOA', 'CHSD', 'CHSP', 'CJLIPA', 'CPC', 'CTLIPA', 'CYFOS3', 'CYFOS4', - 'CYFOS5', 'CYFOS6', 'CYFOS7', 'CYSF', 'CYSG', 'CYSL', 'CYSP', 'DAPA', 'DAPA', 'DAPC', - 'DAPC', 'DAPE', 'DAPE', 'DAPG', 'DAPG', 'DAPS', 'DAPS', 'DBPA', 'DBPC', 'DBPE', - 'DBPG', 'DBPS', 'DBSM', 'DCPC', 'DDA', 'DDAO', 'DDAOP', 'DDAP', 'DDMG', 'DDOPC', - 'DDOPE', 'DDOPS', 'DDPC', 'DEPA', 'DEPC', 'DEPE', 'DEPG', 'DEPS', 'DFPA', 'DFPC', - 'DFPE', 'DFPG', 'DFPS', 'DGLA', 'DGLAP', 'DGPA', 'DGPA', 'DGPC', 'DGPC', 'DGPE', - 'DGPE', 'DGPG', 'DGPG', 'DGPS', 'DGPS', 'DHA', 'DHAP', 'DHPC', 'DHPCE', 'DIPA', - 'DIPA', 'DIPC', 'DIPE', 'DIPG', 'DIPS', 'DLIPC', 'DLIPE', 'DLIPI', 'DLPA', 'DLPA', - 'DLPC', 'DLPC', 'DLPE', 'DLPE', 'DLPG', 'DLPG', 'DLPS', 'DLPS', 'DMPA', 'DMPC', - 'DMPCE', 'DMPE', 'DMPEE', 'DMPG', 'DMPI', 'DMPI13', 'DMPI14', 'DMPI15', 'DMPI24', - 'DMPI25', 'DMPI2A', 'DMPI2B', 'DMPI2C', 'DMPI2D', 'DMPI33', 'DMPI34', 'DMPI35', - 'DMPS', 'DNPA', 'DNPA', 'DNPC', 'DNPC', 'DNPE', 'DNPE', 'DNPG', 'DNPG', 'DNPS', - 'DNPS', 'DOMG', 'DOPA', 'DOPA', 'DOPC', 'DOPC', 'DOPCE', 'DOPE', 'DOPE', 'DOPEE', - 'DOPG', 'DOPG', 'DOPP1', 'DOPP2', 'DOPP3', 'DOPS', 'DOPS', 'DPA', 'DPAP', 'DPC', - 'DPCE', 'DPP1', 'DPP2', 'DPPA', 'DPPA', 'DPPC', 'DPPC', 'DPPE', 'DPPE', 'DPPEE', - 'DPPG', 'DPPG', 'DPPGK', 'DPPI', 'DPPS', 'DPPS', 'DPSM', 'DPT', 'DPTP', 'DRPA', - 'DRPC', 'DRPE', 'DRPG', 'DRPS', 'DSPA', 'DSPC', 'DSPE', 'DSPG', 'DSPS', 'DTPA', - 'DTPA', 'DTPC', 'DTPE', 'DTPG', 'DTPS', 'DUPC', 'DUPE', 'DUPS', 'DVPA', 'DVPC', - 'DVPE', 'DVPG', 'DVPS', 'DXCE', 'DXPA', 'DXPA', 'DXPC', 'DXPC', 'DXPE', 'DXPE', - 'DXPG', 'DXPG', 'DXPS', 'DXPS', 'DXSM', 'DYPA', 'DYPA', 'DYPC', 'DYPC', 'DYPE', - 'DYPE', 'DYPG', 'DYPG', 'DYPS', 'DYPS', 'ECLIPA', 'ECLIPB', 'ECLIPC', 'EDA', 'EDAP', - 'EICO', 'EICOP', 'EPA', 'EPAP', 'ERG', 'ERU', 'ERUP', 'ETA', 'ETAP', 'ETE', 'ETEP', - 'FOIS11', 'FOIS9', 'FOS10', 'FOS12', 'FOS13', 'FOS14', 'FOS15', 'FOS16', 'GLA', - 'GLAP', 'GLYM', 'HPA', 'HPAP', 'HPLIPA', 'HPLIPB', 'HTA', 'HTAP', 'IPC', 'IPPC', - 'KPLIPA', 'KPLIPB', 'KPLIPC', 'LAPAO', 'LAPAOP', 'LAU', 'LAUP', 'LDAO', 'LDAOP', - 'LIGN', 'LIGNP', 'LILIPA', 'LIN', 'LINP', 'LLPA', 'LLPC', 'LLPE', 'LLPS', 'LMPG', - 'LNACL1', 'LNACL2', 'LNBCL1', 'LNBCL2', 'LNCCL1', 'LNCCL2', 'LNDCL1', 'LNDCL2', - 'LOACL1', 'LOACL2', 'LOCCL1', 'LOCCL2', 'LPC', 'LPC12', 'LPC14', 'LPPA', 'LPPC', - 'LPPC', 'LPPE', 'LPPG', 'LPPG', 'LPPS', 'LSM', 'LYSM', 'MCLIPA', 'MEA', 'MEAP', 'MYR', - 'MYRO', 'MYROP', 'MYRP', 'NER', 'NERP', 'NGLIPA', 'NGLIPB', 'NGLIPC', 'NSM', 'OLE', - 'OLEP', 'OPC', 'OSM', 'OSPE', 'OYPE', 'PADG', 'PAL', 'PALIPA', 'PALIPB', 'PALIPC', - 'PALIPD', 'PALIPE', 'PALO', 'PALOP', 'PALP', 'PAPA', 'PAPC', 'PAPE', 'PAPG', 'PAPI', - 'PAPS', 'PDOPC', 'PDOPE', 'PEPC', 'PGPA', 'PGPC', 'PGPE', 'PGPG', 'PGPS', 'PGSM', - 'PIDG', 'PIM1', 'PIM2', 'PIPA', 'PIPC', 'PIPE', 'PIPG', 'PIPI', 'PIPS', 'PLPA', - 'PLPC', 'PLPE', 'PLPG', 'PLPI', 'PLPI13', 'PLPI14', 'PLPI15', 'PLPI24', 'PLPI25', - 'PLPI2A', 'PLPI2B', 'PLPI2C', 'PLPI2D', 'PLPI33', 'PLPI34', 'PLPI35', 'PLPS', 'PMCL1', - 'PMCL2', 'PMPE', 'PMPG', 'PNCE', 'PNPI', 'PNPI13', 'PNPI14', 'PNPI15', 'PNPI24', - 'PNPI25', 'PNPI2A', 'PNPI2B', 'PNPI2C', 'PNPI2D', 'PNPI33', 'PNPI34', 'PNPI35', - 'PNSM', 'PODG', 'POP1', 'POP2', 'POP3', 'POPA', 'POPA', 'POPC', 'POPC', 'POPCE', - 'POPE', 'POPE', 'POPEE', 'POPG', 'POPG', 'POPI', 'POPI', 'POPI13', 'POPI14', 'POPI15', - 'POPI24', 'POPI25', 'POPI2A', 'POPI2B', 'POPI2C', 'POPI2D', 'POPI33', 'POPI34', - 'POPI35', 'POPP1', 'POPP2', 'POPP3', 'POPS', 'POPS', 'POSM', 'PPC', 'PPPE', 'PQPE', - 'PQPS', 'PRPA', 'PRPC', 'PRPE', 'PRPG', 'PRPS', 'PSM', 'PSPG', 'PUDG', 'PUPA', 'PUPC', - 'PUPE', 'PUPI', 'PUPS', 'PVCL2', 'PVDG', 'PVP1', 'PVP2', 'PVP3', 'PVPE', 'PVPG', - 'PVPI', 'PVSM', 'PYPE', 'PYPG', 'PYPI', 'PhPC', 'QMPE', 'SAPA', 'SAPC', 'SAPE', - 'SAPG', 'SAPI', 'SAPI13', 'SAPI14', 'SAPI15', 'SAPI24', 'SAPI25', 'SAPI2A', 'SAPI2B', - 'SAPI2C', 'SAPI2D', 'SAPI33', 'SAPI34', 'SAPI35', 'SAPS', 'SB3-10', 'SB3-12', - 'SB3-14', 'SDA', 'SDAP', 'SDPA', 'SDPC', 'SDPE', 'SDPG', 'SDPS', 'SDS', 'SELIPA', - 'SELIPB', 'SELIPC', 'SFLIPA', 'SITO', 'SLPA', 'SLPC', 'SLPE', 'SLPG', 'SLPS', 'SOPA', - 'SOPC', 'SOPE', 'SOPG', 'SOPS', 'SSM', 'STE', 'STEP', 'STIG', 'THA', 'THAP', 'THCHL', - 'THDPPC', 'TIPA', 'TLCL1', 'TLCL2', 'TMCL1', 'TMCL2', 'TOCL1', 'TOCL2', 'TPA', 'TPAP', - 'TPC', 'TPC', 'TPT', 'TPTP', 'TRI', 'TRIP', 'TRIPAO', 'TRPAOP', 'TSPC', 'TTA', 'TTAP', - 'TXCL1', 'TXCL2', 'TYCL1', 'TYCL2', 'UDAO', 'UDAOP', 'UFOS10', 'UPC', 'VCLIPA', - 'VCLIPB', 'VCLIPC', 'VCLIPD', 'VCLIPE', 'VPC', 'XNCE', 'XNSM', 'YOPA', 'YOPC', 'YOPE', - 'YOPS', 'YPLIPA', 'YPLIPB', 'bondedtypes' + "23SM", + "CDL1", + "CDL2", + "ABLIPA", + "ABLIPB", + "ADR", + "ADRP", + "ALIN", + "ALINP", + "APC", + "APPC", + "ARA", + "ARAN", + "ARANP", + "ARAP", + "ASM", + "BCLIPA", + "BCLIPB", + "BCLIPC", + "BEH", + "BEHP", + "BNSM", + "BSM", + "C6DHPC", + "C7DHPC", + "CER160", + "CER180", + "CER181", + "CER2", + "CER200", + "CER220", + "CER240", + "CER241", + "CER3E", + "CHAPS", + "CHAPSO", + "CHL1", + "CHM1", + "CHNS", + "CHOA", + "CHSD", + "CHSP", + "CJLIPA", + "CPC", + "CTLIPA", + "CYFOS3", + "CYFOS4", + "CYFOS5", + "CYFOS6", + "CYFOS7", + "CYSF", + "CYSG", + "CYSL", + "CYSP", + "DAPA", + "DAPA", + "DAPC", + "DAPC", + "DAPE", + "DAPE", + "DAPG", + "DAPG", + "DAPS", + "DAPS", + "DBPA", + "DBPC", + "DBPE", + "DBPG", + "DBPS", + "DBSM", + "DCPC", + "DDA", + "DDAO", + "DDAOP", + "DDAP", + "DDMG", + "DDOPC", + "DDOPE", + "DDOPS", + "DDPC", + "DEPA", + "DEPC", + "DEPE", + "DEPG", + "DEPS", + "DFPA", + "DFPC", + "DFPE", + "DFPG", + "DFPS", + "DGLA", + "DGLAP", + "DGPA", + "DGPA", + "DGPC", + "DGPC", + "DGPE", + "DGPE", + "DGPG", + "DGPG", + "DGPS", + "DGPS", + "DHA", + "DHAP", + "DHPC", + "DHPCE", + "DIPA", + "DIPA", + "DIPC", + "DIPE", + "DIPG", + "DIPS", + "DLIPC", + "DLIPE", + "DLIPI", + "DLPA", + "DLPA", + "DLPC", + "DLPC", + "DLPE", + "DLPE", + "DLPG", + "DLPG", + "DLPS", + "DLPS", + "DMPA", + "DMPC", + "DMPCE", + "DMPE", + "DMPEE", + "DMPG", + "DMPI", + "DMPI13", + "DMPI14", + "DMPI15", + "DMPI24", + "DMPI25", + "DMPI2A", + "DMPI2B", + "DMPI2C", + "DMPI2D", + "DMPI33", + "DMPI34", + "DMPI35", + "DMPS", + "DNPA", + "DNPA", + "DNPC", + "DNPC", + "DNPE", + "DNPE", + "DNPG", + "DNPG", + "DNPS", + "DNPS", + "DOMG", + "DOPA", + "DOPA", + "DOPC", + "DOPC", + "DOPCE", + "DOPE", + "DOPE", + "DOPEE", + "DOPG", + "DOPG", + "DOPP1", + "DOPP2", + "DOPP3", + "DOPS", + "DOPS", + "DPA", + "DPAP", + "DPC", + "DPCE", + "DPP1", + "DPP2", + "DPPA", + "DPPA", + "DPPC", + "DPPC", + "DPPE", + "DPPE", + "DPPEE", + "DPPG", + "DPPG", + "DPPGK", + "DPPI", + "DPPS", + "DPPS", + "DPSM", + "DPT", + "DPTP", + "DRPA", + "DRPC", + "DRPE", + "DRPG", + "DRPS", + "DSPA", + "DSPC", + "DSPE", + "DSPG", + "DSPS", + "DTPA", + "DTPA", + "DTPC", + "DTPE", + "DTPG", + "DTPS", + "DUPC", + "DUPE", + "DUPS", + "DVPA", + "DVPC", + "DVPE", + "DVPG", + "DVPS", + "DXCE", + "DXPA", + "DXPA", + "DXPC", + "DXPC", + "DXPE", + "DXPE", + "DXPG", + "DXPG", + "DXPS", + "DXPS", + "DXSM", + "DYPA", + "DYPA", + "DYPC", + "DYPC", + "DYPE", + "DYPE", + "DYPG", + "DYPG", + "DYPS", + "DYPS", + "ECLIPA", + "ECLIPB", + "ECLIPC", + "EDA", + "EDAP", + "EICO", + "EICOP", + "EPA", + "EPAP", + "ERG", + "ERU", + "ERUP", + "ETA", + "ETAP", + "ETE", + "ETEP", + "FOIS11", + "FOIS9", + "FOS10", + "FOS12", + "FOS13", + "FOS14", + "FOS15", + "FOS16", + "GLA", + "GLAP", + "GLYM", + "HPA", + "HPAP", + "HPLIPA", + "HPLIPB", + "HTA", + "HTAP", + "IPC", + "IPPC", + "KPLIPA", + "KPLIPB", + "KPLIPC", + "LAPAO", + "LAPAOP", + "LAU", + "LAUP", + "LDAO", + "LDAOP", + "LIGN", + "LIGNP", + "LILIPA", + "LIN", + "LINP", + "LLPA", + "LLPC", + "LLPE", + "LLPS", + "LMPG", + "LNACL1", + "LNACL2", + "LNBCL1", + "LNBCL2", + "LNCCL1", + "LNCCL2", + "LNDCL1", + "LNDCL2", + "LOACL1", + "LOACL2", + "LOCCL1", + "LOCCL2", + "LPC", + "LPC12", + "LPC14", + "LPPA", + "LPPC", + "LPPC", + "LPPE", + "LPPG", + "LPPG", + "LPPS", + "LSM", + "LYSM", + "MCLIPA", + "MEA", + "MEAP", + "MYR", + "MYRO", + "MYROP", + "MYRP", + "NER", + "NERP", + "NGLIPA", + "NGLIPB", + "NGLIPC", + "NSM", + "OLE", + "OLEP", + "OPC", + "OSM", + "OSPE", + "OYPE", + "PADG", + "PAL", + "PALIPA", + "PALIPB", + "PALIPC", + "PALIPD", + "PALIPE", + "PALO", + "PALOP", + "PALP", + "PAPA", + "PAPC", + "PAPE", + "PAPG", + "PAPI", + "PAPS", + "PDOPC", + "PDOPE", + "PEPC", + "PGPA", + "PGPC", + "PGPE", + "PGPG", + "PGPS", + "PGSM", + "PIDG", + "PIM1", + "PIM2", + "PIPA", + "PIPC", + "PIPE", + "PIPG", + "PIPI", + "PIPS", + "PLPA", + "PLPC", + "PLPE", + "PLPG", + "PLPI", + "PLPI13", + "PLPI14", + "PLPI15", + "PLPI24", + "PLPI25", + "PLPI2A", + "PLPI2B", + "PLPI2C", + "PLPI2D", + "PLPI33", + "PLPI34", + "PLPI35", + "PLPS", + "PMCL1", + "PMCL2", + "PMPE", + "PMPG", + "PNCE", + "PNPI", + "PNPI13", + "PNPI14", + "PNPI15", + "PNPI24", + "PNPI25", + "PNPI2A", + "PNPI2B", + "PNPI2C", + "PNPI2D", + "PNPI33", + "PNPI34", + "PNPI35", + "PNSM", + "PODG", + "POP1", + "POP2", + "POP3", + "POPA", + "POPA", + "POPC", + "POPC", + "POPCE", + "POPE", + "POPE", + "POPEE", + "POPG", + "POPG", + "POPI", + "POPI", + "POPI13", + "POPI14", + "POPI15", + "POPI24", + "POPI25", + "POPI2A", + "POPI2B", + "POPI2C", + "POPI2D", + "POPI33", + "POPI34", + "POPI35", + "POPP1", + "POPP2", + "POPP3", + "POPS", + "POPS", + "POSM", + "PPC", + "PPPE", + "PQPE", + "PQPS", + "PRPA", + "PRPC", + "PRPE", + "PRPG", + "PRPS", + "PSM", + "PSPG", + "PUDG", + "PUPA", + "PUPC", + "PUPE", + "PUPI", + "PUPS", + "PVCL2", + "PVDG", + "PVP1", + "PVP2", + "PVP3", + "PVPE", + "PVPG", + "PVPI", + "PVSM", + "PYPE", + "PYPG", + "PYPI", + "PhPC", + "QMPE", + "SAPA", + "SAPC", + "SAPE", + "SAPG", + "SAPI", + "SAPI13", + "SAPI14", + "SAPI15", + "SAPI24", + "SAPI25", + "SAPI2A", + "SAPI2B", + "SAPI2C", + "SAPI2D", + "SAPI33", + "SAPI34", + "SAPI35", + "SAPS", + "SB3-10", + "SB3-12", + "SB3-14", + "SDA", + "SDAP", + "SDPA", + "SDPC", + "SDPE", + "SDPG", + "SDPS", + "SDS", + "SELIPA", + "SELIPB", + "SELIPC", + "SFLIPA", + "SITO", + "SLPA", + "SLPC", + "SLPE", + "SLPG", + "SLPS", + "SOPA", + "SOPC", + "SOPE", + "SOPG", + "SOPS", + "SSM", + "STE", + "STEP", + "STIG", + "THA", + "THAP", + "THCHL", + "THDPPC", + "TIPA", + "TLCL1", + "TLCL2", + "TMCL1", + "TMCL2", + "TOCL1", + "TOCL2", + "TPA", + "TPAP", + "TPC", + "TPC", + "TPT", + "TPTP", + "TRI", + "TRIP", + "TRIPAO", + "TRPAOP", + "TSPC", + "TTA", + "TTAP", + "TXCL1", + "TXCL2", + "TYCL1", + "TYCL2", + "UDAO", + "UDAOP", + "UFOS10", + "UPC", + "VCLIPA", + "VCLIPB", + "VCLIPC", + "VCLIPD", + "VCLIPE", + "VPC", + "XNCE", + "XNSM", + "YOPA", + "YOPC", + "YOPE", + "YOPS", + "YPLIPA", + "YPLIPB", + "bondedtypes", ) diff --git a/molecularnodes/io/__init__.py b/molecularnodes/io/__init__.py index a73a6c1f..97772de6 100644 --- a/molecularnodes/io/__init__.py +++ b/molecularnodes/io/__init__.py @@ -1,12 +1,17 @@ -from .parse import ( +from .parse import CIF, BCIF, PDB, SDF, CellPack, StarFile, MDAnalysisSession +from .wwpdb import fetch +from .local import load +from .retrieve import download + +__all__ = [ CIF, BCIF, PDB, SDF, CellPack, StarFile, - MDAnalysisSession -) -from .wwpdb import fetch -from .local import load -from .retrieve import download + MDAnalysisSession, + fetch, + load, + download, +] diff --git a/molecularnodes/io/cellpack.py b/molecularnodes/io/cellpack.py index 95156abf..29f90055 100644 --- a/molecularnodes/io/cellpack.py +++ b/molecularnodes/io/cellpack.py @@ -3,34 +3,28 @@ from . import parse bpy.types.Scene.mol_import_cell_pack_path = bpy.props.StringProperty( - name='File', - description='File to import (.cif, .bcif)', - subtype='FILE_PATH', - maxlen=0 + name="File", + description="File to import (.cif, .bcif)", + subtype="FILE_PATH", + maxlen=0, ) bpy.types.Scene.mol_import_cell_pack_name = bpy.props.StringProperty( - name='Name', - description='Name of the created object.', - default='NewCellPackModel', - maxlen=0 + name="Name", + description="Name of the created object.", + default="NewCellPackModel", + maxlen=0, ) def load( file_path, - name='NewCellPackModel', + name="NewCellPackModel", node_setup=True, world_scale=0.01, fraction: float = 1, ): - ensemble = parse.CellPack(file_path) - model = ensemble.create_model( - name=name, - node_setup=node_setup, - world_scale=world_scale, - fraction=fraction - ) + model = ensemble.create_model(name=name, node_setup=node_setup, world_scale=world_scale, fraction=fraction) return model @@ -42,23 +36,23 @@ class MN_OT_Import_Cell_Pack(bpy.types.Operator): bl_options = {"REGISTER"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context): return True - def execute(self, context): + def execute(self, context: bpy.types.Context): s = context.scene load( file_path=s.mol_import_cell_pack_path, name=s.mol_import_cell_pack_name, - node_setup=True + node_setup=True, ) return {"FINISHED"} def panel(layout, scene): - layout.label(text="Load CellPack Model", icon='FILE_TICK') + layout.label(text="Load CellPack Model", icon="FILE_TICK") layout.separator() row_import = layout.row() - row_import.prop(scene, 'mol_import_cell_pack_name') - layout.prop(scene, 'mol_import_cell_pack_path') - row_import.operator('mol.import_cell_pack') + row_import.prop(scene, "mol_import_cell_pack_name") + layout.prop(scene, "mol_import_cell_pack_path") + row_import.operator("mol.import_cell_pack") diff --git a/molecularnodes/io/density.py b/molecularnodes/io/density.py index cd471f69..5a8be826 100644 --- a/molecularnodes/io/density.py +++ b/molecularnodes/io/density.py @@ -4,56 +4,56 @@ bpy.types.Scene.MN_import_density_invert = bpy.props.BoolProperty( name="Invert Data", description="Invert the values in the map. Low becomes high, high becomes low.", - default=False + default=False, ) bpy.types.Scene.MN_import_density_center = bpy.props.BoolProperty( name="Center Density", description="Translate the density so that the center of the box is at the origin.", - default=False + default=False, ) bpy.types.Scene.MN_import_density = bpy.props.StringProperty( - name='File', - description='File path for the map file.', - subtype='FILE_PATH', - maxlen=0 + name="File", + description="File path for the map file.", + subtype="FILE_PATH", + maxlen=0, ) bpy.types.Scene.MN_import_density_name = bpy.props.StringProperty( - name='Name', - description='Name for the new density object.', - default='NewDensityObject', - maxlen=0 + name="Name", + description="Name for the new density object.", + default="NewDensityObject", + maxlen=0, ) bpy.types.Scene.MN_import_density_style = bpy.props.EnumProperty( - name='Style', + name="Style", items=( - ('density_surface', 'Surface', - 'A mesh surface based on the specified threshold', 0), - ('density_wire', 'Wire', 'A wire mesh surface based on the specified threshold', 1) - ) + ( + "density_surface", + "Surface", + "A mesh surface based on the specified threshold", + 0, + ), + ( + "density_wire", + "Wire", + "A wire mesh surface based on the specified threshold", + 1, + ), + ), ) def load( file_path: str, - name: str = 'NewDensity', + name: str = "NewDensity", invert: bool = False, setup_nodes: bool = True, - style: str = 'density_surface', + style: str = "density_surface", center: bool = False, - overwrite: bool = False + overwrite: bool = False, ): - density = parse.MRC( - file_path=file_path, - center=center, - invert=invert, - overwrite=overwrite - ) - density.create_model( - name=name, - setup_nodes=setup_nodes, - style=style - ) + density = parse.MRC(file_path=file_path, center=center, invert=invert, overwrite=overwrite) + density.create_model(name=name, setup_nodes=setup_nodes, style=style) return density @@ -64,10 +64,10 @@ class MN_OT_Import_Map(bpy.types.Operator): bl_options = {"REGISTER"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context): return True - def execute(self, context): + def execute(self, context: bpy.types.Context): scene = context.scene load( file_path=scene.MN_import_density, @@ -75,20 +75,20 @@ def execute(self, context): invert=scene.MN_import_density_invert, setup_nodes=scene.MN_import_node_setup, style=scene.MN_import_density_style, - center=scene.MN_import_density_center + center=scene.MN_import_density_center, ) return {"FINISHED"} def panel(layout, scene): - layout.label(text='Load EM Map', icon='FILE_TICK') + layout.label(text="Load EM Map", icon="FILE_TICK") layout.separator() row = layout.row() - row.prop(scene, 'MN_import_density_name') - row.operator('mn.import_density') + row.prop(scene, "MN_import_density_name") + row.operator("mn.import_density") - layout.prop(scene, 'MN_import_density') + layout.prop(scene, "MN_import_density") layout.separator() col = layout.column() col.alignment = "LEFT" @@ -98,18 +98,18 @@ def panel(layout, scene): Please do not delete this file or the volume will not render.\ Move the original .map file to change this location.\ " - for line in label.strip().split(' '): + for line in label.strip().split(" "): col.label(text=line) layout.separator() layout.label(text="Options", icon="MODIFIER") row = layout.row() - row.prop(scene, 'MN_import_node_setup', text="") + row.prop(scene, "MN_import_node_setup", text="") col = row.column() col.prop(scene, "MN_import_density_style") col.enabled = scene.MN_import_node_setup grid = layout.grid_flow() - grid.prop(scene, 'MN_import_density_invert') - grid.prop(scene, 'MN_import_density_center') + grid.prop(scene, "MN_import_density_invert") + grid.prop(scene, "MN_import_density_center") diff --git a/molecularnodes/io/dna.py b/molecularnodes/io/dna.py index 4a9065fe..70f6f9cf 100644 --- a/molecularnodes/io/dna.py +++ b/molecularnodes/io/dna.py @@ -1,31 +1,33 @@ -import numpy as np import bpy +import numpy as np +from numpy.typing import NDArray +from typing import Union, Tuple, Set +from pathlib import Path + from .. import color -from ..blender import ( - obj, coll, nodes -) +from ..blender import coll, nodes, obj bpy.types.Scene.MN_import_oxdna_topology = bpy.props.StringProperty( - name='Toplogy', - description='File path for the topology to import (.top)', - subtype='FILE_PATH', - maxlen=0 + name="Toplogy", + description="File path for the topology to import (.top)", + subtype="FILE_PATH", + maxlen=0, ) bpy.types.Scene.MN_import_oxdna_trajectory = bpy.props.StringProperty( - name='Trajectory', - description='File path for the trajectory to import (.oxdna / .dat)', - subtype='FILE_PATH', - maxlen=0 + name="Trajectory", + description="File path for the trajectory to import (.oxdna / .dat)", + subtype="FILE_PATH", + maxlen=0, ) bpy.types.Scene.MN_import_oxdna_name = bpy.props.StringProperty( - name='Name', - description='Name of the created object.', - default='NewOrigami', - maxlen=0 + name="Name", + description="Name of the created object.", + default="NewOrigami", + maxlen=0, ) -def base_to_int(bases: np.array) -> np.array: +def base_to_int(bases: NDArray[np.character]) -> NDArray[np.int32]: """ Convert an array of DNA bases to their corresponding MN integer values. @@ -40,32 +42,27 @@ def base_to_int(bases: np.array) -> np.array: Array of corresponding integer values for the DNA bases. """ # Values for internal Molecular Nodes use. Defined in data.py - base_lookup = { - 'A': 30, - 'C': 31, - 'G': 32, - 'T': 33 - } + base_lookup = {"A": 30, "C": 31, "G": 32, "T": 33} ints = np.array([base_lookup.get(base, -1) for base in bases]) return ints -def is_new_topology(filepath): +def is_new_topology(filepath: Union[str, Path]) -> bool: with open(filepath) as f: firstline = f.readline() return "5 -> 3" in firstline -def read_topology_new(filepath): - with open(filepath, 'r') as file: +def read_topology_new(filepath: Union[str, Path]) -> NDArray[np.int32]: + with open(filepath, "r") as file: contents = file.read() - lines = np.array(contents.split('\n')) + lines = np.array(contents.split("\n")) - def read_seq_line(line): + def read_seq_line(line: str) -> NDArray[np.character]: sequence = line.split(" ")[0] return np.array([c for c in sequence]) @@ -91,7 +88,7 @@ def read_seq_line(line): return np.vstack(strands) -def read_topology_old(filepath): +def read_topology_old(filepath: Union[str, Path]) -> NDArray[np.int32]: """ Read the topology from a file and convert it to a numpy array. @@ -113,15 +110,15 @@ def read_topology_old(filepath): Returns ------- numpy.ndarray - The topology as a integer numpy array. Base assignment is (30, 31, 32, 33) where + The topology as a integer numpy array. Base assignment is (30, 31, 32, 33) where this corresponds to (A, C, G, T) for use inside of Molecular Nodes. """ - with open(filepath, 'r') as file: + with open(filepath, "r") as file: contents = file.read() - lines = np.array(contents.split('\n')) + lines = np.array(contents.split("\n")) # metadata = lines[0] # read the topology from the file sans the first metadata line @@ -130,20 +127,19 @@ def read_topology_old(filepath): # convert the columns to numeric array_int = np.zeros(array_str.shape, dtype=int) - array_int[:, (0, 2, 3)] = array_str[:, (0, 2, 3)].astype( - int) # easy convert numeric columns to int + array_int[:, (0, 2, 3)] = array_str[:, (0, 2, 3)].astype(int) # easy convert numeric columns to int # convert bases (A, C, G, T) to (30, 31, 32, 33) array_int[:, 1] = base_to_int(array_str[:, 1]) return array_int -def read_trajectory(filepath): +def read_trajectory(filepath: Union[str, Path]) -> NDArray[np.int32]: """ Read an oxDNA trajectory file and return an array of frames. - Each frame becomes a 2D array in a stack. Each frame has 5 three-component vectors. - The vectors are: (position, base_vector, base_normal, veclocity, angular_velocity), + Each frame becomes a 2D array in a stack. Each frame has 5 three-component vectors. + The vectors are: (position, base_vector, base_normal, veclocity, angular_velocity), which totals 15 columns in the array. The (velocity, angular_velocity) are optional and can sometimes not appear in the trajectory. @@ -155,16 +151,16 @@ def read_trajectory(filepath): Returns ------- frames : ndarray - An array of frames, where each frame is a 2D array of positions + An array of frames, where each frame is a 2D array of positions """ # Open the file and read its contents - with open(filepath, 'r') as file: + with open(filepath, "r") as file: contents = file.read() # Split the contents into lines - lines = np.array(contents.split('\n')) - is_meta = np.char.find(lines, '=') > 0 + lines = np.array(contents.split("\n")) + is_meta = np.char.find(lines, "=") > 0 group_id = np.cumsum(np.append([True], np.diff(is_meta))) groups = np.unique(group_id) @@ -182,16 +178,15 @@ def read_trajectory(filepath): return np.stack(frames) -def set_attributes_to_dna_mol(mol, frame, scale_dna=0.1): - attributes = ('base_vector', 'base_normal', 'velocity', 'angular_velocity') +def set_attributes_to_dna_mol(mol: bpy.types.Object, frame: NDArray[np.float64], scale_dna: float = 0.1) -> None: + attributes = ("base_vector", "base_normal", "velocity", "angular_velocity") for i, att in enumerate(attributes): col_idx = np.array([3, 4, 5]) + i * 3 try: data = frame[:, col_idx] except IndexError as e: - print( - f"Unable to get {att} attribute from coordinates. Error: {e}") + print(f"Unable to get {att} attribute from coordinates. Error: {e}") continue if att != "angular_velocity": @@ -200,7 +195,7 @@ def set_attributes_to_dna_mol(mol, frame, scale_dna=0.1): obj.set_attribute(mol, att, data, type="FLOAT_VECTOR") -def toplogy_to_bond_idx_pairs(topology: np.ndarray): +def toplogy_to_bond_idx_pairs(topology: NDArray[np.int32]) -> NDArray[np.int32]: """ Convert the given topology array into pairs of indices representing each distinct bond. @@ -241,8 +236,13 @@ def toplogy_to_bond_idx_pairs(topology: np.ndarray): return np.sort(bond_idxs, axis=1) -def load(top, traj, name='oxDNA', setup_nodes=True, world_scale=0.01): - +def load( + top: Union[str, Path], + traj: Union[str, Path], + name: str = "oxDNA", + setup_nodes: bool = True, + world_scale: float = 0.01, +) -> Tuple[bpy.types.Object, bpy.types.Collection]: # the scale of the oxDNA files seems to be based on nanometres rather than angstrongs # like most structural biology files, so currently adjusting the world_scale to # compensate @@ -264,14 +264,18 @@ def load(top, traj, name='oxDNA', setup_nodes=True, world_scale=0.01): name=name, collection=coll.mn(), vertices=trajectory[0][:, 0:3] * scale_dna, - edges=toplogy_to_bond_idx_pairs(topology) + edges=toplogy_to_bond_idx_pairs(topology), ) # adding additional toplogy information from the topology and frames objects - obj.set_attribute(mol, 'res_name', topology[:, 1], "INT") - obj.set_attribute(mol, 'chain_id', topology[:, 0], "INT") - obj.set_attribute(mol, 'Color', data=color.color_chains_equidistant( - topology[:, 0]), type='FLOAT_COLOR') + obj.set_attribute(mol, "res_name", topology[:, 1], "INT") + obj.set_attribute(mol, "chain_id", topology[:, 0], "INT") + obj.set_attribute( + mol, + "Color", + data=color.color_chains_equidistant(topology[:, 0]), + type="FLOAT_COLOR", + ) set_attributes_to_dna_mol(mol, trajectory[0], scale_dna=scale_dna) # if the 'frames' file only contained one timepoint, return the object without creating @@ -279,8 +283,7 @@ def load(top, traj, name='oxDNA', setup_nodes=True, world_scale=0.01): # object in place of the frames collection if n_frames == 1: if setup_nodes: - nodes.create_starting_node_tree( - mol, style="oxdna", set_color=False) + nodes.create_starting_node_tree(mol, style="oxdna", set_color=False) return mol, None # create a collection to store all of the frame objects that are part of the trajectory @@ -290,39 +293,37 @@ def load(top, traj, name='oxDNA', setup_nodes=True, world_scale=0.01): for i, frame in enumerate(trajectory): fill_n = int(np.ceil(np.log10(n_frames))) frame_name = f"{name}_frame_{str(i).zfill(fill_n)}" - frame_mol = obj.create_object( - frame[:, 0:3] * scale_dna, name=frame_name, collection=collection) + frame_mol = obj.create_object(frame[:, 0:3] * scale_dna, name=frame_name, collection=collection) set_attributes_to_dna_mol(frame_mol, frame, scale_dna) if setup_nodes: - nodes.create_starting_node_tree( - mol, coll_frames=collection, style="oxdna", set_color=False) + nodes.create_starting_node_tree(mol, coll_frames=collection, style="oxdna", set_color=False) return mol, collection -class MN_OT_Import_OxDNA_Trajectory(bpy.types.Operator): +class MN_OT_Import_OxDNA_Trajectory(bpy.types.Operator): # type: ignore bl_idname = "mn.import_oxdna" bl_label = "Load" bl_description = "Will import the given file and toplogy." bl_options = {"REGISTER"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: s = context.scene load( top=s.MN_import_oxdna_topology, traj=s.MN_import_oxdna_trajectory, - name=s.MN_import_oxdna_name + name=s.MN_import_oxdna_name, ) return {"FINISHED"} -def panel(layout, scene): - layout.label(text="Load oxDNA File", icon='FILE_TICK') +def panel(layout: bpy.types.UIList, scene: bpy.types.Scene) -> bpy.types.UIList: + layout.label(text="Load oxDNA File", icon="FILE_TICK") layout.separator() row = layout.row() - row.prop(scene, 'MN_import_oxdna_name') - row.operator('mn.import_oxdna') + row.prop(scene, "MN_import_oxdna_name") + row.operator("mn.import_oxdna") col = layout.column(align=True) - col.prop(scene, 'MN_import_oxdna_topology') - col.prop(scene, 'MN_import_oxdna_trajectory') + col.prop(scene, "MN_import_oxdna_topology") + col.prop(scene, "MN_import_oxdna_trajectory") diff --git a/molecularnodes/io/local.py b/molecularnodes/io/local.py index 4dd3c062..2eb48eff 100644 --- a/molecularnodes/io/local.py +++ b/molecularnodes/io/local.py @@ -1,47 +1,47 @@ -import bpy from pathlib import Path -import warnings + +import bpy + from . import parse bpy.types.Scene.MN_import_local_path = bpy.props.StringProperty( - name='File', - description='File path of the structure to open', - options={'TEXTEDIT_UPDATE'}, - subtype='FILE_PATH', - maxlen=0 + name="File", + description="File path of the structure to open", + options={"TEXTEDIT_UPDATE"}, + subtype="FILE_PATH", + maxlen=0, ) bpy.types.Scene.MN_import_local_name = bpy.props.StringProperty( - name='Name', - description='Name of the molecule on import', - options={'TEXTEDIT_UPDATE'}, - default='NewMolecule', - maxlen=0 + name="Name", + description="Name of the molecule on import", + options={"TEXTEDIT_UPDATE"}, + default="NewMolecule", + maxlen=0, ) def load( file_path, name="Name", - centre='', + centre="", del_solvent=True, - style='spheres', - build_assembly=False + style="spheres", + build_assembly=False, ): from biotite import InvalidFileError suffix = Path(file_path).suffix parser = { - '.pdb': parse.PDB, - '.pdbx': parse.CIF, - '.cif': parse.CIF, - '.bcif': parse.BCIF, - '.mol': parse.SDF, - '.sdf': parse.SDF + ".pdb": parse.PDB, + ".pdbx": parse.CIF, + ".cif": parse.CIF, + ".bcif": parse.BCIF, + ".mol": parse.SDF, + ".sdf": parse.SDF, } if suffix not in parser: - raise ValueError( - f"Unable to open local file. Format '{suffix}' not supported.") + raise ValueError(f"Unable to open local file. Format '{suffix}' not supported.") try: molecule = parser[suffix](file_path) except InvalidFileError: @@ -52,10 +52,11 @@ def load( style=style, build_assembly=build_assembly, centre=centre, - del_solvent=del_solvent + del_solvent=del_solvent, ) return molecule + # operator that calls the function to import the structure from a local file @@ -65,7 +66,7 @@ class MN_OT_Import_Protein_Local(bpy.types.Operator): bl_description = "Open a local structure file" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): + def execute(self, context: bpy.types.Context): scene = context.scene file_path = scene.MN_import_local_path @@ -73,7 +74,7 @@ def execute(self, context): if not scene.MN_import_node_setup: style = None - centre = '' + centre = "" if scene.MN_import_centre: centre = scene.MN_centre_type @@ -83,12 +84,12 @@ def execute(self, context): centre=centre, del_solvent=scene.MN_import_del_solvent, style=style, - build_assembly=scene.MN_import_build_assembly + build_assembly=scene.MN_import_build_assembly, ) # return the good news! bpy.context.view_layer.objects.active = mol.object - self.report({'INFO'}, message=f"Imported '{file_path}' as {mol.name}") + self.report({"INFO"}, message=f"Imported '{file_path}' as {mol.name}") return {"FINISHED"} def invoke(self, context, event): @@ -96,36 +97,35 @@ def invoke(self, context, event): def panel(layout, scene): - - layout.label(text='Load a Local File', icon='FILE_TICK') + layout.label(text="Load a Local File", icon="FILE_TICK") layout.separator() row_name = layout.row(align=False) - row_name.prop(scene, 'MN_import_local_name') - row_name.operator('mn.import_protein_local') + row_name.prop(scene, "MN_import_local_name") + row_name.operator("mn.import_protein_local") row_import = layout.row() - row_import.prop(scene, 'MN_import_local_path') + row_import.prop(scene, "MN_import_local_path") layout.separator() - layout.label(text='Options', icon='MODIFIER') + layout.label(text="Options", icon="MODIFIER") options = layout.column(align=True) row = options.row() - row.prop(scene, 'MN_import_node_setup', text='') + row.prop(scene, "MN_import_node_setup", text="") col = row.column() - col.prop(scene, 'MN_import_style') + col.prop(scene, "MN_import_style") col.enabled = scene.MN_import_node_setup row_centre = options.row() - row_centre.prop(scene, 'MN_import_centre', icon_value=0) + row_centre.prop(scene, "MN_import_centre", icon_value=0) # row_centre.prop() col_centre = row_centre.column() - col_centre.prop(scene, 'MN_centre_type', text='') + col_centre.prop(scene, "MN_centre_type", text="") col_centre.enabled = scene.MN_import_centre options.separator() grid = options.grid_flow() - grid.prop(scene, 'MN_import_build_assembly') - grid.prop(scene, 'MN_import_del_solvent', icon_value=0) + grid.prop(scene, "MN_import_build_assembly") + grid.prop(scene, "MN_import_del_solvent", icon_value=0) diff --git a/molecularnodes/io/md.py b/molecularnodes/io/md.py index c37cb1f1..bee084e9 100644 --- a/molecularnodes/io/md.py +++ b/molecularnodes/io/md.py @@ -22,56 +22,47 @@ class MockUniverse: else: HAS_mda = True -from .parse.mda import MDAnalysisSession from .. import pkg +from .parse.mda import MDAnalysisSession bpy.types.Scene.MN_import_md_topology = bpy.props.StringProperty( - name='Topology', - description='File path for the toplogy file for the trajectory', - subtype='FILE_PATH', - maxlen=0 + name="Topology", + description="File path for the toplogy file for the trajectory", + subtype="FILE_PATH", + maxlen=0, ) bpy.types.Scene.MN_import_md_trajectory = bpy.props.StringProperty( - name='Trajectory', - description='File path for the trajectory file for the trajectory', - subtype='FILE_PATH', - maxlen=0 + name="Trajectory", + description="File path for the trajectory file for the trajectory", + subtype="FILE_PATH", + maxlen=0, ) bpy.types.Scene.MN_import_md_name = bpy.props.StringProperty( - name='Name', - description='Name of the molecule on import', - default='NewTrajectory', - maxlen=0 + name="Name", + description="Name of the molecule on import", + default="NewTrajectory", + maxlen=0, ) bpy.types.Scene.MN_import_md_frame_start = bpy.props.IntProperty( - name="Start", - description="Frame start for importing MD trajectory", - default=0 + name="Start", description="Frame start for importing MD trajectory", default=0 ) bpy.types.Scene.MN_import_md_frame_step = bpy.props.IntProperty( - name="Step", - description="Frame step for importing MD trajectory", - default=1 + name="Step", description="Frame step for importing MD trajectory", default=1 ) bpy.types.Scene.MN_import_md_frame_stop = bpy.props.IntProperty( - name="Stop", - description="Frame stop for importing MD trajectory", - default=499 + name="Stop", description="Frame stop for importing MD trajectory", default=499 ) bpy.types.Scene.MN_md_selection = bpy.props.StringProperty( - name='Import Filter', + name="Import Filter", description='Custom MDAnalysis selection string, removing unselecte atoms. See: "https://docs.mdanalysis.org/stable/documentation_pages/selections.html"', - default='all' + default="all", ) bpy.types.Scene.MN_md_in_memory = bpy.props.BoolProperty( - name='In Memory', - description='True will load all of the requested frames into the scene and into memory. False will stream the trajectory from a live MDAnalysis session', - default=False -) -bpy.types.Scene.list_index = bpy.props.IntProperty( - name="Index for trajectory selection list.", - default=0 + name="In Memory", + description="True will load all of the requested frames into the scene and into memory. False will stream the trajectory from a live MDAnalysis session", + default=False, ) +bpy.types.Scene.list_index = bpy.props.IntProperty(name="Index for trajectory selection list.", default=0) def load( @@ -79,13 +70,13 @@ def load( traj, name="NewTrajectory", style="spheres", - selection: str = 'all', + selection: str = "all", start: int = 0, step: int = 1, stop: int = 499, subframes: int = 0, custom_selections: dict = {}, - in_memory: bool = False + in_memory: bool = False, ): universe = mda.Universe(top, traj) @@ -97,13 +88,14 @@ def load( extra_selections = {} for sel in custom_selections: extra_selections[sel.name] = sel.selection - mol = session.show(atoms=universe, - name=name, - style=style, - selection=selection, - custom_selections=extra_selections, - in_memory=in_memory - ) + mol = session.show( + atoms=universe, + name=name, + style=style, + selection=selection, + custom_selections=extra_selections, + in_memory=in_memory, + ) return mol, universe @@ -115,16 +107,17 @@ class MN_OT_Import_Protein_MD(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context): return True - def execute(self, context): + def execute(self, context: bpy.types.Context): scene = context.scene - if not pkg.is_current('MDAnalysis'): - self.report({'ERROR'}, - message="MDAnalysis is not installed. " - "Please install it to use this feature.") - return {'CANCELLED'} + if not pkg.is_current("MDAnalysis"): + self.report( + {"ERROR"}, + message="MDAnalysis is not installed. " "Please install it to use this feature.", + ) + return {"CANCELLED"} top = scene.MN_import_md_topology traj = scene.MN_import_md_trajectory name = scene.MN_import_md_name @@ -139,17 +132,16 @@ def execute(self, context): stop=scene.MN_import_md_frame_stop, step=scene.MN_import_md_frame_step, custom_selections=scene.trajectory_selection_list, - in_memory=scene.MN_md_in_memory - + in_memory=scene.MN_md_in_memory, ) bpy.context.view_layer.objects.active = mol self.report( - {'INFO'}, + {"INFO"}, message=f"Imported '{top}' as {name} " - f"with {str(universe.trajectory.n_frames)} " - f"frames from '{traj}'." + f"with {str(universe.trajectory.n_frames)} " + f"frames from '{traj}'.", ) return {"FINISHED"} @@ -157,42 +149,37 @@ def execute(self, context): # UI + class TrajectorySelectionItem(bpy.types.PropertyGroup): """Group of properties for custom selections for MDAnalysis import.""" + bl_idname = "testing" - name: bpy.props.StringProperty( - name="Attribute Name", - description="Attribute", - default="custom_selection" - ) + name: bpy.props.StringProperty(name="Attribute Name", description="Attribute", default="custom_selection") selection: bpy.props.StringProperty( name="Selection String", description="String that provides a selection through MDAnalysis", - default="name CA" + default="name CA", ) # have to manually register this class otherwise the PropertyGroup registration fails bpy.utils.register_class(TrajectorySelectionItem) -bpy.types.Scene.trajectory_selection_list = bpy.props.CollectionProperty( - type=TrajectorySelectionItem -) +bpy.types.Scene.trajectory_selection_list = bpy.props.CollectionProperty(type=TrajectorySelectionItem) class MN_UL_TrajectorySelectionListUI(bpy.types.UIList): """UI List""" - def draw_item(self, context, layout, data, item, - icon, active_data, active_propname, index): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): custom_icon = "VIS_SEL_11" - if self.layout_type in {'DEFAULT', 'COMPACT'}: + if self.layout_type in {"DEFAULT", "COMPACT"}: layout.label(text=item.name, icon=custom_icon) - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" layout.label(text="", icon=custom_icon) @@ -202,28 +189,27 @@ class TrajectorySelection_OT_NewItem(bpy.types.Operator): bl_idname = "trajectory_selection_list.new_item" bl_label = "+" - def execute(self, context): + def execute(self, context: bpy.types.Context): context.scene.trajectory_selection_list.add() - return {'FINISHED'} + return {"FINISHED"} class TrajectorySelection_OT_DeleteIem(bpy.types.Operator): - bl_idname = "trajectory_selection_list.delete_item" bl_label = "-" @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context): return context.scene.trajectory_selection_list - def execute(self, context): + def execute(self, context: bpy.types.Context): my_list = context.scene.trajectory_selection_list index = context.scene.list_index my_list.remove(index) context.scene.list_index = min(max(0, index - 1), len(my_list) - 1) - return {'FINISHED'} + return {"FINISHED"} def custom_selections(layout, scene): @@ -231,12 +217,18 @@ def custom_selections(layout, scene): row = layout.row(align=True) row = row.split(factor=0.9) - row.template_list('MN_UL_TrajectorySelectionListUI', 'A list', scene, - "trajectory_selection_list", scene, "list_index", rows=3) + row.template_list( + "MN_UL_TrajectorySelectionListUI", + "A list", + scene, + "trajectory_selection_list", + scene, + "list_index", + rows=3, + ) col = row.column() - col.operator('trajectory_selection_list.new_item', icon="ADD", text="") - col.operator('trajectory_selection_list.delete_item', - icon="REMOVE", text="") + col.operator("trajectory_selection_list.new_item", icon="ADD", text="") + col.operator("trajectory_selection_list.delete_item", icon="REMOVE", text="") if scene.list_index >= 0 and scene.trajectory_selection_list: item = scene.trajectory_selection_list[scene.list_index] @@ -248,29 +240,29 @@ def custom_selections(layout, scene): def panel(layout, scene): - layout.label(text="Load MD Trajectories", icon='FILE_TICK') + layout.label(text="Load MD Trajectories", icon="FILE_TICK") layout.separator() col = layout.column(align=True) row_import = col.row() - row_import.prop(scene, 'MN_import_md_name') - row_import.operator('mn.import_protein_md', text="Load") + row_import.prop(scene, "MN_import_md_name") + row_import.operator("mn.import_protein_md", text="Load") col.separator() - col.prop(scene, 'MN_import_md_topology') - col.prop(scene, 'MN_import_md_trajectory') + col.prop(scene, "MN_import_md_topology") + col.prop(scene, "MN_import_md_trajectory") layout.separator() layout.label(text="Options", icon="MODIFIER") row = layout.row() - row.prop(scene, 'MN_import_node_setup', text="") + row.prop(scene, "MN_import_node_setup", text="") col = row.column() col.prop(scene, "MN_import_style") col.enabled = scene.MN_import_node_setup - layout.prop(scene, 'MN_md_selection') + layout.prop(scene, "MN_md_selection") row_frame = layout.row(heading="Frames", align=True) - row_frame.prop(scene, 'MN_md_in_memory') + row_frame.prop(scene, "MN_md_in_memory") row = row_frame.row(align=True) - row.prop(scene, 'MN_import_md_frame_start') - row.prop(scene, 'MN_import_md_frame_step') - row.prop(scene, 'MN_import_md_frame_stop') + row.prop(scene, "MN_import_md_frame_start") + row.prop(scene, "MN_import_md_frame_step") + row.prop(scene, "MN_import_md_frame_stop") row.enabled = scene.MN_md_in_memory custom_selections(layout, scene) diff --git a/molecularnodes/io/parse/__init__.py b/molecularnodes/io/parse/__init__.py index e19866d0..ec35f957 100644 --- a/molecularnodes/io/parse/__init__.py +++ b/molecularnodes/io/parse/__init__.py @@ -2,12 +2,21 @@ A subpackge which provides classes for parsing the different macromolecular data formats. """ -from .pdbx import CIF, BCIF -# from .bcif import BCIF -# from .cif import CIF -from .pdb import PDB from .cellpack import CellPack -from .star import StarFile -from .sdf import SDF from .mda import MDAnalysisSession from .mrc import MRC +from .pdb import PDB +from .pdbx import BCIF, CIF +from .sdf import SDF +from .star import StarFile + +__all__ = [ + "CIF", + "BCIF", + "PDB", + "CellPack", + "StarFile", + "SDF", + "MDAnalysisSession", + "MRC", +] diff --git a/molecularnodes/io/parse/assembly.py b/molecularnodes/io/parse/assembly.py index 6ae0b1a7..d0ef39c8 100644 --- a/molecularnodes/io/parse/assembly.py +++ b/molecularnodes/io/parse/assembly.py @@ -9,7 +9,6 @@ class AssemblyParser(metaclass=ABCMeta): - @abstractmethod def list_assemblies(self): """ @@ -29,21 +28,21 @@ def get_transformations(self, assembly_id): transformations on sets of chains for this assembly | chain IDs affected by the transformation | | 4x4 rotation, translation & scale matrix - | | | + | | | list[tuple[ndarray, ndarray]]] """ @abstractmethod def get_assemblies(self): """ - Parse all the transformations for each assembly, returning a dictionary of + Parse all the transformations for each assembly, returning a dictionary of key:value pairs of assembly_id:transformations. The transformations list comes from the `get_transformations(assembly_id)` method. Dictionary of all assemblies | Assembly ID | | List of transformations to create biological assembly. - | | | + | | | dict{'1', list[transformations]} """ diff --git a/molecularnodes/io/parse/bcif.py b/molecularnodes/io/parse/bcif.py index f99fe954..362c6ae0 100644 --- a/molecularnodes/io/parse/bcif.py +++ b/molecularnodes/io/parse/bcif.py @@ -39,11 +39,11 @@ def _atom_array_from_bcif(open_bcif): # check if a petworld CellPack model or not is_petworld = False - if 'PDB_model_num' in categories['pdbx_struct_assembly_gen'].field_names: - print('PetWorld!') + if "PDB_model_num" in categories["pdbx_struct_assembly_gen"].field_names: + print("PetWorld!") is_petworld = True - atom_site = categories['atom_site'] + atom_site = categories["atom_site"] n_atoms = atom_site.row_count # Initialise the atom array that will contain all of the data for the atoms @@ -51,32 +51,29 @@ def _atom_array_from_bcif(open_bcif): # we first pull out the coordinates as they are from 3 different fields, but all # other fields should be single self-contained fields mol = AtomArray(n_atoms) - coord_field_names = [f'Cartn_{axis}' for axis in 'xyz'] - mol.coord = np.hstack(list([ - np.array(atom_site[column]).reshape((n_atoms, 1)) for column in coord_field_names - ])) + coord_field_names = [f"Cartn_{axis}" for axis in "xyz"] + mol.coord = np.hstack(list([np.array(atom_site[column]).reshape((n_atoms, 1)) for column in coord_field_names])) # the list of current atom_site_lookup = { - # have to make sure the chain_id ends up being the same as the space operatore - 'label_asym_id': 'chain_id', - 'label_atom_id': 'atom_name', - 'label_comp_id': 'res_name', - 'type_symbol': 'element', - 'label_seq_id': 'res_id', - 'B_iso_or_equiv': 'b_factor', - 'label_entity_id': 'entity_id', - 'pdbx_PDB_model_num': 'model_id', - 'pdbx_formal_charge': 'charge', - 'occupancy': 'occupany', - 'id': 'atom_id' + "label_asym_id": "chain_id", + "label_atom_id": "atom_name", + "label_comp_id": "res_name", + "type_symbol": "element", + "label_seq_id": "res_id", + "B_iso_or_equiv": "b_factor", + "label_entity_id": "entity_id", + "pdbx_PDB_model_num": "model_id", + "pdbx_formal_charge": "charge", + "occupancy": "occupany", + "id": "atom_id", } if is_petworld: # annotations[0][1] = 'pdbx_PDB_model_num' - atom_site_lookup.pop('label_asym_id') - atom_site_lookup['pdbx_PDB_model_num'] = 'chain_id' + atom_site_lookup.pop("label_asym_id") + atom_site_lookup["pdbx_PDB_model_num"] = "chain_id" for name in atom_site.field_names: # the coordinates have already been extracted so we can skip over those field names @@ -95,10 +92,8 @@ def _atom_array_from_bcif(open_bcif): # TODO this could be expanded to capture fields that are entirely '' and drop them # or fill them with 0s - if annotation_name == 'res_id' and data[0] == '': - data = np.array([ - 0 if x == '' else x for x in data - ]) + if annotation_name == "res_id" and data[0] == "": + data = np.array([0 if x == "" else x for x in data]) mol.set_annotation(annotation_name, data) @@ -115,42 +110,37 @@ def rotation_from_matrix(matrix): def _get_ops_from_bcif(open_bcif): is_petworld = False cats = open_bcif.data_blocks[0] - assembly_gen = cats['pdbx_struct_assembly_gen'] - gen_arr = np.column_stack( - list([assembly_gen[name] for name in assembly_gen.field_names])) + assembly_gen = cats["pdbx_struct_assembly_gen"] + gen_arr = np.column_stack(list([assembly_gen[name] for name in assembly_gen.field_names])) dtype = [ - ('assembly_id', int), - ('chain_id', 'U10'), - ('trans_id', int), - ('rotation', float, 4), # quaternion form rotations - ('translation', float, 3) + ("assembly_id", int), + ("chain_id", "U10"), + ("trans_id", int), + ("rotation", float, 4), # quaternion form rotations + ("translation", float, 3), ] - ops = cats['pdbx_struct_oper_list'] + ops = cats["pdbx_struct_oper_list"] ok_names = [ - 'matrix[1][1]', - 'matrix[1][2]', - 'matrix[1][3]', - 'matrix[2][1]', - 'matrix[2][2]', - 'matrix[2][3]', - 'matrix[3][1]', - 'matrix[3][2]', - 'matrix[3][3]', - 'vector[1]', - 'vector[2]', - 'vector[3]' + "matrix[1][1]", + "matrix[1][2]", + "matrix[1][3]", + "matrix[2][1]", + "matrix[2][2]", + "matrix[2][3]", + "matrix[3][1]", + "matrix[3][2]", + "matrix[3][3]", + "vector[1]", + "vector[2]", + "vector[3]", ] # test if petworld - if 'PDB_model_num' in assembly_gen.field_names: - print('PetWorld!') + if "PDB_model_num" in assembly_gen.field_names: + print("PetWorld!") is_petworld = True - op_ids = np.array(ops['id']) - struct_ops = np.column_stack(list([ - np.array(ops[name]).reshape((ops.row_count, 1)) for name in ok_names - ])) - rotations = np.array(list([ - rotation_from_matrix(x[0:9].reshape((3, 3))) for x in struct_ops - ])) + op_ids = np.array(ops["id"]) + struct_ops = np.column_stack(list([np.array(ops[name]).reshape((ops.row_count, 1)) for name in ok_names])) + rotations = np.array(list([rotation_from_matrix(x[0:9].reshape((3, 3))) for x in struct_ops])) translations = struct_ops[:, 9:12] gen_list = [] @@ -160,31 +150,29 @@ def _get_ops_from_bcif(open_bcif): if "," in gen[1]: for gexpr in gen[1].split(","): if "-" in gexpr: - start, end = [int(x) - for x in gexpr.strip('()').split('-')] + start, end = [int(x) for x in gexpr.strip("()").split("-")] ids.extend((np.array(range(start, end + 1))).tolist()) else: - ids.append(int(gexpr.strip('()'))) + ids.append(int(gexpr.strip("()"))) else: - start, end = [int(x) for x in gen[1].strip('()').split('-')] + start, end = [int(x) for x in gen[1].strip("()").split("-")] ids.extend((np.array(range(start, end + 1))).tolist()) else: - ids = np.array([int(x) - for x in gen[1].strip("()").split(",")]).tolist() + ids = np.array([int(x) for x in gen[1].strip("()").split(",")]).tolist() real_ids = np.nonzero(np.in1d(op_ids, ids))[0] - chains = np.array(gen[2].strip(' ').split(',')) + chains = np.array(gen[2].strip(" ").split(",")) if is_petworld: # all chain of the model receive theses transformation chains = np.array([gen[3]]) arr = np.zeros(chains.size * len(real_ids), dtype=dtype) - arr['chain_id'] = np.tile(chains, len(real_ids)) + arr["chain_id"] = np.tile(chains, len(real_ids)) mask = np.repeat(np.array(real_ids), len(chains)) try: - arr['trans_id'] = gen[3] + arr["trans_id"] = gen[3] except IndexError: pass - arr['rotation'] = rotations[mask, :] - arr['translation'] = translations[mask, :] + arr["rotation"] = rotations[mask, :] + arr["translation"] = translations[mask, :] gen_list.append(arr) return np.concatenate(gen_list) @@ -240,8 +228,7 @@ def _decode(encoded_data: EncodedData) -> Union[np.ndarray, List[str]]: result = encoded_data["data"] for encoding in encoded_data["encoding"][::-1]: if encoding["kind"] in _decoders: - result = _decoders[encoding["kind"]]( - result, encoding) # type: ignore + result = _decoders[encoding["kind"]](result, encoding) # type: ignore else: raise ValueError(f"Unsupported encoding '{encoding['kind']}'") @@ -325,20 +312,13 @@ def _decode_fixed_point(data: np.ndarray, encoding: FixedPointEncoding) -> np.nd return np.array(data, dtype=_get_dtype(encoding["srcType"])) / encoding["factor"] -def _decode_interval_quantization( - data: np.ndarray, encoding: IntervalQuantizationEncoding -) -> np.ndarray: +def _decode_interval_quantization(data: np.ndarray, encoding: IntervalQuantizationEncoding) -> np.ndarray: delta = (encoding["max"] - encoding["min"]) / (encoding["numSteps"] - 1) - return ( - np.array(data, dtype=_get_dtype( - encoding["srcType"])) * delta + encoding["min"] - ) + return np.array(data, dtype=_get_dtype(encoding["srcType"])) * delta + encoding["min"] def _decode_run_length(data: np.ndarray, encoding: RunLengthEncoding) -> np.ndarray: - return np.repeat( - np.array(data[::2], dtype=_get_dtype(encoding["srcType"])), repeats=data[1::2] - ) + return np.repeat(np.array(data[::2], dtype=_get_dtype(encoding["srcType"])), repeats=data[1::2]) def _decode_delta(data: np.ndarray, encoding: DeltaEncoding) -> np.ndarray: @@ -348,9 +328,7 @@ def _decode_delta(data: np.ndarray, encoding: DeltaEncoding) -> np.ndarray: return np.cumsum(result, out=result) -def _decode_integer_packing_signed( - data: np.ndarray, encoding: IntegerPackingEncoding -) -> np.ndarray: +def _decode_integer_packing_signed(data: np.ndarray, encoding: IntegerPackingEncoding) -> np.ndarray: upper_limit = 0x7F if encoding["byteCount"] == 1 else 0x7FFF lower_limit = -upper_limit - 1 n = len(data) @@ -371,9 +349,7 @@ def _decode_integer_packing_signed( return output -def _decode_integer_packing_unsigned( - data: np.ndarray, encoding: IntegerPackingEncoding -) -> np.ndarray: +def _decode_integer_packing_unsigned(data: np.ndarray, encoding: IntegerPackingEncoding) -> np.ndarray: upper_limit = 0xFF if encoding["byteCount"] == 1 else 0xFFFF n = len(data) output = np.zeros(encoding["srcSize"], dtype="i4") @@ -393,9 +369,7 @@ def _decode_integer_packing_unsigned( return output -def _decode_integer_packing( - data: np.ndarray, encoding: IntegerPackingEncoding -) -> np.ndarray: +def _decode_integer_packing(data: np.ndarray, encoding: IntegerPackingEncoding) -> np.ndarray: if len(data) == encoding["srcSize"]: return data if encoding["isUnsigned"]: @@ -405,17 +379,13 @@ def _decode_integer_packing( def _decode_string_array(data: bytes, encoding: StringArrayEncoding) -> List[str]: - offsets = _decode( - EncodedData( - encoding=encoding["offsetEncoding"], data=encoding["offsets"]) - ) - indices = _decode(EncodedData( - encoding=encoding["dataEncoding"], data=data)) + offsets = _decode(EncodedData(encoding=encoding["offsetEncoding"], data=encoding["offsets"])) + indices = _decode(EncodedData(encoding=encoding["dataEncoding"], data=data)) str = encoding["stringData"] strings = [""] for i in range(1, len(offsets)): - strings.append(str[offsets[i - 1]: offsets[i]]) # type: ignore + strings.append(str[offsets[i - 1] : offsets[i]]) # type: ignore return [strings[i + 1] for i in indices] # type: ignore @@ -499,12 +469,8 @@ def __contains__(self, key: str): def __init__(self, category: EncodedCategory, lazy: bool): self.field_names = [c["name"] for c in category["columns"]] - self._field_cache = { - c["name"]: None if lazy else _decode_column(c) for c in category["columns"] - } - self._columns: Dict[str, EncodedColumn] = { - c["name"]: c for c in category["columns"] - } + self._field_cache = {c["name"]: None if lazy else _decode_column(c) for c in category["columns"]} + self._columns: Dict[str, EncodedColumn] = {c["name"]: c for c in category["columns"]} self.row_count = category["rowCount"] self.name = category["name"][1:] @@ -530,17 +496,9 @@ def __getitem__(self, index_or_name: Union[int, str]): Access a data block by index or header (case sensitive) """ if isinstance(index_or_name, str): - return ( - self._block_map[index_or_name] - if index_or_name in self._block_map - else None - ) + return self._block_map[index_or_name] if index_or_name in self._block_map else None else: - return ( - self.data_blocks[index_or_name] - if index_or_name < len(self.data_blocks) - else None - ) + return self.data_blocks[index_or_name] if index_or_name < len(self.data_blocks) else None def __len__(self): return len(self.data_blocks) @@ -555,8 +513,7 @@ def __init__(self, data_blocks: List[CifDataBlock]): def _decode_column(column: EncodedColumn) -> CifField: values = _decode(column["data"]) - value_kinds = _decode( - column["mask"]) if column["mask"] else None # type: ignore + value_kinds = _decode(column["mask"]) if column["mask"] else None # type: ignore # type: ignore return CifField(name=column["name"], values=values, value_kinds=value_kinds) @@ -570,16 +527,12 @@ def loads(data: Union[bytes, EncodedFile], lazy=True) -> CifFile: """ import msgpack - file: EncodedFile = data if isinstance( - data, dict) and "dataBlocks" in data else msgpack.loads(data) # type: ignore + file: EncodedFile = data if isinstance(data, dict) and "dataBlocks" in data else msgpack.loads(data) # type: ignore data_blocks = [ CifDataBlock( header=block["header"], - categories={ - cat["name"][1:]: CifCategory(category=cat, lazy=lazy) - for cat in block["categories"] - }, + categories={cat["name"][1:]: CifCategory(category=cat, lazy=lazy) for cat in block["categories"]}, ) for block in file["dataBlocks"] ] diff --git a/molecularnodes/io/parse/cellpack.py b/molecularnodes/io/parse/cellpack.py index f79fae53..cb49f7d7 100644 --- a/molecularnodes/io/parse/cellpack.py +++ b/molecularnodes/io/parse/cellpack.py @@ -1,5 +1,5 @@ from pathlib import Path - +from typing import Union import numpy as np import bpy @@ -12,7 +12,7 @@ class CellPack(Ensemble): - def __init__(self, file_path): + def __init__(self, file_path: Union[str, Path]) -> None: super().__init__(file_path) self.file_type = self._file_type() self.data = self._read(self.file_path) @@ -21,17 +21,10 @@ def __init__(self, file_path): self.chain_ids = self.data.chain_ids def create_model( - self, - name='CellPack', - node_setup: bool = True, - world_scale: float = 0.01, - fraction: float = 1.0 - ): - self.data_object = self._create_data_object(name=f'{name}') - self._create_object_instances( - name=name, - node_setup=node_setup - ) + self, name: str = "StarFileObject", node_setup: bool = True, world_scale: float = 0.01, fraction: float = 1.0 + ) -> bpy.types.Object: + self.data_object = self._create_data_object(name=f"{name}") + self._create_object_instances(name=name, node_setup=node_setup) self._setup_node_tree(fraction=fraction) @@ -53,11 +46,7 @@ def _read(self, file_path): return data - def _create_object_instances( - self, - name: str = 'CellPack', - node_setup: bool = True - ) -> bpy.types.Collection: + def _create_object_instances(self, name: str = "CellPack", node_setup: bool = True) -> bpy.types.Collection: collection = bl.coll.cellpack(name) if self.file_type == "cif": @@ -69,7 +58,7 @@ def _create_object_instances( model, coll_none = molecule._create_model( array=chain_atoms, name=f"{str(i).rjust(4, '0')}_{chain}", - collection=collection + collection=collection, ) colors = np.tile(color.random_rgb(i), (len(chain_atoms), 1)) @@ -78,47 +67,33 @@ def _create_object_instances( name="Color", data=colors, type="FLOAT_COLOR", - overwrite=True + overwrite=True, ) if node_setup: - bl.nodes.create_starting_node_tree( - model, - name=f"MN_pack_instance_{name}", - set_color=False - ) + bl.nodes.create_starting_node_tree(model, name=f"MN_pack_instance_{name}", set_color=False) self.data_collection = collection return collection - def _create_data_object(self, name='DataObject'): - data_object = bl.obj.create_data_object( - self.transformations, - name=name, - collection=bl.coll.mn() - ) + def _create_data_object(self, name="DataObject"): + data_object = bl.obj.create_data_object(self.transformations, name=name, collection=bl.coll.mn()) - data_object['chain_ids'] = self.chain_ids + data_object["chain_ids"] = self.chain_ids return data_object - def _setup_node_tree( - self, - name='CellPack', - fraction=1.0, - as_points=False - ): + def _setup_node_tree(self, name="CellPack", fraction=1.0, as_points=False): mod = bl.nodes.get_mod(self.data_object) - group = bl.nodes.new_group(name=f"MN_ensemble_{name}", fallback=False) + group = bl.nodes.new_tree(name=f"MN_ensemble_{name}", fallback=False) mod.node_group = group - node_pack = bl.nodes.add_custom( - group, 'MN_pack_instances', location=[-100, 0]) - node_pack.inputs['Collection'].default_value = self.data_collection - node_pack.inputs['Fraction'].default_value = fraction - node_pack.inputs['As Points'].default_value = as_points + node_pack = bl.nodes.add_custom(group, "MN_pack_instances", location=[-100, 0]) + node_pack.inputs["Collection"].default_value = self.data_collection + node_pack.inputs["Fraction"].default_value = fraction + node_pack.inputs["As Points"].default_value = as_points link = group.links.new link(bl.nodes.get_input(group).outputs[0], node_pack.inputs[0]) diff --git a/molecularnodes/io/parse/cif.py b/molecularnodes/io/parse/cif.py index 1dafda0f..e1c8d835 100644 --- a/molecularnodes/io/parse/cif.py +++ b/molecularnodes/io/parse/cif.py @@ -11,14 +11,12 @@ def __init__(self, file_path, extra_fields=None, sec_struct=True): super().__init__() self.file_path = file_path self.file = self._read() - self.array = self._get_structure( - extra_fields=extra_fields, - sec_struct=sec_struct - ) + self.array = self._get_structure(extra_fields=extra_fields, sec_struct=sec_struct) self.n_atoms = self.array.array_length() def _read(self): import biotite.structure.io.pdbx as pdbx + return pdbx.legacy.PDBxFile.read(self.file_path) def _get_structure(self, extra_fields: str = None, sec_struct=True, bonds=True): @@ -26,7 +24,7 @@ def _get_structure(self, extra_fields: str = None, sec_struct=True, bonds=True): import biotite.structure as struc from biotite import InvalidFileError - fields = ['b_factor', 'charge', 'occupancy', 'atom_id'] + fields = ["b_factor", "charge", "occupancy", "atom_id"] if extra_fields: [fields.append(x) for x in extra_fields] @@ -35,15 +33,13 @@ def _get_structure(self, extra_fields: str = None, sec_struct=True, bonds=True): try: array = pdbx.get_structure(self.file, extra_fields=extra_fields) try: - array.set_annotation( - 'sec_struct', _get_secondary_structure(array, self.file)) + array.set_annotation("sec_struct", _get_secondary_structure(array, self.file)) except KeyError: - warnings.warn('No secondary structure information.') + warnings.warn("No secondary structure information.") try: - array.set_annotation( - 'entity_id', _get_entity_id(array, self.file)) + array.set_annotation("entity_id", _get_entity_id(array, self.file)) except KeyError: - warnings.warn('Non entity_id information.') + warnings.warn("Non entity_id information.") except InvalidFileError: array = pdbx.get_component(self.file) @@ -51,26 +47,25 @@ def _get_structure(self, extra_fields: str = None, sec_struct=True, bonds=True): # pdbx files don't seem to have bond information defined, so connect them based # on their residue names if not array.bonds and bonds: - array.bonds = struc.bonds.connect_via_residue_names( - array, inter_residue=True) + array.bonds = struc.bonds.connect_via_residue_names(array, inter_residue=True) return array def _entity_ids(self): - entities = self.file['entity'] + entities = self.file["entity"] if not entities: return None - return entities.get('pdbx_description', None) + return entities.get("pdbx_description", None) def _assemblies(self): return CIFAssemblyParser(self.file).get_assemblies() def _ss_label_to_int(label): - if 'HELX' in label: + if "HELX" in label: return 1 - elif 'STRN' in label: + elif "STRN" in label: return 2 else: return 3 @@ -108,23 +103,23 @@ def _get_secondary_structure(array, file): # alpha helices, but will sometimes contain also other secondary structure # information such as in AlphaFold predictions - conf = file.get_category('struct_conf') + conf = file.get_category("struct_conf") if not conf: raise KeyError - starts = conf['beg_auth_seq_id'].astype(int) - ends = conf['end_auth_seq_id'].astype(int) - chains = conf['end_auth_asym_id'].astype(str) - id_label = conf['id'].astype(str) + starts = conf["beg_auth_seq_id"].astype(int) + ends = conf["end_auth_seq_id"].astype(int) + chains = conf["end_auth_asym_id"].astype(str) + id_label = conf["id"].astype(str) # most files will have a separate category for the beta sheets # this can just be appended to the other start / end / id and be processed # as normal - sheet = file.get_category('struct_sheet_range') + sheet = file.get_category("struct_sheet_range") if sheet: - starts = np.append(starts, sheet['beg_auth_seq_id'].astype(int)) - ends = np.append(ends, sheet['end_auth_seq_id'].astype(int)) - chains = np.append(chains, sheet['end_auth_asym_id'].astype(str)) - id_label = np.append(id_label, np.repeat('STRN', len(sheet['id']))) + starts = np.append(starts, sheet["beg_auth_seq_id"].astype(int)) + ends = np.append(ends, sheet["end_auth_seq_id"].astype(int)) + chains = np.append(chains, sheet["end_auth_asym_id"].astype(str)) + id_label = np.append(id_label, np.repeat("STRN", len(sheet["id"]))) # convert the string labels to integer representations of the SS # AH: 1, BS: 2, LOOP: 3 @@ -137,12 +132,12 @@ def _get_secondary_structure(array, file): lookup = dict() for chain in np.unique(chains): arrays = [] - mask = (chain == chains) + mask = chain == chains start_sub = starts[mask] end_sub = ends[mask] id_sub = id_int[mask] - for (start, end, id) in zip(start_sub, end_sub, id_sub): + for start, end, id in zip(start_sub, end_sub, id_sub): idx = np.arange(start, end + 1, dtype=int) arr = np.zeros((len(idx), 2), dtype=int) arr[:, 0] = idx @@ -166,10 +161,10 @@ def _get_secondary_structure(array, file): def _get_entity_id(array, file): - entities = file.get_category('entity_poly') + entities = file.get_category("entity_poly") if not entities: raise KeyError - chain_ids = entities['pdbx_strand_id'] + chain_ids = entities["pdbx_strand_id"] # the chain_ids are an array of individual items np.array(['A,B', 'C', 'D,E,F']) # which need to be categorised as [1, 1, 2, 3, 3, 3] for their belonging to individual @@ -178,13 +173,12 @@ def _get_entity_id(array, file): chains = [] idx = [] for i, chain_str in enumerate(chain_ids): - for chain in chain_str.split(','): + for chain in chain_str.split(","): chains.append(chain) idx.append(i) entity_lookup = dict(zip(chains, idx)) - chain_id_int = np.array([entity_lookup.get(chain, -1) - for chain in array.chain_id], int) + chain_id_int = np.array([entity_lookup.get(chain, -1) for chain in array.chain_id], int) return chain_id_int @@ -196,6 +190,7 @@ def __init__(self, file_cif): def list_assemblies(self): import biotite.structure.io.pdbx as pdbx + return list(pdbx.list_assemblies(self._file).keys()) def get_transformations(self, assembly_id): @@ -277,17 +272,9 @@ def _get_transformations(struct_oper): transformation_dict = {} for index, id in enumerate(struct_oper["id"]): rotation_matrix = np.array( - [ - [ - float(struct_oper[f"matrix[{i}][{j}]"][index]) - for j in (1, 2, 3) - ] - for i in (1, 2, 3) - ] - ) - translation_vector = np.array( - [float(struct_oper[f"vector[{i}]"][index]) for i in (1, 2, 3)] + [[float(struct_oper[f"matrix[{i}][{j}]"][index]) for j in (1, 2, 3)] for i in (1, 2, 3)] ) + translation_vector = np.array([float(struct_oper[f"vector[{i}]"][index]) for i in (1, 2, 3)]) transformation_dict[id] = (rotation_matrix, translation_vector) return transformation_dict @@ -313,18 +300,13 @@ def _parse_operation_expression(expression): for gexpr in expr.split(","): if "-" in gexpr: first, last = gexpr.split("-") - operations.append( - [str(id) - for id in range(int(first), int(last) + 1)] - ) + operations.append([str(id) for id in range(int(first), int(last) + 1)]) else: operations.append([gexpr]) else: # Range of operation IDs, they must be integers first, last = expr.split("-") - operations.append( - [str(id) for id in range(int(first), int(last) + 1)] - ) + operations.append([str(id) for id in range(int(first), int(last) + 1)]) elif "," in expr: # List of operation IDs operations.append(expr.split(",")) diff --git a/molecularnodes/io/parse/density.py b/molecularnodes/io/parse/density.py index bc16cd16..086efff4 100644 --- a/molecularnodes/io/parse/density.py +++ b/molecularnodes/io/parse/density.py @@ -1,8 +1,10 @@ from abc import ABCMeta import os -import numpy as np import bpy +from typing import Union, Optional +from pathlib import Path + class Density(metaclass=ABCMeta): """ @@ -10,14 +12,14 @@ class Density(metaclass=ABCMeta): """ - def __init__(self, file_path): - self.file_path: str = None + def __init__(self, file_path: Union[str, Path]) -> None: + self.file_path = file_path self.grid = None - self.file_vdb: str = None - self.threshold: float = None - self.object: bpy.types.Object = None + self.file_vdb: Union[Path, str] + self.threshold: float = 1.0 + self.object: Optional[bpy.types.Object] = None - def path_to_vdb(self, file: str, center: False, invert: False): + def path_to_vdb(self, file: Union[Path, str], center: False, invert: False) -> Path: """ Convert a file path to a corresponding VDB file path. @@ -36,6 +38,6 @@ def path_to_vdb(self, file: str, center: False, invert: False): name = os.path.basename(file).split(".")[0] name += "_center" if center else "" name += "_invert" if invert else "" - file_name = name + '.vdb' + file_name = name + ".vdb" file_path = os.path.join(folder_path, file_name) - return file_path + return Path(file_path) diff --git a/molecularnodes/io/parse/ensemble.py b/molecularnodes/io/parse/ensemble.py index fb00e781..6b42ea12 100644 --- a/molecularnodes/io/parse/ensemble.py +++ b/molecularnodes/io/parse/ensemble.py @@ -1,12 +1,14 @@ import bpy from abc import ABCMeta import numpy as np +from typing import Union +from pathlib import Path from ... import blender as bl import warnings class Ensemble(metaclass=ABCMeta): - def __init__(self, file_path): + def __init__(self, file_path: Union[str, Path]) -> None: """ Initialize an Ensemble object. @@ -17,13 +19,15 @@ def __init__(self, file_path): """ self.type: str = "ensemble" - self.file_path: str = file_path + self.file_path: str = str(file_path) self.object: bpy.types.Object = None self.instances: bpy.types.Collection = None self.frames: bpy.types.Collection = None @classmethod - def create_model(cls, name: str = "NewEnsemble", node_setup: bool = True, world_scale: float = 0.01, fraction: float = 1.0, simplify=False): + def create_model( + self, name: str = "EnsembleObject", node_setup: bool = True, world_scale: float = 0.01, fraction: float = 1.0 + ) -> bpy.types.Object: """ Create a 3D model in the of the ensemble. @@ -40,7 +44,7 @@ def create_model(cls, name: str = "NewEnsemble", node_setup: bool = True, world_ simplify : bool, optional Whether to isntance the given models or simplify them for debugging and performance. (default is False). - Creates a data object which stores all of the required instancing information. If + Creates a data object which stores all of the required instancing information. If there are molecules to be instanced, they are also created in their own data collection. Parameters: @@ -53,7 +57,7 @@ def create_model(cls, name: str = "NewEnsemble", node_setup: bool = True, world_ """ pass - def get_attribute(self, name='position', evaluate=False) -> np.ndarray | None: + def get_attribute(self, name="position", evaluate=False) -> np.ndarray | None: """ Get the value of an object for the data molecule. @@ -62,8 +66,8 @@ def get_attribute(self, name='position', evaluate=False) -> np.ndarray | None: name : str, optional The name of the attribute. Default is 'position'. evaluate : bool, optional - Whether to first evaluate all node trees before getting the requsted attribute. - False (default) will sample the underlying atomic geometry, while True will + Whether to first evaluate all node trees before getting the requsted attribute. + False (default) will sample the underlying atomic geometry, while True will sample the geometry that is created through the Geometry Nodes tree. Returns @@ -72,8 +76,6 @@ def get_attribute(self, name='position', evaluate=False) -> np.ndarray | None: The value of the attribute. """ if not self.object: - warnings.warn( - 'No object yet created. Use `create_model()` to create a corresponding object.' - ) + warnings.warn("No object yet created. Use `create_model()` to create a corresponding object.") return None return bl.obj.get_attribute(self.object, name=name, evaluate=evaluate) diff --git a/molecularnodes/io/parse/mda.py b/molecularnodes/io/parse/mda.py index 0e684da0..b08b7b00 100644 --- a/molecularnodes/io/parse/mda.py +++ b/molecularnodes/io/parse/mda.py @@ -1,44 +1,22 @@ import bpy from bpy.app.handlers import persistent -try: - import MDAnalysis as mda -except ImportError: - HAS_mda = False - import types - - class MockAtomGroup: - pass - - class MockUniverse: - pass - - mda = types.ModuleType("MDAnalysis") - mda.Universe = MockUniverse - mda.AtomGroup = MockAtomGroup - mda.core = types.ModuleType("core") - mda.topology = types.ModuleType("topology") - -else: - HAS_mda = True + +import MDAnalysis as mda import numpy as np +from numpy.typing import NDArray import warnings import pickle from typing import Union, List, Dict from ... import data from ...pkg import start_logging -from ...blender import ( - coll, obj, nodes -) +from ...blender import coll, obj, nodes from ...utils import lerp class AtomGroupInBlender: - def __init__(self, - ag: mda.AtomGroup, - style: str = "vdw", - world_scale: float = 0.01): + def __init__(self, ag: mda.AtomGroup, style: str = "vdw", world_scale: float = 0.01): """ AtomGroup in Blender. It will be dynamically updated when the frame changes or @@ -108,31 +86,29 @@ def __init__(self, is_solvent : np.ndarray Whether the atoms in the atomgroup are solvent. """ - if not HAS_mda: - raise ImportError("MDAnalysis is not installed.") self.ag = ag self.world_scale = world_scale self.style = style @property def n_atoms(self) -> int: - return self.ag.n_atoms + return int(self.ag.n_atoms) @property def style(self) -> str: - return self._style + return str(self._style) @style.setter - def style(self, style): + def style(self, style: str) -> None: self._style = style @staticmethod - def bool_selection(ag, selection) -> np.ndarray: + def bool_selection(ag: mda.AtomGroup, selection: NDArray[np.bool_]) -> NDArray[np.bool_]: return np.isin(ag.ix, ag.select_atoms(selection).ix).astype(bool) @property - def positions(self) -> np.ndarray: - return self.ag.positions * self.world_scale + def positions(self) -> NDArray[np.float32]: + return self.ag.positions * self.world_scale # type: ignore @property def bonds(self) -> List[List[int]]: @@ -144,8 +120,7 @@ def bonds(self) -> List[List[int]]: index_map = {index: i for i, index in enumerate(self.ag.indices)} - bonds = [[index_map[bond[0]], index_map[bond[1]]] - for bond in bond_indices] + bonds = [[index_map[bond[0]], index_map[bond[1]]] for bond in bond_indices] else: bonds = [] return bonds @@ -154,42 +129,44 @@ def bonds(self) -> List[List[int]]: def elements(self) -> List[str]: try: elements = self.ag.elements.tolist() - except: + except AttributeError: # If 'elements' attribute doesn't exist try: elements = [ - x if x in data.elements.keys() else - mda.topology.guessers.guess_atom_element(x) for x in self.ag.atoms.names] - - except: - elements = ['X'] * self.ag.n_atoms - return elements + x if x in data.elements.keys() else mda.topology.guessers.guess_atom_element(x) + for x in self.ag.atoms.names + ] + except ( + KeyError, + ValueError, + ): # If 'x' is not in 'data.elements.keys()' or 'guess_atom_element(x)' fails + elements = ["X"] * self.ag.n_atoms + return elements # type: ignore @property def atomic_number(self) -> np.ndarray: return np.array( - [data.elements.get(element, - data.elements.get('X')) - .get('atomic_number') for element in self.elements] + [data.elements.get(element, data.elements.get("X")).get("atomic_number") for element in self.elements] ) @property def vdw_radii(self) -> np.ndarray: # pm to Angstrom - return np.array( - [data.elements.get(element,{}).get( - 'vdw_radii',100) for element in self.elements]) * 0.01 * self.world_scale + return ( + np.array([data.elements.get(element, {}).get("vdw_radii", 100) for element in self.elements]) + * 0.01 + * self.world_scale + ) @property def mass(self) -> np.ndarray: # units: daltons - try: + try: masses = np.array([x.mass for x in self.ag.atoms]) except mda.exceptions.NoDataError: masses = np.array( - [data.elements.get(element, - {'standard_mass': 0}) - .get('standard_mass') for element in self.elements]) - return masses + [data.elements.get(element, {"standard_mass": 0}).get("standard_mass") for element in self.elements] + ) + return masses @property def res_id(self) -> np.ndarray: @@ -202,9 +179,7 @@ def res_name(self) -> np.ndarray: @property def res_num(self) -> np.ndarray: return np.array( - [data.residues.get(res_name, - data.residues.get('UNK')) - .get('res_name_num') for res_name in self.res_name] + [data.residues.get(res_name, data.residues.get("UNK")).get("res_name_num") for res_name in self.res_name] ) @property @@ -227,8 +202,7 @@ def chain_ids(self) -> np.ndarray: @property def chain_id_num(self) -> np.ndarray: - chain_ids, chain_id_index = np.unique( - self.chain_id, return_inverse=True) + chain_ids, chain_id_index = np.unique(self.chain_id, return_inverse=True) return chain_id_index @property @@ -241,8 +215,7 @@ def atom_type_unique(self) -> np.ndarray: @property def atom_type_num(self) -> np.ndarray: - atom_type_unique, atom_type_index = np.unique( - self.atom_type, return_inverse=True) + atom_type_unique, atom_type_index = np.unique(self.atom_type, return_inverse=True) return atom_type_index @property @@ -332,7 +305,7 @@ def _attributes_2_blender(self): "atom_name": { "value": self.atom_name_num, "type": "INT", - "domain": "POINT" + "domain": "POINT", }, "is_backbone": { "value": self.is_backbone, @@ -427,8 +400,6 @@ def __init__(self, world_scale: float = 0.01, in_memory: bool = False): Whether the old import is used (default: False). """ log = start_logging(logfile_name="mda") - if not HAS_mda: - raise ImportError("MDAnalysis is not installed.") # if the session already exists, load the existing session if hasattr(bpy.types.Scene, "mda_session"): @@ -446,12 +417,8 @@ def __init__(self, world_scale: float = 0.01, in_memory: bool = False): if in_memory: return bpy.types.Scene.mda_session = self - bpy.app.handlers.frame_change_post.append( - self._update_trajectory_handler_wrapper() - ) - bpy.app.handlers.depsgraph_update_pre.append( - self._update_style_handler_wrapper() - ) + bpy.app.handlers.frame_change_post.append(self._update_trajectory_handler_wrapper()) + bpy.app.handlers.depsgraph_update_pre.append(self._update_style_handler_wrapper()) log.info("MDAnalysis session is initialized.") @property @@ -476,47 +443,47 @@ def show( custom_selections: Dict[str, str] = {}, frame_mapping: np.ndarray = None, subframes: int = 0, - in_memory: bool = False + in_memory: bool = False, ): """ - Display an `MDAnalysis.Universe` or - `MDAnalysis.Atomgroup` in Blender. - - Parameters: - ---------- - atoms : MDAnalysis.Universe or MDAnalysis.Atomgroup - The universe to load into blender. - style : str, optional - The style to represent the atoms inside of Blender - (default: "vdw"). - selection : str, optional - The selection string for atom filtering - (default: "all"). - Uses MDAnalysis selection syntax. - name : str, optional - The name of the default atoms - (default: "atoms"). - custom_selections : dict, optional - A dictionary of custom selections for atom filtering with - {'name' : 'selection string'} - (default: {}). - Uses MDAnalysis selection syntax. - frame_mapping : np.ndarray, optional - A mapping from the frame indices in the Blender frame indices. - for example a frame_mapping of [0, 0, 1, 1, 2, 3] will map - the 1st frame (index 0) in the trajectory to the 1st and 2nd frames - in Blender and so on. - Note a subframes other than 1 will expand the frame_mapping from its - original length to (subframes + 1) * original length. - (default: None) which will map the frames in the trajectory - to the frames in Blender one-to-one. - subframes : int, optional - The number of subframes to interpolate between each frame. - (default: 0). - in_memory : bool, optional - Whether load the display in Blender by loading all the - frames as individual objects. - (default: False) + Display an `MDAnalysis.Universe` or + `MDAnalysis.Atomgroup` in Blender. + + Parameters: + ---------- + atoms : MDAnalysis.Universe or MDAnalysis.Atomgroup + The universe to load into blender. + style : str, optional + The style to represent the atoms inside of Blender + (default: "vdw"). + selection : str, optional + The selection string for atom filtering + (default: "all"). + Uses MDAnalysis selection syntax. + name : str, optional + The name of the default atoms + (default: "atoms"). + custom_selections : dict, optional + A dictionary of custom selections for atom filtering with + {'name' : 'selection string'} + (default: {}). + Uses MDAnalysis selection syntax. + frame_mapping : np.ndarray, optional + A mapping from the frame indices in the Blender frame indices. + for example a frame_mapping of [0, 0, 1, 1, 2, 3] will map + the 1st frame (index 0) in the trajectory to the 1st and 2nd frames + in Blender and so on. + Note a subframes other than 1 will expand the frame_mapping from its + original length to (subframes + 1) * original length. + (default: None) which will map the frames in the trajectory + to the frames in Blender one-to-one. + subframes : int, optional + The number of subframes to interpolate between each frame. + (default: 0). + in_memory : bool, optional + Whether load the display in Blender by loading all the + frames as individual objects. + (default: False) """ log = start_logging(logfile_name="mda") if in_memory: @@ -525,14 +492,12 @@ def show( style=style, selection=selection, name=name, - custom_selections=custom_selections + custom_selections=custom_selections, ) if frame_mapping is not None: - warnings.warn("Custom frame_mapping not supported" - "when in_memory is on.") + warnings.warn("Custom frame_mapping not supported" "when in_memory is on.") if subframes != 0: - warnings.warn("Custom subframes not supported" - "when in_memory is on.") + warnings.warn("Custom subframes not supported" "when in_memory is on.") log.info(f"{atoms} is loaded in memory.") return mol_object if isinstance(atoms, mda.Universe): @@ -542,8 +507,7 @@ def show( # if any frame_mapping is out of range, then raise an error if frame_mapping and (len(frame_mapping) > universe.trajectory.n_frames): - raise ValueError("one or more mapping values are" - "out of range for the trajectory") + raise ValueError("one or more mapping values are" "out of range for the trajectory") mol_object = self._process_atomgroup( ag=atoms, @@ -551,7 +515,8 @@ def show( subframes=subframes, name=name, style=style, - return_object=True) + return_object=True, + ) # add the custom selections if they exist for sel_name, sel in custom_selections.items(): @@ -565,11 +530,10 @@ def show( subframes=subframes, name=sel_name, style=style, - return_object=False + return_object=False, ) except ValueError: - warnings.warn( - "Unable to add custom selection: {}".format(name)) + warnings.warn("Unable to add custom selection: {}".format(name)) bpy.context.view_layer.objects.active = mol_object log.info(f"{atoms} is loaded.") @@ -582,7 +546,7 @@ def in_memory( selection: str = "all", name: str = "atoms", custom_selections: Dict[str, str] = {}, - node_setup: bool = True + node_setup: bool = True, ): """ Display an `MDAnalysis.Universe` or @@ -652,13 +616,15 @@ def in_memory( if add_occupancy: try: obj.set_attribute(frame, "occupancy", ts.data["occupancy"]) - except: + except KeyError: + print("KeyError: 'occupancy' not found in ts.data") + add_occupancy = False + except TypeError: + print("TypeError: ts.data is not a dictionary or similar mapping type") add_occupancy = False # disable the frames collection from the viewer - bpy.context.view_layer.layer_collection.children[coll.mn().name].children[ - coll_frames.name - ].exclude = True + bpy.context.view_layer.layer_collection.children[coll.mn().name].children[coll_frames.name].exclude = True if node_setup: nodes.create_starting_node_tree( @@ -671,9 +637,7 @@ def in_memory( return mol_object - def transfer_to_memory( - self, start=None, stop=None, step=None, verbose=False, **kwargs - ): + def transfer_to_memory(self, start=None, stop=None, step=None, verbose=False, **kwargs): """ Transfer the trajectories in the session to memory. This is an alternative way to make sure the blender session is @@ -705,9 +669,7 @@ def transfer_to_memory( for rep_name in self.rep_names: universe = self.universe_reps[rep_name]["universe"] - universe.transfer_to_memory( - start=start, stop=stop, step=step, verbose=verbose, **kwargs - ) + universe.transfer_to_memory(start=start, stop=stop, step=step, verbose=verbose, **kwargs) log.info("The trajectories in this session is transferred to memory.") def _process_atomgroup( @@ -740,11 +702,7 @@ def _process_atomgroup( return_object : bool Whether to return the blender object or not. Default: False """ - ag_blender = AtomGroupInBlender( - ag=ag, - style=style, - world_scale=self.world_scale - ) + ag_blender = AtomGroupInBlender(ag=ag, style=style, world_scale=self.world_scale) # create the initial model mol_object = obj.create_object( name=name, @@ -755,13 +713,11 @@ def _process_atomgroup( # add the attributes for the model in blender for att_name, att in ag_blender._attributes_2_blender.items(): - obj.set_attribute( - mol_object, att_name, att["value"], att["type"], att["domain"] - ) - mol_object['chain_ids'] = ag_blender.chain_ids - mol_object['atom_type_unique'] = ag_blender.atom_type_unique - mol_object.mn['subframes'] = subframes - mol_object.mn['molecule_type'] = 'md' + obj.set_attribute(mol_object, att_name, att["value"], att["type"], att["domain"]) + mol_object["chain_ids"] = ag_blender.chain_ids + mol_object["atom_type_unique"] = ag_blender.atom_type_unique + mol_object.mn["subframes"] = subframes + mol_object.mn["molecule_type"] = "md" # add the atomgroup to the session # the name of the atomgroup may be different from @@ -770,9 +726,7 @@ def _process_atomgroup( # instead, the name generated by blender is used. if mol_object.name != name: warnings.warn( - "The name of the object is changed to {} because {} is already used.".format( - mol_object.name, name - ) + "The name of the object is changed to {} because {} is already used.".format(mol_object.name, name) ) self.atom_reps[mol_object.name] = ag_blender @@ -802,7 +756,7 @@ def _update_trajectory(self, frame): for rep_name in self.rep_names: universe = self.universe_reps[rep_name]["universe"] frame_mapping = self.universe_reps[rep_name]["frame_mapping"] - subframes = bpy.data.objects[rep_name].mn['subframes'] + subframes = bpy.data.objects[rep_name].mn["subframes"] if frame < 0: continue @@ -852,20 +806,15 @@ def _update_trajectory(self, frame): # then update as a new mol_object if isinstance(ag_rep.ag, mda.core.groups.UpdatingAtomGroup): mol_object.data.clear_geometry() - mol_object.data.from_pydata( - ag_rep.positions, - ag_rep.bonds, - faces=[]) + mol_object.data.from_pydata(ag_rep.positions, ag_rep.bonds, faces=[]) for att_name, att in ag_rep._attributes_2_blender.items(): - obj.set_attribute( - mol_object, att_name, att["value"], att["type"], att["domain"] - ) - mol_object['chain_id'] = ag_rep.chain_ids - mol_object['atom_type_unique'] = ag_rep.atom_type_unique - mol_object.mn['subframes'] = subframes + obj.set_attribute(mol_object, att_name, att["value"], att["type"], att["domain"]) + mol_object["chain_id"] = ag_rep.chain_ids + mol_object["atom_type_unique"] = ag_rep.atom_type_unique + mol_object.mn["subframes"] = subframes else: # update the positions of the underlying vertices - obj.set_attribute(mol_object, 'position', locations) + obj.set_attribute(mol_object, "position", locations) @persistent def _update_trajectory_handler_wrapper(self): @@ -873,6 +822,7 @@ def _update_trajectory_handler_wrapper(self): A wrapper for the update_trajectory function because Blender requires the function to be taking one argument. """ + def update_trajectory_handler(scene): frame = scene.frame_current self._update_trajectory(frame) @@ -885,6 +835,7 @@ def _update_style_handler_wrapper(self): A wrapper for the update_style function because Blender requires the function to be taking one argument. """ + def update_style_handler(scene): self._remove_deleted_mol_objects() # TODO: check for topology changes @@ -906,15 +857,14 @@ def _remove_deleted_mol_objects(self): def _dump(self, blender_save_loc): """ - Dump the session as a pickle file + Dump the session as a pickle file """ log = start_logging(logfile_name="mda") # get blender_save_loc blender_save_loc = blender_save_loc.split(".blend")[0] with open(f"{blender_save_loc}.mda_session", "wb") as f: pickle.dump(self, f) - log.info("MDAnalysis session is dumped to {}". - format(blender_save_loc)) + log.info("MDAnalysis session is dumped to {}".format(blender_save_loc)) @classmethod def _rejuvenate(cls, mol_objects): @@ -931,14 +881,9 @@ def _rejuvenate(cls, mol_objects): cls = pickle.load(f) except FileNotFoundError: return None - bpy.app.handlers.frame_change_post.append( - cls._update_trajectory_handler_wrapper() - ) - bpy.app.handlers.depsgraph_update_pre.append( - cls._update_style_handler_wrapper() - ) - log.info("MDAnalysis session is loaded from {}". - format(blend_file_name)) + bpy.app.handlers.frame_change_post.append(cls._update_trajectory_handler_wrapper()) + bpy.app.handlers.depsgraph_update_pre.append(cls._update_style_handler_wrapper()) + log.info("MDAnalysis session is loaded from {}".format(blend_file_name)) return cls @@ -968,8 +913,7 @@ def _rejuvenate_universe(scene): pass if len(mol_objects) > 0: - bpy.types.Scene.mda_session = MDAnalysisSession._rejuvenate( - mol_objects) + bpy.types.Scene.mda_session = MDAnalysisSession._rejuvenate(mol_objects) @persistent diff --git a/molecularnodes/io/parse/molecule.py b/molecularnodes/io/parse/molecule.py index c2b72e8f..b28a6864 100644 --- a/molecularnodes/io/parse/molecule.py +++ b/molecularnodes/io/parse/molecule.py @@ -1,6 +1,11 @@ from abc import ABCMeta -from typing import Optional, Any +from typing import Optional, Any, Tuple, Union, List, Dict +import biotite.database +import biotite.structure +from numpy.typing import NDArray +from pathlib import Path import warnings +import biotite import time import numpy as np import bpy @@ -17,7 +22,7 @@ class Molecule(metaclass=ABCMeta): (the object). If multiple conformations are imported, then a `frames` collection is also instantiated. - The `get_attribute()` and `set_attribute()` methods access and set attributes on + The `get_attribute()` and `set_attribute()` methods access and set attributes on `object` that is in the Blender scene. Attributes @@ -49,15 +54,16 @@ class Molecule(metaclass=ABCMeta): Get the biological assemblies of the molecule. """ - def __init__(self): - self.file_path: str = None - self.file: str = None + def __init__(self, file_path: str) -> None: + self.file_path: Union[Path, str] + self.file: biotite.file.TextFile self.object: Optional[bpy.types.Object] = None self.frames: Optional[bpy.types.Collection] = None self.array: Optional[np.ndarray] = None + self.entity_ids: Optional[List[str]] = None - def __len__(self): - if hasattr(self, 'object'): + def __len__(self) -> Union[int, None]: + if hasattr(self, "object"): if self.object: return len(self.object.data.vertices) if self.array: @@ -66,36 +72,38 @@ def __len__(self): return None @property - def n_models(self): + def n_models(self) -> int: import biotite.structure as struc + if isinstance(self.array, struc.AtomArray): return 1 - else: - return self.array.shape[0] + elif isinstance(self.array, struc.AtomArrayStack): + return len(self.array) + + return 0 @property - def chain_ids(self) -> Optional[list]: + def chain_ids(self) -> Optional[Any]: if self.array: - if hasattr(self.array, 'chain_id'): + if hasattr(self.array, "chain_id"): return np.unique(self.array.chain_id).tolist() - return None @property def name(self) -> Optional[str]: if self.object is not None: - return self.object.name + return str(self.object.name) else: - return None + return "" def set_attribute( self, data: np.ndarray, - name='NewAttribute', - type=None, - domain='POINT', - overwrite=True - ): + name: str = "NewAttribute", + type: Optional[str] = None, + domain: str = "POINT", + overwrite: bool = True, + ) -> None: """ Set an attribute for the molecule. @@ -107,30 +115,25 @@ def set_attribute( name : str, optional The name of the new attribute. Default is 'NewAttribute'. type : str, optional - If value is None (Default), the data type is inferred. The data type of the - attribute. Possbible values are ('FLOAT_VECTOR', 'FLOAT_COLOR", 'QUATERNION', + If value is None (Default), the data type is inferred. The data type of the + attribute. Possbible values are ('FLOAT_VECTOR', 'FLOAT_COLOR", 'QUATERNION', 'FLOAT', 'INT', 'BOOLEAN'). domain : str, optional - The domain of the attribute. Default is 'POINT'. Possible values are + The domain of the attribute. Default is 'POINT'. Possible values are currently ['POINT', 'EDGE', 'FACE', 'SPLINE'] overwrite : bool, optional - Whether to overwrite an existing attribute with the same name, or create a + Whether to overwrite an existing attribute with the same name, or create a new attribute with always a unique name. Default is True. """ - if not self.object: - warnings.warn( - f'No object yet created. Use `create_model()` to create a corresponding object.' - ) - return None bl.obj.set_attribute( self.object, name=name, data=data, domain=domain, - overwrite=overwrite + overwrite=overwrite, ) - def get_attribute(self, name='position', evaluate=False) -> np.ndarray | None: + def get_attribute(self, name: str = "position", evaluate: bool = False) -> np.ndarray: """ Get the value of an attribute for the associated object. @@ -139,8 +142,8 @@ def get_attribute(self, name='position', evaluate=False) -> np.ndarray | None: name : str, optional The name of the attribute. Default is 'position'. evaluate : bool, optional - Whether to first evaluate all node trees before getting the requsted attribute. - False (default) will sample the underlying atomic geometry, while True will + Whether to first evaluate all node trees before getting the requsted attribute. + False (default) will sample the underlying atomic geometry, while True will sample the geometry that is created through the Geometry Nodes tree. Returns @@ -148,21 +151,16 @@ def get_attribute(self, name='position', evaluate=False) -> np.ndarray | None: np.ndarray The value of the attribute. """ - if not self.object: - warnings.warn( - 'No object yet created. Use `create_model()` to create a corresponding object.' - ) - return None return bl.obj.get_attribute(self.object, name=name, evaluate=evaluate) - def list_attributes(self, evaluate=False) -> list | None: + def list_attributes(self, evaluate: bool = False) -> Optional[List[str]]: """ Returns a list of attribute names for the object. Parameters ---------- evaluate : bool, optional - Whether to first evaluate the modifiers on the object before listing the + Whether to first evaluate the modifiers on the object before listing the available attributes. Returns @@ -178,45 +176,43 @@ def list_attributes(self, evaluate=False) -> list | None: return list(self.object.data.attributes.keys()) - def centre(self, centre_type: str = 'centroid') -> np.ndarray: + def centre(self, centre_type: str = "centroid") -> np.ndarray: """ Calculate the centre of mass/geometry of the Molecule object :return: np.ndarray of shape (3,) user-defined centroid of all atoms in the Molecule object """ - positions = self.get_attribute(name='position') + positions = self.get_attribute(name="position") - if centre_type == 'centroid': + if centre_type == "centroid": return bl.obj.centre(positions) - elif centre_type == 'mass': - mass = self.get_attribute(name='mass') + elif centre_type == "mass": + mass = self.get_attribute(name="mass") return bl.obj.centre_weighted(positions, mass) else: - raise ValueError( - f"`{centre_type}` not a supported selection of ['centroid', 'mass']" - ) + raise ValueError(f"`{centre_type}` not a supported selection of ['centroid', 'mass']") def create_model( self, - name: str = 'NewMolecule', - style: str = 'spheres', - selection: np.ndarray = None, - build_assembly=False, - centre: str = '', + name: str = "NewMolecule", + style: str = "spheres", + selection: Optional[np.ndarray] = None, + build_assembly: bool = False, + centre: str = "", del_solvent: bool = True, - collection=None, + collection: Optional[bpy.types.Collection] = None, verbose: bool = False, ) -> bpy.types.Object: """ - Create a 3D model of the molecule inside of Blender. + Create a 3D model of the molecule inside of Blender. Creates a 3D model with one vertex per atom, and one edge per bond. Each vertex is given attributes which correspond to the atomic data such as `atomic_number` for the element and `res_name` for the residue name that the atom is associated with. If multiple conformations of the structure are detected, the collection attribute - is also created which will store an object for each conformation, so that the + is also created which will store an object for each conformation, so that the object can interpolate between those conformations. Parameters @@ -232,7 +228,7 @@ def create_model( centre : str, optional Denote method used to determine center of structure. Default is '', resulting in no translational motion being removed. Accepted values - are `centroid` or `mass`. Any other value will result in default + are `centroid` or `mass`. Any other value will result in default behavior. del_solvent : bool, optional Whether to delete solvent molecules. Default is True. @@ -271,14 +267,14 @@ def create_model( ) try: - model['entity_ids'] = self.entity_ids + model["entity_ids"] = self.entity_ids except AttributeError: - model['entity_ids'] = None + model["entity_ids"] = None try: - model['biological_assemblies'] = self.assemblies() + model["biological_assemblies"] = self.assemblies() except InvalidFileError: - model['biological_assemblies'] = None + model["biological_assemblies"] = None pass if build_assembly and style: @@ -291,7 +287,7 @@ def create_model( return model - def assemblies(self, as_array=False): + def assemblies(self, as_array: bool = False) -> Dict[str, List[float]] | None: """ Get the biological assemblies of the molecule. @@ -308,6 +304,7 @@ def assemblies(self, as_array=False): transformation matrices, or None if no assemblies are available. """ from biotite import InvalidFileError + try: assemblies_info = self._assemblies() except InvalidFileError: @@ -322,16 +319,18 @@ def __repr__(self) -> str: return f"" -def _create_model(array, - name=None, - centre='', - del_solvent=False, - style='spherers', - collection=None, - world_scale=0.01, - verbose=False - ) -> (bpy.types.Object, bpy.types.Collection): +def _create_model( + array: biotite.structure.AtomArray, + name: Optional[str] = None, + centre: str = "", + del_solvent: bool = False, + style: str = "spherers", + collection: bpy.types.Collection = None, + world_scale: float = 0.01, + verbose: bool = False, +) -> Tuple[bpy.types.Object, bpy.types.Collection]: import biotite.structure as struc + frames = None is_stack = isinstance(array, struc.AtomArrayStack) @@ -344,29 +343,23 @@ def _create_model(array, array = array[mask] try: - mass = np.array([ - data.elements.get(x, {}).get('standard_mass', 0.) - for x in np.char.title(array.element) - ]) - array.set_annotation('mass', mass) + mass = np.array([data.elements.get(x, {}).get("standard_mass", 0.0) for x in np.char.title(array.element)]) + array.set_annotation("mass", mass) except AttributeError: pass - def centre_array(atom_array, centre): - if centre == 'centroid': + def centre_array(atom_array: biotite.structure.AtomArray, centre: str) -> None: + if centre == "centroid": atom_array.coord -= bl.obj.centre(atom_array.coord) - elif centre == 'mass': - atom_array.coord -= bl.obj.centre_weighted( - array=atom_array.coord, - weight=atom_array.mass - ) + elif centre == "mass": + atom_array.coord -= bl.obj.centre_weighted(array=atom_array.coord, weight=atom_array.mass) - if centre in ['mass', 'centroid']: + if centre in ["mass", "centroid"]: if is_stack: for atom_array in array: centre_array(atom_array, centre) else: - centre_array(atom_array, centre) + centre_array(array, centre) if is_stack: if array.stack_depth() > 1: @@ -383,14 +376,14 @@ def centre_array(atom_array, centre): bonds_array = array.bonds.as_array() bond_idx = bonds_array[:, [0, 1]] # the .copy(order = 'C') is to fix a weird ordering issue with the resulting array - bond_types = bonds_array[:, 2].copy(order='C') + bond_types = bonds_array[:, 2].copy(order="C") # creating the blender object and meshes and everything mol = bl.obj.create_object( name=name, collection=collection, vertices=array.coord * world_scale, - edges=bond_idx + edges=bond_idx, ) # Add information about the bond types to the model on the edge domain @@ -398,8 +391,7 @@ def centre_array(atom_array, centre): # 'AROMATIC_SINGLE' = 5, 'AROMATIC_DOUBLE' = 6, 'AROMATIC_TRIPLE' = 7 # https://www.biotite-python.org/apidoc/biotite.structure.BondType.html#biotite.structure.BondType if array.bonds: - bl.obj.set_attribute(mol, name='bond_type', data=bond_types, - type="INT", domain="EDGE") + bl.obj.set_attribute(mol, name="bond_type", data=bond_types, type="INT", domain="EDGE") # The attributes for the model are initially defined as single-use functions. This allows # for a loop that attempts to add each attibute by calling the function. Only during this @@ -410,20 +402,19 @@ def centre_array(atom_array, centre): # I still don't like this as an implementation, and welcome any cleaner approaches that # anybody might have. - def att_atomic_number(): - atomic_number = np.array([ - data.elements.get(x, {'atomic_number': -1}).get("atomic_number") - for x in np.char.title(array.element) - ]) + def att_atomic_number() -> NDArray[np.int32]: + atomic_number = np.array( + [data.elements.get(x, {"atomic_number": -1}).get("atomic_number") for x in np.char.title(array.element)] + ) return atomic_number - def att_atom_id(): + def att_atom_id() -> NDArray[np.int32]: return array.atom_id - def att_res_id(): + def att_res_id() -> NDArray[np.int32]: return array.res_id - def att_res_name(): + def att_res_name() -> NDArray[np.int32]: other_res = [] counter = 0 id_counter = -1 @@ -432,8 +423,7 @@ def att_res_name(): res_nums = [] for name in res_names: - res_num = data.residues.get( - name, {'res_name_num': -1}).get('res_name_num') + res_num = data.residues.get(name, {"res_name_num": -1}).get("res_name_num") if res_num == 9999: if res_names[counter - 1] != name or res_ids[counter] != res_ids[counter - 1]: @@ -442,17 +432,16 @@ def att_res_name(): unique_res_name = str(id_counter + 100) + "_" + str(name) other_res.append(unique_res_name) - num = np.where(np.isin(np.unique(other_res), unique_res_name))[ - 0][0] + 100 + num = np.where(np.isin(np.unique(other_res), unique_res_name))[0][0] + 100 res_nums.append(num) else: res_nums.append(res_num) counter += 1 - mol['ligands'] = np.unique(other_res) + mol["ligands"] = np.unique(other_res) return np.array(res_nums) - def att_chain_id(): + def att_chain_id() -> NDArray[np.int32]: return np.unique(array.chain_id, return_inverse=True)[1] def att_entity_id(): @@ -464,138 +453,250 @@ def att_b_factor(): def att_occupancy(): return array.occupancy - def att_vdw_radii(): - vdw_radii = np.array(list(map( - # divide by 100 to convert from picometres to angstroms which is - # what all of coordinates are in - lambda x: data.elements.get( - x, {}).get('vdw_radii', 100.) / 100, - np.char.title(array.element) - ))) + def att_vdw_radii() -> NDArray[np.float64]: + vdw_radii = np.array( + list( + map( + # divide by 100 to convert from picometres to angstroms which is + # what all of coordinates are in + lambda x: data.elements.get(x, {}).get("vdw_radii", 100.0) / 100, + np.char.title(array.element), + ) + ) + ) return vdw_radii * world_scale - def att_mass(): + def att_mass() -> NDArray[np.float64]: return array.mass - def att_atom_name(): - atom_name = np.array(list(map( - lambda x: data.atom_names.get(x, -1), - array.atom_name - ))) + def att_atom_name() -> NDArray[np.int32]: + atom_name = np.array([data.atom_names.get(x, -1) for x in array.atom_name]) return atom_name - def att_lipophobicity(): - lipo = np.array(list(map( - lambda x, y: data.lipophobicity.get(x, {"0": 0}).get(y, 0), - array.res_name, array.atom_name - ))) + def att_lipophobicity() -> NDArray[np.float64]: + lipo = np.array( + [ + data.lipophobicity.get(res_name, {"0": 0}).get(atom_name, 0) + for (res_name, atom_name) in zip( + array.res_name, + array.atom_name, + ) + ] + ) return lipo - def att_charge(): - charge = np.array(list(map( - lambda x, y: data.atom_charge.get(x, {"0": 0}).get(y, 0), - array.res_name, array.atom_name - ))) + def att_charge() -> NDArray[np.float64]: + charge = np.array( + [ + data.atom_charge.get(res_name, {"0": 0}).get(atom_name, 0) + for (res_name, atom_name) in zip( + array.res_name, + array.atom_name, + ) + ] + ) return charge - def att_color(): + def att_color() -> NDArray[np.float64]: return color.color_chains(att_atomic_number(), att_chain_id()) - def att_is_alpha(): - return np.isin(array.atom_name, 'CA') + def att_is_alpha() -> NDArray[np.bool_]: + return np.isin(array.atom_name, "CA") - def att_is_solvent(): + def att_is_solvent() -> NDArray[np.bool_]: return struc.filter_solvent(array) - def att_is_backbone(): + def att_is_backbone() -> NDArray[np.bool_]: """ Get the atoms that appear in peptide backbone or nucleic acid phosphate backbones. Filter differs from the Biotite's `struc.filter_peptide_backbone()` in that this - includes the peptide backbone oxygen atom, which biotite excludes. Additionally - this selection also includes all of the atoms from the ribose in nucleic acids, + includes the peptide backbone oxygen atom, which biotite excludes. Additionally + this selection also includes all of the atoms from the ribose in nucleic acids, and the other phosphate oxygens. """ backbone_atom_names = [ - 'N', 'C', 'CA', 'O', # peptide backbone atoms - "P", "O5'", "C5'", "C4'", "C3'", "O3'", # 'continuous' nucleic backbone atoms - "O1P", "OP1", "O2P", "OP2", # alternative names for phosphate O's - "O4'", "C1'", "C2'", "O2'" # remaining ribose atoms + "N", + "C", + "CA", + "O", # peptide backbone atoms + "P", + "O5'", + "C5'", + "C4'", + "C3'", + "O3'", # 'continuous' nucleic backbone atoms + "O1P", + "OP1", + "O2P", + "OP2", # alternative names for phosphate O's + "O4'", + "C1'", + "C2'", + "O2'", # remaining ribose atoms ] is_backbone = np.logical_and( np.isin(array.atom_name, backbone_atom_names), - np.logical_not(struc.filter_solvent(array)) + np.logical_not(struc.filter_solvent(array)), ) return is_backbone - def att_is_nucleic(): + def att_is_nucleic() -> NDArray[np.bool_]: return struc.filter_nucleotides(array) - def att_is_peptide(): + def att_is_peptide() -> NDArray[np.bool_]: aa = struc.filter_amino_acids(array) con_aa = struc.filter_canonical_amino_acids(array) return aa | con_aa - def att_is_hetero(): + def att_is_hetero() -> NDArray[np.bool_]: return array.hetero - def att_is_carb(): + def att_is_carb() -> NDArray[np.bool_]: return struc.filter_carbohydrates(array) - def att_sec_struct(): + def att_sec_struct() -> NDArray[np.int32]: return array.sec_struct # these are all of the attributes that will be added to the structure # TODO add capcity for selection of particular attributes to include / not include to potentially # boost performance, unsure if actually a good idea of not. Need to do some testing. attributes = ( - {'name': 'res_id', 'value': att_res_id, - 'type': 'INT', 'domain': 'POINT'}, - {'name': 'res_name', 'value': att_res_name, - 'type': 'INT', 'domain': 'POINT'}, - {'name': 'atomic_number', 'value': att_atomic_number, - 'type': 'INT', 'domain': 'POINT'}, - {'name': 'b_factor', 'value': att_b_factor, - 'type': 'FLOAT', 'domain': 'POINT'}, - {'name': 'occupancy', 'value': att_occupancy, - 'type': 'FLOAT', 'domain': 'POINT'}, - {'name': 'vdw_radii', 'value': att_vdw_radii, - 'type': 'FLOAT', 'domain': 'POINT'}, - {'name': 'mass', 'value': att_mass, - 'type': 'FLOAT', 'domain': 'POINT'}, - {'name': 'chain_id', 'value': att_chain_id, - 'type': 'INT', 'domain': 'POINT'}, - {'name': 'entity_id', 'value': att_entity_id, - 'type': 'INT', 'domain': 'POINT'}, - {'name': 'atom_id', 'value': att_atom_id, - 'type': 'INT', 'domain': 'POINT'}, - {'name': 'atom_name', 'value': att_atom_name, - 'type': 'INT', 'domain': 'POINT'}, - {'name': 'lipophobicity', 'value': att_lipophobicity, - 'type': 'FLOAT', 'domain': 'POINT'}, - {'name': 'charge', 'value': att_charge, - 'type': 'FLOAT', 'domain': 'POINT'}, - {'name': 'Color', 'value': att_color, - 'type': 'FLOAT_COLOR', 'domain': 'POINT'}, - {'name': 'is_backbone', 'value': att_is_backbone, - 'type': 'BOOLEAN', 'domain': 'POINT'}, - {'name': 'is_alpha_carbon', 'value': att_is_alpha, - 'type': 'BOOLEAN', 'domain': 'POINT'}, - {'name': 'is_solvent', 'value': att_is_solvent, - 'type': 'BOOLEAN', 'domain': 'POINT'}, - {'name': 'is_nucleic', 'value': att_is_nucleic, - 'type': 'BOOLEAN', 'domain': 'POINT'}, - {'name': 'is_peptide', 'value': att_is_peptide, - 'type': 'BOOLEAN', 'domain': 'POINT'}, - {'name': 'is_hetero', 'value': att_is_hetero, - 'type': 'BOOLEAN', 'domain': 'POINT'}, - {'name': 'is_carb', 'value': att_is_carb, - 'type': 'BOOLEAN', 'domain': 'POINT'}, - {'name': 'sec_struct', 'value': att_sec_struct, - 'type': 'INT', 'domain': 'POINT'} + { + "name": "res_id", + "value": att_res_id, + "type": "INT", + "domain": "POINT", + }, + { + "name": "res_name", + "value": att_res_name, + "type": "INT", + "domain": "POINT", + }, + { + "name": "atomic_number", + "value": att_atomic_number, + "type": "INT", + "domain": "POINT", + }, + { + "name": "b_factor", + "value": att_b_factor, + "type": "FLOAT", + "domain": "POINT", + }, + { + "name": "occupancy", + "value": att_occupancy, + "type": "FLOAT", + "domain": "POINT", + }, + { + "name": "vdw_radii", + "value": att_vdw_radii, + "type": "FLOAT", + "domain": "POINT", + }, + { + "name": "mass", + "value": att_mass, + "type": "FLOAT", + "domain": "POINT", + }, + { + "name": "chain_id", + "value": att_chain_id, + "type": "INT", + "domain": "POINT", + }, + { + "name": "entity_id", + "value": att_entity_id, + "type": "INT", + "domain": "POINT", + }, + { + "name": "atom_id", + "value": att_atom_id, + "type": "INT", + "domain": "POINT", + }, + { + "name": "atom_name", + "value": att_atom_name, + "type": "INT", + "domain": "POINT", + }, + { + "name": "lipophobicity", + "value": att_lipophobicity, + "type": "FLOAT", + "domain": "POINT", + }, + { + "name": "charge", + "value": att_charge, + "type": "FLOAT", + "domain": "POINT", + }, + { + "name": "Color", + "value": att_color, + "type": "FLOAT_COLOR", + "domain": "POINT", + }, + { + "name": "is_backbone", + "value": att_is_backbone, + "type": "BOOLEAN", + "domain": "POINT", + }, + { + "name": "is_alpha_carbon", + "value": att_is_alpha, + "type": "BOOLEAN", + "domain": "POINT", + }, + { + "name": "is_solvent", + "value": att_is_solvent, + "type": "BOOLEAN", + "domain": "POINT", + }, + { + "name": "is_nucleic", + "value": att_is_nucleic, + "type": "BOOLEAN", + "domain": "POINT", + }, + { + "name": "is_peptide", + "value": att_is_peptide, + "type": "BOOLEAN", + "domain": "POINT", + }, + { + "name": "is_hetero", + "value": att_is_hetero, + "type": "BOOLEAN", + "domain": "POINT", + }, + { + "name": "is_carb", + "value": att_is_carb, + "type": "BOOLEAN", + "domain": "POINT", + }, + { + "name": "sec_struct", + "value": att_sec_struct, + "type": "INT", + "domain": "POINT", + }, ) # assign the attributes to the object @@ -603,26 +704,28 @@ def att_sec_struct(): if verbose: start = time.process_time() try: - bl.obj.set_attribute(mol, name=att['name'], data=att['value']( - ), type=att['type'], domain=att['domain']) + bl.obj.set_attribute( + mol, + name=att["name"], + data=att["value"](), + type=att["type"], + domain=att["domain"], + ) if verbose: - print( - f'Added {att["name"]} after {time.process_time() - start} s') - except: + print(f'Added {att["name"]} after {time.process_time() - start} s') + except AttributeError: if verbose: warnings.warn(f"Unable to add attribute: {att['name']}") - print( - f'Failed adding {att["name"]} after {time.process_time() - start} s') + print(f'Failed adding {att["name"]} after {time.process_time() - start} s') coll_frames = None if frames: - coll_frames = bl.coll.frames(mol.name, parent=bl.coll.data()) for i, frame in enumerate(frames): frame = bl.obj.create_object( - name=mol.name + '_frame_' + str(i), + name=mol.name + "_frame_" + str(i), collection=coll_frames, - vertices=frame.coord * world_scale + vertices=frame.coord * world_scale, # vertices=frame.coord * world_scale - centroid ) # TODO if update_attribute @@ -634,9 +737,9 @@ def att_sec_struct(): # add custom properties to the actual blender object, such as number of chains, biological assemblies etc # currently biological assemblies can be problematic to holding off on doing that try: - mol['chain_ids'] = list(np.unique(array.chain_id)) + mol["chain_ids"] = list(np.unique(array.chain_id)) except AttributeError: - mol['chain_ids'] = None - warnings.warn('No chain information detected.') + mol["chain_ids"] = None + warnings.warn("No chain information detected.") return mol, coll_frames diff --git a/molecularnodes/io/parse/mrc.py b/molecularnodes/io/parse/mrc.py index aae97b42..d1eaa149 100644 --- a/molecularnodes/io/parse/mrc.py +++ b/molecularnodes/io/parse/mrc.py @@ -1,35 +1,31 @@ -from .density import Density - -from ...blender import coll, obj, nodes +import os +import mrcfile +import pyopenvdb as vdb import bpy import numpy as np -import os +from typing import Union +from pathlib import Path +from ...blender import coll, nodes, obj +from .density import Density class MRC(Density): """ A class for parsing EM density files in the format `.map` or `.map.gz`. - It utilises `mrcfile` for file parsing, which is then converted into `pyopevdb` grids, + It utilises `mrcfile` for file parsing, which is then converted into `pyopevdb` grids, that can be written as `.vdb` files and the imported into Blender as volumetric objects. """ - def __init__(self, file_path, center=False, invert=False, overwrite=False): - super().__init__(self) - self.file_path = file_path + def __init__( + self, file_path: Union[str, Path], center: bool = False, invert: bool = False, overwrite: bool = False + ) -> None: + super().__init__(file_path=file_path) self.grid = self.map_to_grid(self.file_path, center=center) - self.file_vdb = self.map_to_vdb( - self.file_path, - center=center, - invert=invert, - overwrite=overwrite - ) + self.file_vdb = self.map_to_vdb(self.file_path, center=center, invert=invert, overwrite=overwrite) def create_model( - self, - name='NewDensity', - style='density_surface', - setup_nodes=True + self, name: str = "NewDensity", style: str = "density_surface", setup_nodes: bool = True ) -> bpy.types.Object: """ Loads an MRC file into Blender as a volumetric object. @@ -51,29 +47,25 @@ def create_model( object = obj.import_vdb(self.file_vdb, collection=coll.mn()) object.location = (0, 0, 0) self.object = object - object.mn['molecule_type'] = 'density' + object.mn["molecule_type"] = "density" if name and name != "": # Rename object to specified name object.name = name if setup_nodes: - nodes.create_starting_nodes_density( - object=object, - style=style, - threshold=self.threshold - ) + nodes.create_starting_nodes_density(object=object, style=style, threshold=self.threshold) return object def map_to_vdb( self, - file: str, + file: Union[str, Path], invert: bool = False, - world_scale=0.01, + world_scale: float = 0.01, center: bool = False, - overwrite=False - ) -> (str, float): + overwrite: bool = False, + ) -> Path: """ Converts an MRC file to a .vdb file using pyopenvdb. @@ -104,25 +96,24 @@ def map_to_vdb( if os.path.exists(file_path) and not overwrite: # Also check that the file has the same invert and center settings grid = vdb.readAllGridMetadata(file_path)[0] - if 'MN_invert' in grid and grid['MN_invert'] == invert and 'MN_center' in grid and grid['MN_center'] == center: - self.threshold = grid['MN_initial_threshold'] + if ( + "MN_invert" in grid + and grid["MN_invert"] == invert + and "MN_center" in grid + and grid["MN_center"] == center + ): + self.threshold = grid["MN_initial_threshold"] return file_path print("Reading new file") # Read in the MRC file and convert it to a pyopenvdb grid - grid = self.map_to_grid( - file=file, - invert=invert, - center=center - ) + grid = self.map_to_grid(file=file, invert=invert, center=center) - grid.transform.scale( - np.array((1, 1, 1)) * world_scale * grid['MN_voxel_size'] - ) + grid.transform.scale(np.array((1, 1, 1)) * world_scale * grid["MN_voxel_size"]) if center: - offset = -np.array(grid['MN_box_size']) * 0.5 - offset *= grid['MN_voxel_size'] * world_scale + offset = -np.array(grid["MN_box_size"]) * 0.5 + offset *= grid["MN_voxel_size"] * world_scale print("transforming") grid.transform.translate(offset) @@ -130,15 +121,15 @@ def map_to_vdb( os.remove(file_path) # Write the grid to a .vdb file - print('writing new file') + print("writing new file") vdb.write(file_path, grids=[grid]) - self.threshold = grid['MN_initial_threshold'] + self.threshold = grid["MN_initial_threshold"] del grid # Return the path to the output file return file_path - def map_to_grid(self, file: str, invert: bool = False, center: bool = False): + def map_to_grid(self, file: Union[str, Path], invert: bool = False, center: bool = False) -> vdb.GridBase: """ Reads an MRC file and converts it into a pyopenvdb FloatGrid object. @@ -158,8 +149,6 @@ def map_to_grid(self, file: str, invert: bool = False, center: bool = False): pyopenvdb.FloatGrid A pyopenvdb FloatGrid object containing the density data. """ - import mrcfile - import pyopenvdb as vdb volume = mrcfile.read(file) @@ -170,7 +159,7 @@ def map_to_grid(self, file: str, invert: bool = False, center: bool = False): if dataType == "float32" or dataType == "float64": grid = vdb.FloatGrid() elif dataType == "int8" or dataType == "int16" or dataType == "int32": - volume = volume.astype('int32') + volume = volume.astype("int32") grid = vdb.Int32Grid() elif dataType == "int64": grid = vdb.Int64Grid() @@ -185,24 +174,26 @@ def map_to_grid(self, file: str, invert: bool = False, center: bool = False): # The np.copy is needed to force numpy to actually rewrite the data in memory # since openvdb seems to read is straight from memory without checking the striding # The np.transpose is needed to convert the data from zyx to xyz - volume = np.copy(np.transpose(volume, (2, 1, 0)), order='C') + volume = np.copy(np.transpose(volume, (2, 1, 0)), order="C") try: grid.copyFromArray(volume.astype(float)) except Exception as e: - print( - f"Grid data type '{volume.dtype}' is an unsupported type.\nError: {e}") + print(f"Grid data type '{volume.dtype}' is an unsupported type.\nError: {e}") grid.gridClass = vdb.GridClass.FOG_VOLUME - grid.name = 'density' + grid.name = "density" # Set some metadata for the vdb file, so we can check if it's already been converted # correctly - grid['MN_invert'] = invert - grid['MN_initial_threshold'] = initial_threshold - grid['MN_center'] = center + grid["MN_invert"] = invert + grid["MN_initial_threshold"] = initial_threshold + grid["MN_center"] = center with mrcfile.open(file) as mrc: - grid['MN_voxel_size'] = float(mrc.voxel_size.x) - grid['MN_box_size'] = (int(mrc.header.nx), int( - mrc.header.ny), int(mrc.header.nz)) + grid["MN_voxel_size"] = float(mrc.voxel_size.x) + grid["MN_box_size"] = ( + int(mrc.header.nx), + int(mrc.header.ny), + int(mrc.header.nz), + ) return grid diff --git a/molecularnodes/io/parse/pdb.py b/molecularnodes/io/parse/pdb.py index bf0638c7..104c8d69 100644 --- a/molecularnodes/io/parse/pdb.py +++ b/molecularnodes/io/parse/pdb.py @@ -1,12 +1,15 @@ import numpy as np +from typing import Union, AnyStr +from pathlib import Path + from .assembly import AssemblyParser from .molecule import Molecule class PDB(Molecule): - def __init__(self, file_path): - super().__init__() + def __init__(self, file_path: Union[Path, AnyStr]): + super().__init__(file_path=file_path) self.file_path = file_path self.file = self.read() self.array = self._get_structure() @@ -14,16 +17,18 @@ def __init__(self, file_path): def read(self): from biotite.structure.io import pdb + return pdb.PDBFile.read(self.file_path) def _get_structure(self): from biotite.structure.io import pdb from biotite.structure import BadStructureError + # TODO: implement entity ID, sec_struct for PDB files array = pdb.get_structure( pdb_file=self.file, - extra_fields=['b_factor', 'occupancy', 'charge', 'atom_id'], - include_bonds=True + extra_fields=["b_factor", "occupancy", "charge", "atom_id"], + include_bonds=True, ) try: @@ -31,9 +36,7 @@ def _get_structure(self): except BadStructureError: sec_struct = _comp_secondary_structure(array[0]) - array.set_annotation( - 'sec_struct', sec_struct - ) + array.set_annotation("sec_struct", sec_struct) return array @@ -45,22 +48,17 @@ def _get_sec_struct(file, array): import biotite.structure as struc lines = np.array(file.lines) - lines_helix = lines[np.char.startswith(lines, 'HELIX')] - lines_sheet = lines[np.char.startswith(lines, 'SHEET')] - if (len(lines_helix) == 0 and len(lines_sheet) == 0): - raise struc.BadStructureError( - 'No secondary structure information detected.' - ) + lines_helix = lines[np.char.startswith(lines, "HELIX")] + lines_sheet = lines[np.char.startswith(lines, "SHEET")] + if len(lines_helix) == 0 and len(lines_sheet) == 0: + raise struc.BadStructureError("No secondary structure information detected.") sec_struct = np.zeros(array.array_length(), int) helix_values = (22, 25, 34, 37, 20) sheet_values = (23, 26, 34, 37, 22) - values = ( - (lines_helix, 1, helix_values), - (lines_sheet, 2, sheet_values) - ) + values = ((lines_helix, 1, helix_values), (lines_sheet, 2, sheet_values)) def _get_mask(line, start1, end1, start2, end2, chainid): """ @@ -80,9 +78,8 @@ def _get_mask(line, start1, end1, start2, end2, chainid): # create a mask for the array based on these values mask = np.logical_and( - np.logical_and(array.chain_id == chain_id, - array.res_id >= start_num), - array.res_id <= end_num + np.logical_and(array.chain_id == chain_id, array.res_id >= start_num), + array.res_id <= end_num, ) return mask @@ -94,10 +91,7 @@ def _get_mask(line, start1, end1, start2, end2, chainid): # assign remaining AA atoms to 3 (loop), while all other remaining # atoms will be 0 (not relevant) - mask = np.logical_and( - sec_struct == 0, - struc.filter_canonical_amino_acids(array) - ) + mask = np.logical_and(sec_struct == 0, struc.filter_canonical_amino_acids(array)) sec_struct[mask] = 3 @@ -119,11 +113,10 @@ def _comp_secondary_structure(array): # TODO Port [PyDSSP](https://github.com/ShintaroMinami/PyDSSP) from biotite.structure import annotate_sse, spread_residue_wise - conv_sse_char_int = {'a': 1, 'b': 2, 'c': 3, '': 0} + conv_sse_char_int = {"a": 1, "b": 2, "c": 3, "": 0} char_sse = annotate_sse(array) - int_sse = np.array([conv_sse_char_int[char] - for char in char_sse], dtype=int) + int_sse = np.array([conv_sse_char_int[char] for char in char_sse], dtype=int) atom_sse = spread_residue_wise(array, int_sse) return atom_sse @@ -140,12 +133,11 @@ def list_assemblies(self): def get_transformations(self, assembly_id): import biotite + # Get lines containing transformations for assemblies remark_lines = self._file.get_remark(350) if remark_lines is None: - raise biotite.InvalidFileError( - "File does not contain assembly information (REMARK 350)" - ) + raise biotite.InvalidFileError("File does not contain assembly information (REMARK 350)") # Get lines corresponding to selected assembly ID assembly_start_i = None assembly_stop_i = None @@ -163,32 +155,27 @@ def get_transformations(self, assembly_id): # the 'stop' is the end of REMARK 350 lines assembly_stop_i = len(remark_lines) if assembly_stop_i is None else i if assembly_start_i is None: - raise KeyError( - f"The assembly ID '{assembly_id}' is not found" - ) - assembly_lines = remark_lines[assembly_start_i: assembly_stop_i] + raise KeyError(f"The assembly ID '{assembly_id}' is not found") + assembly_lines = remark_lines[assembly_start_i:assembly_stop_i] # Get transformations for a sets of chains transformations = [] chain_set_start_indices = [ - i for i, line in enumerate(assembly_lines) - if line.startswith("APPLY THE FOLLOWING TO CHAINS") + i for i, line in enumerate(assembly_lines) if line.startswith("APPLY THE FOLLOWING TO CHAINS") ] # Add exclusive stop at end of records chain_set_start_indices.append(len(assembly_lines)) for i in range(len(chain_set_start_indices) - 1): start = chain_set_start_indices[i] - stop = chain_set_start_indices[i+1] + stop = chain_set_start_indices[i + 1] # Read affected chain IDs from the following line(s) affected_chain_ids = [] transform_start = None - for j, line in enumerate(assembly_lines[start: stop]): - if line.startswith("APPLY THE FOLLOWING TO CHAINS:") or \ - line.startswith(" AND CHAINS:"): - affected_chain_ids += [ - chain_id.strip() - for chain_id in line[30:].split(",") - ] + for j, line in enumerate(assembly_lines[start:stop]): + if line.startswith("APPLY THE FOLLOWING TO CHAINS:") or line.startswith( + " AND CHAINS:" + ): + affected_chain_ids += [chain_id.strip() for chain_id in line[30:].split(",")] else: # Chain specification has finished # BIOMT lines start directly after chain specification @@ -196,12 +183,9 @@ def get_transformations(self, assembly_id): break # Parse transformations from BIOMT lines if transform_start is None: - raise biotite.InvalidFileError( - "No 'BIOMT' records found for chosen assembly" - ) + raise biotite.InvalidFileError("No 'BIOMT' records found for chosen assembly") - matrices = _parse_transformations( - assembly_lines[transform_start: stop]) + matrices = _parse_transformations(assembly_lines[transform_start:stop]) for matrix in matrices: transformations.append((affected_chain_ids, matrix.tolist())) @@ -223,10 +207,10 @@ def _parse_transformations(lines): Return as array of matrices and vectors respectively """ import biotite + # Each transformation requires 3 lines for the (x,y,z) components if len(lines) % 3 != 0: - raise biotite.InvalidFileError( - "Invalid number of transformation vectors") + raise biotite.InvalidFileError("Invalid number of transformation vectors") n_transformations = len(lines) // 3 matrices = np.tile(np.identity(4), (n_transformations, 1, 1)) @@ -239,9 +223,7 @@ def _parse_transformations(lines): transformations = [float(e) for e in line.split()[2:]] if len(transformations) != 4: - raise biotite.InvalidFileError( - "Invalid number of transformation vector elements" - ) + raise biotite.InvalidFileError("Invalid number of transformation vector elements") matrices[transformation_i, component_i, :] = transformations component_i += 1 diff --git a/molecularnodes/io/parse/pdbx.py b/molecularnodes/io/parse/pdbx.py index 49990d73..f722e600 100644 --- a/molecularnodes/io/parse/pdbx.py +++ b/molecularnodes/io/parse/pdbx.py @@ -12,10 +12,10 @@ def __init__(self, file_path): @property def entity_ids(self): - return self.file.block.get('entity').get('pdbx_description').as_array().tolist() + return self.file.block.get("entity").get("pdbx_description").as_array().tolist() def _get_entity_id(self, array, file): - chain_ids = file.block['entity_poly']['pdbx_strand_id'].as_array() + chain_ids = file.block["entity_poly"]["pdbx_strand_id"].as_array() # the chain_ids are an array of individual items np.array(['A,B', 'C', 'D,E,F']) # which need to be categorised as [1, 1, 2, 3, 3, 3] for their belonging to individual @@ -24,104 +24,56 @@ def _get_entity_id(self, array, file): chains = [] idx = [] for i, chain_str in enumerate(chain_ids): - for chain in chain_str.split(','): + for chain in chain_str.split(","): chains.append(chain) idx.append(i) entity_lookup = dict(zip(chains, idx)) - chain_id_int = np.array([entity_lookup.get(chain, -1) - for chain in array.chain_id], int) + chain_id_int = np.array([entity_lookup.get(chain, -1) for chain in array.chain_id], int) return chain_id_int - def get_structure(self, extra_fields=['b_factor', 'occupancy', 'atom_id'], bonds=True): + def get_structure(self, extra_fields=["b_factor", "occupancy", "atom_id"], bonds=True): import biotite.structure.io.pdbx as pdbx import biotite.structure as struc array = pdbx.get_structure(self.file, extra_fields=extra_fields) try: array.set_annotation( - 'sec_struct', self._get_secondary_structure( - array=array, file=self.file) + "sec_struct", + self._get_secondary_structure(array=array, file=self.file), ) except KeyError: - warnings.warn('No secondary structure information.') + warnings.warn("No secondary structure information.") try: - array.set_annotation( - 'entity_id', self._get_entity_id(array, self.file) - ) + array.set_annotation("entity_id", self._get_entity_id(array, self.file)) except KeyError: - warnings.warn('No entity ID information') + warnings.warn("No entity ID information") if not array.bonds and bonds: - array.bonds = struc.bonds.connect_via_residue_names( - array, inter_residue=True) + array.bonds = struc.bonds.connect_via_residue_names(array, inter_residue=True) return array def _assemblies(self): return CIFAssemblyParser(self.file).get_assemblies() - # # in the cif / BCIF file 3x4 transformation matrices are stored in individual - # # columns, this extracts them and returns them with additional row for scaling, - # # meaning an (n, 4, 4) array is returned, where n is the number of transformations - # # and each is a 4x4 transformaiton matrix - # cat_matrix = self.file.block['pdbx_struct_oper_list'] - # matrices = self._extract_matrices(cat_matrix) - - # # sometimes there will be missing opers / matrices. For example in the - # # 'square.bcif' file, the matrix IDs go all the way up to 18024, but only - # # 18023 matrices are defined. That is becuase matrix 12 is never referenced, so - # # isn't included in teh file. To get around this we have to just get the specific - # # IDs that are defined for the matrices and use that to lookup the correct index - # # in the matrices array. - # mat_ids = cat_matrix.get('id').as_array(int) - # mat_lookup = dict(zip(mat_ids, range(len(mat_ids)))) - - # category = self.file.block['pdbx_struct_assembly_gen'] - # ids = category['assembly_id'].as_array(int) - # opers = category['oper_expression'].as_array(str) - # asyms = category['asym_id_list'].as_array() - - # # constructs a dictionary of - # # { - # # '1': ((['A', 'B', C'], [4x4 matrix]), (['A', 'B'], [4x4 matrix])), - # # '2': ((['A', 'B', C'], [4x4 matrix])) - # # } - # # where each entry in the dictionary is a biological assembly, and each dictionary - # # value contains a list of tranasformations which need to be applied. Each entry in - # # the list of transformations is - # # ([chains to be affected], [4x4 transformation matrix]) - # assembly_dic = {} - # for idx, oper, asym in zip(ids, opers, asyms): - # trans = list() - # asym = asym.split(',') - # for op in _parse_opers(oper): - # i = int(op) - # trans.append((asym, matrices[mat_lookup[i]].tolist())) - # assembly_dic[str(idx)] = trans - - # return assembly_dic - def _extract_matrices(self, category): matrix_columns = [ - 'matrix[1][1]', - 'matrix[1][2]', - 'matrix[1][3]', - 'vector[1]', - 'matrix[2][1]', - 'matrix[2][2]', - 'matrix[2][3]', - 'vector[2]', - 'matrix[3][1]', - 'matrix[3][2]', - 'matrix[3][3]', - 'vector[3]' + "matrix[1][1]", + "matrix[1][2]", + "matrix[1][3]", + "vector[1]", + "matrix[2][1]", + "matrix[2][2]", + "matrix[2][3]", + "vector[2]", + "matrix[3][1]", + "matrix[3][2]", + "matrix[3][3]", + "vector[3]", ] - columns = [ - category[name].as_array().astype(float) for - name in matrix_columns - ] + columns = [category[name].as_array().astype(float) for name in matrix_columns] matrices = np.empty((len(columns[0]), 4, 4), float) col_mask = np.tile((0, 1, 2, 3), 3) @@ -163,12 +115,12 @@ def _get_secondary_structure(self, file, array): # alpha helices, but will sometimes contain also other secondary structure # information such as in AlphaFold predictions - conf = file.block.get('struct_conf') + conf = file.block.get("struct_conf") if conf: - starts = conf['beg_auth_seq_id'].as_array().astype(int) - ends = conf['end_auth_seq_id'].as_array().astype(int) - chains = conf['end_auth_asym_id'].as_array().astype(str) - id_label = conf['id'].as_array().astype(str) + starts = conf["beg_auth_seq_id"].as_array().astype(int) + ends = conf["end_auth_seq_id"].as_array().astype(int) + chains = conf["end_auth_asym_id"].as_array().astype(str) + id_label = conf["id"].as_array().astype(str) else: starts = np.empty(0, dtype=int) ends = np.empty(0, dtype=int) @@ -178,15 +130,12 @@ def _get_secondary_structure(self, file, array): # most files will have a separate category for the beta sheets # this can just be appended to the other start / end / id and be processed # as normalquit - sheet = file.block.get('struct_sheet_range') + sheet = file.block.get("struct_sheet_range") if sheet: - starts = np.append( - starts, sheet['beg_auth_seq_id'].as_array().astype(int)) - ends = np.append( - ends, sheet['end_auth_seq_id'].as_array().astype(int)) - chains = np.append( - chains, sheet['end_auth_asym_id'].as_array().astype(str)) - id_label = np.append(id_label, np.repeat('STRN', len(sheet['id']))) + starts = np.append(starts, sheet["beg_auth_seq_id"].as_array().astype(int)) + ends = np.append(ends, sheet["end_auth_seq_id"].as_array().astype(int)) + chains = np.append(chains, sheet["end_auth_asym_id"].as_array().astype(str)) + id_label = np.append(id_label, np.repeat("STRN", len(sheet["id"]))) if not conf and not sheet: raise KeyError @@ -202,12 +151,12 @@ def _get_secondary_structure(self, file, array): lookup = dict() for chain in np.unique(chains): arrays = [] - mask = (chain == chains) + mask = chain == chains start_sub = starts[mask] end_sub = ends[mask] id_sub = id_int[mask] - for (start, end, id) in zip(start_sub, end_sub, id_sub): + for start, end, id in zip(start_sub, end_sub, id_sub): idx = np.arange(start, end + 1, dtype=int) arr = np.zeros((len(idx), 2), dtype=int) arr[:, 0] = idx @@ -230,31 +179,10 @@ def _get_secondary_structure(self, file, array): return secondary_structure -def _parse_opers(oper): - # we want the example '1,3,(5-8)' to expand to (1, 3, 5, 6, 7, 8). - op_ids = list() - - for group in oper.strip(')').split('('): - if "," in group: - for i in group.split(','): - op_ids.append() - - for group in oper.split(","): - if "-" not in group: - op_ids.append(str(group)) - continue - - start, stop = [int(x) for x in group.strip("()").split('-')] - for i in range(start, stop + 1): - op_ids.append(str(i)) - - return op_ids - - -def _ss_label_to_int(label): - if 'HELX' in label: +def _ss_label_to_int(label: str) -> int: + if "HELX" in label: return 1 - elif 'STRN' in label: + elif "STRN" in label: return 2 else: return 3 @@ -269,6 +197,7 @@ def __init__(self, file_path): def _read(self, file_path): import biotite.structure.io.pdbx as pdbx + return pdbx.CIFFile.read(file_path) @@ -281,6 +210,7 @@ def __init__(self, file_path): def _read(self, file_path): import biotite.structure.io.pdbx as pdbx + return pdbx.BinaryCIFFile.read(file_path) @@ -292,6 +222,7 @@ def __init__(self, file_cif): def list_assemblies(self): import biotite.structure.io.pdbx as pdbx + return list(pdbx.list_assemblies(self._file).keys()) def get_transformations(self, assembly_id): @@ -339,24 +270,21 @@ def get_assemblies(self): def _extract_matrices(category, scale=True): matrix_columns = [ - 'matrix[1][1]', - 'matrix[1][2]', - 'matrix[1][3]', - 'vector[1]', - 'matrix[2][1]', - 'matrix[2][2]', - 'matrix[2][3]', - 'vector[2]', - 'matrix[3][1]', - 'matrix[3][2]', - 'matrix[3][3]', - 'vector[3]' + "matrix[1][1]", + "matrix[1][2]", + "matrix[1][3]", + "vector[1]", + "matrix[2][1]", + "matrix[2][2]", + "matrix[2][3]", + "vector[2]", + "matrix[3][1]", + "matrix[3][2]", + "matrix[3][3]", + "vector[3]", ] - columns = [ - category[name].as_array().astype(float) for - name in matrix_columns - ] + columns = [category[name].as_array().astype(float) for name in matrix_columns] n = 4 if scale else 3 matrices = np.empty((len(columns[0]), n, 4), float) @@ -365,7 +293,7 @@ def _extract_matrices(category, scale=True): for column, coli, rowi in zip(columns, col_mask, row_mask): matrices[:, rowi, coli] = column - return dict(zip(category['id'].as_array(str), matrices)) + return dict(zip(category["id"].as_array(str), matrices)) def _chain_transformations(rotations, translations): @@ -400,17 +328,9 @@ def _get_transformations(struct_oper): transformation_dict = {} for index, id in enumerate(struct_oper["id"].as_array()): rotation_matrix = np.array( - [ - [ - float(struct_oper[f"matrix[{i}][{j}]"][index]) - for j in (1, 2, 3) - ] - for i in (1, 2, 3) - ] - ) - translation_vector = np.array( - [float(struct_oper[f"vector[{i}]"][index]) for i in (1, 2, 3)] + [[float(struct_oper[f"matrix[{i}][{j}]"][index]) for j in (1, 2, 3)] for i in (1, 2, 3)] ) + translation_vector = np.array([float(struct_oper[f"vector[{i}]"][index]) for i in (1, 2, 3)]) transformation_dict[id] = (rotation_matrix, translation_vector) return transformation_dict @@ -436,18 +356,13 @@ def _parse_operation_expression(expression): for gexpr in expr.split(","): if "-" in gexpr: first, last = gexpr.split("-") - operations.append( - [str(id) - for id in range(int(first), int(last) + 1)] - ) + operations.append([str(id) for id in range(int(first), int(last) + 1)]) else: operations.append([gexpr]) else: # Range of operation IDs, they must be integers first, last = expr.split("-") - operations.append( - [str(id) for id in range(int(first), int(last) + 1)] - ) + operations.append([str(id) for id in range(int(first), int(last) + 1)]) elif "," in expr: # List of operation IDs operations.append(expr.split(",")) diff --git a/molecularnodes/io/parse/star.py b/molecularnodes/io/parse/star.py index b6a273a5..89b97b86 100644 --- a/molecularnodes/io/parse/star.py +++ b/molecularnodes/io/parse/star.py @@ -1,25 +1,37 @@ import numpy as np import bpy +import starfile.typing +from bpy.app.handlers import persistent +from typing import Union, Optional, Dict +from pathlib import Path +import starfile from .ensemble import Ensemble from ... import blender as bl -@bpy.app.handlers.persistent -def _rehydrate_ensembles(scene): + +@persistent # type: ignore +def _rehydrate_ensembles(scene: bpy.types.Scene) -> None: for obj in bpy.data.objects: - if hasattr(obj, 'mn') and 'molecule_type' in obj.mn.keys(): - if obj.mn['molecule_type'] == 'star': + if hasattr(obj, "mn") and "molecule_type" in obj.mn.keys(): + if obj.mn["molecule_type"] == "star": ensemble = StarFile.from_blender_object(obj) - if not hasattr(bpy.types.Scene, 'MN_starfile_ensembles'): + if not hasattr(bpy.types.Scene, "MN_starfile_ensembles"): bpy.types.Scene.MN_starfile_ensembles = [] bpy.types.Scene.MN_starfile_ensembles.append(ensemble) + class StarFile(Ensemble): - def __init__(self, file_path): + def __init__(self, file_path: Union[str, Path]) -> None: super().__init__(file_path) - - + self.star_node: Optional[bpy.types.GeometryNode] + self.micrograph_material: Optional[bpy.types.Material] + self.data: Dict[str, starfile.typing.DataBlock] + self.current_image: int = -1 + self.n_images: int = 0 + self.star_type: Optional[str] + @classmethod - def from_starfile(cls, file_path): + def from_starfile(cls, file_path: Union[str, Path]) -> Ensemble: self = cls(file_path) self.data = self._read() self.star_type = None @@ -28,10 +40,9 @@ def from_starfile(cls, file_path): self._create_mn_columns() self.n_images = self._n_images() return self - + @classmethod - def from_blender_object(cls, blender_object): - import bpy + def from_blender_object(cls, blender_object: bpy.types.Object) -> Ensemble: self = cls(blender_object["starfile_path"]) self.object = blender_object self.star_node = bl.nodes.get_star_node(self.object) @@ -44,94 +55,99 @@ def from_blender_object(cls, blender_object): self.n_images = self._n_images() bpy.app.handlers.depsgraph_update_post.append(self._update_micrograph_texture) return self - - def _read(self): - import starfile - star = starfile.read(self.file_path) - return star + def _read(self) -> Dict[str, starfile.typing.DataBlock]: + star = starfile.read(self.file_path, always_dict=True) + return star # type: ignore - def _n_images(self): + def _n_images(self) -> int: if isinstance(self.data, dict): return len(self.data) return 1 - def _create_mn_columns(self): + def _create_mn_columns(self) -> None: # only RELION 3.1 and cisTEM STAR files are currently supported, fail gracefully - if isinstance(self.data, dict) and 'particles' in self.data and 'optics' in self.data: - self.star_type = 'relion' + if isinstance(self.data, dict) and "particles" in self.data and "optics" in self.data: + self.star_type = "relion" elif "cisTEMAnglePsi" in self.data: - self.star_type = 'cistem' + self.star_type = "cistem" else: raise ValueError( - 'File is not a valid RELION>=3.1 or cisTEM STAR file, other formats are not currently supported.' + "File is not a valid RELION>=3.1 or cisTEM STAR file, other formats are not currently supported." ) # Get absolute position and orientations - if self.star_type == 'relion': - df = self.data['particles'].merge(self.data['optics'], on='rlnOpticsGroup') + if self.star_type == "relion": + df = self.data["particles"].merge(self.data["optics"], on="rlnOpticsGroup") # get necessary info from dataframes # Standard cryoEM starfile don't have rlnCoordinateZ. If this column is not present # Set it to "0" if "rlnCoordinateZ" not in df: - df['rlnCoordinateZ'] = 0 + df["rlnCoordinateZ"] = 0 - self.positions = df[['rlnCoordinateX', 'rlnCoordinateY', - 'rlnCoordinateZ']].to_numpy() - pixel_size = df['rlnImagePixelSize'].to_numpy().reshape((-1, 1)) + self.positions = df[["rlnCoordinateX", "rlnCoordinateY", "rlnCoordinateZ"]].to_numpy() + pixel_size = df["rlnImagePixelSize"].to_numpy().reshape((-1, 1)) self.positions = self.positions * pixel_size - shift_column_names = ['rlnOriginXAngst', - 'rlnOriginYAngst', 'rlnOriginZAngst'] + shift_column_names = [ + "rlnOriginXAngst", + "rlnOriginYAngst", + "rlnOriginZAngst", + ] if all([col in df.columns for col in shift_column_names]): shifts_ang = df[shift_column_names].to_numpy() self.positions -= shifts_ang - df['MNAnglePhi'] = df['rlnAngleRot'] - df['MNAngleTheta'] = df['rlnAngleTilt'] - df['MNAnglePsi'] = df['rlnAnglePsi'] - df['MNPixelSize'] = df['rlnImagePixelSize'] + df["MNAnglePhi"] = df["rlnAngleRot"] + df["MNAngleTheta"] = df["rlnAngleTilt"] + df["MNAnglePsi"] = df["rlnAnglePsi"] + df["MNPixelSize"] = df["rlnImagePixelSize"] try: - df['MNImageId'] = df['rlnMicrographName'].astype( - 'category').cat.codes.to_numpy() + df["MNImageId"] = df["rlnMicrographName"].astype("category").cat.codes.to_numpy() except KeyError: try: - df['MNImageId'] = df['rlnTomoName'].astype( - 'category').cat.codes.to_numpy() + df["MNImageId"] = df["rlnTomoName"].astype("category").cat.codes.to_numpy() except KeyError: - df['MNImageId'] = 0.0 - + df["MNImageId"] = 0.0 + self.data = df - elif self.star_type == 'cistem': + elif self.star_type == "cistem": df = self.data - df['cisTEMZFromDefocus'] = ( - df['cisTEMDefocus1'] + df['cisTEMDefocus2']) / 2 - df['cisTEMZFromDefocus'] = df['cisTEMZFromDefocus'] - \ - df['cisTEMZFromDefocus'].median() - self.positions = df[['cisTEMOriginalXPosition', - 'cisTEMOriginalYPosition', 'cisTEMZFromDefocus']].to_numpy() - df['MNAnglePhi'] = df['cisTEMAnglePhi'] - df['MNAngleTheta'] = df['cisTEMAngleTheta'] - df['MNAnglePsi'] = df['cisTEMAnglePsi'] - df['MNPixelSize'] = df['cisTEMPixelSize'] - df['MNImageId'] = df['cisTEMOriginalImageFilename'].astype( - 'category').cat.codes.to_numpy() - - def _convert_mrc_to_tiff(self): + df["cisTEMZFromDefocus"] = (df["cisTEMDefocus1"] + df["cisTEMDefocus2"]) / 2 + df["cisTEMZFromDefocus"] = df["cisTEMZFromDefocus"] - df["cisTEMZFromDefocus"].median() + self.positions = df[ + [ + "cisTEMOriginalXPosition", + "cisTEMOriginalYPosition", + "cisTEMZFromDefocus", + ] + ].to_numpy() + df["MNAnglePhi"] = df["cisTEMAnglePhi"] + df["MNAngleTheta"] = df["cisTEMAngleTheta"] + df["MNAnglePsi"] = df["cisTEMAnglePsi"] + df["MNPixelSize"] = df["cisTEMPixelSize"] + df["MNImageId"] = df["cisTEMOriginalImageFilename"].astype("category").cat.codes.to_numpy() + + def _convert_mrc_to_tiff(self) -> Path: import mrcfile from pathlib import Path - if self.star_type == 'relion': - micrograph_path = self.object['rlnMicrographName_categories'][self.star_node.inputs['Image'].default_value - 1] - elif self.star_type == 'cistem': - micrograph_path = self.object['cisTEMOriginalImageFilename_categories'][self.star_node.inputs['Image'].default_value - 1].strip("'") + + if self.star_type == "relion": + micrograph_path = self.object["rlnMicrographName_categories"][ + self.star_node.inputs["Image"].default_value - 1 + ] + elif self.star_type == "cistem": + micrograph_path = self.object["cisTEMOriginalImageFilename_categories"][ + self.star_node.inputs["Image"].default_value - 1 + ].strip("'") else: return False - + # This could be more elegant if not Path(micrograph_path).exists(): pot_micrograph_path = Path(self.file_path).parent / micrograph_path if not pot_micrograph_path.exists(): - if self.star_type == 'relion': + if self.star_type == "relion": pot_micrograph_path = Path(self.file_path).parent.parent.parent / micrograph_path if not pot_micrograph_path.exists(): raise FileNotFoundError(f"Micrograph file {micrograph_path} not found") @@ -139,7 +155,7 @@ def _convert_mrc_to_tiff(self): raise FileNotFoundError(f"Micrograph file {micrograph_path} not found") micrograph_path = pot_micrograph_path - tiff_path = Path(micrograph_path).with_suffix('.tiff') + tiff_path = Path(micrograph_path).with_suffix(".tiff") if not tiff_path.exists(): with mrcfile.open(micrograph_path) as mrc: micrograph_data = mrc.data.copy() @@ -148,26 +164,29 @@ def _convert_mrc_to_tiff(self): if micrograph_data.ndim == 3: micrograph_data = np.sum(micrograph_data, axis=0) # Normalize the data to 0-1 - micrograph_data = (micrograph_data - micrograph_data.min()) / (micrograph_data.max() - micrograph_data.min()) - + micrograph_data = (micrograph_data - micrograph_data.min()) / ( + micrograph_data.max() - micrograph_data.min() + ) + if micrograph_data.dtype != np.float32: micrograph_data = micrograph_data.astype(np.float32) from PIL import Image + # Need to invert in Y to generate the correct tiff - Image.fromarray(micrograph_data[::-1,:]).save(tiff_path) + Image.fromarray(micrograph_data[::-1, :]).save(tiff_path) return tiff_path - - def _update_micrograph_texture(self, *_): + + def _update_micrograph_texture(self, *_) -> None: try: - show_micrograph = self.star_node.inputs['Show Micrograph'] - _ = self.object['mn'] + show_micrograph = self.star_node.inputs["Show Micrograph"] + _ = self.object["mn"] except ReferenceError: bpy.app.handlers.depsgraph_update_post.remove(self._update_micrograph_texture) return - if self.star_node.inputs['Image'].default_value == self.current_image: + if self.star_node.inputs["Image"].default_value == self.current_image: return else: - self.current_image = self.star_node.inputs['Image'].default_value + self.current_image = self.star_node.inputs["Image"].default_value if not show_micrograph: return tiff_path = self._convert_mrc_to_tiff() @@ -176,40 +195,42 @@ def _update_micrograph_texture(self, *_): image_obj = bpy.data.images[tiff_path.name] except KeyError: image_obj = bpy.data.images.load(str(tiff_path)) - image_obj.colorspace_settings.name = 'Non-Color' - self.micrograph_material.node_tree.nodes['Image Texture'].image = image_obj - self.star_node.inputs['Micrograph'].default_value = image_obj + image_obj.colorspace_settings.name = "Non-Color" + self.micrograph_material.node_tree.nodes["Image Texture"].image = image_obj + self.star_node.inputs["Micrograph"].default_value = image_obj - - - def create_model(self, name='StarFileObject', node_setup=True, world_scale=0.01): + def create_model( + self, name: str = "EnsembleObject", node_setup: bool = True, world_scale: float = 0.01, fraction: float = 1.0 + ) -> bpy.types.Object: from molecularnodes.blender.nodes import get_star_node, MN_micrograph_material - blender_object = bl.obj.create_object( - self.positions * world_scale, collection=bl.coll.mn(), name=name) - blender_object.mn['molecule_type'] = 'star' - + blender_object = bl.obj.create_object(self.positions * world_scale, collection=bl.coll.mn(), name=name) + + blender_object.mn["molecule_type"] = "star" + # create attribute for every column in the STAR file for col in self.data.columns: col_type = self.data[col].dtype # If col_type is numeric directly add if np.issubdtype(col_type, np.number): bl.obj.set_attribute( - blender_object, col, self.data[col].to_numpy().reshape(-1), 'FLOAT', 'POINT') + blender_object, + col, + self.data[col].to_numpy().reshape(-1), + "FLOAT", + "POINT", + ) # If col_type is object, convert to category and add integer values elif col_type == object: - codes = self.data[col].astype( - 'category').cat.codes.to_numpy().reshape(-1) - bl.obj.set_attribute(blender_object, col, codes, 'INT', 'POINT') + codes = self.data[col].astype("category").cat.codes.to_numpy().reshape(-1) + bl.obj.set_attribute(blender_object, col, codes, "INT", "POINT") # Add the category names as a property to the blender object - blender_object[f'{col}_categories'] = list( - self.data[col].astype('category').cat.categories) + blender_object[f"{col}_categories"] = list(self.data[col].astype("category").cat.categories) if node_setup: - bl.nodes.create_starting_nodes_starfile( - blender_object, n_images=self.n_images) - self.node_group = blender_object.modifiers['MolecularNodes'].node_group + bl.nodes.create_starting_nodes_starfile(blender_object, n_images=self.n_images) + self.node_group = blender_object.modifiers["MolecularNodes"].node_group blender_object["starfile_path"] = str(self.file_path) self.object = blender_object diff --git a/molecularnodes/io/retrieve.py b/molecularnodes/io/retrieve.py index 533b2926..80de2b9d 100644 --- a/molecularnodes/io/retrieve.py +++ b/molecularnodes/io/retrieve.py @@ -2,6 +2,9 @@ import requests import io +from typing import Union, Optional +from pathlib import Path + class FileDownloadPDBError(Exception): """ @@ -11,12 +14,20 @@ class FileDownloadPDBError(Exception): message -- explanation of the error """ - def __init__(self, message="There was an error downloading the file from the Protein Data Bank. PDB or format for PDB code may not be available."): + def __init__( + self, + message: str = "There was an error downloading the file from the Protein Data Bank. PDB or format for PDB code may not be available.", + ) -> None: self.message = message super().__init__(self.message) -def download(code, format="cif", cache=None, database='rcsb'): +def download( + code: str, + format: str = "cif", + cache: Optional[Union[Path, str]] = None, + database: str = "rcsb", +) -> Union[Path, str, io.StringIO, io.BytesIO]: """ Downloads a structure from the specified protein data bank in the given format. @@ -25,7 +36,7 @@ def download(code, format="cif", cache=None, database='rcsb'): code : str The code of the file to fetch. format : str, optional - The format of the file. Defaults to "cif". Possible values are ['cif', 'pdb', + The format of the file. Defaults to "cif". Possible values are ['cif', 'pdb', 'mmcif', 'pdbx', 'bcif']. cache : str, optional The cache directory to store the fetched file. Defaults to None. @@ -42,21 +53,13 @@ def download(code, format="cif", cache=None, database='rcsb'): ValueError If the specified format is not supported. """ - supported_formats = ['cif', 'pdb', 'bcif'] + supported_formats = ["cif", "pdb", "bcif"] if format not in supported_formats: - raise ValueError( - f"File format '{format}' not in: {supported_formats=}") + raise ValueError(f"File format '{format}' not in: {supported_formats=}") - _is_binary = (format in ['bcif']) + _is_binary = format in ["bcif"] filename = f"{code}.{format}" # create the cache location - if cache: - if not os.path.isdir(cache): - os.makedirs(cache) - - file = os.path.join(cache, filename) - else: - file = None # get the contents of the url try: @@ -67,22 +70,26 @@ def download(code, format="cif", cache=None, database='rcsb'): if _is_binary: content = r.content else: - content = r.text + content = r.text # type: ignore + + if cache: + if not os.path.isdir(cache): + os.makedirs(cache) - if file: + file = os.path.join(cache, filename) mode = "wb+" if _is_binary else "w+" with open(file, mode) as f: f.write(content) else: if _is_binary: - file = io.BytesIO(content) + file = io.BytesIO(content) # type: ignore else: - file = io.StringIO(content) + file = io.StringIO(content) # type: ignore return file -def _url(code, format, database="rcsb"): +def _url(code: str, format: str, database: str = "rcsb") -> str: "Get the URL for downloading the given file form a particular database." if database == "rcsb": @@ -93,27 +100,24 @@ def _url(code, format, database="rcsb"): return f"https://files.rcsb.org/download/{code}.{format}" # if database == "pdbe": # return f"https://www.ebi.ac.uk/pdbe/entry-files/download/{filename}" - elif database == 'alphafold': + elif database == "alphafold": return get_alphafold_url(code, format) # if database == "pdbe": # return f"https://www.ebi.ac.uk/pdbe/entry-files/download/{filename}" else: - ValueError(f"Database {database} not currently supported.") + raise ValueError(f"Database {database} not currently supported.") -def get_alphafold_url(code, format): - if format not in ['pdb', 'cif', 'bcif']: - ValueError( - f'Format {format} not currently supported from AlphaFold databse.' - ) +def get_alphafold_url(code: str, format: str) -> str: + if format not in ["pdb", "cif", "bcif"]: + ValueError(f"Format {format} not currently supported from AlphaFold databse.") # we have to first query the database, then they'll return some JSON with a list # of metadata, some items of which will be the URLs for the computed models # in different formats such as pdbUrl, cifUrl, bcifUrl url = f"https://alphafold.ebi.ac.uk/api/prediction/{code}" - print(f'{url=}') + print(f"{url=}") response = requests.get(url) print(f"{response=}") data = response.json()[0] - # return data[f'{format}Url'] - return data[f'{format}Url'] + return str(data[f"{format}Url"]) diff --git a/molecularnodes/io/star.py b/molecularnodes/io/star.py index 5b6743ca..79d1ac0c 100644 --- a/molecularnodes/io/star.py +++ b/molecularnodes/io/star.py @@ -1,58 +1,57 @@ import bpy from . import parse +from .parse.ensemble import Ensemble +from typing import Union, Set +from pathlib import Path + bpy.types.Scene.MN_import_star_file_path = bpy.props.StringProperty( - name='File', - description='File path for the `.star` file to import.', - subtype='FILE_PATH', - maxlen=0 + name="File", + description="File path for the `.star` file to import.", + subtype="FILE_PATH", + maxlen=0, ) bpy.types.Scene.MN_import_star_file_name = bpy.props.StringProperty( - name='Name', - description='Name of the created object.', - default='NewStarInstances', - maxlen=0 + name="Name", + description="Name of the created object.", + default="NewStarInstances", + maxlen=0, ) def load( - file_path, - name='NewStarInstances', - node_setup=True, - world_scale=0.01 -): - + file_path: Union[str, Path], name: str = "NewStarInstances", node_setup: bool = True, world_scale: float = 0.01 +) -> Ensemble: ensemble = parse.StarFile.from_starfile(file_path) - ensemble.create_model(name=name, node_setup=node_setup, - world_scale=world_scale) + ensemble.create_model(name=name, node_setup=node_setup, world_scale=world_scale) return ensemble -class MN_OT_Import_Star_File(bpy.types.Operator): +class MN_OT_Import_Star_File(bpy.types.Operator): # type: ignore bl_idname = "mn.import_star_file" bl_label = "Load" bl_description = "Will import the given file, setting up the points to instance an object." bl_options = {"REGISTER"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: return True - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: scene = context.scene load( file_path=scene.MN_import_star_file_path, name=scene.MN_import_star_file_name, - node_setup=True + node_setup=True, ) return {"FINISHED"} -def panel(layout, scene): - layout.label(text="Load Star File", icon='FILE_TICK') +def panel(layout: bpy.types.UILayout, scene: bpy.types.Scene) -> None: + layout.label(text="Load Star File", icon="FILE_TICK") layout.separator() row_import = layout.row() - row_import.prop(scene, 'MN_import_star_file_name') - layout.prop(scene, 'MN_import_star_file_path') - row_import.operator('mn.import_star_file') + row_import.prop(scene, "MN_import_star_file_name") + layout.prop(scene, "MN_import_star_file_path") + row_import.operator("mn.import_star_file") diff --git a/molecularnodes/io/wwpdb.py b/molecularnodes/io/wwpdb.py index e56eb5e5..d8418e21 100644 --- a/molecularnodes/io/wwpdb.py +++ b/molecularnodes/io/wwpdb.py @@ -1,86 +1,94 @@ -import bpy from pathlib import Path -from . import parse -from .retrieve import download, FileDownloadPDBError -from requests import HTTPError +from typing import Optional, Union, Set +import bpy -def fetch( - pdb_code, - style='spheres', - centre='', - del_solvent=True, - cache_dir=None, - build_assembly=False, - format="bcif" -): +from .parse import PDB, CIF, BCIF +from .parse.molecule import Molecule +from .retrieve import FileDownloadPDBError, download + +def fetch( + pdb_code: str, + style: Optional[str] = "spheres", + centre: str = "", + del_solvent: bool = True, + cache_dir: Optional[Union[Path, str]] = None, + build_assembly: bool = False, + format: str = "bcif", +) -> Molecule: if build_assembly: - centre = '' + centre = "" file_path = download(code=pdb_code, format=format, cache=cache_dir) - parsers = { - 'pdb': parse.PDB, - 'cif': parse.CIF, - 'bcif': parse.BCIF - } - molecule = parsers[format](file_path=file_path) + if format == "pdb": + molecule = PDB(file_path) + elif format == "cif": + molecule = CIF(file_path) + elif format == "bcif": + molecule = BCIF(file_path) model = molecule.create_model( name=pdb_code, centre=centre, style=style, del_solvent=del_solvent, - build_assembly=build_assembly + build_assembly=build_assembly, ) - model.mn['pdb_code'] = pdb_code - model.mn['molecule_type'] = format + model.mn["pdb_code"] = pdb_code + model.mn["molecule_type"] = format return molecule + # Properties that can be set in the scene, to be passed to the operator bpy.types.Scene.MN_pdb_code = bpy.props.StringProperty( - name='PDB', - description='The 4-character PDB code to download', - options={'TEXTEDIT_UPDATE'}, - maxlen=4 + name="PDB", + description="The 4-character PDB code to download", + options={"TEXTEDIT_UPDATE"}, + maxlen=4, ) bpy.types.Scene.MN_cache_dir = bpy.props.StringProperty( - name='', - description='Directory to save the downloaded files', - options={'TEXTEDIT_UPDATE'}, - default=str(Path('~', '.MolecularNodes').expanduser()), - subtype='DIR_PATH' + name="", + description="Directory to save the downloaded files", + options={"TEXTEDIT_UPDATE"}, + default=str(Path("~", ".MolecularNodes").expanduser()), + subtype="DIR_PATH", ) bpy.types.Scene.MN_cache = bpy.props.BoolProperty( name="Cache Downloads", description="Save the downloaded file in the given directory", - default=True + default=True, ) bpy.types.Scene.MN_import_format_download = bpy.props.EnumProperty( name="Format", description="Format to download as from the PDB", items=( - ("bcif", ".bcif", "Binary compressed .cif file, fastest for downloading"), - ("cif", ".cif", 'The new standard of .cif / .mmcif'), - ("pdb", ".pdb", "The classic (and depcrecated) PDB format") - ) + ( + "bcif", + ".bcif", + "Binary compressed .cif file, fastest for downloading", + ), + ("cif", ".cif", "The new standard of .cif / .mmcif"), + ("pdb", ".pdb", "The classic (and depcrecated) PDB format"), + ), ) # operator that is called by the 'button' press which calls the fetch function + class MN_OT_Import_wwPDB(bpy.types.Operator): bl_idname = "mn.import_wwpdb" bl_label = "Fetch" bl_description = "Download and open a structure from the Protein Data Bank" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: scene = context.scene pdb_code = scene.MN_pdb_code cache_dir = scene.MN_cache_dir @@ -93,7 +101,7 @@ def execute(self, context): if scene.MN_import_node_setup: style = scene.MN_import_style - centre = '' + centre = "" if scene.MN_import_centre: centre = scene.MN_centre_type @@ -105,40 +113,41 @@ def execute(self, context): style=style, cache_dir=cache_dir, build_assembly=scene.MN_import_build_assembly, - format=file_format + format=file_format, ) except FileDownloadPDBError as e: - self.report({'ERROR'}, str(e)) - if file_format == 'pdb': + self.report({"ERROR"}, str(e)) + if file_format == "pdb": self.report( - {'ERROR'}, 'There may not be a `.pdb` formatted file available - try a different download format.') + {"ERROR"}, + "There may not be a `.pdb` formatted file available - try a different download format.", + ) return {"CANCELLED"} bpy.context.view_layer.objects.active = mol.object - self.report( - {'INFO'}, message=f"Imported '{pdb_code}' as {mol.object.name}") + self.report({"INFO"}, message=f"Imported '{pdb_code}' as {mol.name}") return {"FINISHED"} -# the UI for the panel, which will display the operator and the properties +# the UI for the panel, which will display the operator and the properties -def panel(layout, scene): +def panel(layout: bpy.types.UILayout, scene: bpy.types.Scene) -> bpy.types.UILayout: layout.label(text="Download from PDB", icon="IMPORT") layout.separator() row_import = layout.row().split(factor=0.5) - row_import.prop(scene, 'MN_pdb_code') + row_import.prop(scene, "MN_pdb_code") download = row_import.split(factor=0.3) - download.prop(scene, 'MN_import_format_download', text="") - download.operator('mn.import_wwpdb') + download.prop(scene, "MN_import_format_download", text="") + download.operator("mn.import_wwpdb") layout.separator(factor=0.4) row = layout.row().split(factor=0.3) - row.prop(scene, 'MN_cache') + row.prop(scene, "MN_cache") row_cache = row.row() - row_cache.prop(scene, 'MN_cache_dir') + row_cache.prop(scene, "MN_cache_dir") row_cache.enabled = scene.MN_cache layout.separator() @@ -146,18 +155,18 @@ def panel(layout, scene): options = layout.column(align=True) row = options.row() - row.prop(scene, 'MN_import_node_setup', text='') + row.prop(scene, "MN_import_node_setup", text="") col = row.column() - col.prop(scene, 'MN_import_style') + col.prop(scene, "MN_import_style") col.enabled = scene.MN_import_node_setup row_centre = options.row() - row_centre.prop(scene, 'MN_import_centre', icon_value=0) + row_centre.prop(scene, "MN_import_centre", icon_value=0) col_centre = row_centre.column() - col_centre.prop(scene, 'MN_centre_type', text='') + col_centre.prop(scene, "MN_centre_type", text="") col_centre.enabled = scene.MN_import_centre options.separator() grid = options.grid_flow() - grid.prop(scene, 'MN_import_build_assembly') - grid.prop(scene, 'MN_import_del_solvent') + grid.prop(scene, "MN_import_build_assembly") + grid.prop(scene, "MN_import_del_solvent") diff --git a/molecularnodes/pkg.py b/molecularnodes/pkg.py index 8c37d9af..5b258ac9 100644 --- a/molecularnodes/pkg.py +++ b/molecularnodes/pkg.py @@ -6,6 +6,7 @@ import sys import os import logging +from typing import List, Optional, Dict, Set from importlib.metadata import version as get_version, PackageNotFoundError import bpy import pathlib @@ -15,10 +16,10 @@ PYPI_MIRROR = { # the original. - 'Default': '', + "Default": "", # two mirrors in China Mainland to help those poor victims under GFW. - 'BFSU (Beijing)': 'https://mirrors.bfsu.edu.cn/pypi/web/simple', - 'TUNA (Beijing)': 'https://pypi.tuna.tsinghua.edu.cn/simple', + "BFSU (Beijing)": "https://mirrors.bfsu.edu.cn/pypi/web/simple", + "TUNA (Beijing)": "https://pypi.tuna.tsinghua.edu.cn/simple", # append more if necessary. } """ @@ -26,7 +27,7 @@ """ -def start_logging(logfile_name: str = 'side-packages-install') -> logging.Logger: +def start_logging(logfile_name: str = "side-packages-install") -> logging.Logger: """ Configure and start logging to a file. @@ -47,7 +48,7 @@ def start_logging(logfile_name: str = 'side-packages-install') -> logging.Logger """ # Create the logs directory if it doesn't exist - logs_dir = os.path.join(os.path.abspath(ADDON_DIR), 'logs') + logs_dir = os.path.join(os.path.abspath(ADDON_DIR), "logs") os.makedirs(logs_dir, exist_ok=True) # Set up logging configuration @@ -58,7 +59,7 @@ def start_logging(logfile_name: str = 'side-packages-install') -> logging.Logger return logging.getLogger() -def get_pypi_mirror_alias(self, context, edit_text): +def get_pypi_mirror_alias(self: bpy.types.StringProperty, context: bpy.types.Context, edit_text: str) -> List[str]: """ Get the available PyPI mirror aliases. @@ -77,7 +78,7 @@ def get_pypi_mirror_alias(self, context, edit_text): A view object of the available PyPI mirror aliases. """ - return PYPI_MIRROR.keys() + return list(PYPI_MIRROR.keys()) def process_pypi_mirror_to_url(pypi_mirror_provider: str) -> str: @@ -100,61 +101,15 @@ def process_pypi_mirror_to_url(pypi_mirror_provider: str) -> str: If the provided PyPI mirror provider is invalid. """ - if pypi_mirror_provider.startswith('https:'): + if pypi_mirror_provider.startswith("https:"): return pypi_mirror_provider elif pypi_mirror_provider in PYPI_MIRROR.keys(): return PYPI_MIRROR[pypi_mirror_provider] else: - raise ValueError( - f"Invalid PyPI mirror provider: {pypi_mirror_provider}") + raise ValueError(f"Invalid PyPI mirror provider: {pypi_mirror_provider}") -def get_pkgs(requirements: str = None) -> dict: - """ - Read a requirements file and extract package information into a dictionary. - - Parameters - ---------- - requirements : str, optional - The path to the requirements file. If not provided, the function looks for a `requirements.txt` - file in the same directory as the script. - - Returns - ------- - dict - A dictionary containing package information. Each element of the dictionary is a dictionary containing the package name, version, and description. - - Example - ------- - Given the following requirements file: - ```python - Flask==1.1.2 # A micro web framework for Python - pandas==1.2.3 # A fast, powerful, flexible, and easy-to-use data analysis and manipulation tool - numpy==1.20.1 # Fundamental package for scientific computing - ``` - The function would return the following dictionary: - ```python - [ - { - "package": "Flask", - "version": "1.1.2", - "desc": "A micro web framework for Python" - }, - { - "package": "pandas", - "version": "1.2.3", - "desc": "A fast, powerful, flexible, and easy-to-use data analysis and manipulation tool" - }, - { - "package": "numpy", - "version": "1.20.1", - "desc": "Fundamental package for scientific computing" - } - ] - ``` - """ - import pathlib - +def get_pkgs(requirements: Optional[str] = None) -> Dict[str, Dict[str, str]]: if not requirements: folder_path = pathlib.Path(__file__).resolve().parent requirements = f"{folder_path}/requirements.txt" @@ -164,13 +119,13 @@ def get_pkgs(requirements: str = None) -> dict: pkgs = {} for line in lines: try: - pkg, desc = line.split('#') - pkg_meta = pkg.split('==') + pkg, desc = line.split("#") + pkg_meta = pkg.split("==") name = pkg_meta[0].strip() pkgs[name] = { "name": name, "version": pkg_meta[1].strip(), - "desc": desc.strip() + "desc": desc.strip(), } except ValueError: # Skip line if it doesn't have the expected format @@ -194,15 +149,15 @@ def is_current(package: str) -> bool: True if the package is the current version, False otherwise. """ - pkg = get_pkgs().get(package) try: + pkg = get_pkgs()[package] available_version = get_version(package) - return available_version == pkg['version'] + return bool(available_version == pkg["version"]) except PackageNotFoundError: return False -def run_python(cmd_list: list = None, mirror_url: str = '', timeout: int = 600): +def run_python(cmd_list: List[str], mirror_url: str = "", timeout: int = 600) -> subprocess.CompletedProcess: """ Runs pip command using the specified command list and returns the command output. @@ -234,34 +189,33 @@ def run_python(cmd_list: list = None, mirror_url: str = '', timeout: int = 600): """ # path to python.exe - python_exe = os.path.realpath(sys.executable) + python_exe = str(os.path.realpath(sys.executable)) # build the command list cmd_list = [python_exe] + cmd_list # add mirror to the command list if it's valid - if mirror_url and mirror_url.startswith('https'): - cmd_list += ['-i', mirror_url] + if mirror_url and mirror_url.startswith("https"): + cmd_list += ["-i", mirror_url] log = start_logging() log.info(f"Running Pip: '{cmd_list}'") # run the command and capture the output - result = subprocess.run(cmd_list, timeout=timeout, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run(cmd_list, timeout=timeout, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: - log.error('Command failed: %s', cmd_list) - log.error('stdout: %s', result.stdout.decode()) - log.error('stderr: %s', result.stderr.decode()) + log.error("Command failed: %s", cmd_list) + log.error("stdout: %s", result.stdout.decode()) + log.error("stderr: %s", result.stderr.decode()) else: - log.info('Command succeeded: %s', cmd_list) - log.info('stdout: %s', result.stdout.decode()) + log.info("Command succeeded: %s", cmd_list) + log.info("stdout: %s", result.stdout.decode()) # return the command list, return code, stdout, and stderr as a tuple return result -def install_package(package: str, pypi_mirror_provider: str = 'Default') -> list: +def install_package(package: str, pypi_mirror_provider: str = "Default") -> subprocess.CompletedProcess: """ Install a Python package and its dependencies using pip. @@ -296,15 +250,12 @@ def install_package(package: str, pypi_mirror_provider: str = 'Default') -> list print(f"Installing {package}...") - mirror_url = process_pypi_mirror_to_url( - pypi_mirror_provider=pypi_mirror_provider) + mirror_url = process_pypi_mirror_to_url(pypi_mirror_provider=pypi_mirror_provider) print(f"Using PyPI mirror: {pypi_mirror_provider} {mirror_url}") - run_python(["-m", "ensurepip"]), - run_python(["-m", "pip", "install", "--upgrade", "pip"], - mirror_url=mirror_url) - result = run_python(["-m", "pip", "install", package], - mirror_url=mirror_url) + (run_python(["-m", "ensurepip"]),) + run_python(["-m", "pip", "install", "--upgrade", "pip"], mirror_url=mirror_url) + result = run_python(["-m", "pip", "install", package], mirror_url=mirror_url) return result @@ -322,13 +273,13 @@ class InstallationError(Exception): """ - def __init__(self, package_name, error_message): + def __init__(self, package_name: str, error_message: str) -> None: self.package_name = package_name self.error_message = error_message super().__init__(f"Failed to install {package_name}: {error_message}") -def install_all_packages(pypi_mirror_provider: str = 'Default') -> list: +def install_all_packages(pypi_mirror_provider: str = "Default") -> List[subprocess.CompletedProcess]: """ Install all packages listed in the 'requirements.txt' file. @@ -356,80 +307,72 @@ def install_all_packages(pypi_mirror_provider: str = 'Default') -> list: ``` """ - mirror_url = process_pypi_mirror_to_url( - pypi_mirror_provider=pypi_mirror_provider) + mirror_url = process_pypi_mirror_to_url(pypi_mirror_provider=pypi_mirror_provider) pkgs = get_pkgs() results = [] - for pkg in pkgs.items(): - + for name, pkg in pkgs.items(): try: - result = install_package(package=f"{pkg.get('name')}=={pkg.get('version')}", - pypi_mirror_provider=mirror_url) + result = install_package( + package=f"{pkg.get('name')}=={pkg.get('version')}", + pypi_mirror_provider=mirror_url, + ) results.append(result) except InstallationError as e: - raise InstallationError( - f"Error installing package {pkg.get('name')}: {str(e)}") + raise InstallationError(name, str(e)) return results -class MN_OT_Install_Package(bpy.types.Operator): - bl_idname = 'mn.install_package' - bl_label = 'Install Given Python Package' - bl_options = {'REGISTER', 'INTERNAL'} - package: bpy.props.StringProperty( - name='Python Package', - description='Python Package to Install', - default='biotite' - ) - version: bpy.props.StringProperty( - name='Python Package', - description='Python Package to Install', - default='0.36.1' - ) +class MN_OT_Install_Package(bpy.types.Operator): # type: ignore + bl_idname = "mn.install_package" + bl_label = "Install Given Python Package" + bl_options = {"REGISTER", "INTERNAL"} - description: bpy.props.StringProperty( - name='Operator description', - default='Install specified python package.' + package: bpy.props.StringProperty( # type: ignore + name="Python Package", + description="Python Package to Install", + default="biotite", ) + version: bpy.props.StringProperty(name="Python Package", description="Python Package to Install", default="0.36.1") # type: ignore + + description_str: bpy.props.StringProperty(name="Operator description", default="Install specified python package.") # type: ignore @classmethod - def description(cls, context, properties): - return properties.description + def description(cls, context: bpy.types.Context, properties: bpy.types.PropertyGroup) -> str: + return str(properties.description_str) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: installable = f"{self.package}=={self.version}" - result = install_package(package=installable, - pypi_mirror_provider=bpy.context.scene.pypi_mirror_provider) + result = install_package( + package=installable, + pypi_mirror_provider=bpy.context.scene.pypi_mirror_provider, + ) if result.returncode == 0 and is_current(self.package): - self.report( - {'INFO'}, - f"Successfully installed {self.package} v{self.version}" - ) + self.report({"INFO"}, f"Successfully installed {self.package} v{self.version}") else: - log_dir = os.path.join(os.path.abspath(ADDON_DIR), 'logs') + log_dir = os.path.join(os.path.abspath(ADDON_DIR), "logs") self.report( - {'ERROR'}, - f"Error installing package. Please check the log files in '{log_dir}'." + {"ERROR"}, + f"Error installing package. Please check the log files in '{log_dir}'.", ) - return {'FINISHED'} + return {"FINISHED"} -def button_install_pkg(layout, name, version, desc=''): +def button_install_pkg(layout: bpy.types.UILayout, name: str, version: str, desc: str = "") -> bpy.types.UILayout: layout = layout.row() if is_current(name): row = layout.row() row.label(text=f"{name} version {version} is installed.") - op = row.operator('mn.install_package', text=f'Reinstall {name}') + op = row.operator("mn.install_package", text=f"Reinstall {name}") op.package = name op.version = version - op.description = f'Reinstall {name}' + op.description = f"Reinstall {name}" else: row = layout.row(heading=f"Package: {name}") col = row.column() col.label(text=str(desc)) col = row.column() - op = col.operator('mn.install_package', text=f'Install {name}') + op = col.operator("mn.install_package", text=f"Install {name}") op.package = name op.version = version - op.description = f'Install required python package: {name}' + op.description = f"Install required python package: {name}" diff --git a/molecularnodes/props.py b/molecularnodes/props.py index c6234393..37ed26c4 100644 --- a/molecularnodes/props.py +++ b/molecularnodes/props.py @@ -1,58 +1,64 @@ import bpy +from bpy.props import BoolProperty, StringProperty, EnumProperty, IntProperty - -bpy.types.Scene.MN_import_centre = bpy.props.BoolProperty( +bpy.types.Scene.MN_import_centre = BoolProperty( name="Centre Structure", description="Move the imported Molecule on the World Origin", - default=False + default=False, ) -bpy.types.Scene.MN_centre_type = bpy.props.EnumProperty( +bpy.types.Scene.MN_centre_type = EnumProperty( name="Method", - default='mass', + default="mass", items=( - ('mass', "Mass", "Adjust the structure's centre of mass to be at the world origin", 1), - ('centroid', "Centroid", - "Adjust the structure's centroid (centre of geometry) to be at the world origin", 2) - ) + ( + "mass", + "Mass", + "Adjust the structure's centre of mass to be at the world origin", + 1, + ), + ( + "centroid", + "Centroid", + "Adjust the structure's centroid (centre of geometry) to be at the world origin", + 2, + ), + ), ) -bpy.types.Scene.MN_import_del_solvent = bpy.props.BoolProperty( +bpy.types.Scene.MN_import_del_solvent = BoolProperty( name="Remove Solvent", description="Delete the solvent from the structure on import", - default=True + default=True, ) -bpy.types.Scene.MN_import_panel_selection = bpy.props.IntProperty( +bpy.types.Scene.MN_import_panel_selection = IntProperty( name="MN_import_panel_selection", description="Import Panel Selection", - subtype='NONE', - default=0 -) -bpy.types.Scene.MN_import_build_assembly = bpy.props.BoolProperty( - name='Build Assembly', - default=False + subtype="NONE", + default=0, ) -bpy.types.Scene.MN_import_node_setup = bpy.props.BoolProperty( +bpy.types.Scene.MN_import_build_assembly = BoolProperty(name="Build Assembly", default=False) +bpy.types.Scene.MN_import_node_setup = BoolProperty( name="Setup Nodes", default=True, - description='Create and set up a Geometry Nodes tree on import' + description="Create and set up a Geometry Nodes tree on import", ) class MolecularNodesObjectProperties(bpy.types.PropertyGroup): - subframes: bpy.props.IntProperty( + subframes: IntProperty( # type: ignore name="Subframes", description="Number of subframes to interpolate for MD trajectories", - default=0 + default=0, ) - molecule_type: bpy.props.StringProperty( + molecule_type: StringProperty( # type: ignore name="Molecular Type", description="How the file was imported, dictating how MN interacts with it", - default="" + default="", ) - pdb_code: bpy.props.StringProperty( + pdb_code: StringProperty( # type: ignore name="PDB", description="PDB code used to download this structure", maxlen=4, - options={'HIDDEN'} + options={"HIDDEN"}, ) diff --git a/molecularnodes/ui/func.py b/molecularnodes/ui/func.py index d1d20b89..08e6ed9a 100644 --- a/molecularnodes/ui/func.py +++ b/molecularnodes/ui/func.py @@ -1,37 +1,34 @@ -import bpy -from ..blender import nodes - - def build_menu(layout, items): for item in items: # print(item) if item == "break": layout.separator() - elif item['label'] == "custom": - for button in item['values']: - item['function'](layout, - label=button['label'], - field=button['field'], - prefix=button['prefix'], - property_id=button['property_id'] - ) - elif item['name'].startswith("mn."): - layout.operator(item['name']) + elif item["label"] == "custom": + for button in item["values"]: + item["function"]( + layout, + label=button["label"], + field=button["field"], + prefix=button["prefix"], + property_id=button["property_id"], + ) + elif item["name"].startswith("mn."): + layout.operator(item["name"]) else: - label = item['label'] - name = item['name'] - description = item['description'].split('\n')[0].removesuffix('.') - menu_item_interface(layout, label=label, - name=name, description=description) - - -def menu_item_interface(layout_function, - label, - name, - description='Add custom MolecularNodes node group.', - node_link=False - ): - op = layout_function.operator('mn.add_custom_node_group', text=label) + label = item["label"] + name = item["name"] + description = item["description"].split("\n")[0].removesuffix(".") + menu_item_interface(layout, label=label, name=name, description=description) + + +def menu_item_interface( + layout_function, + label, + name, + description="Add custom MolecularNodes node group.", + node_link=False, +): + op = layout_function.operator("mn.add_custom_node_group", text=label) op.node_label = label op.node_name = name op.node_description = description @@ -39,7 +36,7 @@ def menu_item_interface(layout_function, def button_custom_color(layout, label, field, prefix, property_id, starting_value=0): - op = layout.operator('mn.color_custom', text=label) + op = layout.operator("mn.color_custom", text=label) op.field = field op.prefix = prefix op.node_property = property_id @@ -49,7 +46,7 @@ def button_custom_color(layout, label, field, prefix, property_id, starting_valu def button_custom_selection(layout, label, field, prefix, property_id, starting_value=0): - op = layout.operator('mn.selection_custom', text=label) + op = layout.operator("mn.selection_custom", text=label) op.field = field op.prefix = prefix op.node_property = property_id diff --git a/molecularnodes/ui/node_info.py b/molecularnodes/ui/node_info.py index 2d78eaa4..86c218f5 100644 --- a/molecularnodes/ui/node_info.py +++ b/molecularnodes/ui/node_info.py @@ -1,492 +1,478 @@ -from .func import ( - button_custom_color, - button_custom_selection -) +from .func import button_custom_color, button_custom_selection menu_items = { - 'style': [ + "style": [ { - 'label': 'Presets', - 'name': 'MN_style_presets', + "label": "Presets", + "name": "MN_style_presets", "description": "Quickly switch between several different pre-made preset styles. Best used when using MolecularNodes via scripts, ensuring all atoms are displayed using a combination of cartoons and atoms.", - "video_url": "https://imgur.com/gCQRWBk.mp4" + "video_url": "https://imgur.com/gCQRWBk.mp4", }, { - 'label': 'Spheres', - 'name': 'MN_style_spheres', + "label": "Spheres", + "name": "MN_style_spheres", "description": "Style to apply the traditional space-filling atomic representation of atoms. Spheres are scaled based on the `vdw_radii` attribute. By default the _Point Cloud_ rendering system is used, which is only visible inside of Cycles. By enabling 'EEVEE' it creates a sphere mesh object per atom. This makes it visible inside of EEVEE, but has poor performance at high atom counts.", - "video_url": "https://imgur.com/3anAJqz" + "video_url": "https://imgur.com/3anAJqz", }, { - 'label': 'Cartoon', - 'name': 'MN_style_cartoon', + "label": "Cartoon", + "name": "MN_style_cartoon", "description": "Style to apply the traditional cartoon representation of protein structures. This style highlights alpha-helices and beta-sheets with arrows and cylinders.", - "video_url": "https://imgur.com/1xmdfxZ" + "video_url": "https://imgur.com/1xmdfxZ", }, { - 'label': 'Ribbon', - 'name': 'MN_style_ribbon', + "label": "Ribbon", + "name": "MN_style_ribbon", "description": "Style that creates a continuous solid ribbon or licorice tube through the backbones of peptides and nucleic acids.", - "video_url": "https://imgur.com/iMxEJaH" + "video_url": "https://imgur.com/iMxEJaH", }, { - 'label': 'Surface', - 'name': 'MN_style_surface', + "label": "Surface", + "name": "MN_style_surface", "description": "Style that creates a surface representation based on the proximity of atoms to a probe that is moved through the entire structure.", - "video_url": "https://imgur.com/ER8pcYf" + "video_url": "https://imgur.com/ER8pcYf", }, { - 'label': 'Ball and Stick', - 'name': 'MN_style_ball_and_stick', + "label": "Ball and Stick", + "name": "MN_style_ball_and_stick", "description": "Style that creates cylinders for bonds and spheres for atoms. The atoms can be either Eevee or Cycles compatible, with customisation to resolution and radius possible.", - "video_url": "https://imgur.com/kuWuOsw" + "video_url": "https://imgur.com/kuWuOsw", }, { - 'label': 'Stick', - 'name': 'MN_style_stick', + "label": "Stick", + "name": "MN_style_stick", "description": "Style that creates a cylinder for each bond. Cylindrical caps to the cylinders are currently not supported. Best to use [`MN_style_ball_and_stick`](#style-ball-and-stick).", - "video_url": "https://imgur.com/tV4XalY" - } + "video_url": "https://imgur.com/tV4XalY", + }, ], - 'select': [ + "select": [ { - 'label': 'Separate Atoms', - 'name': 'MN_select_separate_atoms', - 'description': "Select only the desired input atoms. The output is bits of geometry, which include the selection and include the inverse of the selected atoms. You can expand the selection to include an entire residue if a single atom in that residue is selected, by setting `Whole Residue` to `True`.", - 'video_url': "https://imgur.com/VsCW0HY" + "label": "Separate Atoms", + "name": "MN_select_separate_atoms", + "description": "Select only the desired input atoms. The output is bits of geometry, which include the selection and include the inverse of the selected atoms. You can expand the selection to include an entire residue if a single atom in that residue is selected, by setting `Whole Residue` to `True`.", + "video_url": "https://imgur.com/VsCW0HY", }, { - 'label': 'Separate Polymers', - 'name': 'MN_select_separate_polymers', - 'description': "Separate the input atomic geometry into it's different polymers or `Protein`, `Nucleic Acid` and `other`.", - 'video_url': 'https://imgur.com/ICQZxxz' + "label": "Separate Polymers", + "name": "MN_select_separate_polymers", + "description": "Separate the input atomic geometry into it's different polymers or `Protein`, `Nucleic Acid` and `other`.", + "video_url": "https://imgur.com/ICQZxxz", }, "break", { - 'label': 'custom', - 'function': button_custom_selection, - 'values': [ + "label": "custom", + "function": button_custom_selection, + "values": [ { - 'label': 'Chain', - 'field': 'chain_id', - 'name': 'MN_select_chain_', - 'prefix': 'Chain ', - 'property_id': 'chain_ids', + "label": "Chain", + "field": "chain_id", + "name": "MN_select_chain_", + "prefix": "Chain ", + "property_id": "chain_ids", "description": "Select single or multiple of the different chains. Creates a selection based on the `chain_id` attribute.", - "video_url": "https://imgur.com/P9ZVT2Z" + "video_url": "https://imgur.com/P9ZVT2Z", }, { - 'label': 'Entity', - 'field': 'entity_id', - 'name': 'MN_select_entity_', - 'prefix': '', - 'property_id': 'entity_ids', + "label": "Entity", + "field": "entity_id", + "name": "MN_select_entity_", + "prefix": "", + "property_id": "entity_ids", "description": "Select single or multiple of the different entities. Creates a selection based on the `entity_id` attribute.", - "video_url": "https://imgur.com/fKQIfGZ" + "video_url": "https://imgur.com/fKQIfGZ", }, { - 'label': 'Ligand', - 'field': 'res_name', - 'name': 'MN_select_ligand_', - 'prefix': '', - 'property_id': 'ligands', + "label": "Ligand", + "field": "res_name", + "name": "MN_select_ligand_", + "prefix": "", + "property_id": "ligands", "description": "Select single or multiple of the different ligands.", - "video_url": "https://imgur.com/s2seWIw" - } - ] + "video_url": "https://imgur.com/s2seWIw", + }, + ], }, "break", { - 'label': 'Cube', - 'name': 'MN_select_cube', + "label": "Cube", + "name": "MN_select_cube", "description": "Create a selection that is inside the `Empty_Cube` object. When this node is first created, an _empty_ object called `Empty_Cube` should be created. You can always create additional empty objects through the add menu, to use a different object. The rotation and scale of the object will be taken into account for the selection.", - "video_url": "https://imgur.com/P4GZ7vq" + "video_url": "https://imgur.com/P4GZ7vq", }, { - 'label': 'Sphere', - 'name': 'MN_select_sphere', + "label": "Sphere", + "name": "MN_select_sphere", "description": "Create a selection that is within a spherical radius of an object, based on that object's scale. By default an _empty_ object called `Empty_Sphere` is created. You can use other objects or create a new empty to use. The origin point for the object will be used, which should be taken in to account when using molecules. Use [`MN_select_proximity`](#select-proximity) for selections which are within a certain distance of a selection of atoms instead of a single origin point.", - "video_url": "https://imgur.com/xdeTZR7" + "video_url": "https://imgur.com/xdeTZR7", }, "break", { - 'label': 'Secondary Structure', - 'name': 'MN_select_sec_struct', + "label": "Secondary Structure", + "name": "MN_select_sec_struct", # or can be calculated using the [`MN_utils_dssp'](#utils-dssp) node.", "description": "Select based on the assigned secondary structure information. Only returns a selection if the `sec_struct` attribute exists on the atoms. Will be imported from files where it is present", - "video_url": "https://imgur.com/IindS3D" + "video_url": "https://imgur.com/IindS3D", }, { - 'label': 'Backbone', - 'name': 'MN_select_backbone', + "label": "Backbone", + "name": "MN_select_backbone", "description": "Selection fields for the backbone and side chains of the protein and nucleic acids.", - "video_url": "https://imgur.com/Sbl6ns5" + "video_url": "https://imgur.com/Sbl6ns5", }, { - 'label': 'Atomic Number', - 'name': 'MN_select_atomic_number', - 'description': "Select single elements, by matching to the `atomic_number` field. Useful for selecting single elements, or combining to select elements higher than 20 on the periodic table.", - 'video_url': "https://imgur.com/Bxn33YK" + "label": "Atomic Number", + "name": "MN_select_atomic_number", + "description": "Select single elements, by matching to the `atomic_number` field. Useful for selecting single elements, or combining to select elements higher than 20 on the periodic table.", + "video_url": "https://imgur.com/Bxn33YK", }, { - 'label': 'Element', - 'name': 'MN_select_element', + "label": "Element", + "name": "MN_select_element", "description": "Select individual elements, for the first 20 elements on the periodic table. For selections of higher elements, use [`MN_select_atomic_number`](#select-atomic-number). Creating a node which includes more elements becomes too large to be practical.", - "video_url": "https://imgur.com/nRQwamG" + "video_url": "https://imgur.com/nRQwamG", }, { - 'label': 'Attribute', - 'name': 'MN_select_attribute', + "label": "Attribute", + "name": "MN_select_attribute", "description": "Selections based on the different attributes that are available on the atomic geometry.", - "video_url": "https://imgur.com/HakZ4sx" + "video_url": "https://imgur.com/HakZ4sx", }, { - 'label': 'Bonded Atoms', - 'name': 'MN_select_bonded', + "label": "Bonded Atoms", + "name": "MN_select_bonded", "description": "Based on an initial selection, finds atoms which are within a certain number of bonds of this selection. Output can include or excluded the original selection.", - "video_url": "https://imgur.com/g8hgXup" + "video_url": "https://imgur.com/g8hgXup", }, "break", { - 'label': 'Res ID', - 'name': 'mn.residues_selection_custom', - 'backup': 'MN_select_res_id_', + "label": "Res ID", + "name": "mn.residues_selection_custom", + "backup": "MN_select_res_id_", "description": "Create a more complex selection for the `res_id` field, by specifying multiple ranges and potential single `res_id` numbers. This node is built uniquely each time, to the inputs will look different for each user.\nIn the example below, residues 10 & 15 are selected, as well as residues between and including 20-100.\nThe node was created by inputting `10, 15, 20-100` into the node creation field.", - "video_url": "https://imgur.com/OwAXsbG" + "video_url": "https://imgur.com/OwAXsbG", }, { - 'label': 'Proximity', - 'name': 'MN_select_proximity', + "label": "Proximity", + "name": "MN_select_proximity", "description": "Create a selection based on the proximity to the Target Atoms of the input. A sub-selection of the Target atoms can be used if the `Selection` input is used. You can expand the selection to include an entire residue if a single atom in that residue is selected, by setting `Whole Residue` to `True`.\nIn the example below, the `MN_style_atoms` is being applied to a selection, which is being calculated from the proximity of atoms to specific chains. As the cutoff for the selection is changed, it includes or excludes more atoms. The `Whole Residue` option also ensures that entire residues are shown.", - "video_url": "https://imgur.com/RI80CRY" + "video_url": "https://imgur.com/RI80CRY", }, { - 'label': 'Res ID Single', - 'name': 'MN_select_res_id_single', + "label": "Res ID Single", + "name": "MN_select_res_id_single", "description": "Select a single residue based on the `res_id` number.", - "video_url": "https://imgur.com/BL6AOP4" + "video_url": "https://imgur.com/BL6AOP4", }, { - 'label': 'Res ID Range', - 'name': 'MN_select_res_id_range', + "label": "Res ID Range", + "name": "MN_select_res_id_range", "description": "Select multiple residues by specifying a _minimum_ and a _maximum_ which will create the selection based on the `res_id` number.", - "video_url": "https://imgur.com/NdoQcdE" + "video_url": "https://imgur.com/NdoQcdE", }, { - 'label': 'Res Name Peptide', - 'name': 'MN_select_res_name_peptide', + "label": "Res Name Peptide", + "name": "MN_select_res_name_peptide", "description": "Select single or multiple protein residues by name. Includes the 20 naturally occurring amino acids.", - "video_url": "https://imgur.com/kjzH9Rs" + "video_url": "https://imgur.com/kjzH9Rs", }, { - 'label': 'Res Name Nucleic', - 'name': 'MN_select_res_name_nucleic', + "label": "Res Name Nucleic", + "name": "MN_select_res_name_nucleic", "description": "Select single or multiple nucleic residues by name.", - "video_url": "https://imgur.com/qnUlHpG" + "video_url": "https://imgur.com/qnUlHpG", }, { - 'label': 'Res Whole', - 'name': 'MN_select_res_whole', + "label": "Res Whole", + "name": "MN_select_res_whole", "description": "Expand the given selection to include a whole residue, if a single atom in that residue is selected. Useful for when a distance or proximity selection includes some of the residue and you wish to include all of the residue.", - "video_url": "https://imgur.com/JFzwE0i" + "video_url": "https://imgur.com/JFzwE0i", }, ], - 'color': [ + "color": [ { - 'label': 'Set Color', - 'name': 'MN_color_set', + "label": "Set Color", + "name": "MN_color_set", "description": "The is the primary way to change the color of structures in Molecular Nodes. Colors for cartoon and ribbon are taken from the _alpha-carbons_ of the structures. Change the color of the input atoms, based on a selection and a color field. The color field can be as complex of a calculation as you wish. In the example below the color for the whole structure can be set, or the color can be based on a color for each chain, or the result of mapping a color to an attribute such as `b_factor`.", - "video_url": "https://imgur.com/667jf0O" + "video_url": "https://imgur.com/667jf0O", }, "break", { - 'label': 'custom', - 'function': button_custom_color, - 'values': [ + "label": "custom", + "function": button_custom_color, + "values": [ { - 'label': 'Chain', - 'field': 'chain_id', - 'name': 'MN_select_chain_', - 'prefix': 'Chain', - 'property_id': 'chain_ids', + "label": "Chain", + "field": "chain_id", + "name": "MN_select_chain_", + "prefix": "Chain", + "property_id": "chain_ids", "description": "Choose the colors for individual chains in the structure. This node is generated for each particular molecule, so the inputs will look different based on the imported structure. For larger structures with many chains this node may become too large to be practical, in which case you might better use [`MN_color_entity_id`](#color-entity-id).", - "video_url": "https://imgur.com/9oM24vB" + "video_url": "https://imgur.com/9oM24vB", }, { - 'label': 'Entity', - 'field': 'entity_id', - 'name': 'MN_color_entity_', - 'prefix': '', - 'property_id': 'entity_ids', + "label": "Entity", + "field": "entity_id", + "name": "MN_color_entity_", + "prefix": "", + "property_id": "entity_ids", "description": "Choose the colors for individual entities in the structure. Multiple chains may be classified as the same entity, if they are copies of the same chain but in different conformations or positions and rotations. The nodes is generated for each individual structure, if `entity_id` is available.", - "video_url": "https://imgur.com/kEvj5Jk" + "video_url": "https://imgur.com/kEvj5Jk", }, { - 'label': 'Ligand', - 'field': 'res_name', - 'name': 'MN_color_ligand_', - 'prefix': '', - 'property_id': 'ligands', + "label": "Ligand", + "field": "res_name", + "name": "MN_color_ligand_", + "prefix": "", + "property_id": "ligands", "description": "Choose the colors for individual ligands in the structure.", - "video_url": "https://imgur.com/bQh8Fd9" - } - ] + "video_url": "https://imgur.com/bQh8Fd9", + }, + ], }, "break", { - 'label': 'Goodsell Colors', - 'name': 'MN_color_goodsell', + "label": "Goodsell Colors", + "name": "MN_color_goodsell", "description": "Change the inputted color to be darker for non-carbon atoms. Creates a _Goodsell Style_ color scheme for individual chains.", - "video_url": "https://imgur.com/gPgMSRa" + "video_url": "https://imgur.com/gPgMSRa", }, { - 'label': 'Attribute Map', - 'name': 'MN_color_attribute_map', + "label": "Attribute Map", + "name": "MN_color_attribute_map", "description": "Interpolate between two or three colors, based on the value of an attribute field such as `b_factor`. Choosing the minimum and maximum values with the inputs.", - "video_url": "https://imgur.com/lc2o6e1" + "video_url": "https://imgur.com/lc2o6e1", }, { - 'label': 'Attribute Random', - 'name': 'MN_color_attribute_random', + "label": "Attribute Random", + "name": "MN_color_attribute_random", "description": "Generate a random color, based on the given attribute. Control the lightness and saturation of the color with the inputs.", - "video_url": "https://imgur.com/5sMcpAu" + "video_url": "https://imgur.com/5sMcpAu", }, + {"label": "Backbone", "name": "MN_color_backbone", "description": ""}, { - 'label': 'Backbone', - 'name': 'MN_color_backbone', - 'description': "" + "label": "pLDTT", + "name": "MN_color_pLDTT", + "description": "Assigns colors using the `b_factor` attribute, which contains the `pLDTT` attribute for models that come from AlphaFold.", }, { - 'label': 'pLDTT', - 'name': 'MN_color_pLDTT', - 'description': 'Assigns colors using the `b_factor` attribute, which contains the `pLDTT` attribute for models that come from AlphaFold.' - }, - { - 'label': 'Secondary Structure', - 'name': 'MN_color_sec_struct', + "label": "Secondary Structure", + "name": "MN_color_sec_struct", "description": "Choose a color for the different secondary structures, based on the `sec_struct` attribute.", - "video_url": "https://imgur.com/wcJAUp9" + "video_url": "https://imgur.com/wcJAUp9", }, "break", { - 'label': 'Element', - 'name': 'MN_color_element', + "label": "Element", + "name": "MN_color_element", "description": "Choose a color for each of the first 20 elements on the periodic table. For higher atomic number elements use [`MN_color_atomic_number`](#color-atomic-number).", - "video_url": "https://imgur.com/iMGZKCx" + "video_url": "https://imgur.com/iMGZKCx", }, { - 'label': 'Atomic Number', - 'name': 'MN_color_atomic_number', + "label": "Atomic Number", + "name": "MN_color_atomic_number", "description": "Choose a color for an individual element. Select the element based on `atomic_number`. Useful for higher atomic number elements which are less commonly found in structures.", - "video_url": "https://imgur.com/pAloaAF" + "video_url": "https://imgur.com/pAloaAF", }, { - 'label': 'Res Name Peptide', - 'name': 'MN_color_res_name_peptide', + "label": "Res Name Peptide", + "name": "MN_color_res_name_peptide", "description": "Choose a color for each of the 20 naturally occurring amino acids. Non AA atoms will retain their currently set color.", - "video_url": "https://imgur.com/1yhSVsW" + "video_url": "https://imgur.com/1yhSVsW", }, { - 'label': 'Res Name Nucleic', - 'name': 'MN_color_res_name_nucleic', + "label": "Res Name Nucleic", + "name": "MN_color_res_name_nucleic", "description": "Choose a color for each of the nucleic acids. Non nucleic acid atoms will retain their currently set color.", - "video_url": "https://imgur.com/LpLZT3F" + "video_url": "https://imgur.com/LpLZT3F", }, - { - 'label': 'Element Common', - 'name': 'MN_color_common', + "label": "Element Common", + "name": "MN_color_common", "description": "Choose a color for each of the common elements. This is a smaller convenience node for elements which commonly appear in macromolecular structures. Use [`MN_color_element`](#color-element) for the first 20 elements and [`MN_color_atomic_number`](#color-atomic-number) for individual elements with higher atomic numbers.", - "video_url": "https://imgur.com/GhLdNwy" + "video_url": "https://imgur.com/GhLdNwy", }, ], - - 'topology': [ + "topology": [ { - 'label': 'Find Bonds', - 'name': 'MN_topo_bonds_find', - 'description': "Finds bonds between atoms based on distance. Based on the vdw_radii for each point, finds other points within a certain radius to create a bond to. Does not preserve the index for the points, detect bond type, or transfer all attributes", - 'video_url': 'https://imgur.com/oUo5TsM' + "label": "Find Bonds", + "name": "MN_topo_bonds_find", + "description": "Finds bonds between atoms based on distance. Based on the vdw_radii for each point, finds other points within a certain radius to create a bond to. Does not preserve the index for the points, detect bond type, or transfer all attributes", + "video_url": "https://imgur.com/oUo5TsM", }, { - 'label': 'Break Bonds', - 'name': 'MN_topo_bonds_break', - 'description': "Will delete a bond between atoms that already exists based on a distance cutoff, or is selected in the `Selection` input. Leaves the atoms unaffected", - 'video_url': 'https://imgur.com/n8cTN0k' + "label": "Break Bonds", + "name": "MN_topo_bonds_break", + "description": "Will delete a bond between atoms that already exists based on a distance cutoff, or is selected in the `Selection` input. Leaves the atoms unaffected", + "video_url": "https://imgur.com/n8cTN0k", }, { - 'label': 'Edge Info', - 'name': 'MN_topo_edge_info', - 'description': 'Get information for the selected edge, evaluated on the point domain. The "Edge Index" selects the edge from all possible connected edges. Edges are unfortunately stored somewhat randomly. The resulting information is between the evaluating point and the point that the edge is between. Point Index returns -1 if not connected.\n\nIn the video example, cones are instanced on each point where the Edge Index returns a valid connection. The Edge Vector can be used to align the instanced cone along that edge. The length of the edge can be used to scale the cone to the other point. As the "Edge Index" is changed, the selected edge changes. When "Edge Index" == 3, only the atoms with 4 connections are selected, which in this model (1BNA) are just the phosphates.', - 'video_url': "https://imgur.com/Ykyis3e" + "label": "Edge Info", + "name": "MN_topo_edge_info", + "description": 'Get information for the selected edge, evaluated on the point domain. The "Edge Index" selects the edge from all possible connected edges. Edges are unfortunately stored somewhat randomly. The resulting information is between the evaluating point and the point that the edge is between. Point Index returns -1 if not connected.\n\nIn the video example, cones are instanced on each point where the Edge Index returns a valid connection. The Edge Vector can be used to align the instanced cone along that edge. The length of the edge can be used to scale the cone to the other point. As the "Edge Index" is changed, the selected edge changes. When "Edge Index" == 3, only the atoms with 4 connections are selected, which in this model (1BNA) are just the phosphates.', + "video_url": "https://imgur.com/Ykyis3e", }, { - 'label': 'Edge Angle', - 'name': 'MN_topo_edge_angle', - 'description': ' Calculate the angle between two edges, selected with the edge indices. For molecule bonds, combinations of [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] will select all possible bond angles.\n\nIn the video example, two edges are selected with their "Edge Index" values. Those atoms which aren\'t valid return false and do not get instanced. The two edge vectors are used to calculate the perpendicular vector through cross product, around which the rotation for the cone is rotated. This demonstrates the ability to calculate the edge angle between the two selected edges.', - "video_url": "https://imgur.com/oQP6Cv8" + "label": "Edge Angle", + "name": "MN_topo_edge_angle", + "description": ' Calculate the angle between two edges, selected with the edge indices. For molecule bonds, combinations of [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] will select all possible bond angles.\n\nIn the video example, two edges are selected with their "Edge Index" values. Those atoms which aren\'t valid return false and do not get instanced. The two edge vectors are used to calculate the perpendicular vector through cross product, around which the rotation for the cone is rotated. This demonstrates the ability to calculate the edge angle between the two selected edges.', + "video_url": "https://imgur.com/oQP6Cv8", }, { - 'label': 'Connected Points for Edge Point', - 'name': 'MN_topo_edge_connected_points', - 'description': 'Finds the conntected point for the selected "Edge Index", and returns each point index for all of the points connected to that point. If the connection doesn\'t exist, or the connection is back to the original point, -1 is returned.\n\nIn the video example, a new point is selected based on the "Edge Index". At that point, all of the connecting points are exposed as indices `0, 1, 2, 3`. If that index is not a valid point or connection, or the point is the same as the original point that is being evaluated, then -1 is returned. \n\nThis is one of the more complicated topology nodes, but allows indexing of the atoms that are bonded to a bonded atom. This helps with doing calculations for planar molecules.', - 'video_url': 'https://imgur.com/fZ6srIS', + "label": "Connected Points for Edge Point", + "name": "MN_topo_edge_connected_points", + "description": 'Finds the conntected point for the selected "Edge Index", and returns each point index for all of the points connected to that point. If the connection doesn\'t exist, or the connection is back to the original point, -1 is returned.\n\nIn the video example, a new point is selected based on the "Edge Index". At that point, all of the connecting points are exposed as indices `0, 1, 2, 3`. If that index is not a valid point or connection, or the point is the same as the original point that is being evaluated, then -1 is returned. \n\nThis is one of the more complicated topology nodes, but allows indexing of the atoms that are bonded to a bonded atom. This helps with doing calculations for planar molecules.', + "video_url": "https://imgur.com/fZ6srIS", }, "break", { - 'label': 'Backbone Positions', - 'name': 'MN_topo_backbone', - 'description': 'If the atoms have been through the "Compute Backbone" node, then the backbone atom positions will be available as attributes through this node.\n\nIn the video example, the `Alpha Carbons` output is styled as spheres, where the position is mixed with some of the backbone posiitons. The backbone positions can also be selected from the AA residue higher or lower with the specified offset.', - 'video_url': 'https://imgur.com/6X2wnpY' + "label": "Backbone Positions", + "name": "MN_topo_backbone", + "description": 'If the atoms have been through the "Compute Backbone" node, then the backbone atom positions will be available as attributes through this node.\n\nIn the video example, the `Alpha Carbons` output is styled as spheres, where the position is mixed with some of the backbone posiitons. The backbone positions can also be selected from the AA residue higher or lower with the specified offset.', + "video_url": "https://imgur.com/6X2wnpY", }, { - 'label': 'Compute Backbone', - 'name': 'MN_topo_compute_backbone', - 'description': 'Gets the backbone positions for each AA residue and stores them as attributes, and additionally computes the phi and psi angles for each residue in radians.\n\nIn the video example, the Phi and Psi angles are mapped from (-Pi, Pi) to (0, 1), which is used in the Color Ramp node to choose colors. This is computed on the alpha carbons, but can be used on any of the resulting atoms for the corresponding residues, which is shown in the second video.', - 'video_url': ['https://imgur.com/9DNzngY', 'https://imgur.com/W3P9l10'] + "label": "Compute Backbone", + "name": "MN_topo_compute_backbone", + "description": "Gets the backbone positions for each AA residue and stores them as attributes, and additionally computes the phi and psi angles for each residue in radians.\n\nIn the video example, the Phi and Psi angles are mapped from (-Pi, Pi) to (0, 1), which is used in the Color Ramp node to choose colors. This is computed on the alpha carbons, but can be used on any of the resulting atoms for the corresponding residues, which is shown in the second video.", + "video_url": ["https://imgur.com/9DNzngY", "https://imgur.com/W3P9l10"], }, "break", { - 'label': '3-Point Angle', - 'name': 'MN_topo_angle_3point', - 'description': 'Calculate the angle between 3 different points. These points are selected based on their index in the point domain, with Index B being the centre of the calculation.\n\nIn the video example, the same calculation that is occurring internally inside of the `MN_topo_edge_angle` node, is being handled explicity by this node. If the `Index` is being used as `Index B` then the current point that is being evaluated is the centre of the angle calculation. If this value is changed, then the point at the corresponding index is used, which results in a smaller angle in the example video.', - 'video_url': 'https://imgur.com/qXyy2ln' + "label": "3-Point Angle", + "name": "MN_topo_angle_3point", + "description": "Calculate the angle between 3 different points. These points are selected based on their index in the point domain, with Index B being the centre of the calculation.\n\nIn the video example, the same calculation that is occurring internally inside of the `MN_topo_edge_angle` node, is being handled explicity by this node. If the `Index` is being used as `Index B` then the current point that is being evaluated is the centre of the angle calculation. If this value is changed, then the point at the corresponding index is used, which results in a smaller angle in the example video.", + "video_url": "https://imgur.com/qXyy2ln", }, { - 'label': '2-Point Angle', - 'name': 'MN_topo_angle_2point', - 'description': 'Calculate the angle that two points make, relative to the current point being evaluated. Points are selected based on their index, with the centre of the angle calculation being the current point\'s position. Equivalent to using 3-Point angle and using `Index` as the `Index B`.\n\nIn the example video, the angle calculation is similar to that of the 3-Point Angle node, but the middle point is always the current point.', - 'video_url': 'https://imgur.com/xp7Vbaj' + "label": "2-Point Angle", + "name": "MN_topo_angle_2point", + "description": "Calculate the angle that two points make, relative to the current point being evaluated. Points are selected based on their index, with the centre of the angle calculation being the current point's position. Equivalent to using 3-Point angle and using `Index` as the `Index B`.\n\nIn the example video, the angle calculation is similar to that of the 3-Point Angle node, but the middle point is always the current point.", + "video_url": "https://imgur.com/xp7Vbaj", }, { - 'label': 'Point Distance', - 'name': 'MN_topo_point_distance', - 'description': 'Calculate the distance and the vector between the evaluating point and the point selected via the Index.\n\nIn the example video, each point is calculating a vector and a distance between itself and the indexed point. When the Point Mask node is used, this index is then on a per-group basis, so each point in the group points to just the group\'s corresponding point.', - 'video_url': 'https://imgur.com/AykNvDz' + "label": "Point Distance", + "name": "MN_topo_point_distance", + "description": "Calculate the distance and the vector between the evaluating point and the point selected via the Index.\n\nIn the example video, each point is calculating a vector and a distance between itself and the indexed point. When the Point Mask node is used, this index is then on a per-group basis, so each point in the group points to just the group's corresponding point.", + "video_url": "https://imgur.com/AykNvDz", }, "break", { - 'label': 'Group Point Mask', - 'name': 'MN_topo_point_mask', - 'description': 'Returns the index for the atom for each unique group (from res_id) for each point in that group. Allows for example, all atoms in a group to be rotated around the position of the selected atom.\n\nIn the video example, the `atom_name` is used to select an atom within the groups. Each atom\'s position is then offset to that position, showing the group-wise selection.', - 'video_url': 'https://imgur.com/sD3jRTR' + "label": "Group Point Mask", + "name": "MN_topo_point_mask", + "description": "Returns the index for the atom for each unique group (from res_id) for each point in that group. Allows for example, all atoms in a group to be rotated around the position of the selected atom.\n\nIn the video example, the `atom_name` is used to select an atom within the groups. Each atom's position is then offset to that position, showing the group-wise selection.", + "video_url": "https://imgur.com/sD3jRTR", }, ], - - 'assembly': [ + "assembly": [ { - 'label': 'Biological Assembly', - 'name': 'mn.assembly_bio', - 'backup': 'MN_assembly_', + "label": "Biological Assembly", + "name": "mn.assembly_bio", + "backup": "MN_assembly_", "description": "Creates a biological assembly by applying rotation and translation matrices to individual chains in the structure. It is created on an individual molecule basis, if assembly instructions are detected when imported.", - "video_url": "https://imgur.com/6jyAP1z" + "video_url": "https://imgur.com/6jyAP1z", }, { - 'label': 'Center Assembly', - 'name': 'MN_assembly_center', + "label": "Center Assembly", + "name": "MN_assembly_center", "description": "Move an instanced assembly to the world origin. Some structures are not centred on the world origin, so this node can reset them to the world origin for convenient rotation and translation and animation.", - "video_url": "https://imgur.com/pgFTmgC" - } + "video_url": "https://imgur.com/pgFTmgC", + }, ], - - - 'DNA': [ + "DNA": [ { - 'label': 'Double Helix', - 'name': 'MN_dna_double_helix', - 'description': "Create a DNA double helix from an input curve.\nTakes an input curve and instances for the bases, returns instances of the bases in a double helix formation" + "label": "Double Helix", + "name": "MN_dna_double_helix", + "description": "Create a DNA double helix from an input curve.\nTakes an input curve and instances for the bases, returns instances of the bases in a double helix formation", }, { - 'label': 'Bases', - 'name': 'MN_dna_bases', - 'description': "Provide the DNA bases as instances to be styled and passed onto the Double Helix node" + "label": "Bases", + "name": "MN_dna_bases", + "description": "Provide the DNA bases as instances to be styled and passed onto the Double Helix node", }, "break", { - 'label': 'Style Spheres Cycles', - 'name': 'MN_dna_style_spheres_cycles', - 'description': "Style the DNA bases with spheres only visible in Cycles" + "label": "Style Spheres Cycles", + "name": "MN_dna_style_spheres_cycles", + "description": "Style the DNA bases with spheres only visible in Cycles", }, { - 'label': 'Style Spheres EEVEE', - 'name': 'MN_dna_style_spheres_eevee', - 'description': "Style the DNA bases with spheres visible in Cycles and EEVEE" + "label": "Style Spheres EEVEE", + "name": "MN_dna_style_spheres_eevee", + "description": "Style the DNA bases with spheres visible in Cycles and EEVEE", }, { - 'label': 'Style Surface', - 'name': 'MN_dna_style_surface', - 'description': "Style the DNA bases with surface representation" + "label": "Style Surface", + "name": "MN_dna_style_surface", + "description": "Style the DNA bases with surface representation", }, { - 'label': 'Style Ball and Stick', - 'name': 'MN_dna_style_ball_and_stick', - 'description': "Style the DNA bases with ball and stick representation" - } + "label": "Style Ball and Stick", + "name": "MN_dna_style_ball_and_stick", + "description": "Style the DNA bases with ball and stick representation", + }, ], - - 'animate': [ + "animate": [ { - 'label': 'Animate Frames', - 'name': 'MN_animate_frames', + "label": "Animate Frames", + "name": "MN_animate_frames", "description": "Animate the atoms of a structure, based on the frames of a trajectory from the `Frames` collection in the input. The structure animates through the trajectory from the given start frame to the given end frame, as the `Animate 0..1` value moves from `0` to `1`. Values higher than `1` start at the beginning again and the trajectory will loop repeating every `1.00`.\nPosition and `b_factor` are interpolated if available. By default linear interpolation is used. Smoothing in and out of each frame can be applied with the `Smoother Step`, or no interpolation at all.", - "video_url": "https://imgur.com/m3BPUxh" + "video_url": "https://imgur.com/m3BPUxh", }, { - 'label': 'Animate Value', - 'name': 'MN_animate_value', + "label": "Animate Value", + "name": "MN_animate_value", "description": "Animate a float value between the specified min and max values, over specified range of frames. If clamped, frames above and below the start and end will result in the min and max output values, otherwise it will continue to linearly interpolate the value beyond the min and max values.", - "video_url": "https://imgur.com/2oOnwRm" + "video_url": "https://imgur.com/2oOnwRm", }, "break", { - 'label': 'Res Wiggle', - 'name': 'MN_animate_res_wiggle', + "label": "Res Wiggle", + "name": "MN_animate_res_wiggle", "description": "Create a procedural animation of side-chain movement. 'Wiggles' the side-chains of peptide amino acids based on the `b_factor` attribute. Wiggle is currently only supported for protein side-chains and does not check for steric clashing so higher amplitudes will result in strange results. The animation should seamlessly loop every `1.00` of the `Animate 0..1` input.", - "video_url": "https://imgur.com/GK1nyUz" + "video_url": "https://imgur.com/GK1nyUz", }, { - 'label': 'Res to Curve', - 'name': 'MN_animate_res_to_curve', + "label": "Res to Curve", + "name": "MN_animate_res_to_curve", "description": "Take the protein residues from a structure and align then along an input curve. Editing the curve will change how the atoms are arranged. The output atoms can be styled as normal.", - "video_url": "https://imgur.com/FcEXSZx" + "video_url": "https://imgur.com/FcEXSZx", }, "break", { - 'label': 'Noise Position', - 'name': 'MN_animate_noise_position', + "label": "Noise Position", + "name": "MN_animate_noise_position", "description": "Create 3D noise vector based on the position of points in 3D space. Evolve the noise function with the `Animate` input, and change the characteristics of the noise function with the other inputs such as scale and detail. There is also a 1-dimensional noise output called `Fac`.\n\nAn example of using this noise is to offset the positions of atoms with the `Set Position` node.", - "video_url": "https://imgur.com/B8frW1C" + "video_url": "https://imgur.com/B8frW1C", }, { - 'label': 'Noise Field', - 'name': 'MN_animate_noise_field', + "label": "Noise Field", + "name": "MN_animate_noise_field", "description": "Create a 3D noise vector based on the input field. Evolve the noise function with the `Animate` input, and change the characteristics of the noise function with the other inputs such as scale and detail. There is also a 1-dimensional noise output called `Fac`.\n\nAn example of using this noise is to offset the positions of atoms with the `Set Position` node. Different field inputs result in different noise being applied. Using the `chain_id` results in the same noise being generated for each atom in each chain, but different between chains.", - "video_url": "https://imgur.com/hqemVQy" + "video_url": "https://imgur.com/hqemVQy", }, { - 'label': 'Noise Repeat', - 'name': 'MN_animate_noise_repeat', + "label": "Noise Repeat", + "name": "MN_animate_noise_repeat", "description": "Create a 3D noise vector based on the input field, that repeats every `1.00` for the `Animate 0..1` input. Evolve the noise function with the `Animate` input, and change the characteristics of the noise function with the other inputs such as scale and detail. There is also a 1-dimensional noise output called `Fac`.\n\nAn example of using this noise is to offset the positions of atoms with the `Set Position` node. Different field inputs result in different noise being applied. Using the `chain_id` results in the same noise being generated for each atom in each chain, but different between chains.", - "video_url": "https://imgur.com/GNQcIlx" - } + "video_url": "https://imgur.com/GNQcIlx", + }, ], - - 'utils': [ + "utils": [ { - 'label': 'Curve Resample', - 'name': 'MN_utils_curve_resample', - 'description': '' + "label": "Curve Resample", + "name": "MN_utils_curve_resample", + "description": "", }, { - 'label': 'Vector Angle', - 'name': 'MN_utils_vector_angle', - 'description': 'Compute the angle in radians between two vectors.' + "label": "Vector Angle", + "name": "MN_utils_vector_angle", + "description": "Compute the angle in radians between two vectors.", }, { - 'label': 'Vector Axis Angle', - 'name': 'MN_utils_vector_angle_axis', - 'description': 'Computes the angle between two vectors, AB & CD around around the axis of BC. The first vector AB is treated as the "12 O\'clock" up position, looking down the axis towards C, with angles being return in the range of (-Pi, Pi). Clockwise angles are positive and anti-clockwise angles are negative.', - 'video_url': '' + "label": "Vector Axis Angle", + "name": "MN_utils_vector_angle_axis", + "description": 'Computes the angle between two vectors, AB & CD around around the axis of BC. The first vector AB is treated as the "12 O\'clock" up position, looking down the axis towards C, with angles being return in the range of (-Pi, Pi). Clockwise angles are positive and anti-clockwise angles are negative.', + "video_url": "", }, # { # 'label': 'Determine Secondary Structure', @@ -494,48 +480,40 @@ # 'description': '' # }, { - 'label': 'Cartoon Utilities', - 'name': '.MN_utils_style_cartoon', - 'description': 'The underlying node group which powers the cartoon style' + "label": "Cartoon Utilities", + "name": ".MN_utils_style_cartoon", + "description": "The underlying node group which powers the cartoon style", }, { - 'label': 'Spheres Cycles', - 'name': '.MN_utils_style_spheres_cycles', - 'description': 'A sphere atom representation, visible ONLY in Cycles. Based on point-cloud rendering' + "label": "Spheres Cycles", + "name": ".MN_utils_style_spheres_cycles", + "description": "A sphere atom representation, visible ONLY in Cycles. Based on point-cloud rendering", }, { - 'label': 'Spheres EEVEE', - 'name': '.MN_utils_style_spheres_eevee', - 'description': 'A sphere atom representation, visible in EEVEE and Cycles. Based on mesh instancing which slows down viewport performance' - } - ], - - 'cellpack': [ - { - 'label': 'Pack Instances', - 'name': 'MN_pack_instances', - 'description': '' - } + "label": "Spheres EEVEE", + "name": ".MN_utils_style_spheres_eevee", + "description": "A sphere atom representation, visible in EEVEE and Cycles. Based on mesh instancing which slows down viewport performance", + }, ], - - 'density': [ + "cellpack": [{"label": "Pack Instances", "name": "MN_pack_instances", "description": ""}], + "density": [ { - 'label': 'Style Surface', - 'name': 'MN_density_style_surface', + "label": "Style Surface", + "name": "MN_density_style_surface", "description": "A surface made from the electron density given a certain threshold value.", - "video_url": "https://imgur.com/jGgMSd4" + "video_url": "https://imgur.com/jGgMSd4", }, { - 'label': 'Style Wire', - 'name': 'MN_density_style_wire', + "label": "Style Wire", + "name": "MN_density_style_wire", "description": "A wire surface made from the electron density given a certain threshold value.", - "video_url": "https://imgur.com/jGgMSd4" + "video_url": "https://imgur.com/jGgMSd4", }, { - 'label': 'Sample Nearest Attribute', - 'name': 'MN_density_sample_nearest', + "label": "Sample Nearest Attribute", + "name": "MN_density_sample_nearest", "description": "Sample the nearest atoms from another object, to get the colors or other attributes and apply them to a volume mesh.", - "video_url": "https://imgur.com/UzNwLv2" - } - ] + "video_url": "https://imgur.com/UzNwLv2", + }, + ], } diff --git a/molecularnodes/ui/node_menu.py b/molecularnodes/ui/node_menu.py index 6d330295..76b8b300 100644 --- a/molecularnodes/ui/node_menu.py +++ b/molecularnodes/ui/node_menu.py @@ -5,133 +5,129 @@ class MN_MT_Node_Color(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_COLOR' - bl_label = '' + bl_idname = "MN_MT_NODE_COLOR" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout - build_menu(layout, menu_items['color']) + build_menu(layout, menu_items["color"]) class MN_MT_Node_Bonds(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_BONDS' - bl_label = '' + bl_idname = "MN_MT_NODE_BONDS" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout - build_menu(layout, menu_items['bonds']) + build_menu(layout, menu_items["bonds"]) class MN_MT_Node_Style(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_STYLE' - bl_label = '' + bl_idname = "MN_MT_NODE_STYLE" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout layout.operator_context = "INVOKE_DEFAULT" - build_menu(layout, menu_items['style']) + build_menu(layout, menu_items["style"]) class MN_MT_Node_Select(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_SELECT' - bl_label = '' + bl_idname = "MN_MT_NODE_SELECT" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout layout.operator_context = "INVOKE_DEFAULT" - build_menu(layout, menu_items['select']) + build_menu(layout, menu_items["select"]) class MN_MT_Node_Assembly(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_ASSEMBLY' - bl_label = '' + bl_idname = "MN_MT_NODE_ASSEMBLY" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout layout.operator_context = "INVOKE_DEFAULT" - build_menu(layout, menu_items['assembly']) + build_menu(layout, menu_items["assembly"]) class MN_MT_Node_DNA(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_DNA' - bl_label = '' + bl_idname = "MN_MT_NODE_DNA" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout - build_menu(layout, menu_items['DNA']) + build_menu(layout, menu_items["DNA"]) class MN_MT_Node_Animate(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_ANIMATE' - bl_label = '' + bl_idname = "MN_MT_NODE_ANIMATE" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout - build_menu(layout, menu_items['animate']) + build_menu(layout, menu_items["animate"]) class MN_MT_Node_Utils(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_UTILS' - bl_label = '' + bl_idname = "MN_MT_NODE_UTILS" + bl_label = "" - def draw(self, context): - build_menu(self.layout, menu_items['utils']) + def draw(self, context: bpy.types.Context): + build_menu(self.layout, menu_items["utils"]) class MN_MT_Node_CellPack(bpy.types.Menu): bl_idname = "MN_MT_NODE_CELLPACK" - bl_label = '' + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout - build_menu(layout, menu_items['cellpack']) + build_menu(layout, menu_items["cellpack"]) class MN_MT_Node_Density(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_DENSITY' - bl_label = '' + bl_idname = "MN_MT_NODE_DENSITY" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout layout.operator_context = "INVOKE_DEFAULT" - build_menu(layout, menu_items['density']) + build_menu(layout, menu_items["density"]) class MN_MT_Node_Topology(bpy.types.Menu): - bl_idname = 'MN_MT_NODE_TOPOLOGY' - bl_label = '' + bl_idname = "MN_MT_NODE_TOPOLOGY" + bl_label = "" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout layout.operator_context = "INVOKE_DEFAULT" - build_menu(layout, menu_items['topology']) + build_menu(layout, menu_items["topology"]) class MN_MT_Node(bpy.types.Menu): bl_idname = "MN_MT_NODE" bl_label = "Menu for Adding Nodes in GN Tree" - def draw(self, context): + def draw(self, context: bpy.types.Context): layout = self.layout.column_flow(columns=1) layout.operator_context = "INVOKE_DEFAULT" - layout.menu('MN_MT_NODE_STYLE', text='Style', icon_value=77) - layout.menu('MN_MT_NODE_SELECT', text='Selection', icon_value=256) - layout.menu('MN_MT_NODE_COLOR', text='Color', icon='COLORSET_07_VEC') - layout.menu('MN_MT_NODE_ANIMATE', text='Animation', icon_value=409) - layout.menu('MN_MT_NODE_TOPOLOGY', text='Topology', - icon='ORIENTATION_CURSOR') - layout.menu('MN_MT_NODE_ASSEMBLY', - text='Assemblies', icon='GROUP_VERTEX') - layout.menu('MN_MT_NODE_CELLPACK', - text='CellPack', icon='PARTICLE_POINT') - layout.menu('MN_MT_NODE_DENSITY', text='Density', icon="VOLUME_DATA") - layout.menu('MN_MT_NODE_DNA', text='DNA', - icon='GP_SELECT_BETWEEN_STROKES') - layout.menu('MN_MT_NODE_UTILS', text='Utilities', icon_value=92) - - -def MN_add_node_menu(self, context): - if ('GeometryNodeTree' == bpy.context.area.spaces[0].tree_type): + layout.menu("MN_MT_NODE_STYLE", text="Style", icon_value=77) + layout.menu("MN_MT_NODE_SELECT", text="Selection", icon_value=256) + layout.menu("MN_MT_NODE_COLOR", text="Color", icon="COLORSET_07_VEC") + layout.menu("MN_MT_NODE_ANIMATE", text="Animation", icon_value=409) + layout.menu("MN_MT_NODE_TOPOLOGY", text="Topology", icon="ORIENTATION_CURSOR") + layout.menu("MN_MT_NODE_ASSEMBLY", text="Assemblies", icon="GROUP_VERTEX") + layout.menu("MN_MT_NODE_CELLPACK", text="CellPack", icon="PARTICLE_POINT") + layout.menu("MN_MT_NODE_DENSITY", text="Density", icon="VOLUME_DATA") + layout.menu("MN_MT_NODE_DNA", text="DNA", icon="GP_SELECT_BETWEEN_STROKES") + layout.menu("MN_MT_NODE_UTILS", text="Utilities", icon_value=92) + + +def MN_add_node_menu(self, context: bpy.types.Context): + if "GeometryNodeTree" == bpy.context.area.spaces[0].tree_type: layout = self.layout - layout.menu('MN_MT_NODE', text='Molecular Nodes', icon_value=88) + layout.menu("MN_MT_NODE", text="Molecular Nodes", icon_value=88) diff --git a/molecularnodes/ui/ops.py b/molecularnodes/ui/ops.py index 9fe943f6..e8995e01 100644 --- a/molecularnodes/ui/ops.py +++ b/molecularnodes/ui/ops.py @@ -1,5 +1,6 @@ import bpy from ..blender import nodes +from typing import Set class MN_OT_Add_Custom_Node_Group(bpy.types.Operator): @@ -7,33 +8,29 @@ class MN_OT_Add_Custom_Node_Group(bpy.types.Operator): bl_label = "Add Custom Node Group" # bl_description = "Add Molecular Nodes custom node group." bl_options = {"REGISTER", "UNDO"} - node_name: bpy.props.StringProperty( - name='node_name', - description='', - default='', - subtype='NONE', - maxlen=0 - ) - node_label: bpy.props.StringProperty(name='node_label', default='') - node_description: bpy.props.StringProperty( + node_name: bpy.props.StringProperty(name="node_name", description="", default="", subtype="NONE", maxlen=0) # type: ignore + node_label: bpy.props.StringProperty(name="node_label", default="") # type: ignore + node_description: bpy.props.StringProperty( # type: ignore name="node_description", description="", default="Add MolecularNodes custom node group.", - subtype="NONE" + subtype="NONE", ) - node_link: bpy.props.BoolProperty(name='node_link', default=True) + node_link: bpy.props.BoolProperty(name="node_link", default=True) # type: ignore @classmethod - def description(cls, context, properties): - return properties.node_description + def description(cls, context: bpy.types.Context, properties: bpy.types.PropertyGroup) -> str: + return str(properties.node_description) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: try: nodes.append(self.node_name, link=self.node_link) nodes.add_node(self.node_name) # , label=self.node_label) except RuntimeError: - self.report({'ERROR'}, - message='Failed to add node. Ensure you are not in edit mode.') + self.report( + {"ERROR"}, + message="Failed to add node. Ensure you are not in edit mode.", + ) return {"FINISHED"} @@ -47,11 +44,11 @@ class MN_OT_Assembly_Bio(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(self, context): + def poll(self, context: bpy.types.Context) -> bool: mol = context.active_object - return mol.mn['molecule_type'] in ['pdb', 'local'] + return mol.mn["molecule_type"] in ["pdb", "local"] - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: tree_assembly = nodes.assembly_initialise(context.active_object) nodes.add_node(tree_assembly.name) @@ -66,8 +63,7 @@ class MN_OT_Color_Custom(bpy.types.Operator): description: bpy.props.StringProperty(name="description", default="") node_name: bpy.props.StringProperty(name="node_name", default="") - node_property: bpy.props.StringProperty( - name="node_property", default="chain_ids") + node_property: bpy.props.StringProperty(name="node_property", default="chain_ids") field: bpy.props.StringProperty(name="field", default="chain_id") prefix: bpy.props.StringProperty(name="prefix", default="Chain") starting_value: bpy.props.IntProperty(name="starting_value", default=0) @@ -76,18 +72,20 @@ class MN_OT_Color_Custom(bpy.types.Operator): def description(cls, context, properties): return properties.description - def execute(self, context): + def execute(self, context: bpy.types.Context): object = context.active_object prop = object[self.node_property] if not prop: self.report( - {"WARNING"}, message=f"{self.node_property} not available for {object.name}.") + {"WARNING"}, + message=f"{self.node_property} not available for {object.name}.", + ) return {"CANCELLED"} node_color = nodes.custom_iswitch( name=f"MN_color_{self.node_name}_{object.name}", iter_list=prop, - dtype='RGBA', + dtype="RGBA", field=self.field, prefix=self.prefix, start=self.starting_value, @@ -106,8 +104,7 @@ class MN_OT_selection_custom(bpy.types.Operator): description: bpy.props.StringProperty(name="Description") field: bpy.props.StringProperty(name="field", default="chain_id") prefix: bpy.props.StringProperty(name="prefix", default="Chain ") - node_property: bpy.props.StringProperty( - name="node_property", default="chain_ids") + node_property: bpy.props.StringProperty(name="node_property", default="chain_ids") node_name: bpy.props.StringProperty(name="node_name", default="chain") starting_value: bpy.props.IntProperty(name="starting_value", default=0) @@ -115,22 +112,24 @@ class MN_OT_selection_custom(bpy.types.Operator): def description(cls, context, properties): return properties.description - def execute(self, context): + def execute(self, context: bpy.types.Context): object = context.view_layer.objects.active prop = object[self.node_property] name = object.name if not prop: self.report( - {"WARNING"}, message=f"{self.node_property} not available for {object.name}.") + {"WARNING"}, + message=f"{self.node_property} not available for {object.name}.", + ) return {"CANCELLED"} node_chains = nodes.custom_iswitch( - name=f'MN_select_{self.node_name}_{name}', - dtype='BOOLEAN', + name=f"MN_select_{self.node_name}_{name}", + dtype="BOOLEAN", iter_list=prop, start=self.starting_value, field=self.field, - prefix=self.prefix + prefix=self.prefix, ) nodes.add_node(node_chains.name) @@ -149,12 +148,12 @@ class MN_OT_Residues_Selection_Custom(bpy.types.Operator): input_resid_string: bpy.props.StringProperty( name="Select residue IDs: ", description="Enter a string value.", - default="19,94,1-16" - ) + default="19,94,1-16", + ) # type: ignore - def execute(self, context): + def execute(self, context: bpy.types.Context): node_residues = nodes.resid_multiple_selection( - node_name='MN_select_res_id_custom', + node_name="MN_select_res_id_custom", input_resid_string=self.input_resid_string, ) diff --git a/molecularnodes/ui/panel.py b/molecularnodes/ui/panel.py index 3556b3df..22a4671f 100644 --- a/molecularnodes/ui/panel.py +++ b/molecularnodes/ui/panel.py @@ -1,103 +1,97 @@ import bpy +from typing import List, Set from .. import pkg from ..blender import nodes -from ..io import ( - wwpdb, local, star, cellpack, md, density, dna -) +from ..io import wwpdb, local, star, cellpack, md, density, dna bpy.types.Scene.MN_panel = bpy.props.EnumProperty( name="Panel Selection", items=( - ('import', "Import", "Import macromolecules", 0), - ('object', "Object", "Adjust settings affecting the selected object", 1), - ('scene', "Scene", "Change settings for the world and rendering", 2) - ) + ("import", "Import", "Import macromolecules", 0), + ("object", "Object", "Adjust settings affecting the selected object", 1), + ("scene", "Scene", "Change settings for the world and rendering", 2), + ), ) bpy.types.Scene.MN_panel_import = bpy.props.EnumProperty( name="Method", items=( - ('pdb', "PDB", "Download from the PDB", 0), - ('local', "Local", "Open a local file", 1), - ('md', "MD", "Import a molecular dynamics trajectory", 2), - ('density', "Density", "Import an EM Density Map", 3), - ('star', 'Starfile', "Import a .starfile mapback file", 4), - ('cellpack', 'CellPack', "Import a CellPack .cif/.bcif file", 5), - ('dna', 'oxDNA', 'Import an oxDNA file', 6) - ) + ("pdb", "PDB", "Download from the PDB", 0), + ("local", "Local", "Open a local file", 1), + ("md", "MD", "Import a molecular dynamics trajectory", 2), + ("density", "Density", "Import an EM Density Map", 3), + ("star", "Starfile", "Import a .starfile mapback file", 4), + ("cellpack", "CellPack", "Import a CellPack .cif/.bcif file", 5), + ("dna", "oxDNA", "Import an oxDNA file", 6), + ), ) chosen_panel = { - 'pdb': wwpdb, - 'local': local, - 'star': star, - 'md': md, - 'density': density, - 'cellpack': cellpack, - 'dna': dna - + "pdb": wwpdb, + "local": local, + "star": star, + "md": md, + "density": density, + "cellpack": cellpack, + "dna": dna, } packages = { - 'pdb': ['biotite'], - 'star': ['starfile','mrcfile','pillow'], - 'local': ['biotite'], - 'cellpack': ['biotite', 'msgpack'], - 'md': ['MDAnalysis'], - 'density': ['mrcfile'], - 'dna': [] + "pdb": ["biotite"], + "star": ["starfile", "mrcfile", "pillow"], + "local": ["biotite"], + "cellpack": ["biotite", "msgpack"], + "md": ["MDAnalysis"], + "density": ["mrcfile"], + "dna": [], } -class MN_OT_Change_Style(bpy.types.Operator): - bl_idname = 'mn.style_change' - bl_label = 'Style' +class MN_OT_Change_Style(bpy.types.Operator): # type: ignore + bl_idname = "mn.style_change" + bl_label = "Style" - style: bpy.props.EnumProperty( - name="Style", - items=nodes.STYLE_ITEMS - ) + style: bpy.props.EnumProperty(name="Style", items=nodes.STYLE_ITEMS) # type: ignore - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: object = context.active_object nodes.change_style_node(object, self.style) - return {'FINISHED'} + return {"FINISHED"} -def check_installs(selection): - for package in packages[selection]: +def check_installs(selection: List[str]) -> bool: + for package in packages[selection]: # type: ignore if not pkg.is_current(package): return False return True -def panel_import(layout, context): +def panel_import(layout: bpy.types.UILayout, context: bpy.types.Context) -> bpy.types.UILayout: scene = context.scene selection = scene.MN_panel_import - layout.prop(scene, 'MN_panel_import') + layout.prop(scene, "MN_panel_import") install_required = not check_installs(selection) buttons = layout.column(align=True) if install_required: - buttons.label(text='Please install the requried packages.') - for package in packages[selection]: - pkg.button_install_pkg(buttons, package, pkg.get_pkgs()[ - package]['version']) + buttons.label(text="Please install the requried packages.") + for package in packages[selection]: # type: ignore + pkg.button_install_pkg(buttons, package, pkg.get_pkgs()[package]["version"]) col = layout.column() col.enabled = not install_required chosen_panel[selection].panel(col, scene) -def ui_from_node(layout, node): +def ui_from_node(layout: bpy.types.UILayout, node: bpy.types.GeometryNode) -> bpy.types.UILayout: """ Generate the UI for a particular node, which displays the relevant node inputs for user control in a panel, rather than through the node editor. """ col = layout.column(align=True) - ntree = bpy.context.active_object.modifiers['MolecularNodes'].node_group + ntree = bpy.context.active_object.modifiers["MolecularNodes"].node_group tree = node.node_tree.interface.items_tree @@ -114,7 +108,7 @@ def ui_from_node(layout, node): col.template_node_view(ntree, node, node.inputs[item.identifier]) -def panel_object(layout, context): +def panel_object(layout: bpy.types.UILayout, context: bpy.types.Context) -> bpy.types.UILayout: object = context.active_object mol_type = object.mn.molecule_type if mol_type == "": @@ -124,26 +118,25 @@ def panel_object(layout, context): if mol_type == "pdb": layout.label(text=f"PDB: {object.mn.pdb_code.upper()}") if mol_type == "md": - layout.prop(object.mn, 'subframes') + layout.prop(object.mn, "subframes") if mol_type == "star": - layout.label(text=f"Ensemble") + layout.label(text="Ensemble") box = layout.box() ui_from_node(box, nodes.get_star_node(object)) return row = layout.row(align=True) row.label(text="Style") - current_style = nodes.format_node_name( - nodes.get_style_node(object).node_tree.name).replace("Style ", "") - row.operator_menu_enum('mn.style_change', 'style', text=current_style) + current_style = nodes.format_node_name(nodes.get_style_node(object).node_tree.name).replace("Style ", "") + row.operator_menu_enum("mn.style_change", "style", text=current_style) box = layout.box() ui_from_node(box, nodes.get_style_node(object)) row = layout.row() row.label(text="Experimental", icon_value=2) - row.operator('mn.add_armature') + row.operator("mn.add_armature") -def panel_scene(layout, context): +def panel_scene(layout: bpy.types.UILayout, context: bpy.types.Context) -> bpy.types.UILayout: scene = context.scene cam = bpy.data.cameras[bpy.data.scenes["Scene"].camera.name] @@ -158,10 +151,10 @@ def panel_scene(layout, context): else: world.prop(bpy.data.scenes["Scene"].eevee, "taa_render_samples") world.label(text="Background") - world.prop(world_shader.inputs[1], 'default_value', text='HDRI Strength') + world.prop(world_shader.inputs[1], "default_value", text="HDRI Strength") row = world.row() - row.prop(scene.render, 'film_transparent') - row.prop(world_shader.inputs[2], 'default_value', text="Background") + row.prop(scene.render, "film_transparent") + row.prop(world_shader.inputs[2], "default_value", text="Background") col = grid.column() col.label(text="Camera Settings") @@ -172,39 +165,39 @@ def panel_scene(layout, context): row.prop(bpy.data.scenes["Scene"].render, "resolution_x", text="X") row.prop(bpy.data.scenes["Scene"].render, "resolution_y", text="Y") row = camera.grid_flow() - row.prop(cam.dof, 'use_dof') + row.prop(cam.dof, "use_dof") row.prop(bpy.data.scenes["Scene"].render, "use_motion_blur") focus = camera.column() focus.enabled = cam.dof.use_dof - focus.prop(cam.dof, 'focus_object') + focus.prop(cam.dof, "focus_object") distance = focus.row() - distance.enabled = (cam.dof.focus_object is None) - distance.prop(cam.dof, 'focus_distance') - focus.prop(cam.dof, 'aperture_fstop') + distance.enabled = cam.dof.focus_object is None + distance.prop(cam.dof, "focus_distance") + focus.prop(cam.dof, "aperture_fstop") -class MN_PT_panel(bpy.types.Panel): - bl_label = 'Molecular Nodes' - bl_idname = 'MN_PT_panel' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'scene' +class MN_PT_panel(bpy.types.Panel): # type: ignore + bl_label = "Molecular Nodes" + bl_idname = "MN_PT_panel" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" bl_order = 0 - bl_options = {'HEADER_LAYOUT_EXPAND'} + bl_options = {"HEADER_LAYOUT_EXPAND"} bl_ui_units_x = 0 - def draw(self, context): + def draw(self, context: bpy.types.Context) -> None: layout = self.layout scene = context.scene row = layout.row(align=True) - for p in ['import', 'object', 'scene']: - row.prop_enum(scene, 'MN_panel', p) + for p in ["import", "object", "scene"]: + row.prop_enum(scene, "MN_panel", p) # the possible panel functions to choose between which_panel = { "import": panel_import, "scene": panel_scene, - "object": panel_object + "object": panel_object, } # call the required panel function with the layout and context which_panel[scene.MN_panel](layout, context) diff --git a/molecularnodes/ui/pref.py b/molecularnodes/ui/pref.py index 68079fc9..f7324d56 100644 --- a/molecularnodes/ui/pref.py +++ b/molecularnodes/ui/pref.py @@ -1,17 +1,19 @@ -import bpy import pathlib -from .. import pkg + +import bpy from bpy.types import AddonPreferences +from .. import pkg + install_instructions = "https://bradyajohnston.github.io/MolecularNodes/installation.html#installing-biotite-mdanalysis" ADDON_DIR = pathlib.Path(__file__).resolve().parent.parent bpy.types.Scene.pypi_mirror_provider = bpy.props.StringProperty( - name='pypi_mirror_provider', - description='PyPI Mirror Provider', - options={'TEXTEDIT_UPDATE', 'LIBRARY_EDITABLE'}, - default='Default', - subtype='NONE', + name="pypi_mirror_provider", + description="PyPI Mirror Provider", + options={"TEXTEDIT_UPDATE", "LIBRARY_EDITABLE"}, + default="Default", + subtype="NONE", search=pkg.get_pypi_mirror_alias, ) @@ -19,17 +21,16 @@ # installing and reinstalling the required python packages defined in 'requirements.txt' -class MolecularNodesPreferences(AddonPreferences): - bl_idname = 'molecularnodes' +class MolecularNodesPreferences(AddonPreferences): # type: ignore + bl_idname = "molecularnodes" - def draw(self, context): + def draw(self, context: bpy.types.Context) -> None: layout = self.layout layout.label(text="Install the required packages for MolecularNodes.") - col_main = layout.column(heading='', align=False) + col_main = layout.column(heading="", align=False) row_import = col_main.row() - row_import.prop(bpy.context.scene, - 'pypi_mirror_provider', text='Set PyPI Mirror') + row_import.prop(bpy.context.scene, "pypi_mirror_provider", text="Set PyPI Mirror") pkgs = pkg.get_pkgs() for package in pkgs.values(): @@ -38,7 +39,7 @@ def draw(self, context): row = col.row() pkg.button_install_pkg( layout=row, - name=package.get('name'), - version=package.get('version'), - desc=package.get('desc') + name=package["name"], + version=package["version"], + desc=package["desc"], ) diff --git a/molecularnodes/utils.py b/molecularnodes/utils.py index e38a24af..3ffe203c 100644 --- a/molecularnodes/utils.py +++ b/molecularnodes/utils.py @@ -1,10 +1,11 @@ -import bpy -import traceback import os +import traceback import zipfile + +import bpy import numpy as np -from mathutils import Matrix from bpy.app.translations import pgettext_tip as tip_ +from mathutils import Matrix from .ui.pref import ADDON_DIR @@ -53,6 +54,7 @@ def _module_filesystem_remove(path_base, module_name): # The `module_name` is expected to be a result from `_zipfile_root_namelist`. import os import shutil + module_name = os.path.splitext(module_name)[0] for f in os.listdir(path_base): f_base = os.path.splitext(f)[0] @@ -68,6 +70,7 @@ def _zipfile_root_namelist(file_to_extract): # taken from the bpy.ops.preferences.app_template_install() operator source code # Return a list of root paths from zipfile.ZipFile.namelist. import os + root_paths = [] for f in file_to_extract.namelist(): # Python's `zipfile` API always adds a separate at the end of directories. @@ -85,26 +88,26 @@ def _zipfile_root_namelist(file_to_extract): def template_install(): print(os.path.abspath(ADDON_DIR)) - template = os.path.join(os.path.abspath(ADDON_DIR), - 'assets', 'template', 'Molecular Nodes.zip') + template = os.path.join(os.path.abspath(ADDON_DIR), "assets", "template", "Molecular Nodes.zip") _install_template(template) bpy.utils.refresh_script_paths() def template_uninstall(): import shutil + for folder in bpy.utils.app_template_paths(): - path = os.path.join(os.path.abspath(folder), 'MolecularNodes') + path = os.path.join(os.path.abspath(folder), "MolecularNodes") if os.path.exists(path): shutil.rmtree(path) bpy.utils.refresh_script_paths() -def _install_template(filepath, subfolder='', overwrite=True): +def _install_template(filepath, subfolder="", overwrite=True): # taken from the bpy.ops.preferences.app_template_install() operator source code path_app_templates = bpy.utils.user_resource( - 'SCRIPTS', + "SCRIPTS", path=os.path.join("startup", "bl_app_templates_user", subfolder), create=True, ) @@ -112,18 +115,30 @@ def _install_template(filepath, subfolder='', overwrite=True): if not os.path.isdir(path_app_templates): try: os.makedirs(path_app_templates, exist_ok=True) - except: + except PermissionError: + print("Permission denied: You do not have the necessary permissions to create the directory.") + traceback.print_exc() + except OSError as e: + print(f"OS error: {e}") traceback.print_exc() app_templates_old = set(os.listdir(path_app_templates)) - # check to see if the file is in compressed format (.zip) if zipfile.is_zipfile(filepath): try: - file_to_extract = zipfile.ZipFile(filepath, 'r') - except: + file_to_extract = zipfile.ZipFile(filepath, "r") + except FileNotFoundError: + print("File not found: The specified file does not exist.") traceback.print_exc() - return {'CANCELLED'} + return {"CANCELLED"} + except PermissionError: + print("Permission denied: You do not have the necessary permissions to open the file.") + traceback.print_exc() + return {"CANCELLED"} + except zipfile.BadZipFile: + print("Bad zip file: The file is not a zip file or it is corrupted.") + traceback.print_exc() + return {"CANCELLED"} file_to_extract_root = _zipfile_root_namelist(file_to_extract) if overwrite: @@ -131,22 +146,26 @@ def _install_template(filepath, subfolder='', overwrite=True): _module_filesystem_remove(path_app_templates, f) else: for f in file_to_extract_root: - path_dest = os.path.join( - path_app_templates, os.path.basename(f)) + path_dest = os.path.join(path_app_templates, os.path.basename(f)) if os.path.exists(path_dest): # self.report({'WARNING'}, tip_("File already installed to %r\n") % path_dest) - return {'CANCELLED'} + return {"CANCELLED"} try: # extract the file to "bl_app_templates_user" file_to_extract.extractall(path_app_templates) - except: + except PermissionError: + print("Permission denied: You do not have the necessary permissions to write to the directory.") + traceback.print_exc() + return {"CANCELLED"} + except OSError as e: + print(f"OS error: {e}") traceback.print_exc() - return {'CANCELLED'} + return {"CANCELLED"} else: # Only support installing zipfiles - print('no zipfile') - return {'CANCELLED'} + print("no zipfile") + return {"CANCELLED"} app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old @@ -154,24 +173,25 @@ def _install_template(filepath, subfolder='', overwrite=True): bpy.utils.refresh_script_paths() # print message - msg = ( - tip_("Template Installed (%s) from %r into %r") % - (", ".join(sorted(app_templates_new)), filepath, path_app_templates) + msg = tip_("Template Installed (%s) from %r into %r") % ( + ", ".join(sorted(app_templates_new)), + filepath, + path_app_templates, ) print(msg) # data types for the np.array that will store per-chain symmetry operations dtype = [ - ('assembly_id', int), - ('transform_id', int), - ('chain_id', 'U10'), - ('rotation', float, 4), # quaternion form - ('translation', float, 3) + ("assembly_id", int), + ("transform_id", int), + ("chain_id", "U10"), + ("rotation", float, 4), # quaternion form + ("translation", float, 3), ] -def array_quaternions_from_dict(transforms_dict): +def array_quaternions_from_dict(transforms_dict: dict) -> np.ndarray: n_transforms = 0 for assembly in transforms_dict.values(): for transform in assembly: @@ -186,11 +206,11 @@ def array_quaternions_from_dict(transforms_dict): matrix = transform[1] arr = np.zeros((len(chains)), dtype=dtype) translation, rotation, scale = Matrix(matrix).decompose() - arr['assembly_id'] = i + 1 - arr['transform_id'] = j - arr['chain_id'] = chains - arr['rotation'] = rotation - arr['translation'] = translation + arr["assembly_id"] = i + 1 + arr["transform_id"] = j + arr["chain_id"] = chains + arr["rotation"] = rotation + arr["translation"] = translation transforms.append(arr) return np.hstack(transforms) diff --git a/pyproject.toml b/pyproject.toml index d16126f2..543d0d43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ documentation = "https://bradyajohnston.github.io/MolecularNodes" [tool.poetry.dependencies] python = "~=3.11.0" -bpy = "~=4.1" +# bpy = "~=4.1" MDAnalysis = "~=2.7.0" biotite = "==0.40.0" mrcfile = "==1.4.3" @@ -26,6 +26,8 @@ pytest-cov = "*" syrupy = "*" quartodoc = "*" scipy = "*" +mypy = "*" +ruff = "*" [build-system] @@ -33,3 +35,46 @@ requires = ["poetry-core>=1.1.0"] build-backend = "poetry.core.masonry.api" [tool.setuptools_scm] + +[tool.mypy] +strict = true +ignore_missing_imports = true +plugins = "numpy.typing.mypy_plugin" + + +[tool.ruff] +# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. +lint.select = ["E", "F"] +lint.ignore = ["E501"] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +lint.fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] +lint.unfixable = [] + +# Exclude a variety of commonly ignored directories. +lint.exclude = [ + "*auto_load.py", + "tests", + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv"] + +line-length = 120 + diff --git a/tests/__snapshots__/test_ops.ambr b/tests/__snapshots__/test_ops.ambr index e74455fa..d41ed712 100644 --- a/tests/__snapshots__/test_ops.ambr +++ b/tests/__snapshots__/test_ops.ambr @@ -1,612 +1,44 @@ # serializer version: 1 -# name: test_op_api_cartoon[1BNA] - [29.5 23.6 29.0 33.5 38.1 34.0 36.6 26.9 37.5 28.6 23.4 33.0 29.6 27.4 - 50.9 34.0 35.3 39.6 31.8 33.9 28.5 42.5 42.5 40.4 34.6 27.7 35.6 31.6 - 36.0 28.7 28.5 38.8 29.7 32.6 26.4 45.3 40.5 26.4 29.2 41.2 36.6 38.0 - 32.5 33.9 28.7 37.6 42.7 27.7 32.0 28.6 32.3 38.5 29.2 31.8 33.9 39.1 - 29.2 39.1 28.7 37.4 34.8 37.6 29.2 35.3 28.0 46.7 46.0 30.1 28.6 29.3 - 26.4 37.4 36.5 40.8 33.9 29.9 27.7 38.8 49.3 26.1 33.9 26.6 29.0 31.5 - 27.5 28.3 30.6 40.8 35.7 35.6 34.5 34.9 35.7 39.1 44.0 33.8 37.0 31.6 - 31.6 47.6] -# --- -# name: test_op_api_cartoon[1BNA].1 - AttributeError("The selected attribute 'occupancy' does not exist on the mesh.") +# name: test_op_api_cartoon[4ozs] + [66.0 36.4 44.1 44.5 39.7 55.8 44.4 65.4 41.1 42.6 39.8 57.1 45.3 38.4 + 50.7 36.7 35.7 52.8 34.2 58.8 45.5 36.7 47.0 51.6 60.7 51.6 42.8 54.3 + 45.4 77.8 52.2 36.5 37.4 52.4 51.3 43.1 43.1 44.6 33.7 49.4 65.3 44.7 + 43.0 37.1 63.1 34.1 50.7 61.7 50.6 50.1 37.1 40.3 60.6 40.2 37.2 35.6 + 42.2 48.6 69.3 40.2 50.2 40.8 41.8 40.3 50.1 41.3 55.3 46.0 32.5 42.9 + 43.8 35.7 71.2 40.5 48.8 45.3 47.7 40.3 47.0 64.7 54.6 79.5 37.1 50.5 + 76.1 74.5 42.2 36.8 50.6 56.3 64.4 35.2 57.6 40.0 35.2 36.7 76.1 44.7 + 48.3 42.7] # --- -# name: test_op_api_cartoon[1BNA].10 - AttributeError("The selected attribute 'atom_id' does not exist on the mesh.") +# name: test_op_api_cartoon[4ozs].1 + [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. + 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. + 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. + 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. + 1. 1. 1. 1.] +# --- +# name: test_op_api_cartoon[4ozs].10 + [1366 194 1156 629 87 2 354 1120 892 764 55 649 1165 474 + 982 1275 1278 232 459 1345 1318 636 216 1008 803 229 866 973 + 602 1064 230 470 691 241 107 482 860 1161 460 1215 1195 70 + 862 585 1023 722 851 290 220 1189 1246 477 284 639 743 469 + 908 1154 535 336 506 1237 29 448 398 44 800 1330 727 895 + 903 1252 924 685 305 1165 119 439 1002 941 236 1054 587 219 + 1067 1070 908 183 1222 1013 1121 719 970 210 456 636 1076 1163 + 408 47] # --- -# name: test_op_api_cartoon[1BNA].11 - AttributeError("The selected attribute 'atom_name' does not exist on the mesh.") +# name: test_op_api_cartoon[4ozs].11 + [2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2] # --- -# name: test_op_api_cartoon[1BNA].12 - [[0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0]] -# --- -# name: test_op_api_cartoon[1BNA].13 - [[ 0.2 0.3 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.0] - [ 0.2 0.2 0.3] - [ 0.1 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.1 0.2] - [ 0.1 0.2 0.2] - [ 0.1 0.3 0.1] - [ 0.2 0.2 -0.0] - [ 0.2 0.2 0.3] - [ 0.1 0.2 0.3] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 -0.0] - [ 0.2 0.2 0.3] - [ 0.2 0.3 0.2] - [ 0.1 0.2 0.0] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.3] - [ 0.1 0.1 -0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.1 0.2 0.1] - [ 0.2 0.3 0.0] - [ 0.2 0.3 0.0] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.3] - [ 0.1 0.2 0.0] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.1 0.1 -0.1] - [ 0.1 0.3 -0.0] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.1] - [ 0.1 0.1 -0.1] - [ 0.1 0.2 0.0] - [ 0.2 0.1 -0.0] - [ 0.2 0.2 0.3] - [ 0.1 0.1 0.1] - [ 0.1 0.2 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.2 -0.0] - [ 0.2 0.2 0.2] - [ 0.2 0.3 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.1] - [ 0.2 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.1 0.3 0.1] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.3 0.1] - [ 0.2 0.1 -0.1] - [ 0.1 0.3 -0.0] - [ 0.2 0.3 -0.1] - [ 0.1 0.3 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.1] - [ 0.2 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.1 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.3] - [ 0.1 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.1 0.2 0.1] - [ 0.1 0.3 -0.1] - [ 0.2 0.3 0.0] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.1] - [ 0.1 0.3 0.1] - [ 0.1 0.3 -0.0]] -# --- -# name: test_op_api_cartoon[1BNA].14 - AttributeError("The selected attribute 'is_backbone' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].15 - AttributeError("The selected attribute 'is_alpha_carbon' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].16 - AttributeError("The selected attribute 'is_solvent' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].17 - AttributeError("The selected attribute 'is_nucleic' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].18 - AttributeError("The selected attribute 'is_peptide' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].19 - AttributeError("The selected attribute 'is_hetero' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].2 - AttributeError("The selected attribute 'vdw_radii' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].20 - AttributeError("The selected attribute 'is_carb' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].21 - AttributeError("The selected attribute 'bond_type' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].22 - AttributeError("The selected attribute 'mass' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].23 - [29.5 23.6 29.0 33.5 38.1 34.0 36.6 26.9 37.5 28.6 23.4 33.0 29.6 27.4 - 50.9 34.0 35.3 39.6 31.8 33.9 28.5 42.5 42.5 40.4 34.6 27.7 35.6 31.6 - 36.0 28.7 28.5 38.8 29.7 32.6 26.4 45.3 40.5 26.4 29.2 41.2 36.6 38.0 - 32.5 33.9 28.7 37.6 42.7 27.7 32.0 28.6 32.3 38.5 29.2 31.8 33.9 39.1 - 29.2 39.1 28.7 37.4 34.8 37.6 29.2 35.3 28.0 46.7 46.0 30.1 28.6 29.3 - 26.4 37.4 36.5 40.8 33.9 29.9 27.7 38.8 49.3 26.1 33.9 26.6 29.0 31.5 - 27.5 28.3 30.6 40.8 35.7 35.6 34.5 34.9 35.7 39.1 44.0 33.8 37.0 31.6 - 31.6 47.6] -# --- -# name: test_op_api_cartoon[1BNA].24 - AttributeError("The selected attribute 'occupancy' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].25 - AttributeError("The selected attribute 'vdw_radii' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].26 - AttributeError("The selected attribute 'lipophobicity' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].27 - AttributeError("The selected attribute 'charge' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].28 - [18 3 16 24 8 2 1 5 21 19 15 23 23 12 10 24 2 9 16 7 23 14 17 17 - 21 4 6 18 18 22 23 9 19 21 4 14 11 4 11 13 8 14 23 7 22 20 9 4 - 2 19 7 20 11 16 7 16 18 20 22 16 2 20 11 8 19 14 10 11 19 4 4 16 - 21 17 7 4 19 9 10 22 7 12 16 7 5 19 7 17 1 6 24 6 1 20 10 18 - 21 19 19 10] -# --- -# name: test_op_api_cartoon[1BNA].29 - [30 31 32 32 32 32 31 30 32 32 31 31 31 32 32 32 32 32 32 32 32 32 30 30 - 31 32 31 30 30 32 32 32 33 31 32 32 32 32 31 31 33 32 31 32 32 33 31 32 - 32 32 32 32 31 32 32 31 31 33 32 32 32 33 31 33 33 32 32 31 32 31 32 32 - 31 31 32 32 33 32 32 32 32 32 32 33 30 32 33 31 31 31 32 30 31 33 32 30 - 31 33 33 32] -# --- -# name: test_op_api_cartoon[1BNA].3 - AttributeError("The selected attribute 'lipophobicity' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].30 - AttributeError("The selected attribute 'atomic_number' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].31 - [1 0 1 1 0 0 0 0 1 1 1 1 1 0 0 1 0 0 1 0 1 1 1 1 1 0 0 1 1 1 1 0 1 1 0 1 0 - 0 0 1 0 1 1 0 1 1 0 0 0 1 0 1 0 1 0 1 1 1 1 1 0 1 0 0 1 1 0 0 1 0 0 1 1 1 - 0 0 1 0 0 1 0 0 1 0 0 1 0 1 0 0 1 0 0 1 0 1 1 1 1 0] -# --- -# name: test_op_api_cartoon[1BNA].32 - AttributeError("The selected attribute 'entity_id' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].33 - AttributeError("The selected attribute 'atom_id' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].34 - AttributeError("The selected attribute 'atom_name' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].35 - [[0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.8 0.8 1.0] - [0.4 0.4 0.8 1.0]] -# --- -# name: test_op_api_cartoon[1BNA].36 - [[ 0.2 0.3 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.0] - [ 0.2 0.2 0.3] - [ 0.1 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.1 0.2] - [ 0.1 0.2 0.2] - [ 0.1 0.3 0.1] - [ 0.2 0.2 -0.0] - [ 0.2 0.2 0.3] - [ 0.1 0.2 0.3] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 -0.0] - [ 0.2 0.2 0.3] - [ 0.2 0.3 0.2] - [ 0.1 0.2 0.0] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.3] - [ 0.1 0.1 -0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.1 0.2 0.1] - [ 0.2 0.3 0.0] - [ 0.2 0.3 0.0] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.3] - [ 0.1 0.2 0.0] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.1 0.1 -0.1] - [ 0.1 0.3 -0.0] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.1] - [ 0.1 0.1 -0.1] - [ 0.1 0.2 0.0] - [ 0.2 0.1 -0.0] - [ 0.2 0.2 0.3] - [ 0.1 0.1 0.1] - [ 0.1 0.2 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.2 -0.0] - [ 0.2 0.2 0.2] - [ 0.2 0.3 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.1] - [ 0.2 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.1 0.3 0.1] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.3 0.1] - [ 0.2 0.1 -0.1] - [ 0.1 0.3 -0.0] - [ 0.2 0.3 -0.1] - [ 0.1 0.3 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.1] - [ 0.2 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.1 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.3] - [ 0.1 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.1 0.2 0.1] - [ 0.1 0.3 -0.1] - [ 0.2 0.3 0.0] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.1] - [ 0.1 0.3 0.1] - [ 0.1 0.3 -0.0]] -# --- -# name: test_op_api_cartoon[1BNA].37 - AttributeError("The selected attribute 'is_backbone' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].38 - AttributeError("The selected attribute 'is_alpha_carbon' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].39 - AttributeError("The selected attribute 'is_solvent' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].4 - AttributeError("The selected attribute 'charge' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].40 - AttributeError("The selected attribute 'is_nucleic' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].41 - AttributeError("The selected attribute 'is_peptide' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].42 - AttributeError("The selected attribute 'is_hetero' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].43 - AttributeError("The selected attribute 'is_carb' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].44 - AttributeError("The selected attribute 'bond_type' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].45 - AttributeError("The selected attribute 'mass' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].5 - [18 3 16 24 8 2 1 5 21 19 15 23 23 12 10 24 2 9 16 7 23 14 17 17 - 21 4 6 18 18 22 23 9 19 21 4 14 11 4 11 13 8 14 23 7 22 20 9 4 - 2 19 7 20 11 16 7 16 18 20 22 16 2 20 11 8 19 14 10 11 19 4 4 16 - 21 17 7 4 19 9 10 22 7 12 16 7 5 19 7 17 1 6 24 6 1 20 10 18 - 21 19 19 10] -# --- -# name: test_op_api_cartoon[1BNA].6 - [30 31 32 32 32 32 31 30 32 32 31 31 31 32 32 32 32 32 32 32 32 32 30 30 - 31 32 31 30 30 32 32 32 33 31 32 32 32 32 31 31 33 32 31 32 32 33 31 32 - 32 32 32 32 31 32 32 31 31 33 32 32 32 33 31 33 33 32 32 31 32 31 32 32 - 31 31 32 32 33 32 32 32 32 32 32 33 30 32 33 31 31 31 32 30 31 33 32 30 - 31 33 33 32] -# --- -# name: test_op_api_cartoon[1BNA].7 - AttributeError("The selected attribute 'atomic_number' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[1BNA].8 - [1 0 1 1 0 0 0 0 1 1 1 1 1 0 0 1 0 0 1 0 1 1 1 1 1 0 0 1 1 1 1 0 1 1 0 1 0 - 0 0 1 0 1 1 0 1 1 0 0 0 1 0 1 0 1 0 1 1 1 1 1 0 1 0 0 1 1 0 0 1 0 0 1 1 1 - 0 0 1 0 0 1 0 0 1 0 0 1 0 1 0 0 1 0 0 1 0 1 1 1 1 0] -# --- -# name: test_op_api_cartoon[1BNA].9 - AttributeError("The selected attribute 'entity_id' does not exist on the mesh.") -# --- -# name: test_op_api_cartoon[4ozs] - [66.0 36.4 44.1 44.5 39.7 55.8 44.4 65.4 41.1 42.6 39.8 57.1 45.3 38.4 - 50.7 36.7 35.7 52.8 34.2 58.8 45.5 36.7 47.0 51.6 60.7 51.6 42.8 54.3 - 45.4 77.8 52.2 36.5 37.4 52.4 51.3 43.1 43.1 44.6 33.7 49.4 65.3 44.7 - 43.0 37.1 63.1 34.1 50.7 61.7 50.6 50.1 37.1 40.3 60.6 40.2 37.2 35.6 - 42.2 48.6 69.3 40.2 50.2 40.8 41.8 40.3 50.1 41.3 55.3 46.0 32.5 42.9 - 43.8 35.7 71.2 40.5 48.8 45.3 47.7 40.3 47.0 64.7 54.6 79.5 37.1 50.5 - 76.1 74.5 42.2 36.8 50.6 56.3 64.4 35.2 57.6 40.0 35.2 36.7 76.1 44.7 - 48.3 42.7] -# --- -# name: test_op_api_cartoon[4ozs].1 - [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. - 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. - 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. - 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. - 1. 1. 1. 1.] -# --- -# name: test_op_api_cartoon[4ozs].10 - [1366 194 1156 629 87 2 354 1120 892 764 55 649 1165 474 - 982 1275 1278 232 459 1345 1318 636 216 1008 803 229 866 973 - 602 1064 230 470 691 241 107 482 860 1161 460 1215 1195 70 - 862 585 1023 722 851 290 220 1189 1246 477 284 639 743 469 - 908 1154 535 336 506 1237 29 448 398 44 800 1330 727 895 - 903 1252 924 685 305 1165 119 439 1002 941 236 1054 587 219 - 1067 1070 908 183 1222 1013 1121 719 970 210 456 636 1076 1163 - 408 47] -# --- -# name: test_op_api_cartoon[4ozs].11 - [2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2] -# --- -# name: test_op_api_cartoon[4ozs].12 - [[0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] - [0.4 0.4 0.8 1.0] +# name: test_op_api_cartoon[4ozs].12 + [[0.4 0.4 0.8 1.0] + [0.4 0.4 0.8 1.0] + [0.4 0.4 0.8 1.0] + [0.4 0.4 0.8 1.0] + [0.4 0.4 0.8 1.0] + [0.4 0.4 0.8 1.0] + [0.4 0.4 0.8 1.0] [0.4 0.4 0.8 1.0] [0.4 0.4 0.8 1.0] [0.4 0.4 0.8 1.0] @@ -3413,1135 +2845,115 @@ [0.9 0.2 0.6] [1.1 0.6 0.4] [0.8 0.4 0.8] - [0.8 0.2 0.6] - [0.5 0.4 0.5] - [1.2 0.4 0.7] - [1.1 0.4 0.4]] -# --- -# name: test_op_api_mda.39 - [ True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True True True True False False - True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True False True True True True - True True True True True True True True True True True True - True False True True] -# --- -# name: test_op_api_mda.4 - AttributeError("The selected attribute 'charge' does not exist on the mesh.") -# --- -# name: test_op_api_mda.40 - [ True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True True True True False False - True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True False True True True True - True True True True True True True True True True True True - True False True True] -# --- -# name: test_op_api_mda.41 - [False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False] -# --- -# name: test_op_api_mda.42 - [False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False False False False False False False False False - False False False False] -# --- -# name: test_op_api_mda.43 - [ True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True True True True False False - True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True True True True True True - True True True True True True True False True True True True - True True True True True True True True True True True True - True False True True] -# --- -# name: test_op_api_mda.44 - AttributeError("The selected attribute 'is_hetero' does not exist on the mesh.") -# --- -# name: test_op_api_mda.45 - AttributeError("The selected attribute 'is_carb' does not exist on the mesh.") -# --- -# name: test_op_api_mda.46 - AttributeError("The selected attribute 'bond_type' does not exist on the mesh.") -# --- -# name: test_op_api_mda.47 - [12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. - 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 0. 0. - 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. - 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. - 12. 12. 12. 12. 12. 12. 12. 0. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. - 12. 12. 12. 12. 12. 12. 12. 0. 12. 12.] -# --- -# name: test_op_api_mda.5 - [ 99 590 319 46 2 178 572 454 389 28 332 594 241 503 115 235 323 108 - 516 408 114 441 499 305 541 115 240 354 119 56 245 438 592 236 624 614 - 35 440 298 522 370 435 146 110 608 243 143 326 380 239 462 588 271 169 - 256 16 230 206 22 405 373 456 459 473 352 155 594 66 226 514 484 117 - 537 299 109 542 544 462 94 625 519 573 369 497 105 234 324 549 593 210 - 24 28 151 306 439 229 312 613 549 401] -# --- -# name: test_op_api_mda.6 - [ 7 8 17 9 19 17 3 14 17 12 9 14 11 7 8 6 8 17 7 11 3 5 6 7 - 3 6 10 13 2 9 7 5 15 5 41 42 8 3 5 16 3 5 16 5 11 15 17 10 - 10 5 11 11 6 8 9 3 0 9 7 0 8 11 18 5 19 6 14 9 2 12 7 4 - 5 5 18 8 5 10 12 43 8 6 4 10 5 12 6 12 12 15 12 12 10 5 4 5 - 19 43 13 3] -# --- -# name: test_op_api_mda.7 - [6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 0 0 6 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 - 6 6 6 6 6 0 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 0 6 6] -# --- -# name: test_op_api_mda.8 - [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] -# --- -# name: test_op_api_mda.9 - AttributeError("The selected attribute 'entity_id' does not exist on the mesh.") -# --- -# name: test_op_local[bcif-1BNA] - [[ 0.2 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.1 -0.1] - [ 0.2 0.3 0.2] - [ 0.2 0.3 0.3] - [ 0.1 0.2 0.0] - [ 0.2 0.3 0.3] - [ 0.2 0.3 0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.2 0.2 -0.1] - [ 0.2 0.1 0.1] - [ 0.1 0.3 -0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.3 -0.0] - [ 0.1 0.2 0.1] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.1] - [ 0.2 0.2 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.3 0.0] - [ 0.2 0.2 0.1] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.2] - [ 0.1 0.2 -0.1] - [ 0.1 0.2 0.3] - [ 0.1 0.1 0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.0] - [ 0.2 0.2 0.1] - [ 0.2 0.2 0.2] - [ 0.1 0.3 -0.1] - [ 0.1 0.3 0.1] - [ 0.1 0.3 0.2] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 -0.1] - [ 0.3 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.1 -0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.3 0.1] - [ 0.1 0.1 0.1] - [ 0.2 0.2 0.1] - [ 0.2 0.1 0.1] - [ 0.1 0.3 -0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.3 0.1] - [ 0.2 0.3 0.2] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.1 0.1] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.1] - [ 0.1 0.3 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.2 0.1 -0.0] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.1] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.3 0.0] - [ 0.3 0.1 -0.0] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.1] - [ 0.1 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.2 0.3 0.1] - [ 0.2 0.1 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.3] - [ 0.3 0.2 0.0] - [ 0.1 0.3 -0.1]] -# --- -# name: test_op_local[bcif-1BNA].1 - [[ 4.1e-02 -7.4e-02 7.3e-02] - [ 7.4e-02 4.9e-02 6.6e-02] - [ 2.2e-02 -1.0e-01 -1.4e-01] - [ 4.6e-02 4.6e-02 1.3e-01] - [ 4.3e-02 1.3e-01 1.7e-01] - [-8.7e-02 1.7e-02 -4.1e-02] - [ 2.2e-02 4.3e-02 1.8e-01] - [ 2.7e-02 4.2e-02 4.0e-03] - [ 5.4e-02 2.8e-02 -8.4e-02] - [ 7.5e-02 6.2e-02 1.4e-01] - [ 6.5e-03 -4.8e-02 -1.5e-01] - [ 6.1e-03 -9.1e-02 3.4e-02] - [-1.9e-02 6.4e-02 -1.5e-01] - [-8.2e-02 3.2e-03 1.1e-01] - [ 3.8e-03 5.0e-02 -9.6e-02] - [ 2.8e-03 -4.3e-03 5.3e-02] - [-2.0e-02 -1.7e-02 -1.2e-01] - [-2.0e-02 -7.0e-02 -1.4e-01] - [ 1.7e-02 -4.1e-02 6.0e-02] - [-3.1e-02 1.4e-02 1.3e-01] - [ 5.1e-02 6.1e-02 -6.3e-02] - [ 2.4e-02 2.2e-03 4.4e-02] - [-1.9e-02 5.2e-02 -1.3e-02] - [-1.1e-02 2.9e-02 7.9e-02] - [-5.0e-02 -2.6e-02 -1.8e-01] - [-3.7e-03 -1.5e-02 1.7e-01] - [-2.1e-02 -1.0e-01 -8.6e-02] - [-3.0e-02 6.0e-03 -1.2e-01] - [-5.2e-03 -6.3e-02 -1.0e-01] - [ 1.1e-02 -2.9e-02 5.8e-02] - [ 9.4e-02 -6.9e-03 1.0e-01] - [-4.1e-03 6.1e-02 -1.5e-01] - [-2.9e-02 6.2e-02 -1.5e-02] - [-5.9e-03 7.1e-02 1.3e-01] - [-1.2e-02 -6.4e-03 -1.2e-01] - [-7.2e-02 -4.0e-03 -1.8e-01] - [ 1.1e-01 -8.1e-02 1.0e-01] - [ 3.5e-02 6.5e-02 1.3e-01] - [-9.2e-02 6.4e-02 1.8e-03] - [-3.8e-02 -8.0e-02 -1.8e-01] - [ 5.9e-03 2.0e-02 1.1e-01] - [ 3.8e-02 -2.7e-02 -8.4e-02] - [-8.9e-03 9.7e-02 -3.3e-02] - [-8.0e-02 -7.0e-02 3.5e-02] - [ 3.0e-02 -3.9e-02 5.5e-02] - [ 1.5e-02 -8.7e-02 5.7e-03] - [-1.9e-02 7.6e-02 -1.4e-01] - [-2.5e-02 -2.2e-02 2.9e-02] - [-1.9e-02 -3.6e-02 -1.5e-01] - [ 4.5e-03 4.9e-03 -9.2e-02] - [-1.7e-02 5.7e-03 -1.2e-01] - [-6.1e-02 5.1e-02 1.8e-02] - [ 1.8e-02 8.2e-02 1.1e-01] - [ 5.2e-02 1.4e-02 -2.0e-01] - [-9.3e-02 -3.0e-02 -1.0e-02] - [ 7.1e-02 -2.7e-02 -9.9e-02] - [-4.3e-02 -6.9e-02 4.4e-02] - [-6.5e-02 -1.4e-02 1.2e-01] - [-8.7e-03 4.5e-03 1.3e-01] - [-3.9e-02 -2.9e-02 1.7e-01] - [ 1.6e-02 1.3e-02 -2.4e-02] - [-5.3e-02 6.1e-02 8.2e-02] - [ 4.3e-02 1.0e-01 1.4e-01] - [ 3.7e-02 -3.9e-02 -4.0e-02] - [-1.1e-02 1.8e-02 4.7e-02] - [ 1.3e-02 -7.6e-02 6.9e-02] - [ 3.4e-02 8.7e-02 1.6e-01] - [ 5.1e-02 2.9e-02 -6.0e-02] - [-5.6e-02 3.0e-02 -8.6e-02] - [ 4.3e-02 -8.5e-02 -1.3e-01] - [-1.7e-02 5.4e-03 1.7e-01] - [ 7.9e-02 -5.5e-02 -5.0e-02] - [ 3.0e-04 -1.7e-02 -8.4e-02] - [-6.0e-03 -3.6e-02 -7.6e-02] - [-2.8e-02 -2.9e-02 -4.3e-02] - [-3.8e-02 1.7e-02 -1.2e-01] - [-5.1e-02 5.7e-05 -1.1e-01] - [-6.5e-03 -6.9e-02 -1.4e-01] - [ 1.5e-03 -8.3e-03 -1.2e-01] - [-8.3e-02 3.3e-03 -3.8e-02] - [ 6.6e-02 -4.8e-02 8.3e-02] - [ 2.5e-02 6.0e-02 -6.4e-02] - [ 1.1e-01 -8.3e-02 -1.1e-01] - [ 5.6e-02 5.5e-02 1.3e-01] - [ 1.5e-02 -7.2e-03 5.0e-02] - [-9.6e-03 -1.6e-03 -1.5e-01] - [ 7.5e-02 -4.1e-02 -1.0e-01] - [ 5.0e-02 8.5e-02 -2.7e-02] - [ 4.1e-02 -7.2e-02 2.9e-02] - [ 5.5e-02 1.9e-02 1.1e-01] - [ 5.4e-02 2.3e-02 -1.7e-01] - [-6.8e-02 -4.4e-02 2.9e-02] - [-7.6e-02 3.8e-04 9.8e-02] - [ 8.7e-02 5.5e-03 1.0e-01] - [-5.4e-03 -7.1e-04 1.6e-01] - [ 7.9e-02 -7.1e-03 -8.7e-02] - [-5.6e-02 -4.9e-02 -7.6e-02] - [-5.2e-02 -2.4e-02 1.7e-01] - [ 1.2e-01 2.8e-02 -7.4e-02] - [-2.5e-02 4.2e-02 -1.9e-01]] -# --- -# name: test_op_local[bcif-4ozs] - [[ 0.3 -0.0 0.3] - [ 0.5 0.2 -0.0] - [ 0.2 -0.0 0.2] - [ 0.3 0.1 -0.1] - [ 0.5 0.1 -0.0] - [ 0.4 0.1 -0.2] - [ 0.4 0.1 -0.0] - [ 0.4 -0.2 0.2] - [ 0.2 -0.0 0.4] - [ 0.2 0.0 0.2] - [ 0.2 0.0 0.1] - [ 0.3 0.1 0.2] - [ 0.4 0.1 -0.1] - [ 0.3 0.0 0.0] - [ 0.2 -0.0 0.2] - [ 0.4 0.1 0.1] - [ 0.3 0.0 0.1] - [ 0.3 -0.1 0.2] - [ 0.4 -0.1 0.2] - [ 0.5 0.2 0.1] - [ 0.4 0.2 -0.0] - [ 0.4 0.2 0.1] - [ 0.3 -0.0 0.2] - [ 0.3 -0.1 0.2] - [ 0.3 0.1 0.0] - [ 0.3 -0.1 0.3] - [ 0.4 0.3 -0.1] - [ 0.5 0.2 -0.1] - [ 0.3 0.0 0.2] - [ 0.2 0.1 0.2] - [ 0.4 0.2 -0.1] - [ 0.2 0.1 0.1] - [ 0.3 -0.0 0.1] - [ 0.3 0.2 -0.1] - [ 0.3 0.0 0.2] - [ 0.4 0.1 0.1] - [ 0.2 -0.1 0.2] - [ 0.3 0.1 0.1] - [ 0.4 0.2 -0.1] - [ 0.5 0.1 0.0] - [ 0.3 -0.0 0.4] - [ 0.4 0.2 0.1] - [ 0.3 -0.2 0.2] - [ 0.2 0.0 0.1] - [ 0.2 -0.0 0.2] - [ 0.4 0.2 0.1] - [ 0.1 -0.1 0.2] - [ 0.3 -0.0 0.3] - [ 0.3 -0.2 0.2] - [ 0.2 -0.1 0.1] - [ 0.5 0.1 -0.1] - [ 0.3 -0.2 0.2] - [ 0.2 0.1 0.1] - [ 0.3 0.2 -0.0] - [ 0.2 -0.1 0.4] - [ 0.3 0.0 0.2] - [ 0.4 0.0 0.1] - [ 0.2 0.1 0.1] - [ 0.3 -0.0 0.4] - [ 0.4 0.2 -0.2] - [ 0.4 0.2 -0.1] - [ 0.2 -0.1 0.1] - [ 0.2 -0.2 0.3] - [ 0.2 -0.1 0.1] - [ 0.4 0.2 0.1] - [ 0.4 0.2 -0.2] - [ 0.3 -0.1 0.3] - [ 0.3 0.0 0.0] - [ 0.3 0.1 0.2] - [ 0.4 0.2 0.1] - [ 0.2 0.1 0.0] - [ 0.2 0.0 0.2] - [ 0.3 0.1 0.0] - [ 0.3 0.1 -0.1] - [ 0.3 -0.1 0.4] - [ 0.4 0.1 0.1] - [ 0.2 -0.1 0.1] - [ 0.5 0.1 -0.1] - [ 0.4 0.2 0.1] - [ 0.4 0.1 0.0] - [ 0.4 0.1 -0.1] - [ 0.3 -0.2 0.2] - [ 0.2 0.1 0.2] - [ 0.3 -0.0 0.3] - [ 0.6 0.2 -0.0] - [ 0.3 -0.1 0.3] - [ 0.3 -0.1 0.3] - [ 0.3 0.1 0.1] - [ 0.2 0.0 0.1] - [ 0.2 0.0 0.1] - [ 0.3 -0.1 0.1] - [ 0.2 0.0 0.0] - [ 0.3 0.0 0.1] - [ 0.3 0.1 -0.1] - [ 0.2 -0.1 0.4] - [ 0.2 0.0 0.2] - [ 0.5 0.1 0.0] - [ 0.4 0.1 0.1] - [ 0.3 -0.0 0.2] - [ 0.2 -0.0 0.3]] -# --- -# name: test_op_local[bcif-4ozs].1 - [[-3.2e-02 -6.2e-02 2.2e-01] - [ 1.3e-01 1.4e-01 -1.3e-01] - [-1.1e-01 -5.9e-02 1.0e-01] - [-5.4e-02 2.2e-02 -1.6e-01] - [ 1.4e-01 2.3e-02 -1.2e-01] - [ 1.0e-01 7.9e-02 -2.7e-01] - [ 6.4e-02 6.0e-02 -1.3e-01] - [ 5.0e-02 -2.6e-01 9.3e-02] - [-6.8e-02 -6.1e-02 2.6e-01] - [-1.1e-01 -2.3e-02 1.3e-01] - [-6.9e-02 -7.8e-03 -1.9e-02] - [ 3.1e-02 7.6e-02 9.7e-02] - [ 1.2e-01 3.4e-02 -1.7e-01] - [-3.3e-02 -4.1e-02 -6.7e-02] - [-9.3e-02 -8.0e-02 9.4e-02] - [ 3.8e-02 7.9e-02 9.2e-03] - [-4.8e-02 -2.8e-02 6.8e-03] - [ 2.9e-02 -1.3e-01 1.1e-01] - [ 4.4e-02 -1.5e-01 1.0e-01] - [ 1.5e-01 1.1e-01 -2.7e-02] - [ 5.0e-02 1.2e-01 -1.4e-01] - [ 1.2e-01 1.3e-01 -2.9e-02] - [-4.5e-02 -9.3e-02 1.4e-01] - [ 9.1e-03 -1.2e-01 1.4e-01] - [-4.4e-02 4.1e-02 -9.5e-02] - [-4.6e-02 -1.6e-01 2.2e-01] - [ 5.3e-02 2.2e-01 -1.8e-01] - [ 1.6e-01 1.0e-01 -1.8e-01] - [-2.3e-03 -3.0e-02 6.4e-02] - [-8.1e-02 7.1e-02 1.1e-01] - [ 5.6e-02 1.2e-01 -1.5e-01] - [-1.2e-01 2.9e-02 -1.0e-02] - [ 1.7e-02 -5.6e-02 5.5e-03] - [-5.1e-02 1.1e-01 -1.7e-01] - [-3.4e-02 -1.9e-02 1.4e-01] - [ 5.2e-02 9.8e-02 1.8e-02] - [-8.3e-02 -1.8e-01 1.3e-01] - [ 2.8e-02 1.4e-02 -2.1e-02] - [ 8.4e-02 1.9e-01 -1.6e-01] - [ 1.4e-01 5.1e-02 -9.8e-02] - [-6.2e-02 -9.0e-02 2.8e-01] - [ 4.4e-02 1.5e-01 4.0e-03] - [ 2.7e-03 -2.2e-01 9.1e-02] - [-9.6e-02 -3.0e-02 2.9e-02] - [-9.9e-02 -8.0e-02 7.3e-02] - [ 1.3e-01 1.4e-01 -4.0e-02] - [-1.7e-01 -1.1e-01 5.9e-02] - [-4.6e-02 -8.5e-02 2.4e-01] - [-3.3e-02 -2.4e-01 1.3e-01] - [-9.8e-02 -1.0e-01 4.6e-02] - [ 1.5e-01 5.5e-02 -1.7e-01] - [-4.7e-02 -2.1e-01 1.2e-01] - [-1.2e-01 2.5e-02 4.3e-03] - [-5.8e-02 1.2e-01 -1.3e-01] - [-8.6e-02 -1.8e-01 2.8e-01] - [-4.8e-02 -8.6e-03 5.3e-02] - [ 4.1e-02 -1.9e-03 3.2e-02] - [-1.0e-01 7.6e-03 -6.2e-03] - [-6.6e-02 -9.9e-02 2.8e-01] - [ 1.3e-01 1.7e-01 -2.5e-01] - [ 9.8e-02 1.3e-01 -1.6e-01] - [-1.2e-01 -1.0e-01 4.0e-02] - [-8.6e-02 -2.1e-01 1.8e-01] - [-8.0e-02 -1.1e-01 -5.9e-03] - [ 4.8e-02 1.3e-01 3.5e-03] - [ 7.0e-02 1.6e-01 -2.6e-01] - [-4.8e-02 -1.4e-01 2.1e-01] - [-3.1e-02 -8.5e-03 -8.5e-02] - [ 5.9e-03 3.4e-02 7.2e-02] - [ 4.6e-02 1.2e-01 -2.1e-04] - [-1.5e-01 1.6e-02 -7.4e-02] - [-1.5e-01 -3.3e-02 1.1e-01] - [-8.0e-03 9.8e-02 -7.8e-02] - [ 7.3e-03 3.8e-02 -1.6e-01] - [-3.4e-02 -1.4e-01 2.8e-01] - [ 4.6e-02 9.2e-02 -3.4e-02] - [-9.3e-02 -1.3e-01 6.8e-03] - [ 1.4e-01 5.3e-02 -2.2e-01] - [ 9.7e-02 1.0e-01 1.8e-02] - [ 7.3e-02 3.8e-02 -5.7e-02] - [ 1.1e-01 9.5e-02 -2.2e-01] - [-5.2e-02 -2.7e-01 1.3e-01] - [-6.8e-02 7.9e-02 8.1e-02] - [ 5.0e-03 -8.2e-02 1.5e-01] - [ 2.4e-01 1.1e-01 -1.0e-01] - [-5.3e-02 -1.5e-01 1.7e-01] - [-6.3e-02 -1.7e-01 1.8e-01] - [ 3.3e-02 3.4e-02 2.5e-02] - [-1.3e-01 -1.7e-03 -4.4e-02] - [-1.2e-01 -2.3e-02 1.0e-02] - [-4.8e-02 -1.6e-01 1.8e-02] - [-9.1e-02 -2.4e-02 -7.9e-02] - [-9.9e-03 -3.3e-02 -2.2e-02] - [ 3.3e-02 9.9e-02 -2.2e-01] - [-9.7e-02 -1.2e-01 2.5e-01] - [-8.0e-02 -3.2e-02 7.5e-02] - [ 2.0e-01 3.5e-02 -9.4e-02] - [ 1.2e-01 6.5e-02 -7.1e-03] - [-1.6e-02 -9.4e-02 6.7e-02] - [-1.1e-01 -7.9e-02 2.1e-01]] -# --- -# name: test_op_local[bcif-8H1B] - [[-0.1 0.3 0.3] - [-0.2 0.2 0.3] - [ 0.2 -0.3 0.2] - [ 0.1 0.2 0.3] - [ 0.1 0.0 0.3] - [-0.2 0.0 0.2] - [ 0.1 0.2 0.1] - [ 0.0 -0.1 0.5] - [-0.1 -0.2 0.2] - [-0.0 0.2 0.2] - [ 0.2 -0.1 0.2] - [ 0.1 0.0 0.2] - [ 0.2 0.0 0.2] - [-0.3 -0.1 -0.0] - [-0.0 0.1 0.3] - [ 0.1 -0.0 0.1] - [ 0.3 -0.2 0.2] - [-0.1 0.2 0.1] - [-0.1 -0.1 -0.0] - [ 0.1 -0.1 0.3] - [-0.0 0.1 0.3] - [ 0.1 -0.1 0.3] - [ 0.1 -0.0 0.6] - [ 0.2 -0.2 0.0] - [-0.1 -0.0 -0.1] - [-0.1 0.1 0.3] - [ 0.1 0.0 0.1] - [ 0.0 -0.2 0.2] - [-0.1 0.1 0.4] - [-0.1 0.3 0.2] - [ 0.2 -0.0 0.1] - [ 0.1 -0.1 0.3] - [-0.1 0.0 0.5] - [ 0.1 -0.0 0.1] - [-0.1 -0.1 0.2] - [-0.0 0.2 0.4] - [ 0.2 -0.1 0.3] - [ 0.2 -0.1 -0.1] - [ 0.0 -0.1 0.1] - [ 0.2 -0.1 0.2] - [ 0.0 -0.1 0.2] - [-0.1 0.2 0.2] - [-0.0 0.2 0.2] - [-0.0 -0.0 0.1] - [ 0.2 -0.0 0.2] - [-0.1 0.2 0.2] - [ 0.3 -0.1 0.1] - [-0.0 -0.1 0.2] - [ 0.1 -0.0 0.1] - [ 0.0 0.0 0.5] - [-0.0 0.3 0.2] - [ 0.2 -0.0 0.0] - [-0.1 -0.0 0.4] - [ 0.1 -0.2 0.0] - [ 0.0 0.1 0.2] - [-0.0 -0.0 0.1] - [ 0.0 -0.1 0.4] - [-0.0 0.1 0.2] - [ 0.1 -0.1 0.3] - [ 0.2 -0.0 0.2] - [ 0.0 -0.1 0.5] - [-0.0 -0.0 0.5] - [-0.0 0.0 0.4] - [ 0.0 -0.2 0.1] - [-0.0 0.1 0.2] - [ 0.2 -0.3 0.2] - [-0.1 0.2 0.4] - [-0.0 0.1 0.2] - [-0.1 -0.1 -0.0] - [ 0.1 0.0 0.4] - [-0.1 0.1 0.4] - [-0.1 -0.0 0.0] - [ 0.2 -0.1 -0.0] - [-0.1 0.2 0.2] - [-0.1 -0.0 -0.1] - [-0.1 -0.1 -0.1] - [-0.0 0.0 0.5] - [-0.2 0.2 0.3] - [-0.0 -0.1 -0.0] - [-0.0 0.0 0.4] - [ 0.2 -0.1 0.2] - [ 0.1 0.0 0.6] - [-0.1 0.3 0.2] - [ 0.1 -0.0 0.1] - [ 0.3 -0.2 0.2] - [-0.2 -0.1 -0.1] - [-0.2 0.1 0.3] - [-0.0 0.0 0.3] - [ 0.0 0.2 0.2] - [-0.0 0.1 0.1] - [ 0.2 -0.2 0.0] - [ 0.2 -0.1 0.2] - [-0.0 -0.0 0.1] - [ 0.1 -0.2 0.1] - [-0.0 -0.3 0.2] - [-0.2 -0.1 -0.1] - [ 0.0 -0.2 0.2] - [ 0.1 -0.3 0.1] - [ 0.1 0.0 0.6] - [-0.1 -0.2 -0.0]] -# --- -# name: test_op_local[bcif-8H1B].1 - [[-1.6e-01 2.6e-01 9.2e-02] - [-2.1e-01 2.0e-01 4.2e-02] - [ 1.8e-01 -2.7e-01 -5.0e-02] - [ 5.1e-02 2.4e-01 5.9e-02] - [ 1.4e-01 1.4e-02 4.9e-02] - [-1.8e-01 2.3e-03 2.1e-02] - [ 5.1e-02 1.9e-01 -7.3e-02] - [ 2.6e-02 -6.8e-02 3.3e-01] - [-6.3e-02 -1.7e-01 -5.7e-02] - [-1.4e-02 2.5e-01 -1.0e-02] - [ 2.0e-01 -6.7e-02 -3.3e-02] - [ 1.0e-01 7.3e-03 -1.6e-02] - [ 1.4e-01 2.5e-03 -4.4e-02] - [-2.6e-01 -5.2e-02 -2.3e-01] - [-5.0e-02 1.1e-01 1.1e-01] - [ 8.0e-02 -2.1e-02 -1.1e-01] - [ 2.7e-01 -1.8e-01 -4.5e-02] - [-9.1e-02 2.5e-01 -8.5e-02] - [-1.0e-01 -1.3e-01 -2.4e-01] - [ 9.8e-02 -9.0e-02 1.3e-01] - [-5.3e-02 1.1e-01 8.6e-02] - [ 1.3e-01 -5.0e-02 6.8e-02] - [ 5.9e-02 -1.8e-02 3.5e-01] - [ 2.0e-01 -1.7e-01 -1.7e-01] - [-1.0e-01 -4.0e-02 -2.7e-01] - [-1.1e-01 1.4e-01 7.8e-02] - [ 1.3e-01 9.9e-03 -9.8e-02] - [ 1.1e-02 -2.4e-01 -2.4e-02] - [-6.2e-02 6.4e-02 2.2e-01] - [-9.4e-02 2.9e-01 2.4e-02] - [ 1.7e-01 -4.3e-02 -7.9e-02] - [ 1.4e-01 -8.8e-02 4.3e-02] - [-8.6e-02 4.0e-02 2.5e-01] - [ 6.9e-02 -3.6e-02 -1.6e-01] - [-1.0e-01 -8.7e-02 -2.1e-02] - [-3.2e-02 1.8e-01 1.8e-01] - [ 1.4e-01 -7.8e-02 6.4e-02] - [ 1.9e-01 -1.1e-01 -2.7e-01] - [-4.3e-03 -1.1e-01 -1.6e-01] - [ 2.1e-01 -8.2e-02 -3.8e-03] - [ 1.5e-02 -9.5e-02 -8.6e-03] - [-1.5e-01 1.9e-01 -3.0e-02] - [-4.9e-02 1.9e-01 -3.8e-02] - [-4.3e-02 -1.7e-02 -1.1e-01] - [ 1.7e-01 -2.4e-02 -4.9e-02] - [-1.5e-01 1.7e-01 1.5e-02] - [ 2.5e-01 -1.4e-01 -1.0e-01] - [-2.1e-02 -1.2e-01 -5.5e-02] - [ 1.1e-01 -4.1e-02 -9.1e-02] - [ 3.1e-02 4.1e-02 3.1e-01] - [-2.5e-02 3.1e-01 -2.7e-02] - [ 2.3e-01 -2.7e-02 -1.8e-01] - [-9.3e-02 -2.4e-02 1.9e-01] - [ 8.5e-02 -1.6e-01 -1.7e-01] - [ 1.4e-02 1.0e-01 -7.8e-03] - [-2.2e-02 -2.9e-02 -7.3e-02] - [-4.0e-03 -6.6e-02 2.0e-01] - [-1.3e-02 1.5e-01 -4.4e-02] - [ 5.6e-02 -1.2e-01 1.0e-01] - [ 1.7e-01 -3.5e-02 2.3e-02] - [-2.6e-03 -7.1e-02 2.9e-01] - [-3.4e-02 -1.5e-02 2.9e-01] - [-4.3e-02 4.4e-02 1.9e-01] - [ 2.2e-02 -1.9e-01 -1.0e-01] - [-5.0e-02 1.0e-01 3.0e-02] - [ 2.1e-01 -2.7e-01 -3.8e-02] - [-7.2e-02 2.5e-01 1.9e-01] - [-2.2e-02 7.1e-02 -4.7e-02] - [-1.3e-01 -1.2e-01 -2.6e-01] - [ 1.1e-01 2.0e-02 1.8e-01] - [-8.7e-02 9.1e-02 1.5e-01] - [-9.3e-02 -1.4e-03 -2.0e-01] - [ 2.2e-01 -8.6e-02 -2.6e-01] - [-8.7e-02 2.1e-01 -1.5e-02] - [-1.1e-01 -3.1e-02 -2.7e-01] - [-1.2e-01 -8.5e-02 -3.4e-01] - [-2.4e-02 3.9e-02 2.8e-01] - [-1.6e-01 2.1e-01 7.2e-02] - [-5.8e-02 -1.3e-01 -2.2e-01] - [-3.9e-02 1.4e-02 1.5e-01] - [ 1.6e-01 -1.2e-01 8.9e-05] - [ 4.5e-02 2.2e-02 3.8e-01] - [-1.3e-01 2.5e-01 -3.5e-02] - [ 5.2e-02 -3.6e-02 -8.9e-02] - [ 2.8e-01 -2.0e-01 -4.4e-02] - [-1.8e-01 -1.2e-01 -3.4e-01] - [-2.3e-01 5.8e-02 5.4e-02] - [-2.7e-02 1.8e-02 9.5e-02] - [-1.2e-03 1.9e-01 -2.1e-02] - [-5.8e-02 1.5e-01 -7.6e-02] - [ 1.6e-01 -1.9e-01 -1.8e-01] - [ 1.7e-01 -1.1e-01 3.8e-02] - [-2.2e-02 -5.0e-03 -1.6e-01] - [ 7.1e-02 -2.0e-01 -9.2e-02] - [-1.0e-02 -2.6e-01 -3.5e-02] - [-1.8e-01 -1.4e-01 -3.3e-01] - [ 3.7e-02 -1.7e-01 2.9e-02] - [ 1.2e-01 -2.7e-01 -1.1e-01] - [ 6.0e-02 2.0e-02 4.2e-01] - [-1.4e-01 -1.5e-01 -2.3e-01]] -# --- -# name: test_op_local[bcif-8U8W] - [[-0.2 -0.2 0.1] - [-0.3 -0.0 0.3] - [-0.3 -0.1 0.2] - [-0.4 0.0 0.1] - [-0.4 -0.1 0.3] - [-0.2 0.2 0.2] - [-0.3 0.0 0.3] - [-0.3 -0.3 0.1] - [-0.4 -0.1 0.2] - [-0.2 0.0 0.0] - [-0.3 0.1 -0.0] - [-0.3 -0.1 0.3] - [-0.5 0.1 0.1] - [-0.2 -0.2 0.2] - [-0.4 0.1 0.2] - [-0.2 -0.0 0.1] - [-0.2 -0.2 0.2] - [-0.3 -0.3 0.2] - [-0.4 -0.0 0.2] - [-0.4 0.1 0.2] - [-0.5 0.1 0.1] - [-0.2 -0.2 0.2] - [-0.4 -0.0 0.1] - [-0.2 0.0 0.2] - [-0.4 -0.0 0.2] - [-0.1 -0.0 0.0] - [-0.3 0.2 -0.0] - [-0.4 -0.1 0.2] - [-0.3 -0.1 0.1] - [-0.2 -0.1 0.1] - [-0.3 0.1 0.2] - [-0.2 -0.1 0.1] - [-0.4 -0.1 0.2] - [-0.5 0.1 0.2] - [-0.4 0.1 0.1] - [-0.4 -0.0 0.2] - [-0.3 -0.1 0.4] - [-0.5 -0.0 0.2] - [-0.5 0.0 0.2] - [-0.3 -0.1 0.1] - [-0.3 -0.2 0.3] - [-0.4 0.1 0.2] - [-0.4 -0.1 0.2] - [-0.3 0.0 -0.0] - [-0.4 -0.2 0.3] - [-0.4 -0.2 0.3] - [-0.2 -0.3 0.1] - [-0.3 -0.1 0.0] - [-0.3 0.2 0.3] - [-0.2 -0.1 0.4] - [-0.1 -0.1 0.0] - [-0.3 0.1 0.1] - [-0.3 -0.0 0.1] - [-0.3 0.2 0.0] - [-0.3 0.0 0.3] - [-0.4 -0.1 0.2] - [-0.3 -0.2 0.3] - [-0.4 -0.3 0.1] - [-0.5 0.1 0.2] - [-0.3 -0.0 0.3] - [-0.3 0.2 -0.0] - [-0.5 0.0 0.1] - [-0.3 0.0 0.1] - [-0.4 0.1 0.1] - [-0.2 0.1 0.1] - [-0.3 -0.2 0.2] - [-0.4 0.2 0.2] - [-0.2 0.0 0.3] - [-0.3 -0.1 0.0] - [-0.5 0.1 0.2] - [-0.4 -0.2 0.1] - [-0.2 0.1 0.1] - [-0.3 0.1 0.3] - [-0.4 0.0 0.3] - [-0.2 0.0 0.2] - [-0.2 0.1 0.1] - [-0.2 -0.1 0.2] - [-0.3 -0.2 0.1] - [-0.5 0.0 0.3] - [-0.3 0.1 0.1] - [-0.2 0.0 0.0] - [-0.2 0.1 0.1] - [-0.3 -0.3 0.1] - [-0.2 -0.1 0.2] - [-0.4 0.2 0.1] - [-0.3 -0.0 0.4] - [-0.3 -0.2 0.2] - [-0.3 -0.1 0.3] - [-0.3 0.0 0.3] - [-0.1 -0.0 0.1] - [-0.3 0.1 0.4] - [-0.2 -0.1 0.2] - [-0.5 -0.0 0.2] - [-0.2 -0.1 0.1] - [-0.3 0.2 0.3] - [-0.4 -0.0 0.2] - [-0.3 -0.2 0.3] - [-0.2 -0.2 0.1] - [-0.3 -0.1 0.1] - [-0.2 0.1 0.1]] + [0.8 0.2 0.6] + [0.5 0.4 0.5] + [1.2 0.4 0.7] + [1.1 0.4 0.4]] # --- -# name: test_op_local[bcif-8U8W].1 - [[ 9.4e-02 -1.6e-01 -9.6e-02] - [ 5.5e-02 7.9e-03 8.1e-02] - [ 4.1e-02 -1.3e-01 4.2e-02] - [-7.1e-02 1.7e-02 -6.5e-02] - [-1.2e-01 -1.3e-01 1.2e-01] - [ 1.1e-01 1.9e-01 -2.0e-02] - [-7.1e-03 6.0e-02 8.0e-02] - [-7.2e-03 -2.6e-01 -6.0e-02] - [-4.0e-02 -1.2e-01 6.5e-03] - [ 1.0e-01 4.3e-02 -1.5e-01] - [ 7.5e-03 1.1e-01 -2.2e-01] - [ 3.4e-02 -4.4e-02 7.2e-02] - [-1.4e-01 7.6e-02 -1.3e-01] - [ 7.5e-02 -2.1e-01 5.1e-02] - [-1.2e-01 1.1e-01 -7.5e-03] - [ 1.3e-01 1.3e-02 -6.9e-02] - [ 6.8e-02 -2.3e-01 -6.4e-03] - [ 5.8e-02 -2.7e-01 3.0e-02] - [-8.3e-02 -2.3e-02 5.5e-02] - [-7.1e-02 1.0e-01 2.9e-02] - [-1.5e-01 1.5e-01 -1.1e-01] - [ 1.6e-01 -1.8e-01 -2.7e-02] - [-1.2e-01 4.5e-03 -6.9e-02] - [ 1.5e-01 5.3e-02 3.3e-02] - [-6.5e-02 -1.2e-02 9.6e-03] - [ 2.2e-01 -2.6e-04 -1.8e-01] - [ 6.6e-02 1.7e-01 -2.0e-01] - [-8.0e-02 -7.1e-02 4.4e-02] - [-4.8e-03 -5.1e-02 -1.1e-01] - [ 1.6e-01 -3.5e-02 -8.7e-02] - [-5.8e-03 1.4e-01 3.3e-02] - [ 1.1e-01 -1.3e-01 -1.1e-01] - [-6.8e-02 -9.1e-02 5.0e-02] - [-1.5e-01 8.5e-02 -1.7e-02] - [-7.1e-02 1.5e-01 -5.7e-02] - [-1.0e-01 1.3e-02 1.8e-02] - [-2.1e-02 -4.8e-02 2.1e-01] - [-1.6e-01 2.3e-04 5.3e-02] - [-1.4e-01 3.0e-02 8.9e-03] - [ 3.0e-02 -5.2e-02 -5.3e-02] - [ 1.4e-02 -1.9e-01 6.8e-02] - [-9.2e-02 8.3e-02 -8.4e-03] - [-1.1e-01 -1.1e-01 1.3e-02] - [-2.2e-03 3.5e-02 -2.0e-01] - [-6.7e-02 -1.6e-01 6.9e-02] - [-4.7e-02 -1.4e-01 1.2e-01] - [ 7.9e-02 -2.6e-01 -7.8e-02] - [ 3.8e-02 -5.1e-02 -1.4e-01] - [-9.6e-03 2.1e-01 1.5e-01] - [ 9.5e-02 -6.9e-02 1.9e-01] - [ 1.8e-01 -6.1e-02 -1.7e-01] - [ 2.1e-02 9.3e-02 -5.2e-02] - [-2.6e-02 -1.9e-02 -1.0e-01] - [ 2.1e-02 1.8e-01 -1.8e-01] - [-2.1e-02 3.6e-02 1.1e-01] - [-4.1e-02 -4.4e-02 2.8e-02] - [-1.1e-02 -1.7e-01 1.1e-01] - [-5.4e-02 -2.7e-01 -4.5e-02] - [-1.8e-01 6.7e-02 -1.2e-02] - [-5.9e-03 -4.6e-03 1.4e-01] - [ 5.2e-02 2.1e-01 -2.0e-01] - [-1.5e-01 4.9e-02 -6.1e-02] - [-1.1e-02 3.4e-02 -1.3e-01] - [-1.0e-01 8.4e-02 -4.6e-02] - [ 9.9e-02 9.9e-02 -6.6e-02] - [ 3.6e-02 -1.6e-01 3.4e-02] - [-1.1e-01 1.9e-01 -3.8e-03] - [ 1.2e-01 3.4e-02 8.8e-02] - [ 4.8e-02 -4.5e-02 -1.7e-01] - [-1.8e-01 1.3e-01 -2.1e-02] - [-1.0e-01 -2.2e-01 -3.7e-02] - [ 1.2e-01 1.5e-01 -4.0e-02] - [ 4.2e-03 1.1e-01 6.6e-02] - [-8.5e-02 5.6e-02 1.3e-01] - [ 6.9e-02 4.8e-02 2.4e-02] - [ 8.9e-02 1.4e-01 -1.2e-01] - [ 1.5e-01 -1.1e-01 3.4e-02] - [ 5.1e-02 -1.8e-01 -9.3e-02] - [-1.6e-01 2.4e-02 1.4e-01] - [-7.8e-03 7.2e-02 -5.8e-02] - [ 1.3e-01 4.6e-02 -1.8e-01] - [ 7.4e-02 7.9e-02 -1.2e-01] - [-3.3e-02 -2.4e-01 -4.5e-02] - [ 7.8e-02 -6.8e-02 2.8e-03] - [-8.5e-02 2.2e-01 -1.1e-01] - [ 5.0e-02 1.5e-02 1.7e-01] - [ 6.5e-02 -2.0e-01 5.1e-02] - [ 5.7e-02 -9.1e-02 1.4e-01] - [ 1.4e-02 4.3e-02 1.2e-01] - [ 2.3e-01 3.7e-03 -1.1e-01] - [-3.2e-02 1.5e-01 1.7e-01] - [ 1.2e-01 -6.1e-02 -2.3e-02] - [-1.5e-01 -2.4e-02 5.4e-02] - [ 8.2e-02 -4.1e-02 -1.3e-01] - [-8.9e-03 1.9e-01 1.6e-01] - [-6.8e-02 3.0e-03 -9.8e-03] - [-2.2e-02 -2.1e-01 1.1e-01] - [ 1.2e-01 -1.6e-01 -1.4e-01] - [ 5.3e-02 -1.1e-01 -1.0e-01] - [ 8.9e-02 9.3e-02 -6.2e-02]] +# name: test_op_api_mda.39 + [ True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True True True True False False + True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True False True True True True + True True True True True True True True True True True True + True False True True] # --- -# name: test_op_local[cif-1BNA] - [[ 0.2 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.1 -0.1] - [ 0.2 0.3 0.2] - [ 0.2 0.3 0.3] - [ 0.1 0.2 0.0] - [ 0.2 0.3 0.3] - [ 0.2 0.3 0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.2 0.2 -0.1] - [ 0.2 0.1 0.1] - [ 0.1 0.3 -0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.3 -0.0] - [ 0.1 0.2 0.1] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.1] - [ 0.2 0.2 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.3 0.0] - [ 0.2 0.2 0.1] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.2] - [ 0.1 0.2 -0.1] - [ 0.1 0.2 0.3] - [ 0.1 0.1 0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.0] - [ 0.2 0.2 0.1] - [ 0.2 0.2 0.2] - [ 0.1 0.3 -0.1] - [ 0.1 0.3 0.1] - [ 0.1 0.3 0.2] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 -0.1] - [ 0.3 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.1 -0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.3 0.1] - [ 0.1 0.1 0.1] - [ 0.2 0.2 0.1] - [ 0.2 0.1 0.1] - [ 0.1 0.3 -0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.3 0.1] - [ 0.2 0.3 0.2] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.1 0.1] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.1] - [ 0.1 0.3 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.2 0.1 -0.0] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.1] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.3 0.0] - [ 0.3 0.1 -0.0] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.1] - [ 0.1 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.2 0.3 0.1] - [ 0.2 0.1 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.3] - [ 0.3 0.2 0.0] - [ 0.1 0.3 -0.1]] -# --- -# name: test_op_local[cif-1BNA].1 - [[ 4.1e-02 -7.4e-02 7.3e-02] - [ 7.4e-02 4.9e-02 6.6e-02] - [ 2.2e-02 -1.0e-01 -1.4e-01] - [ 4.6e-02 4.6e-02 1.3e-01] - [ 4.3e-02 1.3e-01 1.7e-01] - [-8.7e-02 1.7e-02 -4.1e-02] - [ 2.2e-02 4.3e-02 1.8e-01] - [ 2.7e-02 4.2e-02 4.0e-03] - [ 5.4e-02 2.8e-02 -8.4e-02] - [ 7.5e-02 6.2e-02 1.4e-01] - [ 6.5e-03 -4.8e-02 -1.5e-01] - [ 6.1e-03 -9.1e-02 3.4e-02] - [-1.9e-02 6.4e-02 -1.5e-01] - [-8.2e-02 3.2e-03 1.1e-01] - [ 3.8e-03 5.0e-02 -9.6e-02] - [ 2.8e-03 -4.3e-03 5.3e-02] - [-2.0e-02 -1.7e-02 -1.2e-01] - [-2.0e-02 -7.0e-02 -1.4e-01] - [ 1.7e-02 -4.1e-02 6.0e-02] - [-3.1e-02 1.4e-02 1.3e-01] - [ 5.1e-02 6.1e-02 -6.3e-02] - [ 2.4e-02 2.2e-03 4.4e-02] - [-1.9e-02 5.2e-02 -1.3e-02] - [-1.1e-02 2.9e-02 7.9e-02] - [-5.0e-02 -2.6e-02 -1.8e-01] - [-3.7e-03 -1.5e-02 1.7e-01] - [-2.1e-02 -1.0e-01 -8.6e-02] - [-3.0e-02 6.0e-03 -1.2e-01] - [-5.2e-03 -6.3e-02 -1.0e-01] - [ 1.1e-02 -2.9e-02 5.8e-02] - [ 9.4e-02 -6.9e-03 1.0e-01] - [-4.1e-03 6.1e-02 -1.5e-01] - [-2.9e-02 6.2e-02 -1.5e-02] - [-5.9e-03 7.1e-02 1.3e-01] - [-1.2e-02 -6.4e-03 -1.2e-01] - [-7.2e-02 -4.0e-03 -1.8e-01] - [ 1.1e-01 -8.1e-02 1.0e-01] - [ 3.5e-02 6.5e-02 1.3e-01] - [-9.2e-02 6.4e-02 1.8e-03] - [-3.8e-02 -8.0e-02 -1.8e-01] - [ 5.9e-03 2.0e-02 1.1e-01] - [ 3.8e-02 -2.7e-02 -8.4e-02] - [-8.9e-03 9.7e-02 -3.3e-02] - [-8.0e-02 -7.0e-02 3.5e-02] - [ 3.0e-02 -3.9e-02 5.5e-02] - [ 1.5e-02 -8.7e-02 5.7e-03] - [-1.9e-02 7.6e-02 -1.4e-01] - [-2.5e-02 -2.2e-02 2.9e-02] - [-1.9e-02 -3.6e-02 -1.5e-01] - [ 4.5e-03 4.9e-03 -9.2e-02] - [-1.7e-02 5.7e-03 -1.2e-01] - [-6.1e-02 5.1e-02 1.8e-02] - [ 1.8e-02 8.2e-02 1.1e-01] - [ 5.2e-02 1.4e-02 -2.0e-01] - [-9.3e-02 -3.0e-02 -1.0e-02] - [ 7.1e-02 -2.7e-02 -9.9e-02] - [-4.3e-02 -6.9e-02 4.4e-02] - [-6.5e-02 -1.4e-02 1.2e-01] - [-8.7e-03 4.5e-03 1.3e-01] - [-3.9e-02 -2.9e-02 1.7e-01] - [ 1.6e-02 1.3e-02 -2.4e-02] - [-5.3e-02 6.1e-02 8.2e-02] - [ 4.3e-02 1.0e-01 1.4e-01] - [ 3.7e-02 -3.9e-02 -4.0e-02] - [-1.1e-02 1.8e-02 4.7e-02] - [ 1.3e-02 -7.6e-02 6.9e-02] - [ 3.4e-02 8.7e-02 1.6e-01] - [ 5.1e-02 2.9e-02 -6.0e-02] - [-5.6e-02 3.0e-02 -8.6e-02] - [ 4.3e-02 -8.5e-02 -1.3e-01] - [-1.7e-02 5.4e-03 1.7e-01] - [ 7.9e-02 -5.5e-02 -5.0e-02] - [ 3.0e-04 -1.7e-02 -8.4e-02] - [-6.0e-03 -3.6e-02 -7.6e-02] - [-2.8e-02 -2.9e-02 -4.3e-02] - [-3.8e-02 1.7e-02 -1.2e-01] - [-5.1e-02 5.7e-05 -1.1e-01] - [-6.5e-03 -6.9e-02 -1.4e-01] - [ 1.5e-03 -8.3e-03 -1.2e-01] - [-8.3e-02 3.3e-03 -3.8e-02] - [ 6.6e-02 -4.8e-02 8.3e-02] - [ 2.5e-02 6.0e-02 -6.4e-02] - [ 1.1e-01 -8.3e-02 -1.1e-01] - [ 5.6e-02 5.5e-02 1.3e-01] - [ 1.5e-02 -7.2e-03 5.0e-02] - [-9.6e-03 -1.6e-03 -1.5e-01] - [ 7.5e-02 -4.1e-02 -1.0e-01] - [ 5.0e-02 8.5e-02 -2.7e-02] - [ 4.1e-02 -7.2e-02 2.9e-02] - [ 5.5e-02 1.9e-02 1.1e-01] - [ 5.4e-02 2.3e-02 -1.7e-01] - [-6.8e-02 -4.4e-02 2.9e-02] - [-7.6e-02 3.8e-04 9.8e-02] - [ 8.7e-02 5.5e-03 1.0e-01] - [-5.4e-03 -7.1e-04 1.6e-01] - [ 7.9e-02 -7.1e-03 -8.7e-02] - [-5.6e-02 -4.9e-02 -7.6e-02] - [-5.2e-02 -2.4e-02 1.7e-01] - [ 1.2e-01 2.8e-02 -7.4e-02] - [-2.5e-02 4.2e-02 -1.9e-01]] +# name: test_op_api_mda.4 + AttributeError("The selected attribute 'charge' does not exist on the mesh.") # --- -# name: test_op_local[cif-4ozs] +# name: test_op_api_mda.40 + [ True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True True True True False False + True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True False True True True True + True True True True True True True True True True True True + True False True True] +# --- +# name: test_op_api_mda.41 + [False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False] +# --- +# name: test_op_api_mda.42 + [False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False False False False False False False False False + False False False False] +# --- +# name: test_op_api_mda.43 + [ True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True True True True False False + True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True True True True True True + True True True True True True True False True True True True + True True True True True True True True True True True True + True False True True] +# --- +# name: test_op_api_mda.44 + AttributeError("The selected attribute 'is_hetero' does not exist on the mesh.") +# --- +# name: test_op_api_mda.45 + AttributeError("The selected attribute 'is_carb' does not exist on the mesh.") +# --- +# name: test_op_api_mda.46 + AttributeError("The selected attribute 'bond_type' does not exist on the mesh.") +# --- +# name: test_op_api_mda.47 + [12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. + 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 0. 0. + 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. + 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. + 12. 12. 12. 12. 12. 12. 12. 0. 12. 12. 12. 12. 12. 12. 12. 12. 12. 12. + 12. 12. 12. 12. 12. 12. 12. 0. 12. 12.] +# --- +# name: test_op_api_mda.5 + [ 99 590 319 46 2 178 572 454 389 28 332 594 241 503 115 235 323 108 + 516 408 114 441 499 305 541 115 240 354 119 56 245 438 592 236 624 614 + 35 440 298 522 370 435 146 110 608 243 143 326 380 239 462 588 271 169 + 256 16 230 206 22 405 373 456 459 473 352 155 594 66 226 514 484 117 + 537 299 109 542 544 462 94 625 519 573 369 497 105 234 324 549 593 210 + 24 28 151 306 439 229 312 613 549 401] +# --- +# name: test_op_api_mda.6 + [ 7 8 17 9 19 17 3 14 17 12 9 14 11 7 8 6 8 17 7 11 3 5 6 7 + 3 6 10 13 2 9 7 5 15 5 41 42 8 3 5 16 3 5 16 5 11 15 17 10 + 10 5 11 11 6 8 9 3 0 9 7 0 8 11 18 5 19 6 14 9 2 12 7 4 + 5 5 18 8 5 10 12 43 8 6 4 10 5 12 6 12 12 15 12 12 10 5 4 5 + 19 43 13 3] +# --- +# name: test_op_api_mda.7 + [6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 0 0 6 + 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 + 6 6 6 6 6 0 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 0 6 6] +# --- +# name: test_op_api_mda.8 + [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] +# --- +# name: test_op_api_mda.9 + AttributeError("The selected attribute 'entity_id' does not exist on the mesh.") +# --- +# name: test_op_local[bcif-4ozs] [[ 0.3 -0.0 0.3] [ 0.5 0.2 -0.0] [ 0.2 -0.0 0.2] @@ -4643,7 +3055,7 @@ [ 0.3 -0.0 0.2] [ 0.2 -0.0 0.3]] # --- -# name: test_op_local[cif-4ozs].1 +# name: test_op_local[bcif-4ozs].1 [[-3.2e-02 -6.2e-02 2.2e-01] [ 1.3e-01 1.4e-01 -1.3e-01] [-1.1e-01 -5.9e-02 1.0e-01] @@ -4745,7 +3157,7 @@ [-1.6e-02 -9.4e-02 6.7e-02] [-1.1e-01 -7.9e-02 2.1e-01]] # --- -# name: test_op_local[cif-8H1B] +# name: test_op_local[bcif-8H1B] [[-0.1 0.3 0.3] [-0.2 0.2 0.3] [ 0.2 -0.3 0.2] @@ -4847,7 +3259,7 @@ [ 0.1 0.0 0.6] [-0.1 -0.2 -0.0]] # --- -# name: test_op_local[cif-8H1B].1 +# name: test_op_local[bcif-8H1B].1 [[-1.6e-01 2.6e-01 9.2e-02] [-2.1e-01 2.0e-01 4.2e-02] [ 1.8e-01 -2.7e-01 -5.0e-02] @@ -4949,7 +3361,7 @@ [ 6.0e-02 2.0e-02 4.2e-01] [-1.4e-01 -1.5e-01 -2.3e-01]] # --- -# name: test_op_local[cif-8U8W] +# name: test_op_local[bcif-8U8W] [[-0.2 -0.2 0.1] [-0.3 -0.0 0.3] [-0.3 -0.1 0.2] @@ -5051,7 +3463,7 @@ [-0.3 -0.1 0.1] [-0.2 0.1 0.1]] # --- -# name: test_op_local[cif-8U8W].1 +# name: test_op_local[bcif-8U8W].1 [[ 9.4e-02 -1.6e-01 -9.6e-02] [ 5.5e-02 7.9e-03 8.1e-02] [ 4.1e-02 -1.3e-01 4.2e-02] @@ -5153,211 +3565,7 @@ [ 5.3e-02 -1.1e-01 -1.0e-01] [ 8.9e-02 9.3e-02 -6.2e-02]] # --- -# name: test_op_local[pdb-1BNA] - [[ 0.2 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.1 -0.1] - [ 0.2 0.3 0.2] - [ 0.2 0.3 0.3] - [ 0.1 0.2 0.0] - [ 0.2 0.3 0.3] - [ 0.2 0.3 0.1] - [ 0.2 0.2 0.0] - [ 0.2 0.3 0.2] - [ 0.2 0.2 -0.1] - [ 0.2 0.1 0.1] - [ 0.1 0.3 -0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.3 -0.0] - [ 0.1 0.2 0.1] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.1] - [ 0.2 0.2 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.3 0.0] - [ 0.2 0.2 0.1] - [ 0.1 0.3 0.1] - [ 0.1 0.2 0.2] - [ 0.1 0.2 -0.1] - [ 0.1 0.2 0.3] - [ 0.1 0.1 0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.0] - [ 0.2 0.2 0.1] - [ 0.2 0.2 0.2] - [ 0.1 0.3 -0.1] - [ 0.1 0.3 0.1] - [ 0.1 0.3 0.2] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 -0.1] - [ 0.3 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.1 0.3 0.1] - [ 0.1 0.1 -0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.3 0.1] - [ 0.1 0.1 0.1] - [ 0.2 0.2 0.1] - [ 0.2 0.1 0.1] - [ 0.1 0.3 -0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.3 0.1] - [ 0.2 0.3 0.2] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 0.1] - [ 0.2 0.2 -0.0] - [ 0.1 0.1 0.1] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.2] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.1] - [ 0.1 0.3 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.1 0.2] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.2 0.1 -0.0] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 -0.0] - [ 0.1 0.1 -0.1] - [ 0.1 0.2 -0.0] - [ 0.1 0.2 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.3 0.0] - [ 0.3 0.1 -0.0] - [ 0.2 0.3 0.2] - [ 0.2 0.2 0.1] - [ 0.1 0.2 -0.1] - [ 0.2 0.2 -0.0] - [ 0.2 0.3 0.1] - [ 0.2 0.1 0.1] - [ 0.2 0.2 0.2] - [ 0.2 0.2 -0.1] - [ 0.1 0.2 0.1] - [ 0.1 0.2 0.2] - [ 0.2 0.2 0.2] - [ 0.1 0.2 0.3] - [ 0.2 0.2 0.0] - [ 0.1 0.2 0.0] - [ 0.1 0.2 0.3] - [ 0.3 0.2 0.0] - [ 0.1 0.3 -0.1]] -# --- -# name: test_op_local[pdb-1BNA].1 - [[ 4.1e-02 -7.4e-02 7.3e-02] - [ 7.4e-02 4.9e-02 6.6e-02] - [ 2.2e-02 -1.0e-01 -1.4e-01] - [ 4.6e-02 4.6e-02 1.3e-01] - [ 4.3e-02 1.3e-01 1.7e-01] - [-8.7e-02 1.7e-02 -4.1e-02] - [ 2.2e-02 4.3e-02 1.8e-01] - [ 2.7e-02 4.2e-02 4.0e-03] - [ 5.4e-02 2.8e-02 -8.4e-02] - [ 7.5e-02 6.2e-02 1.4e-01] - [ 6.5e-03 -4.8e-02 -1.5e-01] - [ 6.1e-03 -9.1e-02 3.4e-02] - [-1.9e-02 6.4e-02 -1.5e-01] - [-8.2e-02 3.2e-03 1.1e-01] - [ 3.8e-03 5.0e-02 -9.6e-02] - [ 2.8e-03 -4.3e-03 5.3e-02] - [-2.0e-02 -1.7e-02 -1.2e-01] - [-2.0e-02 -7.0e-02 -1.4e-01] - [ 1.7e-02 -4.1e-02 6.0e-02] - [-3.1e-02 1.4e-02 1.3e-01] - [ 5.1e-02 6.1e-02 -6.3e-02] - [ 2.4e-02 2.2e-03 4.4e-02] - [-1.9e-02 5.2e-02 -1.3e-02] - [-1.1e-02 2.9e-02 7.9e-02] - [-5.0e-02 -2.6e-02 -1.8e-01] - [-3.7e-03 -1.5e-02 1.7e-01] - [-2.1e-02 -1.0e-01 -8.6e-02] - [-3.0e-02 6.0e-03 -1.2e-01] - [-5.2e-03 -6.3e-02 -1.0e-01] - [ 1.1e-02 -2.9e-02 5.8e-02] - [ 9.4e-02 -6.9e-03 1.0e-01] - [-4.1e-03 6.1e-02 -1.5e-01] - [-2.9e-02 6.2e-02 -1.5e-02] - [-5.9e-03 7.1e-02 1.3e-01] - [-1.2e-02 -6.4e-03 -1.2e-01] - [-7.2e-02 -4.0e-03 -1.8e-01] - [ 1.1e-01 -8.1e-02 1.0e-01] - [ 3.5e-02 6.5e-02 1.3e-01] - [-9.2e-02 6.4e-02 1.8e-03] - [-3.8e-02 -8.0e-02 -1.8e-01] - [ 5.9e-03 2.0e-02 1.1e-01] - [ 3.8e-02 -2.7e-02 -8.4e-02] - [-8.9e-03 9.7e-02 -3.3e-02] - [-8.0e-02 -7.0e-02 3.5e-02] - [ 3.0e-02 -3.9e-02 5.5e-02] - [ 1.5e-02 -8.7e-02 5.7e-03] - [-1.9e-02 7.6e-02 -1.4e-01] - [-2.5e-02 -2.2e-02 2.9e-02] - [-1.9e-02 -3.6e-02 -1.5e-01] - [ 4.5e-03 4.9e-03 -9.2e-02] - [-1.7e-02 5.7e-03 -1.2e-01] - [-6.1e-02 5.1e-02 1.8e-02] - [ 1.8e-02 8.2e-02 1.1e-01] - [ 5.2e-02 1.4e-02 -2.0e-01] - [-9.3e-02 -3.0e-02 -1.0e-02] - [ 7.1e-02 -2.7e-02 -9.9e-02] - [-4.3e-02 -6.9e-02 4.4e-02] - [-6.5e-02 -1.4e-02 1.2e-01] - [-8.7e-03 4.5e-03 1.3e-01] - [-3.9e-02 -2.9e-02 1.7e-01] - [ 1.6e-02 1.3e-02 -2.4e-02] - [-5.3e-02 6.1e-02 8.2e-02] - [ 4.3e-02 1.0e-01 1.4e-01] - [ 3.7e-02 -3.9e-02 -4.0e-02] - [-1.1e-02 1.8e-02 4.7e-02] - [ 1.3e-02 -7.6e-02 6.9e-02] - [ 3.4e-02 8.7e-02 1.6e-01] - [ 5.1e-02 2.9e-02 -6.0e-02] - [-5.6e-02 3.0e-02 -8.6e-02] - [ 4.3e-02 -8.5e-02 -1.3e-01] - [-1.7e-02 5.4e-03 1.7e-01] - [ 7.9e-02 -5.5e-02 -5.0e-02] - [ 3.0e-04 -1.7e-02 -8.4e-02] - [-6.0e-03 -3.6e-02 -7.6e-02] - [-2.8e-02 -2.9e-02 -4.3e-02] - [-3.8e-02 1.7e-02 -1.2e-01] - [-5.1e-02 5.7e-05 -1.1e-01] - [-6.5e-03 -6.9e-02 -1.4e-01] - [ 1.5e-03 -8.3e-03 -1.2e-01] - [-8.3e-02 3.3e-03 -3.8e-02] - [ 6.6e-02 -4.8e-02 8.3e-02] - [ 2.5e-02 6.0e-02 -6.4e-02] - [ 1.1e-01 -8.3e-02 -1.1e-01] - [ 5.6e-02 5.5e-02 1.3e-01] - [ 1.5e-02 -7.2e-03 5.0e-02] - [-9.6e-03 -1.6e-03 -1.5e-01] - [ 7.5e-02 -4.1e-02 -1.0e-01] - [ 5.0e-02 8.5e-02 -2.7e-02] - [ 4.1e-02 -7.2e-02 2.9e-02] - [ 5.5e-02 1.9e-02 1.1e-01] - [ 5.4e-02 2.3e-02 -1.7e-01] - [-6.8e-02 -4.4e-02 2.9e-02] - [-7.6e-02 3.8e-04 9.8e-02] - [ 8.7e-02 5.5e-03 1.0e-01] - [-5.4e-03 -7.1e-04 1.6e-01] - [ 7.9e-02 -7.1e-03 -8.7e-02] - [-5.6e-02 -4.9e-02 -7.6e-02] - [-5.2e-02 -2.4e-02 1.7e-01] - [ 1.2e-01 2.8e-02 -7.4e-02] - [-2.5e-02 4.2e-02 -1.9e-01]] -# --- -# name: test_op_local[pdb-4ozs] +# name: test_op_local[cif-4ozs] [[ 0.3 -0.0 0.3] [ 0.5 0.2 -0.0] [ 0.2 -0.0 0.2] @@ -5459,7 +3667,7 @@ [ 0.3 -0.0 0.2] [ 0.2 -0.0 0.3]] # --- -# name: test_op_local[pdb-4ozs].1 +# name: test_op_local[cif-4ozs].1 [[-3.2e-02 -6.2e-02 2.2e-01] [ 1.3e-01 1.4e-01 -1.3e-01] [-1.1e-01 -5.9e-02 1.0e-01] @@ -5561,7 +3769,7 @@ [-1.6e-02 -9.4e-02 6.7e-02] [-1.1e-01 -7.9e-02 2.1e-01]] # --- -# name: test_op_local[pdb-8H1B] +# name: test_op_local[cif-8H1B] [[-0.1 0.3 0.3] [-0.2 0.2 0.3] [ 0.2 -0.3 0.2] @@ -5663,7 +3871,7 @@ [ 0.1 0.0 0.6] [-0.1 -0.2 -0.0]] # --- -# name: test_op_local[pdb-8H1B].1 +# name: test_op_local[cif-8H1B].1 [[-1.6e-01 2.6e-01 9.2e-02] [-2.1e-01 2.0e-01 4.2e-02] [ 1.8e-01 -2.7e-01 -5.0e-02] @@ -5765,7 +3973,7 @@ [ 6.0e-02 2.0e-02 4.2e-01] [-1.4e-01 -1.5e-01 -2.3e-01]] # --- -# name: test_op_local[pdb-8U8W] +# name: test_op_local[cif-8U8W] [[-0.2 -0.2 0.1] [-0.3 -0.0 0.3] [-0.3 -0.1 0.2] @@ -5867,7 +4075,7 @@ [-0.3 -0.1 0.1] [-0.2 0.1 0.1]] # --- -# name: test_op_local[pdb-8U8W].1 +# name: test_op_local[cif-8U8W].1 [[ 9.4e-02 -1.6e-01 -9.6e-02] [ 5.5e-02 7.9e-03 8.1e-02] [ 4.1e-02 -1.3e-01 4.2e-02] diff --git a/tests/constants.py b/tests/constants.py index 7c81f080..24c90363 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -1,7 +1,31 @@ from pathlib import Path import os -codes = ['4ozs', '8H1B', '1BNA', '8U8W'] -attributes = ['b_factor', 'occupancy', 'vdw_radii', 'lipophobicity', 'charge', 'res_id', 'res_name', 'atomic_number', 'chain_id', 'entity_id', 'atom_id', - 'atom_name', 'sec_struct', 'Color', 'position', 'is_backbone', 'is_alpha_carbon', 'is_solvent', 'is_nucleic', 'is_peptide', 'is_hetero', 'is_carb', 'bond_type','mass'] -data_dir = Path(os.path.abspath(Path(__file__).parent / 'data')) +codes = ["4ozs", "8H1B", "1BNA", "8U8W"] +attributes = [ + "b_factor", + "occupancy", + "vdw_radii", + "lipophobicity", + "charge", + "res_id", + "res_name", + "atomic_number", + "chain_id", + "entity_id", + "atom_id", + "atom_name", + "sec_struct", + "Color", + "position", + "is_backbone", + "is_alpha_carbon", + "is_solvent", + "is_nucleic", + "is_peptide", + "is_hetero", + "is_carb", + "bond_type", + "mass", +] +data_dir = Path(os.path.abspath(Path(__file__).parent / "data")) diff --git a/tests/install.py b/tests/install.py index 0080a40f..a75df146 100644 --- a/tests/install.py +++ b/tests/install.py @@ -3,23 +3,20 @@ import os import pathlib -REQUIREMENTS = pathlib.Path( - pathlib.Path(__file__).resolve().parent.parent -) / "molecularnodes/requirements.txt" +REQUIREMENTS = pathlib.Path(pathlib.Path(__file__).resolve().parent.parent) / "molecularnodes/requirements.txt" def main(): - python = os.path.realpath(sys.executable) commands = [ - f'{python} -m pip install -r molecularnodes/requirements.txt', + f"{python} -m pip install -r molecularnodes/requirements.txt", # f'{python} -m pip uninstall pytest-snapshot' - f'{python} -m pip install pytest pytest-cov syrupy' + f"{python} -m pip install pytest pytest-cov syrupy", ] for command in commands: - subprocess.run(command.split(' ')) + subprocess.run(command.split(" ")) if __name__ == "__main__": diff --git a/tests/python.py b/tests/python.py index a0b9329e..ee62c8cf 100644 --- a/tests/python.py +++ b/tests/python.py @@ -3,10 +3,10 @@ import os argv = sys.argv -argv = argv[argv.index("--") + 1:] +argv = argv[argv.index("--") + 1 :] -def main(): +def main() -> None: python = os.path.realpath(sys.executable) subprocess.run([python] + argv) diff --git a/tests/run.py b/tests/run.py index 49695580..cc184bde 100644 --- a/tests/run.py +++ b/tests/run.py @@ -1,22 +1,24 @@ import pytest import sys + argv = sys.argv -argv = argv[argv.index("--") + 1:] +argv = argv[argv.index("--") + 1 :] # run this script like this: # /Applications/Blender.app/Contents/MacOS/Blender -b -P tests/run.py -- . -v # /Applications/Blender.app/Contents/MacOS/Blender -b -P tests/run.py -- . -k test_color_lookup_supplied -def main(): + +def main() -> None: # run the test suite, and we have to manually return the result value if non-zero # value is returned for a failing test if len(argv) == 0: result = pytest.main() else: result = pytest.main(argv) - if result.value != 0: - sys.exit(result.value) + if result != 0: + sys.exit(result) if __name__ == "__main__": diff --git a/tests/test_assembly.py b/tests/test_assembly.py index edd3e7c7..08033d5e 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -11,10 +11,7 @@ DATA_DIR = join(dirname(realpath(__file__)), "data") -@pytest.mark.parametrize("pdb_id, format", itertools.product( - ["1f2n", "5zng"], - ["pdb", "cif"] -)) +@pytest.mark.parametrize("pdb_id, format", itertools.product(["1f2n", "5zng"], ["pdb", "cif"])) def test_get_transformations(pdb_id, format): """ Compare an assembly built from transformation information in @@ -30,7 +27,9 @@ def test_get_transformations(pdb_id, format): cif_file = biotite_cif.PDBxFile.read(path) atoms = biotite_cif.get_structure( # Make sure `label_asym_id` is used instead of `auth_asym_id` - cif_file, model=1, use_author_fields=False + cif_file, + model=1, + use_author_fields=False, ) ref_assembly = biotite_cif.get_assembly(cif_file, model=1) test_parser = cif.CIFAssemblyParser(cif_file) @@ -43,7 +42,7 @@ def test_get_transformations(pdb_id, format): check_transformations(test_transformations, atoms, ref_assembly) -@pytest.mark.parametrize("assembly_id", [str(i+1) for i in range(5)]) +@pytest.mark.parametrize("assembly_id", [str(i + 1) for i in range(5)]) def test_get_transformations_cif(assembly_id): """ Compare an assembly built from transformation information in @@ -55,11 +54,11 @@ def test_get_transformations_cif(assembly_id): cif_file = biotite_cif.PDBxFile.read(join(DATA_DIR, "1f2n.cif")) atoms = biotite_cif.get_structure( # Make sure `label_asym_id` is used instead of `auth_asym_id` - cif_file, model=1, use_author_fields=False - ) - ref_assembly = biotite_cif.get_assembly( - cif_file, model=1, assembly_id=assembly_id + cif_file, + model=1, + use_author_fields=False, ) + ref_assembly = biotite_cif.get_assembly(cif_file, model=1, assembly_id=assembly_id) test_parser = cif.CIFAssemblyParser(cif_file) test_transformations = test_parser.get_transformations(assembly_id) diff --git a/tests/test_attributes.py b/tests/test_attributes.py index c27bb536..16a75968 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -4,16 +4,12 @@ from .utils import sample_attribute -from .constants import ( - codes, - attributes, - data_dir -) +from .constants import codes, attributes, data_dir mn.unregister() mn.register() -formats = ['pdb', 'cif', 'bcif'] +formats = ["pdb", "cif", "bcif"] @pytest.mark.parametrize("code, format", itertools.product(codes, formats)) diff --git a/tests/test_cellpack.py b/tests/test_cellpack.py index a321c552..0d8d8a9a 100644 --- a/tests/test_cellpack.py +++ b/tests/test_cellpack.py @@ -1,37 +1,26 @@ import molecularnodes as mn import pytest import bpy -from .utils import ( - sample_attribute, - NumpySnapshotExtension -) -from .constants import ( - data_dir -) +from .utils import sample_attribute, NumpySnapshotExtension +from .constants import data_dir mn.unregister() mn.register() -@pytest.mark.parametrize('format', ['bcif', 'cif']) +@pytest.mark.parametrize("format", ["bcif", "cif"]) def test_load_cellpack(snapshot_custom: NumpySnapshotExtension, format): bpy.ops.wm.read_homefile(app_template="") name = f"Cellpack_{format}" - ens = mn.io.cellpack.load( - data_dir / f"square1.{format}", - name=name, - node_setup=False, - fraction=0.1 - ) + ens = mn.io.cellpack.load(data_dir / f"square1.{format}", name=name, node_setup=False, fraction=0.1) - coll = bpy.data.collections[f'cellpack_{name}'] + coll = bpy.data.collections[f"cellpack_{name}"] instance_names = [object.name for object in coll.objects] assert snapshot_custom == "\n".join(instance_names) assert ens.name == name - ens.modifiers['MolecularNodes'].node_group.nodes['MN_pack_instances'].inputs['As Points'].default_value = False + ens.modifiers["MolecularNodes"].node_group.nodes["MN_pack_instances"].inputs["As Points"].default_value = False mn.blender.nodes.realize_instances(ens) for attribute in ens.data.attributes.keys(): - assert snapshot_custom == sample_attribute( - ens, attribute, evaluate=True) - assert snapshot_custom == str(ens['chain_ids']) + assert snapshot_custom == sample_attribute(ens, attribute, evaluate=True) + assert snapshot_custom == str(ens["chain_ids"]) diff --git a/tests/test_color.py b/tests/test_color.py index d481a4c7..0208a257 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -6,10 +6,7 @@ def test_random_rgb(snapshot_custom): n = 100 - colors = np.array(list(map( - lambda x: mn.color.random_rgb(x), - range(n) - ))) + colors = np.array(list(map(lambda x: mn.color.random_rgb(x), range(n)))) assert snapshot_custom == colors diff --git a/tests/test_density.py b/tests/test_density.py index b3bd2fb3..0862dba6 100644 --- a/tests/test_density.py +++ b/tests/test_density.py @@ -1,16 +1,16 @@ -import molecularnodes as mn +import importlib +import itertools + import bpy -import pytest import numpy as np -import itertools +import pytest + +import molecularnodes as mn + from .constants import data_dir -from .utils import ( - sample_attribute, - NumpySnapshotExtension -) -try: - import pyopenvdb -except ImportError: +from .utils import NumpySnapshotExtension, sample_attribute + +if not importlib.util.find_spec("pyopenvdb"): pytest.skip("pyopenvdb not installed", allow_module_level=True) mn.unregister() @@ -30,7 +30,6 @@ def density_file(): def test_density_load(density_file): - obj = mn.io.density.load(density_file).object evaluated = mn.blender.obj.evaluate_using_mesh(obj) pos = mn.blender.obj.get_attribute(evaluated, "position") @@ -63,7 +62,6 @@ def test_density_centered(density_file): def test_density_invert(density_file): - # First load using standar parameters to test recreation of vdb o = mn.io.density.load(density_file).object # Then refresh the scene @@ -92,14 +90,14 @@ def test_density_multiple_load(): assert obj2.users_collection[0] == mn.blender.coll.mn() -@pytest.mark.parametrize('name', ['', 'NewDensity']) +@pytest.mark.parametrize("name", ["", "NewDensity"]) def test_density_naming_op(density_file, name): bpy.context.scene.MN_import_density_name = name bpy.context.scene.MN_import_density = str(density_file) bpy.ops.mn.import_density() - if name == '': - object_name = 'emd_24805' + if name == "": + object_name = "emd_24805" else: object_name = name object = bpy.data.objects[object_name] @@ -107,11 +105,11 @@ def test_density_naming_op(density_file, name): assert object.name == object_name -@pytest.mark.parametrize('name', ['', 'NewDensity']) +@pytest.mark.parametrize("name", ["", "NewDensity"]) def test_density_naming_api(density_file, name): object = mn.io.density.load(density_file, name).object - if name == '': - object_name = 'emd_24805' + if name == "": + object_name = "emd_24805" else: object_name = name @@ -132,7 +130,4 @@ def test_density_operator(snapshot_custom: NumpySnapshotExtension, density_file, for bob in bpy.data.objects: if bob.name not in bobs: new_bob = bob - assert snapshot_custom == sample_attribute( - mn.blender.obj.evaluate_using_mesh(new_bob), - 'position' - ) + assert snapshot_custom == sample_attribute(mn.blender.obj.evaluate_using_mesh(new_bob), "position") diff --git a/tests/test_dna.py b/tests/test_dna.py index 617f5bde..e0a2ed6e 100644 --- a/tests/test_dna.py +++ b/tests/test_dna.py @@ -1,13 +1,8 @@ import numpy as np import molecularnodes as mn from molecularnodes.io import dna -from .utils import ( - sample_attribute, - NumpySnapshotExtension -) -from .constants import ( - data_dir -) +from .utils import sample_attribute, NumpySnapshotExtension +from .constants import data_dir def test_read_topology(): @@ -21,11 +16,7 @@ def test_read_topology(): def test_topology_to_idx(): - top = np.array([ - [1, 31, -1, 1], - [1, 3, 0, 1], - [1, 2, 1, -1] - ]) + top = np.array([[1, 31, -1, 1], [1, 3, 0, 1], [1, 2, 1, -1]]) bonds = dna.toplogy_to_bond_idx_pairs(top) expected = np.array([[0, 1], [1, 2]]) @@ -34,7 +25,7 @@ def test_topology_to_idx(): def test_base_lookup(): - bases = np.array(['A', 'C', 'C', 'G', 'T', '-10', 'G', 'C', '-3']) + bases = np.array(["A", "C", "C", "G", "T", "-10", "G", "C", "-3"]) expected = np.array([30, 31, 31, 32, 33, -1, 32, 31, -1]) ints = dna.base_to_int(bases) @@ -49,11 +40,11 @@ def test_read_trajectory(): def test_read_oxdna(snapshot_custom: NumpySnapshotExtension): - name = 'holliday' + name = "holliday" mol, coll_frames = dna.load( top=data_dir / "oxdna/holliday.top", traj=data_dir / "oxdna/holliday.dat", - name=name + name=name, ) assert len(coll_frames.objects) == 20 diff --git a/tests/test_download.py b/tests/test_download.py index 072c113e..d99a5172 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -10,48 +10,45 @@ # currently can't figure out downloading from other services -databases = ['rcsb'] +databases = ["rcsb"] def _filestart(format): if format == "cif": - return 'data_' + return "data_" else: - return 'HEADER' + return "HEADER" def test_download_raises_error_on_invalid_format(): with pytest.raises(ValueError) as excinfo: - download('1abc', 'invalid_format') - assert "File format 'invalid_format' not in: supported_formats=['cif', 'pdb', 'bcif']" in str( - excinfo.value) + download("1abc", "invalid_format") + assert "File format 'invalid_format' not in: supported_formats=['cif', 'pdb', 'bcif']" in str(excinfo.value) def test_fail_download_pdb_large_structure_raises(): with pytest.raises(FileDownloadPDBError) as excinfo: - download('7D6Z', format='pdb') + download("7D6Z", format="pdb") - assert "There was an error downloading the file from the Protein Data Bank. PDB or format for PDB code may not be available." in str( - excinfo.value + assert ( + "There was an error downloading the file from the Protein Data Bank. PDB or format for PDB code may not be available." + in str(excinfo.value) ) -@pytest.mark.parametrize('format', ['cif', 'bcif', 'pdb']) +@pytest.mark.parametrize("format", ["cif", "bcif", "pdb"]) def test_compare_biotite(format): - struc_download = load_structure(mn.io.download( - '4ozs', format=format, cache=tempfile.TemporaryDirectory().name)) - struc_biotite = load_structure(rcsb.fetch( - '4ozs', format=format, target_path=tempfile.TemporaryDirectory().name)) + struc_download = load_structure(mn.io.download("4ozs", format=format, cache=tempfile.TemporaryDirectory().name)) + struc_biotite = load_structure(rcsb.fetch("4ozs", format=format, target_path=tempfile.TemporaryDirectory().name)) assert struc_download == struc_biotite -@pytest.mark.parametrize('code', codes) -@pytest.mark.parametrize('database', databases) -@pytest.mark.parametrize('format', ['pdb', 'cif']) +@pytest.mark.parametrize("code", codes) +@pytest.mark.parametrize("database", databases) +@pytest.mark.parametrize("format", ["pdb", "cif"]) def test_fetch_with_cache(tmpdir, code, format, database): cache_dir = tmpdir.mkdir("cache") - file = mn.io.download(code, format, cache=str( - cache_dir), database=database) + file = mn.io.download(code, format, cache=str(cache_dir), database=database) assert isinstance(file, str) assert os.path.isfile(file) @@ -62,12 +59,12 @@ def test_fetch_with_cache(tmpdir, code, format, database): assert content.startswith(_filestart(format)) -databases = ['rcsb'] # currently can't figure out downloading from the pdbe +databases = ["rcsb"] # currently can't figure out downloading from the pdbe -@pytest.mark.parametrize('code', codes) -@pytest.mark.parametrize('database', databases) -@pytest.mark.parametrize('format', ['pdb', 'cif']) +@pytest.mark.parametrize("code", codes) +@pytest.mark.parametrize("database", databases) +@pytest.mark.parametrize("format", ["pdb", "cif"]) def test_fetch_without_cache(tmpdir, code, format, database): file = mn.io.download(code, format, cache=None, database=database) @@ -76,22 +73,21 @@ def test_fetch_without_cache(tmpdir, code, format, database): assert content.startswith(_filestart(format)) -@pytest.mark.parametrize('database', databases) +@pytest.mark.parametrize("database", databases) def test_fetch_with_invalid_format(database): - code = '4OZS' + code = "4OZS" format = "xyz" with pytest.raises(ValueError): mn.io.download(code, format, cache=None, database=database) -@pytest.mark.parametrize('code', codes) -@pytest.mark.parametrize('database', databases) -@pytest.mark.parametrize('format', ['bcif']) +@pytest.mark.parametrize("code", codes) +@pytest.mark.parametrize("database", databases) +@pytest.mark.parametrize("format", ["bcif"]) def test_fetch_with_binary_format(tmpdir, code, database, format): cache_dir = tmpdir.mkdir("cache") - file = mn.io.download(code, format, cache=str( - cache_dir), database=database) + file = mn.io.download(code, format, cache=str(cache_dir), database=database) assert isinstance(file, str) assert os.path.isfile(file) @@ -106,10 +102,10 @@ def test_fetch_with_binary_format(tmpdir, code, database, format): # , 'bcif')) # TODO bcif tests once supported -@pytest.mark.parametrize('format', ('cif', 'pdb')) -@pytest.mark.parametrize('code', ('A0A5E8G9H8', 'A0A5E8G9T8', 'K4PA18')) +@pytest.mark.parametrize("format", ("cif", "pdb")) +@pytest.mark.parametrize("code", ("A0A5E8G9H8", "A0A5E8G9T8", "K4PA18")) def test_alphafold_download(format: str, code: str) -> None: - file = mn.io.download(code=code, format=format, database='alphafold') + file = mn.io.download(code=code, format=format, database="alphafold") if format == "bcif": model = mn.io.parse.BCIF(file) elif format == "cif": diff --git a/tests/test_load.py b/tests/test_load.py index bc7170a5..e6deb3a2 100644 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -3,55 +3,48 @@ import pytest import itertools import molecularnodes as mn -from .constants import ( - data_dir, - codes, - attributes -) +from .constants import data_dir, codes, attributes from .utils import sample_attribute, NumpySnapshotExtension mn.unregister() mn.register() -styles = ['preset_1', 'cartoon', 'ribbon', - 'spheres', 'surface', 'ball_and_stick'] +styles = ["preset_1", "cartoon", "ribbon", "spheres", "surface", "ball_and_stick"] -centre_methods = ['', 'centroid', 'mass'] +centre_methods = ["", "centroid", "mass"] def useful_function(snapshot_custom, style, code, assembly, cache_dir=None): - obj = mn.io.fetch( - code, - style=style, - build_assembly=assembly, - cache_dir=cache_dir - ).object + obj = mn.io.fetch(code, style=style, build_assembly=assembly, cache_dir=cache_dir).object node = mn.blender.nodes.get_style_node(obj) - if 'EEVEE' in node.inputs.keys(): - node.inputs['EEVEE'].default_value = True + if "EEVEE" in node.inputs.keys(): + node.inputs["EEVEE"].default_value = True mn.blender.nodes.realize_instances(obj) - dont_realise = style == 'cartoon' and code == '1BNA' + dont_realise = style == "cartoon" and code == "1BNA" for att in attributes: - assert snapshot_custom == sample_attribute( - obj, att, evaluate=dont_realise) + assert snapshot_custom == sample_attribute(obj, att, evaluate=dont_realise) @pytest.mark.parametrize("assembly, code, style", itertools.product([False], codes, styles)) def test_style_1(snapshot_custom: NumpySnapshotExtension, assembly, code, style): useful_function(snapshot_custom, style, code, assembly, cache_dir=data_dir) + # have to test a subset of styles with the biological assembly. # testing some of the heavier styles run out of memory and fail on github actions -@pytest.mark.parametrize("assembly, code, style", itertools.product([True], codes, ['cartoon', 'surface', 'ribbon'])) +@pytest.mark.parametrize( + "assembly, code, style", + itertools.product([True], codes, ["cartoon", "surface", "ribbon"]), +) def test_style_2(snapshot_custom: NumpySnapshotExtension, assembly, code, style): useful_function(snapshot_custom, style, code, assembly, cache_dir=data_dir) -@pytest.mark.parametrize("code, format", itertools.product(codes, ['bcif', 'cif', 'pdb'])) +@pytest.mark.parametrize("code, format", itertools.product(codes, ["bcif", "cif", "pdb"])) def test_download_format(code, format): mol = mn.io.fetch(code, format=format, style=None).object scene = bpy.context.scene @@ -66,29 +59,29 @@ def test_download_format(code, format): mol2 = o def verts(object): - return mn.blender.obj.get_attribute(object, 'position') + return mn.blender.obj.get_attribute(object, "position") assert np.isclose(verts(mol), verts(mol2)).all() -@pytest.mark.parametrize('code, style', itertools.product(codes, styles)) +@pytest.mark.parametrize("code, style", itertools.product(codes, styles)) def test_style_positions(snapshot_custom: NumpySnapshotExtension, code, style): mol = mn.io.fetch(code, style=style).object - assert snapshot_custom == sample_attribute(mol, 'position') + assert snapshot_custom == sample_attribute(mol, "position") -@pytest.mark.parametrize('code, centre_method', itertools.product(codes, centre_methods)) +@pytest.mark.parametrize("code, centre_method", itertools.product(codes, centre_methods)) def test_centring(snapshot_custom: NumpySnapshotExtension, code, centre_method): - """fetch a pdb structure using code and translate the model using the + """fetch a pdb structure using code and translate the model using the centre_method. Check the CoG and CoM values against the snapshot file. """ mol = mn.io.fetch(code, centre=centre_method, cache_dir=data_dir) CoG = mol.centre() - CoM = mol.centre(centre_type='mass') + CoM = mol.centre(centre_type="mass") - if centre_method == 'centroid': + if centre_method == "centroid": assert np.linalg.norm(CoG) < 1e-06 - elif centre_method == 'mass': + elif centre_method == "mass": assert np.linalg.norm(CoM) < 1e-06 CoG = np.array_str(CoG, precision=4, suppress_small=True) @@ -96,58 +89,46 @@ def test_centring(snapshot_custom: NumpySnapshotExtension, code, centre_method): assert snapshot_custom == [CoG, CoM] -@pytest.mark.parametrize('code', codes) +@pytest.mark.parametrize("code", codes) def test_centring_different(code): - """fetch multiple instances of the same pdb structure and translate - each by a different centring method. Check that their centroids and + """fetch multiple instances of the same pdb structure and translate + each by a different centring method. Check that their centroids and positions are in fact different. """ - mols = [mn.io.fetch(code, centre=method, cache_dir=data_dir) - for method in centre_methods] + mols = [mn.io.fetch(code, centre=method, cache_dir=data_dir) for method in centre_methods] for mol1, mol2 in itertools.combinations(mols, 2): - assert not np.allclose( - mol1.centre(centre_type='centroid'), - mol2.centre(centre_type='centroid') - ) - assert not np.allclose( - mol1.centre(centre_type='mass'), - mol2.centre(centre_type='mass') - ) - assert not np.allclose( - mol1.get_attribute('position'), - mol2.get_attribute('position') - ) + assert not np.allclose(mol1.centre(centre_type="centroid"), mol2.centre(centre_type="centroid")) + assert not np.allclose(mol1.centre(centre_type="mass"), mol2.centre(centre_type="mass")) + assert not np.allclose(mol1.get_attribute("position"), mol2.get_attribute("position")) # THESE TEST FUNCTIONS ARE NOT RUN def test_local_pdb(snapshot_custom): - molecules = [ - mn.io.load(data_dir / f'1l58.{ext}', style='spheres') - for ext in ('cif', 'pdb') - ] - molecules.append(mn.io.fetch('1l58', format='bcif')) - for att in ['position']: + molecules = [mn.io.load(data_dir / f"1l58.{ext}", style="spheres") for ext in ("cif", "pdb")] + molecules.append(mn.io.fetch("1l58", format="bcif")) + for att in ["position"]: for mol in molecules: - assert snapshot_custom == sample_attribute( - mol, att, evaluate=False) + assert snapshot_custom == sample_attribute(mol, att, evaluate=False) def test_rcsb_nmr(snapshot_custom): - mol = mn.io.fetch('2M6Q', style='cartoon') + mol = mn.io.fetch("2M6Q", style="cartoon") assert len(mol.frames.objects) == 10 - assert mol.object.modifiers['MolecularNodes'].node_group.nodes['MN_animate_value'].inputs['To Max'].default_value == 9 + assert ( + mol.object.modifiers["MolecularNodes"].node_group.nodes["MN_animate_value"].inputs["To Max"].default_value == 9 + ) - assert snapshot_custom == sample_attribute(mol, 'position', evaluate=True) + assert snapshot_custom == sample_attribute(mol, "position", evaluate=True) - pos_1 = mol.get_attribute('position', evaluate=True) + pos_1 = mol.get_attribute("position", evaluate=True) bpy.context.scene.frame_set(100) - pos_2 = mol.get_attribute('position', evaluate=True) + pos_2 = mol.get_attribute("position", evaluate=True) assert (pos_1 != pos_2).all() def test_load_small_mol(snapshot_custom): mol = mn.io.load(data_dir / "ASN.cif") - for att in ['position', 'bond_type']: + for att in ["position", "bond_type"]: assert snapshot_custom == sample_attribute(mol, att).tolist() @@ -155,6 +136,7 @@ def test_rcsb_cache(snapshot_custom): from pathlib import Path import tempfile import os + # we want to make sure cached files are freshly downloaded, but # we don't want to delete our entire real cache # Create a temporary directory @@ -162,12 +144,9 @@ def test_rcsb_cache(snapshot_custom): test_cache = Path(data_dir) # Run the test - obj_1 = mn.io.fetch('6BQN', style='cartoon', cache_dir=test_cache) - file = os.path.join(test_cache, '6BQN.bcif') + obj_1 = mn.io.fetch("6BQN", style="cartoon", cache_dir=test_cache) + file = os.path.join(test_cache, "6BQN.bcif") assert os.path.exists(file) - obj_2 = mn.io.fetch('6BQN', style='cartoon', cache_dir=test_cache) - assert ( - sample_attribute(obj_1, 'position') == - sample_attribute(obj_2, 'position') - ).all() + obj_2 = mn.io.fetch("6BQN", style="cartoon", cache_dir=test_cache) + assert (sample_attribute(obj_1, "position") == sample_attribute(obj_2, "position")).all() diff --git a/tests/test_mda.py b/tests/test_mda.py index 73e5f255..bf8d57b2 100644 --- a/tests/test_mda.py +++ b/tests/test_mda.py @@ -1,22 +1,19 @@ -import bpy import os + +import bpy import pytest + import molecularnodes as mn -from . import utils -from molecularnodes.io.md import HAS_mda from molecularnodes.blender.obj import get_attribute +from molecularnodes.io.md import HAS_mda + if HAS_mda: import MDAnalysis as mda import numpy as np -from .constants import ( - data_dir -) -from .utils import ( - remove_all_molecule_objects, - sample_attribute, - NumpySnapshotExtension -) + +from .constants import data_dir +from .utils import NumpySnapshotExtension, remove_all_molecule_objects, sample_attribute @pytest.mark.skipif(not HAS_mda, reason="MDAnalysis is not installed") @@ -50,14 +47,14 @@ def test_create_mda_session(self, mda_session): def reload_mda_session(self, mda_session): with pytest.warns(UserWarning, match="The existing mda session"): - mda_session_2 = mn.mda.create_session() + mn.mda.create_session() @pytest.mark.parametrize("in_memory", [False, True]) def test_show_universe(self, snapshot_custom: NumpySnapshotExtension, in_memory, mda_session, universe): remove_all_molecule_objects(mda_session) mda_session.show(universe, in_memory=in_memory) bob = bpy.data.objects["atoms"] - assert snapshot_custom == sample_attribute(bob, 'position') + assert snapshot_custom == sample_attribute(bob, "position") @pytest.mark.parametrize("in_memory", [False, True]) def test_same_name_atoms(self, in_memory, mda_session, universe): @@ -69,8 +66,8 @@ def test_same_name_atoms(self, in_memory, mda_session, universe): bob_1 = bpy.data.objects["atoms"] bob_2 = bpy.data.objects["atoms.001"] - verts_1 = mn.blender.obj.get_attribute(bob_1, 'position') - verts_2 = mn.blender.obj.get_attribute(bob_2, 'position') + verts_1 = mn.blender.obj.get_attribute(bob_1, "position") + verts_2 = mn.blender.obj.get_attribute(bob_2, "position") assert (verts_1 == verts_2).all() @@ -86,19 +83,18 @@ def test_show_multiple_selection(self, snapshot_custom: NumpySnapshotExtension, custom_selections=custom_selections, ) bob = bpy.data.objects["protein"] - assert snapshot_custom == sample_attribute(bob, 'posiiton') + assert snapshot_custom == sample_attribute(bob, "posiiton") # different bahavior in_memory or not. if not in_memory: bob_ca = bpy.data.objects["name_ca"] - assert snapshot_custom == sample_attribute(bob_ca, 'position') + assert snapshot_custom == sample_attribute(bob_ca, "position") else: # attribute is added as name_ca. assert "name_ca" in bob.data.attributes.keys() @pytest.mark.parametrize("in_memory", [False, True]) def test_include_bonds(self, in_memory, mda_session, universe_with_bonds): - remove_all_molecule_objects(mda_session) mda_session.show(universe_with_bonds, in_memory=in_memory) obj = bpy.data.objects["atoms"] @@ -133,27 +129,26 @@ def test_attributes_added(self, in_memory, mda_session, universe): @pytest.mark.parametrize("in_memory", [False, True]) def test_trajectory_update(self, snapshot_custom: NumpySnapshotExtension, in_memory, universe): - # remove_all_molecule_objects(mda_session) mda_session = mn.io.MDAnalysisSession() - obj = mda_session.show(universe, in_memory=in_memory, style='ribbon') + obj = mda_session.show(universe, in_memory=in_memory, style="ribbon") node = mn.blender.nodes.get_style_node(obj) - group = obj.modifiers['MolecularNodes'].node_group + group = obj.modifiers["MolecularNodes"].node_group if in_memory: - node = group.nodes['MN_animate_value'] - node.inputs['Frame: Start'].default_value = 0 - node.inputs['Frame: End'].default_value = 4 + node = group.nodes["MN_animate_value"] + node.inputs["Frame: Start"].default_value = 0 + node.inputs["Frame: End"].default_value = 4 - if 'EEVEE' in node.inputs.keys(): - node.inputs['EEVEE'].default_value = True + if "EEVEE" in node.inputs.keys(): + node.inputs["EEVEE"].default_value = True if in_memory: mn.blender.nodes.realize_instances(obj) n = 100 - pos_a = sample_attribute(obj, 'position', n=n, evaluate=in_memory) + pos_a = sample_attribute(obj, "position", n=n, evaluate=in_memory) assert snapshot_custom == pos_a # change blender frame to 4 @@ -162,7 +157,7 @@ def test_trajectory_update(self, snapshot_custom: NumpySnapshotExtension, in_mem # if in_memory: # socket.default_value = 250 - pos_b = sample_attribute(obj, 'position', n=n, evaluate=in_memory) + pos_b = sample_attribute(obj, "position", n=n, evaluate=in_memory) assert snapshot_custom == pos_b assert not np.isclose(pos_a, pos_b).all() @@ -171,10 +166,10 @@ def test_trajectory_update(self, snapshot_custom: NumpySnapshotExtension, in_mem def test_show_updated_atoms(self, snapshot_custom: NumpySnapshotExtension, in_memory, mda_session, universe): remove_all_molecule_objects(mda_session) updating_ag = universe.select_atoms("around 5 resid 1", updating=True) - mda_session.show(updating_ag, in_memory=in_memory, style='vdw') + mda_session.show(updating_ag, in_memory=in_memory, style="vdw") bob = bpy.data.objects["atoms"] - nodes = bob.modifiers['MolecularNodes'].node_group.nodes + nodes = bob.modifiers["MolecularNodes"].node_group.nodes for node in nodes: for input in node.inputs: if input.name == "Frame: Start": @@ -189,13 +184,13 @@ def test_show_updated_atoms(self, snapshot_custom: NumpySnapshotExtension, in_me mn.blender.nodes.realize_instances(bob) bpy.context.scene.frame_set(0) - verts_frame_0 = get_attribute(bob, 'position', evaluate=True) + verts_frame_0 = get_attribute(bob, "position", evaluate=True) assert snapshot_custom == verts_frame_0 # change blender frame to 1 bpy.context.scene.frame_set(1) # bob = bpy.data.objects["atoms"] - verts_frame_1 = get_attribute(bob, 'position', evaluate=True) + verts_frame_1 = get_attribute(bob, "position", evaluate=True) assert snapshot_custom == verts_frame_1 @@ -218,7 +213,12 @@ def test_update_deleted_objects(self, in_memory, mda_session, universe): @pytest.mark.parametrize("in_memory", [False, True]) def test_save_persistance( - self, snapshot_custom: NumpySnapshotExtension, tmp_path, in_memory, mda_session, universe + self, + snapshot_custom: NumpySnapshotExtension, + tmp_path, + in_memory, + mda_session, + universe, ): remove_all_molecule_objects(mda_session) mda_session.show(universe, in_memory=in_memory) @@ -231,12 +231,12 @@ def test_save_persistance( remove_all_molecule_objects(mda_session) bpy.ops.wm.open_mainfile(filepath=str(tmp_path / "test.blend")) bob = bpy.data.objects["atoms"] - verts_frame_0 = mn.blender.obj.get_attribute(bob, 'position') + verts_frame_0 = mn.blender.obj.get_attribute(bob, "position") # change blender frame to 1 bpy.context.scene.frame_set(1) bob = bpy.data.objects["atoms"] - verts_frame_1 = mn.blender.obj.get_attribute(bob, 'position') + verts_frame_1 = mn.blender.obj.get_attribute(bob, "position") assert snapshot_custom == verts_frame_1 assert not np.isclose(verts_frame_0, verts_frame_1).all() @@ -273,22 +273,22 @@ def test_create_mda_session(self, mda_session): def reload_mda_session(self, mda_session): with pytest.warns(UserWarning, match="The existing mda session"): - mda_session_2 = mn.mda.create_session() + mn.mda.create_session() def test_frame_mapping(self, mda_session, universe): remove_all_molecule_objects(mda_session) obj = mda_session.show(universe, frame_mapping=[0, 0, 1, 2, 4]) bpy.context.scene.frame_set(0) - verts_a = get_attribute(obj, 'position') + verts_a = get_attribute(obj, "position") bpy.context.scene.frame_set(1) - verts_b = get_attribute(obj, 'position') + verts_b = get_attribute(obj, "position") # test the frame mapping works, that nothing has changed becuase of the mapping assert np.isclose(verts_a, verts_b).all() bpy.context.scene.frame_set(2) - verts_b = get_attribute(obj, 'position') + verts_b = get_attribute(obj, "position") # test that something has now changed assert not np.isclose(verts_a, verts_b).all() @@ -298,45 +298,43 @@ def test_subframes(self, mda_session, universe): obj = bpy.data.objects["atoms"] bpy.context.scene.frame_set(0) - verts_a = get_attribute(obj, 'position') + verts_a = get_attribute(obj, "position") bpy.context.scene.frame_set(1) - verts_b = get_attribute(obj, 'position') + verts_b = get_attribute(obj, "position") # should be no difference because not using subframes assert not np.isclose(verts_a, verts_b).all() for subframes in [1, 2, 3, 4]: frame = 1 fraction = frame % (subframes + 1) / (subframes + 1) - obj.mn['subframes'] = subframes + obj.mn["subframes"] = subframes bpy.context.scene.frame_set(frame) - verts_c = get_attribute(obj, 'position') + verts_c = get_attribute(obj, "position") # now using subframes, there should be a difference assert not np.isclose(verts_b, verts_c).all() - assert np.isclose(verts_c, mn.utils.lerp( - verts_a, verts_b, t=fraction)).all() + assert np.isclose(verts_c, mn.utils.lerp(verts_a, verts_b, t=fraction)).all() def test_subframe_mapping(self, mda_session, universe): remove_all_molecule_objects(mda_session) - mda_session.show(universe, in_memory=False, - frame_mapping=[0, 0, 1, 2, 3]) + mda_session.show(universe, in_memory=False, frame_mapping=[0, 0, 1, 2, 3]) bob = bpy.data.objects["atoms"] bpy.context.scene.frame_set(0) - verts_a = get_attribute(bob, 'position') + verts_a = get_attribute(bob, "position") bpy.context.scene.frame_set(1) - verts_b = get_attribute(bob, 'position') + verts_b = get_attribute(bob, "position") assert np.isclose(verts_a, verts_b).all() bpy.context.scene.frame_set(2) - verts_b = get_attribute(bob, 'position') + verts_b = get_attribute(bob, "position") assert not np.isclose(verts_a, verts_b).all() - bob.mn['subframes'] = 1 + bob.mn["subframes"] = 1 bpy.context.scene.frame_set(3) - verts_c = get_attribute(bob, 'position') + verts_c = get_attribute(bob, "position") assert not np.isclose(verts_b, verts_c).all() assert np.isclose(verts_c, mn.utils.lerp(verts_a, verts_b, 0.5)).all() @@ -346,16 +344,13 @@ def test_subframe_mapping(self, mda_session, universe): def test_martini(snapshot_custom: NumpySnapshotExtension, toplogy): session = mn.io.MDAnalysisSession() remove_all_molecule_objects(session) - universe = mda.Universe( - data_dir / "martini" / toplogy, - data_dir / "martini/pent/PENT2_100frames.xtc" - ) + universe = mda.Universe(data_dir / "martini" / toplogy, data_dir / "martini/pent/PENT2_100frames.xtc") mol = session.show(universe, style="ribbon") - pos_a = sample_attribute(mol, 'position') + pos_a = sample_attribute(mol, "position") bpy.context.scene.frame_set(3) - pos_b = sample_attribute(mol, 'position') + pos_b = sample_attribute(mol, "position") assert not np.isclose(pos_a, pos_b).all() diff --git a/tests/test_mol_sdf.py b/tests/test_mol_sdf.py index 573d3328..9ca4d775 100644 --- a/tests/test_mol_sdf.py +++ b/tests/test_mol_sdf.py @@ -1,7 +1,6 @@ import molecularnodes as mn import molecularnodes.blender as bl import pytest -import bpy from .constants import data_dir, attributes from .utils import sample_attribute, NumpySnapshotExtension @@ -10,28 +9,26 @@ mn.register() -formats = ['mol', 'sdf'] +formats = ["mol", "sdf"] @pytest.mark.parametrize("format", formats) def test_open(snapshot_custom, format): - molecule = mn.io.parse.SDF(data_dir / f'caffeine.{format}') + molecule = mn.io.parse.SDF(data_dir / f"caffeine.{format}") assert molecule.array assert molecule.file @pytest.mark.parametrize("format", formats) -@pytest.mark.parametrize("style", ['ball_and_stick', 'spheres', 'surface']) +@pytest.mark.parametrize("style", ["ball_and_stick", "spheres", "surface"]) def test_load(snapshot_custom: NumpySnapshotExtension, format, style): - mol = mn.io.load(data_dir / f'caffeine.{format}', style=style) + mol = mn.io.load(data_dir / f"caffeine.{format}", style=style) assert mol.object - if style == 'spheres': - bl.nodes.get_style_node( - mol.object).inputs['EEVEE'].default_value = True + if style == "spheres": + bl.nodes.get_style_node(mol.object).inputs["EEVEE"].default_value = True mn.blender.nodes.realize_instances(mol.object) for attribute in attributes: - assert snapshot_custom == sample_attribute( - mol, attribute, evaluate=True) + assert snapshot_custom == sample_attribute(mol, attribute, evaluate=True) diff --git a/tests/test_nodes.py b/tests/test_nodes.py index a1d4491c..0f1748bf 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -7,6 +7,7 @@ from .utils import sample_attribute, NumpySnapshotExtension from .constants import codes, data_dir + random.seed(6) mn.unregister() @@ -14,37 +15,28 @@ def test_node_name_format(): - assert mn.blender.nodes.format_node_name( - "MN_style_cartoon") == "Style Cartoon" - assert mn.blender.nodes.format_node_name( - 'MN_dna_double_helix' - ) == 'DNA Double Helix' - assert mn.blender.nodes.format_node_name( - 'MN_topo_vector_angle' - ) == 'Topology Vector Angle' + assert mn.blender.nodes.format_node_name("MN_style_cartoon") == "Style Cartoon" + assert mn.blender.nodes.format_node_name("MN_dna_double_helix") == "DNA Double Helix" + assert mn.blender.nodes.format_node_name("MN_topo_vector_angle") == "Topology Vector Angle" def test_get_nodes(): - bob = mn.io.fetch('4ozs', style='spheres').object + bob = mn.io.fetch("4ozs", style="spheres").object - assert nodes.get_nodes_last_output(bob.modifiers['MolecularNodes'].node_group)[ - 0].name == "MN_style_spheres" + assert nodes.get_nodes_last_output(bob.modifiers["MolecularNodes"].node_group)[0].name == "MN_style_spheres" nodes.realize_instances(bob) - assert nodes.get_nodes_last_output(bob.modifiers['MolecularNodes'].node_group)[ - 0].name == "Realize Instances" + assert nodes.get_nodes_last_output(bob.modifiers["MolecularNodes"].node_group)[0].name == "Realize Instances" assert nodes.get_style_node(bob).name == "MN_style_spheres" - bob2 = mn.io.fetch('1cd3', style='cartoon', build_assembly=True).object + bob2 = mn.io.fetch("1cd3", style="cartoon", build_assembly=True).object - assert nodes.get_nodes_last_output(bob2.modifiers['MolecularNodes'].node_group)[ - 0].name == "MN_assembly_1cd3" + assert nodes.get_nodes_last_output(bob2.modifiers["MolecularNodes"].node_group)[0].name == "MN_assembly_1cd3" assert nodes.get_style_node(bob2).name == "MN_style_cartoon" def test_selection(): - chain_ids = [let for let in 'ABCDEFG123456'] - node = nodes.custom_iswitch( - 'test_node', chain_ids, prefix="Chain ", dtype='BOOLEAN') + chain_ids = [let for let in "ABCDEFG123456"] + node = nodes.custom_iswitch("test_node", chain_ids, prefix="Chain ", dtype="BOOLEAN") input_sockets = nodes.inputs(node) for letter, socket in zip(chain_ids, input_sockets.values()): @@ -55,10 +47,9 @@ def test_selection(): @pytest.mark.parametrize("code", codes) @pytest.mark.parametrize("attribute", ["chain_id", "entity_id"]) def test_selection_working(snapshot_custom: NumpySnapshotExtension, attribute, code): - mol = mn.io.fetch(code, style='ribbon', cache_dir=data_dir).object - group = mol.modifiers['MolecularNodes'].node_group - node_sel = nodes.add_selection( - group, mol.name, mol[f'{attribute}s'], attribute) + mol = mn.io.fetch(code, style="ribbon", cache_dir=data_dir).object + group = mol.modifiers["MolecularNodes"].node_group + node_sel = nodes.add_selection(group, mol.name, mol[f"{attribute}s"], attribute) n = len(node_sel.inputs) @@ -67,34 +58,29 @@ def test_selection_working(snapshot_custom: NumpySnapshotExtension, attribute, c nodes.realize_instances(mol) - assert snapshot_custom == sample_attribute(mol, 'position', evaluate=True) + assert snapshot_custom == sample_attribute(mol, "position", evaluate=True) @pytest.mark.parametrize("code", codes) -@pytest.mark.parametrize("attribute", ["chain_id", 'entity_id']) -def test_color_custom(snapshot_custom: NumpySnapshotExtension, code, attribute): - mol = mn.io.fetch(code, style='ribbon', cache_dir=data_dir).object +@pytest.mark.parametrize("attribute", ["chain_id", "entity_id"]) +def test_color_custom(snapshot_custom: NumpySnapshotExtension, code, attribute): + mol = mn.io.fetch(code, style="ribbon", cache_dir=data_dir).object group_col = mn.blender.nodes.custom_iswitch( - name=f'MN_color_entity_{mol.name}', - iter_list=mol[f'{attribute}s'], + name=f"MN_color_entity_{mol.name}", + iter_list=mol[f"{attribute}s"], field=attribute, - dtype='RGBA' - ) - group = mol.modifiers['MolecularNodes'].node_group - node_col = mn.blender.nodes.add_custom( - group, group_col.name, [0, -200]) - group.links.new( - node_col.outputs[0], - group.nodes['MN_color_set'].inputs['Color'] + dtype="RGBA", ) + group = mol.modifiers["MolecularNodes"].node_group + node_col = mn.blender.nodes.add_custom(group, group_col.name, [0, -200]) + group.links.new(node_col.outputs[0], group.nodes["MN_color_set"].inputs["Color"]) - assert snapshot_custom == sample_attribute(mol, 'Color', n=50) + assert snapshot_custom == sample_attribute(mol, "Color", n=50) def test_custom_resid_selection(): - node = mn.blender.nodes.resid_multiple_selection( - 'new_node', '1, 5, 10-20, 40-100') + node = mn.blender.nodes.resid_multiple_selection("new_node", "1, 5, 10-20, 40-100") numbers = [1, 5, 10, 20, 40, 100] assert len(nodes.outputs(node)) == 2 counter = 0 @@ -105,222 +91,202 @@ def test_custom_resid_selection(): def test_op_custom_color(): - mol = mn.io.load(data_dir / '1cd3.cif').object + mol = mn.io.load(data_dir / "1cd3.cif").object mol.select_set(True) group = mn.blender.nodes.custom_iswitch( - name=f'MN_color_chain_{mol.name}', - iter_list=mol['chain_ids'], - dtype='RGBA' + name=f"MN_color_chain_{mol.name}", + iter_list=mol["chain_ids"], + dtype="RGBA", ) assert group - assert group.interface.items_tree['G'].name == 'G' - assert group.interface.items_tree[-1].name == 'G' - assert group.interface.items_tree[0].name == 'Color' + assert group.interface.items_tree["G"].name == "G" + assert group.interface.items_tree[-1].name == "G" + assert group.interface.items_tree[0].name == "Color" def test_color_lookup_supplied(): col = mn.color.random_rgb(6) - name = 'test' + name = "test" node = mn.blender.nodes.custom_iswitch( name=name, iter_list=range(10, 20), - dtype='RGBA', + dtype="RGBA", default_values=[col for i in range(10)], - start=10 + start=10, ) assert node.name == name for item in nodes.inputs(node).values(): assert np.allclose(np.array(item.default_value), col) - node = mn.blender.nodes.custom_iswitch( - name='test2', - iter_list=range(10, 20), - dtype='RGBA', - start=10 - ) + node = mn.blender.nodes.custom_iswitch(name="test2", iter_list=range(10, 20), dtype="RGBA", start=10) for item in nodes.inputs(node).values(): assert not np.allclose(np.array(item.default_value), col) def test_color_chain(snapshot_custom: NumpySnapshotExtension): - mol = mn.io.load(data_dir / '1cd3.cif', style='cartoon').object + mol = mn.io.load(data_dir / "1cd3.cif", style="cartoon").object group_col = mn.blender.nodes.custom_iswitch( - name=f'MN_color_chain_{mol.name}', - iter_list=mol['chain_ids'], - dtype='RGBA' + name=f"MN_color_chain_{mol.name}", + iter_list=mol["chain_ids"], + dtype="RGBA", ) - group = mol.modifiers['MolecularNodes'].node_group + group = mol.modifiers["MolecularNodes"].node_group node_col = mn.blender.nodes.add_custom(group, group_col.name, [0, -200]) - group.links.new(node_col.outputs[0], - group.nodes['MN_color_set'].inputs['Color']) + group.links.new(node_col.outputs[0], group.nodes["MN_color_set"].inputs["Color"]) - assert snapshot_custom == sample_attribute(mol, 'Color') + assert snapshot_custom == sample_attribute(mol, "Color") def test_color_entity(snapshot_custom: NumpySnapshotExtension): - mol = mn.io.fetch('1cd3', style='cartoon').object + mol = mn.io.fetch("1cd3", style="cartoon").object group_col = mn.blender.nodes.custom_iswitch( - name=f'MN_color_entity_{mol.name}', - iter_list=mol['entity_ids'], - dtype='RGBA', - field='entity_id' + name=f"MN_color_entity_{mol.name}", + iter_list=mol["entity_ids"], + dtype="RGBA", + field="entity_id", ) - group = mol.modifiers['MolecularNodes'].node_group + group = mol.modifiers["MolecularNodes"].node_group node_col = mn.blender.nodes.add_custom(group, group_col.name, [0, -200]) - group.links.new(node_col.outputs[0], - group.nodes['MN_color_set'].inputs['Color']) + group.links.new(node_col.outputs[0], group.nodes["MN_color_set"].inputs["Color"]) - assert snapshot_custom == sample_attribute(mol, 'Color') + assert snapshot_custom == sample_attribute(mol, "Color") def get_links(sockets): - links = [] for socket in sockets: for link in socket.links: yield link def test_change_style(): - model = mn.io.fetch('1cd3', style='cartoon').object + model = mn.io.fetch("1cd3", style="cartoon").object style_node_1 = nodes.get_style_node(model).name - mn.blender.nodes.change_style_node(model, 'ribbon') + mn.blender.nodes.change_style_node(model, "ribbon") style_node_2 = nodes.get_style_node(model).name assert style_node_1 != style_node_2 - for style in ['ribbon', 'cartoon', 'presets', 'ball_and_stick', 'surface']: + for style in ["ribbon", "cartoon", "presets", "ball_and_stick", "surface"]: style_node_1 = nodes.get_style_node(model) - links_in_1 = [link.from_socket.name for link in get_links( - style_node_1.inputs)] - links_out_1 = [link.from_socket.name for link in get_links( - style_node_1.outputs)] + links_in_1 = [link.from_socket.name for link in get_links(style_node_1.inputs)] + links_out_1 = [link.from_socket.name for link in get_links(style_node_1.outputs)] nodes.change_style_node(model, style) style_node_2 = nodes.get_style_node(model) - links_in_2 = [link.from_socket.name for link in get_links( - style_node_2.inputs)] - links_out_2 = [link.from_socket.name for link in get_links( - style_node_2.outputs)] + links_in_2 = [link.from_socket.name for link in get_links(style_node_2.inputs)] + links_out_2 = [link.from_socket.name for link in get_links(style_node_2.outputs)] assert len(links_in_1) == len(links_in_2) assert len(links_out_1) == len(links_out_2) def test_node_topology(snapshot_custom: NumpySnapshotExtension): - mol = mn.io.fetch('1bna', del_solvent=False).object + mol = mn.io.fetch("1bna", del_solvent=False).object group = nodes.get_mod(mol).node_group - group.links.new(group.nodes['Group Input'].outputs[0], - group.nodes['Group Output'].inputs[0]) - node_att = group.nodes.new('GeometryNodeStoreNamedAttribute') - node_att.inputs[2].default_value = 'test_attribute' + group.links.new( + group.nodes["Group Input"].outputs[0], + group.nodes["Group Output"].inputs[0], + ) + node_att = group.nodes.new("GeometryNodeStoreNamedAttribute") + node_att.inputs[2].default_value = "test_attribute" nodes.insert_last_node(group, node_att) - node_names = [ - node['name'] - for node in mn.ui.node_info.menu_items['topology'] - if not node == "break" - ] + node_names = [node["name"] for node in mn.ui.node_info.menu_items["topology"] if not node == "break"] for node_name in node_names: # exclude these particular nodes, as they aren't field nodes and so we shouldn't # be testing them here. Will create their own particular tests later - if 'backbone' in node_name or 'bonds' in node_name: + if "backbone" in node_name or "bonds" in node_name: continue - node_topo = nodes.add_custom(group, node_name, - location=[x - 300 for x in node_att.location]) + node_topo = nodes.add_custom(group, node_name, location=[x - 300 for x in node_att.location]) if node_name == "MN_topo_point_mask": - node_topo.inputs['atom_name'].default_value = 61 + node_topo.inputs["atom_name"].default_value = 61 type_to_data_type = { - 'VECTOR': 'FLOAT_VECTOR', - 'VALUE': 'FLOAT', - 'BOOLEAN': 'BOOLEAN', - 'INT': 'INT', - 'RGBA': 'FLOAT_COLOR', - 'ROTATION': 'QUATERNION' + "VECTOR": "FLOAT_VECTOR", + "VALUE": "FLOAT", + "BOOLEAN": "BOOLEAN", + "INT": "INT", + "RGBA": "FLOAT_COLOR", + "ROTATION": "QUATERNION", } for output in node_topo.outputs: node_att.data_type = type_to_data_type[output.type] - input = node_att.inputs['Value'] + input = node_att.inputs["Value"] for link in input.links: group.links.remove(link) group.links.new(output, input) - assert snapshot_custom == mn.blender.obj.get_attribute( - mol, 'test_attribute', evaluate=True - ) + assert snapshot_custom == mn.blender.obj.get_attribute(mol, "test_attribute", evaluate=True) def test_compute_backbone(snapshot_custom: NumpySnapshotExtension): - mol = mn.io.fetch('1CCN', del_solvent=False).object + mol = mn.io.fetch("1CCN", del_solvent=False).object group = nodes.get_mod(mol).node_group - group.links.new(group.nodes['Group Input'].outputs[0], - group.nodes['Group Output'].inputs[0]) - node_att = group.nodes.new('GeometryNodeStoreNamedAttribute') - node_att.inputs[2].default_value = 'test_attribute' - node_backbone = nodes.add_custom(group, 'MN_topo_compute_backbone') + group.links.new( + group.nodes["Group Input"].outputs[0], + group.nodes["Group Output"].inputs[0], + ) + node_att = group.nodes.new("GeometryNodeStoreNamedAttribute") + node_att.inputs[2].default_value = "test_attribute" + node_backbone = nodes.add_custom(group, "MN_topo_compute_backbone") nodes.insert_last_node(group, node_backbone) nodes.insert_last_node(group, node_att) - node_names = ['MN_topo_backbone'] + node_names = ["MN_topo_backbone"] for node_name in node_names: - node_topo = nodes.add_custom(group, node_name, - location=[x - 300 for x in node_att.location]) + node_topo = nodes.add_custom(group, node_name, location=[x - 300 for x in node_att.location]) if node_name == "MN_topo_point_mask": - node_topo.inputs['atom_name'].default_value = 61 + node_topo.inputs["atom_name"].default_value = 61 type_to_data_type = { - 'VECTOR': 'FLOAT_VECTOR', - 'VALUE': 'FLOAT', - 'BOOLEAN': 'BOOLEAN', - 'INT': 'INT', - 'RGBA': 'FLOAT_COLOR', - 'ROTATION': 'QUATERNION' + "VECTOR": "FLOAT_VECTOR", + "VALUE": "FLOAT", + "BOOLEAN": "BOOLEAN", + "INT": "INT", + "RGBA": "FLOAT_COLOR", + "ROTATION": "QUATERNION", } for output in node_topo.outputs: node_att.data_type = type_to_data_type[output.type] - input = node_att.inputs['Value'] + input = node_att.inputs["Value"] for link in input.links: group.links.remove(link) group.links.new(output, input) - assert snapshot_custom == mn.blender.obj.get_attribute( - mol, 'test_attribute', evaluate=True - ) + assert snapshot_custom == mn.blender.obj.get_attribute(mol, "test_attribute", evaluate=True) - for angle in ['Phi', 'Psi']: + for angle in ["Phi", "Psi"]: output = node_backbone.outputs[angle] node_att.data_type = type_to_data_type[output.type] - input = node_att.inputs['Value'] + input = node_att.inputs["Value"] for link in input.links: group.links.remove(link) group.links.new(output, input) - assert snapshot_custom == mn.blender.obj.get_attribute( - mol, 'test_attribute', evaluate=True - ) + assert snapshot_custom == mn.blender.obj.get_attribute(mol, "test_attribute", evaluate=True) def test_topo_bonds(): - mol = mn.io.fetch('1BNA', del_solvent=True, style=None).object - group = nodes.get_mod(mol).node_group = nodes.new_group() + mol = mn.io.fetch("1BNA", del_solvent=True, style=None).object + group = nodes.get_mod(mol).node_group = nodes.new_tree() # add the node that will break bonds, set the cutoff to 0 - node_break = nodes.add_custom(group, 'MN_topo_bonds_break') + node_break = nodes.add_custom(group, "MN_topo_bonds_break") nodes.insert_last_node(group, node=node_break) - node_break.inputs['Cutoff'].default_value = 0 + node_break.inputs["Cutoff"].default_value = 0 # compare the number of edges before and after deleting them with bonds = mol.data.edges @@ -330,7 +296,7 @@ def test_topo_bonds(): # add the node to find the bonds, and ensure the number of bonds pre and post the nodes # are the same (other attributes will be different, but for now this is good) - node_find = nodes.add_custom(group, 'MN_topo_bonds_find') + node_find = nodes.add_custom(group, "MN_topo_bonds_find") nodes.insert_last_node(group, node=node_find) bonds_new = mn.blender.obj.evaluated(mol).data.edges assert len(bonds) == len(bonds_new) diff --git a/tests/test_obj.py b/tests/test_obj.py index 2d9f40a5..8c5d60c6 100644 --- a/tests/test_obj.py +++ b/tests/test_obj.py @@ -1,7 +1,5 @@ -import bpy import numpy as np import molecularnodes as mn -from .utils import sample_attribute def test_creat_obj(): @@ -18,16 +16,13 @@ def test_creat_obj(): def test_set_position(): - mol = mn.io.fetch('8FAT') + mol = mn.io.fetch("8FAT") - pos_a = mol.get_attribute('position') + pos_a = mol.get_attribute("position") - mol.set_attribute( - data=mol.get_attribute('position') + 10, - name='position' - ) + mol.set_attribute(data=mol.get_attribute("position") + 10, name="position") - pos_b = mol.get_attribute('position') + pos_b = mol.get_attribute("position") print(f"{pos_a=}") print(f"{pos_b=}") diff --git a/tests/test_ops.py b/tests/test_ops.py index cdc5fede..0c9d477d 100644 --- a/tests/test_ops.py +++ b/tests/test_ops.py @@ -5,11 +5,7 @@ from molecularnodes.blender.obj import ObjectTracker from .utils import sample_attribute, NumpySnapshotExtension -from .constants import ( - data_dir, - codes, - attributes -) +from .constants import data_dir, codes, attributes # register the operators, which isn't done by default when loading bpy # just via headless float_decimals @@ -18,7 +14,7 @@ @pytest.mark.parametrize("code", codes) -def test_op_api_cartoon(snapshot_custom: NumpySnapshotExtension, code, style='ribbon', format="bcif"): +def test_op_api_cartoon(snapshot_custom: NumpySnapshotExtension, code, style="ribbon", format="bcif"): scene = bpy.context.scene scene.MN_import_node_setup = True scene.MN_pdb_code = code @@ -39,22 +35,21 @@ def test_op_api_cartoon(snapshot_custom: NumpySnapshotExtension, code, style='ri for name in attributes: if name == "sec_struct" or name.startswith("."): continue - assert snapshot_custom == sample_attribute( - mol, name, evaluate=True) + assert snapshot_custom == sample_attribute(mol, name, evaluate=True) @pytest.mark.parametrize("code", codes) -@pytest.mark.parametrize("file_format", ['bcif', 'cif', 'pdb']) +@pytest.mark.parametrize("file_format", ["bcif", "cif", "pdb"]) def test_op_local(snapshot_custom, code, file_format): scene = bpy.context.scene scene.MN_import_node_setup = False - scene.MN_import_style = 'spheres' + scene.MN_import_style = "spheres" scene.MN_import_build_assembly = False scene.MN_import_del_solvent = False scene.MN_import_format_download = file_format path = str(mn.io.download(code=code, format=file_format, cache=data_dir)) scene.MN_import_local_path = path - scene.MN_centre_type = 'centroid' + scene.MN_centre_type = "centroid" scene.MN_import_centre = False with ObjectTracker() as o: @@ -66,10 +61,7 @@ def test_op_local(snapshot_custom, code, file_format): bpy.ops.mn.import_protein_local() bob_centred = o.latest() - bob_pos, bob_centred_pos = [ - sample_attribute(x, 'position', evaluate=False) - for x in [bob, bob_centred] - ] + bob_pos, bob_centred_pos = [sample_attribute(x, "position", evaluate=False) for x in [bob, bob_centred]] assert snapshot_custom == bob_pos assert snapshot_custom == bob_centred_pos @@ -83,7 +75,7 @@ def test_op_api_mda(snapshot_custom: NumpySnapshotExtension): bpy.context.scene.MN_import_md_topology = topo bpy.context.scene.MN_import_md_trajectory = traj - bpy.context.scene.MN_import_style = 'ribbon' + bpy.context.scene.MN_import_style = "ribbon" bpy.ops.mn.import_protein_md() obj_1 = bpy.context.active_object @@ -91,9 +83,9 @@ def test_op_api_mda(snapshot_custom: NumpySnapshotExtension): assert not bpy.data.collections.get(f"{name}_frames") bpy.context.scene.MN_md_in_memory = True - name = 'NewTrajectoryInMemory' + name = "NewTrajectoryInMemory" - obj_2, universe = mn.io.md.load(topo, traj, name="test", style='ribbon') + obj_2, universe = mn.io.md.load(topo, traj, name="test", style="ribbon") frames_coll = bpy.data.collections.get(f"{obj_2.name}_frames") assert not frames_coll diff --git a/tests/test_parse.py b/tests/test_parse.py index c3d26aba..75557a6d 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -6,7 +6,7 @@ @pytest.fixture def filepath(): - return data_dir / '1f2n.bcif' + return data_dir / "1f2n.bcif" def test_bcif_init(filepath): @@ -36,4 +36,4 @@ def test_bcif_entity_ids(filepath): bcif = mn.io.parse.BCIF(filepath) entity_ids = bcif.entity_ids assert entity_ids is not None - assert entity_ids == ['CAPSID PROTEIN', 'CALCIUM ION', 'water'] + assert entity_ids == ["CAPSID PROTEIN", "CALCIUM ION", "water"] diff --git a/tests/test_pdbx.py b/tests/test_pdbx.py index 60a9e7b3..0ae5c45a 100644 --- a/tests/test_pdbx.py +++ b/tests/test_pdbx.py @@ -1,32 +1,26 @@ +import random + import molecularnodes as mn -import numpy as np -import random from .constants import data_dir -from .utils import NumpySnapshotExtension, sample_attribute +from .utils import sample_attribute def test_ss_label_to_int(): - examples = ['TURN_TY1_P68', 'BEND64', 'HELX_LH_PP_P9', 'STRN44'] - assert [3, 3, 1, 2] == [ - mn.io.parse.cif._ss_label_to_int(x) for x in examples] + examples = ["TURN_TY1_P68", "BEND64", "HELX_LH_PP_P9", "STRN44"] + assert [3, 3, 1, 2] == [mn.io.parse.cif._ss_label_to_int(x) for x in examples] -def test_get_ss_from_mmcif(snapshot_custom: NumpySnapshotExtension): - mol = mn.io.load(data_dir / '1cd3.cif') - - # mol2, fil2 = mn.io.fetch('1cd3') +def test_get_ss_from_mmcif(snapshot_custom): + mol = mn.io.load(data_dir / "1cd3.cif") random.seed(6) random_idx = random.sample(range(len(mol)), 100) - # assert (mol.sec_struct == mol2.sec_struct)[random_idx].all() - assert snapshot_custom == mol.array.sec_struct[random_idx] def test_secondary_structure_no_helix(snapshot_custom): - m = mn.io.fetch('7ZL4', cache_dir=data_dir) + m = mn.io.fetch("7ZL4", cache_dir=data_dir) - assert snapshot_custom == sample_attribute( - m.object, 'sec_struct', n=500, evaluate=False) + assert snapshot_custom == sample_attribute(m.object, "sec_struct", n=500, evaluate=False) diff --git a/tests/test_pkg.py b/tests/test_pkg.py index 7d19f995..5d4f205f 100644 --- a/tests/test_pkg.py +++ b/tests/test_pkg.py @@ -9,10 +9,10 @@ def test_name_versions(): def test_is_current(): - assert mn.pkg.is_current('biotite') + assert mn.pkg.is_current("biotite") def test_get_pkgs(): - names = ['biotite', 'MDAnalysis', 'mrcfile', 'starfile', 'msgpack', 'pillow'] + names = ["biotite", "MDAnalysis", "mrcfile", "starfile", "msgpack", "pillow"] for name in mn.pkg.get_pkgs().keys(): assert name in names diff --git a/tests/test_select.py b/tests/test_select.py index 255ba642..71b7fed5 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -5,14 +5,6 @@ import pytest -def create_debug_group(name='MolecularNodesDebugGroup'): - group = nodes.new_group(name=name, fallback=False) - info = group.nodes.new('GeometryNodeObjectInfo') - group.links.new(info.outputs['Geometry'], - group.nodes['Group Output'].inputs[0]) - return group - - def evaluate(object): object.update_tag() dg = bpy.context.evaluated_depsgraph_get() @@ -20,29 +12,28 @@ def evaluate(object): custom_selections = [ - ('1, 3, 5-7', np.array((1, 3, 5, 6, 7))), - ('5, 9-20', np.append(5, np.arange(9, 21))), - ('1, 7, 8, 9', np.array((1, 7, 8, 9))) + ("1, 3, 5-7", np.array((1, 3, 5, 6, 7))), + ("5, 9-20", np.append(5, np.arange(9, 21))), + ("1, 7, 8, 9", np.array((1, 7, 8, 9))), ] -@pytest.mark.parametrize('selection', custom_selections) +@pytest.mark.parametrize("selection", custom_selections) def test_select_multiple_residues(selection): n_atoms = 100 object = mn.blender.obj.create_object(np.zeros((n_atoms, 3))) - mn.blender.obj.set_attribute(object, 'res_id', np.arange(n_atoms) + 1) + mn.blender.obj.set_attribute(object, "res_id", np.arange(n_atoms) + 1) mod = nodes.get_mod(object) - group = nodes.new_group(fallback=False) + group = nodes.new_tree(fallback=False) mod.node_group = group - sep = group.nodes.new('GeometryNodeSeparateGeometry') + sep = group.nodes.new("GeometryNodeSeparateGeometry") nodes.insert_last_node(group, sep) - node_sel_group = nodes.resid_multiple_selection('custom', selection[0]) + node_sel_group = nodes.resid_multiple_selection("custom", selection[0]) node_sel = nodes.add_custom(group, node_sel_group.name) - group.links.new(node_sel.outputs['Selection'], sep.inputs['Selection']) + group.links.new(node_sel.outputs["Selection"], sep.inputs["Selection"]) vertices_count = len(mn.blender.obj.evaluated(object).data.vertices) assert vertices_count == len(selection[1]) - assert (mn.blender.obj.get_attribute( - mn.blender.obj.evaluated(object), 'res_id') == selection[1]).all() + assert (mn.blender.obj.get_attribute(mn.blender.obj.evaluated(object), "res_id") == selection[1]).all() diff --git a/tests/test_setup.py b/tests/test_setup.py index a2a1ee18..2ce640be 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -5,4 +5,4 @@ def test_template(): mn.utils.template_install() bpy.ops.wm.read_homefile(app_template="Molecular Nodes") - assert not bpy.data.objects.get('Cube') + assert not bpy.data.objects.get("Cube") diff --git a/tests/test_star.py b/tests/test_star.py index d53df983..e3a74724 100644 --- a/tests/test_star.py +++ b/tests/test_star.py @@ -1,18 +1,16 @@ -import molecularnodes as mn +import bpy import pytest -from scipy.spatial.transform import Rotation as R import starfile +import importlib +from scipy.spatial.transform import Rotation as R + +import molecularnodes as mn + from .constants import data_dir mn.unregister() mn.register() -try: - import pyopenvdb - SKIP = False -except ImportError: - SKIP = True - @pytest.mark.parametrize("type", ["cistem", "relion"]) def test_starfile_attributes(type): @@ -21,26 +19,21 @@ def test_starfile_attributes(type): star = starfile.read(file) - if type == 'relion': - df = star['particles'].merge(star['optics'], on='rlnOpticsGroup') - euler_angles = df[['rlnAngleRot', - 'rlnAngleTilt', 'rlnAnglePsi']].to_numpy() + if type == "relion": + df = star["particles"].merge(star["optics"], on="rlnOpticsGroup") + euler_angles = df[["rlnAngleRot", "rlnAngleTilt", "rlnAnglePsi"]].to_numpy() - elif type == 'cistem': + elif type == "cistem": df = star - euler_angles = df[['cisTEMAnglePhi', - 'cisTEMAngleTheta', 'cisTEMAnglePsi']].to_numpy() + euler_angles = df[["cisTEMAnglePhi", "cisTEMAngleTheta", "cisTEMAnglePsi"]].to_numpy() # Calculate Scipy rotation from the euler angles - rot_from_euler = quats = R.from_euler( - seq='ZYZ', angles=euler_angles, degrees=True - ).inv() + rot_from_euler = R.from_euler(seq="ZYZ", angles=euler_angles, degrees=True).inv() # Activate the rotation debug mode in the nodetreee and get the quaternion attribute - debugnode = mn.blender.nodes.star_node( - ensemble.node_group).node_tree.nodes['Switch.001'] - debugnode.inputs['Switch'].default_value = True - quat_attribute = ensemble.get_attribute('MNDEBUGEuler', evaluate=True) + debugnode = mn.blender.nodes.star_node(ensemble.node_group).node_tree.nodes["Switch.001"] + debugnode.inputs["Switch"].default_value = True + quat_attribute = ensemble.get_attribute("MNDEBUGEuler", evaluate=True) # Convert from blender to scipy conventions and then into Scipy rotation rot_from_geo_nodes = R.from_quat(quat_attribute[:, [1, 2, 3, 0]]) @@ -52,12 +45,10 @@ def test_starfile_attributes(type): def test_categorical_attributes(): file = data_dir / "cistem.star" ensemble = mn.io.star.load(file) - assert 'cisTEMOriginalImageFilename_categories' in ensemble.object + assert "cisTEMOriginalImageFilename_categories" in ensemble.object def test_micrograph_conversion(): - from pathlib import Path - file = data_dir / "cistem.star" ensemble = mn.io.star.load(file) tiff_path = data_dir / "montage.tiff" @@ -68,25 +59,24 @@ def test_micrograph_conversion(): def test_micrograph_loading(): import bpy + file = data_dir / "cistem.star" tiff_path = data_dir / "montage.tiff" tiff_path.unlink(missing_ok=True) ensemble = mn.io.star.load(file) assert not tiff_path.exists() - ensemble.star_node.inputs['Show Micrograph'].default_value = True + ensemble.star_node.inputs["Show Micrograph"].default_value = True bpy.context.evaluated_depsgraph_get().update() assert tiff_path.exists() # Ensure montage get only loaded once - assert sum(1 for image in bpy.data.images.keys() - if 'montage' in image) == 1 - assert ensemble.micrograph_material.node_tree.nodes['Image Texture'].image.name == 'montage.tiff' - assert ensemble.star_node.inputs['Micrograph'].default_value.name == 'montage.tiff' + assert sum(1 for image in bpy.data.images.keys() if "montage" in image) == 1 + assert ensemble.micrograph_material.node_tree.nodes["Image Texture"].image.name == "montage.tiff" + assert ensemble.star_node.inputs["Micrograph"].default_value.name == "montage.tiff" -@pytest.mark.skipif(SKIP, reason='Test may segfault on GHA') +@pytest.mark.skipif(importlib.util.find_spec("pyopenvdb"), reason="Test may segfault on GHA") def test_rehydration(tmp_path): - import bpy bpy.ops.wm.read_homefile() ensemble = mn.io.star.load(data_dir / "cistem.star") bpy.ops.wm.save_as_mainfile(filepath=str(tmp_path / "test.blend")) diff --git a/tests/utils.py b/tests/utils.py index cc7586b5..0f6318aa 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -11,53 +11,19 @@ # and when comparing them, reads the list back into a numpy array for comparison # it checks for 'isclose' for floats and otherwise looks for absolute comparison class NumpySnapshotExtension(AmberSnapshotExtension): - def serialize(self, data, **kwargs): if isinstance(data, np.ndarray): - return np.array2string( - data, - precision=1, - threshold=1e3, - floatmode='maxprec_equal' - ) + return np.array2string(data, precision=1, threshold=1e3, floatmode="maxprec_equal") return super().serialize(data, **kwargs) - # def matches(self, *, serialized_data, snapshot_data): - # print(f"HELLOOOO") - # print(f"{serialized_data=}") - # print(f"{snapshot_data=}") - # serialized_data = np.array(ast.literal_eval(serialized_data)), - # snapshot_data = np.array(ast.literal_eval(snapshot_data)), - # print(f"{serialized_data=}") - # print(f"{snapshot_data=}") - - # # super().assert_match(snapshot_custom, test_value) - # # def assert_match(self, snapshot_custom, test_value): - # if isinstance(serialized_data, np.ndarray): - # # if the values are floats, then we use a rough "isclose" to compare them - # # which helps with floating point issues. Between platforms geometry nodes - # # outputs some differences in the meshes which are usually off by ~0.01 or so - - # else: - # assert (serialized_data == np.array(snapshot_data)).all() - - # else: - # super().matches(serialized_data=serialized_data, snapshot_data=snapshot_data) - - -def sample_attribute(object, - attribute, - n=100, - evaluate=True, - error: bool = False, - seed=6): + +def sample_attribute(object, attribute, n=100, evaluate=True, error: bool = False, seed=6): if isinstance(object, mn.io.parse.molecule.Molecule): object = object.object random.seed(seed) if error: - attribute = mn.blender.obj.get_attribute( - object, attribute, evaluate=evaluate) + attribute = mn.blender.obj.get_attribute(object, attribute, evaluate=evaluate) length = len(attribute) if n > length: @@ -71,11 +37,7 @@ def sample_attribute(object, return attribute[idx, :] else: try: - attribute = mn.blender.obj.get_attribute( - object=object, - name=attribute, - evaluate=evaluate - ) + attribute = mn.blender.obj.get_attribute(object=object, name=attribute, evaluate=evaluate) length = len(attribute) if n > length: @@ -91,21 +53,13 @@ def sample_attribute(object, return np.array(e) -def sample_attribute_to_string(object, - attribute, - n=100, - evaluate=True, - precision=3, - seed=6): +def sample_attribute_to_string(object, attribute, n=100, evaluate=True, precision=3, seed=6): if isinstance(object, mn.io.parse.molecule.Molecule): object = object.object try: - array = sample_attribute( - object, attribute=attribute, n=n, evaluate=evaluate, seed=seed) + array = sample_attribute(object, attribute=attribute, n=n, evaluate=evaluate, seed=seed) except AttributeError as e: - print( - f"Error {e}, unable to sample attribute {attribute} from {object}" - ) + print(f"Error {e}, unable to sample attribute {attribute} from {object}") return str(e) if array.dtype != bool: