首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >膜蛋白在不同约束模型中的扩散

膜蛋白在不同约束模型中的扩散
EN

Code Review用户
提问于 2022-10-01 20:43:33
回答 2查看 494关注 0票数 11

我是一名初级软件工程师,C++通常是我的主要障碍,但我开始为我在大学做的一个研究项目学习Python。我渴望尽可能多地学习Python语法、技巧、最佳实践或软件架构。这是我的项目~300行代码的一部分,它模拟了两种类型的蛋白质通过布朗扩散的运动:在小的圆形域中的扩散(纳米域模拟)和在有间隔的网络中的扩散(跳扩散)。我希望我能得到任何提示和反馈的代码!

项目结构

代码语言:javascript
复制
.
├── main.py
├── plotGenerator.py
├── requirements-dev.txt
├── simulations
│   ├── __init__.py
│   ├── hopDiffusionSimulation.py
│   ├── nanodomainSimulation.py
│   └── simulation.py
└── util.py

In Action示例

代码

./main.py

代码语言:javascript
复制
from simulations.simulation import *
from simulations.nanodomainSimulation import *
from simulations.hopDiffusionSimulation import *    
    

from plotGenerator import *
from util import *

#nanoDomain = Nanodomain()
hopDiffusion = HopDiffusion();
plot(hopDiffusion, SimulationType.HOPDIFFUSION)

./simulations/simulation.py

代码语言:javascript
复制
from typing import List, Tuple
import random
import numpy as np
from enum import Enum

class SimulationType(Enum):
     BROWNIAN = 1
     NANODOMAIN = 2
     HOPDIFFUSION = 3     

PATH = Tuple[List[float]]
DPI = 100
RADIUS_PADDING = 10
RADIUS = 250
CORRECTED_CANVAS_RADIUS = RADIUS - RADIUS_PADDING

TIME_PER_FRAME: float = 0.02 # 20 ms
DIFFUSION_SPEED_CORRECTION: int = 35 # arbitrary
MEMBRANE_DIFFUSION_COEFFICIENT: float = 0.1 # micrometer^2 / s
MEMBRANE_DIFFUSION_FACTOR: float = 2 * np.sqrt(MEMBRANE_DIFFUSION_COEFFICIENT * TIME_PER_FRAME)
MEMBRANE_DIFFUSION_FATOR_CORRECTED: float = MEMBRANE_DIFFUSION_FACTOR * DIFFUSION_SPEED_CORRECTION

class Simulation:
    
    def __init__(self, n: int = 5):
        self.numberOfParticles: int = n
        self.particlesLocation: List[Tuple[int, int]] = [] 
        self.paths: List[List[Tuple[int, int]]] = []
        
        self.init_particles()
        self.init_paths()
        
    def init_paths(self):
        self.paths.extend([[coordinate] for coordinate in self.particlesLocation])
            
    def init_particles(self) -> None:
        mem: List[Tuple] = []
        
        def get_random_canvas_value(self) -> int:
            return int(random.randint(-(CORRECTED_CANVAS_RADIUS), CORRECTED_CANVAS_RADIUS))
        
        def rec(self, x: int = 0, y: int = 0) -> Tuple[int, int]:
            x, y = [get_random_canvas_value(self) for _ in range(2)]
            while (x, y) in mem:
                return rec(self, x, y)
            mem.append((x, y))
            return x,y
        
        self.particlesLocation.extend([rec(self) for _ in range(5)])

./simulations/hopDiffusionSimulation.py

代码语言:javascript
复制
from typing import List, Tuple
from simulations.simulation import *
from util import *
        
BOUNDARY_THICKNESS: int = 15
NUMBER_OF_COMPARTMENTS_PER_DIRECTION: int = 3
BOUNDARY_JUMP: int  = BOUNDARY_THICKNESS
BOUNDARY_OVERFLOW: int = 20
HOP_PROBABILITY_PERCENTAGE: float = 0.15

