examples: improve the pendulum simulation, with several modes and diagrams (#13446)

pull/13454/head
Ulises Jeremias Cornejo Fandos 2022-02-12 14:38:07 -03:00 committed by GitHub
parent a74d28ae5f
commit 4391ae563d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1501 additions and 366 deletions

View File

@ -164,6 +164,14 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
skip_files << 'examples/database/orm.v' // try fix it
}
}
$if windows {
// TODO: remove when closures on windows are supported
skip_files << 'examples/pendulum-simulation/animation.v'
skip_files << 'examples/pendulum-simulation/full.v'
skip_files << 'examples/pendulum-simulation/parallel.v'
skip_files << 'examples/pendulum-simulation/parallel_with_iw.v'
skip_files << 'examples/pendulum-simulation/sequential.v'
}
if testing.github_job != 'sokol-shaders-can-be-compiled' {
// These examples need .h files that are produced from the supplied .glsl files,
// using by the shader compiler tools in https://github.com/floooh/sokol-tools-bin/archive/pre-feb2021-api-changes.tar.gz

View File

@ -0,0 +1,18 @@
# Executables files
test
test.exe
# Temporary files
fns.txt
!/bin/test
/docs/build
*.ppm
main
parallel
parallel_with_iw
sequential
animation
full
*.log

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-2022 Ulises Jeremias Cornejo Fandos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,93 @@
<div align="center">
<p>
<img
style="width: 250px"
width="250"
src="https://user-images.githubusercontent.com/17727170/153699135-a63e9644-1a29-4c04-9de3-c9100b06001d.png"
>
</p>
<h1>Pendulum Simulation in V</h1>
You can see the origin implementation among with some benchmarks at
[ulises-jeremias/v-pendulum-simulation](https://github.com/ulises-jeremias/v-pendulum-simulation).
[vlang.io](https://vlang.io) |
[Docs](https://ulises-jeremias.github.io/v-pendulum-simulation) |
[Contributing](https://github.com/ulises-jeremias/v-pendulum-simulation/blob/main/CONTRIBUTING.md)
</div>
<div align="center">
[![Build Status][workflowbadge]][workflowurl]
[![Docs Validation][validatedocsbadge]][validatedocsurl]
[![License: MIT][licensebadge]][licenseurl]
</div>
## Run the Simulations
### Sequential Simulation
```sh
$ v -gc boehm -prod sequential.v
$ ./sequential # execute ./sequential -h for more info
```
### Parallel Simulation
```sh
$ v -gc boehm -prod parallel.v
$ ./parallel # execute ./parallel -h for more info
```
![image](https://user-images.githubusercontent.com/17727170/153718774-1c93b158-aee3-4be1-bb47-fe601fed7336.png)
### Parallel Simulation with Image Worker
```sh
$ v -gc boehm -prod parallel_with_iw.v
$ ./parallel_with_iw # execute ./parallel_with_iw -h for more info
```
![image](https://user-images.githubusercontent.com/17727170/153718769-eabb334d-454f-469f-a51a-14ffe67507de.png)
### Parallel Simulation with Graphic User Interface
```sh
$ v -gc boehm -prod animation.v
$ ./animation # execute ./animation -h for more info
```
### Full Parallel Simulation with Graphic User Interface and Image Output
```sh
$ v -gc boehm -prod full.v
$ ./full # execute ./full -h for more info
```
## Testing
To test the module, just type the following command:
```sh
$ v test .
```
## Benchmark
Check the original repository for tools to run benchmark tests. In there you can execute
the following command to execute benchmark tests to get a full comparison between implementations:
```sh
$ ./bin/run-benchmark-test --help
```
![image](https://user-images.githubusercontent.com/17727170/152750137-98e7c5a3-936b-4bc8-b71a-1b182c0bbf50.png)
[workflowbadge]: https://github.com/ulises-jeremias/v-pendulum-simulation/workflows/Build%20and%20Test%20with%20deps/badge.svg
[validatedocsbadge]: https://github.com/ulises-jeremias/v-pendulum-simulation/workflows/Validate%20Docs/badge.svg
[licensebadge]: https://img.shields.io/badge/License-MIT-blue.svg
[workflowurl]: https://github.com/ulises-jeremias/v-pendulum-simulation/commits/main
[validatedocsurl]: https://github.com/ulises-jeremias/v-pendulum-simulation/commits/main
[licenseurl]: https://github.com/ulises-jeremias/v-pendulum-simulation/blob/main/LICENSE

View File

@ -0,0 +1,37 @@
module main
import benchmark
import sim
import sim.anim
import sim.args as simargs
fn main() {
args := simargs.parse_args(extra_workers: 1) ? as simargs.ParallelArgs
mut app := anim.new_app(args)
mut workers := []thread{cap: args.workers}
mut bmark := benchmark.start()
defer {
app.request_chan.close()
sim.log('Waiting for workers to finish')
workers.wait()
app.result_chan.close()
sim.log('Workers finished!')
bmark.measure(@FN)
sim.log('Done!')
}
for id in 0 .. args.workers {
workers << go sim.sim_worker(id, app.request_chan, [app.result_chan])
}
handle_request := fn [app] (request &sim.SimRequest) ? {
app.request_chan <- request
}
go app.gg.run()
sim.run(args.params, grid: args.grid, on_request: sim.SimRequestHandler(handle_request))
}

View File

@ -0,0 +1,56 @@
module main
import benchmark
import sim
import sim.anim
import sim.args as simargs
import sim.img
fn main() {
args := simargs.parse_args(extra_workers: 2) ? as simargs.ParallelArgs
img_settings := img.image_settings_from_grid(args.grid)
mut writer := img.ppm_writer_for_fname(args.filename, img_settings) ?
mut app := anim.new_app(args)
mut workers := []thread{cap: args.workers + 1}
mut bmark := benchmark.start()
img_result_chan := chan &sim.SimResult{cap: args.workers}
defer {
image_worker := workers.pop()
app.request_chan.close()
sim.log('Waiting for workers to finish')
workers.wait()
app.result_chan.close()
img_result_chan.close()
sim.log('Waiting for image writer to finish')
image_worker.wait()
sim.log('Workers finished!')
bmark.measure(@FN)
sim.log('Closing writer file')
writer.close()
sim.log('Done!')
}
// start a worker on each core
for id in 0 .. app.args.workers {
workers << go sim.sim_worker(id, app.request_chan, [app.result_chan, img_result_chan])
}
handle_request := fn [app] (request &sim.SimRequest) ? {
app.request_chan <- request
}
workers << go img.image_worker(mut writer, img_result_chan, img_settings)
go app.gg.run()
sim.run(app.args.params,
grid: app.args.grid
on_request: sim.SimRequestHandler(handle_request)
)
}

View File

@ -0,0 +1,64 @@
module anim
import gg
import gx
import sim
import sim.args as simargs
const bg_color = gx.white
struct Pixel {
x f32
y f32
color gx.Color
}
struct App {
pub:
args simargs.ParallelArgs
request_chan chan &sim.SimRequest
result_chan chan &sim.SimResult
pub mut:
gg &gg.Context = 0
iidx int
pixels []u32
}
pub fn new_app(args simargs.ParallelArgs) &App {
total_pixels := args.grid.height * args.grid.width
mut app := &App{
args: args
pixels: []u32{len: total_pixels}
request_chan: chan &sim.SimRequest{cap: args.grid.width}
}
app.gg = gg.new_context(
width: args.grid.width
height: args.grid.height
create_window: true
window_title: 'V Pendulum Simulation'
user_data: app
bg_color: anim.bg_color
frame_fn: frame
init_fn: init
)
return app
}
fn init(mut app App) {
app.iidx = app.gg.new_streaming_image(app.args.grid.width, app.args.grid.height, 4,
pixel_format: .rgba8)
go pixels_worker(mut app)
}
fn frame(mut app App) {
app.gg.begin()
app.draw()
app.gg.end()
}
fn (mut app App) draw() {
mut istream_image := app.gg.get_cached_image_by_idx(app.iidx)
istream_image.update_pixel_data(&app.pixels[0])
app.gg.draw_image(0, 0, app.args.grid.width, app.args.grid.height, istream_image)
}

View File

@ -0,0 +1,19 @@
module anim
import benchmark
import sim
import sim.img
fn pixels_worker(mut app App) {
mut bmark := benchmark.new_benchmark()
for {
result := <-app.result_chan or { break }
bmark.step()
// find the closest magnet
pixel_color := img.compute_pixel(result)
app.pixels[result.id] = u32(pixel_color.abgr8())
bmark.ok()
}
bmark.stop()
println(bmark.total_message(@FN))
}

View File

@ -0,0 +1,158 @@
module args
import flag
import os
import runtime
import sim
import math
// customisable through setting VJOBS
const max_parallel_workers = runtime.nr_jobs()
[params]
pub struct ParserSettings {
sequential bool
img bool
extra_workers int
}
pub struct SequentialArgs {
pub:
params sim.SimParams
grid sim.GridSettings
filename string
}
pub struct ParallelArgs {
SequentialArgs
pub:
workers int = args.max_parallel_workers
}
pub type SimArgs = ParallelArgs | SequentialArgs
pub fn parse_args(config ParserSettings) ?SimArgs {
if config.sequential {
args := parse_sequential_args() ?
return SimArgs(args)
} else {
args := parse_parallel_args(config.extra_workers) ?
return SimArgs(args)
}
}
fn parse_sequential_args() ?SequentialArgs {
mut fp := flag.new_flag_parser(os.args)
fp.application('vps')
fp.version('v0.1.0')
fp.limit_free_args(0, 0) ?
fp.description('This is a pendulum simulation written in pure V')
fp.skip_executable()
// output parameters
width := fp.int('width', `w`, sim.default_width, 'width of the image output. Defaults to $sim.default_width')
height := fp.int('height', `h`, sim.default_height, 'height of the image output. Defaults to $sim.default_height')
filename := fp.string('output', `o`, 'out.ppm', 'name of the image output. Defaults to out.ppm')
// simulation parameters
rope_length := fp.float('rope-length', 0, sim.default_rope_length, 'rope length to use on simulation. Defaults to $sim.default_rope_length')
bearing_mass := fp.float('bearing-mass', 0, sim.default_bearing_mass, 'bearing mass to use on simulation. Defaults to $sim.default_bearing_mass')
magnet_spacing := fp.float('magnet-spacing', 0, sim.default_magnet_spacing, 'magnet spacing to use on simulation. Defaults to $sim.default_magnet_spacing')
magnet_height := fp.float('magnet-height', 0, sim.default_magnet_height, 'magnet height to use on simulation. Defaults to $sim.default_magnet_height')
magnet_strength := fp.float('magnet-strength', 0, sim.default_magnet_strength, 'magnet strength to use on simulation. Defaults to $sim.default_magnet_strength')
gravity := fp.float('gravity', 0, sim.default_gravity, 'gravity to use on simulation. Defaults to $sim.default_gravity')
fp.finalize() or {
println(fp.usage())
return none
}
params := sim.sim_params(
rope_length: rope_length
bearing_mass: bearing_mass
magnet_spacing: magnet_spacing
magnet_height: magnet_height
magnet_strength: magnet_strength
gravity: gravity
)
grid := sim.new_grid_settings(
width: width
height: height
)
args := SequentialArgs{
params: params
filename: filename
grid: grid
}
sim.log('$args')
return args
}
fn parse_parallel_args(extra_workers int) ?ParallelArgs {
mut fp := flag.new_flag_parser(os.args)
fp.application('vps')
fp.version('v0.1.0')
fp.limit_free_args(0, 0) ?
fp.description('This is a pendulum simulation written in pure V')
fp.skip_executable()
workers := fp.int('workers', 0, args.max_parallel_workers, 'amount of workers to use on simulation. Defaults to $args.max_parallel_workers')
// output parameters
width := fp.int('width', `w`, sim.default_width, 'width of the image output. Defaults to $sim.default_width')
height := fp.int('height', `h`, sim.default_height, 'height of the image output. Defaults to $sim.default_height')
filename := fp.string('output', `o`, 'out.ppm', 'name of the image output. Defaults to out.ppm')
// simulation parameters
rope_length := fp.float('rope-length', 0, sim.default_rope_length, 'rope length to use on simulation. Defaults to $sim.default_rope_length')
bearing_mass := fp.float('bearing-mass', 0, sim.default_bearing_mass, 'bearing mass to use on simulation. Defaults to $sim.default_bearing_mass')
magnet_spacing := fp.float('magnet-spacing', 0, sim.default_magnet_spacing, 'magnet spacing to use on simulation. Defaults to $sim.default_magnet_spacing')
magnet_height := fp.float('magnet-height', 0, sim.default_magnet_height, 'magnet height to use on simulation. Defaults to $sim.default_magnet_height')
magnet_strength := fp.float('magnet-strength', 0, sim.default_magnet_strength, 'magnet strength to use on simulation. Defaults to $sim.default_magnet_strength')
gravity := fp.float('gravity', 0, sim.default_gravity, 'gravity to use on simulation. Defaults to $sim.default_gravity')
fp.finalize() or {
println(fp.usage())
return none
}
params := sim.sim_params(
rope_length: rope_length
bearing_mass: bearing_mass
magnet_spacing: magnet_spacing
magnet_height: magnet_height
magnet_strength: magnet_strength
gravity: gravity
)
grid := sim.new_grid_settings(
width: width
height: height
)
args := ParallelArgs{
params: params
filename: filename
grid: grid
workers: get_workers(workers, extra_workers)
}
sim.log('$args')
return args
}
[inline]
fn get_workers(workers int, extra_workers int) int {
result := if workers + extra_workers <= args.max_parallel_workers {
workers
} else {
args.max_parallel_workers - extra_workers
}
return math.max(1, result)
}

View File

@ -0,0 +1,74 @@
module img
import gx
import os
import sim
[params]
pub struct ImageSettings {
pub:
width int = sim.default_width
height int = sim.default_height
cache_size int = 200
}
pub fn new_image_settings(settings ImageSettings) ImageSettings {
return ImageSettings{
...settings
}
}
pub fn image_settings_from_grid(grid sim.GridSettings) ImageSettings {
return ImageSettings{
width: grid.width
height: grid.height
}
}
pub fn (s ImageSettings) to_grid_settings() sim.GridSettings {
return sim.GridSettings{
width: s.width
height: s.height
}
}
pub struct PPMWriter {
mut:
file os.File
cache []byte
cache_size int
}
pub fn ppm_writer_for_fname(fname string, settings ImageSettings) ?&PPMWriter {
mut writer := &PPMWriter{
cache_size: settings.cache_size
cache: []byte{cap: settings.cache_size}
}
writer.start_for_file(fname, settings) ?
return writer
}
pub fn (mut writer PPMWriter) start_for_file(fname string, settings ImageSettings) ? {
writer.file = os.create(fname) ?
writer.file.writeln('P6 $settings.width $settings.height 255') ?
}
pub fn (mut writer PPMWriter) handle_pixel(p gx.Color) ? {
if writer.cache.len >= writer.cache_size {
writer.write() ?
writer.flush() ?
}
writer.cache << [p.r, p.g, p.b]
}
pub fn (mut writer PPMWriter) flush() ? {
writer.cache.clear()
}
pub fn (mut writer PPMWriter) write() ? {
writer.file.write(writer.cache) ?
}
pub fn (mut writer PPMWriter) close() {
writer.file.close()
}

View File

@ -0,0 +1,40 @@
module img
import benchmark
import sim
pub fn image_worker(mut writer PPMWriter, result_chan chan &sim.SimResult, settings ImageSettings) {
width := settings.width
height := settings.height
total_pixels := width * height
// as new pixels come in, write them to the image file
mut current_index := u64(0)
mut pixel_buf := []ValidColor{len: total_pixels, init: ValidColor{
valid: false
}}
mut bmark := benchmark.new_benchmark()
for {
result := <-result_chan or { break }
// find the closest magnet
pixel_buf[result.id].Color = compute_pixel(result)
pixel_buf[result.id].valid = true
for current_index < total_pixels && pixel_buf[current_index].valid {
bmark.step()
writer.handle_pixel(pixel_buf[current_index].Color) or {
bmark.fail()
sim.log(@MOD + '.' + @FN + ': pixel handler failed. Error $err')
break
}
bmark.ok()
current_index++
}
}
bmark.stop()
println(bmark.total_message(@FN))
writer.write() or { panic('Could not write image') }
}

View File

@ -0,0 +1,68 @@
module img
import gx
import sim
pub struct ValidColor {
gx.Color
pub mut:
valid bool
}
pub struct ImageWritter {
settings ImageSettings
pub mut:
writer PPMWriter
current_index int
buffer []ValidColor
}
pub fn new_image_writer(mut writer PPMWriter, settings ImageSettings) &ImageWritter {
total_pixels := settings.width * settings.height
mut buffer := []ValidColor{len: total_pixels, init: ValidColor{
valid: false
}}
return &ImageWritter{
writer: writer
settings: settings
buffer: buffer
}
}
pub fn (mut iw ImageWritter) handle(result sim.SimResult) ?int {
total_pixels := iw.settings.width * iw.settings.height
// find the closest magnet
iw.buffer[result.id].Color = compute_pixel(result)
iw.buffer[result.id].valid = true
for iw.current_index < total_pixels && iw.buffer[iw.current_index].valid {
iw.writer.handle_pixel(iw.buffer[iw.current_index].Color) or {
sim.log(@MOD + '.' + @FN + ': pixel handler failed. Error $err')
break
}
iw.current_index++
}
if iw.current_index == total_pixels {
iw.writer.write() or { panic('Could not write image') }
return none
}
return iw.current_index
}
pub fn compute_pixel(result sim.SimResult) gx.Color {
closest_to_m1 := result.magnet1_distance < result.magnet2_distance
&& result.magnet1_distance < result.magnet3_distance
closest_to_m2 := result.magnet2_distance < result.magnet1_distance
&& result.magnet2_distance < result.magnet3_distance
if closest_to_m1 {
return gx.red
} else if closest_to_m2 {
return gx.green
} else {
return gx.blue
}
}

View File

@ -0,0 +1,9 @@
module sim
// log is a helper function to print debug info
[inline]
pub fn log(info string) {
$if verbose ? {
println(info)
}
}

View File

@ -0,0 +1,96 @@
module sim
import math
pub const (
default_rope_length = 0.25
default_bearing_mass = 0.03
default_magnet_spacing = 0.05
default_magnet_height = 0.03
default_magnet_strength = 10.0
default_gravity = 4.9
)
[params]
pub struct SimParams {
rope_length f64 = sim.default_rope_length
bearing_mass f64 = sim.default_bearing_mass
magnet_spacing f64 = sim.default_magnet_spacing
magnet_height f64 = sim.default_magnet_height
magnet_strength f64 = sim.default_magnet_strength
gravity f64 = sim.default_gravity
}
pub fn sim_params(params SimParams) SimParams {
return SimParams{
...params
}
}
pub fn (params SimParams) get_rope_vector(state SimState) Vector3D {
rope_origin := vector(z: params.rope_length)
return state.position + rope_origin.scale(-1)
}
pub fn (params SimParams) get_forces_sum(state SimState) Vector3D {
// force due to gravity
f_gravity := params.get_grav_force(state)
// force due to magnets
f_magnet1 := params.get_magnet1_force(state)
f_magnet2 := params.get_magnet2_force(state)
f_magnet3 := params.get_magnet3_force(state)
mut f_passive := vector(x: 0.0, y: 0.0, z: 0.0)
for force in [f_gravity, f_magnet1, f_magnet2, f_magnet3] {
f_passive = f_passive + force
}
// force due to tension of the rope
f_tension := params.get_tension_force(state, f_passive)
return f_passive + f_tension
}
pub fn (params SimParams) get_grav_force(state SimState) Vector3D {
return vector(z: -params.bearing_mass * params.gravity)
}
pub fn (params SimParams) get_magnet_position(theta f64) Vector3D {
return vector(
x: math.cos(theta) * params.magnet_spacing
y: math.sin(theta) * params.magnet_spacing
z: -params.magnet_height
)
}
pub fn (params SimParams) get_magnet_force(theta f64, state SimState) Vector3D {
magnet_position := params.get_magnet_position(theta)
mut diff := magnet_position + state.position.scale(-1)
distance_squared := diff.norm_squared()
diff = diff.scale(1.0 / math.sqrt(distance_squared))
return diff.scale(params.magnet_strength / distance_squared)
}
pub fn (params SimParams) get_magnet_dist(theta f64, state SimState) f64 {
return (params.get_magnet_position(theta) + state.position.scale(-1)).norm()
}
pub fn (params SimParams) get_magnet1_force(state SimState) Vector3D {
return params.get_magnet_force(0.0 * math.pi / 3.0, state)
}
pub fn (params SimParams) get_magnet2_force(state SimState) Vector3D {
return params.get_magnet_force(2.0 * math.pi / 3.0, state)
}
pub fn (params SimParams) get_magnet3_force(state SimState) Vector3D {
return params.get_magnet_force(4.0 * math.pi / 3.0, state)
}
pub fn (params SimParams) get_tension_force(state SimState, f_passive Vector3D) Vector3D {
rope_vector := params.get_rope_vector(state)
rope_vector_norm := rope_vector.scale(1.0 / rope_vector.norm())
return rope_vector_norm.scale(-1.0 * (rope_vector_norm * f_passive))
}

View File

@ -0,0 +1,125 @@
module sim
import math
const (
params_test_mock_params = SimParams{
rope_length: 0.25
bearing_mass: 0.03
magnet_spacing: 0.05
magnet_height: 0.03
magnet_strength: 10
gravity: 4.9
}
params_test_mock_state = SimState{
position: vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: 0.002311063475327252
)
velocity: vector(
x: -7.251158929833104
y: -12.559375680227724
z: -105.91539687686381
)
accel: vector(
x: -8.337034766251843e-11
y: -2.842170943040401e-10
z: 1.2126596023639044e-10
)
}
params_test_mock_tetha = 2.0 * math.pi / 3.0
)
pub fn test_get_rope_vector() {
result := sim.params_test_mock_params.get_rope_vector(sim.params_test_mock_state)
expected := vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: -0.24768893652467275
)
assert result == expected
}
pub fn test_get_forces_sum() {
result := sim.params_test_mock_params.get_forces_sum(sim.params_test_mock_state)
expected := vector(
x: 3.637978807091713e-12
y: 5.229594535194337e-12
z: 9.094947017729282e-13
)
assert result == expected
}
pub fn test_get_grav_force() {
result := sim.params_test_mock_params.get_grav_force(sim.params_test_mock_state)
expected := vector(
z: -0.147
)
assert result == expected
}
pub fn test_get_magnet_position() {
result := sim.params_test_mock_params.get_magnet_position(sim.params_test_mock_tetha)
expected := vector(
x: -0.024999999999999988
y: 0.043301270189221946
z: -0.03
)
assert result == expected
}
pub fn test_get_magnet_force() {
result := sim.params_test_mock_params.get_magnet_force(sim.params_test_mock_tetha,
sim.params_test_mock_state)
expected := vector(
x: -157.45722976925555
y: 1422.736432604726
z: -632.5695169850264
)
assert result == expected
}
pub fn test_get_magnet_dist() {
result := sim.params_test_mock_params.get_magnet_dist(sim.params_test_mock_tetha,
sim.params_test_mock_state)
expected := 0.07993696666249227
assert result == expected
}
pub fn test_get_magnet1_force() {
result := sim.params_test_mock_params.get_magnet1_force(sim.params_test_mock_state)
expected := vector(
x: 1310.8545084099674
y: 575.0062553126633
z: -632.5695169850262
)
assert result == expected
}
pub fn test_get_magnet2_force() {
result := sim.params_test_mock_params.get_magnet2_force(sim.params_test_mock_state)
expected := vector(
x: -157.45722976925555
y: 1422.736432604726
z: -632.5695169850264
)
assert result == expected
}
pub fn test_get_magnet3_force() {
result := sim.params_test_mock_params.get_magnet3_force(sim.params_test_mock_state)
expected := vector(
x: -1710.46541088048
y: -2962.612996234165
z: -6871.632889552589
)
assert result == expected
}
pub fn test_get_tension_force() {
result := sim.params_test_mock_params.get_tension_force(sim.params_test_mock_state,
vector(x: 0.0, y: 0.0, z: 0.0))
expected := vector(x: 0.0, y: 0.0, z: 0.0)
assert result == expected
}

View File

@ -0,0 +1,96 @@
module sim
import benchmark
import term
pub type SimRequestHandler = fn (request &SimRequest) ?
pub type SimStartHandler = fn () ?
pub type SimFinishHandler = fn () ?
pub const (
default_width = 600
default_height = 600
)
[params]
pub struct GridSettings {
pub:
width int = sim.default_width
height int = sim.default_height
}
pub fn new_grid_settings(settings GridSettings) GridSettings {
return GridSettings{
...settings
}
}
[params]
pub struct RunnerSettings {
pub:
grid GridSettings
on_request SimRequestHandler
on_start SimStartHandler
on_finish SimFinishHandler
}
pub fn run(params SimParams, settings RunnerSettings) {
height := settings.grid.height
width := settings.grid.width
if !isnil(settings.on_start) {
settings.on_start() or {
log(@MOD + '.' + @FN + ': Simulation start handler failed. Error $err')
}
}
mut index := 0
log('')
mut bmark := benchmark.new_benchmark()
for y in 0 .. height {
$if verbose ? {
term.clear_previous_line()
}
log(@MOD + '.' + @FN + ': y: ${y + 1}')
for x in 0 .. width {
bmark.step()
// setup state conditions
position := vector(
x: 0.1 * ((f64(x) - 0.5 * f64(width - 1)) / f64(width - 1))
y: 0.1 * ((f64(y) - 0.5 * f64(height - 1)) / f64(height - 1))
z: 0.0
)
velocity := vector(x: 0, y: 0, z: 0)
mut state := new_state(
position: position
velocity: velocity
)
state.satisfy_rope_constraint(params)
request := &SimRequest{
id: index
state: state
params: params
}
settings.on_request(request) or {
log(@MOD + '.' + @FN + ': request handler failed. Error $err')
bmark.fail()
break
}
index++
bmark.ok()
}
}
bmark.stop()
println(bmark.total_message(@FN))
if !isnil(settings.on_finish) {
settings.on_finish() or {
log(@MOD + '.' + @FN + ': Simulation stop handler failed. Error $err')
}
}
}

View File

@ -0,0 +1,47 @@
module sim
pub struct SimState {
mut:
position Vector3D
velocity Vector3D
accel Vector3D
}
pub fn new_state(state SimState) SimState {
return SimState{
...state
}
}
pub fn (mut state SimState) satisfy_rope_constraint(params SimParams) {
mut rope_vector := params.get_rope_vector(state)
rope_vector = rope_vector.scale(params.rope_length / rope_vector.norm())
state.position = vector(z: params.rope_length) + rope_vector
}
pub fn (mut state SimState) increment(delta_t f64, params SimParams) {
// 1. add up all forces
// 2. get an accelleration
// 3. add to velocity
// 4. ensure rope constraint is satisfied
// sum up all forces
forces_sum := params.get_forces_sum(state)
// get the acceleration
accel := forces_sum.scale(1.0 / params.bearing_mass)
state.accel = accel
// update the velocity
state.velocity = state.velocity + accel.scale(delta_t)
// update the position
state.position = state.position + state.velocity.scale(delta_t)
// ensure the position satisfies rope constraint
state.satisfy_rope_constraint(params)
}
pub fn (state SimState) done() bool {
return state.velocity.norm() < 0.05 && state.accel.norm() < 0.01
}

View File

@ -0,0 +1,64 @@
module sim
const (
sim_test_mock_params = SimParams{
rope_length: 0.25
bearing_mass: 0.03
magnet_spacing: 0.05
magnet_height: 0.03
magnet_strength: 10
gravity: 4.9
}
sim_test_mock_state = SimState{
position: vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: 0.002311063475327252
)
velocity: vector(
x: -7.251158929833104
y: -12.559375680227724
z: -105.91539687686381
)
accel: vector(
x: -8.337034766251843e-11
y: -2.842170943040401e-10
z: 1.2126596023639044e-10
)
}
)
pub fn test_satisfy_rope_constraint() {
mut state := SimState{
...sim.sim_test_mock_state
}
state.satisfy_rope_constraint(sim.sim_test_mock_params)
assert state.position.x == -0.016957230930171364
assert state.position.y == -0.02937078552673521
assert state.position.z == 0.002311063475327252
assert state.velocity.x == -7.251158929833104
assert state.velocity.y == -12.559375680227724
assert state.velocity.z == -105.91539687686381
assert state.accel.x == -8.337034766251843e-11
assert state.accel.y == -2.842170943040401e-10
assert state.accel.z == 1.2126596023639044e-10
}
pub fn test_increment() {
mut state := SimState{
...sim.sim_test_mock_state
}
delta_t := 0.0005
state.increment(delta_t, sim.sim_test_mock_params)
assert state.position.x == -0.016957230930171364
assert state.position.y == -0.02937078552673524
assert state.position.z == 0.0023110634753272796
assert state.velocity.x == -7.251158929833044
assert state.velocity.y == -12.559375680227637
assert state.velocity.z == -105.9153968768638
assert state.accel.x == 1.2126596023639044e-10
assert state.accel.y == 1.7431981783981126e-10
assert state.accel.z == 3.031649005909761e-11
}

View File

@ -0,0 +1,50 @@
module sim
import math
// Vector3D is a 3D vector
pub struct Vector3D {
x f64
y f64
z f64
}
// vector creates a Vector3D passing x,y,z as parameteres
pub fn vector(data Vector3D) Vector3D {
return Vector3D{
...data
}
}
// addition
pub fn (v Vector3D) + (v2 Vector3D) Vector3D {
return Vector3D{
x: v.x + v2.x
y: v.y + v2.y
z: v.z + v2.z
}
}
// dot product
pub fn (v Vector3D) * (v2 Vector3D) f64 {
return (v.x * v2.x) + (v.y * v2.y) + (v.z * v2.z)
}
// scale gets a scaled vector
pub fn (v Vector3D) scale(scalar f64) Vector3D {
return Vector3D{
x: v.x * scalar
y: v.y * scalar
z: v.z * scalar
}
}
// norm_squared returns the square of the norm of the vector
pub fn (v Vector3D) norm_squared() f64 {
return v * v
}
// norm returns the norm of the vector
pub fn (v Vector3D) norm() f64 {
return math.sqrt(v.norm_squared())
}

View File

@ -0,0 +1,64 @@
module sim
fn test_add() {
v := vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: 0.002311063475327252
)
result := v + v
expected := vector(
x: -0.03391446186034273
y: -0.05874157105347042
z: 0.004622126950654504
)
assert result == expected
}
fn test_dot() {
v := vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: 0.002311063475327252
)
result := v * v
expected := 0.0011555317376636305
assert result == expected
}
fn test_scale() {
v := vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: 0.002311063475327252
)
result := v.scale(2.0)
expected := vector(
x: -0.03391446186034273
y: -0.05874157105347042
z: 0.004622126950654504
)
assert result == expected
}
fn test_norm_squared() {
v := vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: 0.002311063475327252
)
result := v.norm_squared()
expected := 0.0011555317376636305
assert result == expected
}
fn test_norm() {
v := vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: 0.002311063475327252
)
result := v.norm()
expected := 0.033993113091678295
assert result == expected
}

