diff --git a/.github/workflows/commit_validation.yml b/.github/workflows/commit_validation.yml index 91090b0..2ef708e 100644 --- a/.github/workflows/commit_validation.yml +++ b/.github/workflows/commit_validation.yml @@ -16,29 +16,34 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' check-latest: true - name: Install clojure tools # See https://github.com/DeLaGuardo/setup-clojure/commits/main/ - uses: DeLaGuardo/setup-clojure@ada62bb3282a01a296659d48378b812b8e097360 #v13.2 + uses: DeLaGuardo/setup-clojure@3fe9b3ae632c6758d0b7757b0838606ef4287b08 #v13.2 with: - cli: 1.12.0.1530 - bb: 1.12.197 - clj-kondo: 2025.02.20 - zprint: 1.2.9 + cli: 1.12.3.1577 + bb: 1.12.209 + clj-kondo: 2025.10.23 + zprint: 1.3.0 - name: Lint run: bb lint -v - name: Setup zprint run: echo "{:search-config? true}" >> ~/.zprintrc - - name: Install fdfind - run: sudo rm /var/lib/man-db/auto-update && sudo apt-get install -y fd-find && mkdir -p ~/.local/bin/ && PATH=$PATH:~/.local/bin/ && ln -s $(which fdfind) ~/.local/bin/fd + - name: Install fd-find + run: | + sudo apt-get update + sudo apt-get install -y fd-find + mkdir -p ~/.local/bin + ln -s "$(which fdfind)" ~/.local/bin/fd + echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Cache clojure dependencies uses: actions/cache@v4 with: @@ -51,7 +56,7 @@ jobs: run: git diff - name: Pushed code should already be formatted # See https://github.com/CatChen/check-git-status-action - uses: CatChen/check-git-status-action@fb60fe626b56d5a4adcb227327ba4d24326a873a #v1.4.4 + uses: CatChen/check-git-status-action@d75bdbea704869e2a3b01111b4a82c49e9bd348d #v1.4.4 with: fail-if-not-clean: true request-changes-if-not-clean: false diff --git a/.github/workflows/deploy_clojar.yml b/.github/workflows/deploy_clojar.yml index 0e0a75d..0e3ae66 100644 --- a/.github/workflows/deploy_clojar.yml +++ b/.github/workflows/deploy_clojar.yml @@ -14,7 +14,7 @@ jobs: steps: # This step checks out a copy of your repository. - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: jlesquembre/clojars-publish-action@89a4eb7bdbe1270621e6643250afce152701699e # v0.3 env: diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index d6b738b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "clj-xchart"] - path = clj-xchart - url = https://github.com/hypirion/clj-xchart.git diff --git a/bb.edn b/bb.edn index 6f96dae..83ff919 100644 --- a/bb.edn +++ b/bb.edn @@ -1,9 +1,7 @@ -{:deps {com.github.hephaistox/auto-build {:git/sha "6e9a158ebf50ed9d0291f05c767b056c24508ca5"} - io.github.hephaistox/auto-build {:git/sha "e907e29f65049a805c5a33e2f60bbe838c268a1a"}} - :paths ["src"] +{:deps {com.github.hephaistox/auto-build {:git/sha "cd0854aaba46d240ac4852b97d1f4f708ed626dc"}} + :paths [] :tasks - {:init (do (def exit System/exit)) - :requires [[auto-build.os.exit-codes]] + {:requires [[auto-build.os.exit-codes] [auto-build.os.exiting :refer [exit]]] bp {:doc "Before push" :task (binding [exit (fn [exit-code & _] (when-not (zero? exit-code) exit-code))] (-> (or (run 'format) @@ -16,9 +14,16 @@ System/exit))} clean {:doc "Clean temporary files" :requires [[auto-build.tasks.clean :as task-clean] + [babashka.fs :as fs] [auto-build.echo :refer [level1-header]]] - :task (-> (task-clean/clean level1-header "." [".DS_Store"] [".cpcache"] (current-task)) - exit)} + :task (let [rc (task-clean/clean level1-header + "." + [".DS_Store"] + [".cpcache" ".shadow-cljs" "node_modules" "tmp"] + (current-task))] + (fs/create-dir "tmp") + (fs/create-file "tmp/.keep") + (exit rc))} clj-test {:doc "Test with clj" :requires [[auto-build.tasks.clj-test :as build-clj-test] @@ -39,6 +44,9 @@ [auto-build.echo :refer [level1-header]]] :task (-> (build-cljs-node-test/cljs-node-test level1-header "." (current-task) ["ltest"]) exit)} + copy {:doc "Copy files from source projects (as defined in ext_src.edn)" + :requires [[auto-build.tasks.copy :as tc] [auto-build.echo :refer [level1-header]]] + :task (tc/run level1-header "ext_src.edn")} deploy {:doc "Deploy" :requires [[auto-build.tasks.deploy :as task-deploy] [auto-build.echo :refer [level1-header]]] @@ -48,7 +56,7 @@ :task (do (shell "clj -M:antq") (shell "npm upgrade"))} docs {:doc "Docs" :requires [[auto-build.tasks.docs :as build-docs] [auto-build.echo :refer [level1-header]]] - :task (-> (build-docs/docs level1-header "." (current-task) "gh-pages") + :task (-> (build-docs/docs level1-header "." (current-task) "gh-pages" ":codox") exit)} format {:doc "Format" :requires [[auto-build.tasks.formatting :as task-format] @@ -56,7 +64,6 @@ :task (-> (task-format/format level1-header "." (current-task)) exit)} heph-info {:doc "Hephaistox project info" - :extra-paths ["env/dev/resources"] :requires [[auto-build.tasks.heph-info :as heph-info] [auto-build.echo :refer [level1-header]]] :task (-> (heph-info/heph-info level1-header (current-task)) @@ -68,11 +75,5 @@ repl {:doc "Clojure repl" :override-builtin true :requires [[auto-build.tasks.repl :as build-repl] [auto-build.echo :refer [level1-header]]] - :task (-> (build-repl/repl level1-header "." (current-task)) - exit)} - uberjar {:doc "Uberjar" - :override-builtin true - :requires [[auto-build.tasks.uberjar :as build-uberjar] - [auto-build.echo :refer [level1-header]]] - :task (-> (build-uberjar/uberjar level1-header "." "target/uberjar" "") - exit)}}} \ No newline at end of file + :task (-> (build-repl/repl level1-header "." (current-task) [:repl :test-clj] 7005) + exit)}}} diff --git a/build/compile.clj b/build/compile.clj deleted file mode 100644 index e4fa8cb..0000000 --- a/build/compile.clj +++ /dev/null @@ -1,22 +0,0 @@ -(ns compile - (:require - [clojure.tools.build.api :refer [compile-clj create-basis jar]])) - -(defn compile-jar - [{:keys [target-dir] - :as _pars}] - (let [class-dir (str target-dir "/classes") - jar-file (str target-dir "/production/auto_js.jar") - basis (create-basis)] - (try (let [compile (compile-clj {:basis basis - :bindings {#'clojure.core/*assert* false - #'clojure.core/*warn-on-reflection* true} - :out :capture - :err :capture - :class-dir class-dir})] - (jar {:class-dir class-dir - :jar-file jar-file}) - {:compile-jar compile}) - (catch Exception e - {:compile-jar {:exception e} - :status :compilation-failed})))) diff --git a/clj-xchart b/clj-xchart deleted file mode 160000 index 4c184a6..0000000 --- a/clj-xchart +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c184a6a543659d52e684fcb54ef88a42904c17f diff --git a/deps.edn b/deps.edn index f586bb8..697a827 100644 --- a/deps.edn +++ b/deps.edn @@ -1,17 +1,12 @@ {:aliases - {:antq {:deps {com.github.liquidz/antq {:mvn/version "2.10.1241"}} + {:antq {:deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}} :main-opts ["-m" "antq.core"]} - :build {:deps {io.github.clojure/tools.build {:git/sha "52cf7d6" - :git/tag "v0.10.6"}} - :extra-paths ["build"]} :clj-test-runner {:exec-fn cognitect.test-runner.api/test :extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner" :sha "3f288f1f16d167723ad87cc35b1dfee3c1681e10"}}} - :cljs-deps {:extra-deps {metosin/malli {:mvn/version "0.16.1"} - thheller/shadow-cljs {:mvn/version "2.28.19"}} - :extra-paths - ["src/cljc" "src/cljs" "env/dev/resources" "test/cljc" "test/cljs" "test/resources"]} + :cljs-deps {:extra-deps {thheller/shadow-cljs {:mvn/version "2.28.19"}} + :extra-paths ["src/cljc" "test/unit/cljc"]} :codox {:exec-args {:description "`auto-js` is about all core technical functionalities we may need to start a project." @@ -23,17 +18,9 @@ :extra-deps {codox/codox {:mvn/version "0.10.8"}}} :repl {:exec-args {:port 7001} :exec-fn auto-build.repl.entry-point/start - :extra-deps {cider/cider-nrepl {:mvn/version "0.50.2"} - io.github.hephaistox/auto-build {:git/sha - "e907e29f65049a805c5a33e2f60bbe838c268a1a"} - refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}} - :extra-paths - ["env/dev/resources" "test/clj" "test/cljc" "test/resources" "test/src" "test/resources"]} - :test-clj {:extra-paths ["test/clj" "test/cljc" "test/resources"] + :extra-paths []} + :test-clj {:extra-paths ["test/unit/cljc"] :main-opts - ["-m" "cognitect.test-runner" "-r" ".*-test.*" "-d" "test/unit/clj" "test/unit/cljc"]} - :test-demo {:extra-paths ["test/demo/cljc"]}} - :deps {com.github.hephaistox/auto-opti {:git/sha "ddd4c4e56950c354f88e5f0d424dbb710cb67bf4"} - com.hypirion/clj-xchart {:local/root "clj-xchart"} - io.github.hephaistox/auto-core {:git/sha "4d41497668ada474e8089403cf6f5e9d263e18ad"}} - :paths ["src/cljc" "src/clj"]} + ["-m" "cognitect.test-runner" "-r" ".*-test.*" "-d" "test/unit/clj" "test/unit/cljc"]}} + :deps {com.github.hephaistox/auto-opti {:git/sha "12dc1ada264af45901367868ddaa6ec7953b6e21"}} + :paths ["src/cljc"]} diff --git a/env/dev/resources/run_config.edn b/env/dev/resources/run_config.edn deleted file mode 100644 index b2e6130..0000000 --- a/env/dev/resources/run_config.edn +++ /dev/null @@ -1 +0,0 @@ -{:app-name "auto-js"} diff --git a/ext_src.edn b/ext_src.edn new file mode 100644 index 0000000..6e2e530 --- /dev/null +++ b/ext_src.edn @@ -0,0 +1,5 @@ +{com.github.hephaistox/auto-build {:files [".zprintrc" + "LICENSE.md" + "docs/img/automaton_duck.png" + ".github/workflows/deploy_clojar.yml" + ".github/workflows/commit_validation.yml"]}} diff --git a/project.edn b/project.edn deleted file mode 100644 index f657774..0000000 --- a/project.edn +++ /dev/null @@ -1,4 +0,0 @@ -{:app-name "auto-js" - :deps {:excluded-libs - [{:doc "0.16.3 has deprecated warnings, fix is already in main merged but not released yet" - :name "metosin/malli"}]}} diff --git a/src/clj/auto_js/sim/js/xchart.clj b/src/clj/auto_js/sim/js/xchart.clj deleted file mode 100644 index da101be..0000000 --- a/src/clj/auto_js/sim/js/xchart.clj +++ /dev/null @@ -1,63 +0,0 @@ -(ns auto-js.sim.js.xchart - (:require - [com.hypirion.clj-xchart :as c])) - -(defn- canonical-xchart - [tb-var] - (let [v (->> tb-var - :deltas - vec - (sort-by first))] - {:x (map first v) - :style {:marker-type :none} - :y (map second v)})) - -(defn products-input-output - [model-synthesis _params] - (c/xy-chart (-> {"Products in" [:entities-in "vals"] - "Products out" [:entities-out "vals"]} - (update-vals (fn [path] - (-> model-synthesis - (get-in path) - canonical-xchart)))) - {:title "Products input output"})) - -(defn products-nb - [model-synthesis _params] - (c/xy-chart (-> {"Throughputs" (-> (get-in model-synthesis [:entities-in "vals"]) - canonical-xchart)}) - {:title "Products number"})) - -(defn machine-occupation - [model-synthesis _params] - (c/xy-chart (-> (:resources model-synthesis) - (update-vals (fn [v] - (-> v - (get-in [:occupation-ma "vals"]) - canonical-xchart)))) - {:title "Machine occupation" - :render-style :area - :legend :outside-e})) - -(defn input-stock - [model-synthesis _params] - (c/category-chart (c/transpose-map (-> (:resources model-synthesis) - (update-vals (fn [v] - (-> v - (get-in [:nb-in-stock]) - (select-keys - ["stddev" "max" "mean"])))))) - {:title "Input stock"})) - -(defn entity-throughput - [model-synthesis _params] - (c/category-chart {"Time in workshop" (-> (get-in model-synthesis [:entities :throughputs]) - canonical-xchart)} - {:title "Entity time in workshop" - :legend {:visible? false}})) - -(defn build-charts - [chart-builders model-synthesis params] - (->> chart-builders - (mapv (fn [f] (f model-synthesis params))) - (apply c/view))) diff --git a/src/cljc/auto_js/js.cljc b/src/cljc/auto_js/js.cljc new file mode 100644 index 0000000..da9d6a2 --- /dev/null +++ b/src/cljc/auto_js/js.cljc @@ -0,0 +1,138 @@ +(ns auto-js.js + "A jobshop problem is stated with such a map: + + ``` + {:job-nb-ops [3 3] + :op-machine [0 1 2 0 1 2] + :op-processtime [9 9 4 16 4 15]}``` + + As this namespace is extensively executed, it is optimized. Immutability is turned off and results are updated in place, it means that these functions are updating \"in place\" their arguments with `aset-...`." + (:refer-clojure :exclude [compile]) + (:require + [clojure.string :as str])) + +(defn compile + "Turns data of a jobshop instance into arrays, as expected by the rest of the functions below" + [{:keys [op-processtime job-nb-ops op-machine] + :as js-pb}] + (let [op-processtimeI (int-array op-processtime) + op-nb (alength op-processtimeI) + job-nb (count job-nb-ops) + job-ops (->> job-nb-ops + (reductions + 0)) + m-nb (count (distinct op-machine))] + (-> js-pb + (assoc :job-conjI (int-array (repeat job-nb -1)) + :job-nb job-nb + :job-opI (int-array job-ops) + :m-nb m-nb + :mach-disjI (int-array (repeat m-nb -1)) + :op-jobsI (->> job-nb-ops + (interleave (range)) + (partition 2) + (mapcat (fn [[k v]] (repeat v k))) + int-array) + :op-machineI (int-array op-machine) + :op-nb op-nb + :op-processtimeI op-processtimeI)))) + +(defn- is-IntArray-name? + [kw] + (= (-> kw + str + last) + \I)) + +(defn clojurify + "Turns `js-pb` into idiomatic clojure datastructures" + [js-pb] + (->> js-pb + (mapv (fn [[k v]] [k (if (is-IntArray-name? k) (vec v) v)])) + (into {}))) + +(defn create-representation + "A representation is a mutable set of data" + [{:keys [op-nb] + :as _js-pb}] + {:topoI (int-array (repeat op-nb 0))}) + +(defn create-solution + [js-pb] + (let [{:keys [op-nb]} js-pb] + {:op-start-timeI (int-array (repeat op-nb 0)) + :op-prevI (int-array (repeat op-nb -1))})) + +(defn canonical-repetition + "Build the canonical repetitionI representation, + + All operations of the first job, then all operations of second job, ..." + [representation + {:keys [job-nb-ops] + :as _js-pb}] + (-> representation + (assoc :repetitionI + (->> job-nb-ops + (interleave (range)) + (partition 2) + (mapcat (fn [[n machines]] (repeat machines n))) + int-array)))) + +(defn decode-rep + "Turns `representation` - a representation with repetitionI - into a topoIlogical order. + + `topoI` mutably updated with that result" + [representation js-pb] + (let [{:keys [job-opI op-nb]} js-pb + {:keys [repetitionI topoI]} representation + job-firstI (aclone job-opI)] + (loop [iop 0] + (when (< iop op-nb) + (let [job (aget repetitionI iop) + o (aget job-firstI job)] + (aset job-firstI job (inc o)) + (aset topoI iop o) + (recur (inc iop))))) + representation)) + +(defn evaluate-schedule + "Turns a topological order into a schedule" + [representation solution compiled-js-pb] + (let [{:keys [job-conjI mach-disjI op-machineI op-nb op-jobsI op-processtimeI]} compiled-js-pb + {:keys [topoI]} representation + {:keys [op-start-timeI op-prevI]} solution + job-conjI (aclone job-conjI) + mach-disjI (aclone mach-disjI)] + (loop [n 0] + (let [np1 (inc n) + o (aget topoI n) + m (aget op-machineI o) + j (aget op-jobsI o) + os (aget op-start-timeI o)] + (let [od (aget mach-disjI m)] + (when (>= od 0) + (let [de (+ (aget op-start-timeI od) (aget op-processtimeI od))] + (when (> de os) (aset op-prevI o od) (aset op-start-timeI o de))))) + (let [oc (aget job-conjI j)] + (when (>= oc 0) + (let [je (+ (aget op-start-timeI oc) (aget op-processtimeI oc))] + (when (> je os) (aset op-prevI o oc) (aset op-start-timeI o je))))) + (aset mach-disjI m o) + (aset job-conjI j o) + (when (< np1 op-nb) (recur np1)))) + solution)) + +(defn to-mermaid + "Generates the diagram." + [{:keys [op-start-timeI] + :as _solution} + {:keys [op-processtimeI op-machineI op-jobsI] + :as _compiled-js-pb}] + (let [pref (fn [& s] (str " " (str/join " " s)))] + (->> + (for [o (range (count op-start-timeI))] + (pref (str "o" o "-m" (aget op-machineI o) "-j" (aget op-jobsI o)) + " :" (aget op-start-timeI o) + "," (+ (aget op-start-timeI o) (aget op-processtimeI o)))) + (concat + ["---" "displayMode: compact" "---" "gantt" (pref "title" "jobshop") (pref "dateFormat x")]) + (str/join "\n")))) diff --git a/src/cljc/auto_js/rendering/printer.cljc b/src/cljc/auto_js/rendering/printer.cljc new file mode 100644 index 0000000..927750b --- /dev/null +++ b/src/cljc/auto_js/rendering/printer.cljc @@ -0,0 +1,57 @@ +(ns auto-js.rendering.printer + "A printer rendering displaying the list of machines, their stats, which products are in processing or in input stock." + (:require + [auto-core.string.format :refer [f pf pfln]] + [auto-opti.routings :refer [machines]] + [clojure.string :as str])) + +(defn- rate [val all] (if (and (number? val) (pos? all)) (* 100.0 (/ val all)) 0.0)) + +(defn- occupation-s + [machine bucket] + (let [{:keys [cumulated-time machine-start] + :or {cumulated-time 0}} + machine + updated-cumulated + (if machine-start (+ cumulated-time (- bucket machine-start)) cumulated-time)] + (f "%3.2f%%" (rate updated-cumulated bucket)))) + +(defn- wip-machine-s + [machine] + (let [{:keys [machine-start machine-end]} machine] + (if machine-start (f "(%3s -> %3s)" (or machine-start "") (or machine-end "")) "( ________ )"))) + +(defn- stock-occupation [machine bucket] (f "%3.2f%%" (rate (:stock-occupation machine) bucket))) + +(defn separator [] (println (apply str (repeat 80 "*")))) + +(defn print-workshop + [model print-event-fn] + (let [{:keys [entities bucket it resources stats past-events]} model] + (separator) + (pfln "it=%3d, bucket= %3d, #product finished %-5d" + it + bucket + (or (->> stats + (mapcat second) + count) + 0)) + (print-event-fn (first past-events)) + (doseq [m (machines model)] + (let [machine (get resources m) + {:keys [input-stock prod-started]} + (->> entities + (map (fn [[entity-id entity]] (assoc entity :entity-id entity-id))) + (filter (fn [entity] (= m (:m (:current-operation entity))))) + (group-by :step))] + (if prod-started + (pf "%4s %4s %12s (%7s) | " + (name m) + (str/join ", " (mapv #(f "%4s" (:entity-id %)) prod-started)) + (wip-machine-s machine) + (occupation-s machine bucket)) + (pf "%4s (%7s) | " (name m) (occupation-s machine bucket))) + (pf "%s" (stock-occupation machine bucket)) + (if input-stock + (pfln "<<- %10s" (str/join ", " (mapv #(f "%4s" (:entity-id %)) input-stock))) + (println)))))) diff --git a/src/cljc/auto_js/rep/repetition.cljc b/src/cljc/auto_js/rep/repetition.cljc new file mode 100644 index 0000000..aafa024 --- /dev/null +++ b/src/cljc/auto_js/rep/repetition.cljc @@ -0,0 +1,62 @@ +(ns auto-js.rep.repetition) + +(defn canonical + "Build the canonical repetition representation, + + All operations of first operation, then all operations of second operation, ..." + [{:keys [jobs] + :as _js-pb}] + (->> jobs + (mapcat (fn [[job-id job-ops]] (repeat (count job-ops) job-id))))) + +(defn topological-order + "Turns the representation with repetition into a topological order" + [_js-pb rep] + (reduce (fn [_v _job] nil) {} rep) + #_(loop [rep rep + js-pb js-pb + decoded-rep []] + (if rep + (let [job (first rep)] + (recur (next rep) + (assoc js-pb job (next job-sequence)) + (conj decoded-rep (assoc (first job-sequence) :n job)))) + decoded-rep))) + +{:jobs {:J1 [0 1 2] + :J2 [3 4 5]} + :machines {:M0 [0 3] + :M1 [4] + :M11 [1] + :M2 [2 5]} + :name :test-instance + :operations [{:id 0 + :j :J1 + :m :M0 + :p 9 + :pos 0} + {:id 1 + :j :J1 + :m :M11 + :p 9 + :pos 1} + {:id 2 + :j :J1 + :m :M2 + :p 4 + :pos 2} + {:id 3 + :j :J2 + :m :M0 + :p 16 + :pos 0} + {:id 4 + :j :J2 + :m :M1 + :p 4 + :pos 1} + {:id 5 + :j :J2 + :m :M2 + :p 15 + :pos 2}]} diff --git a/src/cljc/auto_js/sim/js.cljc b/src/cljc/auto_js/sim/js.cljc index 4ced8e8..bbd58c1 100644 --- a/src/cljc/auto_js/sim/js.cljc +++ b/src/cljc/auto_js/sim/js.cljc @@ -10,13 +10,17 @@ ;; Simulation runner ;; ******************************************************************************** +(defn prepare + [model] + (-> model + (sim-activity/start (xoroshiro128 (:seed model))) + sim-activity/add-iteration-past-event + sim-activity/errors)) + (defn run "Run the jobshop model described in `model` until it `it-stop`" [model it-stop] - (loop [model (-> model - (sim-activity/start (xoroshiro128 (:seed model))) - sim-activity/add-iteration-past-event - sim-activity/errors)] + (loop [model model] (if (seq (:errors model)) model (let [{:keys [it bucket]} model diff --git a/src/cljc/auto_js/start.cljc b/src/cljc/auto_js/start.cljc index b4cff4c..84887ab 100644 --- a/src/cljc/auto_js/start.cljc +++ b/src/cljc/auto_js/start.cljc @@ -1,8 +1,7 @@ (ns auto-js.start (:require - [auto-js.sim.activity :as sim-activity] - [auto-js.sim.js :as sim-js] - [auto-js.sim.js.xchart :as sim-js-xchart])) + [auto-js.sim.activity :as sim-activity] + [auto-js.sim.js :as sim-js])) ;; ******************************************************************************** ;; Tests @@ -48,18 +47,21 @@ :m4 {}} :seed #uuid "e85427c1-ed25-4ed4-9b11-52238d268265"}) -(def model (sim-js/run data 30000)) +(def model + (-> data + sim-js/prepare + (sim-js/run 30000))) (comment (-> (sim-js/run data 55) sim-activity/print-output) (sim-js/synthesis model) - (sim-js-xchart/build-charts [sim-js-xchart/products-input-output - sim-js-xchart/products-nb - sim-js-xchart/machine-occupation - sim-js-xchart/input-stock - sim-js-xchart/entity-throughput] - (sim-js/synthesis model) - {}) - ; + ;;(require '[auto-js.sim.js.xchart :as sim-js-xchart]) + ;; (sim-js-xchart/build-charts [sim-js-xchart/products-input-output + ;; sim-js-xchart/products-nb + ;; sim-js-xchart/machine-occupation + ;; sim-js-xchart/input-stock + ;; sim-js-xchart/entity-throughput] + ;; (sim-js/synthesis model) + ;; {}) ) diff --git a/test/unit/cljc/auto_js/js_test.cljc b/test/unit/cljc/auto_js/js_test.cljc new file mode 100644 index 0000000..623a766 --- /dev/null +++ b/test/unit/cljc/auto_js/js_test.cljc @@ -0,0 +1,77 @@ +(ns auto-js.js-test + (:require + [auto-js.js :as sut] + #?(:clj [clojure.test :refer [deftest is]] + :cljs [cljs.test :refer [deftest is] :include-macros true]))) + +(def js-pb-example + "Data from the exercise" + (-> {:job-nb-ops [3 3] + :op-machine [0 1 2 0 1 2] + :op-processtime [5 6 7 8 9 10]} + sut/compile)) + +(deftest clojurify-test + (is (= {:job-conjI [-1 -1] + :job-nb 2 + :job-nb-ops [3 3] + :job-opI [0 3 6] + :m-nb 3 + :mach-disjI [-1 -1 -1] + :op-jobsI [0 0 0 1 1 1] + :op-machine [0 1 2 0 1 2] + :op-machineI [0 1 2 0 1 2] + :op-nb 6 + :op-processtime [5 6 7 8 9 10] + :op-processtimeI [5 6 7 8 9 10]} + (sut/clojurify js-pb-example)))) + +(deftest create-representation-test + (is (= {:topoI [0 0 0 0 0 0]} + (-> js-pb-example + sut/create-representation + sut/clojurify)))) + +(deftest create-solution-test + (is (= {:op-start-timeI [0 0 0 0 0 0] + :op-prevI [-1 -1 -1 -1 -1 -1]} + (-> js-pb-example + sut/create-solution + sut/clojurify)))) + +(deftest canonical-repetition-test + (is (= {:topoI [0 0 0 0 0 0] + :repetitionI [0 0 0 1 1 1]} + (-> (sut/create-representation js-pb-example) + (sut/canonical-repetition js-pb-example) + sut/clojurify)))) + +(deftest decode-rep-test + (is (= [0 3 1 4 2 5] + (-> {:topoI [0 0 0 0 0 0] + :repetitionI [0 1 0 1 0 1]} + (update :topoI int-array) + (update :repetitionI int-array) + (sut/decode-rep js-pb-example) + :topoI + vec))) + (is (= [0 1 2 3 4 5] + (-> (sut/create-representation js-pb-example) + (sut/canonical-repetition js-pb-example) + (sut/decode-rep js-pb-example) + :topoI + vec)))) + +(deftest evaluate-schedule-test + (is (-> (sut/create-representation js-pb-example) + (sut/canonical-repetition js-pb-example) + (sut/decode-rep js-pb-example) + (sut/evaluate-schedule (sut/create-solution js-pb-example) js-pb-example) + sut/clojurify))) + +#?(:clj (spit "tmp/test.mermaid" + (-> (sut/create-representation js-pb-example) + (sut/canonical-repetition js-pb-example) + (sut/decode-rep js-pb-example) + (sut/evaluate-schedule (sut/create-solution js-pb-example) js-pb-example) + (sut/to-mermaid js-pb-example)))) diff --git a/test/unit/cljc/auto_js/rep/repetition_test.cljc b/test/unit/cljc/auto_js/rep/repetition_test.cljc new file mode 100644 index 0000000..922f249 --- /dev/null +++ b/test/unit/cljc/auto_js/rep/repetition_test.cljc @@ -0,0 +1,62 @@ +(ns auto-js.rep.repetition-test + #_(:require + ;;[auto-js.pb-data.js-pb :refer [js-pb]] + ;; [auto-js.pb-data.map-ops :refer [operations]] + ;; [auto-js.rep.repetition :as sut] + ;; #?(:clj [clojure.test :refer [deftest is]] + ;; :cljs [cljs.test :refer [deftest is] :include-macros true]) + )) + +;; (deftest canonical-test +;; (is (= [:J1 :J1 :J1 :J2 :J2 :J2] +;; (-> (operations {:J1 [{:p 9 +;; :m :M0} +;; {:p 9 +;; :m :M11} +;; {:p 4 +;; :m :M2}] +;; :J2 [{:p 16 +;; :m :M0} +;; {:p 4 +;; :m :M1} +;; {:p 15 +;; :m :M2}]}) +;; (js-pb :test-instance) +;; (sut/topological-order [:J1 :J1 :J1 :J2 :J2 :J2]) +;; sut/canonical)) +;; "Canonical")) +;; (deftest decode-rep-test +;; (is +;; (= +;; [{:p 9 +;; :m :M0 +;; :n :J1} +;; {:p 9 +;; :m :M11 +;; :n :J1} +;; {:p 4 +;; :m :M2 +;; :n :J1} +;; {:p 16 +;; :m :M0 +;; :n :J2} +;; {:p 4 +;; :m :M1 +;; :n :J2} +;; {:p 15 +;; :m :M2 +;; :n :J2}] +;; (-> (operations {:J1 [{:p 9 +;; :m :M0} +;; {:p 9 +;; :m :M11} +;; {:p 4 +;; :m :M2}] +;; :J2 [{:p 16 +;; :m :M0} +;; {:p 4 +;; :m :M1} +;; {:p 15 +;; :m :M2}]}) +;; (js-pb :test-instance) +;; (sut/topological-order [:J1 :J1 :J1 :J2 :J2 :J2]))))) diff --git a/test/unit/cljc/auto_js/sim/activity_test.cljc b/test/unit/cljc/auto_js/sim/activity_test.cljc new file mode 100644 index 0000000..0d8faee --- /dev/null +++ b/test/unit/cljc/auto_js/sim/activity_test.cljc @@ -0,0 +1,586 @@ +(ns auto-js.sim.activity-test + (:require + [auto-js.sim.activity :as sut] + [auto-opti.prng :as opt-prng] + #?(:clj [clojure.test :refer [deftest is]] + :cljs [cljs.test :refer [deftest is] :include-macros true]))) + +(def u #uuid "0b0a9886-3530-4c55-9330-e0a49111d45b") + +(defn clojurify + "Remove all non clojure datastructure" + [model] + (cond-> (dissoc model :route-dstb) + (:stats model) (update-in [:stats] update-vals #(into {} %)) + (:entity-sources model) + (update :entity-sources + update-vals + (fn [entity-source] (update entity-source :waiting-time #(if (number? %) % (some? %))))) + (get-in model [:stats :resources]) + (update-in [:stats :resources] + update-vals + (fn [resource-stat] + (cond-> resource-stat + (:nb-in-stock resource-stat) (update :nb-in-stock (partial into {})) + (:occupation resource-stat) (update :occupation (partial into {}))))) + (get-in model [:stats :routes]) (update-in [:stats :routes] + update-vals + (fn [route-stat] + (cond-> route-stat + (:route-nb route-stat) + (update :route-nb (partial into {}))))))) + +;; ******************************************************************************** +;; ** Start +;; ******************************************************************************** + +(deftest start-test + (is (= {:bucket 0 + :entities {} + :entity-sources {} + :errors [] + :it 0 + :past-events [] + :resources {} + :route-dstb true + :routes {}} + (-> {} + (sut/start (opt-prng/xoroshiro128 u)) + (update :route-dstb some?))) + "No entity source to start") + (is (= {:bucket 0 + :entities {} + :entity-sources {:product {:next-event 0 + :entity-source-id :product + :nb-max 1 + :waiting-time 2 + :nb-entity 0}} + :errors [] + :it 0 + :past-events [] + :resources {} + :routes {}} + (-> {:entity-sources {:product {:waiting-time 2}}} + (sut/start nil) + clojurify)) + "A simple entity-source")) + +(deftest errors-test + (is (= {:errors [{:error-id :no-source-id}]} + (-> {} + sut/errors)) + "No entity source to errors") + (is (= {:entity-sources {:product {}}} + (-> {:entity-sources {:product {}}} + sut/errors)) + "A simple entity-source")) + +;; ******************************************************************************** +;; ** Entitites +;; ******************************************************************************** + +(deftest create-new-entity-test + (is (= {:model {:errors [{:error-id :create-unkown-entity-id + :entity-source-id nil + :known-ids []}]}} + (sut/create-new-entity {} nil)) + "Creation of an unknown entity raises an error") + (is + (= + {:model {:bucket 3 + :entities {"product-1" {:entity-id "product-1" + :starts 3}} + :entity-sources {:product {:entity-source-id :product + :nb-entity 1 + :nb-max 2 + :next-event 16 + :waiting-time true}} + :errors [] + :it 0 + :past-events [{:bucket 3 + :entity-id "product-1" + :event-id :create-new-entity + :entity-source-id :product + :nb-entity 1 + :nb-max 2 + :waiting-time 13}] + :resources {} + :routes {} + :stats {:entities-in {:deltas {3 1}} + :entities-nb {:deltas {3 1}}}} + :entity {:entity-id "product-1" + :starts 3}} + (-> {:entity-sources {:product {:next-event 666 + :nb-max 2 + :waiting-time {:dstb-name :normal-integer + :scale 2 + :location 13} + :nb-entity 0}} + :bucket 3} + (sut/start (opt-prng/xoroshiro128 u)) + (sut/create-new-entity :product) + (update :model clojurify))) + "Create an entity, some remains") + (is (= {:model {:bucket 3 + :entities {"product-1" {:entity-id "product-1" + :starts 3}} + :entity-sources {} + :errors [] + :it 0 + :past-events [{:bucket 3 + :entity-id "product-1" + :event-id :create-new-entity + :entity-source-id :product + :last? true + :nb-entity 1 + :nb-max 1}] + :resources {} + :routes {} + :stats {:entities-in {:deltas {3 1}} + :entities-nb {:deltas {3 1}}}} + :entity {:entity-id "product-1" + :starts 3}} + (-> {:entity-sources {:product {:next-event 666 + :nb-max 1 + :waiting-time {:dstb-name :normal-integer + :scale 2 + :location 13}}} + :bucket 3} + (sut/start (opt-prng/xoroshiro128 u)) + (sut/create-new-entity :product) + (update :model clojurify))) + "Create the last entity")) + +(deftest destroy-entity-test + (is (= {:bucket 0 + :entities {:a 1 + :b 2} + :errors [{:error-id :unkown-entity-id + :during :destroy-entity + :entity-id :not-existing + :known-ids [:a :b]}]} + (-> {:bucket 0 + :entities {:a 1 + :b 2}} + (sut/destroy-entity {:entity-id :not-existing}))) + "Destroy a non existing entity") + (is (= {:entities {:b 2} + :bucket 3 + :stats {:entities {:throughputs [0]} + :entities-nb {:deltas {3 -1}} + :entities-out {:deltas {3 1}}} + :past-events [{:event-id :destroy-entity + :entity-id :a + :bucket 3}]} + (-> {:entities {:a 1 + :b 2} + :bucket 3} + (sut/destroy-entity {:entity-id :a + :starts 3}) + clojurify)) + "Destroy an existing entity")) + +(deftest next-event-bucket-test + (is (nil? (sut/next-event-bucket {})) "Empty model has no next bucket") + (is (= 12 + (sut/next-event-bucket {:entity-sources {:tst {:next-event 12} + :tst2 {:next-event 13}}})))) + +;; ******************************************************************************** +;; ** Route +;; ******************************************************************************** + +(deftest end-route-test + (is (= {:errors [{:error-id :route-ends-was-not-started + :entity {}}]} + (sut/end-route {} {})) + "Not started entity raise an error when ended") + (is (= {:entities {} + :bucket 112 + :past-events [{:event-id :destroy-entity + :entity-id :e + :bucket 112} + {:event-id :end-route + :entity-id :e + :bucket 112}] + :stats {:routes {:a {:throughputs [100]}} + :entities {:throughputs [100]} + :entities-nb {:deltas {112 -1}} + :entities-out {:deltas {112 1}}}} + (-> {:entities {:e {:foo :bar}} + :bucket 112} + (sut/end-route {:starts 12 + :entity-id :e + :route-id :a}) + clojurify)) + "Ending a route is deleting the entity, and updating stats")) + +(deftest next-op-test + (is (= {:entities {} + :bucket 12 + :past-events [{:event-id :destroy-entity + :entity-id :e + :bucket 12} + {:event-id :end-route + :entity-id :e + :bucket 12}] + :stats {:routes {:r1 {:throughputs [9]}} + :entities {:throughputs [9]} + :entities-nb {:deltas {12 -1}} + :entities-out {:deltas {12 1}}}} + (-> {:entities {:e {}} + :bucket 12} + (sut/next-op {:entity-id :e + :route-id :r1 + :starts 3}) + clojurify)) + "When no next-ops, it is ending the route") + (is (= {:entities {:e1 {:entity-id :e1 + :route-id :r1 + :next-ops [{:e :f}] + :current-operation {:c :d} + :starts 3}} + :bucket 44 + :past-events [{:event-id :next-op + :current-operation {:c :d} + :entity-id :e1 + :bucket 44}]} + (-> {:bucket 44} + (sut/next-op {:entity-id :e1 + :route-id :r1 + :next-ops [{:c :d} {:e :f}] + :current-operation {:a :b} + :starts 3}) + clojurify)) + "next-op with 2 remaining operations")) + +(deftest start-route-test + (is (= {:bucket 2 + :routes {:route-a {:operations [{:a :b} {:c :d}]}} + :stats {:routes {:route-a {:route-nb {:deltas {2 1}}}}} + :past-events [{:event-id :next-op + :bucket 2 + :entity-id :a + :current-operation {:a :b}} + {:event-id :start-route + :bucket 2 + :entity-id :a + :route-id :route-a}] + :entities {:a {:route-id :route-a + :entity-id :a + :current-operation {:a :b} + :next-ops [{:c :d}]}}} + (-> {:bucket 2 + :routes {:route-a {:operations [{:a :b} {:c :d}]}}} + (sut/start-route {:entity-id :a} :route-a) + clojurify)) + "A normal operation to start") + (is (= {:errors [{:error-id :route-not-existing + :route-id :non-existing-route-id}]} + (-> {} + (sut/start-route {} :non-existing-route-id))) + "Non existing route creates an error")) + +(deftest on-new-product-test + (is + (= + {:bucket 12 + :entities {"p-1" {:entity-id "p-1" + :route-id :a + :starts 12 + :next-ops nil + :step :in-production + :current-operation {:m :m1 + :pt 1}}} + :entity-sources {:p {:next-event 14 + :entity-source-id :p + :waiting-time 2 + :nb-entity 1 + :nb-max 2}} + :errors [] + :it 0 + :past-events [{:bucket 12 + :entity-id "p-1" + :event-id :create-new-entity + :entity-source-id :p + :nb-entity 1 + :nb-max 2 + :waiting-time 2} + {:event-id :start-route + :bucket 12 + :entity-id "p-1" + :route-id :a} + {:event-id :next-op + :entity-id "p-1" + :current-operation {:m :m1 + :pt 1} + :bucket 12} + {:event-id :enter-production + :bucket 12 + :entity-id "p-1" + :starts 12 + :ends 13 + :current-operation {:m :m1 + :pt 1} + :machine {:starts 12 + :ends 13 + :resource-id :m1 + :entity-id "p-1" + :next-event 13}}] + :resources {:m1 {:starts 12 + :ends 13 + :entity-id "p-1" + :resource-id :m1 + :next-event 13}} + :routes {:a {:route-id :a + :operations [{:m :m1 + :pt 1}]}} + :stats {:entities-nb {:deltas {12 1}} + :entities-in {:deltas {12 1}} + :routes {:a {:route-nb {:deltas {12 1}}}} + :resources {:m1 {:occupation {:deltas {12 1}}}}}} + (-> {:bucket 12 + :resources {:m1 {}} + :entity-sources {:p {:next-event 12 + :nb-max 2 + :waiting-time 2 + :entity-source-id :p}} + :routes {:a {:operations [{:m :m1 + :pt 1}]}}} + (sut/start nil) + sut/on-new-product + clojurify)) + "Creates a new product. Entity source is not ended.") + (is (= {:bucket 15 + :entity-sources {:p {:next-event 12}} + :errors [{:error-id :next-event-missed + :entity-source-id nil}]} + (-> {:bucket 15 + :entity-sources {:p {:next-event 12}}} + sut/on-new-product)) + "Inconsistency detected") + (is (nil? (-> {:bucket 1 + :entity-sources {:p {:next-event 12}}} + sut/on-new-product)) + "Creation is for later on")) + + +;; ******************************************************************************** +;; ** Machine +;; ******************************************************************************** + +(deftest enter-production-test + (is (= {:errors [{:error-id :current-operation-missing + :entity {}}]} + (-> {} + (sut/enter-production {}))) + "Need a current operation") + (is (= {:resources {:m1 {:starts 10}} + :errors [{:error-id :machine-is-busy-already + :machine {:starts 10}}]} + (-> {:resources {:m1 {:starts 10}}} + (sut/enter-production {:current-operation {:m :m1}}))) + "Can't start if machine is already busy") + (is (= {:resources {:m1 {:starts 12 + :foo :bar + :entity-id :e1 + :ends 13 + :next-event 13}} + :bucket 12 + :past-events [{:bucket 12 + :starts 12 + :ends 13 + :entity-id :e1 + :event-id :enter-production + :machine {:starts 12 + :ends 13 + :foo :bar + :entity-id :e1 + :next-event 13} + :current-operation {:m :m1 + :pt 1}}] + :stats {:resources {:m1 {:occupation {:deltas {12 1}}}}} + :entities {:e1 {:step :in-production}}} + (-> {:resources {:m1 {:foo :bar}} + :bucket 12} + (sut/enter-production {:current-operation {:m :m1 + :pt 1} + :entity-id :e1}) + (update-in [:stats :resources :m1 :occupation] #(into {} %)))) + "Starts on an available machine")) + +(deftest enter-input-stock-test + (is (= {:errors [{:error-id :no-current-operation + :entity {}}]} + (-> {} + (sut/enter-input-stock {}))) + "No current operation is an error") + (is + (= + {:bucket 0 + :entities {:e1 {:step :in-production}} + :entity-sources {:product {:entity-source-id :product + :nb-entity 0 + :nb-max 1 + :next-event 0 + :waiting-time true}} + :errors [] + :it 0 + :past-events [{:bucket 0 + :current-operation {:m :m1 + :pt 1} + :entity-id :e1 + :event-id :enter-production + :starts 0 + :ends 1 + :machine {:starts 0 + :resource-id :m1 + :ends 1 + :entity-id :e1 + :next-event 1}}] + :resources {:m1 {:starts 0 + :ends 1 + :resource-id :m1 + :entity-id :e1 + :next-event 1}} + :routes {} + :stats {:resources {:m1 {:occupation {:deltas {0 1}}}}}} + (-> {:resources {:m1 {}} + :entity-sources {:product {}}} + (sut/start nil) + (sut/enter-input-stock {:current-operation {:m :m1 + :pt 1} + :waiting-time true + :entity-id :e1}) + (dissoc :route-dstb) + (update-in [:entity-sources :product :waiting-time] some?) + (update-in [:stats :resources :m1 :occupation] #(into {} %)))) + "If no production is in progress, starts directly the production") + (is (= {:bucket 0 + :it 0 + :entity-sources {} + :entities {:e1 {:step :wait-in-input-stock}} + :errors [] + :past-events [{:bucket 0 + :current-operation {:m :m1} + :entity-id :e1 + :event-id :enter-input-stock-wait-in-stock + :machine {:starts 1 + :resource-id :m1 + :waiting-products [:e1]}}] + :resources {:m1 {:starts 1 + :resource-id :m1 + :waiting-products [:e1]}} + :routes {} + :stats {:resources {:m1 {:nb-in-stock {:deltas {0 1}}}}}} + (-> {:resources {:m1 {:starts 1}}} + (sut/start nil) + (sut/enter-input-stock {:current-operation {:m :m1} + :entity-id :e1}) + clojurify)) + "Wait in stock if a production is in progress")) + +(deftest ends-production-test + (is (= {:errors [{:error-id :no-current-operation + :entity {:entity-id :e1}}]} + (-> {} + (sut/ends-production {:entity-id :e1}))) + "No current operation raise an error") + (is (= {:bucket 1 + :entities {} + :past-events [{:event-id :destroy-entity + :entity-id :e1 + :bucket 1} + {:bucket 1 + :entity-id :e1 + :event-id :end-route} + {:bucket 1 + :current-operation {:m :m1} + :entity-id :e1 + :event-id :ends-production + :machine {}}] + :resources {:m1 {}} + :stats {:routes {:r1 {:throughputs [1]}} + :entities-nb {:deltas {1 -1}} + :entities-out {:deltas {1 1}} + :entities {:throughputs [1]} + :resources {:m1 {:occupation {:deltas {1 -1}}}}}} + (-> {:bucket 1 + :entities {:e1 {:step :foo + :entity-id :e1}} + :resources {:m1 {:starts 1 + :ends 2 + :next-event 2}}} + (sut/ends-production {:current-operation {:m :m1} + :entity-id :e1 + :route-id :r1 + :starts 0}) + clojurify)) + "Stops production of `e1` on `m1`, no product waits in `m1` stock") + (is + (= + {:bucket 4 + :entities {:e2 {:step :in-production + :current-operation {:m :m1 + :pt 3} + :entity-id :e2}} + :past-events [{:bucket 4 + :current-operation {:m :m1 + :pt 3} + :starts 4 + :ends 7 + :entity-id :e2 + :event-id :enter-production + :machine {:waiting-products [:e3] + :starts 4 + :entity-id :e2 + :ends 7 + :next-event 7}} + {:event-id :destroy-entity + :entity-id :e1 + :bucket 4} + {:event-id :end-route + :entity-id :e1 + :bucket 4} + {:bucket 4 + :current-operation {:m :m1} + :entity-id :e1 + :event-id :ends-production + :machine {:waiting-products [:e2 :e3]}}] + :resources {:m1 {:waiting-products [:e3] + :starts 4 + :entity-id :e2 + :ends 7 + :next-event 7}} + :stats {:routes {:r1 {:throughputs [3]}} + :entities-nb {:deltas {4 -1}} + :entities-out {:deltas {4 1}} + :entities {:throughputs [3]} + :resources {:m1 {:occupation {:deltas {4 0}} + :nb-in-stock {:deltas {4 -1}}}}}} + (-> {:bucket 4 + :entities {:e1 {:step :foo} + :e2 {:step :bar + :current-operation {:m :m1 + :pt 3} + :entity-id :e2}} + :resources {:m1 {:starts 1 + :ends 2 + :waiting-products [:e2 :e3] + :next-event 2}}} + (sut/ends-production {:entity-id :e1 + :route-id :r1 + :starts 1 + :current-operation {:m :m1}}) + clojurify)) + "Stops production of `e1` on `m1` with product `e2` waiting in the stock of that machine and ready to start")) + +(deftest resource-next-event-test + (is (nil? (-> {} + (sut/resource-next-event))) + "No next event") + (is (= 13 + (-> {:resources {:a {:next-event 13} + :b {:next-event 15}}} + (sut/resource-next-event))) + "Next resource event is 13, the smallest date"))