class HopDiffusion(Simulation):
    def __init__(self, n: int = 5):
        self.boundary_coordinates_for_plot: List[List] = []
        self.boundary_coordinates: List[Tuple[Tuple]] = []
        self.generate_boundaries()
        
        super().__init__(n)
        
    def generate_boundaries(self):
        step: int = int((RADIUS << 1) / NUMBER_OF_COMPARTMENTS_PER_DIRECTION)
        
        for i in range(6):
            if i % 3 == 0: continue 
            horizontal: bool = i < NUMBER_OF_COMPARTMENTS_PER_DIRECTION
            curr = i * step if horizontal else (i - NUMBER_OF_COMPARTMENTS_PER_DIRECTION) * step
            
            width = BOUNDARY_THICKNESS if horizontal else (RADIUS << 1) + (BOUNDARY_OVERFLOW << 1)
            height = BOUNDARY_THICKNESS if not horizontal else (RADIUS << 1) + (BOUNDARY_OVERFLOW << 1)
            x = curr - RADIUS - (BOUNDARY_THICKNESS >> 1) if horizontal else -RADIUS - BOUNDARY_OVERFLOW
            y = curr - RADIUS - (BOUNDARY_THICKNESS >> 1) if not horizontal else -RADIUS - BOUNDARY_OVERFLOW
            
            self.boundary_coordinates_for_plot.append(list([x, y, width, height]))
            self.boundary_coordinates.append(list([tuple((x, x + width)), tuple((y, y + height))]))
    
    @property
    def get_boundary_coordinates(self):
        return self.boundary_coordinates_for_plot

    def can_particle_hop_boundary_probability(self) -> bool:
        return random.random() < HOP_PROBABILITY_PERCENTAGE

    def is_particle_on_specific_boudnary(self, pos: Tuple, idx: int):
        return Util.is_point_within_bounds(pos, self.boundary_coordinates[idx])
    
    def is_particle_on_boundary(self, pos: Tuple):   
        return any(
            Util.is_point_within_bounds(pos, bounds_of_boundary)
            for bounds_of_boundary in self.boundary_coordinates
        ) 
    
    def is_particle_in_compartment(self, particle) -> bool:
        return not self.is_particle_on_boundary(particle)
    
    def get_surrounding_boundary_of_particle(self, pos: Tuple) -> int:
        for idx, bounds_of_boundary in enumerate(self.boundary_coordinates):
            if Util.is_point_within_bounds(pos, bounds_of_boundary):
                return idx
        return -1
    
    def make_particle_jump(self, newPos: Tuple, x_dir: int, y_dir: int):
        surrounding_boundary_idx = self.get_surrounding_boundary_of_particle(newPos)
        
        while (self.is_particle_on_specific_boudnary(newPos, surrounding_boundary_idx)):
            newPos = Util.increment_tuple_by_val(
                newPos, tuple((Util.sign(x_dir), Util.sign(y_dir)))
            )
            
        newPos = Util.increment_tuple_by_val(
            newPos, tuple(
                (Util.sign(x_dir) * BOUNDARY_JUMP, 
                 Util.sign(y_dir) * BOUNDARY_JUMP)
            )
        )
        # Special case: In some instances the jump may land the particle
        # on a subsequent boundary so we repeat the function. We decrement
        # the particle's coordinates until it is out.
        new_surrounding_boundary_idx = self.get_surrounding_boundary_of_particle(newPos)
        while (self.is_particle_on_boundary(newPos)):
            newPos = Util.increment_tuple_by_val(
                newPos, tuple((Util.sign(-x_dir), Util.sign(-y_dir)))
            )
        
        return newPos
    
    def update_path(self, idx: int):
        x, y = self.paths[idx][-1]
        assert(not self.is_particle_on_boundary(tuple((x, y))))
        
        diffusion_factor = MEMBRANE_DIFFUSION_FATOR_CORRECTED
        x_dir, y_dir = [Util.get_random_normal_direction() * diffusion_factor for _ in range(2)]
        newPos = tuple((x + x_dir, y + y_dir))
        
        if self.is_particle_on_boundary(newPos):
            if self.can_particle_hop_boundary_probability():
                newPos = self.make_particle_jump(newPos, x_dir, y_dir)
            else:
                newPos = Util.change_direction(tuple((x, y)), tuple((x_dir, y_dir)))
            
        self.paths[idx].append(newPos)
        
    def update(self):
        for i in range(self.numberOfParticles): self.update_path(i)
        
    def init_particles(self) -> None:
        mem: List[Tuple[int, int]] = []
        
        def get_random_canvas_value(self) -> int:
            return int(random.randint(-(CORRECTED_CANVAS_RADIUS), CORRECTED_CANVAS_RADIUS))
        
        def rec(self, x: int = 0, y: int = 0) -> Tuple[int, int]:
            x, y = [get_random_canvas_value(self) for _ in range(2)]
            while (x, y) in mem or self.is_particle_on_boundary(tuple((x, y))):
                return rec(self, x, y)
            mem.append((x, y))
            return x, y
        
        self.particlesLocation.extend([rec(self) for _ in range(5)])

./simulations/nanodomainDiffusionSimulation.py

代码语言:javascript
复制
from typing import List, Tuple
from simulations.simulation import *
from util import *
        
NANODOMAIN_DIFFUSION_FATOR_CORRECTED: float = MEMBRANE_DIFFUSION_FATOR_CORRECTED * 0.4 # type : ignore