View File

@ -0,0 +1,67 @@
module sim
import math
import benchmark
const (
max_iterations = 1000
simulation_delta_t = 0.0005
)
pub struct SimRequest {
params SimParams
state SimState
pub:
id int
}
pub struct SimResult {
state SimState
pub:
id int
magnet1_distance f64
magnet2_distance f64
magnet3_distance f64
}
pub fn sim_worker(id int, request_chan chan &SimRequest, result_channels []chan &SimResult) {
mut bmark := benchmark.new_benchmark()
for {
request := <-request_chan or { break }
bmark.step()
result := compute_result(request)
for ch in result_channels {
ch <- result
}
bmark.ok()
}
bmark.stop()
println(bmark.total_message(@FN + ': worker $id'))
}
pub fn compute_result(request SimRequest) &SimResult {
mut state := request.state
params := request.params
for _ in 0 .. sim.max_iterations {
state.increment(sim.simulation_delta_t, params)
if state.done() {
println('done!')
break
}
}
m1_dist := params.get_magnet_dist(0, state)
m2_dist := params.get_magnet_dist(2.0 * math.pi / 3.0, state)
m3_dist := params.get_magnet_dist(4.0 * math.pi / 3.0, state)
id := request.id
return &SimResult{
id: id
state: state
magnet1_distance: m1_dist
magnet2_distance: m2_dist
magnet3_distance: m3_dist
}
}

