Compare commits

..

5 Commits

Author SHA1 Message Date
f7c5610c95 chore: add README 2024-11-25 14:54:17 +01:00
1b2ff590ec feat: improve UX
- invalid iteration counts are met with a usage error
- pressing 'h' will print out an (incomplete) help menu
- information about the current best point and iteration count is shown
2024-11-25 14:45:05 +01:00
fe62617beb chore: add comments to swarm.hpp 2024-11-25 14:44:55 +01:00
2b20e90a08 feat: add tanh transition curve particle parameter change algorithm 2024-11-25 13:58:02 +01:00
cf19cc7183 ci: add automatic header file deps to Makefile 2024-11-25 13:52:40 +01:00
5 changed files with 106 additions and 22 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.clangd
*.o
bin/swarm
obj/

View File

@@ -6,19 +6,16 @@ OBJ_DIR = ./obj
HEADERS = $(wildcard $(INC_DIR)/*.hpp)
SRC_FILES = $(wildcard $(SRC_DIR)/*.cpp)
DEP_FILES = $(OBJ_FILES:.o=.d)
OBJ_FILES = $(addprefix $(OBJ_DIR)/,$(notdir $(SRC_FILES:.cpp=.o)))
CXXFLAGS += -Wall -std=c++20 -O2
CXXFLAGS += -Wall -std=c++20 -O2 -I $(INC_DIR)
all: bin/swarm
$(OBJ_DIR)/%.o: src/%.cpp
$(CXX) -I $(INC_DIR) $(CXXFLAGS) -c -o $@ $^
bin/swarm: $(OBJ_FILES)
@mkdir -p $(OBJ_DIR)
@mkdir -p bin
$(CXX) -I $(INC_DIR) $(CXXFLAGS) -o $@ $^
$(CXX) $(CXXFLAGS) -o $@ $^
.PHONY: debug
debug: CXXFLAGS += -O0 -g
@@ -28,3 +25,9 @@ debug: clean bin/swarm
clean:
$(RM) obj/*.o
$(RM) bin/swarm
-include $(DEP_FILES)
$(OBJ_DIR)/%.o : src/%.cpp
mkdir -p $(@D)
$(CXX) $(CXXFLAGS) -MMD -c $< -o $@

15
README Normal file
View File

@@ -0,0 +1,15 @@
# swarmc
A [Particle Swarm
Optimisation](https://en.wikipedia.org/wiki/Particle_swarm_optimization)
visualiser in C++. Run with `swarm <iteration count>` and enjoy the pretty
colors. Pressing `h` will print out a help menu.
Requires an ANSI terminal (emulator) with support for 24-bit colours to render.
The implemented showcase works for `R^2 -> R` functions, but the underlying code
can work for much more, including for spaces other than real spaces (see the
type parameters in `Agent` et al).
Supports dynamically changing Particle parameters.
Although not implemented (yet!), the code supports arbitrary swarm topologies

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cmath>
#include <cstddef>
#include <functional>
@@ -7,13 +8,37 @@
#include <vec.hpp>
#include <vector>
/**
* An Agent in a Particle Swarm Optimisation simulation.
* #T is the type of the position vector the Agent uses.
*/
template<typename T>
struct Agent {
/**
* Type of the fitness function.
*/
using F = std::function<float(T)>;
/**
* Moves the Agent along its velocity by a factor of #dt.
*
* The parameter #dt is useful when drawing the Agent moving faster than it
* #tick()s.
*
* @see: step()
*/
virtual void move(float dt) = 0;
/**
* Steps the algorithm values one tick forward.
* Warning: Does not move the Agent. Use #move() for this
* @see: move()
*/
virtual void step() = 0;
/**
* The best position this Agent has seen during its lifetime.
*/
virtual std::pair<float, T> best() const = 0;
Agent(F f) : f(f) {}
@@ -23,6 +48,8 @@ protected:
template <typename A>
concept ParameterChangeAlgorithm = requires(A alg, float f, unsigned int i) {
// initial viscosity, initial nostalgia, initial peer pressure, current
// iteration, max iteration
{ alg(f, f, f, i, i) } -> std::same_as<vec<3>>;
};
@@ -45,6 +72,20 @@ namespace ParamChange {
};
}
};
template<auto steepness = 3>
struct MTanhAlg {
vec<3> operator()(
float visc, float nostal, float peerp, int curr, int max) {
float pct_done = (float)curr / (float)max;
float scale_factor = (std::tanh(steepness * (pct_done - 0.5)) + 1) / 2;
return {
visc - (0.9f - 0.4f) * scale_factor,
nostal - (2.5f - 0.5f) * scale_factor,
peerp + (2.5f - 0.5f) * scale_factor,
};
}
};
};
template<std::size_t N, ParameterChangeAlgorithm A = ParamChange::MLinAlg>
@@ -54,6 +95,16 @@ struct Particle : public Agent<vec<N>> {
void move(float dt=1) override { position = position + velocity * dt; }
void step() override {
// Get the new parameter values
vec<3> params = alg(kviscosity, knostalgia, kpeer_pressure,
curr_iter, max_iter);
viscosity = params.x;
nostalgia = params.y;
peer_pressure = params.z;
curr_iter++;
velocity = viscosity * velocity
+ nostalgia * (pb_pos - position)
+ peer_pressure * (peer.best().second - position);
@@ -63,15 +114,6 @@ struct Particle : public Agent<vec<N>> {
pb = y;
pb_pos = position;
}
vec<3> params = alg(kviscosity, knostalgia, kpeer_pressure,
curr_iter, max_iter);
viscosity = params.x;
nostalgia = params.y;
peer_pressure = params.z;
curr_iter++;
};
const float kviscosity = 0.9f;
@@ -146,7 +188,7 @@ struct Swarm : public Agent<vec<N>> {
const std::vector<Particle<N>>& get_particles() { return particles; };
Swarm(Agent<vec<N>>::F f, unsigned max_iter = 150)
Swarm(Agent<vec<N>>::F f, unsigned max_iter)
: Agent<vec<N>>(f), max_iter(max_iter) {}
private:
unsigned max_iter;

View File

@@ -1,4 +1,4 @@
#include "screen.hpp"
#include <screen.hpp>
#include "vec.hpp"
#include <cmath>
#include <cstdio>
@@ -16,7 +16,13 @@ int
main(int argc, char **argv) {
int iter_count;
if(argc < 2) iter_count = 100;
else sscanf(argv[1], "%d", &iter_count);
else {
int scanned = sscanf(argv[1], "%d", &iter_count);
if(!scanned) {
printf("Usage: %s <iteration count>\n", argv[0]);
exit(1);
}
}
Swarm<2> swarm(f, iter_count);
Screen scr(80, 24);
@@ -63,19 +69,33 @@ main(int argc, char **argv) {
auto update_and_draw = [&]() -> void {
scr.draw();
auto [y, x] = swarm.best();
printf("\033[30;40m\33[2K\r\033[48;2;%d;%d;%dm"
"Current best: (%f, %f) = %f\n",
255, 255, 255, x.x, x.y, y);
};
// We draw to the screen at a rate of `kFPS', but step()ing the swarm at
// this rate would be far too fast to be interesting to look at. On the
// other hand, step()ing once a second is too slow.
//
// So, we step() every quarter second. To line up the move() so that a unit
// move() happens preceding each step(), we also need to move as fast as
// we've shortened our step() interval.
for(int i = 0; i < iter_count * kFPS/4;) {
if(!pause) {
scr.clear();
update_and_draw();
// Since we step() 4x a second, we need to move() 4x as fast as moving by dt
// each frame.
swarm.move(kDT * 4);
if(i % (kFPS/4) == (kFPS/4)-1)
swarm.step();
++i;
printf("Current iteration: %d\nCurrent frame: %d\n", i * 4 / kFPS, i);
}
if(frame_step) {
@@ -146,12 +166,15 @@ main(int argc, char **argv) {
update_and_draw();
break;
case 'p':
auto [y, x] = swarm.best();
printf("Current best: f(%.10f, %.10f) = %.10f", x.x, x.y, y);
case 'h':
printf(" movement zoom coloring pause step \n"
" W i I K SPC . \n"
" ASD o O L \n"
" quit: q \n");
break;
}
// Wait for kDT (time between two frames)
usleep(1000 * 1000 / kFPS);
}
@@ -161,7 +184,7 @@ cleanup:
enter_canonical_mode();
auto [y, x] = swarm.best();
printf("Best: f(" V2_FMT ") = %.3f", V2_ARG(x), y);
printf("Minimum found:\n f(%f, %f) = %f\n", x.x, x.y, y);
return 0;
}