class Nanodomain(Simulation):
    
    def __init__(self, n: int = 5):
        super().__init__(n)
        self.nanodomain_coordinates: List[Tuple[int, int]] = [ 
            (-100, 100), (0, 0), (150, -60), (-130, -160)
        ]
        self.nanodomain_radii: List[int] = [80, 20, 50, 140]
        
    @property
    def get_nanodomain_coordinates(self) -> List[Tuple[int, int]]:
        return self.nanodomain_coordinates
    
    @property
    def get_nanodomain_radii(self) -> List[int]:
        return self.nanodomain_radii
    
    def get_nanodomain_attributes(self) -> List[Tuple]:
        return list(map(
            lambda coord, radius: (coord, radius), 
            self.get_nanodomain_coordinates, 
            self.get_nanodomain_radii
        ))
    
    def is_particle_in_nanodomain(self, particle: Tuple) -> bool:
        return any(
            Util.compute_distance(particle, circle_center) <= radius 
            for circle_center, radius in 
            zip(self.get_nanodomain_coordinates, self.get_nanodomain_radii)
        )
    
    def update_path(self, idx):
        x, y = self.paths[idx][-1]
        diffusion_factor = NANODOMAIN_DIFFUSION_FATOR_CORRECTED if (self.is_particle_in_nanodomain((x, y))) else MEMBRANE_DIFFUSION_FATOR_CORRECTED
        x_dir, y_dir = [Util.get_random_normal_direction() * diffusion_factor for _ in range(2)]
        self.paths[idx].append((x + x_dir, y + y_dir))
        
    def update(self):
        [self.update_path(i) for i in range(self.numberOfParticles)]

./plotGenerator.py

代码语言:javascript
复制
from simulations.hopDiffusionSimulation import HopDiffusion
from simulations.nanodomainSimulation import Nanodomain
from simulations.simulation import *
from util import *

from matplotlib.animation import FuncAnimation # type: ignore
from matplotlib.pyplot import figure
import matplotlib.pyplot as plt
from typing import List, Tuple
import numpy as np
from matplotlib import rcParams # type: ignore

colors: List[str] = ['r', 'b', "orange", 'g', 'y', 'c']
markers: List[str] = ['o', 'v', '<', '>', 's', 'p']
                
def handle_nanodomain(ax, sim: Nanodomain):
    nanodomains = [
        plt.Circle( # type: ignore
            *param,
            color = 'black', 
            alpha = 0.2) 
        for param in sim.get_nanodomain_attributes()
    ]
    [ax.add_patch(nanodomain) for nanodomain in nanodomains]

def handle_hop_diffusion(ax, sim: HopDiffusion):
    compartments = [
        plt.Rectangle( # type: ignore
            tuple((param[0], param[1])),
            param[2], param[3],
            color = 'black',
            alpha = 0.7,
            clip_on = False)
        for param in sim.boundary_coordinates_for_plot
    ]
    [ax.add_patch(boundary) for boundary in compartments]

def get_coordinates_for_plot(sim, idx: int):
    return Util.get_x_coordinates(sim.paths[idx]), Util.get_y_coordinates(sim.paths[idx])

def get_coordinates_for_heads(sim, idx: int):
    return Util.get_last_point(sim.paths[idx])

def set_plot_parameters(ax):
    ax.tick_params(axis = 'y', direction = "in", right = True, labelsize = 16, pad = 20)
    ax.tick_params(axis = 'x', direction = "in", top = True, bottom = True, labelsize = 16, pad = 20)

    ## legends and utilities
    ax.set_xlabel(r"nm", fontsize=16)
    ax.set_ylabel(r"nm", fontsize=16)

    ## border colors
    ax.patch.set_edgecolor('black')  
    ax.patch.set_linewidth('2') 

    ax.set_xlim(-RADIUS, RADIUS)
    ax.set_ylim(-RADIUS, RADIUS)
    
def plot(sim: Simulation, type: SimulationType):
    fig, ax = plt.subplots(figsize = [5, 5], dpi = DPI) # type: ignore

    path_plots: List = [
        ax.plot(
            *get_coordinates_for_plot(sim, i), 
            markersize=15, color = colors[i])[0] 
        for i in range(5)
    ] 
    
    head_plots: List = [
        ax.plot(
            *get_coordinates_for_heads(sim, i), 
            markersize=7, color = colors[i], marker = markers[i], 
            markerfacecolor="white")[0] 
        for i in range(5)
    ]

    def initialize_animation():
        set_plot_parameters(ax)
        if type == SimulationType.NANODOMAIN: handle_nanodomain(ax, sim)
        elif type == SimulationType.HOPDIFFUSION: handle_hop_diffusion(ax, sim)
        return path_plots

    def update_animation(frame):
        sim.update()
        for i, plot in enumerate(path_plots):
            plot.set_data(*get_coordinates_for_plot(sim, i))
        for i, head_marker in enumerate(head_plots):
            head_marker.set_data(*get_coordinates_for_heads(sim, i))
        return path_plots

    animation = FuncAnimation(
        fig, 
        update_animation, 
        init_func = initialize_animation, 
        interval = 20
    )

    plt.show(block = True) # type: ignore
    fig.tight_layout()

rcParams.update({'figure.autolayout': True})

./util.py