View File

@ -0,0 +1,63 @@
module sim
const (
worker_test_mock_params = SimParams{
rope_length: 0.25
bearing_mass: 0.03
magnet_spacing: 0.05
magnet_height: 0.03
magnet_strength: 10
gravity: 4.9
}
worker_test_mock_state = SimState{
position: vector(
x: -0.016957230930171364
y: -0.02937078552673521
z: 0.002311063475327252
)
velocity: vector(
x: -7.251158929833104
y: -12.559375680227724
z: -105.91539687686381
)
accel: vector(
x: -8.337034766251843e-11
y: -2.842170943040401e-10
z: 1.2126596023639044e-10
)
}
)
fn test_compute_result() {
request := SimRequest{
id: 0
params: sim.worker_test_mock_params
state: sim.worker_test_mock_state
}
expected_state := SimState{
position: vector(
x: -0.01695723093017133
y: -0.02937078552673517
z: 0.002311063475327252
)
velocity: vector(
x: -7.251158929832518
y: -12.559375680226692
z: -105.91539687685668
)
accel: vector(
x: -3.789561257387201e-12
y: 3.410605131648481e-11
z: 3.031649005909761e-11
)
}
expected := &SimResult{
state: expected_state
id: 0
magnet1_distance: 0.07993696666249224
magnet2_distance: 0.07993696666249223
magnet3_distance: 0.03609361938278009
}
result := compute_result(request)
assert result == expected
}

