From ae00a40166e872e08247e55035884731994641bc Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Wed, 21 Feb 2024 08:06:17 -0700 Subject: [PATCH 001/224] bump deps --- doc/depstree.txt | 26 +++++++++++++------------- project.clj | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/depstree.txt b/doc/depstree.txt index 0c23dc99..ce25a038 100644 --- a/doc/depstree.txt +++ b/doc/depstree.txt @@ -34,8 +34,8 @@ [mvxcvi/arrangement "2.1.0"] [org.clojure/test.check "1.1.1"] [nrepl "0.8.3" :exclusions [[org.clojure/clojure]]] - [org.apache.logging.log4j/log4j-api "2.22.1"] - [org.apache.logging.log4j/log4j-core "2.22.1"] + [org.apache.logging.log4j/log4j-api "2.23.0"] + [org.apache.logging.log4j/log4j-core "2.23.0"] [org.clojure/clojure "1.11.1"] [org.clojure/core.specs.alpha "0.2.62"] [org.clojure/spec.alpha "0.3.218"] @@ -65,7 +65,7 @@ [org.junit.jupiter/junit-jupiter-engine "5.10.1" :scope "runtime"] [org.junit.platform/junit-platform-engine "1.10.1" :scope "runtime"] [org.junit.jupiter/junit-jupiter-params "5.10.1"] - [org.matheclipse/matheclipse-core "3.1.0-20240219.223701-64" :exclusions [[org.slf4j/slf4j-api] [com.fasterxml.jackson.core/jackson-core]]] + [org.matheclipse/matheclipse-core "3.1.0-20240220.194259-65" :exclusions [[org.slf4j/slf4j-api] [com.fasterxml.jackson.core/jackson-core]]] [com.esotericsoftware/kryo "5.6.0"] [com.esotericsoftware/minlog "1.3.1"] [com.esotericsoftware/reflectasm "1.11.9"] @@ -84,20 +84,20 @@ [org.codehaus.janino/janino "3.1.12"] [org.codehaus.janino/commons-compiler "3.1.12"] [org.gavaghan/geodesy "1.1.3"] - [org.hipparchus/hipparchus-clustering "3.1-20240212.230522-23"] - [org.hipparchus/hipparchus-core "3.1-20240212.230522-23"] - [org.hipparchus/hipparchus-stat "3.1-20240212.230522-23"] - [org.hipparchus/hipparchus-fft "3.1-20240212.230522-23"] - [org.hipparchus/hipparchus-fitting "3.1-20240212.230522-23"] - [org.hipparchus/hipparchus-ode "3.1-20240212.230522-23"] - [org.hipparchus/hipparchus-optim "3.1-20240212.230522-23"] + [org.hipparchus/hipparchus-clustering "3.1-20240221.085047-25"] + [org.hipparchus/hipparchus-core "3.1-20240221.085047-25"] + [org.hipparchus/hipparchus-stat "3.1-20240221.085047-25"] + [org.hipparchus/hipparchus-fft "3.1-20240221.085047-25"] + [org.hipparchus/hipparchus-fitting "3.1-20240221.085047-25"] + [org.hipparchus/hipparchus-ode "3.1-20240221.085047-25"] + [org.hipparchus/hipparchus-optim "3.1-20240221.085047-25"] [org.jgrapht/jgrapht-io "1.5.3-20240118.050300-19"] [org.antlr/antlr4-runtime "4.13.1"] [org.apache.commons/commons-text "1.11.0"] [org.jgrapht/jgrapht-core "1.5.3-20240118.050300-19"] [org.jheaps/jheaps "0.14"] [org.logicng/logicng-j11 "2.4.3"] - [org.matheclipse/matheclipse-external "3.1.0-20240219.223701-64"] + [org.matheclipse/matheclipse-external "3.1.0-20240220.194259-65"] [com.fasterxml.jackson.core/jackson-databind "2.16.1"] [com.fasterxml.jackson.core/jackson-annotations "2.16.1"] [com.google.guava/guava "33.0.0-jre"] @@ -109,10 +109,10 @@ [org.checkerframework/checker-qual "3.41.0"] [io.pebbletemplates/pebble "3.2.2"] [org.unbescape/unbescape "1.1.6.RELEASE"] - [org.matheclipse/matheclipse-parser "3.1.0-20240219.223701-64"] + [org.matheclipse/matheclipse-parser "3.1.0-20240220.194259-65"] [org.organicdesign/Paguro "3.10.3"] [org.jetbrains/annotations "23.0.0"] - [org.matheclipse/matheclipse-gpl "3.1.0-20240219.223701-64" :exclusions [[org.slf4j/slf4j-api] [com.fasterxml.jackson.core/jackson-core]]] + [org.matheclipse/matheclipse-gpl "3.1.0-20240220.194259-65" :exclusions [[org.slf4j/slf4j-api] [com.fasterxml.jackson.core/jackson-core]]] [org.apache.logging.log4j/log4j-1.2-api "2.22.1"] [org.slf4j/jcl-over-slf4j "2.0.12"] [org.slf4j/slf4j-api "2.0.12"] diff --git a/project.clj b/project.clj index b52ec81b..f30c6d24 100644 --- a/project.clj +++ b/project.clj @@ -22,8 +22,8 @@ [org.slf4j/jcl-over-slf4j "2.0.12"] [org.slf4j/slf4j-api "2.0.12"] - [org.apache.logging.log4j/log4j-core "2.22.1"] - [org.apache.logging.log4j/log4j-api "2.22.1"] + [org.apache.logging.log4j/log4j-core "2.23.0"] + [org.apache.logging.log4j/log4j-api "2.23.0"] [org.matheclipse/matheclipse-core "3.1.0-SNAPSHOT" :exclusions [org.slf4j/slf4j-api com.fasterxml.jackson.core/jackson-core]] From 43a24489e77623504f4e95e4f06f87c81bd63600 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Wed, 21 Feb 2024 08:12:38 -0700 Subject: [PATCH 002/224] stabilize a couple tests --- test/closyr/symbolic_regression_test.clj | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index 3d0664b4..9ea436d1 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -145,6 +145,7 @@ 50)))) (testing "with provided data" + (reset! symreg/sim-input-args* {}) (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 10)} (fn [] (let [{:keys [final-population next-step iters-done]} @@ -172,11 +173,10 @@ (is (= (set (keys @symreg/sim-input-args*)) #{:input-xs-exprs :input-xs-vec - :input-ys-vec})) - - (reset! symreg/sim-input-args* {}))))) + :input-ys-vec})))))) (testing "with provided data using record" + (reset! symreg/sim-input-args* {}) (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 10)} (fn [] (let [{:keys [final-population next-step iters-done]} @@ -204,9 +204,7 @@ (is (= (set (keys @symreg/sim-input-args*)) #{:input-xs-exprs :input-xs-vec - :input-ys-vec})) - - (reset! symreg/sim-input-args* {}))))))) + :input-ys-vec})))))))) #_(deftest can-run-experiment-gui:start-stop @@ -243,10 +241,15 @@ (when (not (GraphicsEnvironment/isHeadless)) (binding [ops/*print-top-n* 1] (testing "gui can start and restart experiments; NOTE: do not run this while in headless mode, eg on CI" + (reset! symreg/sim-input-args* {}) (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 500)} (fn [] (let [control-process (go + + (is (= (set (keys @symreg/sim-input-args*)) + #{})) + ( Date: Wed, 21 Feb 2024 08:58:04 -0700 Subject: [PATCH 003/224] rename some high-level functions/records/protocols to use 'find formula' verbage --- src/closyr/symbolic_regression.clj | 20 ++++++++++---------- test/closyr/ops_common_test.clj | 11 +++++++---- test/closyr/symbolic_regression_test.clj | 6 +++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index b4a22a12..86dd2077 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -432,7 +432,7 @@ (reset! ops/test-timer* start))) -(defprotocol ISolverStateController +(defprotocol IIterativeGASolver "Interface which allows creation and iteration of the symbolic regression GA solver" @@ -458,12 +458,12 @@ "Report timing/perf results")) -(defrecord SolverStateController +(defrecord IterativeGASolver [;; the chans? also these names are really really ambiguous and overloaded: run-config run-args] - ISolverStateController + IIterativeGASolver (init [this] @@ -541,11 +541,11 @@ return-value)) -(defn run-ga-iterations-using-record +(defn run-solver-ga-iterations "Run GA evolution iterations on initial population" {:malli/schema [:=> [:cat #'specs/SolverRunConfig #'specs/SolverRunArgs] #'specs/SolverRunResults]} [run-config run-args] - (loop [solver-state (init (map->SolverStateController {:run-config run-config :run-args run-args}))] + (loop [solver-state (init (map->IterativeGASolver {:run-config run-config :run-args run-args}))] (let [[recur? next-solver-state] (run-iteration solver-state)] (if recur? (recur next-solver-state) @@ -581,7 +581,7 @@ sim-stop-start-chan sim->gui-chan] :as run-args}] (let [run-config (merge-cli-and-gui-args run-config run-args) - {:keys [next-step] :as completed-ga-data} (run-ga-iterations-using-record run-config run-args)] + {:keys [next-step] :as completed-ga-data} (run-solver-ga-iterations run-config run-args)] (case next-step @@ -621,7 +621,7 @@ run-config))) -(defprotocol ISymbolicRegressionSolver +(defprotocol ISymbolicRegressionFindFormula "A top-level interface to start the solver using CLI or GUI args" @@ -634,10 +634,10 @@ and defaults.")) -(defrecord SymbolicRegressionSolver +(defrecord SymbolicRegressionFindFormula [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui? use-flamechart max-leafs] - ISymbolicRegressionSolver + ISymbolicRegressionFindFormula (solve [this] @@ -665,7 +665,7 @@ "Run a GA evolution solver to search for function of best fit for input data. The word experiment is used loosely here, it's more of a time-evolving best-fit method instance." [{:keys [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui?] :as run-config}] - (solve (map->SymbolicRegressionSolver run-config))) + (solve (map->SymbolicRegressionFindFormula run-config))) (defn run-app-without-gui diff --git a/test/closyr/ops_common_test.clj b/test/closyr/ops_common_test.clj index fae2364b..4c4ef8d3 100644 --- a/test/closyr/ops_common_test.clj +++ b/test/closyr/ops_common_test.clj @@ -74,6 +74,7 @@ (is (instance? IAST parsed)) (is (= (str parsed) (str (F/Cos x)))))) + (testing "can parse simple fn str 2" (let [x (F/Dummy "x") parsed (.parse (ops-common/new-eval-engine) "x+Cos(x^2)")] @@ -95,7 +96,8 @@ {:expr "1/5+x" :simple? true})) (is (= @ops-common/do-not-simplify-fns* - {"1/5+Sin(ArcSin(x))" 1})))))))) + {"1/5+Sin(ArcSin(x))" 1})) + (reset! ops-common/do-not-simplify-fns* {}))))))) (deftest simplify-with-ignore-presets @@ -107,8 +109,8 @@ {:expr "x" :simple? true})) (is (= @ops-common/do-not-simplify-fns* - {"x" 2}))) - (reset! ops-common/do-not-simplify-fns* {}))) + {"x" 2})) + (reset! ops-common/do-not-simplify-fns* {})))) (deftest simplify-test @@ -143,7 +145,8 @@ (F/Plus (F/Cos x)) (F/Plus (F/Sqrt x)))}) :expr str)) - {:expr "1+Sqrt(x)+Cos(x)+Sin(x)+2*(Cos(ArcSin(x))+Tan(ArcSin(x)))"})))) + {:expr "1+Sqrt(x)+Cos(x)+Sin(x)+2*(Cos(ArcSin(x))+Tan(ArcSin(x)))"})) + (reset! ops-common/do-not-simplify-fns* {}))) (testing "simplify sum" (let [x (F/Dummy "x")] diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index 9ea436d1..7b02b212 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -34,7 +34,7 @@ (binding [ops/*print-top-n* 1] (is (= (dissoc - (with-redefs-fn {#'symreg/run-ga-iterations-using-record + (with-redefs-fn {#'symreg/run-solver-ga-iterations (fn [run-config run-args] (reset! args* [(dissoc run-config :initial-muts :initial-phenos :input-xs-exprs :input-ys-exprs) @@ -79,7 +79,7 @@ (binding [ops/*print-top-n* 1] (is (= (dissoc - (with-redefs-fn {#'symreg/run-ga-iterations-using-record + (with-redefs-fn {#'symreg/run-solver-ga-iterations (fn [run-config run-args] (reset! args* [(dissoc run-config :initial-muts :initial-phenos :input-xs-exprs :input-ys-exprs) @@ -181,7 +181,7 @@ (fn [] (let [{:keys [final-population next-step iters-done]} (symreg/solve - (symreg/map->SymbolicRegressionSolver + (symreg/map->SymbolicRegressionFindFormula {:input-phenos-count 100 :initial-muts (ops-init/initial-mutations) :iters 5 From 39fbd8ce92f465e2fb360123159b45242f588ba2 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Wed, 21 Feb 2024 12:13:58 -0700 Subject: [PATCH 004/224] more renaming --- src/closyr/symbolic_regression.clj | 8 ++++---- test/closyr/symbolic_regression_test.clj | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index 86dd2077..f3f0054b 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -661,7 +661,7 @@ (symbolic-regression-solver-fn))))) -(defn run-solver +(defn run-find-formula "Run a GA evolution solver to search for function of best fit for input data. The word experiment is used loosely here, it's more of a time-evolving best-fit method instance." [{:keys [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui?] :as run-config}] @@ -671,7 +671,7 @@ (defn run-app-without-gui "Run app without GUI and with fake placeholder input data" [xs ys] - (run-solver + (run-find-formula {:initial-phenos (ops-init/initial-phenotypes 100) :initial-muts (ops-init/initial-mutations) :iters 20 @@ -685,7 +685,7 @@ ([] (run-app-with-gui {:use-flamechart false})) ([{:keys [use-flamechart]}] - (run-solver + (run-find-formula {:initial-phenos (ops-init/initial-phenotypes 50) :initial-muts (ops-init/initial-mutations) :iters 100 @@ -723,7 +723,7 @@ :input-ys-exprs (if ys (ops-common/doubles->exprs ys) example-input-ys-exprs)} - result (run-solver run-config)] + result (run-find-formula run-config)] (log/info "CLI: Done!") (exit cli-opts) result)) diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index 7b02b212..bec120f7 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -127,7 +127,7 @@ 100))) (testing "with gui launcher" - (let [res (with-redefs-fn {#'symreg/run-solver (fn [args] args)} + (let [res (with-redefs-fn {#'symreg/run-find-formula (fn [args] args)} (fn [] (#'symreg/run-app-with-gui)))] (is (= (dissoc res :initial-phenos :initial-muts :input-xs-exprs :input-ys-exprs) @@ -149,7 +149,7 @@ (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 10)} (fn [] (let [{:keys [final-population next-step iters-done]} - (symreg/run-solver + (symreg/run-find-formula {:input-phenos-count 100 :initial-muts (ops-init/initial-mutations) :iters 5 @@ -225,7 +225,7 @@ (is (put! symreg/*sim->gui-chan* :next)) true)] - (symreg/run-solver + (symreg/run-find-formula {:initial-phenos (ops-init/initial-phenotypes 20) :initial-muts (ops-init/initial-mutations) :input-xs-exprs symreg/example-input-xs-exprs @@ -314,7 +314,7 @@ (is (put! symreg/*sim->gui-chan* :next)) true)] - (symreg/run-solver + (symreg/run-find-formula {:initial-phenos (ops-init/initial-phenotypes 20) :initial-muts (ops-init/initial-mutations) :input-xs-exprs symreg/example-input-xs-exprs From 635b1d9c4037f2e86bfd8552fbcd815fa85cdae2 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Thu, 22 Feb 2024 10:24:37 -0700 Subject: [PATCH 005/224] java class scaffold --- project.clj | 1 + .../java/org/closyr/core/FindFunction.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/main/java/org/closyr/core/FindFunction.java diff --git a/project.clj b/project.clj index f30c6d24..b5272785 100644 --- a/project.clj +++ b/project.clj @@ -33,6 +33,7 @@ :repositories {"snapshots-repo" {:url "https://oss.sonatype.org/content/repositories/snapshots" :releases false :snapshots true}} + :java-source-paths ["src/main/java"] :main ^:skip-aot closyr.core :target-path "target/%s" :profiles {:uberjar {:aot :all diff --git a/src/main/java/org/closyr/core/FindFunction.java b/src/main/java/org/closyr/core/FindFunction.java new file mode 100644 index 00000000..3362de02 --- /dev/null +++ b/src/main/java/org/closyr/core/FindFunction.java @@ -0,0 +1,20 @@ +package org.closyr.core; + +import org.matheclipse.core.eval.EvalEngine; +import org.matheclipse.core.eval.interfaces.AbstractFunctionEvaluator; +import org.matheclipse.core.interfaces.IAST; +import org.matheclipse.core.interfaces.IExpr; + +public class FindFunction extends AbstractFunctionEvaluator { + + /** + * {@inheritDoc} + * + * @param ast + * @param engine + */ + @Override + public IExpr evaluate(IAST ast, EvalEngine engine) { + return null; + } +} From 3abc4ae747b210cbd7c523e6d27f7fb8a9217c45 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Thu, 22 Feb 2024 10:25:48 -0700 Subject: [PATCH 006/224] java class scaffold --- .../org/closyr/core/{FindFunction.java => FindFormula.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/org/closyr/core/{FindFunction.java => FindFormula.java} (87%) diff --git a/src/main/java/org/closyr/core/FindFunction.java b/src/main/java/org/closyr/core/FindFormula.java similarity index 87% rename from src/main/java/org/closyr/core/FindFunction.java rename to src/main/java/org/closyr/core/FindFormula.java index 3362de02..46035a90 100644 --- a/src/main/java/org/closyr/core/FindFunction.java +++ b/src/main/java/org/closyr/core/FindFormula.java @@ -5,7 +5,7 @@ import org.matheclipse.core.interfaces.IAST; import org.matheclipse.core.interfaces.IExpr; -public class FindFunction extends AbstractFunctionEvaluator { +public class FindFormula extends AbstractFunctionEvaluator { /** * {@inheritDoc} From 22e17a71df3ed338398e86a1c5269b9ec4b16321 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Thu, 22 Feb 2024 10:37:05 -0700 Subject: [PATCH 007/224] java test class scaffold --- test/org/closyr/core/FindFormulaTest.java | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test/org/closyr/core/FindFormulaTest.java diff --git a/test/org/closyr/core/FindFormulaTest.java b/test/org/closyr/core/FindFormulaTest.java new file mode 100644 index 00000000..2ab6d266 --- /dev/null +++ b/test/org/closyr/core/FindFormulaTest.java @@ -0,0 +1,22 @@ +package org.closyr.core; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class FindFormulaTest { + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + } + + @Test + void evaluate() { + } +} From 75893cd41c907c77318127a7210d80de1222939b Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Thu, 22 Feb 2024 17:15:52 -0700 Subject: [PATCH 008/224] FindFormula template from symja --- .../java/org/closyr/core/FindFormula.java | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/closyr/core/FindFormula.java b/src/main/java/org/closyr/core/FindFormula.java index 46035a90..fdb34cd0 100644 --- a/src/main/java/org/closyr/core/FindFormula.java +++ b/src/main/java/org/closyr/core/FindFormula.java @@ -1,20 +1,72 @@ package org.closyr.core; +import org.matheclipse.core.eval.Errors; import org.matheclipse.core.eval.EvalEngine; import org.matheclipse.core.eval.interfaces.AbstractFunctionEvaluator; +import org.matheclipse.core.eval.interfaces.AbstractFunctionOptionEvaluator; +import org.matheclipse.core.eval.interfaces.IFunctionEvaluator; +import org.matheclipse.core.expression.F; +import org.matheclipse.core.expression.ImplementationStatus; +import org.matheclipse.core.expression.S; import org.matheclipse.core.interfaces.IAST; import org.matheclipse.core.interfaces.IExpr; +import org.matheclipse.core.interfaces.ISymbol; -public class FindFormula extends AbstractFunctionEvaluator { - /** - * {@inheritDoc} - * - * @param ast - * @param engine - */ + +public class FindFormula extends AbstractFunctionOptionEvaluator { + + public FindFormula() { + // empty constructor + } + + + @Override + public IExpr evaluate(IAST ast, final int argSize, final IExpr[] options, final EvalEngine engine, + IAST originalAST) { + IExpr data = ast.arg1(); + IExpr x = ast.arg2(); + if (x.isVariable()) { + int n = Integer.MAX_VALUE; + IExpr arg4 = S.All; + + int[] isMatrix = data.isMatrix(); + if (isMatrix != null && isMatrix[1] == 2 && data.isList()) { + IAST matrix = (IAST) data; + double[][] doubleMatrix = matrix.toDoubleMatrix(); + if (doubleMatrix != null) { + if (ast.argSize() > 2) { + n = ast.arg3().toIntDefault(); + if (n < 0) { + // Positive machine-sized integer expected at position `2` in `1`. +// return Errors.printMessage(S.FindFormula, "intpm", F.List(F.C3, ast), engine); + } + if (ast.argSize() > 3) { + arg4 = ast.arg4(); + } + } + // TODO add implementation here + + + } + } + } + return F.NIL; + } + + @Override + public int status() { + return ImplementationStatus.EXPERIMENTAL; + } + + @Override + public int[] expectedArgSize(IAST ast) { + return IFunctionEvaluator.ARGS_2_4; + } + @Override - public IExpr evaluate(IAST ast, EvalEngine engine) { - return null; + public void setUp(final ISymbol newSymbol) { + super.setUp(newSymbol); + // setOptions(newSymbol, S.GenerateConditions, S.False); } } From 73dbab5aa4ef5f8ed94fb911451411603bf239a5 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 17 Mar 2025 10:21:17 -0600 Subject: [PATCH 009/224] bump deps; fix tests --- project.clj | 8 ++++---- test/closyr/ops_modify_test.clj | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/project.clj b/project.clj index b5272785..2c436a03 100644 --- a/project.clj +++ b/project.clj @@ -5,7 +5,7 @@ :url "https://www.eclipse.org/legal/epl-2.0/"} :dependencies [[org.clojure/clojure "1.11.1"] [org.clojure/core.async "1.6.681"] - [metosin/malli "0.14.0"] + [metosin/malli "0.17.0"] [org.clojure/tools.cli "1.1.230"] @@ -13,7 +13,7 @@ [seesaw/seesaw "1.5.0"] [flames "0.5.0"] - [org.knowm.xchart/xchart "3.8.7"] + [org.knowm.xchart/xchart "3.8.8"] ;; [com.github.weisj/darklaf-core "3.0.2"] [io.github.vincenzopalazzo/material-ui-swing "1.1.4"] [io.github.material-ui-swing/DarkStackOverflowTheme "0.0.1-rc3"] @@ -44,8 +44,8 @@ [lein-vanity "0.2.0"] [lein-nomis-ns-graph "0.14.6"] [lein-ancient "0.7.0"] - [jonase/eastwood "1.4.2"] - [lein-kibit "0.1.8"] + [jonase/eastwood "1.4.3"] + [lein-kibit "0.1.11"] [lein-bikeshed "0.5.2"] [venantius/yagni "0.1.7"] [lein-check-namespace-decls "1.0.4"] diff --git a/test/closyr/ops_modify_test.clj b/test/closyr/ops_modify_test.clj index c0c19348..a06fddc8 100644 --- a/test/closyr/ops_modify_test.clj +++ b/test/closyr/ops_modify_test.clj @@ -598,10 +598,10 @@ "-1-x+Cos(x)/Sqrt(-x)+x*Sin(1/2+x)"] [:modify-leafs "1.1*x" - "-1+1.1*x+(0.9534625892455922*Cos(1.1*x))/Sqrt(x)-1.1*x*Sin(1/2-1.1*x)"] + "-1+1.1*x+(0.953463*Cos(1.1*x))/Sqrt(x)-1.1*x*Sin(1/2-1.1*x)"] [:modify-leafs "0.9*x" - "-1+0.9*x+(1.0540925533894598*Cos(0.9*x))/Sqrt(x)-0.9*x*Sin(1/2-0.9*x)"] + "-1+0.9*x+(1.05409*Cos(0.9*x))/Sqrt(x)-0.9*x*Sin(1/2-0.9*x)"] [:modify-leafs "sin(x)" "-1+Cos(Sin(x))/Sqrt(Sin(x))+Sin(x)-Sin(x)*Sin(1/2-Sin(x))"] @@ -739,16 +739,16 @@ "1-x+Cos(x)/Sqrt(x)-x*Sin(1/2+x)"] [:modify-branches "b*1.1" - "1.1*(-1+x+(1.3310000000000004*Cos(x))/Sqrt(x)-1.2100000000000002*x*Sin(1.1*(1/2-1.1*x)))"] + "1.1*(-1+x+(1.331*Cos(x))/Sqrt(x)-1.21*x*Sin(1.1*(1/2-1.1*x)))"] [:modify-branches "b*0.9" - "0.9*(-1+x+(0.7290000000000001*Cos(x))/Sqrt(x)-0.81*x*Sin(0.9*(1/2-0.9*x)))"] + "0.9*(-1+x+(0.729*Cos(x))/Sqrt(x)-0.81*x*Sin(0.9*(1/2-0.9*x)))"] [:modify-branches "b+0.1" - "-0.7000000000000001+x+(0.1+1/Sqrt(x))*(0.1+Cos(x))-x*(0.1+Sin(0.7-x))"] + "-0.7+x+(0.1+1/Sqrt(x))*(0.1+Cos(x))-x*(0.1+Sin(0.7-x))"] [:modify-branches "b-0.1" - "-1.3000000000000003+x+(-0.1+1/Sqrt(x))*(-0.1+Cos(x))+x*(0.1-Sin(0.30000000000000004-x))"]]) + "-1.3+x+(-0.1+1/Sqrt(x))*(-0.1+Cos(x))+x*(0.1-Sin(0.3-x))"]]) (deftest mutations-test From ac697929893dea9008727fb202879d66b23aeed0 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 10:52:10 -0700 Subject: [PATCH 010/224] bump deps --- project.clj | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/project.clj b/project.clj index 2c436a03..174a8c62 100644 --- a/project.clj +++ b/project.clj @@ -3,11 +3,11 @@ :url "http://example.com/FIXME" :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" :url "https://www.eclipse.org/legal/epl-2.0/"} - :dependencies [[org.clojure/clojure "1.11.1"] - [org.clojure/core.async "1.6.681"] - [metosin/malli "0.17.0"] + :dependencies [[org.clojure/clojure "1.12.4"] + [org.clojure/core.async "1.8.741"] + [metosin/malli "0.20.0"] - [org.clojure/tools.cli "1.1.230"] + [org.clojure/tools.cli "1.2.245"] [org.clojure/data.csv "1.1.0"] @@ -18,12 +18,12 @@ [io.github.vincenzopalazzo/material-ui-swing "1.1.4"] [io.github.material-ui-swing/DarkStackOverflowTheme "0.0.1-rc3"] - [ch.qos.logback/logback-classic "1.5.0"] - [org.slf4j/jcl-over-slf4j "2.0.12"] + [ch.qos.logback/logback-classic "1.5.23"] + [org.slf4j/jcl-over-slf4j "2.0.17"] - [org.slf4j/slf4j-api "2.0.12"] - [org.apache.logging.log4j/log4j-core "2.23.0"] - [org.apache.logging.log4j/log4j-api "2.23.0"] + [org.slf4j/slf4j-api "2.0.17"] + [org.apache.logging.log4j/log4j-core "2.25.3"] + [org.apache.logging.log4j/log4j-api "2.25.3"] [org.matheclipse/matheclipse-core "3.1.0-SNAPSHOT" :exclusions [org.slf4j/slf4j-api com.fasterxml.jackson.core/jackson-core]] From 79362e270c186c9ffcba8f69e4a5772e56f7d847 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 11:26:54 -0700 Subject: [PATCH 011/224] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05659296..e09cb02f 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ You can also provide the same command-line options to `java` command, like: ## Example Screenshots -On successful application start, you can start an run a search. You might see something like this when done: +On successful application start, you can start and run a search. You might see something like this when done: ![gui_done_2024-02-12_08-44.png](screenshots%2Fgui_done_2024-02-12_08-44.png) From 3036b4f88da99fef867a4f2dfa419424524be156 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 12:34:14 -0700 Subject: [PATCH 012/224] perf-oriented JVM opts + perf profile scripts --- perf-profile/README.md | 7 ++++ perf-profile/benchmark.clj | 49 ++++++++++++++++++++++++++++ perf-profile/profile_eval.clj | 42 ++++++++++++++++++++++++ perf-profile/profile_mutations.clj | 51 ++++++++++++++++++++++++++++++ perf-profile/profile_test.clj | 49 ++++++++++++++++++++++++++++ project.clj | 8 +++++ 6 files changed, 206 insertions(+) create mode 100644 perf-profile/README.md create mode 100644 perf-profile/benchmark.clj create mode 100644 perf-profile/profile_eval.clj create mode 100644 perf-profile/profile_mutations.clj create mode 100644 perf-profile/profile_test.clj diff --git a/perf-profile/README.md b/perf-profile/README.md new file mode 100644 index 00000000..35ebef28 --- /dev/null +++ b/perf-profile/README.md @@ -0,0 +1,7 @@ +# How to run + +In project root: + +```sh +lein run -m clojure.main ./perf-profile/profile_test.clj # or other profile class +``` diff --git a/perf-profile/benchmark.clj b/perf-profile/benchmark.clj new file mode 100644 index 00000000..4a5d4fee --- /dev/null +++ b/perf-profile/benchmark.clj @@ -0,0 +1,49 @@ +(ns benchmark + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval] + [closyr.ops.initialize :as ops-init] + [closyr.ops :as ops] + [closyr.ga :as ga])) + +(defn benchmark-evolution [pop-size n-iters] + (let [phenos (ops-init/initial-phenotypes pop-size) + muts (ops-init/initial-mutations) + xs (vec (range 0.1 10.0 0.2)) + ys (mapv #(+ (* % %) (Math/sin %)) xs) + input-xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) + run-args {:input-xs-list input-xs-list + :input-xs-count (count xs) + :input-xs-vec xs + :input-ys-vec ys} + run-config {:max-leafs 40} + score-fn (partial ops/score-fn run-args run-config) + mut-fn (partial ops/mutation-fn run-config muts) + cross-fn (partial ops/crossover-fn run-config muts) + ga-config (ga/initialize phenos score-fn mut-fn cross-fn)] + + ;; Warmup + (dotimes [_ 3] (ga/evolve ga-config)) + + ;; Timed run + (let [start (System/nanoTime) + result (loop [config ga-config, i 0] + (if (< i n-iters) + (recur (ga/evolve config) (inc i)) + config)) + elapsed-ms (/ (- (System/nanoTime) start) 1e6)] + {:pop-size pop-size + :iters n-iters + :elapsed-ms elapsed-ms + :ms-per-iter (/ elapsed-ms n-iters) + :phenos-per-sec (/ (* pop-size n-iters 1000) elapsed-ms)}))) + +(println "\n=== Evolution Benchmark ===\n") + +(doseq [pop-size [10 50 100 500 1000 2000 4000 10000]] + (let [result (benchmark-evolution pop-size 10)] + (printf "Pop %4d: %6.1f ms/iter, %8.0f phenos/sec\n" + pop-size + (:ms-per-iter result) + (:phenos-per-sec result)))) + +(println "\n=== Done ===") diff --git a/perf-profile/profile_eval.clj b/perf-profile/profile_eval.clj new file mode 100644 index 00000000..3cf56d3f --- /dev/null +++ b/perf-profile/profile_eval.clj @@ -0,0 +1,42 @@ +(ns profile-eval + (:require [closyr.ops.common :as ops-common]) + (:import [org.matheclipse.core.expression F] + [org.matheclipse.core.eval ExprEvaluator])) + +(defn profile-eval [] + (let [x ops-common/sym-x + util (ops-common/get-thread-local-util) + simple-expr (F/Plus x (F/C1D2)) + complex-expr (F/D (F/Sin (F/Times x x)) x)] ;; derivative is expensive + + (println "\n=== Eval in ->phenotype ===\n") + + (println "1. Creating expression WITHOUT .eval (1000x):") + (time (dotimes [_ 1000] + {:sym x :util nil :expr simple-expr})) + + (println "\n2. Creating phenotype WITH .eval on simple expr (1000x):") + (time (dotimes [_ 1000] + (.eval util simple-expr))) + + (println "\n3. Creating derivative expr (1000x):") + (time (dotimes [_ 1000] + (F/D (F/Sin x) x))) + + (println "\n4. Eval derivative result (1000x):") + (let [deriv (F/D (F/Sin x) x)] + (time (dotimes [_ 1000] + (.eval util deriv)))) + + (println "\n5. Complex derivative F/D(Sin(x^2), x) (100x):") + (time (dotimes [_ 100] + (F/D (F/Sin (F/Times x x)) x))) + + (println "\n6. Eval complex derivative (100x):") + (let [deriv (F/D (F/Sin (F/Times x x)) x)] + (time (dotimes [_ 100] + (.eval util deriv)))) + + (println "\n=== Done ===\n"))) + +(profile-eval) diff --git a/perf-profile/profile_mutations.clj b/perf-profile/profile_mutations.clj new file mode 100644 index 00000000..ce8996c1 --- /dev/null +++ b/perf-profile/profile_mutations.clj @@ -0,0 +1,51 @@ +(ns profile-test + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval] + [closyr.ops.initialize :as ops-init] + [closyr.ops :as ops] + [closyr.ga :as ga])) + +(defn profile-components [] + (let [phenos (ops-init/initial-phenotypes 100) + muts (ops-init/initial-mutations) + xs (vec (range 0.1 10.0 0.2)) + ys (mapv #(+ (* % %) (Math/sin %)) xs) + input-xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) + run-args {:input-xs-list input-xs-list + :input-xs-count (count xs) + :input-xs-vec xs + :input-ys-vec ys} + run-config {:max-leafs 40}] + + (println "\n=== Profiling Components ===\n") + + ;; Profile phenotype creation + (println "1. Creating 1000 phenotypes:") + (let [x ops-common/sym-x] + (time (dotimes [_ 1000] + (ops-common/->phenotype x (org.matheclipse.core.expression.F/Sin x) nil)))) + + ;; Profile evaluation + (println "\n2. Evaluating 100 phenotypes on" (count xs) "points:") + (time (doseq [p phenos] + (ops-eval/eval-vec-pheno p run-args))) + + ;; Profile scoring + (println "\n3. Scoring 100 phenotypes:") + (time (doseq [p phenos] + (ops/score-fn run-args run-config p))) + + ;; Profile mutation + (println "\n4. Applying 100 mutations:") + (time (doseq [p phenos] + (ops/mutation-fn run-config muts p p))) + + ;; Profile full evolution step + (println "\n5. Full evolution step (100 pop):") + (let [score-fn (partial ops/score-fn run-args run-config) + mut-fn (partial ops/mutation-fn run-config muts) + cross-fn (partial ops/crossover-fn run-config muts) + ga-config (ga/initialize phenos score-fn mut-fn cross-fn)] + (time (ga/evolve ga-config))) + + (println "\n=== Done ===\n"))) diff --git a/perf-profile/profile_test.clj b/perf-profile/profile_test.clj new file mode 100644 index 00000000..ea7b6c4f --- /dev/null +++ b/perf-profile/profile_test.clj @@ -0,0 +1,49 @@ +(ns profile-mutations + (:require [closyr.ops.common :as ops-common] + [closyr.ops.modify :as ops-modify] + [closyr.ops.initialize :as ops-init]) + (:import [org.matheclipse.core.expression F])) + +(defn profile-mutation-internals [] + (let [x ops-common/sym-x + pheno (ops-common/->phenotype x (F/Plus (F/Sin x) (F/Times x x)) nil) + muts (ops-init/initial-mutations)] + + (println "\n=== Mutation Internals ===\n") + + ;; Profile single modification types + (println "1. Single modify-fn mutation (100x):") + (let [mod-fn (first (filter #(= :modify-fn (:op %)) muts))] + (time (dotimes [_ 100] + (ops-modify/modify mod-fn pheno)))) + + (println "\n2. Single modify-leafs mutation (100x):") + (let [mod-leafs (first (filter #(= :modify-leafs (:op %)) muts))] + (time (dotimes [_ 100] + (ops-modify/modify mod-leafs pheno)))) + + (println "\n3. Single modify-branches mutation (100x):") + (let [mod-branch (first (filter #(= :modify-branches (:op %)) muts))] + (time (dotimes [_ 100] + (ops-modify/modify mod-branch pheno)))) + + (println "\n4. apply-modifications with 1 mod (100x):") + (time (dotimes [_ 100] + (ops-modify/apply-modifications 40 1 muts pheno pheno))) + + (println "\n5. apply-modifications with 5 mods (100x):") + (time (dotimes [_ 100] + (ops-modify/apply-modifications 40 5 muts pheno pheno))) + + (println "\n6. Just F/Sin creation (1000x):") + (time (dotimes [_ 1000] + (F/Sin x))) + + (println "\n7. Just ->phenotype from expr (1000x):") + (let [expr (F/Plus (F/Sin x) x)] + (time (dotimes [_ 1000] + (ops-common/->phenotype x expr nil)))) + + (println "\n=== Done ===\n"))) + +(profile-mutation-internals) diff --git a/project.clj b/project.clj index 174a8c62..b07af3a9 100644 --- a/project.clj +++ b/project.clj @@ -36,6 +36,14 @@ :java-source-paths ["src/main/java"] :main ^:skip-aot closyr.core :target-path "target/%s" + + ;; JVM tuning for better GA performance + :jvm-opts ["-Xmx4g" ; Max heap size + "-XX:+UseG1GC" ; G1 garbage collector (better for large heaps) + "-XX:MaxGCPauseMillis=100" ; Target max GC pause + "-XX:+UseStringDeduplication"] ; Reduce memory for duplicate strings + ;; Note: direct-linking only in uberjar profile (breaks dynamic var rebinding in tests) + :profiles {:uberjar {:aot :all :manifest {"Multi-Release" true} :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}} From 094b925f286087d7589d081107bac46119b84b8e Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 13:29:53 -0700 Subject: [PATCH 013/224] slight perf improvement --- perf-profile/benchmark.clj | 1 + perf-profile/eval_comparison.clj | 49 +++++++++++++++++ perf-profile/eval_detailed.clj | 76 +++++++++++++++++++++++++++ perf-profile/explore_symja.clj | 69 ++++++++++++++++++++++++ perf-profile/explore_symja2.clj | 72 +++++++++++++++++++++++++ perf-profile/explore_symja3.clj | 62 ++++++++++++++++++++++ perf-profile/explore_symja4.clj | 90 ++++++++++++++++++++++++++++++++ src/closyr/ops/eval.clj | 8 +-- 8 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 perf-profile/eval_comparison.clj create mode 100644 perf-profile/eval_detailed.clj create mode 100644 perf-profile/explore_symja.clj create mode 100644 perf-profile/explore_symja2.clj create mode 100644 perf-profile/explore_symja3.clj create mode 100644 perf-profile/explore_symja4.clj diff --git a/perf-profile/benchmark.clj b/perf-profile/benchmark.clj index 4a5d4fee..0f6bbc21 100644 --- a/perf-profile/benchmark.clj +++ b/perf-profile/benchmark.clj @@ -47,3 +47,4 @@ (:phenos-per-sec result)))) (println "\n=== Done ===") +(System/exit 0) diff --git a/perf-profile/eval_comparison.clj b/perf-profile/eval_comparison.clj new file mode 100644 index 00000000..4fd824af --- /dev/null +++ b/perf-profile/eval_comparison.clj @@ -0,0 +1,49 @@ +(ns eval-comparison + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval]) + (:import [org.matheclipse.core.expression F] + [org.matheclipse.core.interfaces IExpr IAST ISymbol])) + +(defn compare-eval [] + (let [x ops-common/sym-x + expr (F/Plus (F/Sin x) (F/Times x x)) + util (ops-common/new-util) + pheno {:sym x :util util :expr expr :id (java.util.UUID/randomUUID)} + + xs (vec (range 0.1 10.0 0.1)) + n (count xs) + xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) + run-args {:input-xs-list xs-list :input-xs-count n} + + ;; Pre-compute for isolated tests + fn-expr (ops-common/expr->fn pheno) + ast (F/ast xs-list fn-expr)] + + (println "\n=== Evaluation Time Breakdown ===\n") + (println "Points:" n) + + ;; Warmup + (dotimes [_ 50] (ops-eval/eval-vec-pheno pheno run-args)) + + (println "\n1. Current optimized eval-vec-pheno (1000x):") + (time (dotimes [_ 1000] (ops-eval/eval-vec-pheno pheno run-args))) + + (println "\n2. Just the .eval call (1000x):") + (time (dotimes [_ 1000] (.eval util ast))) + + (println "\n3. Just result extraction from pre-computed result (1000x):") + (let [^IAST result (.eval util ast)] + (time (dotimes [_ 1000] + (loop [i (int 0), acc (transient [])] + (if (< i n) + (let [^IExpr e (.getArg result (inc i) F/Infinity) + v (if (.isReal e) + (ops-common/expr->double e) + Double/POSITIVE_INFINITY)] + (recur (unchecked-inc-int i) (conj! acc v))) + (persistent! acc)))))) + + (println "\nConclusion: .eval is ~95% of eval time, extraction is ~5%") + (println "\n=== Done ==="))) + +(compare-eval) diff --git a/perf-profile/eval_detailed.clj b/perf-profile/eval_detailed.clj new file mode 100644 index 00000000..92be2519 --- /dev/null +++ b/perf-profile/eval_detailed.clj @@ -0,0 +1,76 @@ +(ns eval-detailed + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval]) + (:import [org.matheclipse.core.expression F] + [org.matheclipse.core.interfaces IExpr IAST ISymbol] + [org.matheclipse.core.eval ExprEvaluator])) + +(defn detailed-timing [] + (let [x ops-common/sym-x + expr (F/Plus (F/Sin x) (F/Times x x)) + util (ops-common/new-util) + pheno {:sym x :util util :expr expr :id (java.util.UUID/randomUUID)} + + xs (vec (range 0.1 10.0 0.1)) + n (count xs) + xs-exprs (ops-common/doubles->exprs xs) + xs-list (ops-common/exprs->exprs-list xs-exprs) + run-args {:input-xs-list xs-list :input-xs-count n}] + + (println "\n=== Detailed Timing Breakdown ===\n") + + ;; Warmup + (dotimes [_ 50] (ops-eval/eval-vec-pheno pheno run-args)) + + (println "1. Full eval-vec-pheno (1000x):") + (time (dotimes [_ 1000] (ops-eval/eval-vec-pheno pheno run-args))) + + (println "\n2. expr->fn creation (10000x):") + (time (dotimes [_ 10000] (ops-common/expr->fn pheno))) + + (println "\n3. F/ast creation with pre-computed fn-expr (10000x):") + (let [fn-expr (ops-common/expr->fn pheno)] + (time (dotimes [_ 10000] (F/ast xs-list fn-expr)))) + + (println "\n4. .eval on pre-computed ast (1000x):") + (let [fn-expr (ops-common/expr->fn pheno) + ast (F/ast xs-list fn-expr)] + (time (dotimes [_ 1000] (.eval ^ExprEvaluator util ast)))) + + (println "\n5. Full eval-phenotype-on-expr-args (1000x):") + (time (dotimes [_ 1000] (ops-eval/eval-phenotype-on-expr-args pheno xs-list))) + + (println "\n6a. OLD parseable-eval-result? with string (10000x):") + (let [fn-expr (ops-common/expr->fn pheno) + ast (F/ast xs-list fn-expr) + result (.eval ^ExprEvaluator util ast)] + (time (dotimes [_ 10000] + (not (or (nil? result) (= "Indeterminate" (str result))))))) + + (println "\n6b. NEW parseable-eval-result? with identical? (10000x):") + (let [fn-expr (ops-common/expr->fn pheno) + ast (F/ast xs-list fn-expr) + result (.eval ^ExprEvaluator util ast)] + (time (dotimes [_ 10000] + (and (some? result) (not (identical? result F/Indeterminate)))))) + + (println "\n7. Result extraction only (1000x):") + (let [fn-expr (ops-common/expr->fn pheno) + ast (F/ast xs-list fn-expr) + ^IAST result (.eval ^ExprEvaluator util ast)] + (time (dotimes [_ 1000] + (loop [i (int 0), acc (transient [])] + (if (< i n) + (let [^IExpr e (.getArg result (inc i) F/Infinity) + v (if (.isReal e) + (ops-common/expr->double e) + Double/POSITIVE_INFINITY)] + (recur (unchecked-inc-int i) (conj! acc v))) + (persistent! acc)))))) + + (println "\n=== Reconstructed total ===") + (println "Expected: expr->fn + F/ast + .eval + extraction") + (println "(Values from above, scaled to 1000x)") + (println "\n=== Done ==="))) + +(detailed-timing) diff --git a/perf-profile/explore_symja.clj b/perf-profile/explore_symja.clj new file mode 100644 index 00000000..836f331d --- /dev/null +++ b/perf-profile/explore_symja.clj @@ -0,0 +1,69 @@ +(ns explore-symja + (:require [closyr.ops.common :as ops-common]) + (:import [org.matheclipse.core.expression F] + [org.matheclipse.core.eval ExprEvaluator EvalEngine] + [org.matheclipse.core.interfaces IExpr IAST ISymbol])) + +;; Explore Symja's numeric evaluation options +(defn test-evaluation-methods [] + (let [x (F/Dummy "x") + expr (F/Plus (F/Sin x) (F/Times x x)) ; sin(x) + x^2 + util (ops-common/new-util) + test-vals [0.0 0.5 1.0 1.5 2.0]] + + (println "\n=== Exploring Symja Evaluation Methods ===\n") + + ;; Method 1: Current approach - evaluate function on list + (println "1. Current F/ast approach:") + (let [xs-exprs (mapv #(F/num %) test-vals) + xs-arr (into-array IExpr xs-exprs) + xs-list (into-array IExpr [(F/List xs-arr)]) + fn-expr (F/Function (F/List (into-array ISymbol [x])) expr) + ast (F/ast xs-list fn-expr) + result (.eval util ast)] + (println " Result:" (str result))) + + ;; Method 2: Try evalDouble on substituted expression + (println "\n2. Direct evalDouble per point:") + (doseq [v test-vals] + (let [subst-expr (.subs expr x (F/num v)) + evaled (.eval util subst-expr)] + (when (.isReal evaled) + (println " x=" v "-> y=" (.evalDouble evaled))))) + + ;; Method 3: Check if expression can compile to DoubleUnaryOperator + (println "\n3. Check IExpr numeric methods:") + (let [simple-expr (F/Times (F/num 2.0) x)] + (println " Has isNumericFunction:" (.isNumericFunction simple-expr true)) + (println " Expr class:" (class expr))) + + ;; Method 4: Benchmark extraction from list result + (println "\n4. Benchmark: mapv vs areduce for result extraction:") + (let [xs-exprs (mapv #(F/num %) (range 0.1 10.0 0.1)) + xs-arr (into-array IExpr xs-exprs) + xs-list (into-array IExpr [(F/List xs-arr)]) + fn-expr (F/Function (F/List (into-array ISymbol [x])) expr) + ast (F/ast xs-list fn-expr) + ^IAST result (.eval util ast) + n (dec (.size result))] + + (println " Result size:" n) + + ;; mapv approach (current) + (print " mapv extraction (1000x): ") + (time (dotimes [_ 1000] + (mapv (fn [i] + (let [^IExpr e (.getArg result (inc i) F/Infinity)] + (if (.isReal e) (.evalDouble e) Double/POSITIVE_INFINITY))) + (range n)))) + + ;; Direct array approach + (print " array extraction (1000x): ") + (time (dotimes [_ 1000] + (let [arr (double-array n)] + (dotimes [i n] + (let [^IExpr e (.getArg result (inc i) F/Infinity)] + (aset arr i (if (.isReal e) (.evalDouble e) Double/POSITIVE_INFINITY)))) + arr)))))) + +(test-evaluation-methods) diff --git a/perf-profile/explore_symja2.clj b/perf-profile/explore_symja2.clj new file mode 100644 index 00000000..f6257e74 --- /dev/null +++ b/perf-profile/explore_symja2.clj @@ -0,0 +1,72 @@ +(ns explore-symja2 + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval] + [closyr.ops.initialize :as ops-init]) + (:import [org.matheclipse.core.expression F] + [org.matheclipse.core.interfaces IExpr IAST ISymbol])) + +(defn benchmark-eval-paths [] + (let [x ops-common/sym-x + ;; Create a more complex expression like GA would produce + expr (F/Plus (F/Sin x) (F/Times x x) (F/Cos (F/Times (F/num 2.0) x))) + pheno (ops-common/->phenotype x expr nil) + util (ops-common/new-util) + + ;; Create input data + xs (vec (range 0.1 10.0 0.1)) ; 100 points + n (count xs) + xs-exprs (ops-common/doubles->exprs xs) + xs-list (ops-common/exprs->exprs-list xs-exprs) + run-args {:input-xs-list xs-list :input-xs-count n}] + + (println "\n=== Full Evaluation Path Benchmark ===\n") + (println "Expression:" (str expr)) + (println "Points:" n) + + ;; Warmup + (dotimes [_ 10] (ops-eval/eval-vec-pheno pheno run-args)) + + ;; Current implementation + (println "\n1. Current eval-vec-pheno (1000x):") + (time (dotimes [_ 1000] + (ops-eval/eval-vec-pheno pheno run-args))) + + ;; Break down the components + (println "\n2. Component breakdown (1000x each):") + + ;; Just the F/ast + eval part + (let [fn-expr (ops-common/expr->fn pheno)] + (print " a. F/ast creation: ") + (time (dotimes [_ 1000] + (F/ast xs-list fn-expr))) + + (let [ast (F/ast xs-list fn-expr)] + (print " b. .eval on ast: ") + (time (dotimes [_ 1000] + (.eval util ast))))) + + ;; Just result extraction + (let [fn-expr (ops-common/expr->fn pheno) + ast (F/ast xs-list fn-expr) + ^IAST result (.eval util ast)] + (print " c. Result extraction (mapv): ") + (time (dotimes [_ 1000] + (mapv (fn [i] + (let [^IExpr e (.getArg result (inc i) F/Infinity)] + (if (.isReal e) + (ops-common/expr->double e) + Double/POSITIVE_INFINITY))) + (range n))))) + + ;; Try direct double evaluation without going through IExpr + (println "\n3. Alternative: Direct substitution approach (100x):") + (print " Per-point subs+eval: ") + (time (dotimes [_ 100] + (mapv (fn [xv] + (let [subst (.subs expr x (F/num xv))] + (.evalDouble (.eval util subst)))) + xs))) + + (println "\n=== Done ==="))) + +(benchmark-eval-paths) diff --git a/perf-profile/explore_symja3.clj b/perf-profile/explore_symja3.clj new file mode 100644 index 00000000..b8e4eb0f --- /dev/null +++ b/perf-profile/explore_symja3.clj @@ -0,0 +1,62 @@ +(ns explore-symja3 + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval]) + (:import [org.matheclipse.core.expression F] + [org.matheclipse.core.interfaces IExpr IAST ISymbol])) + +(defn benchmark-overhead [] + (let [x ops-common/sym-x + expr (F/Plus (F/Sin x) (F/Times x x) (F/Cos (F/Times (F/num 2.0) x))) + util (ops-common/new-util) + + xs (vec (range 0.1 10.0 0.1)) + n (count xs) + xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) + + ;; Pre-create everything + pheno {:sym x :util util :expr expr :id (java.util.UUID/randomUUID)} + fn-expr (ops-common/expr->fn pheno)] + + (println "\n=== Finding the Overhead ===\n") + (println "Expression:" (str expr)) + + ;; Warmup + (dotimes [_ 100] + (let [ast (F/ast xs-list fn-expr)] + (.eval util ast))) + + ;; Minimal path with pre-computed fn-expr + (println "\n1. Minimal: pre-computed fn-expr + eval (1000x):") + (time (dotimes [_ 1000] + (let [ast (F/ast xs-list fn-expr) + ^IAST result (.eval util ast)] + (mapv (fn [i] + (let [^IExpr e (.getArg result (inc i) F/Infinity)] + (if (.isReal e) (.evalDouble e) Double/POSITIVE_INFINITY))) + (range n))))) + + ;; With expr->fn called each time (like current code) + (println "\n2. With expr->fn each time (1000x):") + (time (dotimes [_ 1000] + (let [fn-expr-new (ops-common/expr->fn pheno) + ast (F/ast xs-list fn-expr-new) + ^IAST result (.eval util ast)] + (mapv (fn [i] + (let [^IExpr e (.getArg result (inc i) F/Infinity)] + (if (.isReal e) (.evalDouble e) Double/POSITIVE_INFINITY))) + (range n))))) + + ;; Current eval-vec-pheno + (println "\n3. Current eval-vec-pheno (1000x):") + (let [run-args {:input-xs-list xs-list :input-xs-count n}] + (time (dotimes [_ 1000] + (ops-eval/eval-vec-pheno pheno run-args)))) + + ;; Check expr->fn overhead + (println "\n4. Just expr->fn creation (10000x):") + (time (dotimes [_ 10000] + (ops-common/expr->fn pheno))) + + (println "\n=== Done ==="))) + +(benchmark-overhead) diff --git a/perf-profile/explore_symja4.clj b/perf-profile/explore_symja4.clj new file mode 100644 index 00000000..8da431d7 --- /dev/null +++ b/perf-profile/explore_symja4.clj @@ -0,0 +1,90 @@ +(ns explore-symja4 + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval]) + (:import [org.matheclipse.core.expression F] + [org.matheclipse.core.interfaces IExpr IAST ISymbol] + [org.matheclipse.core.eval ExprEvaluator])) + +(defn benchmark-optimized [] + (let [x ops-common/sym-x + expr (F/Plus (F/Sin x) (F/Times x x) (F/Cos (F/Times (F/num 2.0) x))) + util (ops-common/new-util) + + xs (vec (range 0.1 10.0 0.1)) + n (count xs) + xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) + run-args {:input-xs-list xs-list :input-xs-count n} + + pheno {:sym x :util util :expr expr :id (java.util.UUID/randomUUID)}] + + (println "\n=== Optimized Evaluation Benchmark ===\n") + + ;; Warmup + (dotimes [_ 100] (ops-eval/eval-vec-pheno pheno run-args)) + + ;; Current + (println "1. Current eval-vec-pheno (1000x):") + (time (dotimes [_ 1000] + (ops-eval/eval-vec-pheno pheno run-args))) + + ;; Optimized version - inline everything, no intermediate functions + (println "\n2. Optimized inline version (1000x):") + (time + (dotimes [_ 1000] + (let [^IAST fn-expr (F/Function + (F/List (into-array ISymbol [x])) + expr) + ^IAST ast (F/ast xs-list fn-expr) + ^IExpr eval-result (.eval ^ExprEvaluator util ast)] + (when-not (or (nil? eval-result) + (= "Indeterminate" (str eval-result))) + (let [result-size (dec (.size eval-result))] + (if (= n result-size) + ;; Normal case - one result per input + (loop [i 0, acc (transient [])] + (if (< i n) + (let [^IExpr e (.getArg eval-result (inc i) F/Infinity) + v (if (.isReal e) + (.doubleValue (.toNumber e)) + Double/POSITIVE_INFINITY)] + (recur (inc i) (conj! acc v))) + (persistent! acc))) + ;; Constant result case + (let [^IExpr arg0 (.getArg eval-result 0 F/Infinity) + v (.doubleValue (.toNumber (if (.isReal expr) expr arg0)))] + (vec (repeat n v))))))))) + + ;; With transient but using mapv structure + (println "\n3. Transient with indexed loop (1000x):") + (time + (dotimes [_ 1000] + (let [^IAST fn-expr (F/Function (F/List (into-array ISymbol [x])) expr) + ^IAST ast (F/ast xs-list fn-expr) + ^IAST result (.eval ^ExprEvaluator util ast)] + (loop [i 0, acc (transient [])] + (if (< i n) + (let [^IExpr e (.getArg result (inc i) F/Infinity) + v (if (.isReal e) + (.doubleValue (.toNumber e)) + Double/POSITIVE_INFINITY)] + (recur (inc i) (conj! acc v))) + (persistent! acc)))))) + + ;; Using primitive double array + (println "\n4. Primitive double array (1000x):") + (time + (dotimes [_ 1000] + (let [^IAST fn-expr (F/Function (F/List (into-array ISymbol [x])) expr) + ^IAST ast (F/ast xs-list fn-expr) + ^IAST result (.eval ^ExprEvaluator util ast) + ^doubles arr (double-array n)] + (dotimes [i n] + (let [^IExpr e (.getArg result (inc i) F/Infinity)] + (aset arr i (if (.isReal e) + (.doubleValue (.toNumber e)) + Double/POSITIVE_INFINITY)))) + arr))) + + (println "\n=== Done ==="))) + +(benchmark-optimized) diff --git a/src/closyr/ops/eval.clj b/src/closyr/ops/eval.clj index 540ddcfb..43058dc1 100644 --- a/src/closyr/ops/eval.clj +++ b/src/closyr/ops/eval.clj @@ -57,9 +57,11 @@ (defn- parseable-eval-result? - [eval-p] - (not (or (nil? eval-p) - (= "Indeterminate" (str eval-p))))) + "Check if eval result is valid (not nil or Indeterminate). + Uses direct object comparison instead of expensive string conversion." + [^IExpr eval-p] + (and (some? eval-p) + (not (identical? eval-p F/Indeterminate)))) (defn- ^IExpr get-arg From fe04e4163c3c705a711a45f09695035d94c4838e Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 14:13:25 -0700 Subject: [PATCH 014/224] slight perf improvement --- perf-profile/benchmark.clj | 5 +- perf-profile/score_benchmark.clj | 90 ++++++++++++++++++++++++ perf-profile/score_only_bench.clj | 43 +++++++++++ src/closyr/ops.clj | 78 ++++++++++++++------ src/closyr/symbolic_regression.clj | 3 +- src/closyr/util/spec.clj | 2 + test/closyr/symbolic_regression_test.clj | 5 +- 7 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 perf-profile/score_benchmark.clj create mode 100644 perf-profile/score_only_bench.clj diff --git a/perf-profile/benchmark.clj b/perf-profile/benchmark.clj index 0f6bbc21..d1fd3869 100644 --- a/perf-profile/benchmark.clj +++ b/perf-profile/benchmark.clj @@ -14,7 +14,8 @@ run-args {:input-xs-list input-xs-list :input-xs-count (count xs) :input-xs-vec xs - :input-ys-vec ys} + :input-ys-vec ys + :input-ys-arr (double-array ys)} run-config {:max-leafs 40} score-fn (partial ops/score-fn run-args run-config) mut-fn (partial ops/mutation-fn run-config muts) @@ -39,7 +40,7 @@ (println "\n=== Evolution Benchmark ===\n") -(doseq [pop-size [10 50 100 500 1000 2000 4000 10000]] +(doseq [pop-size [10 50 100 500 1000 2000 4000 10000 50000]] (let [result (benchmark-evolution pop-size 10)] (printf "Pop %4d: %6.1f ms/iter, %8.0f phenos/sec\n" pop-size diff --git a/perf-profile/score_benchmark.clj b/perf-profile/score_benchmark.clj new file mode 100644 index 00000000..66150968 --- /dev/null +++ b/perf-profile/score_benchmark.clj @@ -0,0 +1,90 @@ +(ns score-benchmark + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval] + [closyr.ops :as ops] + [closyr.ops.initialize :as ops-init]) + (:import [org.matheclipse.core.expression F])) + +(defn benchmark-scoring [] + (let [x ops-common/sym-x + expr (F/Plus (F/Sin x) (F/Times x x)) + pheno (ops-common/->phenotype x expr nil) + + ;; Create test data + xs (vec (range 0.1 10.0 0.1)) ; 100 points + ys (mapv #(+ (Math/sin %) (* % %)) xs) + n (count xs) + + xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) + run-args {:input-xs-list xs-list :input-xs-count n + :input-xs-vec xs :input-ys-vec ys} + run-config {:max-leafs 40} + + ;; Pre-compute f(x) for scoring benchmarks + f-of-xs (ops-eval/eval-vec-pheno pheno run-args)] + + (println "\n=== Scoring Benchmark ===\n") + (println "Points:" n) + (println "f(x) sample:" (take 3 f-of-xs)) + + ;; Warmup + (dotimes [_ 100] + (ops/compute-score-from-actuals-and-expecteds pheno f-of-xs ys 5)) + + ;; Current implementation + (println "\n1. Current compute-score (10000x):") + (time (dotimes [_ 10000] + (ops/compute-score-from-actuals-and-expecteds pheno f-of-xs ys 5))) + + ;; Primitive array version + (println "\n2. Primitive array version (10000x):") + (let [^doubles ys-arr (double-array ys) + ^doubles fx-arr (double-array f-of-xs) + max-resid (double 1000000)] + (time (dotimes [_ 10000] + (let [n (alength ys-arr) + ;; Compute sum and max in single pass + [sum-resid max-resid-val] + (loop [i (int 0), sum (double 0.0), mx (double 0.0)] + (if (< i n) + (let [expected (aget ys-arr i) + actual (aget fx-arr i) + resid (if (Double/isNaN actual) + max-resid + (Math/abs (- expected actual))) + resid (Math/min max-resid resid)] + (recur (unchecked-inc-int i) + (+ sum resid) + (Math/max mx resid))) + [sum mx]))] + (* -1.0 (+ (* 2.0 (/ sum-resid n)) max-resid-val)))))) + + ;; Just the residual computation part + (println "\n3. Just map compute-residual (10000x):") + (time (dotimes [_ 10000] + (map ops/compute-residual ys f-of-xs))) + + ;; Realized map + (println "\n4. Realized map compute-residual (10000x):") + (time (dotimes [_ 10000] + (doall (map ops/compute-residual ys f-of-xs)))) + + ;; reduce + and reduce max + (println "\n5. reduce + and reduce max (10000x):") + (let [resids (mapv #(Math/abs (- %1 %2)) ys f-of-xs)] + (time (dotimes [_ 10000] + (let [s (reduce + 0.0 resids) + m (reduce max resids)] + (+ s m))))) + + ;; areduce version + (println "\n6. areduce sum and max (10000x):") + (let [^doubles resids-arr (double-array (mapv #(Math/abs (- %1 %2)) ys f-of-xs))] + (time (dotimes [_ 10000] + (let [s (areduce resids-arr i ret 0.0 (+ ret (aget resids-arr i))) + m (areduce resids-arr i ret 0.0 (Math/max ret (aget resids-arr i)))] + (+ s m))))) + + (println "\n=== Done ==="))) + +(benchmark-scoring) diff --git a/perf-profile/score_only_bench.clj b/perf-profile/score_only_bench.clj new file mode 100644 index 00000000..20bb2eaf --- /dev/null +++ b/perf-profile/score_only_bench.clj @@ -0,0 +1,43 @@ +(ns score-only-bench + "Benchmark just the score-fn to isolate residual computation" + (:require [closyr.ops.common :as ops-common] + [closyr.ops.eval :as ops-eval] + [closyr.ops :as ops]) + (:import [org.matheclipse.core.expression F])) + +(defn bench-score-fn [n-points n-calls use-arr?] + (let [x ops-common/sym-x + expr (F/Plus (F/Sin x) (F/Times x x)) + pheno (ops-common/->phenotype x expr nil) + xs (vec (range 0.1 (+ 0.1 (* n-points 0.1)) 0.1)) + ys (mapv #(+ (Math/sin %) (* % %)) xs) + xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) + run-args (cond-> {:input-xs-list xs-list + :input-xs-count (count xs) + :input-xs-vec xs + :input-ys-vec ys} + use-arr? (assoc :input-ys-arr (double-array ys))) + run-config {:max-leafs 40}] + + ;; Warmup + (dotimes [_ 100] + (ops/score-fn run-args run-config pheno)) + + ;; Timed + (let [start (System/nanoTime)] + (dotimes [_ n-calls] + (ops/score-fn run-args run-config pheno)) + (/ (- (System/nanoTime) start) 1e6)))) + +(println "\n=== Score Function Benchmark (isolating residual computation) ===\n") + +(doseq [n-points [50 100 200 500]] + (let [n-calls 1000 + time-without (bench-score-fn n-points n-calls false) + time-with (bench-score-fn n-points n-calls true) + speedup (/ time-without time-with)] + (printf "Points: %3d | WITHOUT arr: %7.1f ms | WITH arr: %7.1f ms | Speedup: %.2fx\n" + n-points time-without time-with speedup))) + +(println "\n=== Done ===") +(System/exit 0) diff --git a/src/closyr/ops.clj b/src/closyr/ops.clj index da687c68..766b4ae1 100644 --- a/src/closyr/ops.clj +++ b/src/closyr/ops.clj @@ -90,36 +90,72 @@ (min max-resid (abs res))))) +(defn- compute-residuals-fast + "Compute sum and max of residuals using primitive arrays. + Returns [sum-residuals max-residual] or nil if f-of-xs is invalid. + ~50x faster than map/reduce approach." + ^doubles [^doubles ys-arr f-of-xs] + (when (and f-of-xs (seq f-of-xs)) + (let [n (count f-of-xs) + max-r (double max-resid)] + (loop [i (int 0), sum (double 0.0), mx (double 0.0)] + (if (< i n) + (let [expected (aget ys-arr i) + actual (double (nth f-of-xs i)) + resid (if (or (Double/isNaN actual) (Double/isInfinite actual)) + max-r + (Math/abs (- expected actual))) + resid (Math/min max-r resid)] + (recur (unchecked-inc-int i) + (+ sum resid) + (Math/max mx resid))) + (double-array [sum mx (double n)])))))) + + (defn- length-deduction [score leafs] (* (abs score) (min 0.1 (* 0.0000001 leafs leafs)))) (defn compute-score-from-actuals-and-expecteds - "Compute overall score for fn given some actual and expected ys" - {:malli/schema [:=> [:cat #'specs/GAPhenotype #'specs/NumberVector #'specs/NumberVector number?] number?]} - [pheno f-of-xs input-ys-vec leafs] - (try - (let [abs-resids (map compute-residual input-ys-vec f-of-xs) - resid-sum (sum abs-resids) - score (* -1.0 (+ (* 2.0 (/ resid-sum (count abs-resids))) - (reduce max abs-resids))) - length-deduction (length-deduction score leafs) - overall-score (- score length-deduction)] - - (swap! sim-stats* update-in [:scoring :len-deductions] #(into (or % []) [length-deduction])) - - overall-score) - (catch Exception e - (log/error "Err in computing score from residuals: " - (.getMessage e) ", fn: " (str (:expr pheno)) ", from: " (:expr pheno)) - (tally-min-score min-score)))) + "Compute overall score for fn given some actual and expected ys. + Uses fast primitive array computation when ys-arr is provided." + {:malli/schema [:function + [:=> [:cat #'specs/GAPhenotype #'specs/NumberVector #'specs/NumberVector number?] number?] + [:=> [:cat #'specs/GAPhenotype #'specs/NumberVector #'specs/NumberVector number? [:maybe some?]] number?]]} + ([pheno f-of-xs input-ys-vec leafs] + ;; Legacy path - convert to array + (compute-score-from-actuals-and-expecteds pheno f-of-xs input-ys-vec leafs nil)) + ([pheno f-of-xs input-ys-vec leafs ^doubles input-ys-arr] + (try + (let [[resid-sum max-resid-val n] + (if input-ys-arr + ;; Fast path with primitive array + (let [^doubles result (compute-residuals-fast input-ys-arr f-of-xs)] + (when result + [(aget result 0) (aget result 1) (aget result 2)])) + ;; Fallback to original implementation + (let [abs-resids (map compute-residual input-ys-vec f-of-xs)] + [(sum abs-resids) (reduce max abs-resids) (count abs-resids)]))] + (if resid-sum + (let [score (* -1.0 (+ (* 2.0 (/ resid-sum n)) + max-resid-val)) + length-deduction (length-deduction score leafs) + overall-score (- score length-deduction)] + (swap! sim-stats* update-in [:scoring :len-deductions] #(into (or % []) [length-deduction])) + overall-score) + (tally-min-score min-score))) + (catch Exception e + (log/error "Err in computing score from residuals: " + (.getMessage e) ", fn: " (str (:expr pheno)) ", from: " (:expr pheno)) + (tally-min-score min-score))))) (defn score-fn - "Symbolic regression scoring" + "Symbolic regression scoring. + Uses primitive array for fast residual computation when input-ys-arr is available." {:malli/schema [:=> [:cat #'specs/ScoreFnArgs [:map {:closed false} [:max-leafs number?]] #'specs/GAPhenotype] number?]} - [{:keys [input-xs-list input-xs-count input-ys-vec] + [{:keys [input-xs-list input-xs-count input-ys-vec input-ys-arr] :as run-args} {:keys [max-leafs]} pheno] @@ -129,7 +165,7 @@ (tally-min-score min-score) (let [f-of-xs (ops-eval/eval-vec-pheno pheno run-args)] (if f-of-xs - (compute-score-from-actuals-and-expecteds pheno f-of-xs input-ys-vec leafs) + (compute-score-from-actuals-and-expecteds pheno f-of-xs input-ys-vec leafs input-ys-arr) (tally-min-score min-score))))) (catch Exception e (log/error "Err in score fn: " (.getMessage e) ", fn: " (str (:expr pheno)) ", from: " (:expr pheno)) diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index f3f0054b..bb15434b 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -377,6 +377,7 @@ :input-xs-count (count input-xs-exprs) :input-xs-vec input-xs-vec :input-ys-vec input-ys-vec + :input-ys-arr (double-array input-ys-vec) :input-iters (or input-iters iters) :initial-phenos initial-phenos :input-phenos-count input-phenos-count @@ -736,6 +737,6 @@ (comment (println "FN SCHEMAS: " (m/function-schemas))) (comment (macroexpand-1 `(log/info "Hello"))) (comment (log/info "Hello")) -(comment (run-app-without-gui)) +(comment (run-app-without-gui [1 2 3] [6 12 99])) (comment (run-app-with-gui {:use-flamechart true})) (comment (run-app-with-gui)) diff --git a/src/closyr/util/spec.clj b/src/closyr/util/spec.clj index 346ada8e..016bfe06 100644 --- a/src/closyr/util/spec.clj +++ b/src/closyr/util/spec.clj @@ -215,6 +215,7 @@ [:input-xs-count #'PointsCount] [:input-xs-vec #'NumberVector] [:input-ys-vec #'NumberVector] + [:input-ys-arr {:optional true} some?] [:input-iters #'Iterations] [:initial-phenos [:maybe #'GAPopulationPhenotypes]] [:input-phenos-count [:maybe #'PopulationCount]] @@ -232,6 +233,7 @@ [:map {:closed false} [:input-ys-vec #'NumberVector] + [:input-ys-arr {:optional true} some?] [:input-xs-list #'PrimitiveArrayOfIExpr] [:input-xs-count #'PointsCount]]) diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index bec120f7..df7baed9 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -38,7 +38,7 @@ (fn [run-config run-args] (reset! args* [(dissoc run-config :initial-muts :initial-phenos :input-xs-exprs :input-ys-exprs) - (dissoc run-args :extended-domain-args :initial-phenos :input-xs-list)]) + (dissoc run-args :extended-domain-args :initial-phenos :input-xs-list :input-ys-arr)]) {:iters-done 123 :final-population {:pop [] :score-fn #() @@ -83,7 +83,7 @@ (fn [run-config run-args] (reset! args* [(dissoc run-config :initial-muts :initial-phenos :input-xs-exprs :input-ys-exprs) - (dissoc run-args :extended-domain-args :initial-phenos :input-xs-list)]) + (dissoc run-args :extended-domain-args :initial-phenos :input-xs-list :input-ys-arr)]) {:iters-done 123 :final-population {:pop [] :score-fn #() @@ -371,5 +371,6 @@ :input-xs-count :input-xs-list :input-xs-vec + :input-ys-arr :input-ys-vec :max-leafs})))) From 9356d2553f8bf4e78776c57bfd8cc13e160f7ad1 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 14:31:10 -0700 Subject: [PATCH 015/224] update deptree info --- doc/depstree.txt | 103 ++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/doc/depstree.txt b/doc/depstree.txt index ce25a038..8ffc50f2 100644 --- a/doc/depstree.txt +++ b/doc/depstree.txt @@ -1,5 +1,5 @@ - [ch.qos.logback/logback-classic "1.5.0"] - [ch.qos.logback/logback-core "1.5.0"] + [ch.qos.logback/logback-classic "1.5.23"] + [ch.qos.logback/logback-core "1.5.23"] [clojure-complete "0.2.5" :exclusions [[org.clojure/clojure]]] [flames "0.5.0"] [http-kit "2.6.0"] @@ -26,30 +26,30 @@ [org.swinglabs/swingx "1.6.1" :scope "runtime"] [com.jhlabs/filters "2.0.235" :scope "runtime"] [org.swinglabs/swing-worker "1.1" :scope "runtime"] - [metosin/malli "0.14.0"] + [metosin/malli "0.20.0"] [borkdude/dynaload "0.3.5"] - [borkdude/edamame "1.3.23"] - [fipp "0.6.26"] + [borkdude/edamame "1.4.32"] + [fipp "0.6.29"] [org.clojure/core.rrb-vector "0.1.2"] [mvxcvi/arrangement "2.1.0"] [org.clojure/test.check "1.1.1"] [nrepl "0.8.3" :exclusions [[org.clojure/clojure]]] - [org.apache.logging.log4j/log4j-api "2.23.0"] - [org.apache.logging.log4j/log4j-core "2.23.0"] - [org.clojure/clojure "1.11.1"] - [org.clojure/core.specs.alpha "0.2.62"] - [org.clojure/spec.alpha "0.3.218"] - [org.clojure/core.async "1.6.681"] - [org.clojure/tools.analyzer.jvm "1.2.3"] - [org.clojure/core.memoize "1.0.253"] - [org.clojure/core.cache "1.0.225"] - [org.clojure/data.priority-map "1.1.0"] - [org.clojure/tools.analyzer "1.1.1"] - [org.clojure/tools.reader "1.3.6"] + [org.apache.logging.log4j/log4j-api "2.25.3"] + [org.apache.logging.log4j/log4j-core "2.25.3"] + [org.clojure/clojure "1.12.4"] + [org.clojure/core.specs.alpha "0.4.74"] + [org.clojure/spec.alpha "0.5.238"] + [org.clojure/core.async "1.8.741"] + [org.clojure/tools.analyzer.jvm "1.3.2"] + [org.clojure/core.memoize "1.1.266"] + [org.clojure/core.cache "1.1.234"] + [org.clojure/data.priority-map "1.2.0"] + [org.clojure/tools.analyzer "1.2.0"] + [org.clojure/tools.reader "1.5.0"] [org.ow2.asm/asm "9.2"] [org.clojure/data.csv "1.1.0"] - [org.clojure/tools.cli "1.1.230"] - [org.knowm.xchart/xchart "3.8.7"] + [org.clojure/tools.cli "1.2.245"] + [org.knowm.xchart/xchart "3.8.8"] [com.madgag/animated-gif-lib "1.4"] [de.erichseifert.vectorgraphics2d/VectorGraphics2D "0.13"] [de.rototor.pdfbox/graphics2d "3.0.1"] @@ -65,57 +65,60 @@ [org.junit.jupiter/junit-jupiter-engine "5.10.1" :scope "runtime"] [org.junit.platform/junit-platform-engine "1.10.1" :scope "runtime"] [org.junit.jupiter/junit-jupiter-params "5.10.1"] - [org.matheclipse/matheclipse-core "3.1.0-20240220.194259-65" :exclusions [[org.slf4j/slf4j-api] [com.fasterxml.jackson.core/jackson-core]]] - [com.esotericsoftware/kryo "5.6.0"] + [org.matheclipse/matheclipse-core "3.1.0-SNAPSHOT" :exclusions [[org.slf4j/slf4j-api] [com.fasterxml.jackson.core/jackson-core]]] + [com.esotericsoftware/kryo "5.6.2"] [com.esotericsoftware/minlog "1.3.1"] [com.esotericsoftware/reflectasm "1.11.9"] - [org.objenesis/objenesis "3.3"] + [org.objenesis/objenesis "3.4"] [com.github.freva/ascii-table "1.8.0"] [com.univocity/univocity-parsers "2.9.1"] + [commons-io "2.18.0"] [de.labathome/AdaptiveQuadrature "1.0.2"] - [it.unimi.dsi/fastutil-core "8.5.13"] - [org.apache.commons/commons-csv "1.10.0"] - [org.apache.commons/commons-lang3 "3.14.0"] - [org.apfloat/apfloat "1.13.0"] - [org.choco-solver/choco-solver "4.10.15-20240118.152009-24"] + [it.unimi.dsi/fastutil-core "8.5.15"] + [org.apache.commons/commons-csv "1.13.0"] + [commons-codec "1.17.2"] + [org.apache.commons/commons-lang3 "3.17.0"] + [org.apfloat/apfloat "1.14.1-SNAPSHOT"] + [org.choco-solver/choco-solver "4.10.18" :exclusions [[org.knowm.xchart/xchart]]] [dk.brics.automaton/automaton "1.11-8"] [net.sf.trove4j/trove4j "3.0.3"] [org.ehcache/sizeof "0.4.3"] [org.codehaus.janino/janino "3.1.12"] [org.codehaus.janino/commons-compiler "3.1.12"] [org.gavaghan/geodesy "1.1.3"] - [org.hipparchus/hipparchus-clustering "3.1-20240221.085047-25"] - [org.hipparchus/hipparchus-core "3.1-20240221.085047-25"] - [org.hipparchus/hipparchus-stat "3.1-20240221.085047-25"] - [org.hipparchus/hipparchus-fft "3.1-20240221.085047-25"] - [org.hipparchus/hipparchus-fitting "3.1-20240221.085047-25"] - [org.hipparchus/hipparchus-ode "3.1-20240221.085047-25"] - [org.hipparchus/hipparchus-optim "3.1-20240221.085047-25"] - [org.jgrapht/jgrapht-io "1.5.3-20240118.050300-19"] + [org.hipparchus/hipparchus-clustering "4.0-20250316.160502-27"] + [org.hipparchus/hipparchus-stat "4.0-20250316.160502-27"] + [org.hipparchus/hipparchus-fft "4.0-20250316.160502-27"] + [org.hipparchus/hipparchus-fitting "4.0-20250316.160502-27"] + [org.hipparchus/hipparchus-ode "4.0-20250316.160502-27"] + [org.hipparchus/hipparchus-optim "4.0-20250316.160502-27"] + [org.jgrapht/jgrapht-io "1.5.3-SNAPSHOT"] [org.antlr/antlr4-runtime "4.13.1"] [org.apache.commons/commons-text "1.11.0"] - [org.jgrapht/jgrapht-core "1.5.3-20240118.050300-19"] + [org.jgrapht/jgrapht-core "1.5.3-SNAPSHOT"] [org.jheaps/jheaps "0.14"] - [org.logicng/logicng-j11 "2.4.3"] - [org.matheclipse/matheclipse-external "3.1.0-20240220.194259-65"] - [com.fasterxml.jackson.core/jackson-databind "2.16.1"] - [com.fasterxml.jackson.core/jackson-annotations "2.16.1"] - [com.google.guava/guava "33.0.0-jre"] + [org.logicng/logicng "2.6.0"] + [org.matheclipse/matheclipse-external "3.1.0-SNAPSHOT"] + [com.fasterxml.jackson.core/jackson-databind "2.18.3"] + [com.fasterxml.jackson.core/jackson-annotations "2.18.3"] + [com.google.guava/guava "33.4.0-jre"] [com.google.code.findbugs/jsr305 "3.0.2"] - [com.google.errorprone/error_prone_annotations "2.23.0"] + [com.google.errorprone/error_prone_annotations "2.36.0"] [com.google.guava/failureaccess "1.0.2"] [com.google.guava/listenablefuture "9999.0-empty-to-avoid-conflict-with-guava"] - [com.google.j2objc/j2objc-annotations "2.8"] - [org.checkerframework/checker-qual "3.41.0"] - [io.pebbletemplates/pebble "3.2.2"] + [com.google.j2objc/j2objc-annotations "3.0.0"] + [org.checkerframework/checker-qual "3.43.0"] + [io.pebbletemplates/pebble "3.2.3"] [org.unbescape/unbescape "1.1.6.RELEASE"] - [org.matheclipse/matheclipse-parser "3.1.0-20240220.194259-65"] + [org.hipparchus/hipparchus-core "4.0-20250316.160502-27"] + [org.matheclipse/matheclipse-parser "3.1.0-SNAPSHOT"] [org.organicdesign/Paguro "3.10.3"] [org.jetbrains/annotations "23.0.0"] - [org.matheclipse/matheclipse-gpl "3.1.0-20240220.194259-65" :exclusions [[org.slf4j/slf4j-api] [com.fasterxml.jackson.core/jackson-core]]] - [org.apache.logging.log4j/log4j-1.2-api "2.22.1"] - [org.slf4j/jcl-over-slf4j "2.0.12"] - [org.slf4j/slf4j-api "2.0.12"] + [us.hebi.matlab.mat/mfl-core "0.5.15"] + [org.matheclipse/matheclipse-gpl "3.1.0-SNAPSHOT" :exclusions [[org.slf4j/slf4j-api] [com.fasterxml.jackson.core/jackson-core]]] + [org.apache.logging.log4j/log4j-1.2-api "2.24.3"] + [org.slf4j/jcl-over-slf4j "2.0.17"] + [org.slf4j/slf4j-api "2.0.17"] [seesaw "1.5.0"] [com.fifesoft/rsyntaxtextarea "2.5.6"] [com.jgoodies/forms "1.2.1"] From b247eeca58d6bbdf775ba3885a302a54a19268c7 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 15:44:16 -0700 Subject: [PATCH 016/224] improve GC tuning --- README.md | 10 +++++----- perf-profile/benchmark.clj | 28 +++++++++++++++++++--------- project.clj | 21 ++++++++++++++++++--- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e09cb02f..3c13b981 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ Use the application via GUI or in the terminal. There are 4 ways to run the application. See next section for details about the options you can provide when running the app. -### Run the provided realase JAR - -Requirements: Java +### Run the provided release JAR +Requirements: Java, Leiningen + $ lein uberjar $ java -jar closyr-0.1.0-SNAPSHOT-standalone.jar ...options here... @@ -72,11 +72,11 @@ Requirements: Java, Leiningen $ lein uberjar - $ java -jar target/uberjar/closyr-0.1.0-standalone.jar + $ java -jar target/uberjar/closyr-0.1.0-SNAPSHOT-standalone.jar You can also provide the same command-line options to `java` command, like: - $ java -jar target/uberjar/closyr-0.1.0-standalone.jar -t -p 25 -i 5 -x 0,1,2,3,4,5,6 -y 1,2,30,4,5,6,10 + $ java -jar target/uberjar/closyr-0.1.0-SNAPSHOT-standalone.jar -t -p 25 -i 5 -x 0,1,2,3,4,5,6 -y 1,2,30,4,5,6,10 ## Options | Short, Long Option | Required? | Example | Default | Description | diff --git a/perf-profile/benchmark.clj b/perf-profile/benchmark.clj index d1fd3869..e801e475 100644 --- a/perf-profile/benchmark.clj +++ b/perf-profile/benchmark.clj @@ -5,10 +5,10 @@ [closyr.ops :as ops] [closyr.ga :as ga])) -(defn benchmark-evolution [pop-size n-iters] +(defn benchmark-evolution [pop-size n-iters max-leafs xs-max] (let [phenos (ops-init/initial-phenotypes pop-size) muts (ops-init/initial-mutations) - xs (vec (range 0.1 10.0 0.2)) + xs (vec (range 0.1 xs-max 0.2)) ys (mapv #(+ (* % %) (Math/sin %)) xs) input-xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) run-args {:input-xs-list input-xs-list @@ -16,7 +16,7 @@ :input-xs-vec xs :input-ys-vec ys :input-ys-arr (double-array ys)} - run-config {:max-leafs 40} + run-config {:max-leafs max-leafs} score-fn (partial ops/score-fn run-args run-config) mut-fn (partial ops/mutation-fn run-config muts) cross-fn (partial ops/crossover-fn run-config muts) @@ -40,12 +40,22 @@ (println "\n=== Evolution Benchmark ===\n") -(doseq [pop-size [10 50 100 500 1000 2000 4000 10000 50000]] - (let [result (benchmark-evolution pop-size 10)] - (printf "Pop %4d: %6.1f ms/iter, %8.0f phenos/sec\n" - pop-size - (:ms-per-iter result) - (:phenos-per-sec result)))) +(def results* (atom [])) + +(doseq [pop-size [10 100 500 1000 20000]] + (swap! results* concat ["\n"]) + (doseq [xs-max [5.0 20.0]] + (doseq [max-leafs [40 120]] + (let [result (benchmark-evolution pop-size 10 max-leafs xs-max)] + (swap! results* concat [(format "Pop %4d, XsMax %4f, MaxLeafs %4d: %6.1f ms/iter, %8.0f phenos/sec\n" + pop-size + xs-max + max-leafs + (:ms-per-iter result) + (:phenos-per-sec result))]))))) (println "\n=== Done ===") +(println "\n=== Results ===") +(println @results*) + (System/exit 0) diff --git a/project.clj b/project.clj index b07af3a9..a9044da3 100644 --- a/project.clj +++ b/project.clj @@ -38,10 +38,25 @@ :target-path "target/%s" ;; JVM tuning for better GA performance - :jvm-opts ["-Xmx4g" ; Max heap size - "-XX:+UseG1GC" ; G1 garbage collector (better for large heaps) + :jvm-opts ["-Xms4g" ; Initial heap = max (avoid resizing) + "-Xmx4g" ; Max heap size + + + ;"-XX:+UseG1GC" ; G1 garbage collector (better for large heaps) + + ;"-XX:+UseZGC" + ;"-XX:+ZGenerational" + + "-XX:+UseParallelGC" + + "-XX:MaxGCPauseMillis=100" ; Target max GC pause - "-XX:+UseStringDeduplication"] ; Reduce memory for duplicate strings + "-XX:+UseStringDeduplication" ; Reduce memory for duplicate strings + "-XX:+AlwaysPreTouch" ; Pre-touch memory pages at startup + "-XX:+UseNUMA" ; Optimize for NUMA architectures + "-XX:+OptimizeStringConcat" ; Optimize string concatenation + "-XX:AutoBoxCacheMax=20000" ; Cache more Integer objects (reduce boxing overhead) + "-XX:+DisableExplicitGC"] ; Ignore System.gc() calls ;; Note: direct-linking only in uberjar profile (breaks dynamic var rebinding in tests) :profiles {:uberjar {:aot :all From 98b5fa63522003b2324bccf92a4cd4956f352cf7 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 15:49:42 -0700 Subject: [PATCH 017/224] project meta --- project.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project.clj b/project.clj index a9044da3..83df5041 100644 --- a/project.clj +++ b/project.clj @@ -1,6 +1,6 @@ (defproject closyr "0.1.0-SNAPSHOT" - :description "FIXME: write description" - :url "http://example.com/FIXME" + :description "Symbolic Regression in Clojure" + :url "https://github.com/ogeagla/closyr" :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" :url "https://www.eclipse.org/legal/epl-2.0/"} :dependencies [[org.clojure/clojure "1.12.4"] From bda3d33046ac91c4fe50ac2cac35a5ef5db62e20 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 18:54:12 -0700 Subject: [PATCH 018/224] java API for closyr WIP --- project.clj | 6 +- .../java/org/closyr/core/FindFormula.java | 384 ++++++++++++++++-- test/closyr/find_formula_java_test.clj | 159 ++++++++ test/org/closyr/core/FindFormulaTest.java | 257 +++++++++++- 4 files changed, 776 insertions(+), 30 deletions(-) create mode 100644 test/closyr/find_formula_java_test.clj diff --git a/project.clj b/project.clj index 83df5041..112657cb 100644 --- a/project.clj +++ b/project.clj @@ -34,6 +34,7 @@ :releases false :snapshots true}} :java-source-paths ["src/main/java"] + :java-test-paths ["test"] :main ^:skip-aot closyr.core :target-path "target/%s" @@ -61,7 +62,10 @@ :profiles {:uberjar {:aot :all :manifest {"Multi-Release" true} - :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}} + :jvm-opts ["-Dclojure.compiler.direct-linking=true"]} + :test {:dependencies [[org.junit.jupiter/junit-jupiter-api "5.10.2"] + [org.junit.jupiter/junit-jupiter-engine "5.10.2"] + [org.junit.platform/junit-platform-launcher "1.10.2"]]}} :plugins [[lein-cloverage "1.2.4"] [lein-vanity "0.2.0"] diff --git a/src/main/java/org/closyr/core/FindFormula.java b/src/main/java/org/closyr/core/FindFormula.java index fdb34cd0..ea0d2daa 100644 --- a/src/main/java/org/closyr/core/FindFormula.java +++ b/src/main/java/org/closyr/core/FindFormula.java @@ -1,56 +1,385 @@ package org.closyr.core; -import org.matheclipse.core.eval.Errors; +import clojure.java.api.Clojure; +import clojure.lang.IFn; +import clojure.lang.IPersistentMap; +import clojure.lang.Keyword; +import clojure.lang.PersistentVector; import org.matheclipse.core.eval.EvalEngine; -import org.matheclipse.core.eval.interfaces.AbstractFunctionEvaluator; import org.matheclipse.core.eval.interfaces.AbstractFunctionOptionEvaluator; import org.matheclipse.core.eval.interfaces.IFunctionEvaluator; import org.matheclipse.core.expression.F; import org.matheclipse.core.expression.ImplementationStatus; -import org.matheclipse.core.expression.S; import org.matheclipse.core.interfaces.IAST; import org.matheclipse.core.interfaces.IExpr; import org.matheclipse.core.interfaces.ISymbol; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +/** + * FindFormula - A Symja function that uses genetic algorithm-based symbolic regression + * to find a formula that best fits the given data points. + * + *

Usage from Symja:

+ *
+ *   FindFormula[{{x1, y1}, {x2, y2}, ...}, x]
+ *   FindFormula[{{x1, y1}, {x2, y2}, ...}, x, n]
+ *   FindFormula[{{x1, y1}, {x2, y2}, ...}, x, n, All]
+ * 
+ * + *

Usage from Java:

+ *
+ *   double[] xs = {1.0, 2.0, 3.0, 4.0, 5.0};
+ *   double[] ys = {2.0, 4.0, 6.0, 8.0, 10.0};
+ *   FindFormula.Result result = FindFormula.findFormula(xs, ys);
+ *   System.out.println("Best formula: " + result.getFormulaString());
+ *   System.out.println("Score: " + result.getScore());
+ * 
+ */ public class FindFormula extends AbstractFunctionOptionEvaluator { + private static volatile boolean clojureInitialized = false; + private static IFn runAppWithoutGuiFn; + private static IFn initialPhenotypesFn; + private static IFn initialMutationsFn; + private static IFn doublesToExprsFn; + private static IFn runFindFormulaFn; + + /** + * Result of a symbolic regression run. + */ + public static class Result { + private final String formulaString; + private final double score; + private final IExpr formulaExpr; + private final int iterationsDone; + private final List allSolutions; + + public Result(String formulaString, double score, IExpr formulaExpr, + int iterationsDone, List allSolutions) { + this.formulaString = formulaString; + this.score = score; + this.formulaExpr = formulaExpr; + this.iterationsDone = iterationsDone; + this.allSolutions = allSolutions; + } + + /** The formula as a human-readable string (e.g., "2*x + 1") */ + public String getFormulaString() { + return formulaString; + } + + /** The fitness score (higher/closer to 0 is better, negative values) */ + public double getScore() { + return score; + } + + /** The formula as a Symja IExpr for further symbolic computation */ + public IExpr getFormulaExpr() { + return formulaExpr; + } + + /** Number of GA iterations completed */ + public int getIterationsDone() { + return iterationsDone; + } + + /** All solutions from the final population, sorted by score (best first) */ + public List getAllSolutions() { + return allSolutions; + } + + @Override + public String toString() { + return "Result{formula='" + formulaString + "', score=" + score + + ", iterations=" + iterationsDone + "}"; + } + } + + /** + * A single solution (phenotype) from the genetic algorithm. + */ + public static class Solution { + private final String formulaString; + private final double score; + private final IExpr formulaExpr; + + public Solution(String formulaString, double score, IExpr formulaExpr) { + this.formulaString = formulaString; + this.score = score; + this.formulaExpr = formulaExpr; + } + + public String getFormulaString() { + return formulaString; + } + + public double getScore() { + return score; + } + + public IExpr getFormulaExpr() { + return formulaExpr; + } + + @Override + public String toString() { + return "Solution{formula='" + formulaString + "', score=" + score + "}"; + } + } + + /** + * Configuration for the symbolic regression solver. + */ + public static class Config { + private int iterations = 20; + private int populationSize = 100; + private int maxLeafs = 40; + + public Config() {} + + /** Number of GA iterations (default: 20) */ + public Config iterations(int iterations) { + this.iterations = iterations; + return this; + } + + /** Population size (default: 100) */ + public Config populationSize(int populationSize) { + this.populationSize = populationSize; + return this; + } + + /** Maximum number of leaves in expression tree (default: 40) */ + public Config maxLeafs(int maxLeafs) { + this.maxLeafs = maxLeafs; + return this; + } + + public int getIterations() { return iterations; } + public int getPopulationSize() { return populationSize; } + public int getMaxLeafs() { return maxLeafs; } + } + public FindFormula() { - // empty constructor + // empty constructor for Symja + } + + /** + * Initialize the Clojure runtime and load required namespaces. + * This is called automatically but can be called explicitly to warm up. + */ + public static synchronized void initializeClojure() { + if (clojureInitialized) { + return; + } + + // Require the necessary Clojure namespaces + IFn require = Clojure.var("clojure.core", "require"); + require.invoke(Clojure.read("closyr.symbolic-regression")); + require.invoke(Clojure.read("closyr.ops.initialize")); + require.invoke(Clojure.read("closyr.ops.common")); + + // Get references to the Clojure functions we need + runAppWithoutGuiFn = Clojure.var("closyr.symbolic-regression", "run-app-without-gui"); + runFindFormulaFn = Clojure.var("closyr.symbolic-regression", "run-find-formula"); + initialPhenotypesFn = Clojure.var("closyr.ops.initialize", "initial-phenotypes"); + initialMutationsFn = Clojure.var("closyr.ops.initialize", "initial-mutations"); + doublesToExprsFn = Clojure.var("closyr.ops.common", "doubles->exprs"); + + clojureInitialized = true; } + /** + * Find a formula that fits the given x,y data points using default settings. + * + * @param xs array of x values + * @param ys array of y values (same length as xs) + * @return Result containing the best formula found + */ + public static Result findFormula(double[] xs, double[] ys) { + return findFormula(xs, ys, new Config()); + } + + /** + * Find a formula that fits the given x,y data points with custom configuration. + * + * @param xs array of x values + * @param ys array of y values (same length as xs) + * @param config configuration for the solver + * @return Result containing the best formula found + */ + public static Result findFormula(double[] xs, double[] ys, Config config) { + if (xs == null || ys == null) { + throw new IllegalArgumentException("xs and ys arrays cannot be null"); + } + if (xs.length != ys.length) { + throw new IllegalArgumentException("xs and ys arrays must have the same length"); + } + if (xs.length < 2) { + throw new IllegalArgumentException("At least 2 data points are required"); + } + + initializeClojure(); + + // Convert Java arrays to Clojure vectors + List xsList = new ArrayList<>(xs.length); + List ysList = new ArrayList<>(ys.length); + for (int i = 0; i < xs.length; i++) { + xsList.add(xs[i]); + ysList.add(ys[i]); + } + PersistentVector xsVec = PersistentVector.create(xsList); + PersistentVector ysVec = PersistentVector.create(ysList); + + // Convert to IExpr vectors + Object xsExprs = doublesToExprsFn.invoke(xsVec); + Object ysExprs = doublesToExprsFn.invoke(ysVec); + + // Create initial population and mutations + Object initialPhenos = initialPhenotypesFn.invoke(config.getPopulationSize()); + Object initialMuts = initialMutationsFn.invoke(); + + // Build the run configuration map + Keyword initialPhenosKey = Keyword.intern("initial-phenos"); + Keyword initialMutsKey = Keyword.intern("initial-muts"); + Keyword itersKey = Keyword.intern("iters"); + Keyword useGuiKey = Keyword.intern("use-gui?"); + Keyword useFlamechartKey = Keyword.intern("use-flamechart"); + Keyword inputXsExprsKey = Keyword.intern("input-xs-exprs"); + Keyword inputYsExprsKey = Keyword.intern("input-ys-exprs"); + Keyword maxLeafsKey = Keyword.intern("max-leafs"); + + IFn hashMap = Clojure.var("clojure.core", "hash-map"); + Object runConfig = hashMap.invoke( + initialPhenosKey, initialPhenos, + initialMutsKey, initialMuts, + itersKey, config.getIterations(), + useGuiKey, false, + useFlamechartKey, false, + maxLeafsKey, config.getMaxLeafs(), + inputXsExprsKey, xsExprs, + inputYsExprsKey, ysExprs + ); + + // Run the solver + Object result = runFindFormulaFn.invoke(runConfig); + + // Extract results from the Clojure map + return extractResult(result); + } + + @SuppressWarnings("unchecked") + private static Result extractResult(Object result) { + if (!(result instanceof IPersistentMap)) { + throw new RuntimeException("Unexpected result type from Clojure: " + result.getClass()); + } + + IPersistentMap resultMap = (IPersistentMap) result; + + Keyword itersDoneKey = Keyword.intern("iters-done"); + Keyword finalPopulationKey = Keyword.intern("final-population"); + Keyword popKey = Keyword.intern("pop"); + Keyword exprKey = Keyword.intern("expr"); + Keyword scoreKey = Keyword.intern("score"); + + int itersDone = ((Number) resultMap.valAt(itersDoneKey)).intValue(); + IPersistentMap finalPopulation = (IPersistentMap) resultMap.valAt(finalPopulationKey); + Object pop = finalPopulation.valAt(popKey); + + List allSolutions = new ArrayList<>(); + + if (pop instanceof Iterable) { + for (Object phenotype : (Iterable) pop) { + if (phenotype instanceof IPersistentMap) { + IPersistentMap phenoMap = (IPersistentMap) phenotype; + Object exprObj = phenoMap.valAt(exprKey); + Object scoreObj = phenoMap.valAt(scoreKey); + + IExpr expr = (exprObj instanceof IExpr) ? (IExpr) exprObj : null; + String formulaStr = (expr != null) ? expr.toString() : "unknown"; + double score = (scoreObj instanceof Number) ? ((Number) scoreObj).doubleValue() : Double.NEGATIVE_INFINITY; + allSolutions.add(new Solution(formulaStr, score, expr)); + } + } + } + + // Sort by score (descending - higher/closer to 0 is better) + allSolutions.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); + + Solution best = allSolutions.isEmpty() ? new Solution("x", Double.NEGATIVE_INFINITY, null) : allSolutions.get(0); + + return new Result( + best.getFormulaString(), + best.getScore(), + best.getFormulaExpr(), + itersDone, + allSolutions + ); + } + + // ==================== Symja Integration ==================== + + /** + * Symja function evaluator implementation. + * Called when FindFormula[data, x] is evaluated in Symja. + */ @Override - public IExpr evaluate(IAST ast, final int argSize, final IExpr[] options, final EvalEngine engine, - IAST originalAST) { + public IExpr evaluate(IAST ast, final int argSize, final IExpr[] options, + final EvalEngine engine, IAST originalAST) { IExpr data = ast.arg1(); IExpr x = ast.arg2(); - if (x.isVariable()) { - int n = Integer.MAX_VALUE; - IExpr arg4 = S.All; - - int[] isMatrix = data.isMatrix(); - if (isMatrix != null && isMatrix[1] == 2 && data.isList()) { - IAST matrix = (IAST) data; - double[][] doubleMatrix = matrix.toDoubleMatrix(); - if (doubleMatrix != null) { - if (ast.argSize() > 2) { - n = ast.arg3().toIntDefault(); - if (n < 0) { - // Positive machine-sized integer expected at position `2` in `1`. -// return Errors.printMessage(S.FindFormula, "intpm", F.List(F.C3, ast), engine); - } - if (ast.argSize() > 3) { - arg4 = ast.arg4(); - } - } - // TODO add implementation here + if (!x.isVariable()) { + return F.NIL; + } - } + int[] isMatrix = data.isMatrix(); + if (isMatrix == null || isMatrix[1] != 2 || !data.isList()) { + return F.NIL; + } + + IAST matrix = (IAST) data; + double[][] doubleMatrix = matrix.toDoubleMatrix(); + if (doubleMatrix == null) { + return F.NIL; + } + + // Extract x and y values from the matrix + double[] xs = new double[doubleMatrix.length]; + double[] ys = new double[doubleMatrix.length]; + for (int i = 0; i < doubleMatrix.length; i++) { + xs[i] = doubleMatrix[i][0]; + ys[i] = doubleMatrix[i][1]; + } + + // Parse optional arguments + Config config = new Config(); + if (argSize > 2) { + int n = ast.arg3().toIntDefault(-1); + if (n > 0) { + config.iterations(n); } } + + try { + Result result = findFormula(xs, ys, config); + IExpr formulaExpr = result.getFormulaExpr(); + + if (formulaExpr != null) { + // Substitute the Symja variable for our internal 'x' + // The formula uses a dummy symbol 'x', we need to map it to the user's variable + return formulaExpr; + } + } catch (Exception e) { + System.out.println("FindFormula: " + e.getMessage()); +// engine.printMessage("FindFormula: " + e.getMessage()); + } + return F.NIL; } @@ -67,6 +396,5 @@ public int[] expectedArgSize(IAST ast) { @Override public void setUp(final ISymbol newSymbol) { super.setUp(newSymbol); - // setOptions(newSymbol, S.GenerateConditions, S.False); } } diff --git a/test/closyr/find_formula_java_test.clj b/test/closyr/find_formula_java_test.clj new file mode 100644 index 00000000..0942628d --- /dev/null +++ b/test/closyr/find_formula_java_test.clj @@ -0,0 +1,159 @@ +(ns closyr.find-formula-java-test + "Tests for the Java FindFormula API that calls into Clojure symbolic regression." + (:require + [clojure.test :refer :all]) + (:import + (org.closyr.core + FindFormula + FindFormula$Config + FindFormula$Result + FindFormula$Solution))) + + +(deftest test-find-formula-with-linear-data + (testing "Simple linear relationship: y = 2x" + (let [xs (double-array [1.0 2.0 3.0 4.0 5.0]) + ys (double-array [2.0 4.0 6.0 8.0 10.0]) + config (-> (FindFormula$Config.) + (.iterations 5) + (.populationSize 30)) + result (FindFormula/findFormula xs ys config)] + (is (instance? FindFormula$Result result)) + (is (string? (.getFormulaString result))) + (is (not (empty? (.getFormulaString result)))) + (is (number? (.getScore result))) + (is (pos? (.getIterationsDone result))) + (is (seq (.getAllSolutions result)))))) + + +(deftest test-find-formula-with-default-config + (testing "Using default configuration" + (let [xs (double-array [0.0 1.0 2.0 3.0]) + ys (double-array [1.0 2.0 5.0 10.0]) + result (FindFormula/findFormula xs ys)] + (is (instance? FindFormula$Result result)) + (is (= 20 (.getIterationsDone result))) ; default iterations + (is (string? (.getFormulaString result)))))) + + +(deftest test-result-contains-symja-expr + (testing "Result contains a Symja IExpr for symbolic computation" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [3.0 6.0 9.0]) + config (-> (FindFormula$Config.) + (.iterations 3) + (.populationSize 20)) + result (FindFormula/findFormula xs ys config) + formula-expr (.getFormulaExpr result)] + (is (some? formula-expr) "Formula IExpr should not be null") + (is (string? (.toString formula-expr))) + (is (not (empty? (.toString formula-expr))))))) + + +(deftest test-all-solutions-sorted-by-score + (testing "Solutions are sorted by score (best first)" + (let [xs (double-array [1.0 2.0 3.0 4.0]) + ys (double-array [2.0 4.0 8.0 16.0]) + config (-> (FindFormula$Config.) + (.iterations 3) + (.populationSize 30)) + result (FindFormula/findFormula xs ys config) + solutions (.getAllSolutions result)] + (is (seq solutions)) + ;; Verify sorted by score descending (higher/closer to 0 is better) + (doseq [[prev curr] (partition 2 1 solutions)] + (is (>= (.getScore prev) (.getScore curr)) + "Solutions should be sorted by score descending")) + ;; Best solution should match result's formula + (is (= (.getFormulaString result) + (.getFormulaString (first solutions)))) + (is (= (.getScore result) + (.getScore (first solutions))))))) + + +(deftest test-null-xs-throws-exception + (testing "Null xs array throws IllegalArgumentException" + (let [ys (double-array [1.0 2.0 3.0])] + (is (thrown-with-msg? IllegalArgumentException #"null" + (FindFormula/findFormula nil ys)))))) + + +(deftest test-null-ys-throws-exception + (testing "Null ys array throws IllegalArgumentException" + (let [xs (double-array [1.0 2.0 3.0])] + (is (thrown-with-msg? IllegalArgumentException #"null" + (FindFormula/findFormula xs nil)))))) + + +(deftest test-mismatched-array-lengths-throws-exception + (testing "Mismatched array lengths throws IllegalArgumentException" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [1.0 2.0])] + (is (thrown-with-msg? IllegalArgumentException #"same length" + (FindFormula/findFormula xs ys)))))) + + +(deftest test-too-few-data-points-throws-exception + (testing "Less than 2 data points throws IllegalArgumentException" + (let [xs (double-array [1.0]) + ys (double-array [2.0])] + (is (thrown-with-msg? IllegalArgumentException #"At least 2" + (FindFormula/findFormula xs ys)))))) + + +(deftest test-config-builder + (testing "Config builder works correctly" + (let [config (-> (FindFormula$Config.) + (.iterations 50) + (.populationSize 200) + (.maxLeafs 30))] + (is (= 50 (.getIterations config))) + (is (= 200 (.getPopulationSize config))) + (is (= 30 (.getMaxLeafs config)))))) + + +(deftest test-result-to-string + (testing "Result toString includes expected fields" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [2.0 4.0 6.0]) + config (-> (FindFormula$Config.) + (.iterations 2) + (.populationSize 10)) + result (FindFormula/findFormula xs ys config) + str-repr (.toString result)] + (is (string? str-repr)) + (is (.contains str-repr "Result{")) + (is (.contains str-repr "formula=")) + (is (.contains str-repr "score=")) + (is (.contains str-repr "iterations="))))) + + +(deftest test-solution-to-string + (testing "Solution toString includes expected fields" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [1.0 4.0 9.0]) + config (-> (FindFormula$Config.) + (.iterations 2) + (.populationSize 10)) + result (FindFormula/findFormula xs ys config) + solution (first (.getAllSolutions result)) + str-repr (.toString solution)] + (is (string? str-repr)) + (is (.contains str-repr "Solution{")) + (is (.contains str-repr "formula=")) + (is (.contains str-repr "score="))))) + + +(deftest test-initialize-clojure-idempotent + (testing "initializeClojure can be called multiple times safely" + (FindFormula/initializeClojure) + (FindFormula/initializeClojure) + (FindFormula/initializeClojure) + ;; Verify the API still works + (let [xs (double-array [1.0 2.0]) + ys (double-array [1.0 2.0]) + config (-> (FindFormula$Config.) + (.iterations 1) + (.populationSize 5)) + result (FindFormula/findFormula xs ys config)] + (is (some? result))))) diff --git a/test/org/closyr/core/FindFormulaTest.java b/test/org/closyr/core/FindFormulaTest.java index 2ab6d266..0c1c0c16 100644 --- a/test/org/closyr/core/FindFormulaTest.java +++ b/test/org/closyr/core/FindFormulaTest.java @@ -1,13 +1,26 @@ package org.closyr.core; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.matheclipse.core.interfaces.IExpr; + +import java.util.List; import static org.junit.jupiter.api.Assertions.*; +/** + * Tests for the FindFormula Java API that calls into Clojure symbolic regression. + */ class FindFormulaTest { + @BeforeAll + static void initClojure() { + // Pre-initialize Clojure runtime to avoid timeout in first test + FindFormula.initializeClojure(); + } + @BeforeEach void setUp() { } @@ -17,6 +30,248 @@ void tearDown() { } @Test - void evaluate() { + void testFindFormulaWithLinearData() { + // Simple linear relationship: y = 2x + double[] xs = {1.0, 2.0, 3.0, 4.0, 5.0}; + double[] ys = {2.0, 4.0, 6.0, 8.0, 10.0}; + + FindFormula.Config config = new FindFormula.Config() + .iterations(10) + .populationSize(50); + + FindFormula.Result result = FindFormula.findFormula(xs, ys, config); + + assertNotNull(result); + assertNotNull(result.getFormulaString()); + assertFalse(result.getFormulaString().isEmpty()); + assertTrue(result.getIterationsDone() > 0); + assertNotNull(result.getAllSolutions()); + assertFalse(result.getAllSolutions().isEmpty()); + + System.out.println("Linear data result: " + result); + } + + @Test + void testFindFormulaWithQuadraticData() { + // Quadratic relationship: y = x^2 + double[] xs = {1.0, 2.0, 3.0, 4.0, 5.0}; + double[] ys = {1.0, 4.0, 9.0, 16.0, 25.0}; + + FindFormula.Config config = new FindFormula.Config() + .iterations(15) + .populationSize(100); + + FindFormula.Result result = FindFormula.findFormula(xs, ys, config); + + assertNotNull(result); + assertNotNull(result.getFormulaString()); + assertTrue(result.getIterationsDone() > 0); + + System.out.println("Quadratic data result: " + result); + } + + @Test + void testFindFormulaWithDefaultConfig() { + double[] xs = {0.0, 1.0, 2.0, 3.0}; + double[] ys = {1.0, 2.0, 5.0, 10.0}; + + FindFormula.Result result = FindFormula.findFormula(xs, ys); + + assertNotNull(result); + assertNotNull(result.getFormulaString()); + assertEquals(20, result.getIterationsDone()); // default iterations + + System.out.println("Default config result: " + result); + } + + @Test + void testResultContainsSymjaExpr() { + double[] xs = {1.0, 2.0, 3.0}; + double[] ys = {3.0, 6.0, 9.0}; + + FindFormula.Config config = new FindFormula.Config() + .iterations(5) + .populationSize(30); + + FindFormula.Result result = FindFormula.findFormula(xs, ys, config); + + assertNotNull(result); + IExpr formulaExpr = result.getFormulaExpr(); + assertNotNull(formulaExpr, "Formula IExpr should not be null"); + + // The IExpr should be usable for symbolic computation + String exprString = formulaExpr.toString(); + assertNotNull(exprString); + assertFalse(exprString.isEmpty()); + + System.out.println("IExpr formula: " + exprString); + } + + @Test + void testAllSolutionsSortedByScore() { + double[] xs = {1.0, 2.0, 3.0, 4.0}; + double[] ys = {2.0, 4.0, 8.0, 16.0}; + + FindFormula.Config config = new FindFormula.Config() + .iterations(5) + .populationSize(50); + + FindFormula.Result result = FindFormula.findFormula(xs, ys, config); + + List solutions = result.getAllSolutions(); + assertNotNull(solutions); + assertFalse(solutions.isEmpty()); + + // Verify solutions are sorted by score (descending - higher is better) + for (int i = 1; i < solutions.size(); i++) { + assertTrue(solutions.get(i - 1).getScore() >= solutions.get(i).getScore(), + "Solutions should be sorted by score descending"); + } + + // Best solution should match the result's formula + assertEquals(result.getFormulaString(), solutions.get(0).getFormulaString()); + assertEquals(result.getScore(), solutions.get(0).getScore()); + } + + @Test + void testNullXsThrowsException() { + double[] ys = {1.0, 2.0, 3.0}; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> FindFormula.findFormula(null, ys) + ); + assertTrue(exception.getMessage().contains("null")); + } + + @Test + void testNullYsThrowsException() { + double[] xs = {1.0, 2.0, 3.0}; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> FindFormula.findFormula(xs, null) + ); + assertTrue(exception.getMessage().contains("null")); + } + + @Test + void testMismatchedArrayLengthsThrowsException() { + double[] xs = {1.0, 2.0, 3.0}; + double[] ys = {1.0, 2.0}; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> FindFormula.findFormula(xs, ys) + ); + assertTrue(exception.getMessage().contains("same length")); + } + + @Test + void testTooFewDataPointsThrowsException() { + double[] xs = {1.0}; + double[] ys = {2.0}; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> FindFormula.findFormula(xs, ys) + ); + assertTrue(exception.getMessage().contains("At least 2")); + } + + @Test + void testConfigBuilder() { + FindFormula.Config config = new FindFormula.Config() + .iterations(50) + .populationSize(200) + .maxLeafs(30); + + assertEquals(50, config.getIterations()); + assertEquals(200, config.getPopulationSize()); + assertEquals(30, config.getMaxLeafs()); + } + + @Test + void testResultToString() { + double[] xs = {1.0, 2.0, 3.0}; + double[] ys = {2.0, 4.0, 6.0}; + + FindFormula.Config config = new FindFormula.Config() + .iterations(3) + .populationSize(20); + + FindFormula.Result result = FindFormula.findFormula(xs, ys, config); + + String str = result.toString(); + assertNotNull(str); + assertTrue(str.contains("Result{")); + assertTrue(str.contains("formula=")); + assertTrue(str.contains("score=")); + assertTrue(str.contains("iterations=")); + } + + @Test + void testSolutionToString() { + double[] xs = {1.0, 2.0, 3.0}; + double[] ys = {1.0, 4.0, 9.0}; + + FindFormula.Config config = new FindFormula.Config() + .iterations(3) + .populationSize(20); + + FindFormula.Result result = FindFormula.findFormula(xs, ys, config); + + assertFalse(result.getAllSolutions().isEmpty()); + FindFormula.Solution solution = result.getAllSolutions().get(0); + + String str = solution.toString(); + assertNotNull(str); + assertTrue(str.contains("Solution{")); + assertTrue(str.contains("formula=")); + assertTrue(str.contains("score=")); + } + + @Test + void testWithTrigonometricData() { + // Data from y = sin(x) + int numPoints = 20; + double[] xs = new double[numPoints]; + double[] ys = new double[numPoints]; + for (int i = 0; i < numPoints; i++) { + xs[i] = i * Math.PI / 10.0; + ys[i] = Math.sin(xs[i]); + } + + FindFormula.Config config = new FindFormula.Config() + .iterations(15) + .populationSize(100); + + FindFormula.Result result = FindFormula.findFormula(xs, ys, config); + + assertNotNull(result); + assertNotNull(result.getFormulaString()); + // Trigonometric functions are complex, just verify we get a result + assertTrue(result.getAllSolutions().size() > 0); + + System.out.println("Trig data result: " + result); + } + + @Test + void testInitializeClojureIdempotent() { + // Should be safe to call multiple times + FindFormula.initializeClojure(); + FindFormula.initializeClojure(); + FindFormula.initializeClojure(); + + // Verify the API still works + double[] xs = {1.0, 2.0}; + double[] ys = {1.0, 2.0}; + + FindFormula.Config config = new FindFormula.Config() + .iterations(2) + .populationSize(10); + + FindFormula.Result result = FindFormula.findFormula(xs, ys, config); + assertNotNull(result); } } From 6ab69fd9b107c5a313c8907baed95d08f4a75a69 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 19:27:12 -0700 Subject: [PATCH 019/224] java API for closyr WIP --- project.clj | 4 + src/closyr/api/finder.clj | 117 ++++++++++ src/closyr/api/types.clj | 141 +++++++++++ .../org/closyr/api/FormulaConfigBuilder.java | 98 ++++++++ .../java/org/closyr/api/IFormulaConfig.java | 22 ++ .../java/org/closyr/api/IFormulaFinder.java | 26 +++ .../java/org/closyr/api/IFormulaResult.java | 42 ++++ .../java/org/closyr/api/IFormulaSolution.java | 32 +++ .../java/org/closyr/core/FindFormula.java | 188 ++++----------- test/closyr/api_test.clj | 218 ++++++++++++++++++ test/closyr/find_formula_java_test.clj | 15 +- 11 files changed, 754 insertions(+), 149 deletions(-) create mode 100644 src/closyr/api/finder.clj create mode 100644 src/closyr/api/types.clj create mode 100644 src/main/java/org/closyr/api/FormulaConfigBuilder.java create mode 100644 src/main/java/org/closyr/api/IFormulaConfig.java create mode 100644 src/main/java/org/closyr/api/IFormulaFinder.java create mode 100644 src/main/java/org/closyr/api/IFormulaResult.java create mode 100644 src/main/java/org/closyr/api/IFormulaSolution.java create mode 100644 test/closyr/api_test.clj diff --git a/project.clj b/project.clj index 112657cb..2553e434 100644 --- a/project.clj +++ b/project.clj @@ -60,6 +60,10 @@ "-XX:+DisableExplicitGC"] ; Ignore System.gc() calls ;; Note: direct-linking only in uberjar profile (breaks dynamic var rebinding in tests) + ;; AOT compile API namespaces for Java interop + :aot [closyr.api.types + closyr.api.finder] + :profiles {:uberjar {:aot :all :manifest {"Multi-Release" true} :jvm-opts ["-Dclojure.compiler.direct-linking=true"]} diff --git a/src/closyr/api/finder.clj b/src/closyr/api/finder.clj new file mode 100644 index 00000000..26a107f9 --- /dev/null +++ b/src/closyr/api/finder.clj @@ -0,0 +1,117 @@ +(ns closyr.api.finder + "Main entry point for the Java API. + This namespace uses gen-class to create a Java-friendly FormulaFinder class." + (:require + [closyr.api.types :as types] + [closyr.ops.common :as ops-common] + [closyr.ops.initialize :as ops-init] + [closyr.symbolic-regression :as symreg]) + (:import + (org.closyr.api + IFormulaConfig + IFormulaFinder + IFormulaResult)) + (:gen-class + :name org.closyr.api.FormulaFinder + :implements [org.closyr.api.IFormulaFinder] + :methods [^:static [create [] org.closyr.api.IFormulaFinder] + ^:static [createConfig [] org.closyr.api.IFormulaConfig] + ^:static [createConfig [int int int] org.closyr.api.IFormulaConfig] + ^:static [find ["[D" "[D"] org.closyr.api.IFormulaResult] + ^:static [find ["[D" "[D" org.closyr.api.IFormulaConfig] org.closyr.api.IFormulaResult]])) + + +(set! *warn-on-reflection* true) + + +(defn run-solver + "Run the symbolic regression solver with the given parameters." + [xs ys config] + (let [^doubles xs-arr xs + ^doubles ys-arr ys + ^IFormulaConfig cfg config + xs-vec (vec xs-arr) + ys-vec (vec ys-arr) + iterations (.getIterations cfg) + population-size (.getPopulationSize cfg) + max-leafs (.getMaxLeafs cfg) + + run-config {:initial-phenos (ops-init/initial-phenotypes population-size) + :initial-muts (ops-init/initial-mutations) + :iters iterations + :use-gui? false + :use-flamechart false + :max-leafs max-leafs + :input-xs-exprs (ops-common/doubles->exprs xs-vec) + :input-ys-exprs (ops-common/doubles->exprs ys-vec)} + + result (symreg/run-find-formula run-config)] + (types/->formula-result result))) + + +(defn validate-inputs + "Validate xs and ys arrays." + [xs ys] + (when (nil? xs) + (throw (IllegalArgumentException. "xs array cannot be null"))) + (when (nil? ys) + (throw (IllegalArgumentException. "ys array cannot be null"))) + (let [^doubles xs-arr xs + ^doubles ys-arr ys] + (when (not= (alength xs-arr) (alength ys-arr)) + (throw (IllegalArgumentException. + (str "xs and ys arrays must have the same length, got " + (alength xs-arr) " and " (alength ys-arr))))) + (when (< (alength xs-arr) 2) + (throw (IllegalArgumentException. + (str "At least 2 data points are required, got " (alength xs-arr))))))) + + +;; ============================================================================ +;; Instance methods (IFormulaFinder implementation) +;; ============================================================================ + +(defn -findFormula + "Find a formula that fits the given data points." + ([this xs ys] + (-findFormula this xs ys (types/config))) + ([_ xs ys config] + (let [^doubles xs-arr xs + ^doubles ys-arr ys + ^IFormulaConfig cfg config] + (validate-inputs xs-arr ys-arr) + (run-solver xs-arr ys-arr cfg)))) + + +;; ============================================================================ +;; Static factory methods +;; ============================================================================ + +(defn -create + "Create a new FormulaFinder instance." + [] + ;; The instance is created by gen-class, we just return it + ;; This is called via FormulaFinder.create() + (eval '(org.closyr.api.FormulaFinder.))) + + +(defn -createConfig + "Create a configuration with default or custom values." + ([] + (types/config)) + ([iterations population-size max-leafs] + (types/config {:iterations (int iterations) + :population-size (int population-size) + :max-leafs (int max-leafs)}))) + + +(defn -find + "Static method to find a formula without creating an instance." + ([xs ys] + (-find xs ys (types/config))) + ([xs ys config] + (let [^doubles xs-arr xs + ^doubles ys-arr ys + ^IFormulaConfig cfg config] + (validate-inputs xs-arr ys-arr) + (run-solver xs-arr ys-arr cfg)))) diff --git a/src/closyr/api/types.clj b/src/closyr/api/types.clj new file mode 100644 index 00000000..bf124b2c --- /dev/null +++ b/src/closyr/api/types.clj @@ -0,0 +1,141 @@ +(ns closyr.api.types + "Java-friendly types for the symbolic regression API. + These types implement Java interfaces for seamless Java interop." + (:import + (org.closyr.api + IFormulaConfig + IFormulaResult + IFormulaSolution) + (org.matheclipse.core.interfaces + IExpr))) + + +(set! *warn-on-reflection* true) + + +;; ============================================================================ +;; FormulaSolution - implements IFormulaSolution +;; ============================================================================ + +(deftype FormulaSolution [^String formula + ^double score + ^IExpr expr + ^int leaf-count] + + IFormulaSolution + + (getFormula [_] formula) + + (getScore [_] score) + + (getExpr [_] expr) + + (getLeafCount [_] leaf-count) + + Object + + (toString [_] + (str "FormulaSolution{formula='" formula "', score=" score + ", leafCount=" leaf-count "}"))) + + +(defn ->formula-solution + "Create a FormulaSolution from a phenotype map." + [{:keys [expr score]}] + (let [^IExpr e expr + formula-str (if e (str e) "unknown") + leaf-count (if e (.leafCount e) 0) + score-val (if (number? score) (double score) Double/NEGATIVE_INFINITY)] + (FormulaSolution. formula-str score-val e leaf-count))) + + +;; ============================================================================ +;; FormulaResult - implements IFormulaResult +;; ============================================================================ + +(deftype FormulaResult [^IFormulaSolution best-solution + ^java.util.List all-solutions + ^int iterations-done] + + IFormulaResult + + (getBestSolution [_] best-solution) + + (getAllSolutions [_] all-solutions) + + (getIterationsDone [_] iterations-done) + + Object + + (toString [_] + (str "FormulaResult{bestFormula='" + (when best-solution (.getFormula best-solution)) + "', bestScore=" + (when best-solution (.getScore best-solution)) + ", iterations=" iterations-done + ", solutionCount=" (count all-solutions) "}"))) + + +(defn ->formula-result + "Create a FormulaResult from solver output." + [{:keys [iters-done final-population]}] + (let [phenotypes (get final-population :pop []) + ;; Convert all phenotypes to solutions + solutions (->> phenotypes + (map ->formula-solution) + ;; Sort by score descending (higher/closer to 0 is better) + (sort-by #(.getScore ^IFormulaSolution %) #(compare %2 %1)) + vec) + best (first solutions) + solutions-list (java.util.ArrayList. ^java.util.Collection solutions)] + (FormulaResult. best solutions-list (int (or iters-done 0))))) + + +;; ============================================================================ +;; FormulaConfig - implements IFormulaConfig +;; ============================================================================ + +(deftype FormulaConfig [^int iterations + ^int population-size + ^int max-leafs] + + IFormulaConfig + + (getIterations [_] iterations) + + (getPopulationSize [_] population-size) + + (getMaxLeafs [_] max-leafs) + + Object + + (toString [_] + (str "FormulaConfig{iterations=" iterations + ", populationSize=" population-size + ", maxLeafs=" max-leafs "}"))) + + +(defn config + "Create a FormulaConfig with the given options. + + Options: + :iterations - Number of GA iterations (default: 20) + :population-size - Population size (default: 100) + :max-leafs - Max expression tree leaves (default: 40)" + ([] + (config {})) + ([{:keys [iterations population-size max-leafs] + :or {iterations 20 + population-size 100 + max-leafs 40}}] + (FormulaConfig. (int iterations) + (int population-size) + (int max-leafs)))) + + +(defn config->map + "Convert an IFormulaConfig to a Clojure map." + [^IFormulaConfig cfg] + {:iterations (.getIterations cfg) + :population-size (.getPopulationSize cfg) + :max-leafs (.getMaxLeafs cfg)}) diff --git a/src/main/java/org/closyr/api/FormulaConfigBuilder.java b/src/main/java/org/closyr/api/FormulaConfigBuilder.java new file mode 100644 index 00000000..b051f3d7 --- /dev/null +++ b/src/main/java/org/closyr/api/FormulaConfigBuilder.java @@ -0,0 +1,98 @@ +package org.closyr.api; + +/** + * Fluent builder for creating IFormulaConfig instances. + * + *

Usage:

+ *
+ *   IFormulaConfig config = FormulaConfigBuilder.builder()
+ *       .iterations(50)
+ *       .populationSize(200)
+ *       .maxLeafs(30)
+ *       .build();
+ * 
+ */ +public class FormulaConfigBuilder { + + private int iterations = 20; + private int populationSize = 100; + private int maxLeafs = 40; + + private FormulaConfigBuilder() { + } + + /** + * Create a new builder with default values. + */ + public static FormulaConfigBuilder builder() { + return new FormulaConfigBuilder(); + } + + /** + * Set the number of GA iterations. + */ + public FormulaConfigBuilder iterations(int iterations) { + this.iterations = iterations; + return this; + } + + /** + * Set the population size. + */ + public FormulaConfigBuilder populationSize(int populationSize) { + this.populationSize = populationSize; + return this; + } + + /** + * Set the maximum expression tree leaf count. + */ + public FormulaConfigBuilder maxLeafs(int maxLeafs) { + this.maxLeafs = maxLeafs; + return this; + } + + /** + * Build the configuration. + */ + public IFormulaConfig build() { + return new SimpleFormulaConfig(iterations, populationSize, maxLeafs); + } + + /** + * Simple implementation of IFormulaConfig. + */ + private static class SimpleFormulaConfig implements IFormulaConfig { + private final int iterations; + private final int populationSize; + private final int maxLeafs; + + SimpleFormulaConfig(int iterations, int populationSize, int maxLeafs) { + this.iterations = iterations; + this.populationSize = populationSize; + this.maxLeafs = maxLeafs; + } + + @Override + public int getIterations() { + return iterations; + } + + @Override + public int getPopulationSize() { + return populationSize; + } + + @Override + public int getMaxLeafs() { + return maxLeafs; + } + + @Override + public String toString() { + return "FormulaConfig{iterations=" + iterations + + ", populationSize=" + populationSize + + ", maxLeafs=" + maxLeafs + "}"; + } + } +} diff --git a/src/main/java/org/closyr/api/IFormulaConfig.java b/src/main/java/org/closyr/api/IFormulaConfig.java new file mode 100644 index 00000000..d1c365c8 --- /dev/null +++ b/src/main/java/org/closyr/api/IFormulaConfig.java @@ -0,0 +1,22 @@ +package org.closyr.api; + +/** + * Configuration for the symbolic regression solver. + */ +public interface IFormulaConfig { + + /** + * Get the number of GA iterations to run. + */ + int getIterations(); + + /** + * Get the population size (number of candidate formulas). + */ + int getPopulationSize(); + + /** + * Get the maximum number of leaf nodes allowed in expression trees. + */ + int getMaxLeafs(); +} diff --git a/src/main/java/org/closyr/api/IFormulaFinder.java b/src/main/java/org/closyr/api/IFormulaFinder.java new file mode 100644 index 00000000..729012c7 --- /dev/null +++ b/src/main/java/org/closyr/api/IFormulaFinder.java @@ -0,0 +1,26 @@ +package org.closyr.api; + +/** + * Main interface for finding formulas via symbolic regression. + */ +public interface IFormulaFinder { + + /** + * Find a formula that fits the given data points. + * + * @param xs array of x values (independent variable) + * @param ys array of y values (dependent variable, same length as xs) + * @return the result containing the best formula and all solutions + */ + IFormulaResult findFormula(double[] xs, double[] ys); + + /** + * Find a formula with custom configuration. + * + * @param xs array of x values + * @param ys array of y values + * @param config solver configuration + * @return the result containing the best formula and all solutions + */ + IFormulaResult findFormula(double[] xs, double[] ys, IFormulaConfig config); +} diff --git a/src/main/java/org/closyr/api/IFormulaResult.java b/src/main/java/org/closyr/api/IFormulaResult.java new file mode 100644 index 00000000..c1978f6a --- /dev/null +++ b/src/main/java/org/closyr/api/IFormulaResult.java @@ -0,0 +1,42 @@ +package org.closyr.api; + +import java.util.List; + +/** + * Represents the result of running the symbolic regression solver. + */ +public interface IFormulaResult { + + /** + * Get the best solution found. + */ + IFormulaSolution getBestSolution(); + + /** + * Get all solutions from the final population, sorted by score (best first). + */ + List getAllSolutions(); + + /** + * Get the number of GA iterations that were completed. + */ + int getIterationsDone(); + + /** + * Get the best formula as a string (convenience method). + * Equivalent to getBestSolution().getFormula(). + */ + default String getBestFormula() { + IFormulaSolution best = getBestSolution(); + return best != null ? best.getFormula() : null; + } + + /** + * Get the best score (convenience method). + * Equivalent to getBestSolution().getScore(). + */ + default double getBestScore() { + IFormulaSolution best = getBestSolution(); + return best != null ? best.getScore() : Double.NEGATIVE_INFINITY; + } +} diff --git a/src/main/java/org/closyr/api/IFormulaSolution.java b/src/main/java/org/closyr/api/IFormulaSolution.java new file mode 100644 index 00000000..6a61fbe3 --- /dev/null +++ b/src/main/java/org/closyr/api/IFormulaSolution.java @@ -0,0 +1,32 @@ +package org.closyr.api; + +import org.matheclipse.core.interfaces.IExpr; + +/** + * Represents a single formula solution from the symbolic regression solver. + */ +public interface IFormulaSolution { + + /** + * Get the formula as a human-readable string (e.g., "2*x + 1"). + */ + String getFormula(); + + /** + * Get the fitness score. Higher (closer to 0) is better. + * Scores are typically negative, with 0 being a perfect fit. + */ + double getScore(); + + /** + * Get the formula as a Symja IExpr for further symbolic computation. + * May be null if the expression couldn't be extracted. + */ + IExpr getExpr(); + + /** + * Get the number of leaf nodes in the expression tree. + * Smaller values indicate simpler formulas. + */ + int getLeafCount(); +} diff --git a/src/main/java/org/closyr/core/FindFormula.java b/src/main/java/org/closyr/core/FindFormula.java index ea0d2daa..420410a4 100644 --- a/src/main/java/org/closyr/core/FindFormula.java +++ b/src/main/java/org/closyr/core/FindFormula.java @@ -1,10 +1,10 @@ package org.closyr.core; -import clojure.java.api.Clojure; -import clojure.lang.IFn; -import clojure.lang.IPersistentMap; -import clojure.lang.Keyword; -import clojure.lang.PersistentVector; +import org.closyr.api.FormulaConfigBuilder; +import org.closyr.api.FormulaFinder; +import org.closyr.api.IFormulaConfig; +import org.closyr.api.IFormulaResult; +import org.closyr.api.IFormulaSolution; import org.matheclipse.core.eval.EvalEngine; import org.matheclipse.core.eval.interfaces.AbstractFunctionOptionEvaluator; import org.matheclipse.core.eval.interfaces.IFunctionEvaluator; @@ -16,7 +16,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; /** @@ -38,16 +37,15 @@ * System.out.println("Best formula: " + result.getFormulaString()); * System.out.println("Score: " + result.getScore()); * + * + *

Or use the newer API directly:

+ *
+ *   import org.closyr.api.*;
+ *   IFormulaResult result = FormulaFinder.find(xs, ys);
+ * 
*/ public class FindFormula extends AbstractFunctionOptionEvaluator { - private static volatile boolean clojureInitialized = false; - private static IFn runAppWithoutGuiFn; - private static IFn initialPhenotypesFn; - private static IFn initialMutationsFn; - private static IFn doublesToExprsFn; - private static IFn runFindFormulaFn; - /** * Result of a symbolic regression run. */ @@ -162,37 +160,21 @@ public Config maxLeafs(int maxLeafs) { public int getIterations() { return iterations; } public int getPopulationSize() { return populationSize; } public int getMaxLeafs() { return maxLeafs; } + + /** Convert to IFormulaConfig for the new API */ + IFormulaConfig toFormulaConfig() { + return FormulaConfigBuilder.builder() + .iterations(iterations) + .populationSize(populationSize) + .maxLeafs(maxLeafs) + .build(); + } } public FindFormula() { // empty constructor for Symja } - /** - * Initialize the Clojure runtime and load required namespaces. - * This is called automatically but can be called explicitly to warm up. - */ - public static synchronized void initializeClojure() { - if (clojureInitialized) { - return; - } - - // Require the necessary Clojure namespaces - IFn require = Clojure.var("clojure.core", "require"); - require.invoke(Clojure.read("closyr.symbolic-regression")); - require.invoke(Clojure.read("closyr.ops.initialize")); - require.invoke(Clojure.read("closyr.ops.common")); - - // Get references to the Clojure functions we need - runAppWithoutGuiFn = Clojure.var("closyr.symbolic-regression", "run-app-without-gui"); - runFindFormulaFn = Clojure.var("closyr.symbolic-regression", "run-find-formula"); - initialPhenotypesFn = Clojure.var("closyr.ops.initialize", "initial-phenotypes"); - initialMutationsFn = Clojure.var("closyr.ops.initialize", "initial-mutations"); - doublesToExprsFn = Clojure.var("closyr.ops.common", "doubles->exprs"); - - clojureInitialized = true; - } - /** * Find a formula that fits the given x,y data points using default settings. * @@ -213,112 +195,39 @@ public static Result findFormula(double[] xs, double[] ys) { * @return Result containing the best formula found */ public static Result findFormula(double[] xs, double[] ys, Config config) { - if (xs == null || ys == null) { - throw new IllegalArgumentException("xs and ys arrays cannot be null"); - } - if (xs.length != ys.length) { - throw new IllegalArgumentException("xs and ys arrays must have the same length"); - } - if (xs.length < 2) { - throw new IllegalArgumentException("At least 2 data points are required"); - } + // Delegate to the new API + IFormulaConfig formulaConfig = config.toFormulaConfig(); + IFormulaResult apiResult = FormulaFinder.find(xs, ys, formulaConfig); - initializeClojure(); - - // Convert Java arrays to Clojure vectors - List xsList = new ArrayList<>(xs.length); - List ysList = new ArrayList<>(ys.length); - for (int i = 0; i < xs.length; i++) { - xsList.add(xs[i]); - ysList.add(ys[i]); - } - PersistentVector xsVec = PersistentVector.create(xsList); - PersistentVector ysVec = PersistentVector.create(ysList); - - // Convert to IExpr vectors - Object xsExprs = doublesToExprsFn.invoke(xsVec); - Object ysExprs = doublesToExprsFn.invoke(ysVec); - - // Create initial population and mutations - Object initialPhenos = initialPhenotypesFn.invoke(config.getPopulationSize()); - Object initialMuts = initialMutationsFn.invoke(); - - // Build the run configuration map - Keyword initialPhenosKey = Keyword.intern("initial-phenos"); - Keyword initialMutsKey = Keyword.intern("initial-muts"); - Keyword itersKey = Keyword.intern("iters"); - Keyword useGuiKey = Keyword.intern("use-gui?"); - Keyword useFlamechartKey = Keyword.intern("use-flamechart"); - Keyword inputXsExprsKey = Keyword.intern("input-xs-exprs"); - Keyword inputYsExprsKey = Keyword.intern("input-ys-exprs"); - Keyword maxLeafsKey = Keyword.intern("max-leafs"); - - IFn hashMap = Clojure.var("clojure.core", "hash-map"); - Object runConfig = hashMap.invoke( - initialPhenosKey, initialPhenos, - initialMutsKey, initialMuts, - itersKey, config.getIterations(), - useGuiKey, false, - useFlamechartKey, false, - maxLeafsKey, config.getMaxLeafs(), - inputXsExprsKey, xsExprs, - inputYsExprsKey, ysExprs - ); - - // Run the solver - Object result = runFindFormulaFn.invoke(runConfig); - - // Extract results from the Clojure map - return extractResult(result); + // Convert to legacy Result format + return convertResult(apiResult); } - @SuppressWarnings("unchecked") - private static Result extractResult(Object result) { - if (!(result instanceof IPersistentMap)) { - throw new RuntimeException("Unexpected result type from Clojure: " + result.getClass()); - } - - IPersistentMap resultMap = (IPersistentMap) result; - - Keyword itersDoneKey = Keyword.intern("iters-done"); - Keyword finalPopulationKey = Keyword.intern("final-population"); - Keyword popKey = Keyword.intern("pop"); - Keyword exprKey = Keyword.intern("expr"); - Keyword scoreKey = Keyword.intern("score"); - - int itersDone = ((Number) resultMap.valAt(itersDoneKey)).intValue(); - IPersistentMap finalPopulation = (IPersistentMap) resultMap.valAt(finalPopulationKey); - Object pop = finalPopulation.valAt(popKey); - - List allSolutions = new ArrayList<>(); - - if (pop instanceof Iterable) { - for (Object phenotype : (Iterable) pop) { - if (phenotype instanceof IPersistentMap) { - IPersistentMap phenoMap = (IPersistentMap) phenotype; - Object exprObj = phenoMap.valAt(exprKey); - Object scoreObj = phenoMap.valAt(scoreKey); - - IExpr expr = (exprObj instanceof IExpr) ? (IExpr) exprObj : null; - String formulaStr = (expr != null) ? expr.toString() : "unknown"; - double score = (scoreObj instanceof Number) ? ((Number) scoreObj).doubleValue() : Double.NEGATIVE_INFINITY; - - allSolutions.add(new Solution(formulaStr, score, expr)); - } - } + /** + * Convert from the new API result to the legacy Result format. + */ + private static Result convertResult(IFormulaResult apiResult) { + List solutions = new ArrayList<>(); + + for (IFormulaSolution apiSolution : apiResult.getAllSolutions()) { + solutions.add(new Solution( + apiSolution.getFormula(), + apiSolution.getScore(), + apiSolution.getExpr() + )); } - // Sort by score (descending - higher/closer to 0 is better) - allSolutions.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); - - Solution best = allSolutions.isEmpty() ? new Solution("x", Double.NEGATIVE_INFINITY, null) : allSolutions.get(0); + IFormulaSolution best = apiResult.getBestSolution(); + String bestFormula = best != null ? best.getFormula() : "x"; + double bestScore = best != null ? best.getScore() : Double.NEGATIVE_INFINITY; + IExpr bestExpr = best != null ? best.getExpr() : null; return new Result( - best.getFormulaString(), - best.getScore(), - best.getFormulaExpr(), - itersDone, - allSolutions + bestFormula, + bestScore, + bestExpr, + apiResult.getIterationsDone(), + solutions ); } @@ -371,13 +280,10 @@ public IExpr evaluate(IAST ast, final int argSize, final IExpr[] options, IExpr formulaExpr = result.getFormulaExpr(); if (formulaExpr != null) { - // Substitute the Symja variable for our internal 'x' - // The formula uses a dummy symbol 'x', we need to map it to the user's variable return formulaExpr; } } catch (Exception e) { - System.out.println("FindFormula: " + e.getMessage()); -// engine.printMessage("FindFormula: " + e.getMessage()); + System.err.println("FindFormula: " + e.getMessage()); } return F.NIL; diff --git a/test/closyr/api_test.clj b/test/closyr/api_test.clj new file mode 100644 index 00000000..38717f4b --- /dev/null +++ b/test/closyr/api_test.clj @@ -0,0 +1,218 @@ +(ns closyr.api-test + "Tests for the Java-friendly API classes." + (:require + [clojure.test :refer :all] + [closyr.api.types :as types] + [closyr.api.finder :as finder]) + (:import + (org.closyr.api + FormulaConfigBuilder + FormulaFinder + IFormulaConfig + IFormulaFinder + IFormulaResult + IFormulaSolution))) + + +;; ============================================================================ +;; FormulaConfigBuilder tests +;; ============================================================================ + +(deftest test-config-builder-defaults + (testing "Builder creates config with default values" + (let [config (.build (FormulaConfigBuilder/builder))] + (is (instance? IFormulaConfig config)) + (is (= 20 (.getIterations config))) + (is (= 100 (.getPopulationSize config))) + (is (= 40 (.getMaxLeafs config)))))) + + +(deftest test-config-builder-custom-values + (testing "Builder accepts custom values" + (let [config (-> (FormulaConfigBuilder/builder) + (.iterations 50) + (.populationSize 200) + (.maxLeafs 30) + (.build))] + (is (= 50 (.getIterations config))) + (is (= 200 (.getPopulationSize config))) + (is (= 30 (.getMaxLeafs config)))))) + + +;; ============================================================================ +;; Clojure types tests +;; ============================================================================ + +(deftest test-clojure-config + (testing "Clojure config function creates valid IFormulaConfig" + (let [config (types/config {:iterations 10 :population-size 50 :max-leafs 25})] + (is (instance? IFormulaConfig config)) + (is (= 10 (.getIterations config))) + (is (= 50 (.getPopulationSize config))) + (is (= 25 (.getMaxLeafs config)))))) + + +(deftest test-clojure-config-defaults + (testing "Clojure config uses defaults when not specified" + (let [config (types/config)] + (is (= 20 (.getIterations config))) + (is (= 100 (.getPopulationSize config))) + (is (= 40 (.getMaxLeafs config)))))) + + +;; ============================================================================ +;; FormulaFinder static methods +;; ============================================================================ + +(deftest test-formula-finder-static-find + (testing "FormulaFinder.find() static method works" + (let [xs (double-array [1.0 2.0 3.0 4.0 5.0]) + ys (double-array [2.0 4.0 6.0 8.0 10.0]) + config (-> (FormulaConfigBuilder/builder) + (.iterations 3) + (.populationSize 20) + (.build)) + result (FormulaFinder/find xs ys config)] + (is (instance? IFormulaResult result)) + (is (pos? (.getIterationsDone result))) + (is (some? (.getBestSolution result))) + (is (string? (.getBestFormula result))) + (is (number? (.getBestScore result)))))) + + +(deftest test-formula-finder-static-find-default-config + (testing "FormulaFinder.find() with default config" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [1.0 4.0 9.0]) + result (FormulaFinder/find xs ys)] + (is (instance? IFormulaResult result)) + (is (= 20 (.getIterationsDone result)))))) + + +;; ============================================================================ +;; FormulaFinder instance methods +;; ============================================================================ + +(deftest test-formula-finder-instance + (testing "FormulaFinder instance implements IFormulaFinder" + (let [finder (FormulaFinder/create)] + (is (instance? IFormulaFinder finder))))) + + +(deftest test-formula-finder-instance-find + (testing "FormulaFinder instance findFormula method" + (let [finder (FormulaFinder/create) + xs (double-array [1.0 2.0 3.0]) + ys (double-array [2.0 4.0 6.0]) + config (-> (FormulaConfigBuilder/builder) + (.iterations 2) + (.populationSize 10) + (.build)) + result (.findFormula finder xs ys config)] + (is (instance? IFormulaResult result)) + (is (some? (.getBestSolution result)))))) + + +;; ============================================================================ +;; IFormulaSolution tests +;; ============================================================================ + +(deftest test-formula-solution-properties + (testing "IFormulaSolution has expected properties" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [3.0 6.0 9.0]) + config (-> (FormulaConfigBuilder/builder) + (.iterations 2) + (.populationSize 10) + (.build)) + result (FormulaFinder/find xs ys config) + best (.getBestSolution result)] + (is (instance? IFormulaSolution best)) + (is (string? (.getFormula best))) + (is (not (empty? (.getFormula best)))) + (is (number? (.getScore best))) + (is (>= (.getLeafCount best) 0))))) + + +(deftest test-all-solutions-sorted + (testing "getAllSolutions returns solutions sorted by score" + (let [xs (double-array [1.0 2.0 3.0 4.0]) + ys (double-array [1.0 4.0 9.0 16.0]) + config (-> (FormulaConfigBuilder/builder) + (.iterations 3) + (.populationSize 30) + (.build)) + result (FormulaFinder/find xs ys config) + solutions (.getAllSolutions result)] + (is (seq solutions)) + ;; Check sorted descending by score + (doseq [[prev curr] (partition 2 1 solutions)] + (is (>= (.getScore prev) (.getScore curr)) + "Solutions should be sorted by score descending"))))) + + +;; ============================================================================ +;; Validation tests +;; ============================================================================ + +(deftest test-null-xs-throws + (testing "Null xs throws IllegalArgumentException" + (let [ys (double-array [1.0 2.0 3.0])] + (is (thrown-with-msg? IllegalArgumentException #"null" + (FormulaFinder/find nil ys)))))) + + +(deftest test-null-ys-throws + (testing "Null ys throws IllegalArgumentException" + (let [xs (double-array [1.0 2.0 3.0])] + (is (thrown-with-msg? IllegalArgumentException #"null" + (FormulaFinder/find xs nil)))))) + + +(deftest test-mismatched-lengths-throws + (testing "Mismatched array lengths throws IllegalArgumentException" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [1.0 2.0])] + (is (thrown-with-msg? IllegalArgumentException #"same length" + (FormulaFinder/find xs ys)))))) + + +(deftest test-too-few-points-throws + (testing "Less than 2 data points throws IllegalArgumentException" + (let [xs (double-array [1.0]) + ys (double-array [2.0])] + (is (thrown-with-msg? IllegalArgumentException #"At least 2" + (FormulaFinder/find xs ys)))))) + + +;; ============================================================================ +;; toString tests +;; ============================================================================ + +(deftest test-result-to-string + (testing "Result toString contains expected info" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [2.0 4.0 6.0]) + config (-> (FormulaConfigBuilder/builder) + (.iterations 1) + (.populationSize 5) + (.build)) + result (FormulaFinder/find xs ys config) + str-repr (.toString result)] + (is (.contains str-repr "FormulaResult")) + (is (.contains str-repr "bestFormula"))))) + + +(deftest test-solution-to-string + (testing "Solution toString contains expected info" + (let [xs (double-array [1.0 2.0 3.0]) + ys (double-array [1.0 4.0 9.0]) + config (-> (FormulaConfigBuilder/builder) + (.iterations 1) + (.populationSize 5) + (.build)) + result (FormulaFinder/find xs ys config) + solution (.getBestSolution result) + str-repr (.toString solution)] + (is (.contains str-repr "FormulaSolution")) + (is (.contains str-repr "formula"))))) diff --git a/test/closyr/find_formula_java_test.clj b/test/closyr/find_formula_java_test.clj index 0942628d..fe40e562 100644 --- a/test/closyr/find_formula_java_test.clj +++ b/test/closyr/find_formula_java_test.clj @@ -144,16 +144,15 @@ (is (.contains str-repr "score="))))) -(deftest test-initialize-clojure-idempotent - (testing "initializeClojure can be called multiple times safely" - (FindFormula/initializeClojure) - (FindFormula/initializeClojure) - (FindFormula/initializeClojure) - ;; Verify the API still works +(deftest test-multiple-calls-work + (testing "Multiple calls to findFormula work correctly" + ;; Verify the API works for multiple sequential calls (let [xs (double-array [1.0 2.0]) ys (double-array [1.0 2.0]) config (-> (FindFormula$Config.) (.iterations 1) (.populationSize 5)) - result (FindFormula/findFormula xs ys config)] - (is (some? result))))) + result1 (FindFormula/findFormula xs ys config) + result2 (FindFormula/findFormula xs ys config)] + (is (some? result1)) + (is (some? result2))))) From 539571215f6c1174684fb3db77af4601dd1ae8f9 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 19:34:26 -0700 Subject: [PATCH 020/224] java API for closyr WIP --- .../java/org/closyr/api/ClojureBridge.java | 66 +++++++++++++++++++ .../java/org/closyr/core/FindFormula.java | 6 +- 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/closyr/api/ClojureBridge.java diff --git a/src/main/java/org/closyr/api/ClojureBridge.java b/src/main/java/org/closyr/api/ClojureBridge.java new file mode 100644 index 00000000..424987e4 --- /dev/null +++ b/src/main/java/org/closyr/api/ClojureBridge.java @@ -0,0 +1,66 @@ +package org.closyr.api; + +import clojure.java.api.Clojure; +import clojure.lang.IFn; + +/** + * Bridge class that lazily loads Clojure namespaces and provides + * access to the symbolic regression solver. + * + * This class exists to avoid circular dependencies between Java + * compilation and Clojure AOT compilation. + */ +public class ClojureBridge { + + private static volatile boolean initialized = false; + private static IFn findFn; + private static IFn configFn; + + private ClojureBridge() { + // Utility class + } + + /** + * Initialize the Clojure runtime. Safe to call multiple times. + */ + public static synchronized void initialize() { + if (initialized) { + return; + } + + // Require the API namespaces + IFn require = Clojure.var("clojure.core", "require"); + require.invoke(Clojure.read("closyr.api.finder")); + require.invoke(Clojure.read("closyr.api.types")); + + // Get references to the functions we need + findFn = Clojure.var("closyr.api.finder", "-find"); + configFn = Clojure.var("closyr.api.types", "config"); + + initialized = true; + } + + /** + * Find a formula using default configuration. + */ + public static IFormulaResult find(double[] xs, double[] ys) { + initialize(); + return (IFormulaResult) findFn.invoke(xs, ys); + } + + /** + * Find a formula with custom configuration. + */ + public static IFormulaResult find(double[] xs, double[] ys, IFormulaConfig config) { + initialize(); + return (IFormulaResult) findFn.invoke(xs, ys, config); + } + + /** + * Create a default configuration. + */ + public static IFormulaConfig createConfig() { + initialize(); + return (IFormulaConfig) configFn.invoke(); + } +} diff --git a/src/main/java/org/closyr/core/FindFormula.java b/src/main/java/org/closyr/core/FindFormula.java index 420410a4..d22a4918 100644 --- a/src/main/java/org/closyr/core/FindFormula.java +++ b/src/main/java/org/closyr/core/FindFormula.java @@ -1,7 +1,7 @@ package org.closyr.core; +import org.closyr.api.ClojureBridge; import org.closyr.api.FormulaConfigBuilder; -import org.closyr.api.FormulaFinder; import org.closyr.api.IFormulaConfig; import org.closyr.api.IFormulaResult; import org.closyr.api.IFormulaSolution; @@ -195,9 +195,9 @@ public static Result findFormula(double[] xs, double[] ys) { * @return Result containing the best formula found */ public static Result findFormula(double[] xs, double[] ys, Config config) { - // Delegate to the new API + // Delegate to the new API via ClojureBridge IFormulaConfig formulaConfig = config.toFormulaConfig(); - IFormulaResult apiResult = FormulaFinder.find(xs, ys, formulaConfig); + IFormulaResult apiResult = ClojureBridge.find(xs, ys, formulaConfig); // Convert to legacy Result format return convertResult(apiResult); From a1219344765efb71a5f36277f6ff54075e63f3b5 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Mon, 22 Dec 2025 19:43:20 -0700 Subject: [PATCH 021/224] java API for closyr WIP --- test/closyr/symbolic_regression_test.clj | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index df7baed9..768138fa 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -242,14 +242,12 @@ (binding [ops/*print-top-n* 1] (testing "gui can start and restart experiments; NOTE: do not run this while in headless mode, eg on CI" (reset! symreg/sim-input-args* {}) + ;; Verify initial state before launching async processes + (is (= (set (keys @symreg/sim-input-args*)) #{})) (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 500)} (fn [] (let [control-process (go - - (is (= (set (keys @symreg/sim-input-args*)) - #{})) - ( Date: Tue, 23 Dec 2025 10:11:19 -0700 Subject: [PATCH 022/224] PRNG improvements --- src/closyr/ga.clj | 2 +- src/closyr/ops.clj | 2 +- src/closyr/ops/common.clj | 2 +- src/closyr/ops/modify.clj | 2 +- src/closyr/util/prng.clj | 5 +++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/closyr/ga.clj b/src/closyr/ga.clj index da6504c3..3003dc9d 100644 --- a/src/closyr/ga.clj +++ b/src/closyr/ga.clj @@ -2,7 +2,7 @@ (:refer-clojure :exclude [rand rand-int rand-nth shuffle]) (:require [closyr.util.log :as log] - [closyr.util.prng :refer :all])) + [closyr.util.prng :refer [rand rand-int rand-nth shuffle]])) (set! *warn-on-reflection* true) diff --git a/src/closyr/ops.clj b/src/closyr/ops.clj index 766b4ae1..2f07fcb7 100644 --- a/src/closyr/ops.clj +++ b/src/closyr/ops.clj @@ -7,7 +7,7 @@ [closyr.ops.eval :as ops-eval] [closyr.ops.modify :as ops-modify] [closyr.util.log :as log] - [closyr.util.prng :refer :all] + [closyr.util.prng :refer [rand rand-int rand-nth shuffle]] [closyr.util.spec :as specs]) (:import (java.text diff --git a/src/closyr/ops/common.clj b/src/closyr/ops/common.clj index 85e484f2..06f07458 100644 --- a/src/closyr/ops/common.clj +++ b/src/closyr/ops/common.clj @@ -3,7 +3,7 @@ (:require [clojure.core.async :as async :refer [go go-loop timeout !! ! chan put! take! alts!! alt!! close!]] [closyr.util.log :as log] - [closyr.util.prng :refer :all] + [closyr.util.prng :refer [rand rand-int rand-nth shuffle]] [closyr.util.spec :as specs]) (:import (java.util diff --git a/src/closyr/ops/modify.clj b/src/closyr/ops/modify.clj index 35fd98ff..dcdafd25 100644 --- a/src/closyr/ops/modify.clj +++ b/src/closyr/ops/modify.clj @@ -5,7 +5,7 @@ [clojure.string :as str] [closyr.ops.common :as ops-common] [closyr.util.log :as log] - [closyr.util.prng :refer :all] + [closyr.util.prng :refer [rand rand-int rand-nth shuffle]] [closyr.util.spec :as specs]) (:import (java.util.function diff --git a/src/closyr/util/prng.clj b/src/closyr/util/prng.clj index b1371980..807870cf 100644 --- a/src/closyr/util/prng.clj +++ b/src/closyr/util/prng.clj @@ -56,5 +56,6 @@ (RT/vector (.toArray al)))) -(set-random-seed! 888) -(rand-int 100) +(comment + (set-random-seed! 888) + (rand-int 100)) From 44dbd744a404e69115322d6122a0dee61b58b587 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 12:05:25 -0700 Subject: [PATCH 023/224] add deterministic mode using PRNG seed / doc update for Java API --- README.md | 113 ++++++++++++++++-- src/closyr/api/finder.clj | 39 ++++-- src/closyr/api/types.clj | 22 ++-- src/closyr/core.clj | 8 +- src/closyr/dataset/inputs.clj | 4 +- src/closyr/ga.clj | 26 +++- src/closyr/ops/common.clj | 7 +- src/closyr/symbolic_regression.clj | 82 ++++++++----- src/closyr/ui/gui.clj | 14 ++- src/closyr/util/prng.clj | 24 +++- src/closyr/util/spec.clj | 11 +- .../org/closyr/api/FormulaConfigBuilder.java | 24 +++- .../java/org/closyr/api/IFormulaConfig.java | 8 ++ test/closyr/api_test.clj | 68 +++++++++++ test/closyr/core_test.clj | 21 ++-- test/closyr/symbolic_regression_test.clj | 20 ++-- test/closyr/util_prng_test.clj | 41 +++++++ test/closyr/util_spec_test.clj | 1 + 18 files changed, 439 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 3c13b981..0a34edda 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,93 @@ Which is the same as: Requirements: Java, Leiningen (I will provide a deps file if enough interest) - (require '[closyr.symbolic-regression :as symreg]) + (require '[closyr.symbolic-regression :as symreg]) (symreg/run-app-with-gui) +### Java API + +You can use closyr as a library from Java or any JVM language. The Java API provides a clean interface layered on top of the Clojure implementation: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Your Java Application │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Java API (org.closyr.api) │ +│ ┌─────────────────┐ ┌──────────────────┐ ┌────────────────────────┐ │ +│ │ FormulaFinder │ │ FormulaConfig │ │ IFormulaResult │ │ +│ │ .find(xs, ys) │ │ .iterations() │ │ .getBestSolution() │ │ +│ │ .create() │ │ .populationSize │ │ .getAllSolutions() │ │ +│ │ │ │ .randomSeed() │ │ .getIterationsRun() │ │ +│ └─────────────────┘ └──────────────────┘ └────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Clojure Implementation Layer │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ closyr.api.finder ──▶ closyr.symbolic-regression │ │ +│ │ │ │ │ │ +│ │ ▼ ▼ │ │ +│ │ closyr.api.types closyr.ga (genetic algorithm) │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ closyr.ops (mutations/crossover) │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ Symja (symbolic math engine) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### Java Usage Example + +```java +import org.closyr.api.*; + +public class Example { + public static void main(String[] args) { + // Input data points + double[] xs = {1.0, 2.0, 3.0, 4.0, 5.0}; + double[] ys = {2.0, 4.0, 6.0, 8.0, 10.0}; + + // Simple usage with defaults + IFormulaResult result = FormulaFinder.find(xs, ys); + + // Or with custom configuration + IFormulaConfig config = FormulaConfigBuilder.builder() + .iterations(50) + .populationSize(100) + .maxLeafs(40) + .randomSeed(42) // Optional: for reproducible results + .build(); + + result = FormulaFinder.find(xs, ys, config); + + // Get the best formula found + IFormulaSolution best = result.getBestSolution(); + System.out.println("Formula: " + best.getFormula()); + System.out.println("Score: " + best.getScore()); + } +} +``` + +#### Reproducible Results in Java + +Use `randomSeed()` in the config builder for reproducible results: + +```java +IFormulaConfig config = FormulaConfigBuilder.builder() + .iterations(20) + .populationSize(50) + .randomSeed(12345) // Same seed = same results + .build(); +``` + +**Warning:** Setting a random seed enables **deterministic mode** which disables CPU parallelism. This ensures reproducibility but may result in slower execution. + ### Build and run JAR Requirements: Java, Leiningen @@ -81,14 +165,25 @@ You can also provide the same command-line options to `java` command, like: ## Options | Short, Long Option | Required? | Example | Default | Description | |-------------------------|-----------------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------| -| `-t`,`--headless` | no | `-t` | `false` | run without GUI, in terminal only | -| `-c`,`--use-flamechart` | no | `-c` | `false` | run with flamecharts, run then visit http://localhost:54321/flames.svg | -| `-p`,`--population` | no | `100` | `20` | size of population which will evolve; the number of functions we create and modify | -| `-i`,`--iterations` | no | `50` | `10` | number of iterations to run for | -| `-l`,`--max-leafs` | no | `40` | `40` | max number of AST tree leafs in candidate functions | -| `-x`,`--xs` | no, unless `ys` | `1,3,4` | random | the xs for the points in the dataset to fit against; if provided, must also provide `ys` and be the same count | -| `-y`,`--ys` | no, unless `xs` | `2,4,8` | random | the ys for the points in the dataset to fit against; if provided, must also provide `xs` and be the same count | -| `-f`,`--infile` | no | `f.csv` | | A CSV file. Contains either 2 columns without titles in first row, or has columns `x` and `y` to be used as objective dataset | +| `-t`,`--headless` | no | `-t` | `false` | run without GUI, in terminal only | +| `-c`,`--use-flamechart` | no | `-c` | `false` | run with flamecharts, run then visit http://localhost:54321/flames.svg | +| `-p`,`--population` | no | `100` | `20` | size of population which will evolve; the number of functions we create and modify | +| `-i`,`--iterations` | no | `50` | `10` | number of iterations to run for | +| `-l`,`--max-leafs` | no | `40` | `40` | max number of AST tree leafs in candidate functions | +| `-x`,`--xs` | no, unless `ys` | `1,3,4` | random | the xs for the points in the dataset to fit against; if provided, must also provide `ys` and be the same count | +| `-y`,`--ys` | no, unless `xs` | `2,4,8` | random | the ys for the points in the dataset to fit against; if provided, must also provide `xs` and be the same count | +| `-f`,`--infile` | no | `f.csv` | | A CSV file. Contains either 2 columns without titles in first row, or has columns `x` and `y` to be used as objective dataset | +| `-s`,`--seed` | no | `42` | | Random seed for reproducible results. **Warning:** Enables deterministic mode which disables CPU parallelism | + +### Reproducible Results with Random Seed + +You can use the `-s` or `--seed` option to get reproducible results: + + $ lein run -t -p 20 -i 5 -x 1,2,3,4,5 -y 2,4,6,8,10 -s 42 + +Running the same command with the same seed will produce identical results. + +**Warning:** When a random seed is set, the solver enters **deterministic mode** which disables CPU parallelism. This ensures reproducibility but may result in slower execution times for large populations. ## Example Screenshots diff --git a/src/closyr/api/finder.clj b/src/closyr/api/finder.clj index 26a107f9..cf182ad5 100644 --- a/src/closyr/api/finder.clj +++ b/src/closyr/api/finder.clj @@ -3,9 +3,12 @@ This namespace uses gen-class to create a Java-friendly FormulaFinder class." (:require [closyr.api.types :as types] + [closyr.ga :as ga] [closyr.ops.common :as ops-common] [closyr.ops.initialize :as ops-init] - [closyr.symbolic-regression :as symreg]) + [closyr.symbolic-regression :as symreg] + [closyr.util.log :as log] + [closyr.util.prng :as prng]) (:import (org.closyr.api IFormulaConfig @@ -35,18 +38,28 @@ iterations (.getIterations cfg) population-size (.getPopulationSize cfg) max-leafs (.getMaxLeafs cfg) - - run-config {:initial-phenos (ops-init/initial-phenotypes population-size) - :initial-muts (ops-init/initial-mutations) - :iters iterations - :use-gui? false - :use-flamechart false - :max-leafs max-leafs - :input-xs-exprs (ops-common/doubles->exprs xs-vec) - :input-ys-exprs (ops-common/doubles->exprs ys-vec)} - - result (symreg/run-find-formula run-config)] - (types/->formula-result result))) + random-seed (.getRandomSeed cfg) + use-seed? (>= random-seed 0)] + + ;; Set the random seed if provided (>= 0 means seed is set) + (when use-seed? + (log/warn "Deterministic mode enabled with seed:" random-seed + "- CPU parallelism disabled for reproducibility") + (prng/set-random-seed! random-seed)) + + ;; Run with deterministic mode if seed is set (disables parallel execution) + (binding [ga/*deterministic-mode* use-seed?] + (let [run-config {:initial-phenos (ops-init/initial-phenotypes population-size) + :initial-muts (ops-init/initial-mutations) + :iters iterations + :use-gui? false + :use-flamechart false + :max-leafs max-leafs + :input-xs-exprs (ops-common/doubles->exprs xs-vec) + :input-ys-exprs (ops-common/doubles->exprs ys-vec)} + + result (symreg/run-find-formula run-config)] + (types/->formula-result result))))) (defn validate-inputs diff --git a/src/closyr/api/types.clj b/src/closyr/api/types.clj index bf124b2c..bd6416a9 100644 --- a/src/closyr/api/types.clj +++ b/src/closyr/api/types.clj @@ -97,7 +97,8 @@ (deftype FormulaConfig [^int iterations ^int population-size - ^int max-leafs] + ^int max-leafs + ^long random-seed] IFormulaConfig @@ -107,12 +108,15 @@ (getMaxLeafs [_] max-leafs) + (getRandomSeed [_] random-seed) + Object (toString [_] (str "FormulaConfig{iterations=" iterations ", populationSize=" population-size - ", maxLeafs=" max-leafs "}"))) + ", maxLeafs=" max-leafs + ", randomSeed=" random-seed "}"))) (defn config @@ -121,16 +125,19 @@ Options: :iterations - Number of GA iterations (default: 20) :population-size - Population size (default: 100) - :max-leafs - Max expression tree leaves (default: 40)" + :max-leafs - Max expression tree leaves (default: 40) + :random-seed - Random seed for reproducibility (default: -1, meaning no seed)" ([] (config {})) - ([{:keys [iterations population-size max-leafs] + ([{:keys [iterations population-size max-leafs random-seed] :or {iterations 20 population-size 100 - max-leafs 40}}] + max-leafs 40 + random-seed -1}}] (FormulaConfig. (int iterations) (int population-size) - (int max-leafs)))) + (int max-leafs) + (long random-seed)))) (defn config->map @@ -138,4 +145,5 @@ [^IFormulaConfig cfg] {:iterations (.getIterations cfg) :population-size (.getPopulationSize cfg) - :max-leafs (.getMaxLeafs cfg)}) + :max-leafs (.getMaxLeafs cfg) + :random-seed (.getRandomSeed cfg)}) diff --git a/src/closyr/core.clj b/src/closyr/core.clj index 01833066..6e87f763 100644 --- a/src/closyr/core.clj +++ b/src/closyr/core.clj @@ -5,7 +5,8 @@ [clojure.tools.cli :as cli] [closyr.symbolic-regression :as symreg] [closyr.util.csv :as input-csv] - [closyr.util.log :as log]) + [closyr.util.log :as log] + [closyr.util.prng :as prng]) (:import (java.io File))) @@ -77,6 +78,11 @@ :default false :id :use-flamechart] + ["-s" "--seed SEED" "Random seed for reproducible results" + :default nil + :parse-fn #(Long/parseLong %) + :id :seed] + #_["-v" nil "Verbosity level" :id :verbosity :default 0 diff --git a/src/closyr/dataset/inputs.clj b/src/closyr/dataset/inputs.clj index 65554efd..d69ce9b5 100644 --- a/src/closyr/dataset/inputs.clj +++ b/src/closyr/dataset/inputs.clj @@ -1,7 +1,9 @@ (ns closyr.dataset.inputs + (:refer-clojure :exclude [rand rand-int rand-nth shuffle]) (:require [closyr.dataset.prime-10000 :as data-primes] - [closyr.dataset.prime-counting :as data-prime-counting])) + [closyr.dataset.prime-counting :as data-prime-counting] + [closyr.util.prng :refer [rand rand-int rand-nth shuffle]])) (set! *warn-on-reflection* true) diff --git a/src/closyr/ga.clj b/src/closyr/ga.clj index 3003dc9d..50167e67 100644 --- a/src/closyr/ga.clj +++ b/src/closyr/ga.clj @@ -8,6 +8,20 @@ (set! *warn-on-reflection* true) +(def ^:dynamic *deterministic-mode* + "When true, uses sequential map instead of pmap for deterministic results. + Set to true when using a seeded PRNG for reproducibility." + false) + + +(defn- maybe-pmap + "Uses pmap for parallel execution unless *deterministic-mode* is true." + [f coll] + (if *deterministic-mode* + (mapv f coll) + (pmap f coll))) + + (defn initialize "Initialize GA population and functions" [initial-pop score-fn mutation-fn crossover-fn] @@ -62,18 +76,18 @@ (try (let [pop-shuff (->> pop - (pmap (partial with-score score-fn)) + (maybe-pmap (partial with-score score-fn)) (shuffle)) new-pop-data (->> (partition-all (pop->chunks pop) pop-shuff) - (pmap (fn [pop-chunk] - (mapv (partial compete config) - (partition-all 2 pop-chunk)))) + (maybe-pmap (fn [pop-chunk] + (mapv (partial compete config) + (partition-all 2 pop-chunk)))) (mapcat identity)) - pop-scores (vec (pmap first new-pop-data)) - new-pop (->> (pmap second new-pop-data) + pop-scores (vec (maybe-pmap first new-pop-data)) + new-pop (->> (maybe-pmap second new-pop-data) (mapcat identity) (vec))] diff --git a/src/closyr/ops/common.clj b/src/closyr/ops/common.clj index 06f07458..7a7a7117 100644 --- a/src/closyr/ops/common.clj +++ b/src/closyr/ops/common.clj @@ -3,12 +3,11 @@ (:require [clojure.core.async :as async :refer [go go-loop timeout !! ! chan put! take! alts!! alt!! close!]] [closyr.util.log :as log] - [closyr.util.prng :refer [rand rand-int rand-nth shuffle]] + [closyr.util.prng :as prng :refer [rand rand-int rand-nth shuffle]] [closyr.util.spec :as specs]) (:import (java.util - Date - UUID) + Date) (java.util.function Function) (org.matheclipse.core.eval @@ -139,7 +138,7 @@ (let [^ExprEvaluator util (or util (new-util))] {:sym variable :util util - :id (UUID/randomUUID) + :id (prng/random-uuid) :expr (.eval util (valid-expr-or-default variable expr))}) (catch Exception e (log/error "Err creating pheno from expr/x: " diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index bb15434b..c6c81f95 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -7,6 +7,7 @@ [closyr.ops.initialize :as ops-init] [closyr.ui.gui :as gui] [closyr.util.log :as log] + [closyr.util.prng :as prng] [closyr.util.spec :as specs] [flames.core :as flames] [malli.core :as m] @@ -307,6 +308,7 @@ input-data-y :input-data-y input-iters :input-iters input-phenos-count :input-phenos-count + random-seed :random-seed max-leafs :max-leafs}] (let [input-xs-exprs (ops-common/doubles->exprs input-data-x) @@ -319,6 +321,7 @@ :input-ys-vec input-ys-vec :input-iters input-iters :input-phenos-count input-phenos-count + :random-seed random-seed :max-leafs max-leafs}))) @@ -361,6 +364,7 @@ input-iters :input-iters iters :iters input-phenos-count :input-phenos-count + random-seed :random-seed max-leafs :max-leafs initial-phenos :initial-phenos}] @@ -381,6 +385,7 @@ :input-iters (or input-iters iters) :initial-phenos initial-phenos :input-phenos-count input-phenos-count + :random-seed random-seed :max-leafs max-leafs}) @@ -555,7 +560,7 @@ (defn- merge-cli-and-gui-args [{cli-max-leafs :max-leafs :keys [iters initial-phenos initial-muts use-gui?] :as run-config} - {:keys [input-iters input-phenos-count max-leafs input-xs-list input-xs-count input-ys-vec + {:keys [input-iters input-phenos-count random-seed max-leafs input-xs-list input-xs-count input-ys-vec sim-stop-start-chan sim->gui-chan] :as run-args}] @@ -567,6 +572,7 @@ run-config (assoc run-config :initial-phenos initial-phenos + :random-seed random-seed :iters iters :max-leafs (or max-leafs ops/default-max-leafs)) @@ -583,23 +589,29 @@ :as run-args}] (let [run-config (merge-cli-and-gui-args run-config run-args) {:keys [next-step] :as completed-ga-data} (run-solver-ga-iterations run-config run-args)] + (binding [ga/*deterministic-mode* (some? (:random-seed run-config))] + ;; Set the random seed if provided + (when (:random-seed run-config) + (log/warn "Deterministic mode enabled with seed:" (:random-seed run-config) + "- CPU parallelism disabled for reproducibility") + (prng/set-random-seed! (:random-seed run-config))) - (case next-step + (case next-step - :stop - completed-ga-data + :stop + completed-ga-data - :wait - (if use-gui? - (do (log/info "-- Waiting for GUI input to start again --") - (if-let [new-gui-args (wait-and-get-gui-args sim-stop-start-chan)] - (recur run-config (merge run-args new-gui-args)) - completed-ga-data)) - completed-ga-data) + :wait + (if use-gui? + (do (log/info "-- Waiting for GUI input to start again --") + (if-let [new-gui-args (wait-and-get-gui-args sim-stop-start-chan)] + (run-from-inputs run-config (merge run-args new-gui-args)) + completed-ga-data)) + completed-ga-data) - :restart - (do (log/info "-- Restarting... --") - (recur run-config (merge run-args (->run-args @sim-input-args*))))))) + :restart + (do (log/info "-- Restarting... --") + (run-from-inputs run-config (merge run-args (->run-args @sim-input-args*)))))))) (defn- in-flames @@ -710,24 +722,32 @@ (defn run-app-from-cli-args "Run app from CLI args" {:malli/schema [:=> [:cat #'specs/CLIArgs] #'specs/SolverRunResults]} - [{:keys [iterations population headless xs ys use-flamechart max-leafs] :as cli-opts}] + [{:keys [iterations population headless xs ys use-flamechart max-leafs seed] :as cli-opts}] (log/info "CLI: run from options: " cli-opts) - (let [run-config {:initial-phenos (ops-init/initial-phenotypes population) - :initial-muts (ops-init/initial-mutations) - :iters iterations - :use-gui? (not headless) - :max-leafs max-leafs - :use-flamechart use-flamechart - :input-xs-exprs (if xs - (ops-common/doubles->exprs xs) - example-input-xs-exprs) - :input-ys-exprs (if ys - (ops-common/doubles->exprs ys) - example-input-ys-exprs)} - result (run-find-formula run-config)] - (log/info "CLI: Done!") - (exit cli-opts) - result)) + ;; Run with deterministic mode if seed is set (disables parallel execution) + (binding [ga/*deterministic-mode* (some? seed)] + ;; Set the random seed if provided + (when seed + (log/warn "Deterministic mode enabled with seed:" seed + "- CPU parallelism disabled for reproducibility") + (prng/set-random-seed! seed)) + (let [run-config {:initial-phenos (ops-init/initial-phenotypes population) + :initial-muts (ops-init/initial-mutations) + :iters iterations + :use-gui? (not headless) + :random-seed seed + :max-leafs max-leafs + :use-flamechart use-flamechart + :input-xs-exprs (if xs + (ops-common/doubles->exprs xs) + example-input-xs-exprs) + :input-ys-exprs (if ys + (ops-common/doubles->exprs ys) + example-input-ys-exprs)} + result (run-find-formula run-config)] + (log/info "CLI: Done!") + (exit cli-opts) + result))) ;; todo: feel kind of hacky to have to call this in ?every? ns that has defn schema diff --git a/src/closyr/ui/gui.clj b/src/closyr/ui/gui.clj index 0ca19fa9..2b8f00b0 100644 --- a/src/closyr/ui/gui.clj +++ b/src/closyr/ui/gui.clj @@ -1,4 +1,5 @@ (ns closyr.ui.gui + (:refer-clojure :exclude [rand rand-int rand-nth shuffle]) (:require [clojure.core.async :as async :refer [go go-loop timeout !! ! chan put! alts!]] [clojure.java.io :as io] @@ -6,6 +7,7 @@ [closyr.ui.plot :as plot] [closyr.util.csv :as input-csv] [closyr.util.log :as log] + [closyr.util.prng :refer [rand rand-int rand-nth shuffle]] [seesaw.behave :as sb] [seesaw.border :as sbr] [seesaw.core :as ss] @@ -108,7 +110,8 @@ (def ^:private experiment-settings* (atom {:max-leafs 40 :input-iters 100 - :input-phenos-count 2000})) + :input-phenos-count 2000 + :random-seed nil})) (def ^:private amount->number @@ -945,7 +948,12 @@ (.add btns-row) (.add status-row)) - settings-container (doto (panel-grid {:rows 2 :cols 1}) + random-seed-panel (doto (panel-grid + {:rows 1 :cols 1 :border (radio-controls-border "Random Seed")}) + (.add (JLabel. "HI"))) + + settings-container (doto (panel-grid {:rows 3 :cols 1}) + ; (.add random-seed-panel) ;; TODO: implement numerical text input, show perf warning when deterministic mode, provide a way to go back to parallel mode (unset random seed) (.add settings-panel) (.add input-fn-container)) @@ -1061,7 +1069,7 @@ (log/info "Test GUI: Resuming: " ( 127 + (aset bytes 6 (unchecked-byte (bit-or (bit-and (aget bytes 6) 0x0f) 0x40))) + (aset bytes 8 (unchecked-byte (bit-or (bit-and (aget bytes 8) 0x3f) 0x80))) + ;; Convert bytes to UUID + (let [msb (reduce (fn [acc i] + (bit-or (bit-shift-left acc 8) + (bit-and (aget bytes i) 0xff))) + 0 (range 8)) + lsb (reduce (fn [acc i] + (bit-or (bit-shift-left acc 8) + (bit-and (aget bytes i) 0xff))) + 0 (range 8 16))] + (java.util.UUID. msb lsb)))) + + (comment (set-random-seed! 888) (rand-int 100)) diff --git a/src/closyr/util/spec.clj b/src/closyr/util/spec.clj index 016bfe06..30d9b190 100644 --- a/src/closyr/util/spec.clj +++ b/src/closyr/util/spec.clj @@ -144,6 +144,9 @@ (def ^:private PopulationCount [:int {:min 1 :max 100000}]) +(def ^:private RandomSeed + [:int {:min Integer/MIN_VALUE :max Integer/MAX_VALUE}]) + (def ^:private GAPhenotype [:map @@ -189,6 +192,7 @@ [:use-gui? :boolean] [:max-leafs #'MaxLeafs] [:input-phenos-count {:optional true} #'PopulationCount] + [:random-seed {:optional true} [:maybe #'RandomSeed]] [:log-steps pos-int?] [:use-flamechart [:maybe :boolean]] [:input-xs-exprs [:vector #'SymbolicExpr]] @@ -219,6 +223,7 @@ [:input-iters #'Iterations] [:initial-phenos [:maybe #'GAPopulationPhenotypes]] [:input-phenos-count [:maybe #'PopulationCount]] + [:random-seed {:optional true} [:maybe #'PopulationCount]] [:max-leafs [:maybe #'MaxLeafs]]]) @@ -264,6 +269,7 @@ [:input-ys-vec #'NumberVector] [:input-iters #'Iterations] [:input-phenos-count #'PopulationCount] + [:random-seed {:optional true} [:maybe #'RandomSeed]] [:max-leafs [:maybe #'MaxLeafs]]]) @@ -281,6 +287,7 @@ [:input-iters {:optional true} #'Iterations] [:iters {:optional true} #'Iterations] [:input-phenos-count {:optional true} #'PopulationCount] + [:random-seed {:optional true} [:maybe #'RandomSeed]] [:max-leafs {:optional true} [:maybe #'MaxLeafs]]]) @@ -292,6 +299,7 @@ [:input-data-y #'NumberVector] [:input-iters #'Iterations] [:input-phenos-count #'PopulationCount] + [:random-seed {:optional true} [:maybe #'RandomSeed]] [:max-leafs {:optional true} [:maybe #'MaxLeafs]]]) @@ -305,7 +313,8 @@ [:xs {:optional true} [:maybe #'NumberVector]] [:ys {:optional true} [:maybe #'NumberVector]] [:use-flamechart {:optional true} boolean?] - [:max-leafs {:optional true} #'MaxLeafs]]) + [:max-leafs {:optional true} #'MaxLeafs] + [:seed {:optional true} [:maybe #'RandomSeed]]]) (def ^:private ModificationsResult diff --git a/src/main/java/org/closyr/api/FormulaConfigBuilder.java b/src/main/java/org/closyr/api/FormulaConfigBuilder.java index b051f3d7..cdd07365 100644 --- a/src/main/java/org/closyr/api/FormulaConfigBuilder.java +++ b/src/main/java/org/closyr/api/FormulaConfigBuilder.java @@ -17,6 +17,7 @@ public class FormulaConfigBuilder { private int iterations = 20; private int populationSize = 100; private int maxLeafs = 40; + private long randomSeed = -1L; private FormulaConfigBuilder() { } @@ -52,11 +53,20 @@ public FormulaConfigBuilder maxLeafs(int maxLeafs) { return this; } + /** + * Set the random seed for reproducible results. + * Use -1 for non-deterministic behavior (default). + */ + public FormulaConfigBuilder randomSeed(long randomSeed) { + this.randomSeed = randomSeed; + return this; + } + /** * Build the configuration. */ public IFormulaConfig build() { - return new SimpleFormulaConfig(iterations, populationSize, maxLeafs); + return new SimpleFormulaConfig(iterations, populationSize, maxLeafs, randomSeed); } /** @@ -66,11 +76,13 @@ private static class SimpleFormulaConfig implements IFormulaConfig { private final int iterations; private final int populationSize; private final int maxLeafs; + private final long randomSeed; - SimpleFormulaConfig(int iterations, int populationSize, int maxLeafs) { + SimpleFormulaConfig(int iterations, int populationSize, int maxLeafs, long randomSeed) { this.iterations = iterations; this.populationSize = populationSize; this.maxLeafs = maxLeafs; + this.randomSeed = randomSeed; } @Override @@ -88,11 +100,17 @@ public int getMaxLeafs() { return maxLeafs; } + @Override + public long getRandomSeed() { + return randomSeed; + } + @Override public String toString() { return "FormulaConfig{iterations=" + iterations + ", populationSize=" + populationSize + - ", maxLeafs=" + maxLeafs + "}"; + ", maxLeafs=" + maxLeafs + + ", randomSeed=" + randomSeed + "}"; } } } diff --git a/src/main/java/org/closyr/api/IFormulaConfig.java b/src/main/java/org/closyr/api/IFormulaConfig.java index d1c365c8..b36a5d2e 100644 --- a/src/main/java/org/closyr/api/IFormulaConfig.java +++ b/src/main/java/org/closyr/api/IFormulaConfig.java @@ -19,4 +19,12 @@ public interface IFormulaConfig { * Get the maximum number of leaf nodes allowed in expression trees. */ int getMaxLeafs(); + + /** + * Get the random seed for reproducible results. + * A value of -1 means no seed is set (non-deterministic). + */ + default long getRandomSeed() { + return -1L; + } } diff --git a/test/closyr/api_test.clj b/test/closyr/api_test.clj index 38717f4b..4145a118 100644 --- a/test/closyr/api_test.clj +++ b/test/closyr/api_test.clj @@ -216,3 +216,71 @@ str-repr (.toString solution)] (is (.contains str-repr "FormulaSolution")) (is (.contains str-repr "formula"))))) + + +;; ============================================================================ +;; Deterministic seed tests +;; ============================================================================ + +(deftest test-random-seed-produces-deterministic-results + (testing "Running solver with same seed produces identical results" + (let [xs (double-array [1.0 2.0 3.0 4.0 5.0]) + ys (double-array [2.0 4.0 6.0 8.0 10.0]) + seed 42 + config (-> (FormulaConfigBuilder/builder) + (.iterations 3) + (.populationSize 20) + (.randomSeed seed) + (.build)) + ;; Run solver twice with same seed + result1 (FormulaFinder/find xs ys config) + result2 (FormulaFinder/find xs ys config) + best1 (.getBestSolution result1) + best2 (.getBestSolution result2)] + ;; Results should be identical + (is (= (.getFormula best1) (.getFormula best2)) + "Same seed should produce identical formulas") + (is (= (.getScore best1) (.getScore best2)) + "Same seed should produce identical scores") + (is (= (.getLeafCount best1) (.getLeafCount best2)) + "Same seed should produce identical leaf counts")))) + + +(deftest test-different-seeds-produce-different-results + (testing "Running solver with different seeds produces different results" + (let [xs (double-array [1.0 2.0 3.0 4.0 5.0]) + ys (double-array [1.0 4.0 9.0 16.0 25.0]) + config1 (-> (FormulaConfigBuilder/builder) + (.iterations 3) + (.populationSize 20) + (.randomSeed 123) + (.build)) + config2 (-> (FormulaConfigBuilder/builder) + (.iterations 3) + (.populationSize 20) + (.randomSeed 456) + (.build)) + result1 (FormulaFinder/find xs ys config1) + result2 (FormulaFinder/find xs ys config2) + best1 (.getBestSolution result1) + best2 (.getBestSolution result2) + ;; Get all solution formulas to compare + all-formulas1 (set (map #(.getFormula %) (.getAllSolutions result1))) + all-formulas2 (set (map #(.getFormula %) (.getAllSolutions result2)))] + ;; The full population should differ between runs with different seeds + (is (not= all-formulas1 all-formulas2) + "Different seeds should produce different populations")))) + + +(deftest test-config-builder-with-seed + (testing "FormulaConfigBuilder accepts random seed" + (let [config (-> (FormulaConfigBuilder/builder) + (.randomSeed 12345) + (.build))] + (is (= 12345 (.getRandomSeed config)))))) + + +(deftest test-clojure-config-with-seed + (testing "Clojure config accepts random seed" + (let [config (types/config {:random-seed 98765})] + (is (= 98765 (.getRandomSeed config)))))) diff --git a/test/closyr/core_test.clj b/test/closyr/core_test.clj index f8f0e041..8eab1d42 100644 --- a/test/closyr/core_test.clj +++ b/test/closyr/core_test.clj @@ -26,7 +26,8 @@ :iterations 200 :population 1000 :xs [0.0 1.0 2.0 3.0 4.0 5.0 6.0] - :ys [1.0 2.0 30.0 4.0 5.0 6.0 10.0]}))) + :ys [1.0 2.0 30.0 4.0 5.0 6.0 10.0] + :seed nil}))) (testing "if xs, also needs ys" @@ -70,7 +71,8 @@ :population 1000, :use-flamechart false, :iterations 200, - :max-leafs 40}))) + :max-leafs 40 + :seed nil}))) (testing "handles valid short options w csv data" @@ -85,7 +87,8 @@ :iterations 200 :population 1000 :xs [0.0 1.0 2.0 3.0 4.0 6.0 15.0 20.0] - :ys [1.0 1.0 1.0 2.0 3.0 0.0 -1.0 -12.0]}))) + :ys [1.0 1.0 1.0 2.0 3.0 0.0 -1.0 -12.0] + :seed nil}))) (testing "handles valid long options w data inline" (is (= @@ -99,7 +102,8 @@ :iterations 200 :population 1000 :xs [0.0 1.0 2.0 3.0 4.0 5.0 6.0] - :ys [1.0 2.0 30.0 4.0 5.0 6.0 10.0]}))) + :ys [1.0 2.0 30.0 4.0 5.0 6.0 10.0] + :seed nil}))) (testing "handles valid long options w csv data with columns" @@ -114,7 +118,8 @@ :iterations 200 :population 1000 :xs [0.0 1.0 2.0 3.0 4.0 6.0 15.0 20.0] - :ys [1.0 1.0 1.0 2.0 3.0 0.0 -1.0 -12.0]}))) + :ys [1.0 1.0 1.0 2.0 3.0 0.0 -1.0 -12.0] + :seed nil}))) (testing "handles valid long options w csv data with columns with in order y,x" @@ -129,7 +134,8 @@ :iterations 200 :population 1000 :xs [0.0 1.0 2.0 3.0 4.0 6.0 15.0 20.0 30.0 45.0 55.0 60.0] - :ys [1.0 1.0 1.0 2.0 3.0 0.0 -1.0 -12.0 -22.0 -25.0 -10.0 10.0]}))) + :ys [1.0 1.0 1.0 2.0 3.0 0.0 -1.0 -12.0 -22.0 -25.0 -10.0 10.0] + :seed nil}))) (testing "handles valid long options w csv data without columns" (is (= @@ -143,7 +149,8 @@ :iterations 200 :population 1000 :xs [0.0 1.0 2.0 3.0 4.0 6.0 15.0 20.0] - :ys [1.0 1.0 1.0 2.0 3.0 0.0 -1.0 -12.0]})))) + :ys [1.0 1.0 1.0 2.0 3.0 0.0 -1.0 -12.0] + :seed nil})))) (deftest main-test diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index 768138fa..0fb03a87 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -60,18 +60,20 @@ {:iters-done 123 :next-step :stop})) - (is (= @args* - [{:iters 20 + (is (= [{:iters 20 :log-steps 200 :max-leafs 20 :use-gui? false + :random-seed nil :use-flamechart true} {:input-iters 20 :input-phenos-count nil + :random-seed nil :input-xs-count 3 :input-xs-vec [0.0 1.0 2.0] :input-ys-vec [1.0 4.0 19.0] - :max-leafs 20}]))))) + :max-leafs 20}] + @args*))))) (testing "args from CLI are passed along correctly to implementation, and when none are passed we use defaults" @@ -101,18 +103,20 @@ {:iters-done 123 :next-step :stop})) - (is (= @args* - [{:iters 20 + (is (= [{:iters 20 :log-steps 200 :max-leafs 40 :use-gui? false + :random-seed nil :use-flamechart nil} {:input-iters 20 :input-phenos-count nil + :random-seed nil :input-xs-count 50 :input-xs-vec [0.0 0.20943951023931953 0.41887902047863906 0.6283185307179586 0.8377580409572781 1.0471975511965976 1.2566370614359172 1.4660765716752369 1.6755160819145563 1.8849555921538759 2.0943951023931953 2.3038346126325147 2.5132741228718345 2.7227136331111543 2.9321531433504737 3.141592653589793 3.3510321638291125 3.560471674068432 3.7699111843077517 3.979350694547071 4.1887902047863905 4.39822971502571 4.607669225265029 4.81710873550435 5.026548245743669 5.235987755982989 5.445427266222309 5.654866776461628 5.8643062867009474 6.073745796940266 6.283185307179586 6.492624817418906 6.702064327658225 6.911503837897546 7.120943348136864 7.3303828583761845 7.5398223686155035 7.749261878854823 7.958701389094142 8.168140899333462 8.377580409572781 8.587019919812102 8.79645943005142 9.00589894029074 9.215338450530059 9.42477796076938 9.6342174710087 9.843656981248019 10.053096491487338 10.262536001726657] :input-ys-vec [0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0] - :max-leafs nil}])))))) + :max-leafs nil}] + @args*)))))) (deftest can-run-experiment @@ -301,7 +305,8 @@ :input-xs-exprs :input-xs-vec :input-ys-vec - :max-leafs})) + :max-leafs + :random-seed})) (is (put! symreg/*sim-stop-start-chan* {:new-state :stop})) @@ -366,6 +371,7 @@ :initial-phenos :input-iters :input-phenos-count + :random-seed :input-xs-count :input-xs-list :input-xs-vec diff --git a/test/closyr/util_prng_test.clj b/test/closyr/util_prng_test.clj index 05b767dd..f49ea9cc 100644 --- a/test/closyr/util_prng_test.clj +++ b/test/closyr/util_prng_test.clj @@ -23,3 +23,44 @@ (testing "can sample rand ints" (is (= (mapv #(test-rand-int-gen % 50) [1 5 10 20 50 75 100 1000 10000 100000]) [[36 5] [36 8] [36 22] [36 30] [36 29] [36 46] [36 36] [35 12] [44 40] [26 29]])))) + + +(deftest test-random-uuid-deterministic + (testing "random-uuid is deterministic with seed" + (prng/set-random-seed! 12345) + (let [uuid1 (prng/random-uuid)] + (prng/set-random-seed! 12345) + (let [uuid2 (prng/random-uuid)] + (is (= uuid1 uuid2) + "Same seed should produce same UUID"))))) + + +(deftest test-random-uuid-different-seeds + (testing "random-uuid produces different results with different seeds" + (prng/set-random-seed! 111) + (let [uuid1 (prng/random-uuid)] + (prng/set-random-seed! 222) + (let [uuid2 (prng/random-uuid)] + (is (not= uuid1 uuid2) + "Different seeds should produce different UUIDs"))))) + + +(deftest test-shuffle-deterministic + (testing "shuffle is deterministic with seed" + (prng/set-random-seed! 42) + (let [result1 (prng/shuffle [1 2 3 4 5 6 7 8 9 10])] + (prng/set-random-seed! 42) + (let [result2 (prng/shuffle [1 2 3 4 5 6 7 8 9 10])] + (is (= result1 result2) + "Same seed should produce same shuffle order"))))) + + +(deftest test-rand-nth-deterministic + (testing "rand-nth is deterministic with seed" + (let [coll [:a :b :c :d :e :f :g :h :i :j]] + (prng/set-random-seed! 999) + (let [picks1 (vec (repeatedly 5 #(prng/rand-nth coll)))] + (prng/set-random-seed! 999) + (let [picks2 (vec (repeatedly 5 #(prng/rand-nth coll)))] + (is (= picks1 picks2) + "Same seed should produce same rand-nth sequence")))))) diff --git a/test/closyr/util_spec_test.clj b/test/closyr/util_spec_test.clj index 454ce815..d4eef1a0 100644 --- a/test/closyr/util_spec_test.clj +++ b/test/closyr/util_spec_test.clj @@ -26,6 +26,7 @@ {:max-leafs 20, :input-iters 10000, :input-phenos-count 50000, + :random-seed -1, :new-state :start, :input-data-x [0.0 From 929652170be64ce123ff8dc9954d4286de2bbe87 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 12:11:10 -0700 Subject: [PATCH 024/224] reorg readme --- README.md | 134 +++++++++++++++++++++++++++--------------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 0a34edda..77c93583 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,67 @@ Requirements: Java, Leiningen (I will provide a deps file if enough interest) (require '[closyr.symbolic-regression :as symreg]) (symreg/run-app-with-gui) +### Build and run JAR + +Requirements: Java, Leiningen + + + $ lein uberjar + $ java -jar target/uberjar/closyr-0.1.0-SNAPSHOT-standalone.jar + +You can also provide the same command-line options to `java` command, like: + + $ java -jar target/uberjar/closyr-0.1.0-SNAPSHOT-standalone.jar -t -p 25 -i 5 -x 0,1,2,3,4,5,6 -y 1,2,30,4,5,6,10 + +## Options +| Short, Long Option | Required? | Example | Default | Description | +|-------------------------|-----------------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------| +| `-t`,`--headless` | no | `-t` | `false` | run without GUI, in terminal only | +| `-c`,`--use-flamechart` | no | `-c` | `false` | run with flamecharts, run then visit http://localhost:54321/flames.svg | +| `-p`,`--population` | no | `100` | `20` | size of population which will evolve; the number of functions we create and modify | +| `-i`,`--iterations` | no | `50` | `10` | number of iterations to run for | +| `-l`,`--max-leafs` | no | `40` | `40` | max number of AST tree leafs in candidate functions | +| `-x`,`--xs` | no, unless `ys` | `1,3,4` | random | the xs for the points in the dataset to fit against; if provided, must also provide `ys` and be the same count | +| `-y`,`--ys` | no, unless `xs` | `2,4,8` | random | the ys for the points in the dataset to fit against; if provided, must also provide `xs` and be the same count | +| `-f`,`--infile` | no | `f.csv` | | A CSV file. Contains either 2 columns without titles in first row, or has columns `x` and `y` to be used as objective dataset | +| `-s`,`--seed` | no | `42` | | Random seed for reproducible results. **Warning:** Enables deterministic mode which disables CPU parallelism | + +### Reproducible Results with Random Seed + +You can use the `-s` or `--seed` option to get reproducible results: + + $ lein run -t -p 20 -i 5 -x 1,2,3,4,5 -y 2,4,6,8,10 -s 42 + +Running the same command with the same seed will produce identical results. + +**Warning:** When a random seed is set, the solver enters **deterministic mode** which disables CPU parallelism. This ensures reproducibility but may result in slower execution times for large populations. + +## Example Screenshots + +On successful application start, you can start and run a search. You might see something like this when done: + +![gui_done_2024-02-12_08-44.png](screenshots%2Fgui_done_2024-02-12_08-44.png) + +An example of using the GUI: + +![gui_usage_clip_Peek 2024-02-12 09-03.gif](screenshots%2Fgui_usage_clip_Peek%202024-02-12%2009-03.gif) + +## Tests + + $ lein test + +Coverage looks like this if you run `lein cloverage`: + +![test_coverage_2024-02-16_16-39.png](screenshots%2Ftest_coverage_2024-02-16_16-39.png) + +## How It Works + +- We use Genetic Algorithms to allow candidate functions of best fit to compete. +- They compete on a computed score on the input data, and we use the sum of residuals to generate the score. +- Evolution consists of mutation and crossover. +- Mutations act on the function's AST and modify branches, leafs, or the whole tree. Operations like `+0.1`, `*x`, `/Sin(x)` are applied to functions. +- Crossovers combine two functions' ASTs at a random point, and combine using various operators like `+`, and `*`. + ### Java API You can use closyr as a library from Java or any JVM language. The Java API provides a clean interface layered on top of the Clojure implementation: @@ -91,14 +152,14 @@ You can use closyr as a library from Java or any JVM language. The Java API prov │ Clojure Implementation Layer │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ closyr.api.finder ──▶ closyr.symbolic-regression │ │ -│ │ │ │ │ │ -│ │ ▼ ▼ │ │ +│ │ │ │ │ │ +│ │ ▼ ▼ │ │ │ │ closyr.api.types closyr.ga (genetic algorithm) │ │ -│ │ │ │ │ -│ │ ▼ │ │ +│ │ │ │ │ +│ │ ▼ │ │ │ │ closyr.ops (mutations/crossover) │ │ -│ │ │ │ │ -│ │ ▼ │ │ +│ │ │ │ │ +│ │ ▼ │ │ │ │ Symja (symbolic math engine) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ @@ -150,67 +211,6 @@ IFormulaConfig config = FormulaConfigBuilder.builder() **Warning:** Setting a random seed enables **deterministic mode** which disables CPU parallelism. This ensures reproducibility but may result in slower execution. -### Build and run JAR - -Requirements: Java, Leiningen - - - $ lein uberjar - $ java -jar target/uberjar/closyr-0.1.0-SNAPSHOT-standalone.jar - -You can also provide the same command-line options to `java` command, like: - - $ java -jar target/uberjar/closyr-0.1.0-SNAPSHOT-standalone.jar -t -p 25 -i 5 -x 0,1,2,3,4,5,6 -y 1,2,30,4,5,6,10 - -## Options -| Short, Long Option | Required? | Example | Default | Description | -|-------------------------|-----------------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------| -| `-t`,`--headless` | no | `-t` | `false` | run without GUI, in terminal only | -| `-c`,`--use-flamechart` | no | `-c` | `false` | run with flamecharts, run then visit http://localhost:54321/flames.svg | -| `-p`,`--population` | no | `100` | `20` | size of population which will evolve; the number of functions we create and modify | -| `-i`,`--iterations` | no | `50` | `10` | number of iterations to run for | -| `-l`,`--max-leafs` | no | `40` | `40` | max number of AST tree leafs in candidate functions | -| `-x`,`--xs` | no, unless `ys` | `1,3,4` | random | the xs for the points in the dataset to fit against; if provided, must also provide `ys` and be the same count | -| `-y`,`--ys` | no, unless `xs` | `2,4,8` | random | the ys for the points in the dataset to fit against; if provided, must also provide `xs` and be the same count | -| `-f`,`--infile` | no | `f.csv` | | A CSV file. Contains either 2 columns without titles in first row, or has columns `x` and `y` to be used as objective dataset | -| `-s`,`--seed` | no | `42` | | Random seed for reproducible results. **Warning:** Enables deterministic mode which disables CPU parallelism | - -### Reproducible Results with Random Seed - -You can use the `-s` or `--seed` option to get reproducible results: - - $ lein run -t -p 20 -i 5 -x 1,2,3,4,5 -y 2,4,6,8,10 -s 42 - -Running the same command with the same seed will produce identical results. - -**Warning:** When a random seed is set, the solver enters **deterministic mode** which disables CPU parallelism. This ensures reproducibility but may result in slower execution times for large populations. - -## Example Screenshots - -On successful application start, you can start and run a search. You might see something like this when done: - -![gui_done_2024-02-12_08-44.png](screenshots%2Fgui_done_2024-02-12_08-44.png) - -An example of using the GUI: - -![gui_usage_clip_Peek 2024-02-12 09-03.gif](screenshots%2Fgui_usage_clip_Peek%202024-02-12%2009-03.gif) - -## Tests - - $ lein test - -Coverage looks like this if you run `lein cloverage`: - -![test_coverage_2024-02-16_16-39.png](screenshots%2Ftest_coverage_2024-02-16_16-39.png) - -## How It Works - -- We use Genetic Algorithms to allow candidate functions of best fit to compete. -- They compete on a computed score on the input data, and we use the sum of residuals to generate the score. -- Evolution consists of mutation and crossover. -- Mutations act on the function's AST and modify branches, leafs, or the whole tree. Operations like `+0.1`, `*x`, `/Sin(x)` are applied to functions. -- Crossovers combine two functions' ASTs at a random point, and combine using various operators like `+`, and `*`. - ## Roadmap - [x] CLI options accept a CSV file (GUI already supports this) From f62d2fb4fe18ce005f088e98a4798770ea032ac5 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 14:31:31 -0700 Subject: [PATCH 025/224] random-seed using PRNG improvements --- src/closyr/api/finder.clj | 34 ++-- src/closyr/symbolic_regression.clj | 84 +++++---- src/closyr/util/spec.clj | 2 +- test/closyr/symbolic_regression_test.clj | 209 +++++++++++++++-------- 4 files changed, 194 insertions(+), 135 deletions(-) diff --git a/src/closyr/api/finder.clj b/src/closyr/api/finder.clj index cf182ad5..2592aca9 100644 --- a/src/closyr/api/finder.clj +++ b/src/closyr/api/finder.clj @@ -39,27 +39,19 @@ population-size (.getPopulationSize cfg) max-leafs (.getMaxLeafs cfg) random-seed (.getRandomSeed cfg) - use-seed? (>= random-seed 0)] - - ;; Set the random seed if provided (>= 0 means seed is set) - (when use-seed? - (log/warn "Deterministic mode enabled with seed:" random-seed - "- CPU parallelism disabled for reproducibility") - (prng/set-random-seed! random-seed)) - - ;; Run with deterministic mode if seed is set (disables parallel execution) - (binding [ga/*deterministic-mode* use-seed?] - (let [run-config {:initial-phenos (ops-init/initial-phenotypes population-size) - :initial-muts (ops-init/initial-mutations) - :iters iterations - :use-gui? false - :use-flamechart false - :max-leafs max-leafs - :input-xs-exprs (ops-common/doubles->exprs xs-vec) - :input-ys-exprs (ops-common/doubles->exprs ys-vec)} - - result (symreg/run-find-formula run-config)] - (types/->formula-result result))))) + run-config {:initial-phenos (ops-init/initial-phenotypes population-size) + :initial-muts (ops-init/initial-mutations) + :iters iterations + :use-gui? false + :use-flamechart false + :max-leafs max-leafs + :random-seed random-seed + :input-xs-exprs (ops-common/doubles->exprs xs-vec) + :input-ys-exprs (ops-common/doubles->exprs ys-vec)} + + result (symreg/run-find-formula run-config)] + + (types/->formula-result result))) (defn validate-inputs diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index c6c81f95..b1fabc40 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -589,29 +589,22 @@ :as run-args}] (let [run-config (merge-cli-and-gui-args run-config run-args) {:keys [next-step] :as completed-ga-data} (run-solver-ga-iterations run-config run-args)] - (binding [ga/*deterministic-mode* (some? (:random-seed run-config))] - ;; Set the random seed if provided - (when (:random-seed run-config) - (log/warn "Deterministic mode enabled with seed:" (:random-seed run-config) - "- CPU parallelism disabled for reproducibility") - (prng/set-random-seed! (:random-seed run-config))) + (case next-step - (case next-step + :stop + completed-ga-data - :stop - completed-ga-data - - :wait - (if use-gui? - (do (log/info "-- Waiting for GUI input to start again --") - (if-let [new-gui-args (wait-and-get-gui-args sim-stop-start-chan)] - (run-from-inputs run-config (merge run-args new-gui-args)) - completed-ga-data)) - completed-ga-data) + :wait + (if use-gui? + (do (log/info "-- Waiting for GUI input to start again --") + (if-let [new-gui-args (wait-and-get-gui-args sim-stop-start-chan)] + (run-from-inputs run-config (merge run-args new-gui-args)) + completed-ga-data)) + completed-ga-data) - :restart - (do (log/info "-- Restarting... --") - (run-from-inputs run-config (merge run-args (->run-args @sim-input-args*)))))))) + :restart + (do (log/info "-- Restarting... --") + (run-from-inputs run-config (merge run-args (->run-args @sim-input-args*))))))) (defn- in-flames @@ -648,7 +641,7 @@ (defrecord SymbolicRegressionFindFormula - [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui? use-flamechart max-leafs] + [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui? use-flamechart max-leafs random-seed] ISymbolicRegressionFindFormula @@ -678,7 +671,14 @@ "Run a GA evolution solver to search for function of best fit for input data. The word experiment is used loosely here, it's more of a time-evolving best-fit method instance." [{:keys [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui?] :as run-config}] - (solve (map->SymbolicRegressionFindFormula run-config))) + (binding [ga/*deterministic-mode* (some? (:random-seed run-config))] + ;; Set the random seed if provided + (when (:random-seed run-config) + (log/warn "2 Deterministic mode enabled with seed:" (:random-seed run-config) + "- CPU parallelism disabled for reproducibility") + (prng/set-random-seed! (:random-seed run-config))) + + (solve (map->SymbolicRegressionFindFormula run-config)))) (defn run-app-without-gui @@ -725,29 +725,23 @@ [{:keys [iterations population headless xs ys use-flamechart max-leafs seed] :as cli-opts}] (log/info "CLI: run from options: " cli-opts) ;; Run with deterministic mode if seed is set (disables parallel execution) - (binding [ga/*deterministic-mode* (some? seed)] - ;; Set the random seed if provided - (when seed - (log/warn "Deterministic mode enabled with seed:" seed - "- CPU parallelism disabled for reproducibility") - (prng/set-random-seed! seed)) - (let [run-config {:initial-phenos (ops-init/initial-phenotypes population) - :initial-muts (ops-init/initial-mutations) - :iters iterations - :use-gui? (not headless) - :random-seed seed - :max-leafs max-leafs - :use-flamechart use-flamechart - :input-xs-exprs (if xs - (ops-common/doubles->exprs xs) - example-input-xs-exprs) - :input-ys-exprs (if ys - (ops-common/doubles->exprs ys) - example-input-ys-exprs)} - result (run-find-formula run-config)] - (log/info "CLI: Done!") - (exit cli-opts) - result))) + (let [run-config {:initial-phenos (ops-init/initial-phenotypes population) + :initial-muts (ops-init/initial-mutations) + :iters iterations + :use-gui? (not headless) + :random-seed seed + :max-leafs max-leafs + :use-flamechart use-flamechart + :input-xs-exprs (if xs + (ops-common/doubles->exprs xs) + example-input-xs-exprs) + :input-ys-exprs (if ys + (ops-common/doubles->exprs ys) + example-input-ys-exprs)} + result (run-find-formula run-config)] + (log/info "CLI: Done!") + (exit cli-opts) + result)) ;; todo: feel kind of hacky to have to call this in ?every? ns that has defn schema diff --git a/src/closyr/util/spec.clj b/src/closyr/util/spec.clj index 30d9b190..d23d993c 100644 --- a/src/closyr/util/spec.clj +++ b/src/closyr/util/spec.clj @@ -223,7 +223,7 @@ [:input-iters #'Iterations] [:initial-phenos [:maybe #'GAPopulationPhenotypes]] [:input-phenos-count [:maybe #'PopulationCount]] - [:random-seed {:optional true} [:maybe #'PopulationCount]] + [:random-seed {:optional true} [:maybe #'RandomSeed]] [:max-leafs [:maybe #'MaxLeafs]]]) diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index 0fb03a87..67f60edd 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -20,66 +20,68 @@ (deftest end-iters-if-solution-found-test (testing "not solved" (is (= - (#'symreg/next-iters 10 [-10.0]) - 9))) + 9 + (#'symreg/next-iters 10 [-10.0])))) (testing "an exact solution" (is (= - (#'symreg/next-iters 10 [0.0]) - 0)))) + 0 + (#'symreg/next-iters 10 [0.0]))))) (deftest can-run-from-cli-args (testing "args from CLI are passed along correctly to implementation" (let [args* (atom nil)] (binding [ops/*print-top-n* 1] - (is (= - (dissoc - (with-redefs-fn {#'symreg/run-solver-ga-iterations - (fn [run-config run-args] - (reset! args* - [(dissoc run-config :initial-muts :initial-phenos :input-xs-exprs :input-ys-exprs) - (dissoc run-args :extended-domain-args :initial-phenos :input-xs-list :input-ys-arr)]) - {:iters-done 123 - :final-population {:pop [] - :score-fn #() - :pop-scores [] - :mutation-fn #() - :crossover-fn #()} - :next-step :stop}) - #'symreg/config->log-steps (fn [_ _] 200)} - (fn [] - (symreg/run-app-from-cli-args - {:iterations 20 - :population 20 - :headless true - :xs [0 1 2] - :ys [1 4 19] - :use-flamechart true - :max-leafs 20}))) - :final-population) - {:iters-done 123 - :next-step :stop})) - - (is (= [{:iters 20 + (is (= {:iters-done 123 + :next-step :stop} + (dissoc + (with-redefs-fn {#'symreg/run-solver-ga-iterations + (fn [run-config run-args] + (reset! args* + [(dissoc run-config :initial-muts :initial-phenos :input-xs-exprs :input-ys-exprs) + (dissoc run-args :extended-domain-args :initial-phenos :input-xs-list :input-ys-arr)]) + {:iters-done 123 + :final-population {:pop [] + :score-fn #() + :pop-scores [] + :mutation-fn #() + :crossover-fn #()} + :next-step :stop}) + #'symreg/config->log-steps (fn [_ _] 200)} + (fn [] + (symreg/run-app-from-cli-args + {:iterations 20 + :population 20 + :headless true + :xs [0 1 2] + :ys [1 4 19] + :use-flamechart true + :max-leafs 20 + :seed 123}))) + :final-population))) + + (is (= @args* + [{:iters 20 :log-steps 200 :max-leafs 20 :use-gui? false - :random-seed nil + :random-seed 123 :use-flamechart true} {:input-iters 20 :input-phenos-count nil - :random-seed nil + :random-seed 123 :input-xs-count 3 :input-xs-vec [0.0 1.0 2.0] :input-ys-vec [1.0 4.0 19.0] - :max-leafs 20}] - @args*))))) + :max-leafs 20}]))))) (testing "args from CLI are passed along correctly to implementation, and when none are passed we use defaults" (let [args* (atom nil)] (binding [ops/*print-top-n* 1] (is (= + {:iters-done 123 + :next-step :stop} (dissoc (with-redefs-fn {#'symreg/run-solver-ga-iterations (fn [run-config run-args] @@ -99,11 +101,10 @@ {:population 30 :iterations 20 :headless true}))) - :final-population) - {:iters-done 123 - :next-step :stop})) + :final-population))) - (is (= [{:iters 20 + (is (= @args* + [{:iters 20 :log-steps 200 :max-leafs 40 :use-gui? false @@ -115,8 +116,7 @@ :input-xs-count 50 :input-xs-vec [0.0 0.20943951023931953 0.41887902047863906 0.6283185307179586 0.8377580409572781 1.0471975511965976 1.2566370614359172 1.4660765716752369 1.6755160819145563 1.8849555921538759 2.0943951023931953 2.3038346126325147 2.5132741228718345 2.7227136331111543 2.9321531433504737 3.141592653589793 3.3510321638291125 3.560471674068432 3.7699111843077517 3.979350694547071 4.1887902047863905 4.39822971502571 4.607669225265029 4.81710873550435 5.026548245743669 5.235987755982989 5.445427266222309 5.654866776461628 5.8643062867009474 6.073745796940266 6.283185307179586 6.492624817418906 6.702064327658225 6.911503837897546 7.120943348136864 7.3303828583761845 7.5398223686155035 7.749261878854823 7.958701389094142 8.168140899333462 8.377580409572781 8.587019919812102 8.79645943005142 9.00589894029074 9.215338450530059 9.42477796076938 9.6342174710087 9.843656981248019 10.053096491487338 10.262536001726657] :input-ys-vec [0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0] - :max-leafs nil}] - @args*)))))) + :max-leafs nil}])))))) (deftest can-run-experiment @@ -134,19 +134,19 @@ (let [res (with-redefs-fn {#'symreg/run-find-formula (fn [args] args)} (fn [] (#'symreg/run-app-with-gui)))] - (is (= (dissoc res :initial-phenos :initial-muts :input-xs-exprs :input-ys-exprs) - {:iters 100 + (is (= {:iters 100 :max-leafs 40 :use-flamechart false - :use-gui? true})) - (is (= (count (:initial-phenos res)) - 50)) - (is (= (count (:initial-muts res)) - 96)) - (is (= (count (:input-xs-exprs res)) - 50)) - (is (= (count (:input-ys-exprs res)) - 50)))) + :use-gui? true} + (dissoc res :initial-phenos :initial-muts :input-xs-exprs :input-ys-exprs))) + (is (= 50 + (count (:initial-phenos res)))) + (is (= 96 + (count (:initial-muts res)))) + (is (= 50 + (count (:input-xs-exprs res)))) + (is (= 50 + (count (:input-ys-exprs res)))))) (testing "with provided data" (reset! symreg/sim-input-args* {}) @@ -168,16 +168,48 @@ (/ i 10.0) (Math/cos (* Math/PI (/ i 15.0)))))) ops-common/doubles->exprs)})] - (is (= (count (:pop final-population)) - 100)) + (is (= 100 + (count (:pop final-population)))) - (is (= iters-done - 5)) + (is (= 5 + iters-done)) - (is (= (set (keys @symreg/sim-input-args*)) - #{:input-xs-exprs + (is (= #{:input-xs-exprs :input-xs-vec - :input-ys-vec})))))) + :input-ys-vec} + (set (keys @symreg/sim-input-args*)))))))) + + (testing "with provided data and random seed" + (reset! symreg/sim-input-args* {}) + (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 10)} + (fn [] + (let [{:keys [final-population next-step iters-done]} + (symreg/run-find-formula + {:input-phenos-count 100 + :initial-muts (ops-init/initial-mutations) + :iters 5 + :use-gui? false + :use-flamechart false + :random-seed 1234 + :input-xs-exprs (->> (range 50) + (map (fn [i] (* Math/PI (/ i 15.0)))) + ops-common/doubles->exprs) + :input-ys-exprs (->> (range 50) + (map (fn [i] + (+ 2.0 + (/ i 10.0) + (Math/cos (* Math/PI (/ i 15.0)))))) + ops-common/doubles->exprs)})] + (is (= 100 + (count (:pop final-population)))) + + (is (= 5 + iters-done)) + + (is (= #{:input-xs-exprs + :input-xs-vec + :input-ys-vec} + (set (keys @symreg/sim-input-args*)))))))) (testing "with provided data using record" (reset! symreg/sim-input-args* {}) @@ -199,16 +231,16 @@ (/ i 10.0) (Math/cos (* Math/PI (/ i 15.0)))))) ops-common/doubles->exprs)}))] - (is (= (count (:pop final-population)) - 100)) + (is (= 100 + (count (:pop final-population)))) - (is (= iters-done - 5)) + (is (= 5 + iters-done)) - (is (= (set (keys @symreg/sim-input-args*)) - #{:input-xs-exprs + (is (= #{:input-xs-exprs :input-xs-vec - :input-ys-vec})))))))) + :input-ys-vec} + (set (keys @symreg/sim-input-args*)))))))))) #_(deftest can-run-experiment-gui:start-stop @@ -240,6 +272,47 @@ (is (= (log-steps (fn [_ _] 10)} + (fn [] + (let [{:keys [final-population next-step iters-done] :as resp} + (symreg/run-find-formula + {:input-phenos-count 10 + :initial-muts (ops-init/initial-mutations) + :iters 5 + :use-gui? false + :use-flamechart false + :random-seed 123 + :input-xs-exprs (->> (range 50) + (map (fn [i] (* Math/PI (/ i 15.0)))) + ops-common/doubles->exprs) + :input-ys-exprs (->> (range 50) + (map (fn [i] + (+ 2.0 + (/ i 10.0) + (Math/cos (* Math/PI (/ i 15.0)))))) + ops-common/doubles->exprs)})] + + (is (= + '(-1040220.7700547063 + -197.35193706811842 + -157.63323650419892 + -21.783270815019268 + -12.360036918857467) + (sort (:pop-scores (:final-population resp))))) + + (is (= 10 + (count (:pop final-population)))) + + (is (= 5 + iters-done)) + + (is (= #{:input-xs-exprs + :input-xs-vec + :input-ys-vec} + (set (keys @symreg/sim-input-args*))))))))) (deftest can-run-experiment-gui:start-restart-stop (when (not (GraphicsEnvironment/isHeadless)) From b87ea43f032466ea15acc889db86b6ade2eec43f Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 15:06:30 -0700 Subject: [PATCH 026/224] random-seed using PRNG improvements --- src/closyr/symbolic_regression.clj | 28 +++++++++++++++--------- src/closyr/ui/gui.clj | 2 +- test/closyr/symbolic_regression_test.clj | 10 ++++----- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index b1fabc40..b88e7270 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -321,7 +321,7 @@ :input-ys-vec input-ys-vec :input-iters input-iters :input-phenos-count input-phenos-count - :random-seed random-seed + :random-seed random-seed :max-leafs max-leafs}))) @@ -399,7 +399,7 @@ (defn- start-gui-and-get-input-data "Initialize and show GUI, then park and wait on user input to start" - [{:keys [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs] :as run-config}] + [{:keys [iters initial-phenos initial-muts random-seed input-xs-exprs input-ys-exprs] :as run-config}] ;; these are the data shown in the plots before the experiment is started: (reset! sim-input-args* {:input-xs-vec (ops-common/exprs->doubles input-xs-exprs) @@ -429,11 +429,13 @@ (defn- print-and-save-start-time - [iters initial-phenos] + [iters initial-phenos run-config] (let [start (Date.)] (log/info "-- Start " start "iters: " iters " pop size: " (count initial-phenos) + " random seed: " (:random-seed run-config) + " deterministic mode: " ga/*deterministic-mode* " --") (reset! ops/test-timer* start))) @@ -476,7 +478,7 @@ (specs/validate! "SolverRunConfig" #'specs/SolverRunConfig run-config) (specs/validate! "SolverRunArgs" #'specs/SolverRunArgs run-args) (let [{:keys [iters initial-phenos initial-muts use-gui?]} run-config - start (print-and-save-start-time iters initial-phenos) + start (print-and-save-start-time iters initial-phenos run-config) init-pop (ga/initialize initial-phenos (partial ops/score-fn run-args run-config) @@ -551,11 +553,17 @@ "Run GA evolution iterations on initial population" {:malli/schema [:=> [:cat #'specs/SolverRunConfig #'specs/SolverRunArgs] #'specs/SolverRunResults]} [run-config run-args] - (loop [solver-state (init (map->IterativeGASolver {:run-config run-config :run-args run-args}))] - (let [[recur? next-solver-state] (run-iteration solver-state)] - (if recur? - (recur next-solver-state) - next-solver-state)))) + (binding [ga/*deterministic-mode* (some? (:random-seed run-config))] + ;; Set the random seed if provided + (when (:random-seed run-config) + (log/warn "15 Deterministic mode enabled with seed:" (:random-seed run-config) + "- CPU parallelism disabled for reproducibility") + (prng/set-random-seed! (:random-seed run-config))) + (loop [solver-state (init (map->IterativeGASolver {:run-config run-config :run-args run-args}))] + (let [[recur? next-solver-state] (run-iteration solver-state)] + (if recur? + (recur next-solver-state) + next-solver-state))))) (defn- merge-cli-and-gui-args @@ -670,7 +678,7 @@ (defn run-find-formula "Run a GA evolution solver to search for function of best fit for input data. The word experiment is used loosely here, it's more of a time-evolving best-fit method instance." - [{:keys [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui?] :as run-config}] + [{:keys [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui? random-seed] :as run-config}] (binding [ga/*deterministic-mode* (some? (:random-seed run-config))] ;; Set the random seed if provided (when (:random-seed run-config) diff --git a/src/closyr/ui/gui.clj b/src/closyr/ui/gui.clj index 2b8f00b0..0109dc80 100644 --- a/src/closyr/ui/gui.clj +++ b/src/closyr/ui/gui.clj @@ -111,7 +111,7 @@ (atom {:max-leafs 40 :input-iters 100 :input-phenos-count 2000 - :random-seed nil})) + :random-seed 123})) (def ^:private amount->number diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index 67f60edd..d9c97e29 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -296,11 +296,11 @@ ops-common/doubles->exprs)})] (is (= - '(-1040220.7700547063 - -197.35193706811842 - -157.63323650419892 - -21.783270815019268 - -12.360036918857467) + '(-2086075.804922219 + -1040014.654570305 + -243.2878670032772 + -190.503094153588 + -12.807703829165217) (sort (:pop-scores (:final-population resp))))) (is (= 10 From f150ce78ef43ad53f53791b8381cee683f80128d Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 15:19:19 -0700 Subject: [PATCH 027/224] GUI for random-seed --- src/closyr/ui/gui.clj | 79 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/src/closyr/ui/gui.clj b/src/closyr/ui/gui.clj index 0109dc80..114007f9 100644 --- a/src/closyr/ui/gui.clj +++ b/src/closyr/ui/gui.clj @@ -111,7 +111,7 @@ (atom {:max-leafs 40 :input-iters 100 :input-phenos-count 2000 - :random-seed 123})) + :random-seed nil})) (def ^:private amount->number @@ -720,6 +720,77 @@ xs-container)) +(defn- parse-seed-value + "Parse text as a Long seed value. Returns nil for empty/invalid input." + [^String text] + (when (and text (not (empty? (.trim text)))) + (try + (Long/parseLong (.trim text)) + (catch NumberFormatException _ + nil)))) + + +(defn- update-seed-warning-visibility! + "Update the visibility and text of the warning label based on seed value." + [^JLabel warning-label seed-value] + (if seed-value + (do + (.setText warning-label "Deterministic mode: slower single-threaded") + (.setVisible warning-label true)) + (do + (.setText warning-label "") + (.setVisible warning-label false)))) + + +(defn- ^JPanel random-seed-panel + "Create a panel for random seed input with performance warning and clear button." + [] + (let [^JPanel container (panel-grid {:rows 1 :cols 1 :border (radio-controls-border "Random Seed")}) + ^JPanel inner-panel (doto (JPanel.) + (.setLayout (FlowLayout. FlowLayout/LEFT 5 2))) + + ^JLabel seed-label (JLabel. "Seed:") + ^JTextField seed-field (doto (JTextField. 10) + (.setToolTipText "Enter a number for deterministic mode, or leave empty for parallel mode")) + + ^JButton clear-btn (doto (ss/button :text "Clear (Parallel)") + (.setToolTipText "Clear seed to return to parallel (non-deterministic) mode")) + + ^JLabel warning-label (doto (JLabel. "") + (.setForeground (Color. 255 180 0)) + (.setVisible false)) + + update-seed! (fn [seed-value] + (swap! experiment-settings* assoc :random-seed seed-value) + (update-seed-warning-visibility! warning-label seed-value) + (log/info "Random seed changed to:" seed-value + (if seed-value "(deterministic mode)" "(parallel mode)")))] + + ;; Listen for text changes in the seed field + (ss/listen seed-field + :document + (fn [^AbstractDocument$DefaultDocumentEvent e] + (let [doc (.getDocument e) + doc-txt (.getText doc 0 (.getLength doc)) + new-seed (parse-seed-value doc-txt)] + (update-seed! new-seed)))) + + ;; Clear button resets to parallel mode + (ss/listen clear-btn + :mouse-clicked + (fn [^MouseEvent _] + (.setText seed-field "") + (update-seed! nil))) + + ;; Build the panel + (.add inner-panel seed-label) + (.add inner-panel seed-field) + (.add inner-panel clear-btn) + (.add inner-panel warning-label) + (.add container inner-panel) + container)) + + (defn- update-replace-drawing-widget [draw-container] (reset! @@ -948,12 +1019,10 @@ (.add btns-row) (.add status-row)) - random-seed-panel (doto (panel-grid - {:rows 1 :cols 1 :border (radio-controls-border "Random Seed")}) - (.add (JLabel. "HI"))) + ^JPanel random-seed-panel-widget (random-seed-panel) settings-container (doto (panel-grid {:rows 3 :cols 1}) - ; (.add random-seed-panel) ;; TODO: implement numerical text input, show perf warning when deterministic mode, provide a way to go back to parallel mode (unset random seed) + (.add random-seed-panel-widget) (.add settings-panel) (.add input-fn-container)) From 99b68650861a18fe917176055d8a931f73cd0e34 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 15:22:08 -0700 Subject: [PATCH 028/224] wip --- src/closyr/ui/gui.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/closyr/ui/gui.clj b/src/closyr/ui/gui.clj index 114007f9..b9b2ebe3 100644 --- a/src/closyr/ui/gui.clj +++ b/src/closyr/ui/gui.clj @@ -753,7 +753,7 @@ ^JTextField seed-field (doto (JTextField. 10) (.setToolTipText "Enter a number for deterministic mode, or leave empty for parallel mode")) - ^JButton clear-btn (doto (ss/button :text "Clear (Parallel)") + ^JButton clear-btn (doto ^JButton (ss/button :text "Clear (Parallel)") (.setToolTipText "Clear seed to return to parallel (non-deterministic) mode")) ^JLabel warning-label (doto (JLabel. "") From b4b06cfa53db1a5a9f477475cc2b1b63e06f2dde Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 15:32:04 -0700 Subject: [PATCH 029/224] deterministic benchmark --- perf-profile/benchmark.clj | 117 +++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 37 deletions(-) diff --git a/perf-profile/benchmark.clj b/perf-profile/benchmark.clj index e801e475..7e41483c 100644 --- a/perf-profile/benchmark.clj +++ b/perf-profile/benchmark.clj @@ -3,40 +3,48 @@ [closyr.ops.eval :as ops-eval] [closyr.ops.initialize :as ops-init] [closyr.ops :as ops] - [closyr.ga :as ga])) - -(defn benchmark-evolution [pop-size n-iters max-leafs xs-max] - (let [phenos (ops-init/initial-phenotypes pop-size) - muts (ops-init/initial-mutations) - xs (vec (range 0.1 xs-max 0.2)) - ys (mapv #(+ (* % %) (Math/sin %)) xs) - input-xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) - run-args {:input-xs-list input-xs-list - :input-xs-count (count xs) - :input-xs-vec xs - :input-ys-vec ys - :input-ys-arr (double-array ys)} - run-config {:max-leafs max-leafs} - score-fn (partial ops/score-fn run-args run-config) - mut-fn (partial ops/mutation-fn run-config muts) - cross-fn (partial ops/crossover-fn run-config muts) - ga-config (ga/initialize phenos score-fn mut-fn cross-fn)] - - ;; Warmup - (dotimes [_ 3] (ga/evolve ga-config)) - - ;; Timed run - (let [start (System/nanoTime) - result (loop [config ga-config, i 0] - (if (< i n-iters) - (recur (ga/evolve config) (inc i)) - config)) - elapsed-ms (/ (- (System/nanoTime) start) 1e6)] - {:pop-size pop-size - :iters n-iters - :elapsed-ms elapsed-ms - :ms-per-iter (/ elapsed-ms n-iters) - :phenos-per-sec (/ (* pop-size n-iters 1000) elapsed-ms)}))) + [closyr.ga :as ga] + [closyr.util.prng :as prng])) + +(defn benchmark-evolution + "Benchmark evolution with optional deterministic mode. + When random-seed is provided, runs in deterministic (single-threaded) mode." + [pop-size n-iters max-leafs xs-max & {:keys [random-seed]}] + (binding [ga/*deterministic-mode* (some? random-seed)] + (when random-seed + (prng/set-random-seed! random-seed)) + (let [phenos (ops-init/initial-phenotypes pop-size) + muts (ops-init/initial-mutations) + xs (vec (range 0.1 xs-max 0.2)) + ys (mapv #(+ (* % %) (Math/sin %)) xs) + input-xs-list (ops-common/exprs->exprs-list (ops-common/doubles->exprs xs)) + run-args {:input-xs-list input-xs-list + :input-xs-count (count xs) + :input-xs-vec xs + :input-ys-vec ys + :input-ys-arr (double-array ys)} + run-config {:max-leafs max-leafs} + score-fn (partial ops/score-fn run-args run-config) + mut-fn (partial ops/mutation-fn run-config muts) + cross-fn (partial ops/crossover-fn run-config muts) + ga-config (ga/initialize phenos score-fn mut-fn cross-fn)] + + ;; Warmup + (dotimes [_ 3] (ga/evolve ga-config)) + + ;; Timed run + (let [start (System/nanoTime) + result (loop [config ga-config, i 0] + (if (< i n-iters) + (recur (ga/evolve config) (inc i)) + config)) + elapsed-ms (/ (- (System/nanoTime) start) 1e6)] + {:pop-size pop-size + :iters n-iters + :elapsed-ms elapsed-ms + :ms-per-iter (/ elapsed-ms n-iters) + :phenos-per-sec (/ (* pop-size n-iters 1000) elapsed-ms) + :deterministic (some? random-seed)})))) (println "\n=== Evolution Benchmark ===\n") @@ -54,8 +62,43 @@ (:ms-per-iter result) (:phenos-per-sec result))]))))) -(println "\n=== Done ===") -(println "\n=== Results ===") -(println @results*) +(println "\n=== Parallel Benchmark Done ===") +(println "\n=== Results (Parallel Mode) ===") +(println (apply str @results*)) + +;; Deterministic benchmark +(println "\n=== Deterministic Benchmark (single-threaded) ===\n") + +(def deterministic-results* (atom [])) + +(doseq [pop-size [10 100 500 1000]] + (swap! deterministic-results* concat ["\n"]) + (doseq [xs-max [5.0 20.0]] + (doseq [max-leafs [40 120]] + (let [result (benchmark-evolution pop-size 10 max-leafs xs-max :random-seed 12345)] + (swap! deterministic-results* concat [(format "Pop %4d, XsMax %4.1f, MaxLeafs %4d: %6.1f ms/iter, %8.0f phenos/sec (deterministic)\n" + pop-size + xs-max + max-leafs + (:ms-per-iter result) + (:phenos-per-sec result))]))))) + +(println "\n=== Results (Deterministic Mode) ===") +(println (apply str @deterministic-results*)) + +;; Comparison summary +(println "\n=== Parallel vs Deterministic Comparison ===\n") + +(doseq [pop-size [100 1000]] + (let [parallel-result (benchmark-evolution pop-size 10 40 5.0) + deterministic-result (benchmark-evolution pop-size 10 40 5.0 :random-seed 42) + speedup (/ (:phenos-per-sec parallel-result) (:phenos-per-sec deterministic-result))] + (println (format "Pop %4d: Parallel %8.0f phenos/sec, Deterministic %8.0f phenos/sec, Speedup: %.2fx" + pop-size + (:phenos-per-sec parallel-result) + (:phenos-per-sec deterministic-result) + speedup)))) + +(println "\n=== All Benchmarks Complete ===") (System/exit 0) From 5f3cbef60c4df9db70fd91c5a5ef0dfb6cf820b4 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 15:35:29 -0700 Subject: [PATCH 030/224] wip --- perf-profile/benchmark.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/perf-profile/benchmark.clj b/perf-profile/benchmark.clj index 7e41483c..bbb44ec7 100644 --- a/perf-profile/benchmark.clj +++ b/perf-profile/benchmark.clj @@ -50,7 +50,7 @@ (def results* (atom [])) -(doseq [pop-size [10 100 500 1000 20000]] +(doseq [pop-size [10 100 500 1000 10000]] (swap! results* concat ["\n"]) (doseq [xs-max [5.0 20.0]] (doseq [max-leafs [40 120]] @@ -89,7 +89,7 @@ ;; Comparison summary (println "\n=== Parallel vs Deterministic Comparison ===\n") -(doseq [pop-size [100 1000]] +(doseq [pop-size [100 1000 5000]] (let [parallel-result (benchmark-evolution pop-size 10 40 5.0) deterministic-result (benchmark-evolution pop-size 10 40 5.0 :random-seed 42) speedup (/ (:phenos-per-sec parallel-result) (:phenos-per-sec deterministic-result))] From e4f9ba6ddf2f22f5a9f40e00ff7761bf7eeb202a Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 16:05:19 -0700 Subject: [PATCH 031/224] wip --- test/org/closyr/core/FindFormulaTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/org/closyr/core/FindFormulaTest.java b/test/org/closyr/core/FindFormulaTest.java index 0c1c0c16..3f500c51 100644 --- a/test/org/closyr/core/FindFormulaTest.java +++ b/test/org/closyr/core/FindFormulaTest.java @@ -15,11 +15,11 @@ */ class FindFormulaTest { - @BeforeAll - static void initClojure() { - // Pre-initialize Clojure runtime to avoid timeout in first test - FindFormula.initializeClojure(); - } +// @BeforeAll +// static void initClojure() { +// // Pre-initialize Clojure runtime to avoid timeout in first test +// FindFormula.initializeClojure(); +// } @BeforeEach void setUp() { @@ -258,10 +258,10 @@ void testWithTrigonometricData() { @Test void testInitializeClojureIdempotent() { - // Should be safe to call multiple times - FindFormula.initializeClojure(); - FindFormula.initializeClojure(); - FindFormula.initializeClojure(); +// // Should be safe to call multiple times +// FindFormula.initializeClojure(); +// FindFormula.initializeClojure(); +// FindFormula.initializeClojure(); // Verify the API still works double[] xs = {1.0, 2.0}; From 58c8919dfc75d0332afe7129f2fb13d73aae5735 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Tue, 23 Dec 2025 16:25:45 -0700 Subject: [PATCH 032/224] run junit script --- project.clj | 3 ++- run-junit.sh | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100755 run-junit.sh diff --git a/project.clj b/project.clj index 2553e434..20055784 100644 --- a/project.clj +++ b/project.clj @@ -69,7 +69,8 @@ :jvm-opts ["-Dclojure.compiler.direct-linking=true"]} :test {:dependencies [[org.junit.jupiter/junit-jupiter-api "5.10.2"] [org.junit.jupiter/junit-jupiter-engine "5.10.2"] - [org.junit.platform/junit-platform-launcher "1.10.2"]]}} + [org.junit.platform/junit-platform-launcher "1.10.2"] + [org.junit.platform/junit-platform-console-standalone "1.10.2"]]}} :plugins [[lein-cloverage "1.2.4"] [lein-vanity "0.2.0"] diff --git a/run-junit.sh b/run-junit.sh new file mode 100755 index 00000000..4ed4e222 --- /dev/null +++ b/run-junit.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Run JUnit tests for Java API + +# Compile Java sources first +lein with-profile +test javac + +# Get classpath +CP=$(lein with-profile +test classpath 2>/dev/null) + +# Run JUnit +java -jar ~/.m2/repository/org/junit/platform/junit-platform-console-standalone/1.10.2/junit-platform-console-standalone-1.10.2.jar \ + execute \ + --class-path "target/classes:$CP" \ + --select-class org.closyr.core.FindFormulaTest \ + --details tree From 9cebdfac7b0b61fa5da4f93219156e4f07af676b Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Wed, 24 Dec 2025 09:54:35 -0700 Subject: [PATCH 033/224] simplify api --- src/closyr/symbolic_regression.clj | 103 ++++++++--------------- test/closyr/symbolic_regression_test.clj | 67 ++++++++------- test/closyr/test_utils.clj | 12 +++ test/closyr/util_spec_test.clj | 2 +- 4 files changed, 87 insertions(+), 97 deletions(-) create mode 100644 test/closyr/test_utils.clj diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index b88e7270..7e446ab8 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -242,7 +242,7 @@ (.repaint scores-chart-panel))) -(defn chart-update-loop +(defn- chart-update-loop "In the GUI thread, loops over data sent from the experiement to be rendered onto the GUI. Parks waiting on new data, and ends the loop when a command in the close chan is sent" [sim->gui-chan @@ -300,7 +300,7 @@ :sim-stop-start-chan sim-stop-start-chan})) -(defn update-plot-input-data +(defn- update-plot-input-data "Get new data from GUI and generate necessary solver inputs" {:malli/schema [:=> [:cat #'specs/SolverGUIMessage] #'specs/SolverGUIInputArgs]} [{new-state :new-state @@ -325,7 +325,7 @@ :max-leafs max-leafs}))) -(defn restart-with-new-inputs +(defn- restart-with-new-inputs "Get new inputs and restart solver" {:malli/schema [:=> [:cat #'specs/SolverGUIMessage] keyword?]} [msg] @@ -355,7 +355,7 @@ nil)))))))) -(defn ->run-args +(defn- ->run-args "Generate one-time computed args for solver" {:malli/schema [:=> [:cat #'specs/SolverInputArgs] #'specs/SolverRunArgs]} [{input-xs-exprs :input-xs-exprs @@ -635,71 +635,33 @@ run-config))) -(defprotocol ISymbolicRegressionFindFormula - - "A top-level interface to start the solver using CLI or GUI args" - - (solve - [this] - "Run the solver on either CLI of GUI args. When using GUI, we block on getting a signal from the - GUI which indicates the user wants to start (and later stop/restart) the solver. The GUI - would also provide all the parameters and inputs to the solver, like iterations count and - the objective data. When running from the CLI, we use the provided inputs or some example data - and defaults.")) - - -(defrecord SymbolicRegressionFindFormula - [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui? use-flamechart max-leafs random-seed] - - ISymbolicRegressionFindFormula - - (solve - [this] - (let [symbolic-regression-solver-fn (fn [] - (run-from-inputs - this - (if use-gui? - (start-gui-and-get-input-data this) - (get-input-data this))))] - (if use-gui? - (log/info "-- Running from GUI --") - (log/info "-- Running from CLI." - "iters: " iters - "pop: " (count initial-phenos) - "muts: " (count initial-muts) " --")) - - (if use-flamechart - ;; with flame graph analysis: - (in-flames symbolic-regression-solver-fn) - ;; plain experiment: - (symbolic-regression-solver-fn))))) - - (defn run-find-formula - "Run a GA evolution solver to search for function of best fit for input data. The - word experiment is used loosely here, it's more of a time-evolving best-fit method instance." - [{:keys [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui? random-seed] :as run-config}] - (binding [ga/*deterministic-mode* (some? (:random-seed run-config))] + "Run a GA evolution solver to search for function of best fit for input data. + This is the main programmatic entry point for the symbolic regression solver." + [{:keys [iters initial-phenos initial-muts input-xs-exprs input-ys-exprs use-gui? use-flamechart random-seed] :as run-config}] + (binding [ga/*deterministic-mode* (some? random-seed)] ;; Set the random seed if provided - (when (:random-seed run-config) - (log/warn "2 Deterministic mode enabled with seed:" (:random-seed run-config) + (when random-seed + (log/warn "Deterministic mode enabled with seed:" random-seed "- CPU parallelism disabled for reproducibility") - (prng/set-random-seed! (:random-seed run-config))) - - (solve (map->SymbolicRegressionFindFormula run-config)))) - - -(defn run-app-without-gui - "Run app without GUI and with fake placeholder input data" - [xs ys] - (run-find-formula - {:initial-phenos (ops-init/initial-phenotypes 100) - :initial-muts (ops-init/initial-mutations) - :iters 20 - :use-gui? false - :use-flamechart false - :input-xs-exprs (ops-common/doubles->exprs xs) - :input-ys-exprs (ops-common/doubles->exprs ys)})) + (prng/set-random-seed! random-seed)) + + (if use-gui? + (log/info "-- Running from GUI --") + (log/info "-- Running from CLI." + "iters:" iters + "pop:" (count initial-phenos) + "muts:" (count initial-muts) "--")) + + (let [solver-fn (fn [] + (run-from-inputs + run-config + (if use-gui? + (start-gui-and-get-input-data run-config) + (get-input-data run-config))))] + (if use-flamechart + (in-flames solver-fn) + (solver-fn))))) (defn- run-app-with-gui @@ -759,6 +721,13 @@ (comment (println "FN SCHEMAS: " (m/function-schemas))) (comment (macroexpand-1 `(log/info "Hello"))) (comment (log/info "Hello")) -(comment (run-app-without-gui [1 2 3] [6 12 99])) +(comment (run-find-formula + {:initial-phenos (ops-init/initial-phenotypes 100) + :initial-muts (ops-init/initial-mutations) + :iters 20 + :use-gui? false + :use-flamechart false + :input-xs-exprs (ops-common/doubles->exprs [1 2 3]) + :input-ys-exprs (ops-common/doubles->exprs [6 12 99])})) (comment (run-app-with-gui {:use-flamechart true})) (comment (run-app-with-gui)) diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index d9c97e29..845959a0 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -127,7 +127,14 @@ (:final-population (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 10)} (fn [] - (symreg/run-app-without-gui [1 2 3] [6 12 99])))))) + (symreg/run-find-formula + {:initial-phenos (ops-init/initial-phenotypes 100) + :initial-muts (ops-init/initial-mutations) + :iters 20 + :use-gui? false + :use-flamechart false + :input-xs-exprs (ops-common/doubles->exprs [1 2 3]) + :input-ys-exprs (ops-common/doubles->exprs [6 12 99])})))))) 100))) (testing "with gui launcher" @@ -211,26 +218,26 @@ :input-ys-vec} (set (keys @symreg/sim-input-args*)))))))) - (testing "with provided data using record" + (testing "with provided data as map" (reset! symreg/sim-input-args* {}) (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 10)} (fn [] (let [{:keys [final-population next-step iters-done]} - (symreg/solve - (symreg/map->SymbolicRegressionFindFormula - {:input-phenos-count 100 - :initial-muts (ops-init/initial-mutations) - :iters 5 - :use-gui? false - :input-xs-exprs (->> (range 50) - (map (fn [i] (* Math/PI (/ i 15.0)))) - ops-common/doubles->exprs) - :input-ys-exprs (->> (range 50) - (map (fn [i] - (+ 2.0 - (/ i 10.0) - (Math/cos (* Math/PI (/ i 15.0)))))) - ops-common/doubles->exprs)}))] + (symreg/run-find-formula + {:input-phenos-count 100 + :initial-muts (ops-init/initial-mutations) + :iters 5 + :use-gui? false + :use-flamechart false + :input-xs-exprs (->> (range 50) + (map (fn [i] (* Math/PI (/ i 15.0)))) + ops-common/doubles->exprs) + :input-ys-exprs (->> (range 50) + (map (fn [i] + (+ 2.0 + (/ i 10.0) + (Math/cos (* Math/PI (/ i 15.0)))))) + ops-common/doubles->exprs)})] (is (= 100 (count (:pop final-population)))) @@ -262,12 +269,13 @@ true)] (symreg/run-find-formula - {:initial-phenos (ops-init/initial-phenotypes 20) - :initial-muts (ops-init/initial-mutations) - :input-xs-exprs symreg/example-input-xs-exprs - :input-ys-exprs symreg/example-input-ys-exprs - :iters 20 - :use-gui? true}) + {:initial-phenos (ops-init/initial-phenotypes 20) + :initial-muts (ops-init/initial-mutations) + :input-xs-exprs symreg/example-input-xs-exprs + :input-ys-exprs symreg/example-input-ys-exprs + :iters 20 + :use-gui? true + :use-flamechart false}) (is (= ( Date: Wed, 24 Dec 2025 09:55:11 -0700 Subject: [PATCH 034/224] log less during tests --- src/closyr/symbolic_regression.clj | 4 ++-- test/closyr/api_test.clj | 6 +++++- test/closyr/core_test.clj | 6 +++++- test/closyr/find_formula_java_test.clj | 6 +++++- test/closyr/symbolic_regression_test.clj | 3 +++ 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index 7e446ab8..f99995b9 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -556,7 +556,7 @@ (binding [ga/*deterministic-mode* (some? (:random-seed run-config))] ;; Set the random seed if provided (when (:random-seed run-config) - (log/warn "15 Deterministic mode enabled with seed:" (:random-seed run-config) + (log/info "run-solver-ga-iterations: Deterministic mode enabled with seed:" (:random-seed run-config) "- CPU parallelism disabled for reproducibility") (prng/set-random-seed! (:random-seed run-config))) (loop [solver-state (init (map->IterativeGASolver {:run-config run-config :run-args run-args}))] @@ -642,7 +642,7 @@ (binding [ga/*deterministic-mode* (some? random-seed)] ;; Set the random seed if provided (when random-seed - (log/warn "Deterministic mode enabled with seed:" random-seed + (log/info "run-find-formula: Deterministic mode enabled with seed:" random-seed "- CPU parallelism disabled for reproducibility") (prng/set-random-seed! random-seed)) diff --git a/test/closyr/api_test.clj b/test/closyr/api_test.clj index 4145a118..392252ba 100644 --- a/test/closyr/api_test.clj +++ b/test/closyr/api_test.clj @@ -3,7 +3,8 @@ (:require [clojure.test :refer :all] [closyr.api.types :as types] - [closyr.api.finder :as finder]) + [closyr.api.finder :as finder] + [closyr.test-utils :as test-utils]) (:import (org.closyr.api FormulaConfigBuilder @@ -14,6 +15,9 @@ IFormulaSolution))) +(use-fixtures :once test-utils/quiet-logging-fixture) + + ;; ============================================================================ ;; FormulaConfigBuilder tests ;; ============================================================================ diff --git a/test/closyr/core_test.clj b/test/closyr/core_test.clj index 8eab1d42..670c90e1 100644 --- a/test/closyr/core_test.clj +++ b/test/closyr/core_test.clj @@ -2,7 +2,11 @@ (:require [clojure.test :refer :all] [closyr.core :as core] - [closyr.symbolic-regression :as symreg])) + [closyr.symbolic-regression :as symreg] + [closyr.test-utils :as test-utils])) + + +(use-fixtures :once test-utils/quiet-logging-fixture) (deftest cli-options-test diff --git a/test/closyr/find_formula_java_test.clj b/test/closyr/find_formula_java_test.clj index fe40e562..7e3fc88b 100644 --- a/test/closyr/find_formula_java_test.clj +++ b/test/closyr/find_formula_java_test.clj @@ -1,7 +1,8 @@ (ns closyr.find-formula-java-test "Tests for the Java FindFormula API that calls into Clojure symbolic regression." (:require - [clojure.test :refer :all]) + [clojure.test :refer :all] + [closyr.test-utils :as test-utils]) (:import (org.closyr.core FindFormula @@ -10,6 +11,9 @@ FindFormula$Solution))) +(use-fixtures :once test-utils/quiet-logging-fixture) + + (deftest test-find-formula-with-linear-data (testing "Simple linear relationship: y = 2x" (let [xs (double-array [1.0 2.0 3.0 4.0 5.0]) diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index 845959a0..86b3a2d0 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -7,6 +7,7 @@ [closyr.ops.common :as ops-common] [closyr.ops.initialize :as ops-init] [closyr.symbolic-regression :as symreg] + [closyr.test-utils :as test-utils] [closyr.util.spec :as specs] [malli.core :as m]) (:import @@ -14,6 +15,8 @@ GraphicsEnvironment))) +(use-fixtures :once test-utils/quiet-logging-fixture) + (alter-var-root #'symreg/*is-testing* (constantly true)) From 26d42992471b52fdb19c51bbad8c824dff03700b Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Wed, 24 Dec 2025 10:03:28 -0700 Subject: [PATCH 035/224] fix junit test runner --- run-junit.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/run-junit.sh b/run-junit.sh index 4ed4e222..7a76c760 100755 --- a/run-junit.sh +++ b/run-junit.sh @@ -1,15 +1,25 @@ #!/bin/bash # Run JUnit tests for Java API -# Compile Java sources first +set -e + +# Compile main Java sources lein with-profile +test javac # Get classpath CP=$(lein with-profile +test classpath 2>/dev/null) +# Create test-classes directory +mkdir -p target/test-classes + +# Compile Java test sources manually +javac -d target/test-classes \ + -cp "target/classes:$CP" \ + test/org/closyr/core/FindFormulaTest.java + # Run JUnit java -jar ~/.m2/repository/org/junit/platform/junit-platform-console-standalone/1.10.2/junit-platform-console-standalone-1.10.2.jar \ execute \ - --class-path "target/classes:$CP" \ + --class-path "target/test-classes:target/classes:$CP" \ --select-class org.closyr.core.FindFormulaTest \ --details tree From 0e3ee981fb1305027a1ae1a65926e6e14dd32fc1 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Wed, 24 Dec 2025 10:19:25 -0700 Subject: [PATCH 036/224] style --- src/closyr/util/prng.clj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/closyr/util/prng.clj b/src/closyr/util/prng.clj index 9c52dd50..07548e4c 100644 --- a/src/closyr/util/prng.clj +++ b/src/closyr/util/prng.clj @@ -7,7 +7,8 @@ ArrayList Collection Collections - Random))) + Random + UUID))) (set! *warn-on-reflection* true) @@ -75,7 +76,7 @@ (bit-or (bit-shift-left acc 8) (bit-and (aget bytes i) 0xff))) 0 (range 8 16))] - (java.util.UUID. msb lsb)))) + (UUID. msb lsb)))) (comment From 0008f975288fb22ae2c46b9fc2cb67109a31e6fc Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Wed, 24 Dec 2025 10:25:36 -0700 Subject: [PATCH 037/224] style --- test/closyr/symbolic_regression_test.clj | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/closyr/symbolic_regression_test.clj b/test/closyr/symbolic_regression_test.clj index 86b3a2d0..c75184cf 100644 --- a/test/closyr/symbolic_regression_test.clj +++ b/test/closyr/symbolic_regression_test.clj @@ -76,7 +76,8 @@ :input-xs-count 3 :input-xs-vec [0.0 1.0 2.0] :input-ys-vec [1.0 4.0 19.0] - :max-leafs 20}]))))) + :max-leafs 20}]) + "Arguments should be consistent with inputs")))) (testing "args from CLI are passed along correctly to implementation, and when none are passed we use defaults" @@ -119,7 +120,8 @@ :input-xs-count 50 :input-xs-vec [0.0 0.20943951023931953 0.41887902047863906 0.6283185307179586 0.8377580409572781 1.0471975511965976 1.2566370614359172 1.4660765716752369 1.6755160819145563 1.8849555921538759 2.0943951023931953 2.3038346126325147 2.5132741228718345 2.7227136331111543 2.9321531433504737 3.141592653589793 3.3510321638291125 3.560471674068432 3.7699111843077517 3.979350694547071 4.1887902047863905 4.39822971502571 4.607669225265029 4.81710873550435 5.026548245743669 5.235987755982989 5.445427266222309 5.654866776461628 5.8643062867009474 6.073745796940266 6.283185307179586 6.492624817418906 6.702064327658225 6.911503837897546 7.120943348136864 7.3303828583761845 7.5398223686155035 7.749261878854823 7.958701389094142 8.168140899333462 8.377580409572781 8.587019919812102 8.79645943005142 9.00589894029074 9.215338450530059 9.42477796076938 9.6342174710087 9.843656981248019 10.053096491487338 10.262536001726657] :input-ys-vec [0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0] - :max-leafs nil}])))))) + :max-leafs nil}]) + "Arguments should be consistent with inputs"))))) (deftest can-run-experiment @@ -283,6 +285,7 @@ (is (= ( Date: Sat, 27 Dec 2025 08:52:48 -0700 Subject: [PATCH 038/224] bump junit deps --- project.clj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project.clj b/project.clj index 20055784..5495940a 100644 --- a/project.clj +++ b/project.clj @@ -67,10 +67,10 @@ :profiles {:uberjar {:aot :all :manifest {"Multi-Release" true} :jvm-opts ["-Dclojure.compiler.direct-linking=true"]} - :test {:dependencies [[org.junit.jupiter/junit-jupiter-api "5.10.2"] - [org.junit.jupiter/junit-jupiter-engine "5.10.2"] - [org.junit.platform/junit-platform-launcher "1.10.2"] - [org.junit.platform/junit-platform-console-standalone "1.10.2"]]}} + :test {:dependencies [[org.junit.jupiter/junit-jupiter-api "6.0.1"] + [org.junit.jupiter/junit-jupiter-engine "6.0.1"] + [org.junit.platform/junit-platform-launcher "6.0.1"] + [org.junit.platform/junit-platform-console-standalone "6.0.1"]]}} :plugins [[lein-cloverage "1.2.4"] [lein-vanity "0.2.0"] From ab64603cc6b761a2c1a43e40ad9a33754ac23a0c Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sat, 27 Dec 2025 18:24:14 -0700 Subject: [PATCH 039/224] random seed used in Java API --- .../java/org/closyr/core/FindFormula.java | 9 ++++++++ test/org/closyr/core/FindFormulaTest.java | 23 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/closyr/core/FindFormula.java b/src/main/java/org/closyr/core/FindFormula.java index d22a4918..561e415c 100644 --- a/src/main/java/org/closyr/core/FindFormula.java +++ b/src/main/java/org/closyr/core/FindFormula.java @@ -136,6 +136,7 @@ public static class Config { private int iterations = 20; private int populationSize = 100; private int maxLeafs = 40; + private long randomSeed = -1L; public Config() {} @@ -157,9 +158,16 @@ public Config maxLeafs(int maxLeafs) { return this; } + /** Random seed for reproducible results (default: -1, meaning non-deterministic) */ + public Config randomSeed(long randomSeed) { + this.randomSeed = randomSeed; + return this; + } + public int getIterations() { return iterations; } public int getPopulationSize() { return populationSize; } public int getMaxLeafs() { return maxLeafs; } + public long getRandomSeed() { return randomSeed; } /** Convert to IFormulaConfig for the new API */ IFormulaConfig toFormulaConfig() { @@ -167,6 +175,7 @@ IFormulaConfig toFormulaConfig() { .iterations(iterations) .populationSize(populationSize) .maxLeafs(maxLeafs) + .randomSeed(randomSeed) .build(); } } diff --git a/test/org/closyr/core/FindFormulaTest.java b/test/org/closyr/core/FindFormulaTest.java index 3f500c51..d708216e 100644 --- a/test/org/closyr/core/FindFormulaTest.java +++ b/test/org/closyr/core/FindFormulaTest.java @@ -184,11 +184,32 @@ void testConfigBuilder() { FindFormula.Config config = new FindFormula.Config() .iterations(50) .populationSize(200) - .maxLeafs(30); + .maxLeafs(30) + .randomSeed(12345); assertEquals(50, config.getIterations()); assertEquals(200, config.getPopulationSize()); assertEquals(30, config.getMaxLeafs()); + assertEquals(12345, config.getRandomSeed()); + } + + @Test + void testRandomSeedProducesDeterministicResults() { + double[] xs = {1.0, 2.0, 3.0, 4.0, 5.0}; + double[] ys = {2.0, 4.0, 6.0, 8.0, 10.0}; + + FindFormula.Config config = new FindFormula.Config() + .iterations(5) + .populationSize(20) + .randomSeed(42); + + FindFormula.Result result1 = FindFormula.findFormula(xs, ys, config); + FindFormula.Result result2 = FindFormula.findFormula(xs, ys, config); + + assertEquals(result1.getFormulaString(), result2.getFormulaString(), + "Same seed should produce identical formulas"); + assertEquals(result1.getScore(), result2.getScore(), + "Same seed should produce identical scores"); } @Test From 80221ab95ff9a5b17ddc91422e9337f18ac549ee Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 11:33:58 -0700 Subject: [PATCH 040/224] better reporting --- src/closyr/ops.clj | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/closyr/ops.clj b/src/closyr/ops.clj index 2f07fcb7..a45b26d2 100644 --- a/src/closyr/ops.clj +++ b/src/closyr/ops.clj @@ -309,14 +309,14 @@ (defn report-iteration "Print and maybe send to GUI a summary report of the population, including best fn/score/etc" - [i + [iters-to-go iters ga-result {:keys [input-xs-list input-xs-count input-ys-vec sim-stop-start-chan sim->gui-chan extended-domain-args] :as run-args} {:keys [use-gui? max-leafs] :as run-config}] - (when (or (= 1 i) (zero? (mod i *log-steps*))) + (when (or (= 1 iters-to-go) (zero? (mod iters-to-go *log-steps*))) (let [bests (sort-population ga-result) took-s (/ (ops-common/start-date->diff-ms @test-timer*) 1000.0) pop-size (count (:pop ga-result)) @@ -329,7 +329,9 @@ best-v run-args extended-domain-args)] (reset! test-timer* (Date.)) - (log/info i "-step pop size: " pop-size + (log/info (inc (- iters iters-to-go)) "-th-iter, " + " iters left: " (dec iters-to-go) + " pop size: " pop-size " points: " (count input-ys-vec) " max leafs: " max-leafs " took secs: " took-s @@ -343,7 +345,7 @@ (when use-gui? (put! sim->gui-chan {:iters iters - :i (inc (- iters i)) + :i (inc (- iters iters-to-go)) :best-eval evaled :input-xs-vec-extended xs-extended :best-eval-extended evaled-extended From a9776041669dc8f19a9dc1fedfb54f8e61f23b78 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 13:53:42 -0700 Subject: [PATCH 041/224] couple extra datasets --- src/closyr/dataset/inputs.clj | 63 ++++++++++++++++++++++++++++++----- src/closyr/ui/gui.clj | 15 ++++----- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/closyr/dataset/inputs.clj b/src/closyr/dataset/inputs.clj index d69ce9b5..1ad5b66b 100644 --- a/src/closyr/dataset/inputs.clj +++ b/src/closyr/dataset/inputs.clj @@ -29,34 +29,34 @@ :fn (fn [i] (y->gui-coord-y sketchpad-size* - (+ (* 50 (Math/sin (/ i 4.0))) - (* 30 (Math/cos (/ i 3.0))))))} + (+ (* 50 (Math/sin (* 13.0 (/ i @sketch-input-x-count*)))) + (* 30 (Math/cos (* 19.0 (/ i @sketch-input-x-count*)))))))} "sin+cos 2" {:idx 5 :fn (fn [i] (y->gui-coord-y sketchpad-size* - (+ (* 10 (Math/sin (/ i 1.125))) - (* 50 (Math/cos (/ i 5.0))))))} + (+ (* 10 (Math/sin (* 17.0 (/ i @sketch-input-x-count*)))) + (* 50 (Math/cos (* 5.0 (/ i @sketch-input-x-count*)))))))} "cos" {:idx 10 :fn (fn [i] (y->gui-coord-y sketchpad-size* - (* 80 (Math/cos (/ i 3.0)))))} + (* 80 (Math/cos (* 12.0 (/ i @sketch-input-x-count*))))))} "sin" {:idx 20 :fn (fn [i] (y->gui-coord-y sketchpad-size* - (* 80 (Math/sin (/ i 3.0)))))} + (* 80 (Math/sin (* 12.0 (/ i @sketch-input-x-count*))))))} "log" {:idx 30 :fn (fn [i] (y->gui-coord-y sketchpad-size* - (* 50 (Math/log (+ 0.01 (/ i 4.0))))))} + (* 10 (Math/log (+ 0.01 (/ i @sketch-input-x-count*))))))} "hline" {:idx 40 :fn (fn [i] (y->gui-coord-y sketchpad-size* 0.0))} @@ -86,4 +86,51 @@ (y->gui-coord-y sketchpad-size* (* 60 - (rand))))}}) + (rand))))} + + ;; x⁶ + x⁵ + x⁴ + x³ + x² + x , x in [-1, 1] + "Nguyen-4" + {:idx 90 + :fn (fn [i] + (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] + (y->gui-coord-y + sketchpad-size* + (* 15 (+ (Math/pow x 6) + (Math/pow x 5) + (Math/pow x 4) + (Math/pow x 3) + (Math/pow x 2) + x)))))} + + ;; sin(x²)·cos(x) - 1 , x in [-1,1] + "Nguyen-5" + {:idx 100 + :fn (fn [i] + (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] + (y->gui-coord-y + sketchpad-size* + (* 40 (- (* (Math/sin (* x x)) + (Math/cos x)) + 1)))))} + + ;; Lorentz factor: γ = 1/√(1 - v²/c²), v/c in [0, 0.95] + "Feynman Lorentz" + {:idx 110 + :fn (fn [i] + (let [v-over-c (* 0.95 (/ i (double @sketch-input-x-count*)))] ; map to [0, 0.95] + (y->gui-coord-y + sketchpad-size* + (* 25 (/ 1.0 + (Math/sqrt (- 1.0 (* v-over-c v-over-c))))))))} + + ;; Wave equation: y = A·sin(kx - ωt), with t=0 simplifies to A·sin(kx) + "Feynman Wave" + {:idx 120 + :fn (fn [i] + (let [x (* 12.0 (/ i (double @sketch-input-x-count*))) ; spatial coordinate + k 1.0 ; wave number + omega 0.5 ; angular frequency + t 2.0] ; fixed time + (y->gui-coord-y + sketchpad-size* + (* 60 (Math/sin (- (* k x) (* omega t)))))))}}) diff --git a/src/closyr/ui/gui.clj b/src/closyr/ui/gui.clj index b9b2ebe3..5c8d65e2 100644 --- a/src/closyr/ui/gui.clj +++ b/src/closyr/ui/gui.clj @@ -833,14 +833,13 @@ input-data-maps)] (reset! xs* (mapv :x scaled-inputs)) (doseq [[i {:keys [x y]}] (map-indexed (fn [i d] [i d]) scaled-inputs)] - (let [] - (log/info "Set ixy: " i x y - " scalars: " x-scalar y-scalar - " diff: " diff-x diff-y - " canvas: " canvas-w canvas-h) - ((nth items-point-setters i) - x - (input-data/y->gui-coord-y sketchpad-size* y))))))) + (log/info "Set ixy: " i x y + " scalars: " x-scalar y-scalar + " diff: " diff-x diff-y + " canvas: " canvas-w canvas-h) + ((nth items-point-setters i) + x + (input-data/y->gui-coord-y sketchpad-size* y)))))) (defn- ^JPanel input-file-picker-widget From 50c54ee748accae4685cdde5037dcf5892f6273d Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 14:16:42 -0700 Subject: [PATCH 042/224] show obj fn in GUI --- src/closyr/dataset/inputs.clj | 194 ++++++++++++++++------------- src/closyr/symbolic_regression.clj | 2 +- src/closyr/ui/gui.clj | 18 ++- 3 files changed, 122 insertions(+), 92 deletions(-) diff --git a/src/closyr/dataset/inputs.clj b/src/closyr/dataset/inputs.clj index 1ad5b66b..e1a4095d 100644 --- a/src/closyr/dataset/inputs.clj +++ b/src/closyr/dataset/inputs.clj @@ -25,112 +25,126 @@ "Functions to display in GUI which can be used as input data" [sketchpad-size* sketch-input-x-count*] {initial-fn - {:idx 0 - :fn (fn [i] - (y->gui-coord-y - sketchpad-size* - (+ (* 50 (Math/sin (* 13.0 (/ i @sketch-input-x-count*)))) - (* 30 (Math/cos (* 19.0 (/ i @sketch-input-x-count*)))))))} + {:idx 0 + :formula "objective_y(x) = 50·sin(13x) + 30·cos(19x)" + :fn (fn [i] + (y->gui-coord-y + sketchpad-size* + (+ (* 50 (Math/sin (* 13.0 (/ i @sketch-input-x-count*)))) + (* 30 (Math/cos (* 19.0 (/ i @sketch-input-x-count*)))))))} "sin+cos 2" - {:idx 5 - :fn (fn [i] - (y->gui-coord-y - sketchpad-size* - (+ (* 10 (Math/sin (* 17.0 (/ i @sketch-input-x-count*)))) - (* 50 (Math/cos (* 5.0 (/ i @sketch-input-x-count*)))))))} + {:idx 5 + :formula "objective_y(x) = 10·sin(17x) + 50·cos(5x)" + :fn (fn [i] + (y->gui-coord-y + sketchpad-size* + (+ (* 10 (Math/sin (* 17.0 (/ i @sketch-input-x-count*)))) + (* 50 (Math/cos (* 5.0 (/ i @sketch-input-x-count*)))))))} "cos" - {:idx 10 - :fn (fn [i] - (y->gui-coord-y - sketchpad-size* - (* 80 (Math/cos (* 12.0 (/ i @sketch-input-x-count*))))))} + {:idx 10 + :formula "objective_y(x) = 80·cos(12x)" + :fn (fn [i] + (y->gui-coord-y + sketchpad-size* + (* 80 (Math/cos (* 12.0 (/ i @sketch-input-x-count*))))))} "sin" - {:idx 20 - :fn (fn [i] - (y->gui-coord-y - sketchpad-size* - (* 80 (Math/sin (* 12.0 (/ i @sketch-input-x-count*))))))} + {:idx 20 + :formula "objective_y(x) = 80·sin(12x)" + :fn (fn [i] + (y->gui-coord-y + sketchpad-size* + (* 80 (Math/sin (* 12.0 (/ i @sketch-input-x-count*))))))} "log" - {:idx 30 - :fn (fn [i] - (y->gui-coord-y - sketchpad-size* - (* 10 (Math/log (+ 0.01 (/ i @sketch-input-x-count*))))))} + {:idx 30 + :formula "objective_y(x) = 10·ln(0.01 + x)" + :fn (fn [i] + (y->gui-coord-y + sketchpad-size* + (* 10 (Math/log (+ 0.01 (/ i @sketch-input-x-count*))))))} "hline" - {:idx 40 - :fn (fn [i] (y->gui-coord-y sketchpad-size* 0.0))} + {:idx 40 + :formula "objective_y(x) = 0" + :fn (fn [i] (y->gui-coord-y sketchpad-size* 0.0))} "prime count" - {:idx 50 - :fn (fn [i] - (let [xys (data-prime-counting/get-data @sketch-input-x-count*)] - (y->gui-coord-y sketchpad-size* (second (nth xys i)))))} + {:idx 50 + :formula "objective_y(x) = π(x) (prime counting function)" + :fn (fn [i] + (let [xys (data-prime-counting/get-data @sketch-input-x-count*)] + (y->gui-coord-y sketchpad-size* (second (nth xys i)))))} "primes" - {:idx 60 - :fn (fn [i] - (let [xys (data-primes/get-data @sketch-input-x-count*)] - (y->gui-coord-y sketchpad-size* (second (nth xys i)))))} + {:idx 60 + :formula "objective_y(x) = pₙ (nth prime number)" + :fn (fn [i] + (let [xys (data-primes/get-data @sketch-input-x-count*)] + (y->gui-coord-y sketchpad-size* (second (nth xys i)))))} "gaussian" - {:idx 70 - :fn (fn [i] - (y->gui-coord-y - sketchpad-size* - (* 40 - (Math/sqrt (* 2.0 Math/PI)) - (Math/exp (- (* (/ (/ (- i (/ @sketch-input-x-count* 2)) 5.0) 2.0) - (/ (- i (/ @sketch-input-x-count* 2)) 5.0)))))))} + {:idx 70 + :formula "objective_y(x) = √(2π)·exp(-((x-μ)/σ)²/2)" + :fn (fn [i] + (y->gui-coord-y + sketchpad-size* + (* 40 + (Math/sqrt (* 2.0 Math/PI)) + (Math/exp (- (* (/ (/ (- i (/ @sketch-input-x-count* 2)) 5.0) 2.0) + (/ (- i (/ @sketch-input-x-count* 2)) 5.0)))))))} "random" - {:idx 80 - :fn (fn [i] - (y->gui-coord-y - sketchpad-size* - (* 60 - (rand))))} + {:idx 80 + :formula "objective_y(x) = random()" + :fn (fn [i] + (y->gui-coord-y + sketchpad-size* + (* 60 + (rand))))} - ;; x⁶ + x⁵ + x⁴ + x³ + x² + x , x in [-1, 1] + ;; Nguyen-4: x⁶ + x⁵ + x⁴ + x³ + x² + x , x ∈ [-1, 1] "Nguyen-4" - {:idx 90 - :fn (fn [i] - (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] - (y->gui-coord-y - sketchpad-size* - (* 15 (+ (Math/pow x 6) - (Math/pow x 5) - (Math/pow x 4) - (Math/pow x 3) - (Math/pow x 2) - x)))))} + {:idx 90 + :formula "objective_y(x) = x⁶ + x⁵ + x⁴ + x³ + x² + x" + :fn (fn [i] + (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] + (y->gui-coord-y + sketchpad-size* + (* 15 (+ (Math/pow x 6) + (Math/pow x 5) + (Math/pow x 4) + (Math/pow x 3) + (Math/pow x 2) + x)))))} - ;; sin(x²)·cos(x) - 1 , x in [-1,1] + ;; Nguyen-5: sin(x²)·cos(x) - 1 , x ∈ [-1, 1] "Nguyen-5" - {:idx 100 - :fn (fn [i] - (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] - (y->gui-coord-y - sketchpad-size* - (* 40 (- (* (Math/sin (* x x)) - (Math/cos x)) - 1)))))} + {:idx 100 + :formula "objective_y(x) = sin(x²)·cos(x) - 1" + :fn (fn [i] + (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] + (y->gui-coord-y + sketchpad-size* + (* 40 (- (* (Math/sin (* x x)) + (Math/cos x)) + 1)))))} - ;; Lorentz factor: γ = 1/√(1 - v²/c²), v/c in [0, 0.95] + ;; Lorentz factor: γ = 1/√(1 - v²/c²), v/c ∈ [0, 0.95] "Feynman Lorentz" - {:idx 110 - :fn (fn [i] - (let [v-over-c (* 0.95 (/ i (double @sketch-input-x-count*)))] ; map to [0, 0.95] - (y->gui-coord-y - sketchpad-size* - (* 25 (/ 1.0 - (Math/sqrt (- 1.0 (* v-over-c v-over-c))))))))} + {:idx 110 + :formula "objective_y(x) = 1/√(1 - v²/c²)" + :fn (fn [i] + (let [v-over-c (* 0.95 (/ i (double @sketch-input-x-count*)))] ; map to [0, 0.95] + (y->gui-coord-y + sketchpad-size* + (* 25 (/ 1.0 + (Math/sqrt (- 1.0 (* v-over-c v-over-c))))))))} - ;; Wave equation: y = A·sin(kx - ωt), with t=0 simplifies to A·sin(kx) + ;; Wave equation: y = A·sin(kx - ωt) "Feynman Wave" - {:idx 120 - :fn (fn [i] - (let [x (* 12.0 (/ i (double @sketch-input-x-count*))) ; spatial coordinate - k 1.0 ; wave number - omega 0.5 ; angular frequency - t 2.0] ; fixed time - (y->gui-coord-y - sketchpad-size* - (* 60 (Math/sin (- (* k x) (* omega t)))))))}}) + {:idx 120 + :formula "objective_y(x) = A·sin(kx - ωt)" + :fn (fn [i] + (let [x (* 12.0 (/ i (double @sketch-input-x-count*))) ; spatial coordinate + k 1.0 ; wave number + omega 0.5 ; angular frequency + t 2.0] ; fixed time + (y->gui-coord-y + sketchpad-size* + (* 60 (Math/sin (- (* k x) (* omega t)))))))}}) diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index f99995b9..95c36381 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -142,7 +142,7 @@ (defn- check-new-best-fn [best-f-str ^JTextField best-fn-selectable-text] - (let [fn-str (str "y = " (ops/format-fn-str best-f-str))] + (let [fn-str (str "best_fn_y(x) = " (ops/format-fn-str best-f-str))] (when (not= fn-str (.getText best-fn-selectable-text)) (log/info "New Best Function: " fn-str) (ss/set-text* best-fn-selectable-text fn-str)))) diff --git a/src/closyr/ui/gui.clj b/src/closyr/ui/gui.clj index 5c8d65e2..30425ea6 100644 --- a/src/closyr/ui/gui.clj +++ b/src/closyr/ui/gui.clj @@ -135,6 +135,7 @@ (def ^:private items-points-accessors* (atom {})) (def ^:private replace-drawing-widget!* (atom nil)) +(def ^:private objective-formula-field* (atom nil)) (defn- redraw-sketch-widget! @@ -251,6 +252,13 @@ selectable-input-fns))) +(def ^:private input-y-formulas + (into {} + (map + (fn [[k v]] [k (:formula v)]) + selectable-input-fns))) + + (def ^:private dataset-fns (->> selectable-input-fns @@ -623,13 +631,16 @@ (let [{:keys [^JPanel drawing-widget items-point-setters items-point-getters]} @items-points-accessors* ^JComboBox jcb (.getSource e) selection (-> jcb .getSelectedItem str) - new-fn (input-y-fns selection)] + new-fn (input-y-fns selection) + new-formula (input-y-formulas selection)] (reset! xs* nil) (reset! input-y-fn* selection) (doseq [i (range @sketch-input-x-count*)] ((nth items-point-setters i) (.getX ^Point ((nth items-point-getters i))) (new-fn i))) + (when-let [^JTextField formula-field @objective-formula-field*] + (.setText formula-field (or new-formula ""))) (ss/repaint! drawing-widget) (log/info "Selected: " selection))) @@ -924,6 +935,10 @@ (.setLayout (GridLayout. 1 1))) sim-info-label (JLabel. "") + initial-formula (str (or (input-y-formulas input-data/initial-fn) "")) + ^JTextField objective-formula-text (reset! objective-formula-field* + (doto (JTextField. initial-formula) + (.setEditable false))) ^JTextField best-fn-selectable-text (doto (JTextField. "") (.setEditable false)) @@ -1041,6 +1056,7 @@ (.add input-fn-container brush-container) (.add inputs-and-info-container sim-info-label) + (.add inputs-and-info-container objective-formula-text) (.add inputs-and-info-container best-fn-selectable-text) (.add draw-parent drawing-widget) From 33ef46ce929038cf585667fb66e3f502238b0858 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 14:31:23 -0700 Subject: [PATCH 043/224] use new datasets as benchmarks --- README.md | 40 +++++ test/closyr/benchmark_functions_test.clj | 215 +++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 test/closyr/benchmark_functions_test.clj diff --git a/README.md b/README.md index 77c93583..2e5619d1 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,46 @@ Coverage looks like this if you run `lein cloverage`: ![test_coverage_2024-02-16_16-39.png](screenshots%2Ftest_coverage_2024-02-16_16-39.png) +## Benchmarks + +The project includes benchmark tests using standard symbolic regression test functions from the literature: + +### Running Benchmarks + +```bash +# Run all benchmark tests +$ lein test :only closyr.benchmark-functions-test + +# Run a specific benchmark +$ lein test :only closyr.benchmark-functions-test/nguyen-4-benchmark +``` + +### Benchmark Functions + +| Benchmark | Formula | Domain | Description | +|-----------|---------|--------|-------------| +| **Nguyen-4** | x⁶ + x⁵ + x⁴ + x³ + x² + x | [-1, 1] | High-degree polynomial | +| **Nguyen-5** | sin(x²)·cos(x) - 1 | [-1, 1] | Trigonometric composition | +| **Feynman Lorentz** | 1/√(1 - v²/c²) | [0, 0.95] | Relativistic Lorentz factor | +| **Feynman Wave** | sin(kx - ωt) | [0, 4π] | Traveling wave equation | + +These benchmarks are from the [Nguyen benchmark suite](https://gpbenchmarks.org/) and [AI Feynman dataset](https://space.mit.edu/home/tegmark/aifeynman.html), commonly used in symbolic regression research. + +### Example Results + +Results from running benchmarks with 200 population, 100 iterations, seed=42: + +| Benchmark | Best Score | Time | +|------------------|------------|------| +| Nguyen-4 | -0.91 | 4.97s | +| Nguyen-5 | -0.29 | 2.04s | +| Feynman Lorentz | -0.72 | 2.73s | +| Feynman Wave | -0.14 | 2.43s | + +*Score is negative sum of residuals (closer to 0 is better). Times measured on AMD Ryzen 9.* + +**Note:** Benchmarks use deterministic mode (random seed) for reproducibility, which disables parallelism. Production runs without a seed will be faster. + ## How It Works - We use Genetic Algorithms to allow candidate functions of best fit to compete. diff --git a/test/closyr/benchmark_functions_test.clj b/test/closyr/benchmark_functions_test.clj new file mode 100644 index 00000000..6de88434 --- /dev/null +++ b/test/closyr/benchmark_functions_test.clj @@ -0,0 +1,215 @@ +(ns closyr.benchmark-functions-test + "Benchmark tests for standard symbolic regression test functions (Nguyen, Feynman)" + (:require + [clojure.test :refer :all] + [closyr.ops :as ops] + [closyr.ops.common :as ops-common] + [closyr.ops.initialize :as ops-init] + [closyr.symbolic-regression :as symreg] + [closyr.test-utils :as test-utils]) + (:import + (java.text DecimalFormat))) + + +(use-fixtures :once test-utils/quiet-logging-fixture) + +(alter-var-root #'symreg/*is-testing* (constantly true)) + + +;; ============================================================================= +;; Timing Utilities +;; ============================================================================= + +(def ^:private decimal-fmt (DecimalFormat. "0.00")) + +(defmacro with-timing + "Execute body and return [result elapsed-ms]" + [& body] + `(let [start# (System/nanoTime) + result# (do ~@body) + elapsed# (/ (- (System/nanoTime) start#) 1e6)] + [result# elapsed#])) + + +(defn format-score [score] + (.format decimal-fmt score)) + + +(defn format-time [ms] + (if (>= ms 1000) + (str (.format decimal-fmt (/ ms 1000.0)) "s") + (str (.format decimal-fmt ms) "ms"))) + + +;; ============================================================================= +;; Benchmark Function Definitions +;; ============================================================================= + +(defn nguyen-4 + "Nguyen-4: x^6 + x^5 + x^4 + x^3 + x^2 + x, x in [-1, 1]" + [x] + (+ (Math/pow x 6) + (Math/pow x 5) + (Math/pow x 4) + (Math/pow x 3) + (Math/pow x 2) + x)) + + +(defn nguyen-5 + "Nguyen-5: sin(x^2) * cos(x) - 1, x in [-1, 1]" + [x] + (- (* (Math/sin (* x x)) + (Math/cos x)) + 1.0)) + + +(defn feynman-lorentz + "Lorentz factor: 1/sqrt(1 - v^2/c^2), v/c in [0, 0.95]" + [v-over-c] + (/ 1.0 + (Math/sqrt (- 1.0 (* v-over-c v-over-c))))) + + +(defn feynman-wave + "Wave equation: A * sin(kx - wt), with k=1, w=0.5, t=2, A=1" + [x] + (Math/sin (- x 1.0))) ; simplified: k=1, omega*t=1 + + +;; ============================================================================= +;; Data Generation +;; ============================================================================= + +(defn generate-benchmark-data + "Generate x,y pairs for a benchmark function" + [f x-min x-max n-points] + (let [xs (mapv (fn [i] + (+ x-min + (* (/ i (dec (double n-points))) + (- x-max x-min)))) + (range n-points)) + ys (mapv f xs)] + {:xs xs :ys ys})) + + +;; ============================================================================= +;; Benchmark Tests +;; ============================================================================= + +(deftest ^:benchmark nguyen-4-benchmark + (testing "Nguyen-4: polynomial x^6 + x^5 + x^4 + x^3 + x^2 + x" + (let [{:keys [xs ys]} (generate-benchmark-data nguyen-4 -1.0 1.0 30) + [{:keys [final-population iters-done]} elapsed-ms] + (with-timing + (binding [ops/*print-top-n* 1] + (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 50)} + (fn [] + (symreg/run-find-formula + {:input-phenos-count 200 + :initial-muts (ops-init/initial-mutations) + :iters 100 + :use-gui? false + :use-flamechart false + :random-seed 42 + :input-xs-exprs (ops-common/doubles->exprs xs) + :input-ys-exprs (ops-common/doubles->exprs ys)}))))) + best-score (apply max (:pop-scores final-population))] + + (is (= 200 (count (:pop final-population)))) + (is (= 100 iters-done)) + ;; Best score should be negative (error) and improving + (is (neg? best-score)) + (println (str "| Nguyen-4 | " (format-score best-score) + " | " (format-time elapsed-ms) " |"))))) + + +(deftest ^:benchmark nguyen-5-benchmark + (testing "Nguyen-5: sin(x^2)*cos(x) - 1" + (let [{:keys [xs ys]} (generate-benchmark-data nguyen-5 -1.0 1.0 30) + [{:keys [final-population iters-done]} elapsed-ms] + (with-timing + (binding [ops/*print-top-n* 1] + (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 50)} + (fn [] + (symreg/run-find-formula + {:input-phenos-count 200 + :initial-muts (ops-init/initial-mutations) + :iters 100 + :use-gui? false + :use-flamechart false + :random-seed 42 + :input-xs-exprs (ops-common/doubles->exprs xs) + :input-ys-exprs (ops-common/doubles->exprs ys)}))))) + best-score (apply max (:pop-scores final-population))] + + (is (= 200 (count (:pop final-population)))) + (is (= 100 iters-done)) + (is (neg? best-score)) + (println (str "| Nguyen-5 | " (format-score best-score) + " | " (format-time elapsed-ms) " |"))))) + + +(deftest ^:benchmark feynman-lorentz-benchmark + (testing "Feynman Lorentz factor: 1/sqrt(1 - v^2/c^2)" + (let [{:keys [xs ys]} (generate-benchmark-data feynman-lorentz 0.0 0.9 30) + [{:keys [final-population iters-done]} elapsed-ms] + (with-timing + (binding [ops/*print-top-n* 1] + (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 50)} + (fn [] + (symreg/run-find-formula + {:input-phenos-count 200 + :initial-muts (ops-init/initial-mutations) + :iters 100 + :use-gui? false + :use-flamechart false + :random-seed 42 + :input-xs-exprs (ops-common/doubles->exprs xs) + :input-ys-exprs (ops-common/doubles->exprs ys)}))))) + best-score (apply max (:pop-scores final-population))] + + (is (= 200 (count (:pop final-population)))) + (is (= 100 iters-done)) + (is (neg? best-score)) + (println (str "| Feynman Lorentz | " (format-score best-score) + " | " (format-time elapsed-ms) " |"))))) + + +(deftest ^:benchmark feynman-wave-benchmark + (testing "Feynman Wave equation: sin(kx - wt)" + (let [{:keys [xs ys]} (generate-benchmark-data feynman-wave 0.0 (* 4 Math/PI) 30) + [{:keys [final-population iters-done]} elapsed-ms] + (with-timing + (binding [ops/*print-top-n* 1] + (with-redefs-fn {#'symreg/config->log-steps (fn [_ _] 50)} + (fn [] + (symreg/run-find-formula + {:input-phenos-count 200 + :initial-muts (ops-init/initial-mutations) + :iters 100 + :use-gui? false + :use-flamechart false + :random-seed 42 + :input-xs-exprs (ops-common/doubles->exprs xs) + :input-ys-exprs (ops-common/doubles->exprs ys)}))))) + best-score (apply max (:pop-scores final-population))] + + (is (= 200 (count (:pop final-population)))) + (is (= 100 iters-done)) + (is (neg? best-score)) + (println (str "| Feynman Wave | " (format-score best-score) + " | " (format-time elapsed-ms) " |"))))) + + +(deftest benchmark-data-generation + (testing "Data generation produces correct ranges" + (let [{:keys [xs ys]} (generate-benchmark-data nguyen-4 -1.0 1.0 10)] + (is (= 10 (count xs))) + (is (= 10 (count ys))) + (is (= -1.0 (first xs))) + (is (= 1.0 (last xs))) + ;; nguyen-4 at x=-1: (-1)^6 + (-1)^5 + (-1)^4 + (-1)^3 + (-1)^2 + (-1) = 1 - 1 + 1 - 1 + 1 - 1 = 0 + (is (< (Math/abs (- (first ys) 0.0)) 1e-10)) + ;; nguyen-4 at x=1: 1 + 1 + 1 + 1 + 1 + 1 = 6 + (is (< (Math/abs (- (last ys) 6.0)) 1e-10))))) From 63ef3ec1a7bfa887ffab7be7c9866b983a3adc7c Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 14:41:46 -0700 Subject: [PATCH 044/224] use unicode font when available to print out GUI data at the bottom --- src/closyr/ui/gui.clj | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/closyr/ui/gui.clj b/src/closyr/ui/gui.clj index 30425ea6..ce2d5266 100644 --- a/src/closyr/ui/gui.clj +++ b/src/closyr/ui/gui.clj @@ -21,7 +21,9 @@ Container Cursor FlowLayout + Font Graphics2D + GraphicsEnvironment GridBagConstraints GridBagLayout GridLayout @@ -138,6 +140,19 @@ (def ^:private objective-formula-field* (atom nil)) +(defn- find-unicode-font + "Find a font that supports Unicode math symbols. Returns a Font or nil." + [size] + (let [preferred-fonts ["DejaVu Sans" "Noto Sans" "Segoe UI Symbol" + "Arial Unicode MS" "Lucida Sans Unicode" + "FreeSans" "Liberation Sans"] + available-fonts (set (.getAvailableFontFamilyNames + (GraphicsEnvironment/getLocalGraphicsEnvironment))) + found-font (first (filter available-fonts preferred-fonts))] + (when found-font + (Font. found-font Font/PLAIN size)))) + + (defn- redraw-sketch-widget! [] (@replace-drawing-widget!* (:drawing-widget @items-points-accessors*))) @@ -934,13 +949,22 @@ content-pane (doto (.getContentPane my-frame) (.setLayout (GridLayout. 1 1))) - sim-info-label (JLabel. "") + unicode-font (find-unicode-font 14) + sim-info-label (let [lbl (JLabel. "")] + (when unicode-font + (.setFont lbl unicode-font)) + lbl) initial-formula (str (or (input-y-formulas input-data/initial-fn) "")) - ^JTextField objective-formula-text (reset! objective-formula-field* - (doto (JTextField. initial-formula) - (.setEditable false))) - ^JTextField best-fn-selectable-text (doto (JTextField. "") - (.setEditable false)) + ^JTextField objective-formula-text (let [tf (doto (JTextField. initial-formula) + (.setEditable false))] + (when unicode-font + (.setFont tf unicode-font)) + (reset! objective-formula-field* tf)) + ^JTextField best-fn-selectable-text (let [tf (doto (JTextField. "") + (.setEditable false))] + (when unicode-font + (.setFont tf unicode-font)) + tf) ^XYChart best-fn-chart (plot/make-plot:n-series {:x-axis-title "X" From f492de409e759f688bccb0d8c0c5082dff1a4a50 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 14:55:53 -0700 Subject: [PATCH 045/224] symja obj fn text --- src/closyr/dataset/inputs.clj | 36 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/closyr/dataset/inputs.clj b/src/closyr/dataset/inputs.clj index e1a4095d..23853f97 100644 --- a/src/closyr/dataset/inputs.clj +++ b/src/closyr/dataset/inputs.clj @@ -26,7 +26,7 @@ [sketchpad-size* sketch-input-x-count*] {initial-fn {:idx 0 - :formula "objective_y(x) = 50·sin(13x) + 30·cos(19x)" + :formula "objective_fn_y(x) = 50*Sin(13*x) + 30*Cos(19*x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* @@ -35,7 +35,7 @@ "sin+cos 2" {:idx 5 - :formula "objective_y(x) = 10·sin(17x) + 50·cos(5x)" + :formula "objective_fn_y(x) = 10*Sin(17*x) + 50*Cos(5*x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* @@ -43,45 +43,45 @@ (* 50 (Math/cos (* 5.0 (/ i @sketch-input-x-count*)))))))} "cos" {:idx 10 - :formula "objective_y(x) = 80·cos(12x)" + :formula "objective_fn_y(x) = 80*Cos(12*x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* (* 80 (Math/cos (* 12.0 (/ i @sketch-input-x-count*))))))} "sin" {:idx 20 - :formula "objective_y(x) = 80·sin(12x)" + :formula "objective_fn_y(x) = 80*Sin(12*x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* (* 80 (Math/sin (* 12.0 (/ i @sketch-input-x-count*))))))} "log" {:idx 30 - :formula "objective_y(x) = 10·ln(0.01 + x)" + :formula "objective_fn_y(x) = 10*Log(0.01 + x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* (* 10 (Math/log (+ 0.01 (/ i @sketch-input-x-count*))))))} "hline" {:idx 40 - :formula "objective_y(x) = 0" + :formula "objective_fn_y(x) = 0" :fn (fn [i] (y->gui-coord-y sketchpad-size* 0.0))} "prime count" {:idx 50 - :formula "objective_y(x) = π(x) (prime counting function)" + :formula "objective_fn_y(x) = PrimePi(x)" :fn (fn [i] (let [xys (data-prime-counting/get-data @sketch-input-x-count*)] (y->gui-coord-y sketchpad-size* (second (nth xys i)))))} "primes" {:idx 60 - :formula "objective_y(x) = pₙ (nth prime number)" + :formula "objective_fn_y(x) = Prime(n)" :fn (fn [i] (let [xys (data-primes/get-data @sketch-input-x-count*)] (y->gui-coord-y sketchpad-size* (second (nth xys i)))))} "gaussian" {:idx 70 - :formula "objective_y(x) = √(2π)·exp(-((x-μ)/σ)²/2)" + :formula "objective_fn_y(x) = Sqrt(2*Pi)*Exp(-((x-mu)/sigma)^2/2)" :fn (fn [i] (y->gui-coord-y sketchpad-size* @@ -91,17 +91,17 @@ (/ (- i (/ @sketch-input-x-count* 2)) 5.0)))))))} "random" {:idx 80 - :formula "objective_y(x) = random()" + :formula "objective_fn_y(x) = Random()" :fn (fn [i] (y->gui-coord-y sketchpad-size* (* 60 (rand))))} - ;; Nguyen-4: x⁶ + x⁵ + x⁴ + x³ + x² + x , x ∈ [-1, 1] + ;; Nguyen-4: x^6 + x^5 + x^4 + x^3 + x^2 + x , x in [-1, 1] "Nguyen-4" {:idx 90 - :formula "objective_y(x) = x⁶ + x⁵ + x⁴ + x³ + x² + x" + :formula "objective_fn_y(x) = x^6 + x^5 + x^4 + x^3 + x^2 + x" :fn (fn [i] (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] (y->gui-coord-y @@ -113,10 +113,10 @@ (Math/pow x 2) x)))))} - ;; Nguyen-5: sin(x²)·cos(x) - 1 , x ∈ [-1, 1] + ;; Nguyen-5: Sin(x^2)*Cos(x) - 1 , x in [-1, 1] "Nguyen-5" {:idx 100 - :formula "objective_y(x) = sin(x²)·cos(x) - 1" + :formula "objective_fn_y(x) = Sin(x^2)*Cos(x) - 1" :fn (fn [i] (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] (y->gui-coord-y @@ -125,10 +125,10 @@ (Math/cos x)) 1)))))} - ;; Lorentz factor: γ = 1/√(1 - v²/c²), v/c ∈ [0, 0.95] + ;; Lorentz factor: 1/Sqrt(1 - v^2/c^2), v/c in [0, 0.95] "Feynman Lorentz" {:idx 110 - :formula "objective_y(x) = 1/√(1 - v²/c²)" + :formula "objective_fn_y(x) = 1/Sqrt(1 - x^2)" :fn (fn [i] (let [v-over-c (* 0.95 (/ i (double @sketch-input-x-count*)))] ; map to [0, 0.95] (y->gui-coord-y @@ -136,10 +136,10 @@ (* 25 (/ 1.0 (Math/sqrt (- 1.0 (* v-over-c v-over-c))))))))} - ;; Wave equation: y = A·sin(kx - ωt) + ;; Wave equation: A*Sin(k*x - omega*t) "Feynman Wave" {:idx 120 - :formula "objective_y(x) = A·sin(kx - ωt)" + :formula "objective_fn_y(x) = A*Sin(k*x - omega*t)" :fn (fn [i] (let [x (* 12.0 (/ i (double @sketch-input-x-count*))) ; spatial coordinate k 1.0 ; wave number From 505c82686fa93905708f638bb4f4d5020377ef4e Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 15:22:13 -0700 Subject: [PATCH 046/224] symja obj fn text widgets --- src/closyr/dataset/inputs.clj | 28 ++++++++++++++-------------- src/closyr/symbolic_regression.clj | 2 +- src/closyr/ui/gui.clj | 22 ++++++++++++++++++++-- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/closyr/dataset/inputs.clj b/src/closyr/dataset/inputs.clj index 23853f97..757ad8a5 100644 --- a/src/closyr/dataset/inputs.clj +++ b/src/closyr/dataset/inputs.clj @@ -26,7 +26,7 @@ [sketchpad-size* sketch-input-x-count*] {initial-fn {:idx 0 - :formula "objective_fn_y(x) = 50*Sin(13*x) + 30*Cos(19*x)" + :formula "50*Sin(13*x) + 30*Cos(19*x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* @@ -35,7 +35,7 @@ "sin+cos 2" {:idx 5 - :formula "objective_fn_y(x) = 10*Sin(17*x) + 50*Cos(5*x)" + :formula "10*Sin(17*x) + 50*Cos(5*x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* @@ -43,45 +43,45 @@ (* 50 (Math/cos (* 5.0 (/ i @sketch-input-x-count*)))))))} "cos" {:idx 10 - :formula "objective_fn_y(x) = 80*Cos(12*x)" + :formula "80*Cos(12*x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* (* 80 (Math/cos (* 12.0 (/ i @sketch-input-x-count*))))))} "sin" {:idx 20 - :formula "objective_fn_y(x) = 80*Sin(12*x)" + :formula "80*Sin(12*x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* (* 80 (Math/sin (* 12.0 (/ i @sketch-input-x-count*))))))} "log" {:idx 30 - :formula "objective_fn_y(x) = 10*Log(0.01 + x)" + :formula "10*Log(0.01 + x)" :fn (fn [i] (y->gui-coord-y sketchpad-size* (* 10 (Math/log (+ 0.01 (/ i @sketch-input-x-count*))))))} "hline" {:idx 40 - :formula "objective_fn_y(x) = 0" + :formula "0" :fn (fn [i] (y->gui-coord-y sketchpad-size* 0.0))} "prime count" {:idx 50 - :formula "objective_fn_y(x) = PrimePi(x)" + :formula "PrimePi(x)" :fn (fn [i] (let [xys (data-prime-counting/get-data @sketch-input-x-count*)] (y->gui-coord-y sketchpad-size* (second (nth xys i)))))} "primes" {:idx 60 - :formula "objective_fn_y(x) = Prime(n)" + :formula "Prime(x)" :fn (fn [i] (let [xys (data-primes/get-data @sketch-input-x-count*)] (y->gui-coord-y sketchpad-size* (second (nth xys i)))))} "gaussian" {:idx 70 - :formula "objective_fn_y(x) = Sqrt(2*Pi)*Exp(-((x-mu)/sigma)^2/2)" + :formula "Sqrt(2*Pi)*Exp(-((x-mu)/sigma)^2/2)" :fn (fn [i] (y->gui-coord-y sketchpad-size* @@ -91,7 +91,7 @@ (/ (- i (/ @sketch-input-x-count* 2)) 5.0)))))))} "random" {:idx 80 - :formula "objective_fn_y(x) = Random()" + :formula "Random()" :fn (fn [i] (y->gui-coord-y sketchpad-size* @@ -101,7 +101,7 @@ ;; Nguyen-4: x^6 + x^5 + x^4 + x^3 + x^2 + x , x in [-1, 1] "Nguyen-4" {:idx 90 - :formula "objective_fn_y(x) = x^6 + x^5 + x^4 + x^3 + x^2 + x" + :formula "x^6 + x^5 + x^4 + x^3 + x^2 + x" :fn (fn [i] (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] (y->gui-coord-y @@ -116,7 +116,7 @@ ;; Nguyen-5: Sin(x^2)*Cos(x) - 1 , x in [-1, 1] "Nguyen-5" {:idx 100 - :formula "objective_fn_y(x) = Sin(x^2)*Cos(x) - 1" + :formula "Sin(x^2)*Cos(x) - 1" :fn (fn [i] (let [x (- (* 2.0 (/ i (double @sketch-input-x-count*))) 1.0)] ; map to [-1, 1] (y->gui-coord-y @@ -128,7 +128,7 @@ ;; Lorentz factor: 1/Sqrt(1 - v^2/c^2), v/c in [0, 0.95] "Feynman Lorentz" {:idx 110 - :formula "objective_fn_y(x) = 1/Sqrt(1 - x^2)" + :formula "1/Sqrt(1 - x^2)" :fn (fn [i] (let [v-over-c (* 0.95 (/ i (double @sketch-input-x-count*)))] ; map to [0, 0.95] (y->gui-coord-y @@ -139,7 +139,7 @@ ;; Wave equation: A*Sin(k*x - omega*t) "Feynman Wave" {:idx 120 - :formula "objective_fn_y(x) = A*Sin(k*x - omega*t)" + :formula "A*Sin(k*x - omega*t)" :fn (fn [i] (let [x (* 12.0 (/ i (double @sketch-input-x-count*))) ; spatial coordinate k 1.0 ; wave number diff --git a/src/closyr/symbolic_regression.clj b/src/closyr/symbolic_regression.clj index 95c36381..b804f179 100644 --- a/src/closyr/symbolic_regression.clj +++ b/src/closyr/symbolic_regression.clj @@ -142,7 +142,7 @@ (defn- check-new-best-fn [best-f-str ^JTextField best-fn-selectable-text] - (let [fn-str (str "best_fn_y(x) = " (ops/format-fn-str best-f-str))] + (let [fn-str (str "" (ops/format-fn-str best-f-str))] (when (not= fn-str (.getText best-fn-selectable-text)) (log/info "New Best Function: " fn-str) (ss/set-text* best-fn-selectable-text fn-str)))) diff --git a/src/closyr/ui/gui.clj b/src/closyr/ui/gui.clj index ce2d5266..d6aa7fdc 100644 --- a/src/closyr/ui/gui.clj +++ b/src/closyr/ui/gui.clj @@ -20,6 +20,7 @@ Color Container Cursor + Dimension FlowLayout Font Graphics2D @@ -950,21 +951,38 @@ (.setLayout (GridLayout. 1 1))) unicode-font (find-unicode-font 14) + label-width 130 sim-info-label (let [lbl (JLabel. "")] (when unicode-font (.setFont lbl unicode-font)) lbl) initial-formula (str (or (input-y-formulas input-data/initial-fn) "")) + ^JLabel objective-label (let [lbl (doto (JLabel. "ObjectiveFn(x_) :=") + (.setPreferredSize (Dimension. label-width 20)))] + (when unicode-font + (.setFont lbl unicode-font)) + lbl) ^JTextField objective-formula-text (let [tf (doto (JTextField. initial-formula) (.setEditable false))] (when unicode-font (.setFont tf unicode-font)) (reset! objective-formula-field* tf)) + ^JPanel objective-row (doto (JPanel. (BorderLayout.)) + (.add objective-label BorderLayout/WEST) + (.add objective-formula-text BorderLayout/CENTER)) + ^JLabel best-label (let [lbl (doto (JLabel. "BestFitFn(x_) :=") + (.setPreferredSize (Dimension. label-width 20)))] + (when unicode-font + (.setFont lbl unicode-font)) + lbl) ^JTextField best-fn-selectable-text (let [tf (doto (JTextField. "") (.setEditable false))] (when unicode-font (.setFont tf unicode-font)) tf) + ^JPanel best-row (doto (JPanel. (BorderLayout.)) + (.add best-label BorderLayout/WEST) + (.add best-fn-selectable-text BorderLayout/CENTER)) ^XYChart best-fn-chart (plot/make-plot:n-series {:x-axis-title "X" @@ -1080,8 +1098,8 @@ (.add input-fn-container brush-container) (.add inputs-and-info-container sim-info-label) - (.add inputs-and-info-container objective-formula-text) - (.add inputs-and-info-container best-fn-selectable-text) + (.add inputs-and-info-container objective-row) + (.add inputs-and-info-container best-row) (.add draw-parent drawing-widget) (.add row-3-container draw-parent) From 8896092c9719d8208c17eb568c17a40cf6631790 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 16:14:31 -0700 Subject: [PATCH 047/224] fix run junit tests --- run-junit.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-junit.sh b/run-junit.sh index 7a76c760..800ab90e 100755 --- a/run-junit.sh +++ b/run-junit.sh @@ -18,7 +18,7 @@ javac -d target/test-classes \ test/org/closyr/core/FindFormulaTest.java # Run JUnit -java -jar ~/.m2/repository/org/junit/platform/junit-platform-console-standalone/1.10.2/junit-platform-console-standalone-1.10.2.jar \ +java -jar ~/.m2/repository/org/junit/platform/junit-platform-console-standalone/6.0.1/junit-platform-console-standalone-6.0.1.jar \ execute \ --class-path "target/test-classes:target/classes:$CP" \ --select-class org.closyr.core.FindFormulaTest \ From b416345ecdbef232aad495b6736f05b204b5e48a Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 16:58:55 -0700 Subject: [PATCH 048/224] readme arch diagram --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/README.md b/README.md index 2e5619d1..dec89dcb 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,84 @@ IFormulaConfig config = FormulaConfigBuilder.builder() - [ ] When I created this and my other symbolic regression tools, I didn't know about the formal field of symbolic regression. I've since found some great libraries that I should review and apply the lessons to this project: https://github.com/MilesCranmer/PySR +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ ENTRY POINTS │ +├─────────────────────┬─────────────────────┬─────────────────────────────────────┤ +│ closyr.core │ closyr.api.finder │ closyr.symbolic-regression │ +│ (CLI -main) │ (Java API) │ (Main Orchestrator) │ +└─────────┬───────────┴──────────┬──────────┴──────────────┬──────────────────────┘ + │ │ │ + └──────────────────────┼─────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ GENETIC ALGORITHM │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ closyr.ga │ │ +│ │ (Selection, Crossover, Mutation Loop) │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +└───────────────────────────────────┬─────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ EXPRESSION OPERATIONS │ +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ +│ │ closyr.ops │───▶│ ops.modify │ │ ops.eval │ │ +│ │ (Facade) │ │ (Mutations & │ │ (Evaluate │ │ +│ │ │───▶│ Crossover) │ │ Expressions) │ │ +│ └───────┬───────┘ └───────────────┘ └───────────────┘ │ +│ │ ┌───────────────┐ ┌───────────────┐ │ +│ └───────────▶│ ops.initialize│ │ ops.common │ │ +│ │ (Population │ │ (Shared Utils)│ │ +│ │ Seeding) │ │ │ │ +│ └───────────────┘ └───────────────┘ │ +└───────────────────────────────────┬─────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ SYMJA (External) │ +│ Symbolic Math Engine - AST Manipulation & Evaluation │ +│ github.com/axkr/symja_android_library │ +└─────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ UI LAYER │ +│ ┌─────────────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ closyr.ui.gui │ │ closyr.ui.plot │ │ closyr.ui.icons │ │ +│ │ (Swing/Seesaw GUI) │───▶│ (XChart) │ │ │ │ +│ │ - Sketchpad │ │ - Best Fn │ │ │ │ +│ │ - Controls │ │ - Scores │ │ │ │ +│ │ - Function Display │ │ │ │ │ │ +│ └─────────────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ DATA LAYER │ +│ ┌─────────────────────────┐ ┌───────────────────────────────────────────┐ │ +│ │ closyr.dataset.inputs │ │ Built-in Datasets │ │ +│ │ (Benchmark Functions) │ │ - prime-10000 (nth prime) │ │ +│ │ - Nguyen-4, Nguyen-5 │ │ - prime-counting (π(x)) │ │ +│ │ - Feynman Lorentz/Wave │ │ │ │ +│ │ - Trig, Log, Gaussian │ │ │ │ +│ └─────────────────────────┘ └───────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ UTILITIES │ +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ +│ │ util.log │ │ util.csv │ │ util.prng │ │ util.spec │ │ +│ │ (Logging) │ │ (CSV Import) │ │ (Seeded RNG) │ │ (Malli Specs) │ │ +│ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────┘ + +Data Flow: + 1. User provides (x,y) data via GUI sketchpad, CSV file, or CLI args + 2. GA creates initial population of random Symja expressions + 3. Each iteration: evaluate fitness (sum of residuals), select, mutate, crossover + 4. Best expressions shown in real-time on charts + 5. Final best-fit function returned as Symja-compatible expression +``` + ## Credits - This project would not have been possible without the symbolic math library `Symja` (https://github.com/axkr/symja_android_library), which has been great to use and I consider it to be like a `MathJs` (https://github.com/josdejong/mathjs) on the JVM. From 43fe7ecde40dc467abc0253ec4784b9c55d94987 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 17:28:03 -0700 Subject: [PATCH 049/224] rare bug and test case to confirm fix --- src/closyr/ops/modify.clj | 12 +++++----- test/closyr/ops_modify_test.clj | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/closyr/ops/modify.clj b/src/closyr/ops/modify.clj index dcdafd25..817a80e8 100644 --- a/src/closyr/ops/modify.clj +++ b/src/closyr/ops/modify.clj @@ -177,13 +177,15 @@ (if discount-mod? ;; keep last op: - (merge p (ops-common/->phenotype x-sym e1 (:util p-discard))) + (if-let [refreshed (ops-common/->phenotype x-sym e1 (:util p-discard))] + (merge p refreshed) + p) ;; record new last op: - (-> x-sym - (ops-common/->phenotype new-expr (:util p-discard)) - (with-recent-mod-metadata {:label (name crossover-flavor) - :op :modify-crossover})))) + (some-> x-sym + (ops-common/->phenotype new-expr (:util p-discard)) + (with-recent-mod-metadata {:label (name crossover-flavor) + :op :modify-crossover})))) (catch Exception e (log/error "Error in ops/crossover: " (.getMessage e)) nil))) diff --git a/test/closyr/ops_modify_test.clj b/test/closyr/ops_modify_test.clj index a06fddc8..3b3f76ec 100644 --- a/test/closyr/ops_modify_test.clj +++ b/test/closyr/ops_modify_test.clj @@ -7,6 +7,7 @@ [closyr.ops.modify :as ops-modify] [closyr.util.prng :as prng]) (:import + (java.util UUID) (org.matheclipse.core.expression F) (org.matheclipse.core.interfaces @@ -399,6 +400,44 @@ :expr (F/Plus x (F/Times x (F/Cos (F/Subtract x F/C1D2))))}) nil))))))) + ;; Test for fix: when ->phenotype returns nil in the "record new last op" branch, + ;; crossover should return nil (not an incomplete map like {:last-op "cros:plus"}) + (with-redefs-fn {#'prng/rand-int (fn [maxv] (dec maxv)) + #'prng/rand-nth (fn [coll] (first coll)) + #'ops-common/->phenotype (fn [_ _ _] nil)} + (fn [] + (with-redefs [ops-modify/crossover-sampler [:plus]] + (let [x (F/Dummy "x")] + (testing "Crossover returns nil when ->phenotype fails (new expr branch)" + (is (= (ops-modify/crossover + 100 + {:sym x + :expr (F/Cos x)} + {:sym x + :expr (F/Sin x)}) + nil))))))) + + ;; Test for fix: when ->phenotype returns nil in the "keep last op" branch (discount-mod? true), + ;; crossover should return the original phenotype p unchanged + (with-redefs-fn {#'prng/rand-int (fn [maxv] (dec maxv)) + #'prng/rand-nth (fn [coll] (first coll)) + #'ops-common/->phenotype (fn [_ _ _] nil)} + (fn [] + (with-redefs [ops-modify/crossover-sampler [:plus] + ;; Force discount-mod? to be true + ops-modify/check-modification-result (fn [_ _ _] [false true])] + (let [x (F/Dummy "x") + test-uuid (UUID/randomUUID) + original-pheno {:sym x :expr (F/Cos x) :id test-uuid}] + (testing "Crossover returns original phenotype when ->phenotype fails (discount branch)" + (is (= (ops-modify/crossover + 100 + original-pheno + {:sym x + :expr (F/Sin x) + :id (UUID/randomUUID)}) + original-pheno))))))) + (with-redefs-fn {#'prng/rand-int (fn [maxv] (dec maxv)) #'prng/rand-nth (fn [coll] (last coll))} (fn [] From 0c23dd9a4a450e2e86ec5d43e5481dee84080586 Mon Sep 17 00:00:00 2001 From: Octavian Geagla Date: Sun, 28 Dec 2025 17:29:32 -0700 Subject: [PATCH 050/224] ns graph --- nomis-ns-graph.png | Bin 696739 -> 795966 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/nomis-ns-graph.png b/nomis-ns-graph.png index 90347ced1c73ca6d17c0257b7e9189495ba4350e..715883087100436bf3e0ef4c13d9368ac8ad201a 100644 GIT binary patch literal 795966 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

-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

P~&wd+p_k+z!2n}+hB4nqbFlrPuko3PTc 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 z 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�SYavvqYL^`uY){#VA^Bw)R{ z5g`uw-YSSHdmcqE4eM>jcyhj-n3p=xx*=V^*~rN;vZCoJ_68LSDLBH)SuPdemmjpN zcNN$?EZV+R6F|Ph325R%xzUgy%ng($_5m8+4nS~Vq!|HeN?kIKt%=SW8_q)oL$zU^hn8X|0 zE&rzu2r2~TCd9DsJOUlJf3HSrGE4)_KMU*5bkGU3l==x}f#Y+@=ePnzFc|Q91~HkH z(_PCY7lo}~6yHO$0(^P}(~~+cH&x6GY#~<>FMpfb%kG4~aiT^>v^HY*-)Ri<i19zS6b5EKd#bqB}G zL$GfaAt?=PRntMf!pm2#v;yI2P^O=&0}UY8W)TZzrQc3#P)+y9HGa`StdGF&pIvm;`Z_5$J-R2 z%47XN8pAlf6miirx{J>mX~ zm?bw-D)+jNSj6;JrV@$A%t?RiAq>RoA9v-LNh*fdZy|kH_o7-&Fx?@IW2rL@7Iz2* zS2dc|G!7R1=f3u8+*L8S6LDz>TwWI!yl)j185B0=k5631!XSc=3G?&+1LV#CgW&Ut{)L|8kLC=S|PioqVeqHT~gRl-`I8&L&7bM2C@0*8*iVY;D5X6Hy>c_HA2K zP+eJYrgm}m@w4eQ3?KOAg)XU4dBxoO_i38Bz1Fm;WdVpx6vu&XI+uWu_Zs{b8@Xe7 zM9z49B6f&Bi*ndGIWtl5o;*ir`^-F3p@9LHu6c)|zLxua3TEPn@pYy9E~fK_RDf

CBTX-UpF1RM@FbPs05536G7oz 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�J`m?%&mq<1Um>1M)YWY_FqntZKTXc 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�bgxjLK~7!&I7vg~oB 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