代码语言:javascript
复制
from typing import List, Tuple
import numpy as np
import random

class Util:
    @staticmethod
    def get_bounds(lists) -> Tuple[int, ...]:
        x_min: int = min([min(elem[0]) for elem in lists])
        x_max: int = max([max(elem[0]) for elem in lists])
        y_min: int = min([min(elem[1]) for elem in lists])
        y_max: int = max([max(elem[1]) for elem in lists])
        return x_min, x_max, y_min, y_max

    @staticmethod
    def compute_distance(p1: Tuple, p2: Tuple) -> float:
        return np.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2)

    @staticmethod
    def get_last_point(path: List[Tuple]) -> Tuple[int, ...]:
        return path[-1][0], path[-1][1]

    @staticmethod
    def get_x_coordinates(path) -> List:
        return list(list(zip(*path))[0])

    @staticmethod
    def get_y_coordinates(path) -> List:
        return list(list(zip(*path))[1])

    @staticmethod
    def get_random_normal_direction():
        return np.random.normal() * np.random.choice([1, -1])

    @staticmethod
    def is_point_within_bounds(pos: Tuple, bounds: Tuple[Tuple, ...]):
        x, y = pos[0], pos[1]
        return x >= bounds[0][0] and x <= bounds[0][1] and y >=  bounds[1][0] and y <= bounds[1][1]
    
    @staticmethod
    def sign(x):
        return ((x >= 0) << 1) - 1
    
    @staticmethod
    def increment_tuple_by_val(tuple_object: Tuple, val):
        tuple_object = tuple((tuple_object[0] + val[0], tuple_object[1] + val[1]))
        return tuple_object
    
    @staticmethod
    def change_direction(tuple_object: Tuple, dir):
        tuple_object = tuple((tuple_object[0] - dir[0], tuple_object[1] - dir[1]))
        return tuple_object
EN

回答 2

Code Review用户

回答已采纳

发布于 2022-10-02 15:59:05

拼写:FATOR -> FACTORboudnary -> boundary

升级Python,然后直接使用listtuple作为类型提示,而不是旧的ListTuple

简化链式比较;用于is_point_within_bounds的比较如下所示:

代码语言:javascript
复制
    return (
        bounds[0][0] <= x <= bounds[0][1] and
        bounds[1][0] <= y <= bounds[1][1]
    )

有一个名为dir to change_direction的参数隐藏了内置的dir。将其命名为其他名称,在本例中为direction

在许多地方都有不完整的类型提示。不要不指定list;在[]中填充它的元素类型。将Mypy配置为不允许未指定的类型。

Util不应该是一个类。只需将这些函数放在全局命名空间中,然后在依赖模块中编写没有import utilfrom来创建包含的命名空间。

tuple_object不是一个好名字。当然,这是一个元组:您在类型方面做了正确的事情--暗示它本身(尽管类型提示不完整)。根据内容的含义而不是类型来命名它。

您有一种不需要强制转换的序列转换习惯,如

代码语言:javascript
复制
       self.boundary_coordinates.append(list([tuple((x, x + width)), tuple((y, y + height))]))

那真的只是

代码语言:javascript
复制
self.boundary_coordinates.append(
    [
        (x, x + width), (y, y + height),
    ]
)

这是:

代码语言:javascript
复制
def sign(x):
    return ((x >= 0) << 1) - 1

既棘手又没有必要。只需使用np.sign

ax.patch.set_linewidth('2')将永远不能工作;该字符串需要是一个数字文字。

-(CORRECTED_CANVAS_RADIUS)不应该有父母。

rec存在于init_particles中,因此在闭包范围内已经可以访问self;不要将它再次写入到rec的签名中。签名中也从不使用xy,所以也要删除它们。

get_random_canvas_value应该移到staticmethod of Simulation;不要重复它--停止复制和粘贴代码。

这个循环没有意义:

代码语言:javascript
复制
        while (x, y) in mem:
            return rec(self, x, y)

那只是一个if,而不是一个while

应该对rec进行重新设计,使其不再递归。这是等待发生的堆栈溢出。

本清单理解如下:

代码语言:javascript
复制
        x, y = [self.get_random_canvas_value() for _ in range(2)]

应该保留为生成器(),而不是列表[]。同样,get_bounds中的所有内部列表理解都应该转换为生成器;基本上删除[]

colorsmarkers应该是元组。

numberOfParticles应该是PEP8的number_of_particles

get_bounds未使用,所以请删除它。

你应该在这里使用楼层划分:

代码语言:javascript
复制
step: int = int((RADIUS << 1) // NUMBER_OF_COMPARTMENTS_PER_DIRECTION)

您使用<<>>来加速乘法和除法,这是不成熟的优化,而且您的程序中有些领域的效率相对较低。只需做一些可理解的事情,并编写2。总之:你有固定的框架。只要这个框架受到打击,尝试微观优化就没有任何意义。

get_nanodomain_coordinates像您所拥有的那样成为一个属性是一个好主意,但是在这种情况下,删除"get_“,因为它的调用是类似变量的,而不是类似方法的。然而。您已经有了带有这些名称的变量,所以恰恰相反:从这些方法中删除@property

您将内置的randomnp.random混合使用。别干那事。可能只使用np.random

不要写诸如[self.update_path(i) for i in range(self.n_particles)]这样的无作业理解。只需写一个循环。

理解在handle_hop_diffusion中起不了作用。只需将其展开为一个常规循环:

代码语言:javascript
复制
def handle_hop_diffusion(ax: plt.Axes, sim: HopDiffusion):
    for param in sim.boundary_coordinates_for_plot:
        boundary = plt.Rectangle(  # type: ignore
            tuple((param[0], param[1])),
            param[2], param[3],
            color='black',
            alpha=0.7,
            clip_on=False,
        )
        ax.add_patch(boundary)

type作为一个参数需要消失。您已经知道了在isinstance()变量上使用sim的类型。

Simulation实际上是一个抽象类。您需要在上面添加一个update存根:

代码语言:javascript
复制
    def update(self) -> None:
        raise NotImplementedError()

此语句无效,因此将其删除:

代码语言:javascript
复制
new_surrounding_boundary_idx = self.get_surrounding_boundary_of_particle(new_pos)

你有潜在的窃听器。for i in range(6):硬编码隔间的数量,而这实际上应该是从NUMBER_OF_COMPARTMENTS_PER_DIRECTION得到的.一旦这种不断的变化,您的代码就不会做您想做的事情。同样,for _ in range(5):对粒子计数进行硬编码,而不是使用n_particles中设置的计数参数.

第一关

上面大部分内容的第一次通过如下所示:

代码语言:javascript
复制
import random

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import rcParams  # type: ignore
from matplotlib.animation import FuncAnimation  # type: ignore


DPI = 100
RADIUS_PADDING = 10
RADIUS = 250
CORRECTED_CANVAS_RADIUS = RADIUS - RADIUS_PADDING

TIME_PER_FRAME: float = 0.02  # 20 ms
DIFFUSION_SPEED_CORRECTION: int = 35  # arbitrary
MEMBRANE_DIFFUSION_COEFFICIENT: float = 0.1  # micrometer^2 / s
MEMBRANE_DIFFUSION_FACTOR: float = 2 * np.sqrt(MEMBRANE_DIFFUSION_COEFFICIENT * TIME_PER_FRAME)
MEMBRANE_DIFFUSION_FACTOR_CORRECTED: float = MEMBRANE_DIFFUSION_FACTOR * DIFFUSION_SPEED_CORRECTION
NANODOMAIN_DIFFUSION_FACTOR_CORRECTED: float = MEMBRANE_DIFFUSION_FACTOR_CORRECTED * 0.4

BOUNDARY_THICKNESS: int = 15
NUMBER_OF_COMPARTMENTS_PER_DIRECTION: int = 3
BOUNDARY_JUMP: int = BOUNDARY_THICKNESS
BOUNDARY_OVERFLOW: int = 20
HOP_PROBABILITY_PERCENTAGE: float = 0.15

colors: tuple[str, ...] = ('r', 'b', 'orange', 'g', 'y', 'c')
markers: tuple[str, ...] = ('o', 'v', '<', '>', 's', 'p')


class util:  # convert this to an import of a module with global functions
    @staticmethod
    def compute_distance(p1: tuple[float, float], p2: tuple[float, float]) -> float:
        return np.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2)

    @staticmethod
    def get_last_point(path: list[tuple[float, float]]) -> tuple[float, float]:
        return path[-1][0], path[-1][1]

    @staticmethod
    def get_x_coordinates(path: list[tuple[float, float]]) -> list[float]:
        return list(list(zip(*path))[0])

    @staticmethod
    def get_y_coordinates(path: list[tuple[float, float]]) -> list[float]:
        return list(list(zip(*path))[1])

    @staticmethod
    def get_random_normal_direction() -> float:
        return np.random.normal() * np.random.choice([1, -1])

    @staticmethod
    def is_point_within_bounds(
        pos: tuple[float, float],
        bounds: tuple[tuple[float, float], tuple[float, float]],
    ) -> bool:
        x, y = pos[0], pos[1]
        return (
            bounds[0][0] <= x <= bounds[0][1] and
            bounds[1][0] <= y <= bounds[1][1]
        )

    @staticmethod
    def increment_tuple_by_val(tuple_object: tuple[float, float], val: tuple[float, float]) -> tuple[float, float]:
        return tuple_object[0] + val[0], tuple_object[1] + val[1]

    @staticmethod
    def change_direction(
        tuple_object: tuple[float, float],
        direction: tuple[float, float],
    ) -> tuple[float, float]:
        return tuple_object[0] - direction[0], tuple_object[1] - direction[1]