View File

@ -0,0 +1,84 @@
module main
import benchmark
import sim
import sim.args as simargs
import sim.img
fn main() {
args := simargs.parse_args() ? as simargs.ParallelArgs
img_settings := img.image_settings_from_grid(args.grid)
width := img_settings.width
height := img_settings.height
total_pixels := width * height
request_chan := chan &sim.SimRequest{cap: args.workers}
result_chan := chan &sim.SimResult{cap: args.workers}
mut writer := img.ppm_writer_for_fname(args.filename, img_settings) ?
mut image_writer := img.new_image_writer(mut writer, img_settings)
mut workers := []thread{cap: args.workers}
mut bmark := benchmark.start()
defer {
request_chan.close()
sim.log('Waiting for workers to finish')
workers.wait()
result_chan.close()
bmark.measure(@FN)
sim.log('Closing writer file')
writer.close()
sim.log('Done!')
}
for id in 0 .. args.workers {
workers << go sim.sim_worker(id, request_chan, [result_chan])
}
mut x := 0
mut y := 0
mut request_index := 0
for {
// setup state conditions
position := sim.vector(
x: 0.1 * ((f64(x) - 0.5 * f64(width - 1)) / f64(width - 1))
y: 0.1 * ((f64(y) - 0.5 * f64(height - 1)) / f64(height - 1))
z: 0.0
)
velocity := sim.vector(x: 0, y: 0, z: 0)
mut state := sim.new_state(
position: position
velocity: velocity
)
state.satisfy_rope_constraint(args.params)
request := &sim.SimRequest{
id: request_index
state: state
params: args.params
}
select {
result := <-result_chan {
image_writer.handle(result) or { break }
}
else {
if request.id == total_pixels {
continue
}
request_chan <- request
x++
if x == width {
x = 0
y++
sim.log('y: ${y + 1}')
}
request_index++
}
}
}
}

