Commit 02a3dea7 authored by Bruno Burke's avatar Bruno Burke 😁

Merge branch 'converge-calculation-and-multistep-calculation-exercise' into develop

parents 6dd5cae3 27308077
(ns lernmeister.components.exercise-types.calculation.check
(:require [clojure.string :as string]
(:require [lernmeister.components.exercise-types.calculation.migrate-answer
:refer
[answer-scheme-version migrate-answer]]
[lernmeister.components.exercise-types.check :as e-check]
[clojure.tools.reader.edn :as edn]
[lernmeister.components.helper :refer [math-abs]]))
(def prefix-mapping {"Y" 24
"Z" 21
"E" 18
"P" 15
"T" 12
"G" 9
"M" 6
"k" 3
"h" 2
"d" -1
"c" -2
"m" -3
"µ" -6
"n" -9
"p" -12
"f" -15
"a" -18
"z" -21
"y" -24
"" 0
nil 0})
(def si-unit-mapping {"m" [{:factor 1 :oom 0 :unit "m" :expo 1}]
"g" [{:factor 1 :oom -3 :unit "kg" :expo 1}]
"s" [{:factor 1 :oom 0 :unit "s" :expo 1}]
"A" [{:factor 1 :oom 0 :unit "A" :expo 1}]
"K" [{:factor 1 :oom 0 :unit "K" :expo 1}]
"mol" [{:factor 1 :oom 0 :unit "mol" :expo 1}]
"cd" [{:factor 1 :oom 0 :unit "cd" :expo 1}]
"rad" []
"sr" []
"Hz" [{:factor 1 :oom 0 :unit "s" :expo -1}]
"N" [{:factor 1 :oom 0 :unit "m" :expo 1}
{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -2}]
"Pa" [{:factor 1 :oom 0 :unit "m" :expo -1}
{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -2}]
"J" [{:factor 1 :oom 0 :unit "m" :expo 2}
{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -2}]
"W" [{:factor 1 :oom 0 :unit "m" :expo 2}
{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -3}]
"C" [{:factor 1 :oom 0 :unit "s" :expo 1}
{:factor 1 :oom 0 :unit "A" :expo 1}]
"V" [{:factor 1 :oom 0 :unit "m" :expo 2}
{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -3}
{:factor 1 :oom 0 :unit "A" :expo -1}]
"F" [{:factor 1 :oom 0 :unit "m" :expo -2}
{:factor 1 :oom 0 :unit "kg" :expo -1}
{:factor 1 :oom 0 :unit "s" :expo 4}
{:factor 1 :oom 0 :unit "A" :expo 2}]
"Ω" [{:factor 1 :oom 0 :unit "m" :expo 2}
{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -3}
{:factor 1 :oom 0 :unit "A" :expo -2}]
"S" [{:factor 1 :oom 0 :unit "m" :expo -2}
{:factor 1 :oom 0 :unit "kg" :expo -1}
{:factor 1 :oom 0 :unit "s" :expo 3}
{:factor 1 :oom 0 :unit "A" :expo 2}]
"Wb" [{:factor 1 :oom 0 :unit "m" :expo 2}
{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -2}
{:factor 1 :oom 0 :unit "A" :expo -2}]
"T" [{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -2}
{:factor 1 :oom 0 :unit "A" :expo -1}]
"H" [{:factor 1 :oom 0 :unit "m" :expo 2}
{:factor 1 :oom 0 :unit "kg" :expo 1}
{:factor 1 :oom 0 :unit "s" :expo -2}
{:factor 1 :oom 0 :unit "A" :expo -2}]
"lm" [{:factor 1 :oom 0 :unit "cd" :expo 1}]
"lx" [{:factor 1 :oom 0 :unit "m" :expo -2}
{:factor 1 :oom 0 :unit "cd" :expo 1}]
"Bq" [{:factor 1 :oom 0 :unit "s" :expo -1}]
"Gy" [{:factor 1 :oom 0 :unit "m" :expo 2}
{:factor 1 :oom 0 :unit "s" :expo -2}]
"Sv" [{:factor 1 :oom 0 :unit "m" :expo 2}
{:factor 1 :oom 0 :unit "s" :expo -2}]
"kat" [{:factor 1 :oom 0 :unit "s" :expo -1}
{:factor 1 :oom 0 :unit "mol" :expo 1}]
"h" [{:factor 3.6 :oom 3 :unit "s" :expo 1}]
"d" [{:factor 8.64 :oom 4 :unit "s" :expo 1}]
"l" [{:factor 1 :oom -1 :unit "m" :expo 3}]})
(def additional-unit-mappings {"B" [{:factor 1 :oom 1 :unit "dB" :expo 1}]
"°" [{:factor 1 :oom 0 :unit "°" :expo 1}]})
(def base-unit-set #{"m" "kg" "s" "A" "K" "mol" "cd" "dB" "°"})
(def unit-replace-map {"kg" {:unit "g" :prefix "k"}
"dB" {:unit "B" :prefix "d"}})
(def unit-replace-set (set (keys unit-replace-map)))
(def base-unit-map (reduce (fn [res-map base-unit] (assoc res-map (keyword base-unit) 0)) {} base-unit-set))
(def unit-mapping (merge si-unit-mapping additional-unit-mappings))
(def prefix-set (set (keys prefix-mapping)))
(def unit-set (set (keys unit-mapping)))
(defn not-empty-or-minus [number]
((every-pred #((complement empty?) %) #(not= "-" %)) number))
(defn get-number-part [{:keys [number order-of-magnitude]} with-oom target result]
(when (not-empty-or-minus number)
(let [parsed-number (edn/read-string (string/replace number "," "."))]
(if with-oom
(when (not-empty-or-minus order-of-magnitude)
(let [parsed-oom (edn/read-string order-of-magnitude)]
(assoc result target {:number parsed-number :oom parsed-oom})))
(assoc result target {:number parsed-number :oom 0})))))
(defn reduce-to-base-units [outer-multiplier result base-unit]
(let [multiplier (* outer-multiplier (:expo base-unit))
unit-key (keyword (:unit base-unit))
unit-factor (:factor base-unit)
unit-oom (:oom base-unit)]
(-> result
(update unit-key + multiplier)
(update :factor * (Math/pow unit-factor multiplier))
(update :oom + (* multiplier unit-oom)))))
(defn reduce-unit-mapping [result-map unit]
(let [parsed-expo (edn/read-string (:expo unit))
prefix-oom (get prefix-mapping (:prefix unit))
calculated-oom (* parsed-expo prefix-oom)]
(let [reduce-fn (fn [result base-unit]
((partial reduce-to-base-units parsed-expo) result base-unit))]
(reduce reduce-fn
(update result-map :oom + calculated-oom)
(get unit-mapping (:unit unit))))))
(defn reduce-units [units]
(let [result-map (merge base-unit-map {:factor 1 :oom 0})]
(reduce reduce-unit-mapping
result-map
units)))
(defn get-units-part [units with-units target result]
(if with-units
(let [reduced-units (reduce-units units)
base-units (select-keys reduced-units
(reduce (fn [res base-unit] (conj res (keyword base-unit))) [] base-unit-set))
units-factor (:factor reduced-units)
units-oom (:oom reduced-units)]
(update result target merge {:base-units base-units :units-factor units-factor :units-oom units-oom}))
result))
(defn compare-base-units [with-units result]
(if with-units
(let [calc-units (get-in result [:calc :base-units])
ans-units (get-in result [:ans :base-units])]
(when (= calc-units ans-units)
result))
[lernmeister.components.exercise-types.multistep-calculation.check
:refer
[check-phys-vals tolerable-error?]]
[lernmeister.components.helper :refer [migrate-if-necessary]]))
(defn get-calculation-ids [exercise]
(reduce (fn [res item] (conj res (:id item)))
#{} (get-in exercise [:core :calculations])))
(defn no-negative-points [result]
(if (neg? (:points result))
(assoc result :points 0)
result))
(defn compare-numbers [with-oom with-units error-factor result]
(let [get-combined-number (fn [oom-key]
(let [number (get-in result [oom-key :number])]
(if with-units
(let [units-factor (get-in result [oom-key :units-factor])]
(* number units-factor))
number)))
get-combined-oom (fn [oom-key]
(let [units-oom (get-in result [oom-key :units-oom] 0)]
(if with-oom
(let [oom (get-in result [oom-key :oom])]
(+ oom units-oom))
units-oom)))]
(let [calc-number (get-combined-number :calc)
ans-number (get-combined-number :ans)
calc-oom (get-combined-oom :calc)
ans-oom (get-combined-oom :ans)
oom-difference (- calc-oom ans-oom)
calc-multiplier (Math/pow 10 oom-difference)]
(when (<= (math-abs (- ans-number (* calc-multiplier calc-number)))
(math-abs (* error-factor ans-number)))
true))))
(defn check-answer [cur-calc cur-ans & {:keys [with-oom with-units]}]
(let [error-factor (/ (:error-relative cur-calc 1) 100)] ;;is it okay to keep this as ratio-datatype?
(some->> {}
(get-number-part cur-calc with-oom :calc)
(get-number-part cur-ans with-oom :ans)
(get-units-part (:units cur-calc) with-units :calc)
(get-units-part (:units cur-ans) with-units :ans)
(compare-base-units with-units)
(compare-numbers with-oom with-units error-factor))))
(defn check-calculation [id ans-obj ex-obj res-map with-oom with-units]
(let [phys-val-check-res (check-phys-vals ex-obj ans-obj :with-oom with-oom :with-units with-units)
is-correct? (tolerable-error? (:calculation-error phys-val-check-res)
(or (:error-relative ex-obj) 1))]
(assoc res-map id {:correct is-correct?
:points (if is-correct? (:correct-points ex-obj) (:incorrect-points ex-obj))
:points-max (:correct-points ex-obj)})))
(defn calculation-check [exercise answer callback]
(let [no-negative-points (fn [result]
(if (neg? (:points result))
(assoc result :points 0)
result))]
(let [core (:core exercise)
calculations (:calculations core)
with-oom (:with-oom core)
with-units (:with-units core)]
(callback)
(->>
(map
(fn [calculation]
(let [cur-correct-points (:correct-points calculation)
cur-incorrect-points (:incorrect-points calculation)
cur-id (:id calculation)
cur-answer (get answer cur-id)]
(if (and
cur-answer
(check-answer calculation cur-answer :with-oom with-oom :with-units with-units))
{:points cur-correct-points :points-max cur-correct-points :correct-id cur-id}
{:points cur-incorrect-points :points-max cur-correct-points})))
calculations)
(reduce
(fn [result cur-result]
(let [update-correct-calculations (fn [result]
(if (contains? cur-result :correct-id)
(update result :correct-calculations conj (:correct-id cur-result))
result))]
(->
result
(update :points + (:points cur-result))
(update :points-max + (:points-max cur-result))
update-correct-calculations)))
{:points 0 :points-max 0 :correct-calculations #{}})
no-negative-points))))
(let [answer-calculations (:calculations answer)
calculations (get-in exercise [:core :calculations])
with-oom (get-in exercise [:core :with-oom])
with-units (get-in exercise [:core :with-units])
res-map (reduce (fn [r-map calculation]
(let [id (:id calculation)
answer (get answer-calculations id)]
(check-calculation id answer calculation r-map with-oom with-units)))
{} calculations)
update-correct-calcs (fn [result id calculation-check]
(if (:correct calculation-check)
(update result :correct-calculations conj id)
result))]
(callback)
(->> res-map
(reduce-kv (fn [result k v] (-> result
(update :points + (:points v))
(update :points-max + (:points-max v))
(update-correct-calcs k v)))
{:points 0 :points-max 0 :correct-calculations #{}})
no-negative-points)))
(defmethod e-check/check-answer :calculation [exercise answer callback]
(calculation-check exercise answer callback))
(let [calculation-ids (get-calculation-ids exercise)
migrated-answer (migrate-if-necessary answer :answer-scheme "calculation"
answer-scheme-version (partial migrate-answer calculation-ids))]
(calculation-check exercise migrated-answer callback)))
......@@ -11,7 +11,7 @@
(partial modify-exercise-field-vec exercise [:core :calculations]))
(defn set-missing-error-relative [exercise]
((modify-calculations exercise) #(assoc % :error-relative (or (:error-relative %) "1"))))
((modify-calculations exercise) #(assoc % :error-relative (or (:error-relative %) 1))))
(defn convert-numbers [exercise]
((modify-calculations exercise) convert-number-to-object))
......
......@@ -9,9 +9,6 @@
(defn set-current-scheme [answer]
(assoc answer :answer-scheme {:type "calculation" :version answer-scheme-version}))
(defn remove-ooms [answer]
(update answer :calculations (fn [calcs] (migrate-object #(dissoc % :order-of-magnitude) calcs))))
(defn convert-calculation-numbers [answer]
(update answer :calculations (fn [calcs] (migrate-object convert-number-to-object calcs))))
......@@ -32,5 +29,4 @@
wrap-answer-calculations
(add-missing-calculation-objects calculation-ids)
convert-calculation-numbers
remove-ooms
set-current-scheme))
(ns lernmeister.components.exercise-types.calculation.spec
(:require #?(:cljs [cljs.spec.alpha :as s]
:clj [clojure.spec.alpha :as s])
[lernmeister.components.exercise-types.calculation.check :refer [prefix-set unit-set]]))
[lernmeister.components.exercise-types.multistep-calculation.check :refer [prefix-set unit-set]]))
(s/def :calculation/name (s/and string? #(not (empty? %))))
(s/def :calculation/number (s/and string? #(re-matches #"^\-?((((0\,)|([1-9]\d*\,?))\d*)|0)$" %)))
......@@ -27,4 +27,3 @@
:exercise/task-description :exercise/shuffled]))
(s/def ::calculation-question (s/keys :req-un [:calculation-question/core]))
......@@ -12,7 +12,7 @@
[answer-scheme-version migrate-answer]]
[lernmeister.components.helper
:refer
[math-abs migrate-if-necessary]])]
[double-division math-abs migrate-if-necessary]])]
:cljs
[(:require
[clojure.string :as clj-str :refer [join split trim]]
......@@ -23,7 +23,7 @@
[answer-scheme-version migrate-answer]]
[lernmeister.components.helper
:refer
[math-abs migrate-if-necessary]])]))
[double-division math-abs migrate-if-necessary]])]))
(def prefix-mapping {"Y" 24
"Z" 21
......@@ -169,10 +169,14 @@
:factor (:factor based-units-map)
:oom (:oom based-units-map)}))
(defn tolerable-error? [calculation-error error-relative]
(when (every? #(and (number? %) (not (neg? %))) [calculation-error error-relative])
(<= (* calculation-error 100) error-relative)))
(defn convert-phys-val-to-ref-units [phys-val ref-units]
(let [phys-val-base-unit-map (units-to-baseunits (:units phys-val))
ref-base-unit-map (units-to-baseunits ref-units)
convert-factor (/ (:factor phys-val-base-unit-map) (:factor ref-base-unit-map))
convert-factor (double-division (:factor phys-val-base-unit-map) (:factor ref-base-unit-map))
convert-oom (- (:oom phys-val-base-unit-map) (:oom ref-base-unit-map))
significand (edn/read-string (get-in phys-val [:number :significand]))
oom (edn/read-string (get-in phys-val [:number :order-of-magnitude]))]
......@@ -294,14 +298,13 @@
ans-significand (get-combined-significand :ans)
calc-oom (get-combined-oom :calc)
ans-oom (get-combined-oom :ans)
oom-difference (- calc-oom ans-oom)
calc-multiplier (Math/pow 10 oom-difference)]
(when (<=
(math-abs (- ans-significand (* calc-multiplier calc-significand)))
(math-abs (* (/ 1 100) ans-significand)))
true)))
(defn check-answer [cur-calc cur-ans & {:keys [with-oom with-units]}]
oom-difference (- ans-oom calc-oom)
ans-multiplier (Math/pow 10 oom-difference)]
(assoc result :calculation-error (math-abs (double-division
(- calc-significand (* ans-significand ans-multiplier))
calc-significand)))))
(defn check-phys-vals [cur-calc cur-ans & {:keys [with-oom with-units]}]
(some->> {}
(get-number-part cur-calc with-oom :calc)
(get-number-part cur-ans with-oom :ans)
......@@ -368,12 +371,13 @@
(defmethod check-step :constant [_ {:keys [ans-id ans-obj ex-obj assignment res-map]}]
(let [update-res-map (fn [points] (assoc res-map ans-id {:points points :points-max 4}))]
(if (and (get-in ans-obj [:number :significand])
(check-answer
{:number (:number ex-obj) :units (:units assignment)}
(select-keys ans-obj [:number :units])
:with-oom true :with-units true))
(update-res-map 4)
(if (get-in ans-obj [:number :significand])
(let [phys-val-check-res (check-phys-vals {:number (:number ex-obj) :units (:units assignment)}
(select-keys ans-obj [:number :units])
:with-oom true :with-units true)]
(if (tolerable-error? (:calculation-error phys-val-check-res) 1)
(update-res-map 4)
(update-res-map 2)))
(update-res-map 2))))
(defmethod check-step :formula [_ {:keys [ans-id ans-obj ex-obj assignment param-id-map ex-id-map
......@@ -389,11 +393,11 @@
(if-not (some (comp nil? val) match-var-map)
(let [calc-mapping (:calc-mapping assignment)
formula-vec (formula-to-vec (:formula assignment))
calc-number (mathjs-str-to-number-obj (calc-step formula-vec calc-mapping match-var-map))]
(if (check-answer
{:number calc-number :units (:units assignment)}
(select-keys ans-obj [:number :units])
:with-oom true :with-units true)
calc-number (mathjs-str-to-number-obj (calc-step formula-vec calc-mapping match-var-map))
phys-val-check-res (check-phys-vals {:number calc-number :units (:units assignment)}
(select-keys ans-obj [:number :units])
:with-oom true :with-units true)]
(if (tolerable-error? (:calculation-error phys-val-check-res) 1)
(update-res-map 6)
(update-res-map 4)))
(update-res-map 2))))
......
(ns lernmeister.components.exercise-types.multistep-calculation.migrate
(:require [clojure.tools.reader.edn :as edn]
(:require [clojure.string :as string]
[clojure.tools.reader.edn :as edn]
[lernmeister.components.helper :refer [find-in-vec-by-id]]))
(def exercise-scheme-version "2019-11-22")
......@@ -9,6 +10,11 @@
(assoc-in [:authoring :migration-status] :current)
(assoc :exercise-scheme {:type type :version version})))
(defn normalize-decimal-separator [number-string]
(if (string? number-string)
(string/replace number-string #"," ".")
number-string))
(defn migrate-object [migrate-fn obj]
(reduce-kv (fn [res-obj obj-k obj-v] (assoc res-obj obj-k (migrate-fn obj-v))) {} obj))
......@@ -26,7 +32,7 @@
(defn convert-number-obj [obj]
(if (contains? obj :number)
(let [significand (:number obj)
(let [significand (normalize-decimal-separator (:number obj))
order-of-magnitude (or (:order-of-magnitude obj) "0")]
{:significand significand :order-of-magnitude order-of-magnitude})
obj))
......@@ -34,7 +40,7 @@
(defn convert-number-to-object [obj]
(if (contains? obj :number)
(if-not (map? (:number obj))
(let [significand (:number obj)
(let [significand (normalize-decimal-separator (:number obj))
order-of-magnitude (or (:order-of-magnitude obj) "0")]
(-> obj
(dissoc :order-of-magnitude)
......
......@@ -11,6 +11,11 @@
:cljs
(js/console.debug message)))
(defn double-division
"returns double-value in any case"
[divident divisor]
(/ (double divident) divisor))
(defn math-abs
"returns math-abs-val for clj&cljs"
[value]
......
......@@ -28,7 +28,7 @@
:number {:significand nil :order-of-magnitude "0"}
:correct-points 0
:incorrect-points 0
:error-relative "1"})
:error-relative 1})
(defn set-prop-in-all-calcs [prop-key value calc-cursor]
(dorun (map-indexed
......
(ns lernmeister.components.exercise-types.calculation.views.helper
(:require [clojure.string :refer [trim replace join]]
[lernmeister.components.exercise-types.calculation.check
:refer [prefix-mapping prefix-set unit-set not-empty-or-minus]]
[reagent.core :as reagent]
[reagent.dom :as rdom]
[lernmeister.components.ui :as ui]
[lernmeister.components.helper :refer [vec-remove]]))
(defn parse-unit [string]
(when (string? string)
(when-let [matched-string (re-matches #"^[a-zA-ZΩ°]*" string)]
(replace (subs matched-string 0 3) #"Ohm|ohm" "Ω"))))
(defn parse-prefix [string]
(when (string? string)
(when-let [replaced-string (replace string "u" "µ")]
(let [last-char (or (last replaced-string) "")]
(when (contains? prefix-set last-char) last-char)))))
(defn parse-float [string]
(when (string? string)
(when-let [[match sign int-part float-part] (re-matches #"^(\-?)(\d*)([\.|,]?\d*)$" string)]
(if-let [[match stripped-int-part] (re-matches #"^0*(\d+)$" int-part)]
(str sign stripped-int-part (replace float-part "." ","))
(if (= float-part "") sign (str sign "0" (replace float-part "." ",")))))))
(defn parse-int [string]
(when (string? string)
(when-let [[match sign int-part] (re-matches #"^(\-?)(\d*)$" string)]
(if-let [[match stripped-int-part] (re-matches #"^0*(\d+)$" int-part)]
(str sign stripped-int-part)
(str sign)))))
(defn append-to-string-vec [arg append-string]
(update arg :string-vec conj append-string))
(defn add-number [arg]
(let [number (:number arg)]
(when (not-empty-or-minus number)
(if (re-matches #"^\-?0,?0*$" number)
(-> arg
(assoc :number "0")
(append-to-string-vec "0"))
(append-to-string-vec arg (-> number
(replace #",$" "")
(replace "," "{,}")))))))
(defn add-oom [arg]
(let [number (:number arg)
oom (:order-of-magnitude arg)
with-oom (:with-oom arg)]
(if with-oom
(when (not-empty-or-minus oom)
(letfn [(build-oom-string [oom number] (if (or (= "0" number) (re-matches #"^\-?0?$" oom))
""
(str "\\cdot 10^{" oom "}")))]
(append-to-string-vec arg (build-oom-string oom number))))
(append-to-string-vec arg ""))))
(defn append-unit-str [frac-map position unit expo]
(update frac-map position conj (if (= "1" expo) unit (str unit "^{" expo "}"))))
(defn join-with-sep [fraction-map map-key]
(join "{\\cdot}" (map-key fraction-map)))
(defn latex-replace-unit [unit]
(-> unit
(replace "Ω" "\\Omega ")