mempro 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mempro
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Membrane Protein Orientation in Lipid Bilayers
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -107,7 +107,7 @@ MemPrO is intended to be run on PDB files, and will output the following files:
107
107
 
108
108
  **-ng, --grid_size** — Number of initial starting configurations. Defaults to 36. For best efficiency this should be a multiple of the number of CPUs being used. More starting configurations sample the space of minima better, but there are diminishing returns and generally no more than 40 are needed.
109
109
 
110
- **-nc, --num_cpus** — Number of CPU cores to use. Defaults to all available cores. For best performance with `-ng`, set `-nc` to a value that divides `-ng` evenly.
110
+ **-nc, --num_cpus** — Number of CPU cores to use. Defaults to the number of performance cores where the system reports them (e.g. 4 on an Apple M4, whose 6 efficiency cores would slow each batch down), otherwise all available cores. For best performance with `-ng`, set `-nc` to a value that divides `-ng` evenly.
111
111
 
112
112
  **-dev, --device** — Device to run on: `auto` (default; GPU if JAX can see one, otherwise CPU), `cpu` or `gpu`. On GPU, all starting configurations are minimised together in a single batch. GPU support needs `pip install "mempro[gpu]"` (Linux, NVIDIA).
113
113
 
@@ -94,7 +94,7 @@ MemPrO is intended to be run on PDB files, and will output the following files:
94
94
 
95
95
  **-ng, --grid_size** — Number of initial starting configurations. Defaults to 36. For best efficiency this should be a multiple of the number of CPUs being used. More starting configurations sample the space of minima better, but there are diminishing returns and generally no more than 40 are needed.
96
96
 
97
- **-nc, --num_cpus** — Number of CPU cores to use. Defaults to all available cores. For best performance with `-ng`, set `-nc` to a value that divides `-ng` evenly.
97
+ **-nc, --num_cpus** — Number of CPU cores to use. Defaults to the number of performance cores where the system reports them (e.g. 4 on an Apple M4, whose 6 efficiency cores would slow each batch down), otherwise all available cores. For best performance with `-ng`, set `-nc` to a value that divides `-ng` evenly.
98
98
 
99
99
  **-dev, --device** — Device to run on: `auto` (default; GPU if JAX can see one, otherwise CPU), `cpu` or `gpu`. On GPU, all starting configurations are minimised together in a single batch. GPU support needs `pip install "mempro[gpu]"` (Linux, NVIDIA).
100
100
 
@@ -75,11 +75,27 @@ def _pre_parse_device_args():
75
75
  _pre_parse_device_args()
76
76
 
77
77
  #We need to force JAX to fully utilize a multi-core cpu
78
- no_cpu = int(os.environ.get("NUM_CPU", os.cpu_count()))
78
+ #By default one device per performance core: each batch of minimisations waits for its slowest device, so on CPUs with
79
+ #efficiency cores (e.g. Apple Silicon, 4P+6E on an M4) using every core is slower (4G1U: 24 s on 10 cores, 16 s on 4)
80
+ def default_device_count():
81
+ try:
82
+ import subprocess
83
+ perf_cores = subprocess.run(["sysctl","-n","hw.perflevel0.physicalcpu"],capture_output=True,text=True,timeout=5)
84
+ if perf_cores.returncode == 0 and perf_cores.stdout.strip().isdigit():
85
+ return int(perf_cores.stdout.strip())
86
+ except (OSError,subprocess.SubprocessError):
87
+ pass
88
+ return os.cpu_count()
89
+ no_cpu = int(os.environ.get("NUM_CPU", default_device_count()))
79
90
  os.environ["XLA_FLAGS"] = (os.environ.get("XLA_FLAGS", "")+" --xla_force_host_platform_device_count="+str(no_cpu)).strip()
91
+ jax_version = tuple(int(v) for v in jax.__version__.split(".")[:3] if v.isdigit())
80
92
  #The legacy (non-thunk) CPU runtime is faster for MemPrO's scan/cond-heavy loops but only exists up to JAX 0.6.0
81
- if tuple(int(v) for v in jax.__version__.split(".")[:3] if v.isdigit()) <= (0,6,0):
93
+ if jax_version <= (0,6,0):
82
94
  os.environ["XLA_FLAGS"] += " --xla_cpu_use_thunk_runtime=false"
95
+ #XLA's YNNPACK CPU fusions (JAX >= 0.9) intermittently overflow a worker thread's stack on the large vectorised
96
+ #potentials in MemPrO/MemPrOD (SIGBUS in slinky::eval_non_inlined); turning them off costs no measurable speed
97
+ if jax_version >= (0,9,0) and "xla_cpu_experimental_ynn_fusion_type" not in os.environ["XLA_FLAGS"]:
98
+ os.environ["XLA_FLAGS"] += " --xla_cpu_experimental_ynn_fusion_type="
83
99
 