View File

@ -0,0 +1,50 @@
module main
import benchmark
import sim
import sim.args as simargs
import sim.img
fn main() {
args := simargs.parse_args(extra_workers: 1) ? as simargs.ParallelArgs
img_settings := img.image_settings_from_grid(args.grid)
request_chan := chan &sim.SimRequest{cap: args.workers}
result_chan := chan &sim.SimResult{cap: args.workers}
mut writer := img.ppm_writer_for_fname(args.filename, img_settings) ?
mut workers := []thread{cap: args.workers + 1}
mut bmark := benchmark.start()
defer {
image_worker := workers.pop()
request_chan.close()
sim.log('Waiting for workers to finish')
workers.wait()
result_chan.close()
sim.log('Waiting for image writer to finish')
image_worker.wait()
sim.log('Workers finished!')
bmark.measure(@FN)
sim.log('Closing writer file')
writer.close()
sim.log('Done!')
}
for id in 0 .. args.workers {
workers << go sim.sim_worker(id, request_chan, [result_chan])
}
workers << go img.image_worker(mut writer, result_chan, img_settings)
handle_request := fn [request_chan] (request &sim.SimRequest) ? {
request_chan <- request
}
sim.run(args.params,
grid: args.grid
on_request: sim.SimRequestHandler(handle_request)
)
}

