예제 #1
0
def test_surfactant():
    resource_folder = 'tests/test_files/'
    pdb_folder = resource_folder + 'pdb/surfactant/'

    bio_explorer = BioExplorer('localhost:5000')
    bio_explorer.reset()
    print('BioExplorer version ' + bio_explorer.version())

    # Suspend image streaming
    bio_explorer.core_api().set_application_parameters(image_stream_fps=0)
    bio_explorer.core_api().set_camera(
        orientation=[0.0, 0.0, 0.0, 1.0], position=[0, 0, 200], target=[0, 0, 0])

    # Proteins
    protein_representation = bio_explorer.REPRESENTATION_ATOMS

    head_source = pdb_folder + '1pw9.pdb'
    branch_source = pdb_folder + '1k6f.pdb'

    # SP-D
    surfactant_d = Surfactant(
        name='SP-D', surfactant_protein=bio_explorer.SURFACTANT_PROTEIN_D, head_source=head_source,
        branch_source=branch_source)
    bio_explorer.add_surfactant(
        surfactant=surfactant_d, representation=protein_representation, position=Vector3(-50, 0, 0),
        random_seed=10)

    # SP-A
    surfactant_a = Surfactant(
        name='SP-A', surfactant_protein=bio_explorer.SURFACTANT_PROTEIN_A, head_source=head_source,
        branch_source=branch_source)
    bio_explorer.add_surfactant(
        surfactant=surfactant_a, representation=protein_representation, position=Vector3(50, 0, 0))

    # Restore image streaming
    bio_explorer.core_api().set_application_parameters(image_stream_fps=20)
예제 #2
0
def test_layout():
    resource_folder = 'tests/test_files/'
    pdb_folder = resource_folder + 'pdb/'

    bio_explorer = BioExplorer('localhost:5000')
    bio_explorer.reset()
    print('BioExplorer version ' + bio_explorer.version())

    # Suspend image streaming
    bio_explorer.core_api().set_application_parameters(image_stream_fps=0)

    line_surfactant = 5
    line_virus = 25
    line_defense = 45

    # Camera
    brayns = bio_explorer.core_api()
    brayns.set_camera(
        current='orthographic',
        orientation=[0.0, 0.0, 0.0, 1.0],
        position=[23.927943790322814, 24.84577580212592, 260.43975983632527],
        target=[23.927943790322814, 24.84577580212592, 39.93749999999999])
    params = brayns.OrthographicCameraParams()
    params.height = 55
    brayns.set_camera_params(params)

    # Grid
    bio_explorer.add_grid(min_value=0,
                          max_value=100,
                          interval=1,
                          radius=0.005,
                          colored=False,
                          position=Vector3(-10.0, -10.0, -10.0))

    # Layout
    virus_protein_s = Protein(sources=[
        pdb_folder + '6vyb.pdb',  # Open conformation
        pdb_folder + 'sars-cov-2-v1.pdb'  # Closed conformation
    ])

    bio_explorer.add_protein(name='Protein S (open)',
                             protein=virus_protein_s,
                             conformation_index=0,
                             position=Vector3(5.0, line_virus, 0.0),
                             rotation=Quaternion(0.0, 0.0, 1.0, 0.0))
    bio_explorer.add_protein(name='Protein S (closed)',
                             protein=virus_protein_s,
                             conformation_index=1,
                             position=Vector3(20.0, line_virus, 0.0),
                             rotation=Quaternion(0.0, 0.0, 1.0, 0.0))

    # Protein M (QHD43419)
    virus_protein_m = Protein(sources=[pdb_folder + 'QHD43419a.pdb'])
    bio_explorer.add_protein(name='Protein M',
                             protein=virus_protein_m,
                             position=Vector3(35.0, line_virus, 0.0))

    # Protein E (QHD43418 P0DTC4)
    virus_protein_e = Protein(sources=[pdb_folder + 'QHD43418a.pdb'])
    bio_explorer.add_protein(name='Protein E',
                             protein=virus_protein_e,
                             position=Vector3(45.0, line_virus, 0.0))

    # Lactoferrin
    lactoferrin = Protein(sources=[pdb_folder + 'immune/1b0l.pdb'])
    bio_explorer.add_protein(name='Lactoferrin',
                             protein=lactoferrin,
                             position=Vector3(5.0, line_defense, 0.0))

    # Defensin
    defensin = Protein(sources=[pdb_folder + 'immune/1ijv.pdb'])
    bio_explorer.add_protein(name='Defensin',
                             protein=defensin,
                             position=Vector3(20.0, line_defense, 0.0))

    # Glucose
    glucose = Protein(sources=[pdb_folder + 'glucose.pdb'],
                      load_non_polymer_chemicals=True)
    bio_explorer.add_protein(name='Glucose',
                             protein=glucose,
                             position=Vector3(30.0, line_defense, 0.0),
                             rotation=Quaternion(0.0, 0.0, 0.707, 0.707))

    # ACE2 Receptor
    ace2_receptor = Protein(sources=[pdb_folder + '6m18.pdb'])
    bio_explorer.add_protein(name='ACE2 receptor',
                             protein=ace2_receptor,
                             position=Vector3(45.0, line_defense - 2.5, 0.0),
                             rotation=Quaternion(0.5, 0.5, 1.0, 0.0))

    # Surfactant
    head_source = pdb_folder + 'surfactant/1pw9.pdb'
    branch_source = pdb_folder + 'surfactant/1k6f.pdb'
    surfactant_d = Surfactant(
        name='Surfactant',
        surfactant_protein=bio_explorer.SURFACTANT_BRANCH,
        head_source=head_source,
        branch_source=branch_source)

    bio_explorer.add_surfactant(surfactant=surfactant_d,
                                position=Vector3(5.0, line_surfactant, 0.0))

    # Restore image streaming
    bio_explorer.core_api().set_application_parameters(image_stream_fps=20)
예제 #3
0
class HighGlucoseScenario():
    def __init__(self,
                 hostname,
                 port,
                 projection,
                 output_folder,
                 image_k=4,
                 image_samples_per_pixel=64,
                 log_level=1,
                 shaders=list(['bio_explorer']),
                 magnetic=False):
        self._log_level = log_level
        self._hostname = hostname
        self._url = hostname + ':' + str(port)
        self._be = BioExplorer(self._url)
        self._core = self._be.core_api()
        self._image_size = [1920, 1080]
        self._image_samples_per_pixel = image_samples_per_pixel
        self._image_projection = projection
        self._image_output_folder = output_folder
        self._shaders = shaders
        self._magnetic = magnetic
        self._prepare_movie(projection, image_k)
        self._log(
            1,
            '================================================================================'
        )
        self._log(1, '- Version          : ' + self._be.version())
        self._log(1, '- URL              : ' + self._url)
        self._log(1, '- Projection       : ' + projection)
        self._log(1, '- Frame size       : ' + str(self._image_size))
        self._log(1, '- Export folder    : ' + self._image_output_folder)
        self._log(1,
                  '- Samples per pixel: ' + str(self._image_samples_per_pixel))
        self._log(
            1,
            '================================================================================'
        )

    def _log(self, level, message):
        if level <= self._log_level:
            print('[' + str(datetime.now()) + '] ' + message)

    def _get_transformation(self, start_frame, end_frame, frame, data):
        '''Progress'''
        progress = (frame - start_frame) * 1.0 / (end_frame - start_frame)
        progress = max(0.0, progress)
        progress = min(1.0, progress)
        '''Position'''
        start_pos = data[0].to_list()
        end_pos = data[2].to_list()
        pos = start_pos
        for i in range(3):
            pos[i] += (end_pos[i] - start_pos[i]) * progress
        '''Rotation'''
        start_rot = data[1]
        end_rot = data[3]
        rot = Quaternion.slerp(start_rot, end_rot, progress)
        if data[4] == ROTATION_MODE_SINUSOIDAL:
            rot = Quaternion.slerp(start_rot, end_rot,
                                   math.cos((progress - 0.5) * math.pi))

        return [Vector3(pos[0], pos[1], pos[2]), rot, progress * 100.0]

    def _add_viruses(self, frame):
        virus_sequences = [
            [[-1000, 2499], [2500, 2599], [2600, 2799], [2800, 2999],
             [3000, 3099], [3100, 3750]],
            # Virus used for the ACE2 close-up
            [[0, 2100], [2200, 2299], [2300, 2499], [2500, 3049], [3050, 3149],
             [3150, 3750]],
            [[-800, 2549], [2550, 2649], [2650, 2849], [2850, 3199],
             [3200, 3299], [3300, 3750]],
            [[-1400, 3750], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6],
             [1e6, 1e6]],
            [[-400, 2599], [2600, 2699], [2700, 2899], [2900, 3119],
             [3120, 3219], [3220, 3750]],
            [[0, 2649], [2650, 2749], [2750, 2949], [2950, 3199], [3200, 3299],
             [3300, 3750]],

            # new Viruses
            [[-1, -1], [-1, -1], [-1, -1], [-1, 3212], [3213, 3312],
             [3313, 3750]],
            [[-1, -1], [-1, -1], [-1, -1], [-1, 3201], [3202, 3301],
             [3302, 3750]],
            [[-1, -1], [-1, -1], [-1, -1], [-1, 3171], [3172, 3271],
             [3272, 3750]],
            [[-1, -1], [-1, -1], [-1, -1], [-1, 3152], [3153, 3252],
             [3253, 3750]],
            [[-1, -1], [-1, -1], [-1, -1], [-1, 3358], [3359, 3458],
             [3459, 3750]]
        ]
        virus_radii = [
            45.0, 44.0, 45.0, 43.0, 44.0, 43.0, 45.0, 46.0, 44.0, 45.0, 44.0
        ]
        virus_flights_in = [
            [
                Vector3(-250.0, 100.0, -70.0),
                Quaternion(0.519, 0.671, 0.528, -0.036),
                Vector3(-337.3, -92.3, -99.2),
                Quaternion(1.0, 0.0, 0.0, 0.0), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(-50.0, 300.0, 250.0),
                Quaternion(0.456, 0.129, -0.185, -0.860),
                Vector3(-74.9, -99.0, 228.8),
                Quaternion(1.0, 0.0, 0.0, 0.0), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(150.0, 100.0, 50.0),
                Quaternion(0.087, 0.971, -0.147, -0.161),
                Vector3(187.5, -110.4, 51.2),
                Quaternion(1.0, 0.0, 0.0, 0.0), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(40.0, 250.0, -50),
                Quaternion(0.0, 0.0, 0.0, 1.0),
                Vector3(4.5, 100.0, 7.5),
                Quaternion(1.0, 0.0, 0.0, 0.0), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(60.0, 100.0, -240.0),
                Quaternion(-0.095, 0.652, -0.326, 0.677),
                Vector3(73.9, -117.1, -190.4),
                Quaternion(1.0, 0.0, 0.0, 0.0), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(200.0, 100.0, 300.0),
                Quaternion(-0.866, 0.201, 0.308, -0.336),
                Vector3(211.5, -104.9, 339.2),
                Quaternion(1.0, 0.0, 0.0, 0.0), ROTATION_MODE_LINEAR
            ],
            # New viruses (no flying in, only flying out)
            [
                Vector3(),
                Quaternion(),
                Vector3(),
                Quaternion(), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(),
                Quaternion(),
                Vector3(),
                Quaternion(), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(),
                Quaternion(),
                Vector3(),
                Quaternion(), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(),
                Quaternion(),
                Vector3(),
                Quaternion(), ROTATION_MODE_LINEAR
            ],
            [
                Vector3(),
                Quaternion(),
                Vector3(),
                Quaternion(), ROTATION_MODE_LINEAR
            ]
        ]
        virus_flights_out = [[
            Vector3(-250.0, -150.0, -70.0),
            Quaternion(),
            Vector3(-270.0, 200.0, -99.2),
            Quaternion(0.519, 0.671, 0.528, -0.036), ROTATION_MODE_LINEAR
        ],
                             [
                                 Vector3(-50.0, -150.0, 250.0),
                                 Quaternion(),
                                 Vector3(-75.0, 240.0, 228.8),
                                 Quaternion(0.456, 0.129, -0.185, -0.860),
                                 ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(150.0, -150.0, 50.0),
                                 Quaternion(),
                                 Vector3(187.0, 300.0, 51.2),
                                 Quaternion(0.087, 0.971, -0.147, -0.161),
                                 ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(),
                                 Quaternion(),
                                 Vector3(),
                                 Quaternion(), ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(60.0, -150.0, -240.0),
                                 Quaternion(),
                                 Vector3(74.0, 195.0, -220.0),
                                 Quaternion(-0.095, 0.652, -0.326, 0.677),
                                 ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(-200.0, -150.0, 300.0),
                                 Quaternion(),
                                 Vector3(-210.0, 205.0, 330.0),
                                 Quaternion(-0.866, 0.201, 0.308, -0.336),
                                 ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(531, -150.0, -34.0),
                                 Quaternion(),
                                 Vector3(500.0, 215.0, -50.0),
                                 Quaternion(0.431, -0.145, -0.700, -0.550),
                                 ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(225.0, -150.0, 554.0),
                                 Quaternion(),
                                 Vector3(200.0, 190.0, 520.0),
                                 Quaternion(-0.466, -0.086, -0.616, -0.629),
                                 ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(-171, -150.0, -5.0),
                                 Quaternion(),
                                 Vector3(-160.0, 300.0, 10.0),
                                 Quaternion(0.227, 0.834, -0.187, 0.468),
                                 ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(-331, -150.0, 343),
                                 Quaternion(),
                                 Vector3(-310.0, 230.0, 350.0),
                                 Quaternion(0.417, 0.849, -0.075, -0.316),
                                 ROTATION_MODE_LINEAR
                             ],
                             [
                                 Vector3(159.0, -150.0, -341.0),
                                 Quaternion(),
                                 Vector3(170.0, 100.0, -360.0),
                                 Quaternion(0.483, -0.352, 0.769, -0.226),
                                 ROTATION_MODE_LINEAR
                             ]]

        indices = range(len(virus_sequences))
        if self._magnetic:
            indices = [1]

        for virus_index in indices:
            name = 'Coronavirus ' + str(virus_index)
            current_sequence = 0
            sequences = virus_sequences[virus_index]
            for i in range(len(sequences)):
                if frame >= sequences[i][0]:
                    current_sequence = i
            '''Initialize position and rotation to end-of-flight values'''
            start_frame = sequences[current_sequence][0]
            end_frame = sequences[current_sequence][1]
            progress_in_sequence = (frame - start_frame) / (end_frame -
                                                            start_frame)
            morphing_step = 0.0

            if current_sequence == 0:
                '''Flying'''
                pos, rot, progress = self._get_transformation(
                    start_frame, end_frame, frame,
                    virus_flights_in[virus_index])
                self._log(
                    3, '-   Virus %d is flying in... (%.01f pct)' %
                    (virus_index, progress))
            elif current_sequence == 1:
                '''Landing'''
                pos = virus_flights_in[virus_index][2]
                rot = virus_flights_in[virus_index][3]
                pos.y -= landing_distance * progress_in_sequence
                self._log(3, '-   Virus %d is landing...' % virus_index)
            elif current_sequence == 2:
                '''Merging into cell'''
                pos = virus_flights_in[virus_index][2]
                rot = virus_flights_in[virus_index][3]
                morphing_step = (frame - start_frame) / (end_frame -
                                                         start_frame)
                pos.y -= landing_distance
                self._log(
                    3, '-   Virus %d is merging in (%.01f pct)' %
                    (virus_index, morphing_step * 100.0))
            elif current_sequence == 3:
                '''Inside cell'''
                self._log(3, '-   Virus %d is inside cell' % virus_index)
                '''Virus is not added to the scene'''
                self._be.remove_assembly(name=name)
                continue
            elif current_sequence == 4:
                '''Merging out of cell'''
                pos = virus_flights_out[virus_index][0]
                rot = virus_flights_out[virus_index][1]
                morphing_step = 1.0 - (frame - start_frame) / (end_frame -
                                                               start_frame)
                self._log(
                    3, '-   Virus %d is merging out (%.01f pct)' %
                    (virus_index, morphing_step * 100.0))
            else:
                '''Flying out'''
                pos, rot, progress = self._get_transformation(
                    start_frame, end_frame, frame,
                    virus_flights_out[virus_index])
                self._log(
                    3, '-   Virus %d is flying out... (%.01f pct)' %
                    (virus_index, progress))

            self._be.add_coronavirus(name=name,
                                     resource_folder=resource_folder,
                                     representation=protein_representation,
                                     position=pos,
                                     rotation=rot,
                                     add_glycans=add_glycans,
                                     assembly_params=[
                                         virus_radii[virus_index],
                                         5 * frame + 2 * virus_index, 0.25,
                                         frame + 2 * virus_index + 1, 0.1,
                                         morphing_step
                                     ])

    def _add_cell(self, frame):

        name = 'Cell'
        nb_receptors = cell_nb_receptors
        size = scene_size * 2.0
        height = scene_size / 10.0
        position = Vector3(4.5, -186.0, 7.0)
        random_seed = 10

        nb_lipids = cell_nb_lipids
        ace2_receptor = Protein(sources=[pdb_folder + '6m18.pdb'],
                                occurences=nb_receptors,
                                position=Vector3(0.0, 6.0, 0.0))

        membrane = ParametricMembrane(sources=[
            membrane_folder + 'segA.pdb', membrane_folder + 'segB.pdb',
            membrane_folder + 'segC.pdb', membrane_folder + 'segD.pdb'
        ],
                                      occurences=cell_nb_lipids)

        cell = Cell(name=name,
                    size=size,
                    extra_parameters=[height],
                    shape=BioExplorer.ASSEMBLY_SHAPE_SINUSOIDAL,
                    membrane=membrane,
                    receptor=ace2_receptor,
                    random_position_seed=frame + 1,
                    random_position_strength=0.025,
                    random_rotation_seed=frame + 2,
                    random_rotation_strength=0.2)

        self._be.add_cell(cell=cell,
                          position=position,
                          representation=protein_representation,
                          random_seed=random_seed)
        '''Modify receptor position when attached virus enters the cell'''
        receptors_instances = [90, 23, 24, 98, 37, 44]
        receptors_sequences = [[2500, 2599], [2200, 2299], [2550, 2649],
                               [2600, 2699], [2650, 2749], [-1, -1]]

        for i in range(len(receptors_instances)):
            instance_index = receptors_instances[i]
            sequence = receptors_sequences[i]
            start_frame = sequence[0]
            end_frame = sequence[1]
            if frame >= start_frame:
                if frame > end_frame:
                    '''Send receptor to outter space'''
                    status = self._be.set_protein_instance_transformation(
                        assembly_name=name,
                        name=name + '_' + BioExplorer.NAME_RECEPTOR,
                        instance_index=instance_index,
                        position=Vector3(0.0, 1e6, 0.0))
                else:
                    '''Current receptor transformation'''
                    transformation = self._be.get_protein_instance_transformation(
                        assembly_name=name,
                        name=name + '_' + BioExplorer.NAME_RECEPTOR,
                        instance_index=instance_index)
                    p = transformation['position'].split(',')
                    q = transformation['rotation'].split(',')
                    pos = Vector3(float(p[0]), float(p[1]), float(p[2]))
                    q2 = Quaternion(float(q[0]), float(q[1]), float(q[2]),
                                    float(q[3]))
                    '''Bend receptor'''
                    progress = (frame - start_frame) * 1.0 / (end_frame -
                                                              start_frame)
                    q1 = Quaternion(axis=[0, 1, 0], angle=math.pi * progress)
                    rot = q2 * q1

                    pos.x += landing_distance * progress * 0.3
                    pos.y -= landing_distance * progress * 0.3

                    status = self._be.set_protein_instance_transformation(
                        assembly_name=name,
                        name=name + '_' + BioExplorer.NAME_RECEPTOR,
                        instance_index=instance_index,
                        position=pos,
                        rotation=rot)
        '''Glycans'''
        if nb_receptors != 0 and add_glycans:
            self._be.add_multiple_glycans(
                representation=glycan_representation,
                assembly_name=name,
                glycan_type=BioExplorer.NAME_GLYCAN_COMPLEX,
                protein_name=BioExplorer.NAME_RECEPTOR,
                paths=complex_paths,
                indices=[53, 90, 103, 322, 432, 690],
                assembly_params=[0, 0, 0.0, frame + 3, 0.2])
            self._be.add_multiple_glycans(
                representation=glycan_representation,
                assembly_name=name,
                glycan_type=BioExplorer.NAME_GLYCAN_HYBRID,
                protein_name=BioExplorer.NAME_RECEPTOR,
                paths=hybrid_paths,
                indices=[546],
                assembly_params=[0, 0, 0.0, frame + 4, 0.2])

            indices = [[155, Quaternion(0.707, 0.0, 0.707, 0.0)],
                       [730, Quaternion(0.707, 0.0, 0.707, 0.0)]]
            count = 0
            for index in indices:
                o_glycan_name = name + '_' + BioExplorer.NAME_GLYCAN_O_GLYCAN + '_' + str(
                    index[0])
                o_glycan = Sugars(
                    assembly_name=name,
                    name=o_glycan_name,
                    source=o_glycan_paths[0],
                    protein_name=name + '_' + BioExplorer.NAME_RECEPTOR,
                    representation=glycan_representation,
                    chain_ids=[2, 4],
                    site_indices=[index[0]],
                    rotation=index[1],
                    assembly_params=[0, 0, 0.0, frame + count + 5, 0.2])
                self._be.add_sugars(o_glycan)
                count += 1

    def _add_surfactant_d(self, name, position, rotation, random_seed):
        surfactant_d = Surfactant(
            name=name,
            surfactant_protein=BioExplorer.SURFACTANT_PROTEIN_D,
            head_source=surfactant_head_source,
            branch_source=surfactant_branch_source)
        self._be.add_surfactant(surfactant=surfactant_d,
                                representation=protein_representation,
                                position=position,
                                rotation=rotation,
                                random_seed=random_seed)

    def _add_surfactant_a(self, name, position, rotation, random_seed):
        surfactant_a = Surfactant(
            name=name,
            surfactant_protein=BioExplorer.SURFACTANT_PROTEIN_A,
            head_source=surfactant_head_source,
            branch_source=surfactant_branch_source)
        self._be.add_surfactant(surfactant=surfactant_a,
                                representation=protein_representation,
                                position=position,
                                rotation=rotation,
                                random_seed=random_seed)

    def _add_glucose_to_surfactant_head(self, name):
        for index in [321, 323]:
            glucose_name = name + '_' + BioExplorer.NAME_GLUCOSE + '_' + str(
                index)
            glucose = Sugars(assembly_name=name,
                             name=glucose_name,
                             source=glucose_path,
                             protein_name=name + '_' +
                             BioExplorer.NAME_SURFACTANT_HEAD,
                             representation=glycan_representation,
                             site_indices=[index])
            self._be.add_sugars(glucose)

    def _add_surfactants_d(self, frame):
        spd_sequences = [[-1550, 3750], [0, 3750], [0, 3750]]
        spd_random_seeds = [1, 2, 6]
        spd_flights = [[
            Vector3(-340.0, 0.0, -100.0),
            Quaternion(-0.095, 0.652, -0.326, 0.677),
            Vector3(74.0 + (74.0 + 340), 24.0 + (24.0 - 0.0),
                    -45.0 + (-45.0 + 100)),
            Quaternion(1.0, 0.0, 0.0, 0.0), ROTATION_MODE_SINUSOIDAL
        ],
                       [
                           Vector3(-200, 0.0, -200.0),
                           Quaternion(0.087, 0.971, -0.147, -0.161),
                           Vector3(304.0, 75.0, -100.0),
                           Quaternion(1.0, 0.0, 0.0, 0.0),
                           ROTATION_MODE_SINUSOIDAL
                       ],
                       [
                           Vector3(-460.0, 50.0, 0.0),
                           Quaternion(0.519, 0.671, 0.528, -0.036),
                           Vector3(160.0, -50.0, -50.0),
                           Quaternion(1.0, 0.0, 0.0, 0.0),
                           ROTATION_MODE_SINUSOIDAL
                       ]]

        for surfactant_index in range(len(spd_sequences)):
            name = 'Surfactant-D ' + str(surfactant_index)
            sequence = spd_sequences[surfactant_index]
            pos, rot, progress = self._get_transformation(
                start_frame=sequence[0],
                end_frame=sequence[1],
                frame=frame,
                data=spd_flights[surfactant_index])
            self._log(3, '-   ' + name + ' (%.01f pct)' % progress)
            self._add_surfactant_d(
                name=name,
                position=pos,
                rotation=rot,
                random_seed=spd_random_seeds[surfactant_index])
            self._add_glucose_to_surfactant_head(name=name)

    def _add_surfactants_a(self, frame):
        spa_sequences = [[0, 3750]]
        spa_random_seeds = [2]
        spa_frames = [
            [
                Vector3(-400.0, -100.0, 100.0),
                Quaternion(-0.095, 0.652, -0.326, 0.677),
                Vector3(250.0, -50.0, 100.0),
                Quaternion(1.0, 0.0, 0.0, 0.0), ROTATION_MODE_SINUSOIDAL
            ],
        ]

        for surfactant_index in range(len(spa_frames)):
            name = 'Surfactant-A ' + str(surfactant_index)
            sequence = spa_sequences[surfactant_index]
            pos, rot, progress = self._get_transformation(
                start_frame=sequence[0],
                end_frame=sequence[1],
                frame=frame,
                data=spa_frames[surfactant_index])
            self._log(3, '-   ' + name + ' (%.01f pct)' % progress)
            self._add_surfactant_a(
                name=name,
                position=pos,
                rotation=rot,
                random_seed=spa_random_seeds[surfactant_index])
            self._add_glucose_to_surfactant_head(name=name)

    def _add_glucose(self, frame):
        glucose = Protein(sources=[glucose_path],
                          load_non_polymer_chemicals=True,
                          occurences=nb_glucoses)
        volume = Volume(name=BioExplorer.NAME_GLUCOSE,
                        size=scene_size,
                        protein=glucose,
                        random_position_seed=frame + 20,
                        random_position_stength=scene_size / 600.0,
                        random_rotation_seed=frame + 21,
                        random_rotation_stength=0.3)
        status = self._be.add_volume(volume=volume,
                                     representation=protein_representation,
                                     position=Vector3(0.0,
                                                      scene_size / 2.0 - 200.0,
                                                      0.0),
                                     random_seed=100)

    def _add_lactoferrins(self, frame):
        lactoferrin = Protein(sources=[lactoferrin_path],
                              load_non_polymer_chemicals=True,
                              occurences=nb_lactoferrins)
        lactoferrins_volume = Volume(name=BioExplorer.NAME_LACTOFERRIN,
                                     size=scene_size,
                                     protein=lactoferrin,
                                     random_position_seed=frame + 30,
                                     random_position_stength=scene_size /
                                     400.0,
                                     random_rotation_seed=frame + 31,
                                     random_rotation_stength=0.3)
        status = self._be.add_volume(volume=lactoferrins_volume,
                                     representation=protein_representation,
                                     position=Vector3(0.0,
                                                      scene_size / 2.0 - 200.0,
                                                      0.0),
                                     random_seed=101)

    def _add_defensins(self, frame):
        defensin = Protein(sources=[defensin_path],
                           load_non_polymer_chemicals=True,
                           occurences=nb_defensins)
        defensins_volume = Volume(name=BioExplorer.NAME_DEFENSIN,
                                  size=scene_size,
                                  protein=defensin,
                                  random_position_seed=frame + 40,
                                  random_position_stength=scene_size / 400.0,
                                  random_rotation_seed=frame + 41,
                                  random_rotation_stength=0.3)
        status = self._be.add_volume(volume=defensins_volume,
                                     representation=protein_representation,
                                     position=Vector3(0.0,
                                                      scene_size / 2.0 - 200.0,
                                                      0.0),
                                     random_seed=102)

    def _set_materials(self):
        '''Default materials'''
        self._be.apply_default_color_scheme(
            shading_mode=BioExplorer.SHADING_MODE_DIFFUSE,
            specular_exponent=50.0)

    def _create_snapshot(self, shader, frame, movie_maker):
        samples_per_pixel = self._image_samples_per_pixel
        '''Renderer'''
        if shader == 'albedo':
            self._core.set_renderer(current='albedo')
        elif shader == 'ambient_occlusion':
            self._core.set_renderer(current='ambient_occlusion',
                                    samples_per_pixel=1,
                                    subsampling=1,
                                    max_accum_frames=1)
            params = self._core.AmbientOcclusionRendererParams()
            params.samplesPerFrame = 32
            params.rayLength = 5.0
            self._core.set_renderer_params(params)
            samples_per_pixel = 4
        elif shader == 'depth':
            status = self._core.set_renderer(current='depth',
                                             samples_per_pixel=1,
                                             subsampling=1,
                                             max_accum_frames=1)
            params = status = self._core.DepthRendererParams()
            params.infinity = 2000.0
            status = self._core.set_renderer_params(params)
            samples_per_pixel = 2
        elif shader == 'raycast_Ns':
            status = self._core.set_renderer(current='raycast_Ns',
                                             samples_per_pixel=1,
                                             subsampling=1,
                                             max_accum_frames=1)
            samples_per_pixel = 2
        else:
            status = self._core.set_renderer(
                background_color=[96 / 255, 125 / 255, 139 / 255],
                current='bio_explorer',
                head_light=False,
                samples_per_pixel=1,
                subsampling=1,
                max_accum_frames=1)
            params = self._core.BioExplorerRendererParams()
            params.exposure = 1.0
            params.gi_samples = 1
            params.gi_weight = 0.3
            params.gi_distance = 5000
            params.shadows = 0.8
            params.soft_shadows = 0.05
            params.fog_start = 1000
            params.fog_thickness = 300
            params.max_bounces = 1
            params.use_hardware_randomizer = True
            status = self._core.set_renderer_params(params)
            samples_per_pixel = self._image_samples_per_pixel
            '''Lights'''
            status = self._core.clear_lights()
            status = self._core.add_light_directional(
                angularDiameter=0.5,
                color=[1, 1, 1],
                direction=[-0.7, -0.4, -1],
                intensity=1.0,
                is_visible=False)

        movie_maker.create_snapshot(size=self._image_size,
                                    path=self._image_output_folder + '/' +
                                    shader,
                                    base_name='%05d' % frame,
                                    samples_per_pixel=samples_per_pixel)
        '''Camera'''
        status = self._core.set_camera(current='bio_explorer_perspective')

    def _build_frame(self, frame):
        self._log(2, '- Resetting scene...')
        self._be.reset()

        self._log(2, '- Building surfactants...')
        self._add_surfactants_d(frame)
        self._add_surfactants_a(frame)

        self._log(2, '- Building glucose...')
        self._add_glucose(frame)

        self._log(2, '- Building lactoferrins...')
        self._add_lactoferrins(frame)

        self._log(2, '- Building defensins...')
        self._add_defensins(frame)

        self._log(2, '- Building viruses...')
        self._add_viruses(frame)

        self._log(2, '- Building cell...')
        self._add_cell(frame)

        if self._magnetic:
            self._log(2, '- Building fields...')
            self._be.go_magnetic(colormap_filename=colormap_folder +
                                 'high_glucose_v2.1dt',
                                 voxel_size=0.5,
                                 density=0.1,
                                 colormap_range=[0.0, 1.0])
        else:
            self._log(2, '- Setting materials...')
            self._set_materials()

            self._log(2, '- Showing models...')
            status = self._be.set_models_visibility(True)
            status = self._core.set_renderer()

    def _make_export_folder(self, folder):
        import os
        path = self._image_output_folder + '/' + folder
        command_line = 'mkdir -p ' + path
        os.system(command_line)
        command_line = 'ls ' + path
        if os.system(command_line) != 0:
            self._log(3, 'ERROR: Failed to create folder ' + path)

    def _make_export_folders(self):
        for folder in self._shaders:
            self._make_export_folder(folder)

    def _prepare_movie(self, projection, image_k):
        if projection == 'perspective':
            aperture_ratio = 1.0
            self._image_size = [image_k * 960, image_k * 540]
            self._core.set_camera(current='bio_explorer_perspective')
        elif projection == 'fisheye':
            self._image_size = [int(image_k * 1024), int(image_k * 1024)]
            self._core.set_camera(current='fisheye')
        elif projection == 'panoramic':
            self._image_size = [int(image_k * 1024), int(image_k * 1024)]
            self._core.set_camera(current='panoramic')
        elif projection == 'opendeck':
            self._image_size = [7 * 2160, 3840]
            self._core.set_camera(current='cylindric')

        self._image_output_folder = self._image_output_folder + '/' + \
            projection + '/' + str(self._image_size[0]) + 'x' + str(self._image_size[1])
        self._make_export_folders()

    def _set_clipping_planes(self):
        '''Clipping planes'''
        clip_planes = list()
        if self._magnetic:
            pos = Vector3(-74.9, -99.0, 228.8)
            size = Vector3(70.0, 100.0, 70.0)
            clip_planes.append([1.0, 0.0, 0.0, -pos.x + size.x])
            clip_planes.append([-1.0, 0.0, 0.0, pos.x + size.x])
            clip_planes.append([0.0, 1.0, 0.0, -pos.y + size.y])
            clip_planes.append([0.0, -1.0, 0.0, pos.y + size.y])
            clip_planes.append([0.0, 0.0, 1.0, -pos.z + size.z])
            clip_planes.append([0.0, 0.0, -1.0, pos.z + size.z])
        else:
            clip_planes.append([1.0, 0.0, 0.0, scene_size * 1.5 + 5])
            clip_planes.append([-1.0, 0.0, 0.0, scene_size * 1.5 + 5])
            clip_planes.append([0.0, 0.0, 1.0, scene_size + 5])
            clip_planes.append([0.0, 0.0, -1.0, scene_size + 5])

        cps = self._core.get_clip_planes()
        ids = list()
        if cps:
            for cp in cps:
                ids.append(cp['id'])
        self._core.remove_clip_planes(ids)
        for plane in clip_planes:
            self._core.add_clip_plane(plane)

    def render_movie(self,
                     start_frame=0,
                     end_frame=0,
                     frame_step=1,
                     frame_list=list()):
        '''Accelerate loading by not showing models as they are loaded'''
        status = self._be.set_general_settings(
            model_visibility_on_creation=False)

        aperture_ratio = 0.0
        cameras_key_frames = [
            {  # 1. Cell view (frame 0)
                'apertureRadius': aperture_ratio * 0.0,
                'direction': [0.0, 0.0, -1.0],
                'focusDistance': 1.0,
                'origin': [150.0, -170.0, 400.0],
                'up': [0.0, 1.0, 0.0]
            },
            {  # 2. Virus view (frame 500)
                'apertureRadius': aperture_ratio * 0.0,
                'direction': [0.0, 0.0, -1.0],
                'focusDistance': 449.50,
                'origin': [-67.501, -17.451, 254.786],
                'up': [0.0, 1.0, 0.0]
            },
            {  # 3. Surfactant Head (frame 1000)
                'apertureRadius': aperture_ratio * 0.02,
                'direction': [0.276, -0.049, -0.959],
                'focusDistance': 25.54,
                'origin': [38.749, 35.228, 5.536],
                'up': [0.0, 1.0, 0.0]
            },
            {  # 4. Virus overview (frame 1500)
                'apertureRadius': aperture_ratio * 0.0,
                'direction': [0.009, 0.055, -0.998],
                'focusDistance': 109.59,
                'origin': [-0.832, 72.134, 387.389],
                'up': [0.017, 0.998, 0.055]
            },
            {  # 5. ACE2 receptor (frame 2000)
                'apertureRadius': aperture_ratio * 0.02,
                'direction': [-0.436, 0.035, -0.898],
                'focusDistance': 62.17,
                'origin': [-33.619, -164.994, 276.296],
                'up': [0.011, 0.999, 0.033]
            },
            {  # 6. Membrane overview (frame 2500)
                'apertureRadius': aperture_ratio * 0.0,
                'direction': [0.009, 0.055, -0.998],
                'focusDistance': 1.0,
                'origin': [0.293, 19.604, 1000],
                'up': [0.017, 0.998, 0.055]
            },
            {  # 7. (frame 3000)
                'apertureRadius': aperture_ratio * 0.0,
                'focusDistance': 1.0,
                'direction': [0.009, 0.055, -0.998],
                'origin': [0.293, 19.604, 1000],
                'up': [0.017, 0.998, 0.055]
            },
            {  # 8. (frame 3500)
                'apertureRadius': aperture_ratio * 0.0,
                'focusDistance': 60,
                'direction': [0.009, 0.055, -0.998],
                'origin': [0.293, 19.604, 1000],
                'up': [0.017, 0.998, 0.055]
            }
        ]
        '''Double the frames to make it smoother'''
        key_frames = list()
        for cameras_key_frame in cameras_key_frames:
            key_frames.append(cameras_key_frame)
            key_frames.append(cameras_key_frame)

        mm = MovieMaker(self._be)
        mm.build_camera_path(key_frames, 250, 150)
        self._log(1, '- Total number of frames: %d' % mm.get_nb_frames())

        self._core.set_application_parameters(viewport=self._image_size)
        self._core.set_application_parameters(image_stream_fps=0)

        frames_to_render = list()
        if len(frame_list) != 0:
            frames_to_render = frame_list
        else:
            if end_frame == 0:
                end_frame = mm.get_nb_frames()
            for i in range(start_frame, end_frame + 1, frame_step):
                frames_to_render.append(i)

        cumulated_rendering_time = 0
        nb_frames = len(frames_to_render)
        frame_count = 1
        '''Clipping planes'''
        self._set_clipping_planes()
        '''Frames'''
        for frame in frames_to_render:
            try:
                start = time.time()
                self._log(
                    1, '- Rendering frame %i (%i/%i)' %
                    (frame, frame_count, nb_frames))
                self._log(1, '------------------------------')
                '''Stop rendering during the loading of the scene'''
                status = self._core.set_renderer(samples_per_pixel=1,
                                                 subsampling=1,
                                                 max_accum_frames=1)
                '''Frame setup'''
                self._build_frame(frame)
                mm.set_current_frame(frame=frame,
                                     camera_params=self._core.
                                     BioExplorerPerspectiveCameraParams())

                self._log(1, '- Frame buffers')
                for shader in self._shaders:
                    '''Rendering settings'''
                    self._log(2, '-   ' + shader)
                    self._create_snapshot(shader, frame, mm)

                end = time.time()

                rendering_time = end - start
                cumulated_rendering_time += rendering_time
                average_rendering_time = cumulated_rendering_time / frame_count
                remaining_rendering_time = (
                    nb_frames - frame_count) * average_rendering_time
                self._log(1, '------------------------------')
                self._log(
                    1, 'Frame %i successfully rendered in %i seconds' %
                    (frame, rendering_time))

                hours = math.floor(remaining_rendering_time / 3600)
                minutes = math.floor(
                    (remaining_rendering_time - hours * 3600) / 60)
                seconds = math.floor(remaining_rendering_time - hours * 3600 -
                                     minutes * 60)

                expected_end_time = datetime.now() + timedelta(
                    seconds=remaining_rendering_time)
                self._log(
                    1,
                    'Estimated remaining time: %i hours, %i minutes, %i seconds'
                    % (hours, minutes, seconds))
                self._log(1,
                          'Expected end time       : %s' % expected_end_time)
                self._log(
                    1,
                    '--------------------------------------------------------------------------------'
                )
                frame_count += 1
            except Exception as e:
                self._log(1, 'ERROR: Failed to render frame %i' % frame)
                self._log(1, str(e))
                time.sleep(10)
                self._be = BioExplorer(self._url)
                self._core = self._be.core_api()
                mm = MovieMaker(self._be)

        self._core.set_application_parameters(image_stream_fps=20)
        self._log(1, 'Movie rendered, live long and prosper \V/')
class LowGlucoseScenario():

    def __init__(self, hostname, port, projection, output_folder, image_k=4,
                 image_samples_per_pixel=64, log_level=1, shaders=list(['bio_explorer'])):
        self._log_level = log_level
        self._hostname = hostname
        self._url = hostname + ':' + str(port)
        self._be = BioExplorer(self._url)
        self._core = self._be.core_api()
        self._image_size = [1920, 1080]
        self._image_samples_per_pixel = image_samples_per_pixel
        self._image_projection = projection
        self._image_output_folder = output_folder
        self._shaders = shaders
        self._prepare_movie(projection, image_k)
        self._log(1, '================================================================================')
        self._log(1, '- Version          : ' + self._be.version())
        self._log(1, '- URL              : ' + self._url)
        self._log(1, '- Projection       : ' + projection)
        self._log(1, '- Frame size       : ' + str(self._image_size))
        self._log(1, '- Export folder    : ' + self._image_output_folder)
        self._log(1, '- Samples per pixel: ' + str(self._image_samples_per_pixel))
        self._log(1, '================================================================================')

    def _log(self, level, message):
        if level <= self._log_level:
            print('[' + str(datetime.now()) + '] ' + message)

    def _get_transformation(self, start_frame, end_frame, frame, data):
        '''Progress'''
        progress = (frame - start_frame) * 1.0 / (end_frame - start_frame)
        progress = max(0.0, progress)
        progress = min(1.0, progress)

        '''Position'''
        start_pos = data[0].to_list()
        end_pos = data[2].to_list()
        pos = start_pos
        for i in range(3):
            pos[i] += (end_pos[i] - start_pos[i]) * progress

        '''Rotation'''
        start_rot = data[1]
        end_rot = data[3]
        rot = Quaternion.slerp(start_rot, end_rot, progress)
        if data[4] == ROTATION_MODE_SINUSOIDAL:
            rot = Quaternion.slerp(start_rot, end_rot, math.cos((progress - 0.5) * math.pi))

        return [Vector3(pos[0], pos[1], pos[2]), rot, progress * 100.0]

    def _add_viruses(self, frame):
        virus_radii = [45.0, 44.0, 45.0, 43.0, 44.0]
        virus_sequences = [
            [[0, 2599], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6]],
            [[0, 2599], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6]],
            [[0, 2599], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6]],
            [[0, 2999], [3000, 3099], [3100, 3299], [3300, 3750], [1e6, 1e6], [1e6, 1e6]],
            [[0, 599], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1e6, 1e6], [1600, 3750]],
        ]
        virus_flights_in = [
            [Vector3(-35.0, 300.0, -70.0), Quaternion(0.519, 0.671, 0.528, -0.036),
             Vector3(-5.0, 45.0, -33.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR],
            [Vector3(153.0, 300.0, -200.0), Quaternion(0.456, 0.129, -0.185, -0.860),
             Vector3(73.0, 93.0, -130.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR],
            # Virus used for SP-D zoom
            [Vector3(-100.0, 300.0, 20.0), Quaternion(0.087, 0.971, -0.147, -0.161),
             Vector3(-79.0, 108.0, 80.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR],
            # Virus getting inside cell
            [Vector3(224.9, 300.0, -220.0), Quaternion(-0.095, 0.652, -0.326, 0.677),
             Vector3(211.5, -104.9, -339.2), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR],
            # Virus used for detailed view of the Spike
            [Vector3(200.0, 20.0, -150.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             Vector3(200.0, 20.0, -150.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR]
        ]

        virus_flights_out = [
            # Unused
            [Vector3(-5.0, 45.0, -33.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             Vector3(-105.0, 45.0, -33.0), Quaternion(0.0, 1.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR],
            # Unused
            [Vector3(73.0, 93.0, -130.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             Vector3(-33.0, 93.0, -130.0), Quaternion(0.0, 0.0, 1.0, 0.0),
             ROTATION_MODE_LINEAR],
            # Virus used for SP-D zoom
            [Vector3(-84.0, 110.0, 75.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             Vector3(-100.0, -100.0, 51.2), Quaternion(0.087, 0.971, -0.147, -0.161),
             ROTATION_MODE_LINEAR],
            # Unused
            [Vector3(0.0, 0.0, 0.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             Vector3(0.0, 0.0, 0.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR],
            # Virus used for detailed view of the Spike
            [Vector3(200.0, 20.0, -150.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             Vector3(300.0, -100.0, -100.0), Quaternion(0.456, 0.129, -0.185, -0.860),
             ROTATION_MODE_LINEAR]
        ]

        for virus_index in range(len(virus_sequences)):
            name = 'Coronavirus ' + str(virus_index)
            current_sequence = 0
            sequences = virus_sequences[virus_index]
            for i in range(len(sequences)):
                if frame >= sequences[i][0]:
                    current_sequence = i

            '''Initialize position and rotation to end-of-flight values'''
            start_frame = sequences[current_sequence][0]
            end_frame = sequences[current_sequence][1]
            progress_in_sequence = (frame - start_frame) / (end_frame - start_frame)
            morphing_step = 0.0

            if current_sequence == 0:
                '''Flying'''
                pos, rot, progress = self._get_transformation(start_frame, end_frame,
                                                              frame, virus_flights_in[virus_index])
                self._log(3, '-   Virus %d is flying in... (%.01f pct)' % (virus_index, progress))
            elif current_sequence == 1:
                '''Landing'''
                pos = virus_flights_in[virus_index][2]
                rot = virus_flights_in[virus_index][3]
                pos.y -= landing_distance * progress_in_sequence
                self._log(3, '-   Virus %d is landing...' % virus_index)
            elif current_sequence == 2:
                '''Merging into cell'''
                pos = virus_flights_in[virus_index][2]
                rot = virus_flights_in[virus_index][3]
                morphing_step = (frame - start_frame) / (end_frame - start_frame)
                pos.y -= landing_distance
                self._log(3, '-   Virus %d is merging in (%.01f pct)' %
                          (virus_index, morphing_step * 100.0))
            elif current_sequence == 3:
                '''Inside cell'''
                self._log(3, '-   Virus %d is inside cell' % virus_index)
                '''Virus is not added to the scene'''
                self._be.remove_assembly(name=name)
                continue
            elif current_sequence == 4:
                '''Merging out of cell'''
                pos = virus_flights_out[virus_index][0]
                rot = virus_flights_out[virus_index][1]
                morphing_step = 1.0 - (frame - start_frame) / (end_frame - start_frame)
                self._log(3, '-   Virus %d is merging out (%.01f pct)' %
                          (virus_index, morphing_step * 100.0))
            else:
                '''Flying out'''
                pos, rot, progress = self._get_transformation(start_frame, end_frame,
                                                              frame, virus_flights_out[virus_index])
                self._log(3, '-   Virus %d is flying out... (%.01f pct)' % (virus_index, progress))

            if False:
                self._be.add_sphere(name=name, position=pos, radius=virus_radii[virus_index])
            else:
                self._be.add_coronavirus(
                    name=name, resource_folder=resource_folder,
                    representation=protein_representation, position=pos, rotation=rot,
                    add_glycans=add_glycans,
                    assembly_params=[virus_radii[virus_index], 5 * frame + 2 * virus_index,
                                     0.25, frame + 2 * virus_index + 1, 0.1, morphing_step]
                )

    def _add_cell(self, frame):

        name = 'Cell'
        nb_receptors = cell_nb_receptors
        size = scene_size * 2.0
        height = scene_size / 10.0
        position = Vector3(4.5, -186.0, 7.0)
        random_seed = 10

        nb_lipids = cell_nb_lipids
        ace2_receptor = Protein(
            sources=[pdb_folder + '6m18.pdb'], occurences=nb_receptors,
            position=Vector3(0.0, 6.0, 0.0))

        membrane = ParametricMembrane(
            sources=[
                membrane_folder + 'segA.pdb',
                membrane_folder + 'segB.pdb',
                membrane_folder + 'segC.pdb',
                membrane_folder + 'segD.pdb'
            ],
            occurences=cell_nb_lipids
        )

        cell = Cell(
            name=name, size=size, extra_parameters=[height],
            shape=BioExplorer.ASSEMBLY_SHAPE_SINUSOIDAL,
            membrane=membrane, receptor=ace2_receptor,
            random_position_seed=frame + 1, random_position_strength=0.025,
            random_rotation_seed=frame + 2, random_rotation_strength=0.2
        )

        self._be.add_cell(
            cell=cell, position=position,
            representation=protein_representation,
            random_seed=random_seed)

        '''Modify receptor position when attached virus enters the cell'''
        receptors_instances = [37]
        receptors_sequences = [[3000, 3099]]

        for i in range(len(receptors_instances)):
            instance_index = receptors_instances[i]
            sequence = receptors_sequences[i]
            start_frame = sequence[0]
            end_frame = sequence[1]
            if frame >= start_frame:
                if frame > end_frame:
                    '''Send receptor to outter space'''
                    status = self._be.set_protein_instance_transformation(
                        assembly_name=name, name=name + '_' + BioExplorer.NAME_RECEPTOR,
                        instance_index=instance_index, position=Vector3(0.0, 1e6, 0.0)
                    )
                else:
                    '''Current receptor transformation'''
                    transformation = self._be.get_protein_instance_transformation(
                        assembly_name=name, name=name + '_' + BioExplorer.NAME_RECEPTOR,
                        instance_index=instance_index
                    )
                    p = transformation['position'].split(',')
                    q = transformation['rotation'].split(',')
                    pos = Vector3(float(p[0]), float(p[1]), float(p[2]))
                    q2 = Quaternion(float(q[0]), float(q[1]), float(q[2]), float(q[3]))

                    '''Bend receptor'''
                    progress = (frame - start_frame) * 1.0 / (end_frame - start_frame)
                    q1 = Quaternion(axis=[0, 1, 0], angle=math.pi * progress)
                    rot = q2 * q1

                    pos.x += landing_distance * progress * 0.3
                    pos.y -= landing_distance * progress * 0.3

                    status = self._be.set_protein_instance_transformation(
                        assembly_name=name, name=name + '_' + BioExplorer.NAME_RECEPTOR,
                        instance_index=instance_index, position=pos, rotation=rot
                    )

        '''Glycans'''
        if nb_receptors != 0 and add_glycans:
            self._be.add_multiple_glycans(
                representation=glycan_representation, assembly_name=name,
                glycan_type=BioExplorer.NAME_GLYCAN_COMPLEX,
                protein_name=BioExplorer.NAME_RECEPTOR, paths=complex_paths,
                indices=[53, 90, 103, 322, 432, 690],
                assembly_params=[0, 0, 0.0, frame + 3, 0.2]
            )
            self._be.add_multiple_glycans(
                representation=glycan_representation, assembly_name=name,
                glycan_type=BioExplorer.NAME_GLYCAN_HYBRID,
                protein_name=BioExplorer.NAME_RECEPTOR, paths=hybrid_paths,
                indices=[546],
                assembly_params=[0, 0, 0.0, frame + 4, 0.2])

            indices = [[155, Quaternion(0.707, 0.0, 0.707, 0.0)],
                       [730, Quaternion(0.707, 0.0, 0.707, 0.0)]]
            count = 0
            for index in indices:
                o_glycan_name = name + '_' + BioExplorer.NAME_GLYCAN_O_GLYCAN + '_' + str(index[0])
                o_glycan = Sugars(
                    assembly_name=name, name=o_glycan_name, source=o_glycan_paths[0],
                    protein_name=name + '_' + BioExplorer.NAME_RECEPTOR, representation=glycan_representation,
                    chain_ids=[2, 4], site_indices=[index[0]], rotation=index[1],
                    assembly_params=[0, 0, 0.0, frame + count + 5, 0.2])
                self._be.add_sugars(o_glycan)
                count += 1

    def _add_surfactant_d(self, name, position, rotation, random_seed):
        surfactant_d = Surfactant(
            name=name, surfactant_protein=BioExplorer.SURFACTANT_PROTEIN_D,
            head_source=surfactant_head_source,
            branch_source=surfactant_branch_source)
        self._be.add_surfactant(
            surfactant=surfactant_d, representation=protein_representation,
            position=position, rotation=rotation, random_seed=random_seed)

    def _add_surfactant_a(self, name, position, rotation, random_seed):
        surfactant_a = Surfactant(
            name=name, surfactant_protein=BioExplorer.SURFACTANT_PROTEIN_A,
            head_source=surfactant_head_source,
            branch_source=surfactant_branch_source)
        self._be.add_surfactant(
            surfactant=surfactant_a, representation=protein_representation,
            position=position, rotation=rotation, random_seed=random_seed)

    def _add_surfactants_d(self, frame):
        spd_sequences = [[0, 3750], [0, 2600], [0, 2600], [0, 3750]]
        spd_random_seeds = [1, 1, 1, 2]

        spd_flights = [
            [Vector3(300,  124.0, 0.0), Quaternion(-0.095, 0.652, -0.326, 0.677),
             Vector3(74.0,  24.0, -45.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_SINUSOIDAL],
            # SP-D is used for the head focus on 3rd virus spike
            [Vector3(-50,  250.0, 20.0), Quaternion(0.087, 0.971, -0.147, -0.161),
             Vector3(-11.0,  108.0, 20.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR],
            # SP-D attaching to lymphocyte
            [Vector3(-200.0, 100.0, 100.0), Quaternion(0.519, 0.671, 0.528, -0.036),
             Vector3(-135.0, 135.0, 140.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_LINEAR],
            [Vector3(100.0,  0.0, -80.0), Quaternion(-0.095, 0.652, -0.326, 0.677),
             Vector3(-260.0,  50.0, 150.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_SINUSOIDAL]
        ]

        for surfactant_index in range(len(spd_sequences)):
            name = 'Surfactant-D ' + str(surfactant_index)
            sequence = spd_sequences[surfactant_index]
            pos, rot, progress = self._get_transformation(
                start_frame=sequence[0], end_frame=sequence[1],
                frame=frame, data=spd_flights[surfactant_index])
            self._log(3, '-   ' + name + ' (%.01f pct)' % progress)
            self._add_surfactant_d(
                name=name, position=pos, rotation=rot,
                random_seed=spd_random_seeds[surfactant_index])

    def _add_surfactants_a(self, frame):
        spa_sequences = [[0, 3750]]
        spa_random_seeds = [2]
        spa_frames = [
            [Vector3(300.0,  -50.0, -50.0), Quaternion(-0.095, 0.652, -0.326, 0.677),
             Vector3(100.0,  50.0, 150.0), Quaternion(1.0, 0.0, 0.0, 0.0),
             ROTATION_MODE_SINUSOIDAL],
        ]

        for surfactant_index in range(len(spa_frames)):
            name = 'Surfactant-A ' + str(surfactant_index)
            sequence = spa_sequences[surfactant_index]
            pos, rot, progress = self._get_transformation(
                start_frame=sequence[0], end_frame=sequence[1],
                frame=frame, data=spa_frames[surfactant_index])
            self._log(3, '-   ' + name + ' (%.01f pct)' % progress)
            self._add_surfactant_a(
                name=name, position=pos, rotation=rot,
                random_seed=spa_random_seeds[surfactant_index])

    def _add_glucose(self, frame):
        glucose = Protein(
            sources=[glucose_path], load_non_polymer_chemicals=True,
            occurences=nb_glucoses)
        volume = Volume(
            name=BioExplorer.NAME_GLUCOSE, size=scene_size, protein=glucose,
            random_position_seed=frame + 20, random_position_stength=scene_size / 600.0,
            random_rotation_seed=frame + 21, random_rotation_stength=0.3
        )
        status = self._be.add_volume(
            volume=volume, representation=protein_representation,
            position=Vector3(0.0, scene_size / 2.0 - 200.0, 0.0),
            random_seed=100)

    def _add_lactoferrins(self, frame):
        lactoferrin = Protein(
            sources=[lactoferrin_path], load_non_polymer_chemicals=True,
            occurences=nb_lactoferrins)
        lactoferrins_volume = Volume(
            name=BioExplorer.NAME_LACTOFERRIN, size=scene_size, protein=lactoferrin,
            random_position_seed=frame + 30, random_position_stength=scene_size / 400.0,
            random_rotation_seed=frame + 31, random_rotation_stength=0.3
        )
        status = self._be.add_volume(
            volume=lactoferrins_volume, representation=protein_representation,
            position=Vector3(0.0, scene_size / 2.0 - 200.0, 0.0),
            random_seed=101)

    def _add_defensins(self, frame):
        defensin = Protein(
            sources=[defensin_path], load_non_polymer_chemicals=True,
            occurences=nb_defensins)
        defensins_volume = Volume(
            name=BioExplorer.NAME_DEFENSIN, size=scene_size, protein=defensin,
            random_position_seed=frame + 40, random_position_stength=scene_size / 400.0,
            random_rotation_seed=frame + 41, random_rotation_stength=0.3
        )
        status = self._be.add_volume(
            volume=defensins_volume, representation=protein_representation,
            position=Vector3(0.0, scene_size / 2.0 - 200.0, 0.0),
            random_seed=102)

    def _add_lymphocyte(self, frame):
        if frame < 1400:
            '''Lymphocyte is not in the field of view'''
            return

        '''Protein animation params'''
        params = [0, 0, 0.0, frame + 2, 0.2]

        clip_planes = [
            [1.0, 0.0, 0.0, scene_size * 1.5 + 5],
            [-1.0, 0.0, 0.0, scene_size * 1.5 + 5],
            [0.0, 0.0, 1.0, scene_size + 5],
            [0.0, 0.0, -1.0, scene_size + 5]
        ]

        name = 'Emile'
        lymphocyte_sequence = [0, 3750]
        lymphocyte_frames = [Vector3(-2500.0, 100.0, 30.0), Quaternion(1.0, 0.0, 0.0, 0.0),
                             Vector3(-830.0, 100.0, 30.0), Quaternion(0.707, 0.707, 0.0, 0.0),
                             ROTATION_MODE_LINEAR]

        protein_sources = [
            membrane_folder + 'segA.pdb',
            membrane_folder + 'segB.pdb',
            membrane_folder + 'segC.pdb',
            membrane_folder + 'segD.pdb'
        ]

        mesh_based_membrane = MeshBasedMembrane(
            mesh_source=lymphocyte_path, protein_sources=protein_sources,
            density=lymphocyte_density, surface_variable_offset=lymphocyte_surface_variable_offset,
            assembly_params=params
        )

        pos, rot, progress = self._get_transformation(
            start_frame=lymphocyte_sequence[0], end_frame=lymphocyte_sequence[1],
            frame=frame, data=lymphocyte_frames)
        self._log(3, '-   ' + name + ' (%.01f pct)' % progress)

        scale = Vector3(1.0, 1.0, 1.0)
        status = self._be.add_mesh_based_membrane(
            name, mesh_based_membrane, position=pos,
            rotation=rot, scale=scale,
            clipping_planes=clip_planes
        )

        for i in range(len(protein_sources)):
            status = self._be.set_protein_color_scheme(
                assembly_name=name, name=BioExplorer.NAME_MEMBRANE + '_' + str(i),
                color_scheme=BioExplorer.COLOR_SCHEME_CHAINS,
                palette_name='OrRd', palette_size=5)

    def _set_materials(self):
        '''Default materials'''
        self._be.apply_default_color_scheme(
            shading_mode=BioExplorer.SHADING_MODE_DIFFUSE, specular_exponent=50.0)

    def _create_snapshot(self, shader, frame, movie_maker):
        samples_per_pixel = self._image_samples_per_pixel

        '''Renderer'''
        if shader == 'albedo':
            self._core.set_renderer(current='albedo')
        elif shader == 'ambient_occlusion':
            self._core.set_renderer(
                current='ambient_occlusion', samples_per_pixel=1, subsampling=1, max_accum_frames=1)
            params = self._core.AmbientOcclusionRendererParams()
            params.samplesPerFrame = 32
            params.rayLength = 5.0
            self._core.set_renderer_params(params)
            samples_per_pixel = 4
        elif shader == 'depth':
            status = self._core.set_renderer(
                current='depth', samples_per_pixel=1, subsampling=1, max_accum_frames=1)
            params = status = self._core.DepthRendererParams()
            params.infinity = 2000.0
            status = self._core.set_renderer_params(params)
            samples_per_pixel = 2
        elif shader == 'raycast_Ns':
            status = self._core.set_renderer(
                current='raycast_Ns', samples_per_pixel=1, subsampling=1, max_accum_frames=1)
            samples_per_pixel = 2
        else:
            status = self._core.set_renderer(
                background_color=[96 / 255, 125 / 255, 139 / 255],
                current='bio_explorer', head_light=False,
                samples_per_pixel=1, subsampling=1, max_accum_frames=1)
            params = self._core.BioExplorerRendererParams()
            params.exposure = 1.0
            params.gi_samples = 1
            params.gi_weight = 0.3
            params.gi_distance = 5000
            params.shadows = 0.8
            params.soft_shadows = 0.05
            params.fog_start = 1000
            params.fog_thickness = 300
            params.max_bounces = 1
            params.use_hardware_randomizer = True
            status = self._core.set_renderer_params(params)
            samples_per_pixel = self._image_samples_per_pixel

            '''Lights'''
            status = self._core.clear_lights()
            status = self._core.add_light_directional(
                angularDiameter=0.5, color=[1, 1, 1], direction=[-0.7, -0.4, -1],
                intensity=1.0, is_visible=False
            )

        movie_maker.create_snapshot(
            size=self._image_size, path=self._image_output_folder + '/' + shader,
            base_name='%05d' % frame, samples_per_pixel=samples_per_pixel)

        '''Camera'''
        status = self._core.set_camera(current='bio_explorer_perspective')

    def _build_frame(self, frame):
        self._log(2, '- Resetting scene...')
        self._be.reset()

        self._log(2, '- Building surfactants...')
        self._add_surfactants_d(frame)
        self._add_surfactants_a(frame)

        self._log(2, '- Building glucose...')
        self._add_glucose(frame)

        self._log(2, '- Building lactoferrins...')
        self._add_lactoferrins(frame)

        self._log(2, '- Building defensins...')
        self._add_defensins(frame)

        self._log(2, '- Building viruses...')
        self._add_viruses(frame)

        self._log(2, '- Building cell...')
        self._add_cell(frame)

        self._log(2, '- Building lymphocyte...')
        self._add_lymphocyte(frame)

        self._log(2, '- Setting materials...')
        self._set_materials()

        self._log(2, '- Showing models...')
        status = self._be.set_models_visibility(True)
        status = self._core.set_renderer()

    def _make_export_folder(self, folder):
        import os
        path = self._image_output_folder + '/' + folder
        command_line = 'mkdir -p ' + path
        os.system(command_line)
        command_line = 'ls ' + path
        if os.system(command_line) != 0:
            self._log(3, 'ERROR: Failed to create folder ' + path)

    def _make_export_folders(self):
        for folder in self._shaders:
            self._make_export_folder(folder)

    def _prepare_movie(self, projection, image_k):
        if projection == 'perspective':
            aperture_ratio = 1.0
            self._image_size = [image_k*960, image_k*540]
            self._core.set_camera(current='bio_explorer_perspective')
        elif projection == 'fisheye':
            self._image_size = [int(image_k*1024), int(image_k*1024)]
            self._core.set_camera(current='fisheye')
        elif projection == 'panoramic':
            self._image_size = [int(image_k*1024), int(image_k*1024)]
            self._core.set_camera(current='panoramic')
        elif projection == 'opendeck':
            self._image_size = [7*2160, 3840]
            self._core.set_camera(current='cylindric')

        self._image_output_folder = self._image_output_folder + '/' + \
            projection + '/' + str(self._image_size[0]) + 'x' + str(self._image_size[1])
        self._make_export_folders()

    def _set_clipping_planes(self):
        '''Clipping planes'''
        clip_planes = [
            [1.0, 0.0, 0.0, scene_size * 1.5 + 5],
            [-1.0, 0.0, 0.0, scene_size * 1.5 + 5],
            [0.0, 0.0, 1.0, scene_size + 5],
            [0.0, 0.0, -1.0, scene_size + 5]
        ]
        cps = self._core.get_clip_planes()
        ids = list()
        if cps:
            for cp in cps:
                ids.append(cp['id'])
        self._core.remove_clip_planes(ids)
        for plane in clip_planes:
            self._core.add_clip_plane(plane)

    def render_movie(self, start_frame=0, end_frame=0, frame_step=1, frame_list=list()):
        '''Accelerate loading by not showing models as they are loaded'''
        status = self._be.set_general_settings(model_visibility_on_creation=False)

        aperture_ratio = 0.0
        cameras_key_frames = [
            {  # Virus overview (on 5th virus)
                'apertureRadius': aperture_ratio * 0.02,
                'direction': [0.0, 0.0, -1.0],
                'focusDistance': 139.56,
                'origin': [199.840, 20.634, 34.664],
                'up': [0.0, 1.0, 0.0]
            },
            {  # Protein S (on 5th virus)
                'apertureRadius': aperture_ratio * 0.02,
                'direction': [0.0, 0.0, -1.0],
                'focusDistance': 23.60,
                'origin': [195.937, 74.319, -111.767],
                'up': [0.0, 1.0, 0.0]
            },
            {  # Protein M and E
                'apertureRadius': aperture_ratio * 0.02,
                'direction': [-0.047, -0.298, -0.953],
                'focusDistance': 54.56,
                'origin': [208.156, 55.792, -59.805],
                'up': [0.003, 0.954, -0.298]
            },
            {  # Overview SPA
                'apertureRadius': aperture_ratio * 0.001,
                'direction': [-0.471, -0.006, -0.882],
                'focusDistance': 444.63,
                'origin': [238.163, 46.437, 372.585],
                'up': [0.0, 1.0, 0.0]
            },
            {  # Overview SPD
                'apertureRadius': aperture_ratio * 0.001,
                'focusDistance': 444.63,
                'direction': [-0.471, -0.005, -0.881],
                'origin': [238.163, 46.436, 372.584],
                'up': [0.0, 1.0, 0.0]
            },
            {  # Zoom SPD (on 3rd virus)
                'apertureRadius': aperture_ratio * 0.02,
                'focusDistance': 31.86,
                'direction': [-0.821, 0.202, -0.533],
                'origin': [-9.827, 110.720, 60.944],
                'up': [0.178, 0.979, 0.098]
            },
            {  # Overview scene
                'apertureRadius': aperture_ratio * 0.0,
                'focusDistance': 1.0,
                'direction': [-1.0, 0.0, 0.0],
                'origin': [1008.957, 29.057, 113.283],
                'up': [0.0, 1.0, 0.0]
            },
            {  # Cell view
                'apertureRadius': aperture_ratio * 0.0,
                'direction': [0.0, 0.0, -1.0],
                'focusDistance': 1.0,
                'origin': [150.0, -170.0, 400.0],
                'up': [0.0, 1.0, 0.0]
            }
        ]

        '''Double the frames to make it smoother'''
        key_frames = list()
        for cameras_key_frame in cameras_key_frames:
            key_frames.append(cameras_key_frame)
            key_frames.append(cameras_key_frame)

        mm = MovieMaker(self._be)
        mm.build_camera_path(key_frames, 250, 150)
        self._log(1, '- Total number of frames: %d' % mm.get_nb_frames())

        self._core.set_application_parameters(viewport=self._image_size)
        self._core.set_application_parameters(image_stream_fps=0)

        frames_to_render = list()
        if len(frame_list) != 0:
            frames_to_render = frame_list
        else:
            if end_frame == 0:
                end_frame = mm.get_nb_frames()
            for i in range(start_frame, end_frame + 1, frame_step):
                frames_to_render.append(i)

        cumulated_rendering_time = 0
        nb_frames = len(frames_to_render)
        frame_count = 1

        '''Clipping planes'''
        self._set_clipping_planes()

        '''Frames'''
        for frame in frames_to_render:
            try:
                start = time.time()
                self._log(1, '- Rendering frame %i (%i/%i)' % (frame, frame_count, nb_frames))
                self._log(1, '------------------------------')

                '''Stop rendering during the loading of the scene'''
                status = self._core.set_renderer(
                    samples_per_pixel=1, subsampling=1, max_accum_frames=1)

                '''Frame setup'''
                self._build_frame(frame)
                mm.set_current_frame(
                    frame=frame, camera_params=self._core.BioExplorerPerspectiveCameraParams())

                self._log(1, '- Frame buffers')
                for shader in self._shaders:
                    '''Rendering settings'''
                    self._log(2, '-   ' + shader)
                    self._create_snapshot(shader, frame, mm)

                end = time.time()

                rendering_time = end - start
                cumulated_rendering_time += rendering_time
                average_rendering_time = cumulated_rendering_time / frame_count
                remaining_rendering_time = (nb_frames - frame_count) * average_rendering_time
                self._log(1, '------------------------------')
                self._log(1, 'Frame %i successfully rendered in %i seconds' %
                          (frame, rendering_time))

                hours = math.floor(remaining_rendering_time / 3600)
                minutes = math.floor((remaining_rendering_time - hours * 3600) / 60)
                seconds = math.floor(remaining_rendering_time - hours * 3600 - minutes * 60)

                expected_end_time = datetime.now() + timedelta(seconds=remaining_rendering_time)
                self._log(1, 'Estimated remaining time: %i hours, %i minutes, %i seconds' %
                          (hours, minutes, seconds))
                self._log(1, 'Expected end time       : %s' % expected_end_time)
                self._log(
                    1, '--------------------------------------------------------------------------------')
                frame_count += 1
            except Exception as e:
                self._log(1, 'ERROR: Failed to render frame %i' % frame)
                self._log(1, str(e))
                time.sleep(10)
                self._be = BioExplorer(self._url)
                self._core = self._be.core_api()
                mm = MovieMaker(self._be)

        self._core.set_application_parameters(image_stream_fps=20)
        self._log(1, 'Movie rendered, live long and prosper \V/')