84
100
 
85
101
  #This is useful for debugging nans in grad
@@ -558,36 +574,19 @@ class PDB_helper:
558
574
  total = get_sph_circ(total,jnp.array([0,0]),jnp.pi)
559
575
  total_area = jnp.sum(total)
560
576
  #jax.debug.print("TA{x}",x=total_area)
561
- is_surf = jnp.zeros((points.shape[0]))
562
- def sloopv3_1(is_surf,ind):
563
- pind = ind
564
- ang_grid = jnp.zeros((40,20))
565
- def sloopv3_2(ang_grid,ind):
566
- direc = points[pind]-points[bpoints[pind,ind]]
567
- dist = jnp.linalg.norm(direc)
568
- def far(ang_grid):
569
- return ang_grid
570
- def nfar(ang_grid):
571
- #angle = (jnp.pi/3)*srad/jnp.max(jnp.array([4.0,dist]))#jnp.arccos(2*dist/srad)#0.6
572
- angle = jnp.arccos(jnp.clip(dist/(2*srad),0,1))
573
- cen_y = jnp.arctan2(direc[2],jnp.sqrt(direc[0]*direc[0]+direc[1]*direc[1]))
574
- cen_x = jnp.arctan2(direc[1],direc[0])
575
- ang_grid = get_sph_circ(ang_grid,jnp.array([cen_x,cen_y]),angle)
576
- return ang_grid
577
- ang_grid = jax.lax.cond((dist < srad*3)*(dist>1e-5),nfar,far,ang_grid)
578
- return ang_grid,ind
579
- ang_grid,_ = jax.lax.scan(sloopv3_2,ang_grid,jnp.arange(mrad))
580
- perc = jnp.sum(ang_grid)/total_area
581
- #jax.debug.print("Perc {x}",x=perc)
582
- def gthresh(is_surf):
583
- is_surf = is_surf.at[ind].set(1)
584
- return is_surf
585
- def lthresh(is_surf):
586
- return is_surf
587
- is_surf = jax.lax.cond(perc<thresh,gthresh,lthresh,is_surf)
588
- return is_surf,ind
589
- is_surf,_=jax.lax.scan(sloopv3_1,is_surf,jnp.arange(points.shape[0]))
590
- return is_surf
577
+ #Each nearby bead (closer than 3*srad) covers a circle of directions; a bead is on the surface if less than
578
+ #thresh of its directions are covered. get_sph_circ sets cells, so overlapping circles combine as a logical OR
579
+ def is_surf_fun(pind):
580
+ direc = points[pind]-points[bpoints[pind]]
581
+ dist = jnp.linalg.norm(direc,axis=1)
582
+ angle = jnp.arccos(jnp.clip(dist/(2*srad),0,1))
583
+ cen_y = jnp.arctan2(direc[:,2],jnp.sqrt(direc[:,0]*direc[:,0]+direc[:,1]*direc[:,1]))
584
+ cen_x = jnp.arctan2(direc[:,1],direc[:,0])
585
+ circles = jax.vmap(get_sph_circ,in_axes=(None,0,0))(jnp.zeros((40,20)),jnp.stack((cen_x,cen_y),axis=1),angle)
586
+ covered = jnp.any((circles > 0)*((dist < srad*3)*(dist>1e-5))[:,None,None],axis=0)
587
+ perc = jnp.sum(jnp.where(covered,total,0.0))/total_area
588
+ return jnp.where(perc<thresh,1.0,0.0)
589
+ return jax.lax.map(is_surf_fun,jnp.arange(points.shape[0]),batch_size=64)
591
590
 
592
591
  #This function gets the surface residues of a given cg protein
593
592
  def get_surface(self):
@@ -602,26 +601,13 @@ class PDB_helper:
602
601
 
603
602
  #Calculating normals at each point of the cg protein
604
603
  timer = time.time()
605
- @partial(jax.vmap)
604
+ #The normal at a bead points away from the mean of all beads within 32 A (beads processed in batches to bound memory)
606
605
  def normal_fun_2(ind):
607
- def normal_fun_1(ind_fix,ind):
608
- normal_count = jnp.zeros(4)
609
- def in_sphere(normal,posa,posb):
610
- normal = normal.at[3].set(normal[3]+1)
611
- normal = normal.at[:3].set(normal[:3]+(-posa+posb))
612
- return normal
613
- def not_in_sphere(normal,posa,posb):
614
- return normal
615
- normal = jax.lax.cond(jnp.linalg.norm(self.poses[ind]-self.poses[ind_fix])<32,in_sphere,not_in_sphere,normal_count,self.poses[ind],self.poses[ind_fix])#8####################<--------------------------
616
- return ind_fix,normal
617
-
618
- _,normals = jax.lax.scan(normal_fun_1,ind,jnp.arange(self.poses.shape[0]))
619
- normals_sum = jnp.sum(normals,axis=0)
620
- normal = normals_sum[:3]/normals_sum[3]
606
+ near = jnp.linalg.norm(self.poses-self.poses[ind],axis=1)<32
607
+ normal = jnp.sum(jnp.where(near[:,None],self.poses[ind]-self.poses,0.0),axis=0)/jnp.sum(near)
621
608
  normal = normal/jnp.linalg.norm(normal)
