{"id":30684250,"url":"https://github.com/danieljvickers/particle-fortran","last_synced_at":"2025-10-11T15:31:57.001Z","repository":{"id":312150169,"uuid":"1046387316","full_name":"danieljvickers/particle-fortran","owner":"danieljvickers","description":null,"archived":false,"fork":false,"pushed_at":"2025-09-05T15:12:18.000Z","size":144,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-05T15:34:37.373Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Fortran","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danieljvickers.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-28T15:59:47.000Z","updated_at":"2025-08-29T22:16:21.000Z","dependencies_parsed_at":"2025-08-29T01:58:52.001Z","dependency_job_id":"efa292e9-6e9f-47cd-8c0c-c6a94bbe0e22","html_url":"https://github.com/danieljvickers/particle-fortran","commit_stats":null,"previous_names":["danieljvickers/particle-fortran"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/danieljvickers/particle-fortran","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danieljvickers%2Fparticle-fortran","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danieljvickers%2Fparticle-fortran/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danieljvickers%2Fparticle-fortran/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danieljvickers%2Fparticle-fortran/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danieljvickers","download_url":"https://codeload.github.com/danieljvickers/particle-fortran/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danieljvickers%2Fparticle-fortran/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279007604,"owners_count":26084334,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-11T02:00:06.511Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-01T20:15:19.451Z","updated_at":"2025-10-11T15:31:56.988Z","avatar_url":"https://github.com/danieljvickers.png","language":"Fortran","funding_links":[],"categories":[],"sub_categories":[],"readme":"# particle-fortran\n\n## Introduction\n\nWelcome to my particle simulation project. This project is a contiuation of a couple of my previous projects: one a C++ particle simulation I wrote in graduate school, and the other a CUDA fluid simulation. I found that I wanted to explore better paralelization than what was achived in my CUDA fluid simulation project. To modernize and improve parallelization, I have decided to swap to a more-portable compute library with OpenMP and to write multi-device code using MPI. I also wanted to update my previous particle simulation with better output rendering, which I will write in python. The result will be a simulated dust cloud of particles orbiting the sun at a radius between Venus and Earth. The simulation is an N-body problem of asteroid-sized particles which will be allowed to collide with each other via perfectly inelasitic collisions in order to form larger object.\n\nThe primary learning objectives of this project are in working with Fortran, OpenMP, and MPI to yeild parallel code. I will then gather some performance benchmarks on CPU, single-GPU, and multi-GPU implementations.\n\n## A CPU particle Simulation\n\n### Code\n\nTo begin, I want to write a simulation that runs purely in the CPU. I learned my lesson about how to lay out memory, and we definately want to favor using a struct of arrays instead of an array of structs. Here is the data type that we will use for the particles:\n\n```fortran\ntype :: particle_t\n    ! Positions\n    real(8), allocatable :: x(:)\n    real(8), allocatable :: y(:)\n\n    ! Momentums\n    real(8), allocatable :: px(:)\n    real(8), allocatable :: py(:)\n\n    ! Accelerations\n    real(8), allocatable :: ax(:)\n    real(8), allocatable :: ay(:)\n\n    ! Other variables\n    real(8), allocatable :: m(:)  ! mass\n    real(8), allocatable :: r(:)  ! radius, computed from mass\\\n    logical, allocatable :: merged(:)\nend type particle_t\n```\n\nI then wanted to randomly distribute the particles. The only note is that we are generating particles in an anulus. If we select a radius at random, then we will get a higher distribution of particles closer to the central star. To prevent this, we can either generate particles in cartesian coordinates and then check the radius bounds or normalize based on the amount of area. I chose to normalize by area. This ultimately results in the least amount of calculations to perform, but it ultimately negligible in terms of compute time:\n\n```fortran\nsubroutine initialize_particles()\n    integer :: i\n    real(8) :: orbital_radius, start_angle, orbital_momentum  ! placeholders for random number generator\n\n    ! Allocate arrays\n    allocate(particles%x(num_particles), particles%y(num_particles))  ! positions\n    allocate(particles%px(num_particles), particles%py(num_particles))  ! momentums\n    allocate(particles%ax(num_particles), particles%ay(num_particles))  ! accelerations\n    allocate(particles%m(num_particles), particles%r(num_particles), particles%merged(num_particles))  ! other\n\n    call random_seed()\n    do i = 1, num_particles\n        ! randomly distribute the positions of the particles in the allowed anulus\n        call get_random_number(orbital_radius, radius_lower**2, radius_upper**2)  ! temp radius\n        orbital_radius = sqrt(orbital_radius)\n        call get_random_number(start_angle, dble(0.0), 2.0*C_PI)  ! temp angle\n        particles%x(i) = orbital_radius * cos(start_angle)\n        particles%y(i) = orbital_radius * sin(start_angle)\n\n        call get_random_number(particles%m(i), mass_lower, mass_upper)  ! evenly distribute the mass\n        particles%r(i) = (particles%m(i) / C_Density * 0.75 / C_PI)**(1.0/3.0)  ! compute the size of the object from the mass and density\n\n        ! generate the momentums\n        orbital_momentum = sqrt(C_G * C_M_s / orbital_radius) * particles%m(i)  ! compute the optimal orbital momentum magnitude from the mass and orbital radius\n        call get_random_number(orbital_momentum, orbital_momentum * (1-velocity_noise_bound), \u0026\n            orbital_momentum * (1+velocity_noise_bound))  ! get a random momentum magnitude\n        particles%px(i) =  orbital_momentum * particles%y(i) / orbital_radius  ! assign x and y momentum to be in \n        particles%py(i) = -orbital_momentum * particles%x(i) / orbital_radius\n\n        particles%merged(i) = .False.\n\n    end do\n\nend subroutine initialize_particles\n```\n\nThere are really only two more steps. We need to make sure that we are checking for collisions and merging particles. When they collide, they will perfractly maintain momentum and form a new single particle at the center of mass of the two objects. This is done by assigning all properties to one particle, and marking the other as \"merged\" meaning it will not appear in future calcilations.\n\n```fortran\nsubroutine handle_collisions(particles, num_particles)\n\ninteger :: i, j\nreal(8) :: distance, x_com, y_com, combined_mass\n\ntype(particle_t), intent(inout) :: particles\ninteger, intent(in) :: num_particles\n\ndo i = 1, num_particles - 1\n    if (particles%merged(i)) then\n        cycle  ! skips if the first particles has collided\n    end if\n\n    ! check for solar merges\n    call get_distance(distance, particles%x(i), particles%y(i), dble(0.0), dble(0.0))\n    if (distance .le. C_R_s) then\n        ! TODO :: for now we assume the particle is so small that it has no mass comapred to sun\n        particles%merged(i) = .true.\n        cycle\n    end if\n\n    ! check for particle-to-particle merging\n    do j = i+1, num_particles\n        if (particles%merged(j)) then\n            cycle  ! skips if the second particles has collided\n        end if\n\n        call get_distance(distance, particles%x(i), particles%y(i), particles%x(j), particles%y(j))\n        if (distance .le. particles%r(i) + particles%r(j)) then  ! true if they should collide perfectly inelasically\n            ! add the momentum\n            particles%px(i) = particles%px(i) + particles%px(j)\n            particles%py(i) = particles%py(i) + particles%py(j)\n\n            ! compute the center of mass and move particles\n            combined_mass = particles%m(i) + particles%m(j)\n            particles%x(i) = (particles%x(i) * particles%m(i) + particles%x(j) * particles%m(j)) / combined_mass\n            particles%y(i) = (particles%y(i) * particles%m(i) + particles%y(j) * particles%m(j)) / combined_mass\n            \n            ! update mass and radius\n            particles%m(i) = combined_mass\n            particles%r(i) = (particles%m(i) / C_Density * 0.75 / C_PI)**(1.0/3.0)\n\n            ! count the second particle as merged\n            particles%merged(j) = .True.\n        end if\n    end do\nend do\n\nend subroutine handle_collisions\n```\n\nWe also want to step in time. This is the easiest part. We simply iterate over all object to compute the acceleration, and then use the euler method to integrate forward. The only clever trick here is that when we compute the gravitational force between two particles, there is an equal and opposite force. Therefore we can save an additional step by assigning the force to the other particle, and then iterating over only half of the objects.\n\n```fortran\nsubroutine take_time_step(particles, num_particles, dt)\n\n        integer :: i, j\n        real(8) :: acceleration_x, acceleration_y, velocity_x, velocity_y, distance\n\n        type(particle_t), intent(inout) :: particles\n        integer, intent(in) :: num_particles\n        real(8), intent(in) :: dt\n\n        ! reset the memory on acceleration\n        do i = 1, num_particles\n            particles%ax(i) = dble(0.0)\n            particles%ay(i) = dble(0.0)\n        end do\n\n        do i = 1, num_particles\n\n            call get_distance(distance, particles%x(i), particles%y(i), dble(0.0), dble(0.0))\n            particles%ax(i) = particles%ax(i) - (C_G * C_M_s * particles%x(i) / (distance**3))\n            particles%ay(i) = particles%ay(i) - (C_G * C_M_s * particles%y(i) / (distance**3))\n\n            do j = i+1, num_particles\n                if (particles%merged(j)) then\n                    cycle  ! skips if the second particles has collided\n                end if\n\n                acceleration_x = -C_G * particles%m(i) * particles%m(j) * (particles%x(i) - particles%x(j)) / (distance**3)\n                acceleration_y = -C_G * particles%m(i) * particles%m(j) * (particles%y(i) - particles%y(j)) / (distance**3)\n\n                particles%ax(i) = particles%ax(i) + acceleration_x / particles%m(i)\n                particles%ax(j) = particles%ax(j) - acceleration_x / particles%m(j)\n                particles%ay(i) = particles%ay(i) + acceleration_y / particles%m(i)\n                particles%ay(j) = particles%ay(j) - acceleration_y / particles%m(j)\n            end do\n\n        end do\n\n        ! use those accelerations to compute the updated positions and momentums using eulers method\n        do i = 1, num_particles\n\n            velocity_x = (particles%px(i) / particles%m(i)) + dt * particles%ax(i)\n            velocity_y = (particles%py(i) / particles%m(i)) + dt * particles%ay(i)\n\n            particles%x(i) = particles%x(i) + dt * velocity_x\n            particles%y(i) = particles%y(i) + dt * velocity_y\n            particles%px(i) = particles%m(i) * velocity_x\n            particles%py(i) = particles%m(i) * velocity_y\n\n        end do\n\n    end subroutine take_time_step\n```\n\n### Results\n\nI wrote a small python script that allows me to view the output data. Here is the result for an 8-body system that I simulated, and configured to run for about a quarter of a year.\n\n![screenshot](docs/8_body_orbit.png)\n\nI also gathered performance metrics, averaged over 20 time steps, for an increasing number of particles. Those results are shown here:\n\n![screenshot](docs/cpu_scaling_performance.png)\n\nThis is already a very fast performance for baseline, with the largest test case being 2^18 particles which averaged 20.43 seconds. We also clearly see the expected n-squared order time complexity from the system.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanieljvickers%2Fparticle-fortran","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanieljvickers%2Fparticle-fortran","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanieljvickers%2Fparticle-fortran/lists"}