Spaces:
No application file
No application file
# Copyright 2019-21 by Robert T. Miller. All rights reserved. | |
# This file is part of the Biopython distribution and governed by your | |
# choice of the "Biopython License Agreement" or the "BSD 3-Clause License". | |
# Please see the LICENSE file that should have been included as part of this | |
# package. | |
"""SCADIO: write OpenSCAD program to create protein structure 3D model. | |
3D printing a protein structure is a non-trivial exercise due to the | |
overall complexity and the general requirement for supporting overhang regions | |
while printing. This software is a path to generating a model for printing | |
(e.g. an STL file), and does not address the issues around converting the | |
model to a physical product. OpenSCAD <http://www.openscad.org/> can create | |
a printable model from the script this software produces. MeshMixer | |
<http://www.meshmixer.com/>, various slicer software, and the 3D printer | |
technology available to you provide options for addressing the problems around | |
physically rendering the model. | |
The model generated here consists of OpenSCAD primitives, e.g. spheres and | |
cylinders, representing individual atoms and bonds in an explicit model of a | |
protein structure. The benefit is that individual atoms/bonds may be selected | |
for specific print customizations relevant to 3D printing (such as rotatable | |
bond mechanisms or hydrogen bond magnets). Alternatively, use e.g. Chimera to | |
render a structure as ribbons or similar for printing as a single object. | |
I suggest generating your initial model using the OpenSCAD script provided | |
here, then modifying that script according to your needs. Changing the | |
atomScale and bondRadius values can simplify the model by removing gaps and | |
the corresponding need for supports, or you may wish to modify the | |
hedronDispatch() routine to select residues or chain sections for printing | |
separately and subsequently joining with rotatable bonds. During this | |
development phase you will likely have your version include only the data | |
matrices generated here, by using the `includeCode=False` option to | |
write_SCAD(). An example project using rotatable backbone and magnetic | |
hydrogen bonds is at <https://www.thingiverse.com/thing:3957471>. | |
""" | |
# import re | |
from Bio.File import as_handle | |
from Bio.PDB.PDBExceptions import PDBException | |
from Bio.PDB.internal_coords import IC_Residue, IC_Chain | |
# from Bio.PDB.Structure import Structure | |
# from Bio.PDB.Residue import Residue | |
from Bio.PDB.vectors import homog_scale_mtx | |
import numpy as np # type: ignore | |
def _scale_residue(res, scale, scaleMtx): | |
if res.internal_coord: | |
res.internal_coord.applyMtx(scaleMtx) | |
if res.internal_coord.gly_Cbeta: | |
res.internal_coord.scale = scale | |
def write_SCAD( | |
entity, | |
file, | |
scale=None, | |
pdbid=None, | |
backboneOnly=False, | |
includeCode=True, | |
maxPeptideBond=None, | |
start=None, | |
fin=None, | |
handle="protein", | |
): | |
"""Write hedron assembly to file as OpenSCAD matrices. | |
This routine calls both :meth:`.IC_Chain.internal_to_atom_coordinates` and | |
:meth:`.IC_Chain.atom_to_internal_coordinates` due to requirements for | |
scaling, explicit bonds around rings, and setting the coordinate space of | |
the output model. | |
Output data format is primarily: | |
- matrix for each hedron: | |
len1, angle2, len3, atom covalent bond class, flags to indicate | |
atom/bond represented in previous hedron (OpenSCAD very slow with | |
redundant overlapping elements), flags for bond features | |
- transform matrices to assemble each hedron into residue dihedra sets | |
- transform matrices for each residue to position in chain | |
OpenSCAD software is included in this Python file to process these | |
matrices into a model suitable for a 3D printing project. | |
:param entity: Biopython PDB :class:`.Structure` entity | |
structure data to export | |
:param file: Bipoython :func:`.as_handle` filename or open file pointer | |
file to write data to | |
:param float scale: | |
units (usually mm) per angstrom for STL output, written in output | |
:param str pdbid: | |
PDB idcode, written in output. Defaults to '0PDB' if not supplied | |
and no 'idcode' set in entity | |
:param bool backboneOnly: default False. | |
Do not output side chain data past Cbeta if True | |
:param bool includeCode: default True. | |
Include OpenSCAD software (inline below) so output file can be loaded | |
into OpenSCAD; if False, output data matrices only | |
:param float maxPeptideBond: Optional default None. | |
Override the cut-off in IC_Chain class (default 1.4) for detecting | |
chain breaks. If your target has chain breaks, pass a large number | |
here to create a very long 'bond' spanning the break. | |
:param int start,fin: default None | |
Parameters for internal_to_atom_coords() to limit chain segment. | |
:param str handle: default 'protein' | |
name for top level of generated OpenSCAD matrix structure | |
See :meth:`.IC_Residue.set_flexible` to set flags for specific residues to | |
have rotatable bonds, and :meth:`.IC_Residue.set_hbond` to include cavities | |
for small magnets to work as hydrogen bonds. | |
See <https://www.thingiverse.com/thing:3957471> for implementation example. | |
The OpenSCAD code explicitly creates spheres and cylinders to | |
represent atoms and bonds in a 3D model. Options are available | |
to support rotatable bonds and magnetic hydrogen bonds. | |
Matrices are written to link, enumerate and describe residues, | |
dihedra, hedra, and chains, mirroring contents of the relevant IC_* | |
data structures. | |
The OpenSCAD matrix of hedra has additional information as follows: | |
* the atom and bond state (single, double, resonance) are logged | |
so that covalent radii may be used for atom spheres in the 3D models | |
* bonds and atoms are tracked so that each is only created once | |
* bond options for rotation and magnet holders for hydrogen bonds | |
may be specified (see :meth:`.IC_Residue.set_flexible` and | |
:meth:`.IC_Residue.set_hbond` ) | |
Note the application of :data:`Bio.PDB.internal_coords.IC_Chain.MaxPeptideBond` | |
: missing residues may be linked (joining chain segments with arbitrarily | |
long bonds) by setting this to a large value. | |
Note this uses the serial assembly per residue, placing each residue at | |
the origin and supplying the coordinate space transform to OpenaSCAD | |
All ALTLOC (disordered) residues and atoms are written to the output | |
model. (see :data:`Bio.PDB.internal_coords.IC_Residue.no_altloc`) | |
""" | |
if maxPeptideBond is not None: | |
mpbStash = IC_Chain.MaxPeptideBond | |
IC_Chain.MaxPeptideBond = float(maxPeptideBond) | |
# step one need IC_Residue atom_coords loaded in order to scale | |
# so if no internal_coords, initialise from Atom coordinates | |
added_IC_Atoms = False | |
if "S" == entity.level or "M" == entity.level: | |
for chn in entity.get_chains(): | |
if not chn.internal_coord: | |
chn.internal_coord = IC_Chain(chn) | |
added_IC_Atoms = True | |
elif "C" == entity.level: | |
if not entity.internal_coord: # entity.internal_coord: | |
entity.internal_coord = IC_Chain(entity) | |
added_IC_Atoms = True | |
else: | |
raise PDBException("level not S, M or C: " + str(entity.level)) | |
if added_IC_Atoms: | |
# if loaded pdb, need to scale, and asm, gen atomArray | |
entity.atom_to_internal_coordinates() | |
else: | |
# if loaded pic file and need to scale, generate atom coords | |
entity.internal_to_atom_coordinates(None) | |
if scale is not None: | |
scaleMtx = homog_scale_mtx(scale) | |
if "C" == entity.level: | |
entity.internal_coord.atomArray = np.dot( | |
entity.internal_coord.atomArray[:], scaleMtx | |
) | |
entity.internal_coord.hAtoms_needs_update[:] = True | |
entity.internal_coord.scale = scale | |
else: | |
for chn in entity.get_chains(): | |
if hasattr(chn.internal_coord, "atomArray"): | |
chn.internal_coord.atomArray = np.dot( | |
chn.internal_coord.atomArray[:], scaleMtx | |
) | |
chn.internal_coord.hAtoms_needs_update[:] = True | |
chn.internal_coord.scale = scale | |
# generate internal coords for scaled entity | |
# (hedron bond lengths have changed if scaled) | |
# if not scaling, still need to generate internal coordinate | |
# bonds for ring sidechains | |
# AllBonds is a class attribute for IC_Residue.atom_to_internal_coordinates | |
# to generate explicit hedra covering all bonds | |
allBondsStash = IC_Residue._AllBonds | |
IC_Residue._AllBonds = True | |
# trigger rebuild of hedra for AllBonds | |
if "C" == entity.level: | |
entity.internal_coord.ordered_aa_ic_list[0].hedra = {} | |
delattr(entity.internal_coord, "hAtoms_needs_update") | |
delattr(entity.internal_coord, "hedraLen") | |
else: | |
for chn in entity.get_chains(): | |
chn.internal_coord.ordered_aa_ic_list[0].hedra = {} | |
delattr(chn.internal_coord, "hAtoms_needs_update") | |
delattr(chn.internal_coord, "hedraLen") | |
entity.atom_to_internal_coordinates() | |
IC_Residue._AllBonds = allBondsStash | |
# rebuild atom coordinates now with chain starting at origin: in OpenSCAD | |
# code, each residue model is transformed to N-Ca-C start position instead | |
# of updating transform matrix along chain | |
entity.internal_to_atom_coordinates() | |
with as_handle(file, "w") as fp: | |
if includeCode: | |
fp.write(peptide_scad) | |
if not pdbid and hasattr(entity, "header"): | |
pdbid = entity.header.get("idcode", None) | |
if pdbid is None or "" == pdbid: | |
pdbid = "0PDB" | |
fp.write( | |
'protein = [ "' + pdbid + '", ' + str(scale) + ", // ID, protein_scale\n" | |
) | |
if "S" == entity.level or "M" == entity.level: | |
for chn in entity.get_chains(): | |
fp.write(" [\n") | |
chn.internal_coord._write_SCAD( | |
fp, backboneOnly=backboneOnly, start=start, fin=fin | |
) | |
fp.write(" ]\n") | |
elif "C" == entity.level: | |
fp.write(" [\n") | |
entity.internal_coord._write_SCAD( | |
fp, backboneOnly=backboneOnly, start=start, fin=fin | |
) | |
fp.write(" ]\n") | |
elif "R" == entity.level: | |
raise NotImplementedError("writescad single residue not yet implemented.") | |
fp.write("\n];\n") | |
if maxPeptideBond is not None: | |
IC_Chain.MaxPeptideBond = mpbStash | |
peptide_scad = """ | |
/* | |
// | |
// peptide.scad | |
// Copyright (c) 2019 Robert T. Miller. All rights reserved. | |
// This file is part of the Biopython distribution and governed by your | |
// choice of the "Biopython License Agreement" or the "BSD 3-Clause License". | |
// Please see the LICENSE file that should have been included as part of this | |
// package. | |
// | |
// This is the support file to build an OpenSCAD (http://www.openscad.org/) model | |
// of a protein from internal coordinates. The resulting model may be constructed | |
// on a 3D printer. | |
// | |
// data matrices should be appended below to form a program ready to load into | |
// the OpenSCAD application. | |
// | |
// The protein_scale value used throughout is the second element of the | |
// protein[] array appended below. | |
// This is the value supplied when generating the data for build units per | |
// PDB angstrom. | |
// You may wish to modify it here to adjust the appearance of the model in | |
// terms of atom sphere or bond cylinder diameter, however the bond lengths | |
// are fixed with the supplied value when the data matrices are generated. | |
// Atom sphere and bond cylinder radii may be individually adjusted below as | |
// well. | |
// | |
// $fn (fragment number) is an OpenSCAD parameter controlling the smoothness | |
// of the model surface. Smaller values will render faster, but yield more | |
// 'blocky' models. | |
// | |
// This is intended to be a working example, you are encouraged to modify the | |
// OpenSCAD subroutines below to generate a model to your liking. For more | |
// information, start with http://www.openscad.org/cheatsheet/index.html | |
// | |
// Note especially the hedronDispatch() subroutine below: here you may select | |
// hedra based on residue, sequence position, and class (hedron atoms) for | |
// special handling. Also see the per hedron render options in the hedra[] | |
// array. | |
// | |
// If you modify this file, you may find it useful to generate the data | |
// matrices without this OpenSCAD code by calling write_SCAD() with the | |
// includeCode=False option, then use the OpenSCAD 'include <>' facility at | |
// the end of your modified OpenSCAD program. | |
*/ | |
rotate([-90,0,0]) // convenient for default location (no N-Ca-C start coordinates) | |
chain(protein); // this is the main subroutine call to build the structure | |
// top-level OpenSCAD $fn for visible surfaces. Rotatable bonds use $fn=8 | |
// inside, regardless of this setting. | |
$fn = 0; // 0 yields OpenSCAD default of 30. $n=8 should print with minimal support | |
tubes=false; // style: render atoms and bonds as constant diameter cylinders, preferred for rotatable bonds / h-bonds | |
support=false; // enable print-in-place internal support for rotatable bonds | |
// N.B. rotatable bonds must be parallel to build plate for internal support | |
// structures to be generated correctly by slicer | |
// output parameters | |
atomScale=1.0; // 0.8 better for rotatable bonds | |
defaultAtomRadius = 0.77; // used if tubes = true | |
bondRadius = (tubes ? defaultAtomRadius * atomScale : 0.4); | |
jBondRadius = defaultAtomRadius * atomScale; // radius for rotatable bonds | |
// general printer, slicer, print settings | |
layerHeight=0.15; // must match slicer setting for print-in-place support | |
clearance=0.3; // sliding clearance - can be smaller (0.2) if not doing print-in-place | |
pClearance=0.2; // press-fit clearance (magnets for h-bonds) | |
shim=0.05; // extra to make OpenSCAD surfaces distinct in difference() | |
nozzleDiameter=0.4; | |
// need one magnet for each side of hydrogen bond, suggest 3mm x 5mm e.g. from eBay | |
// use compass to identify poles if you care, North pointing (red) repels compass North pointing | |
magR=3/2; // magnet radius | |
magL=5; // magnet length | |
// for $fn=8 which works nice on fdm printer | |
oRot = 22.5; // 45/2, rotate to make fn=8 spheres and cylinders flat on build plate | |
apmFac = cos(180/8); // apothem factor - multiply by radius for center to octagon side distance | |
octSide = 2* tan(180/8); // multiply by radius to get length of octagon side | |
// for values of $fn: | |
fnRot = ($fn ? 90-(180/$fn) : 90-(180/30)); | |
bondLenFac = 0.6; // fraction of bond length to extend from atom for each arm of hedron in join | |
hblen = 1.97; // hydrogen bond length | |
wall = 3*nozzleDiameter; | |
joinerStep = 1; // radius difference between rotatable bond axle and end knob inside bond cylinder | |
caTop = false; // only make top of N_C-alpha_C hedron plus C-beta (see hedron() and hedron_dispatch() examples) | |
/* | |
// | |
// Generate a sphere to represent an atom. | |
// Colour and size determined for the atom covalent radius specified by the | |
// parameter 'a' by lookup in the atomData table below, then scaled by the | |
// supplied parameter 'scal'. | |
// | |
// scal : protein_scale | |
// clr : additional radius if used to create clearance for rotatable bonds | |
// | |
*/ | |
module atom(a,scal,clr=0) | |
{ | |
ad = atomData[search([a],atomData)[0]]; | |
color(ad[1]) { | |
rotate([0,0,fnRot]) sphere(r=((ad[2]*atomScale)*scal)+clr); | |
} | |
} | |
/* | |
// | |
// a hedron (below) may be 'reversed' in terms of the order of its two bonds; | |
// this function fixes the ordering | |
// | |
*/ | |
function hFlip(h,rev) = | |
// yes reversed : not reversed | |
// 0 1 2 3 4 5 6 7 : 0 1 2 3 4 5 6 7 | |
// len1 len3 atom1 atom3 a1 a2 a1-a2 a2-a3 len1 len3 atom1 atom3 a1 a3 a1-a2 a2-a3 | |
(rev ? [ h[2], h[0], h[5], h[3], h[8], h[6], h[10], h[9] ] : [ h[0], h[2], h[3], h[5], h[6], h[8], h[9], h[10] ]); | |
// h[1] = angle2 for both cases | |
/* | |
// | |
// generate the male or female interior cylinders of a rotating bond | |
// | |
*/ | |
module joinUnit(cOuterLen, cOuterRad, cInnerLen, cInnerRad, male=false) { | |
if (male) { | |
rotate([0,0,oRot]) { | |
cylinder(h=cInnerLen, r=cInnerRad, center=false, $fn=8); | |
cylinder(h=cOuterLen, r=cOuterRad, center=false, $fn=8); | |
} | |
} else { | |
rotate([0,0,fnRot]) { | |
cylinder(h=cInnerLen, r=cInnerRad, center=false, $fn=30); | |
cylinder(h=cOuterLen, r=cOuterRad, center=false, $fn=30); | |
} | |
} | |
} | |
/* | |
// | |
// create a rotatable bond | |
// | |
// supportSel : 0 for no support, 1 or 2 for support on top or bottom (needed | |
// for reversed hedra) | |
// | |
*/ | |
module joiner(bondlen, scal, male=0, ver=0, supportSelect=0) { // ver = differentiate joiner part lengths to guide assembly, but not used | |
lenfac = bondLenFac; | |
jClr = clearance+0.05; | |
cOuterRad = (jBondRadius * scal) - (2*wall + (male ? jClr/2 : -jClr/2)); | |
cInnerRad = cOuterRad - joinerStep; // m/f jClr already in cOuterRad; - (male ? 0 : -0*jClr/2); | |
hArmLen = (bondlen * lenfac); | |
lenClr = 0.6*jClr; // length clearance applied to male and female both, so effective clearance is 2x this value | |
cOuterLen = hArmLen * lenfac + (ver ? 0.5 : - 0.5) - (wall+ (male ? lenClr*2 : -lenClr*2 )); | |
joinerOffset = (hArmLen * (1 - lenfac)) + (male ? lenClr : -lenClr) - (ver ? 1 : 0); | |
i=supportSelect-1; | |
oside = cOuterRad*octSide; | |
wid = oside+2*wall+4*jClr+1; | |
if (male) { | |
rotate([0,180,0]) | |
translate([0,0,-(bondlen-joinerOffset)]) { | |
difference() { | |
joinUnit(cOuterLen, cOuterRad, bondlen, cInnerRad, male=true); | |
if (supportSelect) { | |
rotate([0,0,i*180]) { | |
translate([0,(cOuterRad*apmFac)-0.5*layerHeight,cOuterLen/2]) { | |
cube([oside+2*shim,layerHeight+shim,cOuterLen+2*shim],center=true); | |
} | |
} | |
} | |
} | |
if (supportSelect) { | |
rotate([0,0,i*180]) { | |
translate([0,(cOuterRad*apmFac)-0.5*layerHeight,cOuterLen/2]) { | |
for (j=[0:1]) { | |
rotate([0,(j?60:-60),0]) | |
cube([wid,layerHeight,2*nozzleDiameter],center=true); | |
} | |
} | |
} | |
} | |
} | |
} else { | |
translate([0,0,joinerOffset]) { | |
joinUnit(cOuterLen, cOuterRad, bondlen, cInnerRad); | |
if (supportSelect) { // extra gap top and bottom because filament sags | |
supHeight = max(5*layerHeight,2*(cOuterRad-cOuterRad*apmFac)); // double because center=true below | |
for(j=[0:1]) { | |
rotate([0,0,j*180]) { | |
translate([0,(cOuterRad*apmFac),cOuterLen/2]) { | |
cube([oside+2*shim,supHeight+shim,cOuterLen+2*shim],center=true); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
/* | |
// | |
// create bond with different options (regular, skinny, h-bond atom, rotatable | |
// male or female | |
// | |
// parameters: | |
// bl : bond length | |
// br : bond radius | |
// scal : protein_scale | |
// key : option symbols defined below | |
// atm : atomic element symbol, used for color and radius by atom() routine above | |
// ver : make rotatable bonds slightly different based on value; currently unused | |
// supporSel : enable print-in-place support for rotatable bonds | |
// | |
*/ | |
// option symbols - these names generated in BioPython code so avoid changing without thought | |
StdBond = 1; | |
FemaleJoinBond = 2; | |
MaleJoinBond = 3; | |
SkinnyBond = 4; // Calpha - Cbeta bond cylinder needs to be skinny for clearance with rotating bonds | |
HBond = 5; // make room inside atom/bond to insert magnet to appropriate depth | |
module bond(bl, br, scal, key, atm, ver, supportSel=0) { | |
br = (key == FemaleJoinBond ? jBondRadius * scal : br) * (key == SkinnyBond ? 0.65 : 1); // bond radius smaller for skinnyBond | |
bl = (key == FemaleJoinBond ? bl * bondLenFac : bl); // make female joiner shorter | |
if (key == MaleJoinBond) { // male join is direct solid, others need difference() | |
joiner(bl, scal, male = true, ver = ver, supportSelect=supportSel); | |
} else { // regular bond / skinny / h-bond / female join | |
bhblen = bl +(hblen/2 * scal); | |
rotate([0,0,fnRot]) { | |
difference() { | |
union() { | |
cylinder(h=bl,r=br,center=false); | |
if (key == HBond) { // make extension collar for h-bond magnet | |
rotate([0,0,oRot-fnRot]) cylinder(h=bhblen-1,r=(magR + clearance +wall),center=false, $fn=8); | |
} | |
} | |
atom(atm,scal,-clearance); // remove overlap with atom to clear area for female join | |
if (key == HBond) { // make space to insert magnet inside bond cylinder | |
translate([0,0,(bhblen-magL)-pClearance]) | |
cylinder(h=magL+pClearance+shim, r=magR+pClearance, center=false, $fn=8); | |
} | |
} | |
} | |
} | |
} | |
/* | |
// | |
// Generate a 'hedron', one plane of 3 points, consisting of 3 atoms joined by | |
// two bonds. | |
// Defined as bond length - bond angle - bond length | |
// | |
// In some cases the sequence of atoms in the h[] array is reversed (rev flag), | |
// as detailed in the comments. | |
// | |
// other parameters: | |
// | |
// h = hedron array data according to rev flag: | |
// yes reversed : not reversed | |
// 0 1 2 3 4 5 6 7 : 0 1 2 3 4 5 6 7 | |
// len1 len3 atom1 atom3 a1 a2 a1-a2 a2-a3 len1 len3 atom1 atom3 a1 a3 a1-a2 a2-a3 | |
// | |
// split: chop half of the hedron - to selectively print parts of a rotating | |
// bond to be glued together. top or bottom half selected by global caTop | |
// (C-alpha top) variable, undef by default so bottom half. | |
// | |
// supporSel: enable support structure inside rotatable bond to print in place. | |
// Please note the bond needs to be exactly parallel to the buildplate and the | |
// layerHeight global variable above needs to be set correctly for the | |
// structure to be correctly created by your slicer software. | |
// | |
*/ | |
module hedron(h,rev=0,scal,split=0, supportSel) { | |
newh = hFlip(h, rev); // make a consistent hedron array regardless of rev flag | |
bondRad = bondRadius * scal; | |
difference() { | |
union(){ | |
if (h[7]) { | |
// central atom at 0,0,0 | |
atom(h[4],scal); | |
} | |
if (newh[5] && newh[7] != FemaleJoinBond) { // not female join | |
// comments for non-reversed case | |
// atom 3 is len3 up on +z | |
translate([0,0,newh[1]]) | |
difference() { | |
atom(newh[3],scal * (newh[7] == SkinnyBond ? 0.7 : 1)); // if skinny bond make atom (C-beta) same diameter as bond | |
if (newh[7] == HBond) { // make room for hbond magnet through atom - this branch not used for backbone N,O | |
translate([0,0,scal*hblen/2-magL-pClearance]) | |
cylinder(h=magL+pClearance,r=magR+pClearance,$fn=8); | |
} | |
} | |
} | |
if (newh[7]) { | |
// atom 2 - atom 3 bond from origin up +z distance len3 | |
bond(newh[1], bondRad, scal, newh[7], h[4], ver=1, supportSel=supportSel); | |
} | |
rotate([0, h[1], 0]) { // rotate following elements by angle2 about Y | |
if (newh[6]) { | |
bond(newh[0], bondRad, scal, newh[6], h[4], ver=1, supportSel=supportSel); // h[4] is center atom (atom 2) | |
} | |
if (newh[4] && newh[6] != FemaleJoinBond) { // if draw atom 2 and atom1-atom2 not joiner | |
translate([0,0,newh[0]]) { | |
difference() { | |
atom(newh[2],scal * (newh[6] == SkinnyBond ? 0.7 : 1)); // put atom1 sphere len1 away on Z | |
if (newh[6] == HBond) { // make room for hbond magnet through atom | |
translate([0,0,scal*hblen/2-magL-pClearance]) | |
cylinder(h=magL+pClearance,r=magR+pClearance,$fn=8); | |
} | |
} | |
} | |
} | |
} | |
} | |
if (split) { | |
// top / bottom half cutter | |
thick = 2*bondRadius * scal; | |
Zdim = newh[0]; | |
Xdim = newh[1]; | |
cside = 7* defaultAtomRadius * atomScale * scal / 12 + (caTop ? pClearance : -pClearance); | |
difference() { | |
translate([-Xdim,((rev || caTop) ? 0 : -thick),-Zdim]) { | |
cube([2*Xdim,thick,2*Zdim]); | |
} | |
if (!caTop) { | |
rotate([0,(rev ? h[1] : 0),0]) | |
rotate([45,0,0]) | |
cube([cside, cside, cside],center=true); | |
} | |
} | |
if (caTop) { | |
//translate([tx+cside,0,tx+cside]) | |
rotate([0,(rev ? h[1] : 0),0]) | |
rotate([45,0,0]) | |
cube([cside, cside, cside], center=true); | |
} | |
} | |
if (newh[7] == FemaleJoinBond) { // female join | |
joiner(newh[1], scal, male=false, ver=1, supportSelect=supportSel); | |
} | |
if (newh[6] == FemaleJoinBond) { // female join | |
rotate([0, h[1], 0]) { // rotate following elements by angle2 about Y | |
joiner(newh[0], scal, male=false, ver=1, supportSelect=supportSel); | |
translate([0,0,newh[0]]) | |
atom(newh[2],scal+0.5,clearance); // clearance for atom against join outer cylinder | |
} | |
} | |
if (newh[7] == FemaleJoinBond || newh[6] == FemaleJoinBond) { // female join both hedron arms | |
translate([0,0,newh[1]]) atom(newh[3],scal+0.5,clearance); // clearance for atom against join outer cylinder | |
} | |
} | |
} | |
/* | |
// | |
// Hook to call custom routines for specific hedra. | |
// | |
// Residue is h[h_residue] | |
// Sequence position is h[h_seqpos] | |
// | |
*/ | |
module hedronDispatch(h,rev=0,scal) { | |
// default action is just to pass to hedron() | |
hedron(h, rev, scal, 0, (support ? 1 : 0)); | |
/* | |
// Some examples for special handling for specific hedra below: | |
// note use of h_seqpos, h_residue, h_class for selecting hedra | |
// bool flag caTop (for rotatable bond part) needs to be a global variable | |
// so hedron() above can see it. | |
caBase1 = false; // only make bottom of N_C-alpha_C hedron | |
caBase2 = false; // same as caBase1 but for case of reversed hedron (for testing, should be identical to caBase1 result) | |
amideOnly = false; // make only the first amide | |
if (caTop) { | |
// these examples select a specific sequence position (h[h_seqpos] == n) | |
if (h[h_seqpos] == 1) { | |
if (h[h_class] == "NCAC") { | |
hedron(h, rev, scal, 1); | |
} else if (h[h_class] == "CBCAC") { | |
color("yellow") { // ca-cb | |
hedron(h, rev, scal); | |
} | |
} | |
} | |
} else if (caBase1) { | |
if (h[h_seqpos] == 1 && (h[h_class] == "NCAC")) { | |
hedron(h, rev, scal, true, (support ? 1 : 0)); | |
} | |
} else if (caBase2) { | |
if (h[h_seqpos] == 5 && (h[h_class] == "NCAC")) { | |
hedron(h, rev, scal, true, (support ? 1 : 0)); | |
} | |
} else if (amideOnly) { | |
if (h[h_seqpos] == 1) { | |
if (h[h_class] == "CACN") { | |
color("darkgray") { | |
hedron(h, rev, scal); | |
} | |
} else if (h[h_class] == "CACO") { | |
color("red") { // c=o | |
hedron(h, rev, scal); | |
} | |
} else if (h[h_class] == "CNCA") { | |
color("cyan") { // h=n | |
hedron(h, rev, scal); | |
} | |
} | |
} else if ((h[h_seqpos] == 2) && (h[h_class] == "HNCA")) { | |
color("cyan") { // h=n | |
hedron(h, rev, scal); | |
} | |
} | |
// actions above select out only a single hedron | |
} else { | |
// actions below will process hedra all but handle selected ones differently | |
if (h[h_class] == "NCAC") { | |
if (h[h_seqpos] == 1) { | |
if (! CCap && NCap) { // make split rotatable bond for terminal NH3 | |
hedron(h, rev, scal, true, (support ? 1 : 0)); | |
} | |
} else if (h[h_seqpos] == 5) { // make split rotatable bond for terminal COOH | |
hedron(h, rev, scal, true, (support ? 2 : 0)); // note supportSel = 2 | |
} else { | |
hedron(h, rev, scal, 0, (support ? 2 : 0)); | |
} | |
} else if (h[h_class] == "CBCAC") { | |
color("yellow") { // ca-cb -- color yellow in OpenSCAD renderer | |
if (h[h_seqpos] == 1 ) { // don't make here for N-term | |
} else if (h[h_seqpos] == 5 ) { // don't make here for C-term | |
} else { | |
hedron(h, rev, scal); // otherwise do make here | |
} | |
} | |
} else if (h[h_class] == "HNCA") { | |
color("cyan") { // color h-n in OenSCAD renderer | |
if (h[h_seqpos] == 1) { | |
if (NCap) { // only make at N term if variable NCap is true | |
hedron(h, rev, scal, 0, (support ? 1 : 0)); | |
} | |
} else { | |
hedron(h, rev, scal, 0, (support ? 1 : 0)); | |
} | |
} | |
} else if (h[h_residue] == "P") { | |
color("darkgray") // highlight Prolines in OpenSCAD renderer | |
hedron(h, rev, scal); | |
} else { | |
echo("unrecognised hedron", h[h_class]); | |
color("pink") | |
hedron(h, rev, scal, 0, (support ? 1 : 0)); | |
} | |
} | |
*/ | |
} | |
/* | |
// | |
// Generate a hedron rotated to specific angle d | |
// | |
*/ | |
module d2(d,hedra,scal) | |
{ | |
tz = (d[d_reversed] ? hedra[d[d_h2ndx]][2] : hedra[d[d_h2ndx]][0]); // get h2 len1 depending on reversed | |
rotate(d[d_dangle1]) { // 4. rotate h2 to specified dihedral angle | |
translate([0,0,tz]) { // 3. translate h2 h2:len1 up +z | |
rotate([180, 0, 0]) { // 2. rotate h2r about X so h2:a3 in +z and h2:a1 in -z | |
hedronDispatch(hedra[d[d_h2ndx]],(!d[d_reversed]),scal); // 1. reverse hedron 2 orientation = h2r | |
} | |
} | |
} | |
} | |
/* | |
// | |
// Generate two hedra at specified dihedral angle d | |
// | |
*/ | |
module dihedron(d,hedra,scal) | |
{ | |
if (d[d_h1new]) | |
hedronDispatch(hedra[d[d_h1ndx]],d[d_reversed],scal); // reverse h1 if dihedral reversed | |
if (d[d_h2new]) | |
d2(d,hedra,scal); | |
} | |
/* | |
// | |
// Generate a residue consisting of the set of dihedra in the parameter 'r', | |
// referring to hedra the table specified in the parameter 'hedra'. | |
// | |
*/ | |
module residue(r,hedra, scal) | |
{ | |
for (d = r) { | |
multmatrix(d[d_dihedralTransform]) { | |
dihedron(d, hedra, scal); | |
} | |
} | |
} | |
/* | |
// | |
// Generate a chain of residues, each positioned by a supplied | |
// rotation/translation matrix. | |
// | |
*/ | |
module chain(protein) | |
{ | |
chnD = protein[p_chainData]; | |
c = chnD[c_residues]; | |
dihedra = chnD[c_dihedra]; | |
hedra = chnD[c_hedra]; | |
for (r = c) { | |
multmatrix(r[r_resTransform]) { | |
residue(dihedra[r[r_resNdx]],hedra, protein[p_proteinScale]); | |
} | |
} | |
} | |
/* | |
// | |
// OpenSCAD array indices to reference protein data - tied to BioPython code | |
// | |
*/ | |
// protein base level | |
p_pdbid = 0; | |
p_proteinScale = 1; | |
p_chainData = 2; | |
// chain level data | |
c_chainID = 0; | |
c_dihedra = 1; | |
c_hedra = 2; | |
c_residues = 3; | |
// hedra definitions | |
h_len1 = 0; | |
h_angle2 = 1; | |
h_len3 = 2; | |
h_atom1class = 3; | |
h_atom2class = 4; | |
h_atom3class = 5; | |
h_atom1state = 6; | |
h_atom2state = 7; | |
h_atom3state = 8; | |
h_bond1state = 9; | |
h_bond2state = 10; | |
h_residue = 11; | |
h_seqpos = 12; // residue sequence position for first atom in hedra | |
h_class = 13; | |
// dihedra specifications for each residue in sequence, dihedral array | |
d_dangle1 = 0; | |
d_h1ndx = 1; | |
d_h2ndx = 2; | |
d_reversed = 3; | |
d_h1new = 4; | |
d_h2new = 5; | |
d_dihedralTransform = 6; | |
// residueSet: world transform for each residue in sequence array | |
r_resNdx = 0; | |
r_resID = 1; | |
r_resTransform = 2; | |
// use single default atom radius for all atoms if tubes = true, else use | |
// covalent radii from literature | |
atomData = ( tubes ? | |
[ ["Csb","green" , defaultAtomRadius], ["Cres","green" , defaultAtomRadius], ["Cdb","green" , defaultAtomRadius], | |
["Osb","red" , defaultAtomRadius], ["Ores","red" , defaultAtomRadius], ["Odb","red" , defaultAtomRadius], | |
["Nsb","blue" , defaultAtomRadius], ["Nres","blue" , defaultAtomRadius], ["Ndb","blue" , defaultAtomRadius], | |
["Hsb","gray" , defaultAtomRadius], | |
["Ssb","yellow" , defaultAtomRadius] ] | |
: | |
// covalent radii from Heyrovska, Raji : 'Atomic Structures of all the Twenty | |
// Essential Amino Acids and a Tripeptide, with Bond Lengths as Sums of Atomic | |
// Covalent Radii' https://arxiv.org/pdf/0804.2488.pdf | |
[ ["Csb","green" , 0.77], ["Cres","green" , 0.72], ["Cdb","green" , 0.67], | |
["Osb","red" , 0.67], ["Ores","red" , 0.635], ["Odb","red" , 0.60], | |
["Nsb","blue" , 0.70], ["Nres","blue" , 0.66], ["Ndb","blue" , 0.62], | |
["Hsb","gray" , 0.37], | |
["Ssb","yellow" , 1.04] ] | |
); | |
// optionally include protein array data here [ write_SCAD(includeCode=False) ], e.g.: | |
// include <1rtm.scad>; | |
// or paste below | |
""" # noqa | |