622
609
  return normal
623
- shard_nns = jax.device_put(jnp.arange(self.poses.shape[0]),sharding0)
624
- normals = normal_fun_2(shard_nns)
610
+ normals = jax.lax.map(normal_fun_2,jnp.arange(self.poses.shape[0]),batch_size=256)
625
611
 
626
612
  self.normals = jnp.array(jax.device_get(normals))#jax.device_put(normals)#jax.device_put_sharded([normals],[devices[0]])
627
613
 
@@ -646,27 +632,13 @@ class PDB_helper:
646
632
  timer = time.time()
647
633
  #Each point on the surface had an asocciated sphere indicating directions which are exposed
648
634
  #Here we flag the directions which are free
649
- @partial(jax.vmap)
635
+ #A direction is free if no bead lies within 4 A of the point 4 A out along the normal plus that direction
650
636
  def sphere_fun_3(ind):
651
- def sphere_fun_2(ind_fix,ind):
652
- def sphere_fun_1(ind_fix,ind):
653
- def in_prot():
654
- return 0
655
- def not_in_prot():
656
- return 1
657
- return ind_fix,jax.lax.cond(jnp.linalg.norm(self.poses[ind_fix[0]]+normals[ind_fix[0]]*4-self.poses[ind]+self.ball[ind_fix[1]])<4,in_prot,not_in_prot)
658
- _,on_surf_arr=jax.lax.scan(sphere_fun_1,jnp.array([ind_fix,ind]),jnp.arange(self.poses.shape[0]))
659
- on_surf_arr = jnp.array(jax.device_get(on_surf_arr))
660
-
661
- on_surf = jnp.prod(on_surf_arr)
662
- return ind_fix,on_surf
663
- _,on_surf_ball = jax.lax.scan(sphere_fun_2,ind,jnp.arange(self.ball.shape[0]))
664
- ball_arr = on_surf_ball*self.surface[ind]
665
- return ball_arr
666
-
667
-
668
- shard_nn = jax.device_put(jnp.arange(self.poses.shape[0]),sharding0)
669
- spheres = sphere_fun_3(shard_nn)
637
+ test_points = self.poses[ind]+normals[ind]*4+self.ball
638
+ dists = jnp.linalg.norm(test_points[:,None,:]-self.poses[None,:,:],axis=2)
639
+ on_surf_ball = jnp.all(jnp.logical_not(dists<4),axis=1).astype(int)
640
+ return on_surf_ball*self.surface[ind]
641
+ spheres = jax.lax.map(sphere_fun_3,jnp.arange(self.poses.shape[0]),batch_size=32)
670
642
 
671
643
  self.poses = self.poses[:pos_num]
672
644
  self.surface = self.surface[:pos_num,0]
@@ -680,12 +652,8 @@ class PDB_helper:
680
652
  jax.block_until_ready(self.poses)
681
653
 
682
654
  #Here we set the cartesean directions for each sphere (as before we only had flags)
683
- self.spheres = jnp.zeros((surface_number,self.ball.shape[0],3))
684
- def set_sphere_poses_fun_2(sphere_poses,ind):
685
- timeser = jnp.transpose(jnp.array([surface_sphere[ind],surface_sphere[ind],surface_sphere[ind]]))
686
- sphere_poses = sphere_poses.at[ind].set(sphere_poses[ind,:]+timeser*self.ball)
687
- return sphere_poses,ind
688
- self.spheres,_ = jax.lax.scan(set_sphere_poses_fun_2,self.spheres,jnp.arange(surface_sphere.shape[0]))
655
+ #surface_number counts the padding rows too (they match surface_poses), and their spheres stay zero
656
+ self.spheres = jnp.zeros((surface_number,self.ball.shape[0],3)).at[:surface_sphere.shape[0]].set(surface_sphere[:,:,None]*self.ball[None,:,:])
689
657
  surf_mean = jnp.mean(self.surface_poses,axis=0)
690
658
  self.surface_poses = self.surface_poses-surf_mean
691
659
  self.poses = self.poses-surf_mean
@@ -3154,13 +3122,28 @@ def read_martini_lines(martini_file):
3154
3122
  with open(martini_file,"r") as mfile:
3155
3123
  return tuple(mfile.readlines())
3156
3124
 