View File

@ -0,0 +1,30 @@
module main
import benchmark
import sim
import sim.args as simargs
import sim.img
fn main() {
args := simargs.parse_args(sequential: true) ? as simargs.SequentialArgs
mut bmark := benchmark.start()
defer {
bmark.measure(@FN)
}
mut writer := img.ppm_writer_for_fname(args.filename, img.image_settings_from_grid(args.grid)) ?
defer {
writer.close()
}
handle_request := fn [mut writer] (request &sim.SimRequest) ? {
result := sim.compute_result(request)
pixel := img.compute_pixel(result)
return writer.handle_pixel(pixel)
}
sim.run(args.params, grid: args.grid, on_request: sim.SimRequestHandler(handle_request))
writer.write() ?
}

View File

@ -1,366 +0,0 @@
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// sim.v * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// created by: jordan bonecutter * * * * * * * * * * * * * * * * * * *
// jpbonecutter@gmail.com * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
//
// I wrote the pendulum simulator to learn V, I think it could be a
// good addition to the examples directory.
// Essentially, the pendulum sim runs a simulation of a pendulum with
// a metallic tip swinging over three magnets.
// I run this simulation with the initial position at each pixel in an
// image and color the pixel according to the magnet over which it
// finally rests.
// I used some fun features in V like coroutines, channels,
// struct embedding, mutability, methods, and the like.
import math
import os
import term
import runtime
// customisable through setting VJOBS
const parallel_workers = runtime.nr_jobs()
const width = 800
const height = 600
struct Vec3D {
x f64
y f64
z f64
}
fn (v Vec3D) add(v2 Vec3D) Vec3D {
return Vec3D{
x: v.x + v2.x
y: v.y + v2.y
z: v.z + v2.z
}
}
fn (v Vec3D) dot(v2 Vec3D) f64 {
return (v.x * v2.x) + (v.y * v2.y) + (v.z * v2.z)
}
fn (v Vec3D) scale(scalar f64) Vec3D {
return Vec3D{
x: v.x * scalar
y: v.y * scalar
z: v.z * scalar
}
}
fn (v Vec3D) norm_squared() f64 {
return v.dot(v)
}
fn (v Vec3D) norm() f64 {
return math.sqrt(v.norm_squared())
}
struct SimState {
mut:
position Vec3D
velocity Vec3D
accel Vec3D
}
// magnets lie at [
// math.cos(index * 2 * math.pi / 3) * magnet_spacing
// math.sin(index * 2 * math.pi / 3) * magnet_spacing
// -magnet_height
// ]
struct SimParams {
rope_length f64
bearing_mass f64
magnet_spacing f64
magnet_height f64
magnet_strength f64
gravity f64
}
fn (params SimParams) get_rope_vector(state SimState) Vec3D {
rope_origin := Vec3D{
x: 0
y: 0
z: params.rope_length
}
return state.position.add(rope_origin.scale(-1))
}
fn (mut state SimState) satisfy_rope_constraint(params SimParams) {
mut rope_vector := params.get_rope_vector(state)
rope_vector = rope_vector.scale(params.rope_length / rope_vector.norm())
state.position = Vec3D{
x: 0
y: 0
z: params.rope_length
}.add(rope_vector)
}
fn (params SimParams) get_grav_force(state SimState) Vec3D {
return Vec3D{
x: 0
y: 0
z: -params.bearing_mass * params.gravity
}
}
fn (params SimParams) get_magnet_position(theta f64) Vec3D {
return Vec3D{
x: math.cos(theta) * params.magnet_spacing
y: math.sin(theta) * params.magnet_spacing
z: -params.magnet_height
}
}
fn (params SimParams) get_magnet_force(theta f64, state SimState) Vec3D {
magnet_position := params.get_magnet_position(theta)
mut diff := magnet_position.add(state.position.scale(-1))
distance_squared := diff.norm_squared()
diff = diff.scale(1.0 / math.sqrt(distance_squared))
return diff.scale(params.magnet_strength / distance_squared)
}
fn (params SimParams) get_magnet_dist(theta f64, state SimState) f64 {
return params.get_magnet_position(theta).add(state.position.scale(-1)).norm()
}
fn (params SimParams) get_magnet1_force(state SimState) Vec3D {
return params.get_magnet_force(0.0 * math.pi / 3.0, state)
}
fn (params SimParams) get_magnet2_force(state SimState) Vec3D {
return params.get_magnet_force(2.0 * math.pi / 3.0, state)
}
fn (params SimParams) get_magnet3_force(state SimState) Vec3D {
return params.get_magnet_force(4.0 * math.pi / 3.0, state)
}
fn (params SimParams) get_tension_force(state SimState, f_passive Vec3D) Vec3D {
rope_vector := params.get_rope_vector(state)
rope_vector_norm := rope_vector.scale(1.0 / rope_vector.norm())
return rope_vector_norm.scale(-1.0 * rope_vector_norm.dot(f_passive))
}
fn (mut state SimState) increment(delta_t f64, params SimParams) {
// basically just add up all forces =>
// get an accelleration =>
// add to velocity =>
// ensure rope constraint is satisfied
// force due to gravity
f_gravity := params.get_grav_force(state)
// force due to each magnet
f_magnet1 := params.get_magnet1_force(state)
// force due to each magnet
f_magnet2 := params.get_magnet2_force(state)
// force due to each magnet
f_magnet3 := params.get_magnet3_force(state)
// passive forces
f_passive := f_gravity.add(f_magnet1.add(f_magnet2.add(f_magnet3)))
// force due to tension of the rope
f_tension := params.get_tension_force(state, f_passive)
// sum up all the fores
f_sum := f_tension.add(f_passive)
// get the acceleration
accel := f_sum.scale(1.0 / params.bearing_mass)
state.accel = accel
// update the velocity
state.velocity = state.velocity.add(accel.scale(delta_t))
// update the position
state.position = state.position.add(state.velocity.scale(delta_t))
// ensure the position satisfies rope constraint
state.satisfy_rope_constraint(params)
}
fn (state SimState) done() bool {
return state.velocity.norm() < 0.05 && state.accel.norm() < 0.01
}
struct PPMWriter {
mut:
file os.File
}
struct ImageSettings {
width int
height int
}
struct Pixel {
r byte
g byte
b byte
}
fn (mut writer PPMWriter) start_for_file(fname string, settings ImageSettings) {
writer.file = os.create(fname) or { panic("can't create file $fname") }
writer.file.writeln('P6 $settings.width $settings.height 255') or {}
}
fn (mut writer PPMWriter) next_pixel(p Pixel) {
writer.file.write([p.r, p.g, p.b]) or {}
}
fn (mut writer PPMWriter) finish() {
writer.file.close()
}
fn sim_runner(mut state SimState, params SimParams) Pixel {
// do the simulation!
for _ in 0 .. 1000 {
state.increment(0.0005, params)
if state.done() {
println('done!')
break
}
}
// find the closest magnet
m1_dist := params.get_magnet_dist(0, state)
m2_dist := params.get_magnet_dist(2.0 * math.pi / 3.0, state)
m3_dist := params.get_magnet_dist(4.0 * math.pi / 3.0, state)
if m1_dist < m2_dist && m1_dist < m3_dist {
return Pixel{
r: 255
g: 0
b: 0
}
} else if m2_dist < m1_dist && m2_dist < m3_dist {
return Pixel{
r: 0
g: 255
b: 0
}
} else {
return Pixel{
r: 0
g: 0
b: 255
}
}
}
struct SimResult {
id u64
p Pixel
}
struct SimRequest {
id u64
params SimParams
mut:
initial SimState
}
fn sim_worker(request_chan chan SimRequest, result_chan chan SimResult) {
// serve sim requests as they come in
for {
mut request := <-request_chan or { break }
result_chan <- SimResult{
id: request.id
p: sim_runner(mut request.initial, request.params)
}
}
}
struct ValidPixel {
Pixel
mut:
valid bool
}
fn image_worker(mut writer PPMWriter, result_chan chan SimResult, total_pixels u64) {
// as new pixels come in, write them to the image file
mut current_index := u64(0)
mut pixel_buf := []ValidPixel{len: int(total_pixels), init: ValidPixel{
valid: false
}}
for {
result := <-result_chan or { break }
pixel_buf[result.id].Pixel = result.p
pixel_buf[result.id].valid = true
for current_index < total_pixels && pixel_buf[current_index].valid {
writer.next_pixel(pixel_buf[current_index].Pixel)
current_index++
}
if current_index >= total_pixels {
break
}
}
}
fn main() {
params := SimParams{
rope_length: 0.25
bearing_mass: 0.03
magnet_spacing: 0.05
magnet_height: 0.03
magnet_strength: 10.0
gravity: 4.9
}
mut writer := PPMWriter{}
writer.start_for_file('test.ppm', ImageSettings{
width: width
height: height
})
defer {
writer.finish()
}
result_chan := chan SimResult{}
request_chan := chan SimRequest{}
// start a worker on each core
for _ in 0 .. parallel_workers {
go sim_worker(request_chan, result_chan)
}
go fn (request_chan chan SimRequest, params SimParams) {
mut index := u64(0)
println('')
for y in 0 .. height {
term.clear_previous_line()
println('Line: $y')
for x in 0 .. width {
// setup initial conditions
mut state := SimState{}
state.position = Vec3D{
x: 0.1 * ((f64(x) - 0.5 * f64(width - 1)) / f64(width - 1))
y: 0.1 * ((f64(y) - 0.5 * f64(height - 1)) / f64(height - 1))
z: 0.0
}
state.velocity = Vec3D{}
state.satisfy_rope_constraint(params)
request_chan <- SimRequest{
id: index
initial: state
params: params
}
index++
}
}
request_chan.close()
}(request_chan, params)
image_worker(mut writer, result_chan, width * height)
}