Esempio n. 1
0
def main(wave_centers, anti_wave_centers):

    #------------------------------------------------------------------------------------------------------
    # CONFIGS - USER VARIABLES SET IN THE UI
    # Variables from the Blender Panel
    #------------------------------------------------------------------------------------------------------

    config.wave_centers = wave_centers
    config.anti_wave_centers = anti_wave_centers

    #------------------------------------------------------------------------------------------------------
    # PHASE CONFIGURATION
    # Modifications specfic to this phase
    #------------------------------------------------------------------------------------------------------

    # Granule properties
    granule_wavelength = 2  # The default wavelength size such that neutrinos are at even nodes and antineutrinos are at odd nodes
    granule_array_count = round(
        config.spacetime_length / 2
    )  # The total length of the simulation is divided in half for two separate arrays
    granule_size = 0.2  # Granule radius - displayed as a sphere
    granule_force = 1  # Granule force is the default force strength used in Blender

    # Array properties
    array_size = granule_wavelength * granule_array_count  # A grid is created of granule "wavelengths", using half the total length as the array_size because it is duplicated for opposite direction in x, y and z.
    position = (
        array_size - granule_wavelength / 2
    )  # Position of outer array used for calculation of forces and array creation

    # Properties that change based on dimension.
    if config.dimensions == 3:
        range = [
            (-1, -1, -1), (-1, -1, 1), (-1, 1, -1), (-1, 1, 1), (1, -1, -1),
            (1, -1, 1), (1, 1, -1), (1, 1, 1)
        ]  # Starting points and resizing for granule array, wave center emitter and container.
        number_cuts = 2  # Controls the subdivision of array cube for wave.  A larger number is better for wave production, but for performance reasons, it is managed by dimensions.
        granule_count = (
            2 * granule_array_count
        )**3 / 8  # Reduce granules and cuts by dimnesion for performance reasons.
        config.flow = 1  # Flow is different by dimension due to the way granule density is managed for performance.
        transform_value = (array_size, array_size, array_size
                           )  # Resizing cube properties based on dimensions.
    elif config.dimensions == 2:
        range = [(-1, -1, 0), (-1, 1, 0), (1, -1, 0), (1, 1, 0)]
        number_cuts = 3
        granule_count = (8 * granule_array_count)**2 / 2
        config.flow = 10
        transform_value = (array_size, array_size, granule_wavelength)
    else:
        range = [(-1, 0, 0), (1, 0, 0)]
        number_cuts = 4
        granule_count = (24 * granule_array_count)
        config.flow = 0.1
        transform_value = (array_size, granule_wavelength, granule_wavelength)

    ############################################ PROGRAM #####################################################

    text = config.project_text + ": Phase 1 - Spacetime"
    functions.add_text(name=text,
                       text=text,
                       location=(-1000, 1000, 0),
                       radius=50)
    bpy.data.objects[text].hide_set(True)

    #------------------------------------------------------------------------------------------------------
    # COLLECTIONS
    # Collections are used to organize the objects in Blender
    #------------------------------------------------------------------------------------------------------

    context = bpy.context
    scene = context.scene
    granules_collection = bpy.data.collections.new('Granules')
    bpy.context.scene.collection.children.link(granules_collection)
    neutrinos_collection = bpy.data.collections.new('Neutrinos')
    bpy.context.scene.collection.children.link(neutrinos_collection)
    default_collection = context.scene.collection

    #------------------------------------------------------------------------------------------------------
    # CALCULATIONS
    # Calculations used in this phase use EWT equations for neutrinos - see https://energywavetheory.com/subatomic-particles/neutrino/
    #------------------------------------------------------------------------------------------------------

    # Neutrino standing wavelength is the fundamental wavelength.
    calc_radius = config.fundamental_wavelength

    # Neutrino energy should be calculated by collective energy of standing wave granules.  It is calculated based on EWT equations for now.  TODO: use granule physics to calculate total energy.
    calc_energy_joules = (
        (4 / 3) * config.pi * config.fundamental_density *
        (config.fundamental_amplitude**6) * config.fundamental_wavespeed**
        2) / (config.fundamental_wavelength_no_gfactor**3)

    # Neutrino energy is first calculated in joues.  To convert to electron-volts (eV), the following conversion is used: 6.242e+18 J to eV.
    calc_energy = calc_energy_joules * 6.242e+18

    # Determine the time it takes for the wave to reach the center, measured in number of keyframes in Blender.  TODO: This would be automatically displaced when a wave center can reflect granules to create standing waves.
    if config.wave_speed > 0:
        frame_to_center = int(
            ((2 * granule_array_count - granule_wavelength) /
             (config.wave_speed)) * (config.dimensions**(1 / 2)))
    else:
        frame_to_center = 1

    # Determine the total number of neutrinos.  The simulator has different scenarios for showing neutrinos at the center or randomly placed to show wave interference patterns.
    total_neutrinos = config.wave_centers + config.anti_wave_centers

    #------------------------------------------------------------------------------------------------------
    # FUNCTIONS
    # Shared wave properties for the wave modifier
    #------------------------------------------------------------------------------------------------------

    # Add standard wave modifiers based on settings for longitudinal or transverse waves.  This manages the configuration options for Blender's wave modifier.
    def wave_mods(mod):
        m = mod

        if config.longitudinal_wave == True:  # For longitudinal waves, the mesh object deforms.
            m.use_normal = True
            m.use_normal_x = True
        else:
            m.use_normal = False

        if config.transverse_wave == True:  # For a transverse wave, the amplitude is perpendicular to the direction of wave propagation so z direction is used.
            m.use_normal_z = True
        else:
            m.use_normal_z = False

        if config.dimensions == 1:  # For a 1D wave, y is disbaled to show x direction.
            m.use_normal_y = False

        m.height = config.wave_amplitude  # Height of wave is the wave amplitude.
        m.speed = config.wave_speed  # The wave speed is based on speed per frame of the wave ripple using Blender's wave modifier: https://docs.blender.org/manual/en/latest/modeling/modifiers/deform/wave.html
        m.width = granule_wavelength / 2  # Blender's wave modifier width is not a true wavelength.  Two widths to a wavelength.
        m.narrowness = granule_wavelength  # Narrowness is a Blender wave modifier property.  For a good sine wave it should be twice the width.

        if config.transverse_wave == False and config.longitudinal_wave == False:
            m.height = 0  # If no option to view either a longitudinal or transverse wave, amplitude is set to zero so no waves appear

    # Add waves from the corner positions
    def add_waves(mod_name, neutrino_location=False, x=0, y=0):

        # If the neutrino's location is not set as known x, y properties the wave is generated from the corners.  Otherwise, from the neutrino(s).
        if not neutrino_location:

            # The corner positions are determined and iterated through to create a wave at each corner
            for multiplier in range:

                x = multiplier[0] * position
                y = multiplier[1] * position
                z = multiplier[2] * position

                # Create a wave using Blender's wave modifier
                name = mod_name + " " + str(multiplier)

                # A special case to show standing waves
                if mod_name == "Neutrino - Standing Wave":
                    m = o.modifiers.new(name, type='WAVE')
                else:
                    m = s.modifiers.new(name, type='WAVE')
                m.start_position_x = x
                m.start_position_y = y
                wave_mods(m)
                m.falloff_radius = array_size * 2  # Falloff should be inverse square not a set distance.  TODO: Need to modify Blender wave modifier for inverse square law for falloff.

        else:

            # Set the position of the wave to match the neutrino's position as passed in the x, y properties
            name = mod_name + " " + str(x) + ", " + str(y)
            m = s.modifiers.new(name, type='WAVE')
            m.start_position_x = x
            m.start_position_y = y
            wave_mods(m)
            m.falloff_radius = array_size * 2
            m.start_position_object = o  # Linking wave placement with the neutrino to be able to move the object and have wave change with it.

    # Add granules into the mesh object as a particle system.  Returns the particle settings to be overriden outside of function.
    def add_granules(name):
        o = bpy.data.objects[name]
        o.show_instancer_for_viewport = False
        m = o.modifiers.new(name, type='PARTICLE_SYSTEM')
        ps = m.particle_system
        pset = ps.settings
        pset.display_size = granule_size
        pset.frame_end = 1
        pset.lifetime = config.num_frames
        pset.normal_factor = 0
        pset.physics_type = 'NO'
        pset.use_modifier_stack = True
        pset.emit_from = 'VOLUME'
        pset.use_emit_random = True
        pset.distribution = 'RAND'
        return pset

    # Determine a random position in spacetime for the neutrino to be placed.
    def create_random_location(antimatter=False):

        # This function generates even or odd numbers depending on matter (even) or antimatter (odd) so that placement is half wavelength nodes apart for antimatter
        if antimatter:
            start_range = 1
        else:
            start_range = 2
        step_range = granule_wavelength

        # Generates a random x, y and z value for neutrinos (even numbers) and antineutrinos (odd numbers)
        x_rand = random.randrange(-array_size + start_range, array_size - 1,
                                  step_range)
        y_rand = random.randrange(-array_size + start_range, array_size - 1,
                                  step_range)
        z_rand = random.randrange(-array_size + start_range, array_size - 1,
                                  step_range)

        # Due to the way Blender moves waves up in z axis, the neutrino needs to move up the z axis for only the wave view (doesn't apply to granules)
        if config.show_granules == True:
            z_locate = 0
        else:
            z_locate = config.wave_amplitude

        # Depending on the dimensions simulated, y or z axis may be zero for location to keep it in the plane or the line.
        if config.dimensions == 3:
            random_location = (x_rand, y_rand, z_rand)
        elif config.dimensions == 2:
            random_location = (x_rand, y_rand, z_locate)
        else:
            random_location = (x_rand, 0, z_locate)
            y_rand = 0

        # Add waves into the simulation originating from the neutrino particles.  Only x and y passed as Blender's wave modifier doesn't support a z.
        add_waves(mod_name="Neutrino",
                  neutrino_location=True,
                  x=x_rand,
                  y=y_rand)

        return random_location

    #------------------------------------------------------------------------------------------------------
    # SPACETIME CONTAINER
    # Add a "universe" for granules and their wave centers.  The container confines them to move within this user-defined space.
    # The spacetime container is also used to show wave motion, as an aggregate of granule motion when "Wave" view is selected.
    #------------------------------------------------------------------------------------------------------

    bpy.ops.mesh.primitive_cube_add(size=granule_wavelength,
                                    enter_editmode=True,
                                    location=(0, 0, 0))
    bpy.context.active_object.name = "Spacetime"
    s = bpy.data.objects["Spacetime"]
    bpy.ops.transform.resize(value=transform_value)
    bpy.ops.mesh.subdivide(
        number_cuts=(array_size * 2)**2
    )  # The number of cuts is used for wave modifiers.  Since spacetime cube is set by user, this is proportional
    bpy.ops.object.mode_set(mode="OBJECT")
    bpy.ops.object.shade_smooth()
    bpy.ops.object.modifier_add(
        type='COLLISION'
    )  # Spacetime is set as a collision object in Blender to keep particles within the "universe"
    bpy.ops.rigidbody.object_add()
    s.rigid_body.type = 'PASSIVE'

    # There are two views.  One in which GRANULES are shown (and the spacetime cube is hidden), and another view where WAVES are illustrated instead of granules
    if config.show_granules == True and total_neutrinos < 2:
        s.hide_set(
            True
        )  # If Granule view, then hide the spacetime container to see granules.  If more than 2 neutrinos shown, spacetime is hidden a different way in the particle emitter

    else:
        if total_neutrinos < 2:
            add_waves(
                mod_name="Wave"
            )  # If Wave view, waves are shown instead.  Waves are added at corners for zero or one neutrino.  For multiple neutrinos, it is handled a different way in emitter

    #------------------------------------------------------------------------------------------------------
    # GRANULE ARRAY
    # Configuration of granules using an array modifier
    #------------------------------------------------------------------------------------------------------

    # Granule array is used to illustrate waves in scenarios with zero or one neutrinos.  Otherwise, spacetime volume is filled with granules because of complexity of multiple particles.
    if total_neutrinos < 2:

        # Determine the corner positions to create an array of granules where waves are directed toward the center.
        for multiplier in range:

            x = multiplier[0] * position
            y = multiplier[1] * position
            z = multiplier[2] * position

            # Set the first granule cube, which is one wavelength.
            name = "Granule Array " + str(multiplier)
            bpy.ops.mesh.primitive_cube_add(size=granule_wavelength,
                                            enter_editmode=True,
                                            location=(x, y, z))
            bpy.context.active_object.name = name
            o = bpy.data.objects[name]
            functions.link_collection(collection=granules_collection)
            bpy.ops.mesh.subdivide(number_cuts=number_cuts)
            bpy.ops.object.mode_set(mode="OBJECT")
            bpy.ops.object.shade_smooth()

            # Create an array of granule wavelengths in the x or -x direction
            bpy.ops.object.modifier_add(type='ARRAY')
            o.modifiers["Array"].name = name + "x"
            o.modifiers[name + "x"].count = granule_array_count
            o.modifiers[name +
                        "x"].relative_offset_displace[0] = -multiplier[0]

            # Expand the array in the y direction if 2D or 3D set
            if config.dimensions == 2 or config.dimensions == 3:
                bpy.ops.object.modifier_add(type='ARRAY')
                o.modifiers["Array"].name = name + "y"
                o.modifiers[name + "y"].count = granule_array_count
                o.modifiers[name +
                            "y"].relative_offset_displace[1] = -multiplier[1]
                o.modifiers[name + "y"].relative_offset_displace[0] = 0

            # Expand the array in the z direction if 3D set
            if config.dimensions == 3:
                bpy.ops.object.modifier_add(type='ARRAY')
                o.modifiers["Array"].name = name + "z"
                o.modifiers[name + "z"].count = granule_array_count
                o.modifiers[name +
                            "z"].relative_offset_displace[2] = -multiplier[2]
                o.modifiers[name + "z"].relative_offset_displace[0] = 0

            # Create a wave using Blender's wave modifier.
            m = o.modifiers.new(str(multiplier), type='WAVE')
            wave_mods(mod=m)

            # Create the granule particle system.
            pset = add_granules(name=name)

            if config.show_granules == True:
                pset.count = granule_count
            else:
                pset.count = 0  # If show_granules is not selected, then wave motion is shown and no granules are used in the particle emitter

            # Granules use the wave modifier and physics cannot be used.  Subtituting all granules for a collective force at the origination point. TODO: Real physics should be used for granules and this should be removed.
            name = "Granule Force " + str(multiplier)
            bpy.ops.object.effector_add(type='FORCE',
                                        enter_editmode=False,
                                        location=(x, y, z))
            bpy.context.active_object.name = name
            functions.link_collection(collection=granules_collection)
            o = bpy.data.objects[name]
            o.field.strength = -(
                granule_force * granule_count * config.wave_speed *
                config.wave_amplitude**2
            )**(
                1 / config.dimensions
            )  # Force proportional to count and amplitude since it uses a collective force of all granules
            o.field.flow = config.flow
            o.hide_set(True)

    #------------------------------------------------------------------------------------------------------
    # NEUTRINOS
    # Adds wave centers to the spacetime array and the standing wave structures that forms the neutrino particle.
    #------------------------------------------------------------------------------------------------------

    # If no neutrinos or no waves from external forces, it doesn't do anything.  If one neutrino, it places it at the center to show standing waves.  If more than one, they are placed randomly in spacetime.
    if total_neutrinos == 0:

        config.show_calculations = False  # There are no neutrinos to simulate.  Only wave motion.  Disable calculations.

    # One neutrino
    elif total_neutrinos == 1:

        # There are various scenarios based on showing granules, waves or motion.  Begin by creating the neutrino shell used for all scenarios.
        bpy.ops.mesh.primitive_uv_sphere_add(radius=granule_wavelength,
                                             enter_editmode=False,
                                             location=(0, 0, 0))
        functions.link_collection(collection=neutrinos_collection)
        bpy.context.active_object.name = "Neutrino Shell"
        o = bpy.data.objects["Neutrino Shell"]
        bpy.ops.object.shade_smooth()
        functions.add_color(name="Neutrino Shell",
                            color=config.neutrino_color,
                            transparent=True)
        o.hide_set(True)

        # If Show Motion is set in the UI, the neutrino is placed in spacetime and forced towards the center by an external force; else placed at center.
        if config.show_neutrino_motion == True:

            # If show_neutrino_motion is true, use a particle emitter to generate a neutrino that will react to forces as it moves through the spacetime array.
            bpy.ops.mesh.primitive_cube_add(size=granule_wavelength,
                                            enter_editmode=False,
                                            location=(0, 0, 0))
            functions.link_collection(collection=neutrinos_collection)
            bpy.context.active_object.name = "Neutrino"
            o = bpy.data.objects["Neutrino"]
            bpy.ops.transform.resize(value=transform_value)
            o.show_instancer_for_viewport = False

            # Add a neutrino using a particle emitter
            pset = add_granules(name="Neutrino")
            o.particle_systems[0].seed = random.randint(
                1,
                100)  # Random position of neutrino by using particle seeding

            # Override or add particle settings in the particle system
            pset.count = config.wave_centers
            pset.particle_size = 1
            pset.render_type = 'OBJECT'
            pset.instance_object = bpy.data.objects["Neutrino Shell"]
            pset.physics_type = 'NEWTON'

        # No motion scenario.  Static placement of neutrino.
        else:

            # A single neutrino will be displayed in the center of the spacetime array. Neutrino shell is set to be a collision object.
            bpy.ops.object.modifier_add(type='COLLISION')
            bpy.ops.rigidbody.object_add()
            o.rigid_body.type = 'PASSIVE'

            # Show a single neutrino - granule view
            if config.show_granules == True:

                # Show the standing wave, creating the particle.  Due to current inability of granule physics with the wave modifier, this needs to be simulated when the wave reaches the center.
                # TODO: This entire section should be replaced with a true standing wave that occurs naturally.
                bpy.ops.mesh.primitive_uv_sphere_add(radius=granule_size * 2,
                                                     enter_editmode=False,
                                                     location=(0, 0, 0))
                functions.link_collection(collection=neutrinos_collection)
                bpy.ops.object.shade_smooth()
                name = "Neutrino - Wave Center"
                bpy.context.active_object.name = name
                o = bpy.data.objects[name]
                functions.add_color(name=name,
                                    color=config.neutrino_color,
                                    transparent=True)

                # Add a wave center using a particle emitter.  It has force field properties and his hidden until the waves reach the center.
                pset = add_granules(name=name)
                pset.emit_from = 'FACE'
                pset.count = 14 * config.dimensions
                pset.physics_type = 'NEWTON'
                pset.force_field_1.type = 'FORCE'
                pset.force_field_1.strength = granule_force
                pset.force_field_1.flow = 1
                pset.force_field_1.use_max_distance = True
                pset.force_field_1.distance_max = granule_wavelength
                pset.use_self_effect = True
                pset.display_color = 'VELOCITY'
                functions.hide_at_keyframe(name=name,
                                           init_hide=True,
                                           start_frame=1,
                                           end_frame=frame_to_center)

            # Show a single neutrino - wave view
            else:
                # Show a standing wave pattern at the center in wave format instead of granule format to match the rest of the simulation.
                bpy.ops.mesh.primitive_plane_add(
                    size=granule_wavelength * 2,
                    enter_editmode=True,
                    location=(0, 0, -config.wave_amplitude / 2))
                functions.link_collection(collection=neutrinos_collection)
                name = "Neutrino - Standing Wave"
                bpy.context.active_object.name = name
                o = bpy.data.objects[name]
                bpy.ops.mesh.subdivide(number_cuts=50)
                bpy.ops.object.mode_set(mode="OBJECT")
                bpy.ops.object.shade_smooth()
                add_waves(mod_name=name)
                functions.hide_at_keyframe(name=name,
                                           init_hide=True,
                                           start_frame=1,
                                           end_frame=frame_to_center)

    # Scenarios for two or more neutrinos
    else:

        # For multiple neutrinos, don't show the calculations.
        config.show_calculations = False

        # For all scenarios where there are two or more neutrinos, position neutrinos at random points in spacetime and show wave interference patterns.
        neutrino = 1
        while neutrino <= config.wave_centers:

            name = "Neutrino " + str(neutrino)
            functions.add_neutrino(name=name,
                                   color=config.neutrino_color,
                                   radius=granule_wavelength)
            functions.link_collection(collection=neutrinos_collection)
            o = bpy.data.objects[name]
            random_location = create_random_location(antimatter=False)
            o.location = random_location
            neutrino += 1

        # Same as above for antineutrinos but with a difference in placement (odd nodes instead of even nodes) and using the antineutrino's color to differentiate.
        antineutrino = 1
        while antineutrino <= config.anti_wave_centers:

            name = "Antineutrino " + str(antineutrino)
            functions.add_neutrino(name=name,
                                   color=config.antineutrino_color,
                                   radius=granule_wavelength)
            functions.link_collection(collection=neutrinos_collection)
            o = bpy.data.objects[name]
            random_location = create_random_location(antimatter=True)
            o.location = random_location
            antineutrino += 1

        # If show granules, add the granules into spacetime to oscillate as waves from neutrinos
        if config.show_granules:
            s.show_instancer_for_viewport = False
            pset = add_granules(name="Spacetime")
            pset.count = granule_count * (2**config.dimensions)

    # This modifier helps to make the waves looks better (smoother) in the simulation
    m = s.modifiers.new("Smoother", type='CORRECTIVE_SMOOTH')

    #------------------------------------------------------------------------------------------------------
    # SHOW CALCULATIONS
    # If set to True, the calculations for the neutrino's energy and radius are shown
    #------------------------------------------------------------------------------------------------------

    if config.show_calculations and not config.show_neutrino_motion and config.external_force:

        # If it is a single neutrino, the calculated energy and radius from EWT equations are displayed
        calc_text = "Neutrino \n" + "Energy: " + str(round(
            calc_energy,
            3)) + " (eV)" + "\n" + "Radius: " + f"{calc_radius:.3e}" + " (m)"
        functions.add_text(name="Calculations",
                           text=calc_text,
                           location=(array_size + 2, -2, 0),
                           radius=2)

        # Display only after neutrino has formed
        functions.hide_at_keyframe(name="Calculations",
                                   init_hide=True,
                                   start_frame=1,
                                   end_frame=frame_to_center)
Esempio n. 2
0
def main(protons, neutrons, electrons, show_electron_cloud=False):

    #------------------------------------------------------------------------------------------------------
    # CONFIGS - USER VARIABLES SET IN THE UI
    # Variables from the Blender Panel
    #------------------------------------------------------------------------------------------------------

    config.protons = protons
    config.neutrons = neutrons
    config.electrons = electrons
    config.show_electron_cloud = show_electron_cloud

    #------------------------------------------------------------------------------------------------------
    # PHASE CONFIGURATION
    # Modifications specfic to this phase
    #------------------------------------------------------------------------------------------------------

    # Set everything to the electron for this phase to create protons (see phase 3)
    config.neutrinos = 10
    config.num_waves = 10

    # Scaling for visibility.  Electron core diameter is now 1m and a full electron is 10m. Proton radius should be around 1.54m at this size.
    phase_scale_factor = 1 / 40
    config.electron_core_radius = config.electron_core_radius * phase_scale_factor
    config.wavelength = config.electron_core_radius * 4

    # Nucleus
    proton_radius = config.wavelength * config.num_waves / 3.25       # Scale the proton radius based on electron size
    config.spin_frequency = 20                                        # Faster frequency to affect orbitals
    config.spin_strength = 1
    config.flow = 10

    # Orbital force exceptions for hydrogen and helium.
    if config.protons == 1:
        config.orbital_force = config.orbital_force * 0.15            # Hydrogen simulation uses an axial force and is simulated differently to illustrate probability
    if config.protons == 2:
        config.orbital_force = config.orbital_force * 0.75            # Beginning with helium, orbital force is simulated as spherical... helium needs a modification

    # Disable calculations when showing the electron cloud
    if config.show_electron_cloud:
        config.show_calculations = False


    ############################################ PROGRAM #####################################################

    text = config.project_text + ": Phase 4 - Atoms"
    functions.add_text(name=text, text=text, location=(-1000, 1000, 0), radius=50)
    bpy.data.objects[text].hide_set(True)


    #------------------------------------------------------------------------------------------------------
    # COLLECTIONS
    # Collections are used to organize objects in Blender.
    # The default collection and nucleus are set here.  Orbital collections are dynamic and set in the next section.
    #------------------------------------------------------------------------------------------------------

    context = bpy.context
    scene = context.scene
    nucleus_collection = bpy.data.collections.new('Nucleus')
    bpy.context.scene.collection.children.link(nucleus_collection)
    default_collection = context.scene.collection


    #------------------------------------------------------------------------------------------------------
    # CALCULATIONS OF ORBITALS AND ELECTRON EMITTER
    # Calculations used in this section for orbital distances are a ratio of the Bohr radius, requiring
    # solving simulataneous equations.  Mathcad was used, not Blender.  The table is here: https://energywavetheory.com/atoms/calculations-atoms/
    # Energy calculations are shown for the ionized electron. It uses amplitude factors from https://energywavetheory.com/atoms/calculations-amplitude-factors/
    # This emitter generates free electrons that will be subject to an attractive force and repelling force of the nucleus to remain in an orbital.
    # TODO: If Blender/Python can support simultaneous equation solvers, the equations can be imported here. Instead, they use a data file.
    # TODO: Nucleus forces on electrons in orbitals is placed here as temporary workaround until nucleus forces are completed.
    # TODO: Each electron needs to affect other electrons to determine placement of electrons.  Distances should be nearly accurate, but not placement.
    #------------------------------------------------------------------------------------------------------

    # Check to make sure the current atom array in the data file supports the atomic configuration (num of protons)
    if config.protons >= len(data.atoms):
        # reset protons to hydrogen if not supported by simulation
        config.protons = 1

    # Determine what type of atom (ionized, neutral or not a suported atom)
    if config.electrons == config.protons:           # Neutral atom
        atom_name = data.atoms[config.protons]
        orbital_ratio = data.neutral_atom
        amplitude_ratio = data.amp_neutral
    elif config.electrons < config.protons:          # Ionized atom
        ions = config.protons - config.electrons
        atom_name = data.atoms[config.protons] + str(ions) + "+"
        orbital_ratio, amplitude_ratio, atom_name = data.get_orbital_array(config.electrons, atom_name)
    else:                                            # Unsupported atom (there are more electrons than protons)
        orbital_ratio = data.neutral_atom
        amplitude_ratio = data.amp_neutral
        atom_name = "Atom not supported"

    # Loop through each orbital, calculating the distance and adding an electron emitter.
    orbital_text = ""
    energy_text = ""
    e = config.electrons

    i=1
    while i <= len(orbital_ratio):
        orbital = orbital_ratio[i-1][config.protons] * config.hydrogen_radius       # The calculated distance scaled for the simulation
        orbital_calc = orbital_ratio[i-1][config.protons] * config.bohr_radius      # The calculated orbital distance relative to the Bohr radius
        orbital_name = orbital_ratio[i-1][0]
        orbital_text = orbital_name + ": " + f"{orbital_calc:.2e}" + " (m)"
        energy_constants = (1/2) * config.coulomb_constant * config.elementary_charge ** 2      # Orbital energy is based on Coulomb's law - these are constant applied to next line.
        if orbital_calc != 0:
            energy_calc = energy_constants * amplitude_ratio[i-1][config.protons] / orbital_calc    # Energy constants multiplied by constructive wave interference / divide radius
            if energy_calc != 0:
                energy_text = ";  E: ~" + f"{energy_calc:.2e}" + " (J)"

        # Ensure that the right number of electrons are emitted for each shell following the electron sequence for shells: s, p, d, f
        shell_max = data.electron_sequence[i-1]
        if e > shell_max:
            valence_count = electron_count = shell_max
            e = e - shell_max
        elif e == shell_max:
            valence_count = electron_count = shell_max
            e = e - shell_max
            orbital_text = orbital_text + energy_text                                # Only show energy of orbital electron that is ionized
        else:
            valence_count = electron_count = e
            orbital_text = orbital_text + energy_text                                # Only show energy of orbital electron that is ionized
            e = 0

        # Orbitals - Show the calculation and generate electrons at each orbital
        if orbital != 0:

            # Add a collection for the orbital to organize
            orbital_collection = bpy.data.collections.new('Orbital - ' + orbital_name)
            bpy.context.scene.collection.children.link(orbital_collection)

            # Add a visual circle displaying the orbital if show_calculations is True
            if config.show_calculations:
                bpy.ops.mesh.primitive_circle_add(radius=orbital, enter_editmode=False, location=(0, 0, 0))
                bpy.context.active_object.name = orbital_name + " - Orbital"
                functions.link_collection(collection=orbital_collection)
                functions.add_text(name=orbital_name, text=orbital_text, location=(orbital, i*5, 0), radius=5 )
                functions.link_collection(collection=orbital_collection)

            # If displaying an electron cloud, disable electrons affecting themselves and set count higher to simulate the electron's probable positions.
            if config.show_electron_cloud:
                self_effect = False
                electron_count = config.num_frames / 10 * (int(orbital_name[:1])**2)        # Start with a smaller number of electrons near the core and increase with each shell for visibilty
                if config.protons == 1:                                                     # ...except hydrogen
                    electron_count = config.num_frames
            else:
                self_effect = True

            # Add the electron particle emitter at each orbital
            pset = functions.add_emitter(name=orbital_name + " - Emitter",
                particle_type = "electron",
                color = config.electron_color,
                radius = orbital,
                count = electron_count,
                self_effect = True,
                core_only = False)
            pset.force_field_1.flow = config.flow
            pset.emit_from = 'FACE'
            pset.use_emit_random = False

            # Rotate the p emitter. TODO: electrons from s shell should repel electrons in p shell instead of manual rotation; see note about use of effector groups
            if (orbital_name == "2p" or orbital_name == "3p"):
                o = bpy.data.objects[orbital_name + " - Emitter"]
                o.rotation_euler[2] = config.pi /2

            # If showing an electron cloud, ensure that the electrons in the same orbitals do not effect each other
            if config.show_electron_cloud:
                pset.use_self_effect = False
            functions.link_collection(collection=orbital_collection)

            # Atoms greater than hydrogen use collection effector groups to assist with isolating forces.
            if config.protons > 1:
                pset.effector_weights.collection = bpy.data.collections["Orbital - " + orbital_name]    # Create a collection for each shell.

                # Add the attractive force.  Each orbital is assigned a different effector group as a workaround. TODO: This section and next should be replaced when nucleus structure is completed.
                bpy.ops.object.effector_add(type='CHARGE', enter_editmode=False, location=(0, 0, 0))
                bpy.context.active_object.name = orbital_name + " - Force - Attractive"
                o = bpy.data.objects[orbital_name + " - Force - Attractive"]
                o.field.strength = config.protons * config.electron_charge        # The attractive force of protons in the nucleus. Number of protons times proton charge.
                o.field.falloff_power = 2                                         # This attractive electric force reduces at square of distance.
                functions.link_collection(collection=orbital_collection)

                # Add the repulsive force.  This is added here because protons (and spin) need to align for quantum jumps, which is under construction.  See: https://energywavetheory.com/atoms/quantum-leaps/
                repelling_force = config.orbital_force * orbital_ratio[i-1][config.protons] * config.protons       # Uses the data file for repelling force since already calculated.
                bpy.ops.object.effector_add(type='CHARGE', enter_editmode=False, location=(0, 0, 0))
                bpy.context.active_object.name = orbital_name + " - Force - Repelling"
                o = bpy.data.objects[orbital_name + " - Force - Repelling"]
                o.field.strength = -repelling_force    # TODO: this is simulated and needs to be occur naturally with proton alignment in nucleus
                o.field.falloff_power = 3              # The repelling force is an inverse cube decreasing force
                functions.link_collection(collection=orbital_collection)

                # Add the repulsive spin alignment causing electrons to jump at alignment. P orbital.  See: https://energywavetheory.com/atoms/orbital-shapes/.  TODO, this needs to be replaced when nucleus forms automatically.
                if (orbital_name == "2p" or orbital_name == "3p") and config.show_electron_cloud:
                    j = 1
                    if e == 0:
                        num_forces = math.ceil(valence_count/2)                                                # Valence electron shell.  Create an axial force for valence electrons up to 3 depending on valence count.
                    else:
                        num_forces = 3                                                                          # Defaults to three for p subshell if not valence electron shell
                    while j <= num_forces:                                                                      # Create axial forces when protons align to push electrons further
                        bpy.ops.object.effector_add(type='CHARGE', enter_editmode=False, location=(0, 0, 0))
                        bpy.context.active_object.name = orbital_name + " - Force - Axial - " + str(j)
                        o = bpy.data.objects[orbital_name + " - Force - Axial - " + str(j)]
                        o.rotation_euler[j-1] = config.pi / 2                                  # Rotate for the x, y and z planes
                        o.field.shape = 'LINE'
                        o.field.strength = 1
                        o.field.falloff_power = 3
                        o.field.falloff_type = 'TUBE'
                        functions.link_collection(collection=orbital_collection)
                        j += 1
        i += 1


    #------------------------------------------------------------------------------------------------------
    # NUCLEUS - PROTONS AND NEUTRONS
    # Adds protons and neutrons (nucleons) to the core of the atom
    # The emitter generates protons and neutrons in the nucleus for efficiency for atoms greater than hydrogen
    # If spin is set to True, the nucleus will spin using a Vortex force.
    # TODO: Nucleons require placement at standing wave nodes. Similar issue to particles that needs to be
    # TODO: solved in Blender for the formation of the atomic nucleus at the right sequence.
    # TODO: Should inherit from changes in Phase 2 here.  Then, spin should align and create orbital shapes.
    # TODO: The nucleus uses the LennardJones force in Blender which is a good approximation of the nuclear
    # TODO: force but it does not arrange at standing wave nodes. When Phase 2 is corrected it should appear here too.
    # TODO: This spin is not the true spin of the proton and should be changed once the real method is known.
    # TODO: Refer to https://energywavetheory.com/atoms/orbital-shapes/ for more information.
    #------------------------------------------------------------------------------------------------------

    # Nucleus
    functions.add_text(name=str(atom_name), text=str(atom_name), location=(config.protons/10 + 10, -5, 0), radius=5 )
    functions.link_collection(collection=nucleus_collection)

    # Hydrogen displays a detailed proton with quarks to illustrate repulsion and probability. Add a complete proton.
    if config.protons == 1:
        functions.add_nucleon(name = "Proton",            # A complete proton with quarks is shown for hydrogen, but just a halo for simplicity of simulation for all other atoms
            vertex_color = config.electron_color,
            center_color = config.positron_color,
            spin_up = True)
        o = bpy.data.objects["Proton"]
        o.location = (0,0,0)             # Hydrogen - center the proton

    # Reserved for future use.  Add a complete neutron by uncommenting the line below and placing it similar to above.  TODO: Scaling issue with center electron - needs to be core radius.
    # functions.add_nucleon(name = "Neutron", vertex_color = config.electron_color, center_color = config.positron_color, spin_up = True, neutron = True)

    # Atoms from helium and larger use an emitter for efficiency. These protons and neutrons do not show the internal quarks.
    if config.protons > 1:

        # Proton emitter
        pset = functions.add_emitter(name="Emitter - Proton",
            particle_type = "proton",
            color = config.proton_color,
            radius = config.protons/10 + 10,
            count = config.protons,
            self_effect = True,
            scale_factor = 1)
        pset.effector_weights.charge = 0   # TODO: positron not held in place by strong forces, so it can be affected in Blender by the electron.
        pset.particle_size = proton_radius
        pset.display_size = proton_radius
        functions.link_collection(collection=nucleus_collection)

        # Neutron emitter
        pset = functions.add_emitter(name="Emitter - Neutron",
            particle_type = "neutron",
            color = config.neutron_color,
            radius = config.neutrons/10 + 10,
            count = config.neutrons,
            self_effect = True,
            scale_factor = 1)
        pset.effector_weights.charge = 0   # TODO: positron not held in place by strong forces, so it can be affected in Blender by the electron.
        pset.particle_size = proton_radius
        pset.display_size = proton_radius
        functions.link_collection(collection=nucleus_collection)

        # Spin the entire nucleus of the atom (if set)
        if config.spin:
            functions.add_vortex(name="Axis", strength=config.spin_strength, frequency=config.spin_frequency)
            a = bpy.data.objects["Axis Spin Force"]
            b = bpy.data.objects["Axis"]
            nucleus_collection.objects.link(a)
            nucleus_collection.objects.link(b)
            default_collection.objects.unlink(a)
            default_collection.objects.unlink(b)
Esempio n. 3
0
def main(hydrogen_atoms):

    #------------------------------------------------------------------------------------------------------
    # CONFIGS - USER VARIABLES SET IN THE UI
    # Variables from the Blender Panel
    #------------------------------------------------------------------------------------------------------

    config.hydrogen_atoms = hydrogen_atoms


    #------------------------------------------------------------------------------------------------------
    # PHASE CONFIGURATION
    # Modifications specfic to this phase
    #------------------------------------------------------------------------------------------------------

    config.ext_force_radius = 1000 + (100 * config.hydrogen_atoms / 10)   # Making it dynamic based on the number of hydrogen atoms.  Should be sufficiently large to get a number of molecules in view.
    config.emitter_radius = config.hydrogen_radius * 3    # Need sufficient space between atoms to combine to molecules
    config.show_forces = False     # Disable showing proton forces as the focus is molecules
    config.flow = 10               # Slow down the effect of electrons finding electron holes
    phase_scale_factor = 1/40      # Scale down size of particles for this phase

    # Simulates an explosion of particles depending on energy as atoms convert back to fundamental components
    explosion = False
    ext_force_strength_threshold = 100000                               # The threshold in the external force strength value before it becomes an explosion
    repulsive_force_strength = 0                                        # Unless the force is enough for an explosion, there will be no repulsive force (explosion)
    if config.ext_force_strength >= ext_force_strength_threshold:
        config.ext_force_startframe = 25                                # The external force start frame is overriden.  Enough time to allow atoms to be seen before moving to center.
        config.ext_force_endframe = config.ext_force_startframe + 25    # Atoms are held in the center for +X frames before being repelled (exploded)
        attractive_force_strength = 100000                              # Attractive strength does not use ext_force_strength for explosion because then particles can't be seen at the center.
        repulsive_force_strength = 10000                                # Force strength should be small enough to view particles being emitted from center
        explosion = True
    else:
        attractive_force_strength = config.ext_force_strength


    ############################################ PROGRAM #####################################################

    text = config.project_text + ": Phase 5 - Molecules"
    functions.add_text(name=text, text=text, location=(-1000, 1000, 0), radius=50)
    bpy.data.objects[text].hide_set(True)


    #------------------------------------------------------------------------------------------------------
    # CALCULATIONS
    # Calculations used in this phase use EWT equations and explanations - refer to www.energywavetheory.com.
    #------------------------------------------------------------------------------------------------------

    # Based on the number of hydrogen atoms in the configuration, it consists of the following smaller particles. TODO: Other atoms need to be added other than hydrogen.
    electrons = config.hydrogen_atoms * 5       # Based on 4 electrons in vertices of proton and one electron in orbital - https://energywavetheory.com/explanations/whats-in-a-proton/
    positrons = config.hydrogen_atoms           # Based on a positron in the center of the proton - https://energywavetheory.com/explanations/whats-in-a-proton/
    neutrinos = config.hydrogen_atoms * 60      # Based on the above number of electrons and positrons (6 total) and 10 neutrinos per electron. https://energywavetheory.com/subatomic-particles/electron/
    helium_atoms = math.floor(config.hydrogen_atoms / 4)    # Based on two H atoms creating neutrons; result is 4 nucleons and 2 electrons - https://energywavetheory.com/subatomic-particles/neutron/


    #------------------------------------------------------------------------------------------------------
    # ATOMS
    # Add an atom object - to be used by atom emitter.
    # TODO: Only hydrogen (H) currently supported. Future atoms need to be supported.
    #------------------------------------------------------------------------------------------------------

    # Currently supporting hydrogen (H) atoms, building molecular hydrogen (H2).
    atom = "H"
    molecule = "H2"

    # The default text that appears if show_calculations is set (text overriden in explosion scenario).  This is based on H to H2 conversion. TODO: Other atoms need to be added.
    calc_text = "Natural Forces" + "\n\n" + "Begin: " + str(config.hydrogen_atoms) + " Hydrogen Atoms"  + "\n" + "End: "
    calc_text = calc_text + str(math.floor(config.hydrogen_atoms/2)) + " H2 Molecules and " + str(config.hydrogen_atoms % 2) + " H Atoms"

    # Add a hydrogen atom and move it from view (it will be used by the emitter).
    functions.add_atom(name=atom, color=config.hydrogen_color, atom_type=atom, scale_factor=phase_scale_factor)
    o = bpy.data.objects[atom]


    #------------------------------------------------------------------------------------------------------
    # ATOM EMITTER
    # Add a number of atoms based on the configuration settings.
    # TODO: This emitter uses two different forces not charges to keep atoms at distance.  This is incorrect.
    # TODO: This emitter also uses collection groups to keep molecules separated, which is also incorrect.
    # TODO: Both issues above likely require the same fix - Blender changes to create electron holes such that only one electron can fill the hole.
    #------------------------------------------------------------------------------------------------------

    i = 1

    # This emitter is specific to molecular hydrogen.  Take the number of hydrogen atoms, divide by 2 to create molecule collections. Odd numbers will be handled by one emitter.
    while i <= math.ceil(config.hydrogen_atoms / 2):

        # Add a collection for each molecule to organize
        collection_name = 'Molecule - ' + molecule + ' - ' + str(i)
        molecule_collection = bpy.data.collections.new(collection_name)
        bpy.context.scene.collection.children.link(molecule_collection)

        # Generate a random location for the emitter
        range = config.ext_force_radius-1
        if i == 1:                         # Special case for the first molecule which is placed at the center, then others are randomly placed.
            random_location = (0,0,0)
        else:
            random_location = (random.randint(-range,range),random.randint(-range,range),random.randint(-range,range))

        if i != 1:                         # Special case for first hydrogen atom which is already created at the center. If so, ignore creating an emitter.
            # Add first emitter with attractive atom. TODO: There should only be one emitter and no collection groups required. TODO: This is only hydrogen.
            pset = functions.add_emitter(name="Emitter - Atom 1" + ' - ' + str(i), color = config.hydrogen_color, object_name=atom, radius = config.emitter_radius, count = 1)
            pset.particle_size = 1
            pset.force_field_1.type = 'FORCE'
            pset.force_field_1.strength = -config.particle_force
            pset.force_field_1.flow = config.flow
            pset.damping = 1
            pset.effector_weights.collection = bpy.data.collections[collection_name]
            o = bpy.data.objects["Emitter - Atom 1" + ' - ' + str(i)]
            o.particle_systems[0].seed = random.randint(1,100)
            o.location = random_location
            functions.link_collection(collection=molecule_collection)

        # Add second emitter with repulsive atoms to keep the atoms separated at distance, sharing electrons. Only if even number of atoms.
        if not ((i == math.ceil(config.hydrogen_atoms / 2)) and ((config.hydrogen_atoms % 2) == 1)):
            pset = functions.add_emitter(name="Emitter - Atom 2" + ' - ' + str(i), color = config.hydrogen_color, object_name=atom, radius = config.emitter_radius, count = 1)
            pset.particle_size = 1
            pset.force_field_1.type = 'FORCE'
            pset.force_field_1.strength = config.particle_force * 10
            pset.force_field_1.flow = config.flow
            pset.damping = 1
            pset.effector_weights.collection = bpy.data.collections[collection_name]
            pset.force_field_1.use_max_distance = True
            pset.force_field_1.distance_max = config.hydrogen_radius * 2.75    # This value likely needs to be dynamic for atoms beyond hydrogen.
            o = bpy.data.objects["Emitter - Atom 2" + ' - ' + str(i)]
            o.particle_systems[0].seed = random.randint(1,100)
            o.location = random_location
            functions.link_collection(collection=molecule_collection)

        # An explosive force is used instead of external force, which accomplishes the same thing but then reverses force.  TODO: When effector groups removed, this should be moved out of collections.
        if config.external_force:
            functions.add_explosive_force(name="External Force" + ' - ' + str(i),
                attractive_strength=attractive_force_strength,
                repulsive_strength=repulsive_force_strength,
                startframe=config.ext_force_startframe,
                endframe=config.ext_force_endframe)
            functions.link_collection(collection=molecule_collection)
        i += 1


    #------------------------------------------------------------------------------------------------------
    # EXPLOSION
    # If set to True, an explosion is created that breaks atoms into their components depending on external force strength
    # This conversion of atoms to smaller particles is animated and does not use real physics.
    # It simulates the energy in stars for the nuclear process to create higher order atoms (only helium currently supported). TODO: support beyond He fusion.
    # It simulates the energy in particle accelerators to strip atoms to nucleons and begin the separation process for nucleon decay.
    # It simulates the energy of supernovae that breaks down matter and emits most of its energy as neutrinos.  Each is based on increasing energy levels.
    # TODO: When previous phases are completed this section should be rewritten to use the real logic of particle and atom creation and decay and not be animated.
    #------------------------------------------------------------------------------------------------------

    if explosion:

        # The initial atoms created above will be hidden after the explosion.  Set the keyframes using animation.
        for o in bpy.data.objects:
            functions.hide_at_keyframe(name = o.name, init_hide=False, start_frame=1, end_frame=config.ext_force_endframe)

        # Add the explosive force in the main collection to affect the particles that are formed in the process
        functions.add_explosive_force(name="External Force",
            attractive_strength=attractive_force_strength,
            repulsive_strength=repulsive_force_strength,
            startframe=config.ext_force_startframe,
            endframe=config.ext_force_endframe)

        # NUCLEAR. With a large force, the nuclei of atoms merge together to form new atomic elements.  In stars, helium is created in abudance from hydrogen. TODO: Add more atoms.
        if config.ext_force_strength >= ext_force_strength_threshold and config.ext_force_strength < ext_force_strength_threshold*10:
            pset = functions.add_emitter(name="Helium Emitter", color = config.helium_color, radius=10, count=helium_atoms, scale_factor=150)
            pset.mass = 500  # Making helium heavier to slow it down relative to other particles when being emitted
            functions.hide_at_keyframe(name="Helium Emitter", init_hide=True, start_frame=1, end_frame=config.ext_force_endframe)
            pset = functions.add_emitter(name="Hydrogen Emitter", color = config.hydrogen_color, radius=10, count=config.hydrogen_atoms % 4, scale_factor=100) # Remainder is H atoms.
            pset.mass = 200
            functions.hide_at_keyframe(name="Hydrogen Emitter", init_hide=True, start_frame=1, end_frame=config.ext_force_endframe)
            calc_text = "Nuclear Fusion" + "\n\n" + "Begin: " + str(config.hydrogen_atoms) + " Hydrogen Atoms"  + "\n" + "End: " + str(helium_atoms) + " Helium Atoms and " + str(config.hydrogen_atoms % 4) + " Hydrogen Atoms"

        # ACCELERATORS. With a very large force, atomic nuclei separate and protons begin to separate to quarks. With sufficient energy in the future, these quarks should be separated to electrons/positrons.
        elif config.ext_force_strength >= ext_force_strength_threshold*10 and config.ext_force_strength < ext_force_strength_threshold*100:
            pset = functions.add_emitter(name="Electron Emitter", color = config.electron_color, radius=10, count=electrons, scale_factor=40)
            functions.hide_at_keyframe(name="Electron Emitter", init_hide=True, start_frame=1, end_frame=config.ext_force_endframe)
            pset = functions.add_emitter(name="Positron Emitter", color = config.positron_color, radius=10, count=positrons, scale_factor=40)
            o = bpy.data.objects["Positron Emitter"]
            o.particle_systems[0].seed = random.randint(1,100)  # Make the seeding different than electrons so they follow a different path
            functions.hide_at_keyframe(name = "Positron Emitter", init_hide=True, start_frame=1, end_frame=config.ext_force_endframe)
            calc_text = "Accelerator Explosion" + "\n\n" + "Begin: " + str(config.hydrogen_atoms) + " Hydrogen Atoms"  + "\n" + "End: " + str(electrons) + " Electrons and " + str(positrons) + " Positrons"

        # SUPERNOVA. With a very, very large force, all atoms break down to the fundamental particle (neutrinos).  99% of energy emitted from supernovas are neutrinos.
        elif config.ext_force_strength >= ext_force_strength_threshold*100:
            pset = functions.add_emitter(name="Neutrino Emitter", color = config.neutrino_color, radius = 10, scale_factor=10, count = neutrinos)
            functions.hide_at_keyframe(name="Neutrino Emitter", init_hide=True, start_frame=1, end_frame=config.ext_force_endframe)
            calc_text = "Supernova Explosion" + "\n\n" + "Begin: " + str(config.hydrogen_atoms) + " Hydrogen Atoms"  + "\n" + "End: " + str(neutrinos) + " Neutrinos"


    #------------------------------------------------------------------------------------------------------
    # SHOW CALCULATIONS
    # If set to True, the calculations are shown
    #------------------------------------------------------------------------------------------------------

    if config.show_calculations:

        # Display the beginning and ending atom and particle counts
        functions.add_text(name="Molecule Count", text=calc_text, location=(300, -300, 0), radius=50)

        # Display during the duration of the external force so that it is apparent when it is turned off.
        functions.add_text(name="External Force Indicator", text="External Force: ON", location=(300, -100, 0), radius=50)
        functions.hide_at_keyframe(name = "External Force Indicator", init_hide=False, start_frame=1, end_frame=config.ext_force_endframe)
Esempio n. 4
0
def main(neutrinos):

    #------------------------------------------------------------------------------------------------------
    # CONFIGS - USER VARIABLES SET IN THE UI
    # Variables from the Blender Panel
    #------------------------------------------------------------------------------------------------------

    config.neutrinos = neutrinos

    #------------------------------------------------------------------------------------------------------
    # PHASE CONFIGURATION
    # Modifications specfic to this phase.  Must be set after user variables because of dependency.
    #------------------------------------------------------------------------------------------------------

    # WAVES - MODIFIED WITH WAVE CENTER CHANGES
    config.num_waves = config.neutrinos
    config.particle_display_radius = config.neutrino_core_radius * config.neutrinos
    config.particle_charge = config.neutrino_charge * config.neutrinos
    config.core_strength = config.particle_charge * 2
    particle_core_wavelength = config.neutrinos * config.neutrino_wavelength

    # STANDING WAVE GRID CONFIGURATION - MODIFIED WITH WAVE CENTER CHANGES
    config.grid_spacing = particle_core_wavelength / 2
    config.grid_strength = config.neutrinos * 2
    config.grid_size = int((-(-(config.neutrinos * 2)**(1 / 3) // 1)))

    # EXTERNAL FORCE
    if not config.external_force:  # Disable spin and calculating particles if no external force exists to push particles together
        config.spin = False
        config.show_calculations = False

    # VISIBILITY
    config.flow = 7  # Slow down flow for visibility
    config.emitter_radius = config.ext_force_radius / 2
    config.particle_force = config.particle_charge * 5  # TODO: Standing waves should form naturally and not need this force

    ############################################ PROGRAM #####################################################

    text = config.project_text + ": Phase 2 - Particles"
    functions.add_text(name=text,
                       text=text,
                       location=(-1000, 1000, 0),
                       radius=50)
    bpy.data.objects[text].hide_set(True)

    #------------------------------------------------------------------------------------------------------
    # COLLECTIONS
    # Collections are used to organize the objects in Blender in categories to turn on/off visibility
    #------------------------------------------------------------------------------------------------------

    context = bpy.context
    scene = context.scene
    wavelength_collection = bpy.data.collections.new('Wavelength')
    bpy.context.scene.collection.children.link(wavelength_collection)
    standingwaves_collection = bpy.data.collections.new('Standing Waves')
    bpy.context.scene.collection.children.link(standingwaves_collection)
    nodes_collection = bpy.data.collections.new('Nodes')
    bpy.context.scene.collection.children.link(nodes_collection)
    default_collection = context.scene.collection

    #------------------------------------------------------------------------------------------------------
    # STANDING WAVE NODES
    # Standing wave nodes are the point of no displacement and a stable position for waves to converge.
    # This phenomenon should be proven in the previous phase and duplicated here in this phase.
    # TODO: Due to Blender's physics engine limitations, nodes are created manually.
    # TODO: In the future, the standing wave nodes should form naturally from reflections off wave centers.
    #------------------------------------------------------------------------------------------------------

    x = 0  # Standing wave node grid starting point - positive forces
    y = 0
    z = 0
    nodeNum = 0
    while x < config.grid_size:
        while y < config.grid_size:
            while z < config.grid_size:
                bpy.ops.object.effector_add(type='CHARGE',
                                            enter_editmode=False,
                                            location=(x * config.grid_spacing,
                                                      y * config.grid_spacing,
                                                      z * config.grid_spacing))
                if ((x + y + z) % 2) == 0:
                    bpy.context.active_object.name = "Node - Positive (" + str(
                        nodeNum) + ")"
                    o = bpy.data.objects["Node - Positive (" + str(nodeNum) +
                                         ")"]
                    o.field.strength = config.grid_strength
                else:
                    bpy.context.active_object.name = "Node - Negative (" + str(
                        nodeNum) + ")"
                    o = bpy.data.objects["Node - Negative (" + str(nodeNum) +
                                         ")"]
                    o.field.strength = -config.grid_strength
                o.field.flow = config.flow
                o.field.falloff_power = 2
                functions.link_collection(collection=nodes_collection)
                nodeNum += 1
                z += 1
            z = 0
            y += 1
        y = 0
        x += 1

    # Move the standing wave node grid to the origin
    offset = -((config.grid_size - 1) / 2 * config.grid_spacing)
    bpy.ops.object.select_pattern(pattern="Node*")
    bpy.ops.transform.translate(value=(offset, offset, offset))

    # Add a plain axis to the center of the node grid and join the objects together
    bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0, 0, 0))
    bpy.context.active_object.name = "Node - Axis"
    p = bpy.context.active_object
    functions.link_collection(collection=nodes_collection)
    bpy.ops.object.select_all(action='DESELECT')
    bpy.ops.object.select_pattern(pattern="Node*")
    p.select_set(True)
    bpy.context.view_layer.objects.active = p
    bpy.ops.object.parent_set(type='OBJECT')

    #------------------------------------------------------------------------------------------------------
    # PARTICLE SPIN
    # If spin is set to True, the particles at the center of the simulation will spin by spinning the grid.
    # TODO: This spin is animated and should be replaced by true forces when standing waves form naturally.
    #------------------------------------------------------------------------------------------------------

    if config.spin:
        functions.spin_object(name="Node - Axis",
                              frequency=config.spin_frequency)

    if not config.show_forces:
        p.hide_set(True)

    #------------------------------------------------------------------------------------------------------
    # PARTICLE STANDING WAVES
    # In addition to standing wave nodes, a particle should form standing waves from in-waves and out-waves
    # until reaching a boundary (particle radius) that standing waves transition to traveling waves.
    # Standing waves are proportional to the number of wave centers as constructive interference: wavelength and number.
    # TODO: These waves are added manually as a limitation of Blender's physics. Should be automatic in future.
    #------------------------------------------------------------------------------------------------------

    i = 1
    sphere_strength = config.core_strength
    sphere_radius = particle_core_wavelength + (
        2 * particle_core_wavelength
    ) - (
        2 * i * config.neutrino_wavelength
    )  # Distance to wavelength. Standing wavelength decreases propotional to shell number
    sphere_midwave = particle_core_wavelength + (
        ((2 * particle_core_wavelength) -
         (2 * i * config.neutrino_wavelength)) / 2
    )  # Midpoint of wavelength for forces
    sphere_name = "Standing Wave Core"
    harmonic_name = "Standing Wave Harmonic Core"

    # Create spherical, standing waves at a given wavelength distance and number of wavelengths.  This becomes the particle volume.
    while i <= config.num_waves:
        if i > 1:
            sphere_strength = config.core_strength / (
                i**2)  #inverse square strength of wave
            sphere_radius = sphere_radius + (2 * particle_core_wavelength) - (
                2 * i * config.neutrino_wavelength)
            sphere_midwave = sphere_midwave + (
                ((2 * particle_core_wavelength) -
                 (2 * i * config.neutrino_wavelength)))
            sphere_name = "Standing Wave Sphere " + str(i)
            harmonic_name = "Standing Wave Harmonic " + str(i)
        bpy.ops.mesh.primitive_uv_sphere_add(radius=sphere_midwave,
                                             enter_editmode=False,
                                             location=(0, 0, 0))
        bpy.context.active_object.name = sphere_name
        b = bpy.data.objects[sphere_name]
        bpy.ops.object.effector_add(type='HARMONIC',
                                    enter_editmode=False,
                                    location=(0, 0, 0))
        bpy.context.active_object.name = harmonic_name
        a = bpy.data.objects[harmonic_name]
        a.field.strength = sphere_strength
        a.field.harmonic_damping = 0
        if not config.show_forces:
            a.hide_set(True)
        bpy.ops.object.select_all(action='DESELECT')
        a.select_set(True)
        b.select_set(True)
        bpy.context.view_layer.objects.active = b
        bpy.ops.object.parent_set(type='VERTEX', keep_transform=True)
        b.instance_type = 'VERTS'
        b.show_instancer_for_viewport = False
        b.show_instancer_for_render = False
        standingwaves_collection.objects.link(a)
        standingwaves_collection.objects.link(b)
        default_collection.objects.unlink(a)
        default_collection.objects.unlink(b)
        bpy.ops.mesh.primitive_circle_add(radius=sphere_radius,
                                          enter_editmode=False,
                                          location=(0, 0, 0))
        wavelength_name = "Wavelength " + str(i)
        bpy.context.active_object.name = wavelength_name
        functions.link_collection(collection=wavelength_collection)
        i += 1

    #------------------------------------------------------------------------------------------------------
    # PARTICLE SHELL
    # A particle shell is used to visualize a particle - as the volume of standing waves
    # The radius of the shell is the boundary of standing waves where they transition to traveling waves
    # This shell can be made transparent in some Blender views to see its underlying components
    #------------------------------------------------------------------------------------------------------

    bpy.ops.mesh.primitive_uv_sphere_add(radius=(config.num_waves *
                                                 particle_core_wavelength),
                                         enter_editmode=False,
                                         location=(0, 0, 0))
    bpy.context.active_object.name = "Particle Shell"
    o = bpy.data.objects["Particle Shell"]
    bpy.ops.object.shade_smooth()
    functions.add_color(name="Particle Shell",
                        color=config.electron_color,
                        transparent=True)
    o.hide_set(True)

    #------------------------------------------------------------------------------------------------------
    # NEUTRINO EMITTER
    # This emitter generates wave centers (neutrinos) that will be forced together to create particles.
    #------------------------------------------------------------------------------------------------------

    pset = functions.add_emitter(name="Emitter",
                                 color=config.neutrino_color,
                                 radius=config.emitter_radius,
                                 count=config.neutrinos,
                                 self_effect=True)
    pset.particle_size = config.particle_display_radius
    pset.display_size = config.particle_display_radius
    pset.force_field_1.type = 'CHARGE'
    pset.force_field_1.strength = -config.particle_charge
    pset.force_field_1.falloff_power = 2
    pset.force_field_1.flow = config.flow
    pset.force_field_2.type = 'FORCE'
    pset.force_field_2.strength = config.particle_force
    pset.force_field_2.use_max_distance = False
    o = bpy.data.objects["Emitter"]
    o.particle_systems[0].seed = random.randint(1, 100)

    #------------------------------------------------------------------------------------------------------
    # EXTERNAL FORCE
    # If set to True, an external force is applied to wave centers to force them together.
    # This force simulates the energy required to force wave centers to create particles.
    #------------------------------------------------------------------------------------------------------

    if config.external_force:
        functions.add_external_force(name="External Force",
                                     radius=config.ext_force_radius,
                                     location=(0, 0, 0),
                                     strength=config.ext_force_strength,
                                     startframe=config.ext_force_startframe,
                                     endframe=config.ext_force_endframe)

        # Hide everything except for the particle emitter
        for o in bpy.data.objects:
            if o.name != 'Emitter':
                o.hide_viewport = True
                o.keyframe_insert(data_path='hide_viewport', frame=0)

        # Unhide everything when the external force ends and standing waves form
        for o in bpy.data.objects:
            o.hide_viewport = False
            o.keyframe_insert(data_path='hide_viewport',
                              frame=config.ext_force_endframe)

    #------------------------------------------------------------------------------------------------------
    # SHOW CALCULATIONS
    # If set to True, the calculations of particle energy and radius are shown
    #------------------------------------------------------------------------------------------------------

    if config.show_calculations:

        # The simulation is a fundamental wavelength of 2 meters.  To scale, divide by 2.  Then scale by fundamental wavelength.  Proportional to number of wavelengths and wavelength.
        calc_radius = "Radius: " + f"{(config.fundamental_wavelength * config.num_waves * particle_core_wavelength / 2):.3e}" + " (m)"

        # The simulation doesn't automatically calc energy.  A fundamental energy value is used and the EWT equation from https://energywavetheory.com/subatomic-particles/equation/
        shell_multiplier = 0
        n = 1
        while n <= config.neutrinos:
            shell_multiplier = shell_multiplier + (n**3 - ((n - 1)**3)) / n**4
            n += 1
        calc_energy = "Energy: " + str(
            round(config.fundamental_energy *
                  (config.neutrinos**5) * shell_multiplier)) + " (eV)"

        # Calculation is the same, but formatting for eV vs MeV
        if config.neutrinos < 6:
            calc_energy = "Energy: " + str(
                round(
                    config.fundamental_energy *
                    (config.neutrinos**5) * shell_multiplier, 3)) + " (eV)"
        elif config.neutrinos < 40:
            calc_energy = "Energy: " + str(
                round(
                    config.fundamental_energy * (config.neutrinos**5) *
                    shell_multiplier / 1000000, 3)) + " (MeV)"
        else:
            calc_energy = "Energy: " + str(
                round(
                    config.fundamental_energy * (config.neutrinos**5) *
                    shell_multiplier / 1000000000, 3)) + " (GeV)"
        functions.add_text(
            name="Calculations",
            text=calc_energy + "\n" + calc_radius,
            location=(config.num_waves * particle_core_wavelength + 10, 20, 0),
            radius=10)

        # Display the K value for the particle.  K is a variable count of neutrinos at the core of a particle, analogous to Z as the variable count of protons at the core of an atom.
        if config.neutrinos == 1:
            particle_name_text = "Neutrino: " + "K=" + str(config.neutrinos)
        elif config.neutrinos == 10:
            particle_name_text = "Electron: " + "K=" + str(config.neutrinos)
        else:
            particle_name_text = "K=" + str(config.neutrinos)
        functions.add_text(
            name="Wave Center Count",
            text=particle_name_text,
            location=(config.num_waves * particle_core_wavelength + 10, 30, 0),
            radius=10)

        # Display during the duration of the external force so that it is apparent when it is turned off.
        functions.add_text(
            name="External Force Indicator",
            text="External Force: ON",
            location=(config.num_waves * particle_core_wavelength + 10, -30,
                      0),
            radius=10)
        functions.hide_at_keyframe(name="External Force Indicator",
                                   init_hide=False,
                                   start_frame=1,
                                   end_frame=config.ext_force_endframe)
Esempio n. 5
0
def main(electrons,
         positrons,
         particle_accelerator=False,
         accelerator_force=100):

    #------------------------------------------------------------------------------------------------------
    # CONFIGS - USER VARIABLES SET IN THE UI
    # Variables from the Blender Panel
    #------------------------------------------------------------------------------------------------------

    config.electrons = electrons
    config.positrons = positrons
    config.particle_accelerator = particle_accelerator
    config.accelerator_force = accelerator_force

    #------------------------------------------------------------------------------------------------------
    # PHASE CONFIGURATION
    # Modifications specfic to this phase
    #------------------------------------------------------------------------------------------------------

    # Set everything to the electron since it is the stable particle for composite particles.  Electron configs.
    config.neutrinos = 10
    config.num_waves = 10
    config.wavelength = 10 * config.neutrino_core_radius * 4

    # Emitter size
    config.emitter_radius = config.ext_force_radius

    # Disable spin and calculation of particles if no external force pushes particles together
    if not config.external_force:
        config.spin = False
        config.show_calculations = False

    # Shrink the emitter to immediately create a proton so an accelerator can target the proton with particles for collisions
    if config.particle_accelerator:
        config.external_force = True
        config.ext_force_endframe = 15
        config.emitter_radius = config.electron_core_radius * 4
        config.spin = False
        config.show_calculations = False

    # The particle shell color will be the proton's color unless it is a neutron
    shell_color = config.proton_color

    ############################################ PROGRAM #####################################################

    text = config.project_text + ": Phase 3 - Nucleons"
    functions.add_text(name=text,
                       text=text,
                       location=(-1000, 1000, 0),
                       radius=50)
    bpy.data.objects[text].hide_set(True)

    #------------------------------------------------------------------------------------------------------
    # CALCULATIONS
    # Calculations used in this phase using EWT equations - refer to www.energywavetheory.com.
    #------------------------------------------------------------------------------------------------------

    show_radius = False
    neutron = False

    calc_radius_simulation = (config.wavelength * 5) * (
        (3 / 8)**(1 / 2)
    )  # The simulation is a fundamental wavelength of 2 meters.  To scale, divide by 2.  Then scale by fundamental wavelength.  See https://energywavetheory.com/physics-constants/proton-radius
    calc_radius = calc_radius_simulation / 2 * config.fundamental_wavelength_no_gfactor
    calc_radius_text = "Radius: " + f"{calc_radius:.3e}" + " (m)"
    attractive_force = config.electron_energy * config.electron_radius  # Coulomb force of positron from Electric Force equation at https://energywavetheory.com/equations/classical-constants/
    attractive_force_text = "Attractive: " + f"{attractive_force:.3e}" + " (J*m) - decreasing at 1/r^2"
    repelling_force = config.electron_energy * (config.electron_radius**2) / (
        config.fine_structure**2
    )  # Repelling force from Orbital Force equation at https://energywavetheory.com/equations/classical-constants/
    repelling_force_text = "Repelling: " + f"{repelling_force:.3e}" + " (J*m^2) - decreasing at 1/r^3"

    # Determine the type of composite particle based on electron and positron count
    if ((config.electrons == 1) and (config.positrons == 1)):
        particle_type = "Meson"
    elif ((config.electrons == 3) and (config.positrons == 0)):
        particle_type = "Baryon"
    elif ((config.electrons == 4) and
          (config.positrons == 0)) or ((config.electrons == 2) and
                                       (config.positrons == 2)):
        particle_type = "Tetraquark"
    elif ((config.electrons == 4) and (config.positrons == 1)):
        particle_type = "Proton (pentaquark)" + "\n" + calc_radius_text + "\n" + attractive_force_text + "\n" + repelling_force_text
        show_radius = True
    elif ((config.electrons == 4) and (config.positrons == 1)):
        particle_type = "Proton (pentaquark)" + "\n" + calc_radius_text + "\n" + attractive_force_text + "\n" + repelling_force_text
        show_radius = True
    elif ((config.electrons == 5) and (config.positrons == 1)):
        particle_type = "Neutron" + "\n" + calc_radius_text
        show_radius = True
        neutron = True
        config.electrons = 4
    else:
        particle_type = ""

    #------------------------------------------------------------------------------------------------------
    # ELECTRON AND POSITRON OBJECTS
    # These particles are created automatically, assumed to have been proven in the previous phase
    # The electron and positron are hidden from view because they are used by the particle emitters.
    #------------------------------------------------------------------------------------------------------

    # Add electron object (it will be used at the verices of the proton - first wavelength is the core of the electron only)
    functions.add_electron(
        name="Electron",
        color=config.electron_color,
        scale_factor=1 / (config.electron_core_radius * 4) /
        2,  # Scaling factor to compensate for Blender Lennard Jones radius rule for strong force
        core_only=True
    )  # A standalone electron is standing waves, but as a composite particle its waves collapse to only one wavelength - core
    o = bpy.data.objects["Electron"]
    o.location = (1000, 1000, 1000)  # Move it out of view and hide it
    o.hide_set(True)

    # Add positron object (it will be used at the center of the proton - first wavelength core only)
    functions.add_electron(name="Positron",
                           color=config.positron_color,
                           scale_factor=1 / (config.electron_core_radius * 4),
                           core_only=True,
                           antimatter=True)
    o = bpy.data.objects["Positron"]
    o.location = (1000, 1000, 1000)  # Move it out of view and hide it
    o.hide_set(True)

    # Add a free electron object that will be attracted to the positron at the center of the proton (first wavelength core only)
    if neutron:
        functions.add_electron(name="Electron - Free",
                               color=config.electron_color,
                               scale_factor=1 /
                               (config.electron_core_radius * 4),
                               core_only=True)
        o = bpy.data.objects["Electron - Free"]
        o.location = (1000, 1000, 1000)  # Move it out of view and hide it
        o.hide_set(True)

    #------------------------------------------------------------------------------------------------------
    # ELECTRON PARTICLE EMITTER
    # This emitter generates electrons that will be subject to the strong force when forced to close ranges
    # It uses keyframe animation to switch from electric charge to the strong force (Lennard Jones)
    # TODO: The strong force should be a natural property within standing waves and not use animation.
    #------------------------------------------------------------------------------------------------------

    # Add the spherical emitter and link to the electron object as the particle being emitted
    pset = functions.add_emitter(name="Emitter - Electron",
                                 color=config.electron_color,
                                 radius=config.emitter_radius,
                                 count=config.electrons,
                                 self_effect=True,
                                 object_name="Electron")
    pset.particle_size = (
        config.electron_core_radius * 4
    ) * 2  # Blender Lennard Jones force needs to be x2 for particle separation
    pset.display_size = (config.electron_core_radius * 4)
    pset.force_field_1.flow = 0
    pset.force_field_2.type = 'FORCE'
    pset.force_field_2.flow = config.flow

    # Set the initial electron settings of charge property when at distance
    p = bpy.data.particles[-1]
    p.force_field_1.type = 'CHARGE'
    p.force_field_1.strength = config.electron_charge
    p.force_field_1.falloff_power = 2
    p.force_field_1.use_max_distance = False
    p.force_field_1.distance_max = 0
    p.force_field_2.strength = 0
    p.keyframe_insert(data_path='force_field_1.type', frame=1)
    p.keyframe_insert(data_path='force_field_1.strength', frame=1)
    p.keyframe_insert(data_path='force_field_1.falloff_power', frame=1)
    p.keyframe_insert(data_path='force_field_1.use_max_distance', frame=1)
    p.keyframe_insert(data_path='force_field_1.distance_max', frame=1)
    p.keyframe_insert(data_path='force_field_2.strength', frame=1)

    # Apply the strong force if an external force pushes electrons together to close range
    if config.external_force:
        p.force_field_1.type = 'LENNARDJ'  # Now set the values at the keyframe for the strong forces; turn on 10 frames before external force stops
        p.force_field_1.strength = config.particle_strong_force
        p.force_field_1.falloff_power = 0
        p.force_field_1.use_max_distance = True
        p.force_field_1.distance_max = config.electron_core_radius * 40  # The strong force max distance should be standing waves of electrons
        p.force_field_2.strength = -config.particle_force
        p.keyframe_insert(data_path='force_field_1.type',
                          frame=config.ext_force_endframe - 10)
        p.keyframe_insert(data_path='force_field_1.strength',
                          frame=config.ext_force_endframe - 10)
        p.keyframe_insert(data_path='force_field_1.falloff_power',
                          frame=config.ext_force_endframe - 10)
        p.keyframe_insert(data_path='force_field_1.use_max_distance',
                          frame=config.ext_force_endframe - 10)
        p.keyframe_insert(data_path='force_field_1.distance_max',
                          frame=config.ext_force_endframe - 10)
        p.keyframe_insert(data_path='force_field_2.strength',
                          frame=config.ext_force_endframe - 10)
        old_type = bpy.context.area.type  # Make the switch a constant on/off for the transition of forces. Blender default is gradual changes.
        bpy.context.area.type = 'GRAPH_EDITOR'
        bpy.ops.graph.interpolation_type(type='CONSTANT')
        bpy.context.area.type = old_type

    #------------------------------------------------------------------------------------------------------
    # POSITRON PARTICLE EMITTER
    # This emitter generates positrons that will be attracted by electrons.
    # Unlike the electron, this positron is not modeled for the strong force.
    #------------------------------------------------------------------------------------------------------

    pset = functions.add_emitter(name="Emitter - Positron",
                                 color=config.positron_color,
                                 radius=config.emitter_radius / 4,
                                 count=config.positrons,
                                 self_effect=True,
                                 object_name="Positron")
    pset.particle_size = (config.electron_core_radius * 4)
    pset.display_size = (config.electron_core_radius * 4)
    pset.effector_weights.lennardjones = 0  # The strong force should only apply to the electrons at vertices and not the positron in the middle held by weak forces
    pset.effector_weights.harmonic = 0  # Excluding the external (harmonic) force as it should only apply to push electrons to vertices, then positron attracted to center.
    pset.force_field_1.type = 'CHARGE'
    pset.force_field_1.strength = config.electron_charge
    pset.force_field_1.falloff_power = 2
    pset.force_field_1.flow = 0

    #------------------------------------------------------------------------------------------------------
    # FREE ELECTRON PARTICLE EMITTER
    # This emitter generates a free electron that will not be bound by the strong force. Only used for neutron.
    #------------------------------------------------------------------------------------------------------

    if neutron:
        pset = functions.add_emitter(name="Emitter - Electron - Free",
                                     color=config.electron_color,
                                     radius=config.emitter_radius / 2,
                                     count=config.positrons,
                                     self_effect=True,
                                     object_name="Electron - Free")
        pset.particle_size = (config.electron_core_radius * 4)
        pset.display_size = (config.electron_core_radius * 4)
        pset.effector_weights.lennardjones = 0  # The strong force should only apply to the electrons at vertices and not the positron in the middle held by weak forces
        pset.effector_weights.harmonic = 0  # Excluding the external (harmonic) force as it should only apply to push electrons to vertices, then positron attracted to center.
        pset.force_field_1.type = 'CHARGE'  # Standard electric charge force for the free electron.
        pset.force_field_1.strength = -config.electron_charge
        pset.force_field_1.falloff_power = 2
        pset.force_field_1.flow = 0
        shell_color = config.neutron_color

    #------------------------------------------------------------------------------------------------------
    # PROTON SPIN
    # If spin is set to True, the particles at the center of the simulation will spin using a Vortex force.
    # TODO: This spin is not the true spin of the proton and it should be automatic as particles move to nodes.
    #------------------------------------------------------------------------------------------------------

    if config.spin:
        functions.add_vortex(name="Axis",
                             strength=config.spin_strength,
                             frequency=config.spin_frequency)

    #------------------------------------------------------------------------------------------------------
    # PROTON SHELL
    # The proton is a composite particle consisting of smaller particles.
    # This "shell" can be shown to represent a proton object with transparency to view its parts
    #------------------------------------------------------------------------------------------------------

    bpy.ops.mesh.primitive_uv_sphere_add(radius=calc_radius_simulation,
                                         enter_editmode=False,
                                         location=(0, 0, 0))
    bpy.context.active_object.name = "Particle Shell"
    bpy.ops.object.shade_smooth()
    o = bpy.data.objects["Particle Shell"]
    o.hide_set(True)
    functions.add_color(name="Particle Shell",
                        color=shell_color,
                        transparent=True)

    #------------------------------------------------------------------------------------------------------
    # PARTICLE ACCELERATOR
    # If set to true, a particle accelerator is added to the simulation which shoots a particle at a target nucleon.
    # This simulates various forces colliding with composite particles (e.g. proton) that describe its
    # components with the strong and weak interactions. It can also be used to simulate beta decay.
    #------------------------------------------------------------------------------------------------------

    if config.particle_accelerator:

        # The speed is fixed for easier viewing, so this makes the particle display size appear larger as force increases to give it a visual
        if config.accelerator_force <= 1000:
            display_radius = (config.electron_core_radius * 4) * 0.5
        elif config.accelerator_force > 1000 and config.accelerator_force <= 10000:
            display_radius = (config.electron_core_radius * 4) * 1.5
        else:
            display_radius = (config.electron_core_radius * 4) * 2

    # Location and size of the particle accelerator
        x_location = -(config.electron_core_radius * 4) * 40
        x_depth = 500

        # Add the particle accelerator (as a cylinder shooting a particle towards the target composite particle)
        bpy.ops.mesh.primitive_cylinder_add(
            radius=config.electron_core_radius * 12,
            depth=x_depth,
            enter_editmode=False,
            location=(x_location, 0, 0))
        bpy.context.active_object.name = "Particle Accelerator"
        o = bpy.data.objects["Particle Accelerator"]
        o.rotation_euler[1] = 1.5708
        bpy.ops.object.shade_smooth()
        m = o.modifiers.new("Accelerator Particle System",
                            type='PARTICLE_SYSTEM')
        ps = m.particle_system
        pset = ps.settings
        pset.count = 1
        pset.frame_start = config.accelerator_startframe
        pset.frame_end = config.accelerator_startframe
        pset.emit_from = "VOLUME"
        pset.lifetime = config.num_frames
        pset.normal_factor = 0
        pset.particle_size = (config.electron_core_radius * 4)
        pset.display_size = display_radius
        pset.use_self_effect = False
        pset.effector_weights.all = 0  # Due to its speed, the colliding particle should not be affected by forces (it was slowed down to view in sim)
        pset.force_field_1.type = 'FORCE'
        pset.force_field_1.strength = config.accelerator_force
        pset.force_field_1.use_max_distance = True
        pset.force_field_1.distance_max = 40  # Do not affect proton until a diameter of the electron to make simulation easier to see
        pset.force_field_2.type = 'CHARGE'
        pset.force_field_2.strength = config.electron_charge
        pset.force_field_2.use_max_distance = True
        pset.force_field_2.distance_max = 40  # Do not affect proton until a diameter of the electron to make simulation easier to see
        pset.object_align_factor[
            2] = 200  # Speed of accelerated particle - 200 m/s is max that seems to be set in Blender for z-axis.
        functions.add_color(name="Particle Accelerator",
                            color=config.accelerator_color)
        functions.add_text(name="Particle Accelerator - Text",
                           text="Particle Accelerator",
                           location=(x_location - x_depth / 2, 100, 0),
                           radius=50)

    #------------------------------------------------------------------------------------------------------
    # EXTERNAL FORCE
    # If set to True, an external force is applied to particles (e.g. electrons) to force them together.
    # This force simulates the energy required to force particles to within standing waves for the strong force.
    # The force can be turned on and off, by setting the start and end frames that the force is applied.
    #------------------------------------------------------------------------------------------------------

    if config.external_force:
        functions.add_external_force(name="External Force",
                                     radius=config.ext_force_radius,
                                     location=(0, 0, 0),
                                     strength=config.ext_force_strength,
                                     startframe=config.ext_force_startframe,
                                     endframe=config.ext_force_endframe)

    #------------------------------------------------------------------------------------------------------
    # SHOW CALCULATIONS
    # If set to True, the calculations are shown for the proton's radius
    #------------------------------------------------------------------------------------------------------

    if config.show_calculations:

        # If it is a neutron, set correctly back to 5 electrons for the display
        if neutron:
            config.electrons = 5

        # Display the proton's radius
        if show_radius:
            bpy.ops.mesh.primitive_circle_add(radius=calc_radius_simulation,
                                              enter_editmode=False,
                                              location=(0, 0, 0))
            bpy.context.active_object.name = "Proton Radius"
        functions.add_text(name="Calculations",
                           text=str(particle_type),
                           location=(100, 100, 0),
                           radius=10)

        # Hide everything except for the particle emitter and particle
        for o in bpy.data.objects:
            if (o.name.find('Emitter') == -1) and o.name.find(
                    'Electron') == -1 and o.name.find('Positron') == -1:
                o.hide_viewport = True
                o.keyframe_insert(data_path='hide_viewport', frame=0)

        # Unhide everything when the external force ends + 50 frames
        for o in bpy.data.objects:
            o.hide_viewport = False
            o.keyframe_insert(data_path='hide_viewport',
                              frame=config.ext_force_endframe + 50)

        # Display the total count of electrons and positrons used in the composite particle
        text = "Electrons: " + str(
            config.electrons) + "\n" + "Positrons: " + str(config.positrons)
        functions.add_text(name="Particle Count",
                           text=text,
                           location=(-100, 100, 0),
                           radius=10)

        # Display during the duration of the external force so that it is apparent when it is turned off.
        functions.add_text(name="External Force Indicator",
                           text="External Force: ON",
                           location=(100, -100, 0),
                           radius=10)
        functions.hide_at_keyframe(name="External Force Indicator",
                                   init_hide=False,
                                   start_frame=1,
                                   end_frame=config.ext_force_endframe)