3157
- #A fucntion that reads a martini file to get interaction strengths
3125
+ #Index of the first line starting with each 12-character "%6s%6s" bead pair, as in [ nonbond_params ]
3126
+ @lru_cache(maxsize=None)
3127
+ def martini_pair_index(martini_file):
3128
+ index = {}
3129
+ for i,line in enumerate(read_martini_lines(martini_file)):
3130
+ index.setdefault(line[:12],i)
3131
+ return index
3132
+
3133
+ #A fucntion that reads a martini file to get interaction strengths (memoised: each pair is looked up once per run)
3134
+ #Pairs are found through martini_pair_index (one pass over the file); any pair not found at the start of a line
3135
+ #falls back to the original search for the first line containing it
3136
+ @lru_cache(maxsize=None)
3158
3137
  def get_int_strength(bead_1,bead_2,martini_file):
3159
3138
  if(bead_2 == "GHOST"):
3160
3139
  return 0
3161
3140
  string = " "*(6-len(bead_1))+bead_1+" "*(6-len(bead_2))+bead_2
3162
3141
  string2 = " "*(6-len(bead_2))+bead_2+" "*(6-len(bead_1))+bead_1
3163
3142
  content = read_martini_lines(martini_file)
3143
+ index = martini_pair_index(martini_file)
3144
+ found = [index[s] for s in (string,string2) if s in index]
3145
+ if found:
3146
+ return -float(content[min(found)][32:45])
3164
3147
  for i,line in enumerate(content):
3165
3148
  if(string in line or string2 in line):
3166
3149
  return -float(line[32:45])
@@ -3405,21 +3388,9 @@ def get_sph_circ(grid,cen,angle):
3405
3388
  dy = jnp.pi/n
3406
3389
  xs = jnp.linspace(0,2*jnp.pi-dx,2*n)+dx/2
3407
3390
  ys = jnp.linspace(0,jnp.pi-dy,n)+dy/2
3408
- def scloop1(grid,ind):
3409
- ind1 = ind
3410
- def scloop2(grid,ind):
3411
- dist = jnp.arccos(jnp.cos(cen[0])*jnp.cos(xs[ind1])+jnp.sin(cen[0])*jnp.sin(xs[ind1])*jnp.cos(cen[1]-ys[ind]))
3412
- def langle(grid):
3413
- grid = grid.at[ind1,ind].set(dx*(jnp.cos(ys[ind]-dy/2)-jnp.cos(ys[ind]+dy/2)))
3414
- return grid
3415
- def gangle(grid):
3416
- return grid
3417
- grid = jax.lax.cond(dist<angle,langle,gangle,grid)
3418
- return grid,ind
3419
- grid,_ = jax.lax.scan(scloop2,grid,jnp.arange(n))
3420
- return grid,ind
3421
- grid,_ = jax.lax.scan(scloop1,grid,jnp.arange(2*n))
3422
- return grid
3391
+ dist = jnp.arccos(jnp.cos(cen[0])*jnp.cos(xs)[:,None]+jnp.sin(cen[0])*jnp.sin(xs)[:,None]*jnp.cos(cen[1]-ys)[None,:])
3392
+ cell_area = dx*(jnp.cos(ys-dy/2)-jnp.cos(ys+dy/2))
3393
+ return jnp.where(dist<angle,jnp.broadcast_to(cell_area[None,:],grid.shape),grid)
3423
3394
 
3424
3395
  def add_Reses(Res, Resitp):
3425
3396
  current_res_no = len(Reses)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mempro
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Membrane Protein Orientation in Lipid Bilayers
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -107,7 +107,7 @@ MemPrO is intended to be run on PDB files, and will output the following files:
107
107
 
108
108
  **-ng, --grid_size** — Number of initial starting configurations. Defaults to 36. For best efficiency this should be a multiple of the number of CPUs being used. More starting configurations sample the space of minima better, but there are diminishing returns and generally no more than 40 are needed.
109
109
 
110
- **-nc, --num_cpus** — Number of CPU cores to use. Defaults to all available cores. For best performance with `-ng`, set `-nc` to a value that divides `-ng` evenly.
110
+ **-nc, --num_cpus** — Number of CPU cores to use. Defaults to the number of performance cores where the system reports them (e.g. 4 on an Apple M4, whose 6 efficiency cores would slow each batch down), otherwise all available cores. For best performance with `-ng`, set `-nc` to a value that divides `-ng` evenly.
111
111
 
112
112
  **-dev, --device** — Device to run on: `auto` (default; GPU if JAX can see one, otherwise CPU), `cpu` or `gpu`. On GPU, all starting configurations are minimised together in a single batch. GPU support needs `pip install "mempro[gpu]"` (Linux, NVIDIA).
113
113
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mempro"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Membrane Protein Orientation in Lipid Bilayers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes