"""
CLI tool to place lipids to assign circular domains around inclusions or points.
"""
import argparse
from pathlib import Path
import numpy as np
import logging
from dataclasses import dataclass
from typing import List, Optional, Sequence, Dict
from scipy.special import logsumexp
import networkx as nx
from scipy.spatial.distance import cdist
from PointClass.point import Point
logger = logging.getLogger(__name__)
[docs]
@dataclass
class LipidSpec:
"""Specification for a lipid type and its properties"""
domain_id: int
name: str
percentage: float
curvature: float
density: float
[docs]
def parse_lipid_file(file_path: Path) -> List[LipidSpec]:
"""Parse lipid specification file into structured data"""
lipids = []
with open(file_path) as f:
for line in f:
if line.strip() and not line.startswith(';'):
domain_id, name, percentage, curvature, density = line.split()
lipids.append(LipidSpec(
domain_id=int(domain_id),
name=name,
percentage=float(percentage),
curvature=float(curvature),
density=float(density)
))
total = sum(lipid.percentage for lipid in lipids)
if not np.isclose(total, 1.0, atol=0.01):
raise ValueError(f"Lipid percentages must sum to 1.0 (got {total:.2f})")
return lipids
def _get_centers(membrane,Type,dummys):
from_type=[inc["point_id"] for inc in membrane.inclusions.get_by_type(Type)]
from_dummy=dummys.strip().split(",")
to_set=list(set(from_type+from_dummy))
return list(filter(None,to_set))
def _dijkstra_within_radius(distance_matrix, start_index, radius,percent=100):
if percent != 100:
distance_matrix = distance_matrix[distance_matrix < np.percentile(distance_matrix,percent)] = 0
G = nx.from_numpy_array(distance_matrix)
shortest_paths = nx.single_source_dijkstra_path_length(G, start_index)
reachable_nodes = [node for node, dist in shortest_paths.items() if dist < radius]
return reachable_nodes
def _within_radius(distance_matrix,start_index,radius):
reachable_nodes=[]
for i,value in enumerate(distance_matrix[start_index]):
if value <= radius:
reachable_nodes.append(i)
return reachable_nodes
[docs]
def circular_domains(membrane: Point, radius: float, pointid: list, domain: int, path_dist: bool=False, percent: float=100.0, layer: str = "both") -> None:
"""Assign lipids to domains based on curvature preferences"""
layers = [membrane.outer]
if membrane.monolayer or layer.lower() == "outer":
layers = [membrane.outer]
elif layer.lower() == "inner":
layers = [membrane.inner]
else:
layers = [membrane.outer, membrane.inner]
for layer in layers:
dist_matrix = cdist(layer.coordinates, layer.coordinates)
for point in pointid:
if path_dist:
nodes=_dijkstra_within_radius(dist_matrix,int(point),radius,percent)
else:
nodes=_within_radius(dist_matrix,int(point),radius)
for index in nodes:
layer.domain_ids[index]=domain
[docs]
def DAI(args: List[str]) -> None:
"""Main entry point for Domain Placer tool"""
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-p','--point_dir',default="point/",help="Specify the path to the point folder")
parser.add_argument('-r','--radius',default=1,type=float,help="The radius around a protein in which domains should be changed.")
parser.add_argument('-t','--type',default=0,type=int,help="The protein type around which domains should be changed.")
parser.add_argument('-d','--Domain',default=1,type=int,help="The domain number that should be set around the protein.")
parser.add_argument('-l','--leaflet',default="both",help="Choose which membrane leaflet to alter. Default is both")
parser.add_argument('-dummy','--dummy',default="",help="Create a dummy protein to place a circular domain around it. Excepts pointids like 3,7,22")
parser.add_argument('-pd','--path_distance',default=False,type=bool,help="Slower execution, but needed for higher curvature membranes to assign the domain to only one membrane part")
parser.add_argument('-pdP','--path_distance_percentile',default=100.0,type=float,help="Manipulates neighbors in path distance, tests have shown that 2 works well")
args = parser.parse_args(args)
logging.basicConfig(level=logging.INFO)
try:
membrane = Point(args.point_dir)
centers=_get_centers(membrane, args.type,args.dummy)
circular_domains(membrane=membrane,radius=args.radius,pointid=centers,domain=args.Domain, layer=args.leaflet,path_dist=args.path_distance,percent=args.path_distance_percentile)
output_dir = args.point_dir
membrane.save(output_dir)
logger.info(f"Updated membrane domains in {output_dir}")
except Exception as e:
logger.error(f"Error: {e}")
raise