diff --git a/cpp/functions/core/parallel_for.cpp b/cpp/functions/core/parallel_for.cpp new file mode 100644 index 00000000..009f8ea7 --- /dev/null +++ b/cpp/functions/core/parallel_for.cpp @@ -0,0 +1,136 @@ +#include "core/parallel_for.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace fn { + +struct ThreadPool::Impl { + std::vector workers; + std::queue> jobs; + std::mutex m; + std::condition_variable cv; + bool stop = false; + + int n_threads = 0; + + // Estado por parallel_for: + std::atomic active_jobs{0}; + std::mutex done_m; + std::condition_variable done_cv; + std::exception_ptr first_exc; + std::mutex exc_m; + + void worker_loop() { + for (;;) { + std::function job; + { + std::unique_lock lk(m); + cv.wait(lk, [this] { return stop || !jobs.empty(); }); + if (stop && jobs.empty()) return; + job = std::move(jobs.front()); + jobs.pop(); + } + try { + job(); + } catch (...) { + std::lock_guard lk(exc_m); + if (!first_exc) first_exc = std::current_exception(); + } + if (active_jobs.fetch_sub(1) == 1) { + std::lock_guard lk(done_m); + done_cv.notify_all(); + } + } + } +}; + +ThreadPool::ThreadPool(int n_threads) : impl_(new Impl) { + if (n_threads <= 0) { + n_threads = static_cast(std::thread::hardware_concurrency()); + if (n_threads <= 0) n_threads = 1; + } + impl_->n_threads = n_threads; + impl_->workers.reserve(static_cast(n_threads)); + for (int i = 0; i < n_threads; ++i) { + impl_->workers.emplace_back([this] { impl_->worker_loop(); }); + } +} + +ThreadPool::~ThreadPool() { + if (!impl_) return; + { + std::lock_guard lk(impl_->m); + impl_->stop = true; + } + impl_->cv.notify_all(); + for (auto& t : impl_->workers) { + if (t.joinable()) t.join(); + } + delete impl_; + impl_ = nullptr; +} + +int ThreadPool::n_threads() const { return impl_->n_threads; } + +void ThreadPool::parallel_for_chunks(std::size_t begin, std::size_t end, + const std::function& fn) { + if (end <= begin) return; + int N = impl_->n_threads; + std::size_t total = end - begin; + std::size_t chunk = (total + static_cast(N) - 1) / + static_cast(N); + int actual_chunks = 0; + for (int t = 0; t < N; ++t) { + std::size_t lo = begin + chunk * static_cast(t); + if (lo >= end) break; + std::size_t hi = lo + chunk; + if (hi > end) hi = end; + ++actual_chunks; + } + if (actual_chunks == 0) return; + + impl_->first_exc = nullptr; + impl_->active_jobs.store(actual_chunks); + + { + std::lock_guard lk(impl_->m); + for (int t = 0; t < N; ++t) { + std::size_t lo = begin + chunk * static_cast(t); + if (lo >= end) break; + std::size_t hi = lo + chunk; + if (hi > end) hi = end; + int tid = t; + impl_->jobs.emplace([&fn, tid, lo, hi] { fn(tid, lo, hi); }); + } + } + impl_->cv.notify_all(); + + { + std::unique_lock lk(impl_->done_m); + impl_->done_cv.wait(lk, [this] { + return impl_->active_jobs.load() == 0; + }); + } + + if (impl_->first_exc) { + std::exception_ptr e = impl_->first_exc; + impl_->first_exc = nullptr; + std::rethrow_exception(e); + } +} + +void ThreadPool::parallel_for(std::size_t begin, std::size_t end, + const std::function& fn) { + parallel_for_chunks(begin, end, + [&fn](int /*tid*/, std::size_t lo, std::size_t hi) { + for (std::size_t i = lo; i < hi; ++i) fn(i); + }); +} + +} // namespace fn diff --git a/cpp/functions/core/parallel_for.h b/cpp/functions/core/parallel_for.h new file mode 100644 index 00000000..c0e89950 --- /dev/null +++ b/cpp/functions/core/parallel_for.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +namespace fn { + +// Pool de threads opaco. Crea N workers en el constructor (default = hw +// concurrency); el destructor los joinea. Re-utilizable across multiples +// parallel_for; cada llamada bloquea hasta que todos los hilos terminan +// el rango. +class ThreadPool { +public: + explicit ThreadPool(int n_threads = 0); // 0 = std::thread::hardware_concurrency() + ~ThreadPool(); + + ThreadPool(const ThreadPool&) = delete; + ThreadPool& operator=(const ThreadPool&) = delete; + + int n_threads() const; + + // Ejecuta fn(i) para i en [begin, end), repartido entre los workers + // en chunks contiguos. Bloquea hasta finalizar. + // + // Si una excepcion se lanza en algun worker, parallel_for la captura + // y la relanza en el thread caller tras el join (la primera). + void parallel_for(std::size_t begin, std::size_t end, + const std::function& fn); + + // Ejecuta fn(thread_id, begin, end) por chunk — util cuando cada thread + // necesita acumular state local. fn recibe los limites del chunk. + void parallel_for_chunks(std::size_t begin, std::size_t end, + const std::function& fn); + +private: + struct Impl; + Impl* impl_; +}; + +} // namespace fn diff --git a/cpp/functions/core/parallel_for.md b/cpp/functions/core/parallel_for.md new file mode 100644 index 00000000..edccf69f --- /dev/null +++ b/cpp/functions/core/parallel_for.md @@ -0,0 +1,74 @@ +--- +name: parallel_for +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: impure +signature: "ThreadPool::ThreadPool(int n_threads = 0); void ThreadPool::parallel_for(size_t begin, size_t end, fn(i)); void ThreadPool::parallel_for_chunks(size_t begin, size_t end, fn(tid, lo, hi))" +description: "Pool de threads reutilizable con parallel_for por indice y parallel_for_chunks para acumulado por thread. Reparte rango contiguo por hw concurrency. Captura excepciones del worker y las relanza en el caller. Para Monte Carlo CPU multi-core." +tags: [thread, parallel, concurrency, montecarlo, core] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [thread, mutex, condition_variable, atomic, queue, vector, functional, exception] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/parallel_for.cpp" +params: + - name: n_threads + desc: "Numero de workers. 0 = hardware_concurrency. El destructor joinea todos." + - name: begin + desc: "Indice inicial del rango (inclusive)." + - name: end + desc: "Indice final del rango (exclusive). end <= begin es no-op." + - name: fn + desc: "Functor a ejecutar. parallel_for: void(size_t i). parallel_for_chunks: void(int tid, size_t lo, size_t hi) — el chunk [lo, hi) lo procesa el thread tid." +output: "El pool ejecuta el functor en paralelo y bloquea el caller hasta que todos los chunks terminan. Si algun worker lanza una excepcion, se captura la primera y se relanza desde el caller tras el join." +--- + +# parallel_for + +Pool minimo para Monte Carlo CPU. Una instancia mantiene workers vivos entre llamadas — barato hacer muchas N pequenas (no hay overhead de spawn por llamada). + +## Patron tipico — N sesiones independientes + +```cpp +fn::ThreadPool pool; // hardware_concurrency workers + +std::vector pnl(N); +pool.parallel_for(0, N, [&](std::size_t i) { + fn::ds::Rng r; + fn::ds::rng_seed(r, /*master=*/0xC0FFEE ^ i); // seed por sesion + pnl[i] = simulate_session(r); +}); +``` + +## Patron — accumulado por thread (sum / hist local) + +`parallel_for_chunks` evita atomic contention dando un slice contiguo a cada thread: + +```cpp +std::vector partials(pool.n_threads(), 0.0); + +pool.parallel_for_chunks(0, N, + [&](int tid, std::size_t lo, std::size_t hi) { + double local = 0.0; + for (std::size_t i = lo; i < hi; ++i) { + local += compute(i); + } + partials[tid] = local; + }); + +double total = std::accumulate(partials.begin(), partials.end(), 0.0); +``` + +## Notas + +- **Throughput**: 8 cores @ 4 GHz, jobs de ~10us cada uno => ~5x speedup tipico (Amdahl: lo bloquea el job mas largo + cv-wait coste). Para jobs <1us el coste de submit domina; usar `parallel_for_chunks` con chunk grande. +- **Excepciones**: solo se preserva la PRIMERA excepcion. Las demas se silencian (con la garantia de que el resto de los workers terminan limpiamente). +- **No re-entrante**: un worker no puede llamar `parallel_for` sobre el mismo pool — bloquearia el done_cv. Si se necesita anidamiento, crear un segundo pool. +- **Comparar con GPU**: para 10^6 sesiones de 10^4 spins (= 10^10 ops MC), CPU 8-core ~10s, GPU ~1-2s. CPU pool es la fallback portable; GPU es para el caso extremo. diff --git a/cpp/functions/core/slider.cpp b/cpp/functions/core/slider.cpp new file mode 100644 index 00000000..95fb5d20 --- /dev/null +++ b/cpp/functions/core/slider.cpp @@ -0,0 +1,80 @@ +#include "core/slider.h" +#include "core/tokens.h" +#include +#include + +namespace fn_ui { + +static void push_slider_style() { + using namespace fn_tokens; + ImGui::PushStyleColor(ImGuiCol_FrameBg, colors::bg); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, colors::surface_hover); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, colors::surface); + ImGui::PushStyleColor(ImGuiCol_Border, colors::border); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, colors::primary); + ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, colors::primary_hover); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, radius::sm); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, radius::sm); +} + +static void pop_slider_style() { + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(6); +} + +static void label_muted(const char* label) { + using namespace fn_tokens; + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::TextUnformatted(label); + ImGui::PopStyleColor(); +} + +bool slider_float(const char* label, float* value, + float min_v, float max_v, const char* fmt) { + label_muted(label); + push_slider_style(); + char id[160]; + std::snprintf(id, sizeof(id), "##%s", label); + ImGui::SetNextItemWidth(-FLT_MIN); + bool changed = ImGui::SliderFloat(id, value, min_v, max_v, fmt); + pop_slider_style(); + return changed; +} + +bool slider_float_log(const char* label, float* value, + float min_v, float max_v, const char* fmt) { + label_muted(label); + push_slider_style(); + char id[160]; + std::snprintf(id, sizeof(id), "##%s", label); + ImGui::SetNextItemWidth(-FLT_MIN); + bool changed = ImGui::SliderFloat(id, value, min_v, max_v, fmt, + ImGuiSliderFlags_Logarithmic); + pop_slider_style(); + return changed; +} + +bool slider_int(const char* label, int* value, + int min_v, int max_v, const char* fmt) { + label_muted(label); + push_slider_style(); + char id[160]; + std::snprintf(id, sizeof(id), "##%s", label); + ImGui::SetNextItemWidth(-FLT_MIN); + bool changed = ImGui::SliderInt(id, value, min_v, max_v, fmt); + pop_slider_style(); + return changed; +} + +bool slider_double(const char* label, double* value, + double min_v, double max_v, const char* fmt) { + float fv = static_cast(*value); + bool changed = slider_float(label, &fv, + static_cast(min_v), + static_cast(max_v), fmt); + if (changed) *value = static_cast(fv); + return changed; +} + +} // namespace fn_ui diff --git a/cpp/functions/core/slider.h b/cpp/functions/core/slider.h new file mode 100644 index 00000000..7396a58d --- /dev/null +++ b/cpp/functions/core/slider.h @@ -0,0 +1,38 @@ +#pragma once + +// Slider con label muted arriba + value display, estilo acorde con fn_tokens. +// Equivalente simple de de Mantine / fn_library. +// +// Uso: +// static float p = 0.2f; +// fn_ui::slider_float("Probabilidad de pagar", &p, 0.0f, 1.0f); +// +// static int n = 10; +// fn_ui::slider_int("Spins por sesion", &n, 1, 100); + +namespace fn_ui { + +// Renderiza label arriba + slider abajo. fmt es el printf-format que +// ImGui usa para el value display (default "%.3f"). Devuelve true si el +// valor cambio este frame. +bool slider_float(const char* label, float* value, + float min_v, float max_v, + const char* fmt = "%.3f"); + +// Logaritmico: util para parametros que cubren ordenes de magnitud +// (sigma, learning rate, ...). +bool slider_float_log(const char* label, float* value, + float min_v, float max_v, + const char* fmt = "%.4f"); + +bool slider_int(const char* label, int* value, + int min_v, int max_v, + const char* fmt = "%d"); + +// Variante double: ImGui no tiene SliderDouble, asi que internamente +// down-cast a float — ojo si tu rango requiere precision fp64. +bool slider_double(const char* label, double* value, + double min_v, double max_v, + const char* fmt = "%.6f"); + +} // namespace fn_ui diff --git a/cpp/functions/core/slider.md b/cpp/functions/core/slider.md new file mode 100644 index 00000000..ca6deadc --- /dev/null +++ b/cpp/functions/core/slider.md @@ -0,0 +1,66 @@ +--- +name: slider +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: impure +signature: "bool slider_float(const char* label, float* v, float min, float max, const char* fmt); bool slider_float_log(...); bool slider_int(const char* label, int* v, int min, int max, const char* fmt); bool slider_double(const char* label, double* v, double min, double max, const char* fmt)" +description: "Slider ImGui con label muted arriba, estilo acorde con fn_tokens (radius, border, primary grab). Variantes float, float_log (logaritmico), int, double. Equivalente al de Mantine / fn_library." +tags: [imgui, slider, ui, tokens, mantine, core] +uses_functions: ["tokens_cpp_core"] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [imgui.h, cstdio] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/slider.cpp" +framework: imgui +params: + - name: label + desc: "Texto del label (mostrado en text_muted arriba). Tambien se usa como id ImGui (concatenado con ##)." + - name: value + desc: "Pointer al valor; mutado in-place si el usuario lo cambia." + - name: min_v + desc: "Limite inferior." + - name: max_v + desc: "Limite superior." + - name: fmt + desc: "printf-format para el value display. Float default '%.3f'; int default '%d'." +output: "Renderiza label + slider full-width. Devuelve true si el valor cambio este frame." +--- + +# slider + +Slider canonico para los calculadores. Cada calculadora del set tiene 15-30 sliders; tener uno consistente con tokens y full-width acelera el desarrollo y mantiene la identidad visual. + +## Patron tipico + +```cpp +static float prob = 0.2f; +if (fn_ui::slider_float("Probabilidad de hit", &prob, 0.0f, 1.0f, "%.3f")) { + recalculate(); +} + +static float sigma = 0.1f; +fn_ui::slider_float_log("Proposal sigma (log)", &sigma, 0.001f, 10.0f, "%.4f"); + +static int n_chains = 4; +fn_ui::slider_int("Numero de cadenas", &n_chains, 1, 16); + +static double mu = 0.0; +fn_ui::slider_double("Mu", &mu, -10.0, 10.0, "%.6f"); +``` + +## Estilo + +Usa `fn_tokens::colors::primary` para el grab (mismo color que `button` primary). Background `bg`, border `border`, radius `sm`. Coincide con la identidad de `text_input` y demas widgets `core`. + +## Notas + +- `slider_double` down-castea a float internamente (ImGui no expone SliderDouble). Si tu rango requiere fp64 usa `text_input` con parsing manual. +- El label es tambien el id ImGui — duplicar labels en el mismo frame causa colision (ImGui asigna ids unicos basados en label). Si necesitas dos sliders con el mismo nombre visible, agregar un sufijo distinto (ej. "Mu##chain1" / "Mu##chain2"). +- Para multi-value (vec2/vec3) — no incluido aqui; usar 2/3 sliders separados o ImGui::SliderFloat2/3 directamente.