class Simulation:
    def __init__(self, n: int = 5) -> None:
        self.n_particles: int = n

        self.particle_locations: list[tuple[float, float]] = list(self.init_particles())

        self.paths: list[list[tuple[float, float]]] = [
            [coordinate] for coordinate in self.particle_locations
        ]

    @staticmethod
    def get_random_canvas_value() -> int:
        return int(random.randint(-CORRECTED_CANVAS_RADIUS, CORRECTED_CANVAS_RADIUS))

    def init_particles(self) -> set[tuple[float, float]]:
        mem: set[tuple[float, float]] = set()

        for _ in range(5):
            while True:
                pair = (self.get_random_canvas_value(), self.get_random_canvas_value())
                if pair not in mem:
                    break
            mem.add(pair)

        return mem

    def update(self) -> None:
        raise NotImplementedError()


class HopDiffusion(Simulation):
    def __init__(self, n: int = 5) -> None:
        self.boundary_coordinates_for_plot: list[tuple[int, int, int, int]] = []
        self.boundary_coordinates: list[tuple[tuple[float, float], tuple[float, float]]] = []
        self.generate_boundaries()

        super().__init__(n)

    def generate_boundaries(self) -> None:
        step: int = (RADIUS * 2) // NUMBER_OF_COMPARTMENTS_PER_DIRECTION

        for i in range(6):
            if i % 3 == 0:
                continue
            horizontal: bool = i < NUMBER_OF_COMPARTMENTS_PER_DIRECTION
            curr = i * step if horizontal else (i - NUMBER_OF_COMPARTMENTS_PER_DIRECTION) * step

            width = BOUNDARY_THICKNESS if horizontal else (RADIUS * 2) + (BOUNDARY_OVERFLOW * 2)
            height = BOUNDARY_THICKNESS if not horizontal else (RADIUS * 2) + (BOUNDARY_OVERFLOW * 2)
            x = curr - RADIUS - (BOUNDARY_THICKNESS // 2) if horizontal else -RADIUS - BOUNDARY_OVERFLOW
            y = curr - RADIUS - (BOUNDARY_THICKNESS // 2) if not horizontal else -RADIUS - BOUNDARY_OVERFLOW

            self.boundary_coordinates_for_plot.append((x, y, width, height))
            self.boundary_coordinates.append(
                ((x, x + width), (y, y + height))
            )

    @staticmethod
    def can_particle_hop_boundary_probability() -> bool:
        return random.random() < HOP_PROBABILITY_PERCENTAGE

    def is_particle_on_specific_boundary(self, pos: tuple[float, float], idx: int) -> bool:
        return util.is_point_within_bounds(pos, self.boundary_coordinates[idx])

    def is_particle_on_boundary(self, pos: tuple[float, float]) -> bool:
        return any(
            util.is_point_within_bounds(pos, bounds_of_boundary)
            for bounds_of_boundary in self.boundary_coordinates
        )

    def is_particle_in_compartment(self, particle: tuple[float, float]) -> bool:
        return not self.is_particle_on_boundary(particle)

    def get_surrounding_boundary_of_particle(self, pos: tuple[float, float]) -> int:
        for idx, bounds_of_boundary in enumerate(self.boundary_coordinates):
            if util.is_point_within_bounds(pos, bounds_of_boundary):
                return idx
        return -1

    def make_particle_jump(
        self,
        new_pos: tuple[float, float],
        x_dir: float, y_dir: float,
    ) -> tuple[float, float]:
        surrounding_boundary_idx = self.get_surrounding_boundary_of_particle(new_pos)

        while self.is_particle_on_specific_boundary(new_pos, surrounding_boundary_idx):
            new_pos = util.increment_tuple_by_val(
                new_pos, (np.sign(x_dir), np.sign(y_dir))
            )

        new_pos = util.increment_tuple_by_val(
            new_pos, (
                np.sign(x_dir) * BOUNDARY_JUMP,
                np.sign(y_dir) * BOUNDARY_JUMP,
            ),
        )

        # Special case: In some instances the jump may land the particle
        # on a subsequent boundary so we repeat the function. We decrement
        # the particle's coordinates until it is out.
        while self.is_particle_on_boundary(new_pos):
            new_pos = util.increment_tuple_by_val(
                new_pos, (np.sign(-x_dir), np.sign(-y_dir))
            )

        return new_pos

    def update_path(self, idx: int) -> None:
        x, y = self.paths[idx][-1]
        assert not self.is_particle_on_boundary((x, y))

        diffusion_factor = MEMBRANE_DIFFUSION_FACTOR_CORRECTED
        x_dir, y_dir = [util.get_random_normal_direction() * diffusion_factor for _ in range(2)]
        new_pos = x + x_dir, y + y_dir

        if self.is_particle_on_boundary(new_pos):
            if self.can_particle_hop_boundary_probability():
                new_pos = self.make_particle_jump(new_pos, x_dir, y_dir)
            else:
                new_pos = util.change_direction((x, y), (x_dir, y_dir))

        self.paths[idx].append(new_pos)

    def update(self) -> None:
        for i in range(self.n_particles):
            self.update_path(i)

    def init_particles(self) -> set[tuple[float, float]]:
        mem: set[tuple[float, float]] = set()

        for _ in range(5):
            while True:
                pair = (self.get_random_canvas_value(), self.get_random_canvas_value())
                if not (pair in mem or self.is_particle_on_boundary(pair)):
                    break
            mem.add(pair)

        return mem


class Nanodomain(Simulation):
    def __init__(self, n: int = 5):
        super().__init__(n)
        self.nanodomain_coordinates: list[tuple[float, float]] = [
            (-100, 100), (0, 0), (150, -60), (-130, -160)
        ]
        self.nanodomain_radii: list[int] = [80, 20, 50, 140]

    def get_nanodomain_attributes(self) -> list[tuple]:
        return list(map(
            lambda coord, radius: (coord, radius),
            self.nanodomain_coordinates,
            self.nanodomain_radii,
        ))

    def is_particle_in_nanodomain(self, particle: tuple) -> bool:
        return any(
            util.compute_distance(particle, circle_center) <= radius
            for circle_center, radius in
            zip(self.nanodomain_coordinates, self.nanodomain_radii)
        )

    def update_path(self, idx: int) -> None:
        x, y = self.paths[idx][-1]
        diffusion_factor = NANODOMAIN_DIFFUSION_FACTOR_CORRECTED if (
            self.is_particle_in_nanodomain((x, y))) else MEMBRANE_DIFFUSION_FACTOR_CORRECTED
        x_dir, y_dir = [util.get_random_normal_direction() * diffusion_factor for _ in range(2)]
        self.paths[idx].append((x + x_dir, y + y_dir))

    def update(self) -> None:
        for i in range(self.n_particles):
            self.update_path(i)


def handle_nanodomain(ax: plt.Axes, sim: Nanodomain) -> None:
    nanodomains = [
        plt.Circle(
            *param,
            color='black',
            alpha=0.2,
        )
        for param in sim.get_nanodomain_attributes()
    ]
    for nanodomain in nanodomains:
        ax.add_patch(nanodomain)


def handle_hop_diffusion(ax: plt.Axes, sim: HopDiffusion) -> None:
    for param in sim.boundary_coordinates_for_plot:
        boundary = plt.Rectangle(
            tuple((param[0], param[1])),
            param[2], param[3],
            color='black',
            alpha=0.7,
            clip_on=False,
        )
        ax.add_patch(boundary)


def get_coordinates_for_plot(sim: Simulation, idx: int):
    return util.get_x_coordinates(sim.paths[idx]), util.get_y_coordinates(sim.paths[idx])


def get_coordinates_for_heads(sim, idx: int):
    return util.get_last_point(sim.paths[idx])


def set_plot_parameters(ax):
    ax.tick_params(axis='y', direction='in', right=True, labelsize=16, pad=20)
    ax.tick_params(axis='x', direction='in', top=True, bottom=True, labelsize=16, pad=20)

    # legends and utilities
    ax.set_xlabel(r'nm', fontsize=16)
    ax.set_ylabel(r'nm', fontsize=16)

    # border colors
    ax.patch.set_edgecolor('black')
    ax.patch.set_linewidth(2)

    ax.set_xlim(-RADIUS, RADIUS)
    ax.set_ylim(-RADIUS, RADIUS)


def plot(sim: Simulation) -> None:
    fig, ax = plt.subplots(figsize=[5, 5], dpi=DPI)

    path_plots: list[plt.Line2D] = [
        ax.plot(
            *get_coordinates_for_plot(sim, i),
            markersize=15, color=colors[i],
        )[0]
        for i in range(5)
    ]

    head_plots: list[plt.Line2D] = [
        ax.plot(
            *get_coordinates_for_heads(sim, i),
            markersize=7, color=colors[i], marker=markers[i],
            markerfacecolor='white')[0]
        for i in range(5)
    ]

    def initialize_animation() -> list[plt.Artist]:
        set_plot_parameters(ax)
        if isinstance(sim, Nanodomain):
            handle_nanodomain(ax, sim)
        elif isinstance(sim, HopDiffusion):
            handle_hop_diffusion(ax, sim)
        return path_plots

    def update_animation(frame: int) -> list[plt.Artist]:
        sim.update()
        for i, axes in enumerate(path_plots):
            coords = get_coordinates_for_plot(sim, i)
            axes.set_data(*coords)
        for i, head_marker in enumerate(head_plots):
            coords = get_coordinates_for_heads(sim, i)
            head_marker.set_data(*coords)
        return path_plots

    animation = FuncAnimation(
        fig,
        update_animation,
        init_func=initialize_animation,
        interval=20,
    )

    fig.tight_layout()
    plt.show(block=True)


def main() -> None:
    rcParams.update({'figure.autolayout': True})

    sim = HopDiffusion()  # Nanodomain()
    plot(sim)


if __name__ == '__main__':
    main()

第二关

对于任何一种严肃的模拟,尤其是如果你需要将它从动画可视化转换成高吞吐量的无头模拟,你需要用Numpy进行矢量化。这是一个很长的过程,需要重写大部分代码。我将展示例子,但不是完整的程序。

generate_boundaries()成为一个生成两个三维数组的函数:

代码语言:javascript
复制
    @staticmethod
    def generate_boundaries() -> tuple[np.ndarray, np.ndarray]:
        stop = RADIUS + BOUNDARY_OVERFLOW
        inner_edges = np.linspace(
            start=-RADIUS, stop=RADIUS, num=NUMBER_OF_COMPARTMENTS_PER_DIRECTION + 1,
        )[1: -1]
        n_edges = len(inner_edges)

        # edges by x/y by start/stop
        bound_coords = np.empty((2*n_edges, 2, 2))
        bound_coords[:n_edges, 0, 0] = inner_edges - BOUNDARY_THICKNESS/2
        bound_coords[:n_edges, 0, 1] = inner_edges + BOUNDARY_THICKNESS/2
        bound_coords[:n_edges, 1, :] = -stop, stop
        bound_coords[n_edges:, ...] = bound_coords[:n_edges, ::-1, :]

        # edges by x/y by start/size
        plot_coords = bound_coords.copy()
        plot_coords[..., 1] = bound_coords[..., 1] - bound_coords[..., 0]

        return plot_coords, bound_coords

你的随机发生器变成

代码语言:javascript
复制
from numpy.random import default_rng
from numpy.random._generator import Generator


rand: Generator = default_rng(seed=0)

然后,正态分布的布朗偏移量产生。

代码语言:javascript
复制
def get_random_normal_direction() -> float:
    return rand.normal(scale=MEMBRANE_DIFFUSION_FACTOR_CORRECTED, size=2)

边界检查变成

代码语言:javascript
复制
def is_point_within_bounds(
    pos: np.ndarray,
    bounds: np.ndarray,
) -> bool:
    return (
        (bounds[:, 0] <= pos) &
        (bounds[:, 1] >= pos)
    ).all()

你的初始粒子状态失去了它所有的唯一性关注点,只在浮子上运行,然后变成

代码语言:javascript
复制
    def init_particles(self) -> np.ndarray:
        return rand.uniform(
            low=-CORRECTED_CANVAS_RADIUS,
            high=CORRECTED_CANVAS_RADIUS,
            size=(self.n_particles, 2),
        )

等。

票数 7
EN

Code Review用户

发布于 2022-10-02 16:16:43

这些都有点挑剔;我注意到了一两件事,所以我针对代码运行了匹林特,并在这里总结一下,省略了前面提到的东西。

  • main.py:不必要的分号
  • 很多后面的空格。Python对空格很敏感,所以虽然我不认为尾随空格会导致错误,但保持空白整洁是一个好习惯。还有一个地方缩进了五个空格,在某些情况下可能会导致错误。(当然,永远不要使用制表符)
  • 在大多数情况下,使用snake_case而不是camelCase。不过,PascalCase用于类。在约定之间转换是一件痛苦的事,但它比战斗的约定要痛苦得多:
  • 不要对副作用的for循环使用列表理解。(plotGenerator.py:24和其他人)在理解中有副作用是很糟糕的形式。
  • 避免隐藏名字,尤其是内置的名字。(例如你的论点type at plotGenerator.py:59)
  • 把if/else的身体放到新的线路上。
  • 不要把未使用的东西放在你的代码库周围。很明显,所有这些点都可以通过链接器来提高,但是这是你在实践中需要一个链接器的地方。(例如plotGenerator.py:83:未使用的参数frame)
  • 有关于如何组织您的进口,不管是什么约定。
  • 我非常喜欢生成器理解,所以林特建议将util.py:8改为... = min(min(elem[0]) for elem in lists)是一个令人愉快的惊喜。

因此,找到一个链接,你可以站起来,并配置它供您使用。遵守每一种格式约定可能是一项繁重的工作,但对任何同事来说都是很有价值的,除了类型检查程序会发现的之外,还有一些真正的问题是linter可以找到的。

我注意到的第一件事之一是您在simulation.py of PATH = Tuple[List[float]]中的声明。MyPy没有抱怨它,因为它从未被使用过,而linter也没有抱怨它从来没有被使用过,因为(我认为)它是模块中的一个全局定义(所以它可能对模块的使用者有用)。该类型表达式的问题是,它是一个长度为1的元组,这不太可能是您真正想要的(忽略您显然根本不想要PATH )。也许你是说 ``Tuple[列表浮动,.]

票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/280131

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档