Frustratingly close to working, which is another way of saying not working at all.
This commit is contained in:
parent
33dcbf3f9c
commit
45f6b972c7
|
@ -4,5 +4,6 @@
|
||||||
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
|
: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/"}
|
:url "https://www.eclipse.org/legal/epl-2.0/"}
|
||||||
:dependencies [[org.clojure/clojure "1.11.1"]
|
:dependencies [[org.clojure/clojure "1.11.1"]
|
||||||
[org.clojure/clojure "1.12.0"]]
|
[org.clojure/clojure "1.12.0"]
|
||||||
|
[com.taoensso/timbre "4.10.0"]]
|
||||||
:repl-options {:init-ns arboretum.dengine.core})
|
:repl-options {:init-ns arboretum.dengine.core})
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
(ns arboretum.dengine.case
|
(ns arboretum.dengine.case
|
||||||
(:require [arboretum.dengine.protocols :refer [Case]]))
|
(:require [arboretum.dengine.protocols :refer [Case]]))
|
||||||
|
|
||||||
(defrecord CaseImpl [ id name knowledge]
|
(defrecord CaseImpl [case-id case-name knowledge]
|
||||||
Case
|
Case
|
||||||
(id [this] :foo);;(:id this))
|
(authority [this feature-id] (:authority ((:knowledge this) feature-id)))
|
||||||
(name [this] "foo") ;;(or (:name this) (clojure.core/name (:id this))))
|
(case-id [this] (:case-id this))
|
||||||
(knowledge [this] (:knowledge this))
|
(case-name [this] (or (:case-name this) (clojure.core/name (:case-id this)))))
|
||||||
(is? [this feature-id] ((:knowledge this) feature-id))
|
|
||||||
)
|
(defn make-case
|
||||||
|
([id name]
|
||||||
|
(make-case id name {}))
|
||||||
|
([id name knowledge]
|
||||||
|
(CaseImpl. id name knowledge)))
|
|
@ -1,14 +1,38 @@
|
||||||
(ns arboretum.dengine.core
|
(ns arboretum.dengine.core
|
||||||
;; (:require [arboretum.dengine.feature :refer [decide explain]]
|
;; (:require [arboretum.dengine.feature :refer [decide explain]]
|
||||||
;; [arboretum.dengine.protocols :refer [proposition]])
|
;; [arboretum.dengine.protocols :refer [proposition]])
|
||||||
)
|
|
||||||
|
|
||||||
(defn foo
|
(:require
|
||||||
"I don't do a whole lot."
|
[arboretum.dengine.case :refer [make-case]]
|
||||||
[x]
|
[arboretum.dengine.feature :refer [make-feature]]
|
||||||
(println x "Hello, World!"))
|
[arboretum.dengine.kb :refer [!kb]]
|
||||||
|
[arboretum.dengine.utils :refer [string->keyword]])
|
||||||
|
|
||||||
|
(:import [arboretum.dengine.case CaseImpl]))
|
||||||
|
|
||||||
;; (defn is?
|
;; (defn is?
|
||||||
;; [feature case kb]
|
;; [feature case kb]
|
||||||
;; (let [evaluation (decide feature case kb)]
|
;; (let [evaluation (decide feature case kb)]
|
||||||
;; ((proposition feature) evaluation)))
|
;; ((proposition feature) evaluation)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn add-case!
|
||||||
|
"Add a case to the knowledge base with this `name`, and, if supplied, this `id`."
|
||||||
|
([^String name]
|
||||||
|
(add-case! name (string->keyword name (set (map #(:case-id %) (.cases @!kb))))))
|
||||||
|
([^String name ^clojure.lang.Keyword id]
|
||||||
|
(let [c (make-case id name)]
|
||||||
|
(swap! !kb assoc-in [:cases id] c)
|
||||||
|
c))
|
||||||
|
([^String name ^clojure.lang.Keyword id ^clojure.lang.PersistentArrayMap knowledge]
|
||||||
|
(let [c (make-case id name knowledge)]
|
||||||
|
(swap! !kb assoc-in [:cases id] c)
|
||||||
|
c)))
|
||||||
|
|
||||||
|
(defn add-feature!
|
||||||
|
([^String proposition default rootnode]
|
||||||
|
(add-feature! proposition (string->keyword proposition (set (map #(:feature-id %) (.features @!kb)))) default rootnode))
|
||||||
|
([^String proposition ^clojure.lang.Keyword id default rootnode]
|
||||||
|
(let [f (make-feature proposition id default rootnode)]
|
||||||
|
(swap! !kb assoc-in [:features id] f)
|
||||||
|
f)))
|
||||||
|
|
33
src/arboretum/dengine/dengine.clj
Normal file
33
src/arboretum/dengine/dengine.clj
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
(ns arboretum.dengine.dengine
|
||||||
|
(:require
|
||||||
|
[arboretum.dengine.kb :refer [!kb find-case-for find-feature-for remember!]]
|
||||||
|
[arboretum.dengine.protocols :refer [Case]]
|
||||||
|
[taoensso.timbre :as timbre :refer [log]]))
|
||||||
|
|
||||||
|
|
||||||
|
(defn dengine [feat c]
|
||||||
|
(let [c' (if (satisfies? Case c) c (-> @!kb :cases c))
|
||||||
|
f' (find-feature-for @!kb feat)]
|
||||||
|
;; (log (format "Inferring value of `%s` for `%s`"
|
||||||
|
;; (:case-id c') (:feature-id f')))
|
||||||
|
(when (nil? c') (throw (ex-info "Unknown or invalid case"
|
||||||
|
{:case-identifier c})))
|
||||||
|
(when (:rootnode f')
|
||||||
|
(first (remove nil? (map #(.evaluate % c)
|
||||||
|
(:children (:rootnode f'))))))))
|
||||||
|
|
||||||
|
(defn decide-feat [feat c]
|
||||||
|
(let [c' (find-case-for @!kb c)
|
||||||
|
f' (find-feature-for @!kb feat)
|
||||||
|
v (.knowledge @!kb (.case-id c') (.feature-id f'))
|
||||||
|
k (cond v v
|
||||||
|
(:rootnode f') (dengine f' c')
|
||||||
|
:else {:value (:default f')
|
||||||
|
:authority :default
|
||||||
|
:reason (format
|
||||||
|
"I assumed %s was %s of %s."
|
||||||
|
(:name f')
|
||||||
|
(:default f')
|
||||||
|
(.name c'))})]
|
||||||
|
(remember! f' c' k)
|
||||||
|
(:value k)))
|
|
@ -1,35 +1,25 @@
|
||||||
(ns arboretum.dengine.feature
|
(ns arboretum.dengine.feature
|
||||||
(:require [arboretum.dengine.kb :refer [!kb]]
|
(:require
|
||||||
[arboretum.dengine.protocols :refer [Case Feature Node]]
|
[arboretum.dengine.dengine :refer [decide-feat]]
|
||||||
[arboretum.dengine.utils :refer [string->keyword]]
|
[arboretum.dengine.protocols :refer [Feature]]
|
||||||
[clojure.spec.alpha :as spec]))
|
[arboretum.dengine.utils :refer [string->keyword]]))
|
||||||
|
|
||||||
(spec/def :arboretum.dengine.feature/name string?)
|
;; (spec/def :arboretum.dengine.feature/name string?)
|
||||||
|
|
||||||
(spec/def :arboretum.dengine.feature/default boolean?)
|
;; (spec/def :arboretum.dengine.feature/default boolean?)
|
||||||
|
|
||||||
(defn- dengine [this c]
|
|
||||||
(let [c' (if (satisfies? Case c) c (-> @!kb :cases c))]
|
|
||||||
(when (nil? c') (throw (ex-info "Unknown or invalid case"
|
|
||||||
{:case-identifier c})))
|
|
||||||
(if (:rootnode this)
|
|
||||||
(when-not (satisfies? Node (:rootnode this))
|
|
||||||
(throw (ex-info "Invalid DTree node"
|
|
||||||
{:node (:rootnode this)
|
|
||||||
:context this})))
|
|
||||||
(first
|
|
||||||
(filter boolean?
|
|
||||||
(map #(.evaluate % c)
|
|
||||||
(-> this :rootnode :children)))))))
|
|
||||||
|
|
||||||
(defrecord FeatureImpl [^String proposition ^boolean default rootnode]
|
(defrecord FeatureImpl [^String proposition ^clojure.lang.Keyword feature-id ^boolean default rootnode]
|
||||||
Feature
|
Feature
|
||||||
(decide [this c] (let [v ((.id this) c)]
|
(decide [this c] (decide-feat this c))
|
||||||
(cond (boolean? v) v
|
|
||||||
(:rootnode this) (dengine (:rootnode this) c)
|
|
||||||
:else (:default this))))
|
|
||||||
(default [this] (:default this))
|
(default [this] (:default this))
|
||||||
(id [this] (string->keyword (:proposition this)))
|
(feature-id [this] (string->keyword (:proposition this)))
|
||||||
(rootnode [this] (:rootnode this))
|
(rootnode [this] (:rootnode this))
|
||||||
(proposition [this] (:proposition this)))
|
(proposition [this] (:proposition this)))
|
||||||
|
|
||||||
|
(defn make-feature
|
||||||
|
"A convenience function to make features."
|
||||||
|
([proposition default rootnode]
|
||||||
|
(make-feature proposition (string->keyword proposition) default rootnode))
|
||||||
|
([proposition id default rootnode]
|
||||||
|
(FeatureImpl. proposition id default rootnode)))
|
|
@ -1,44 +1,78 @@
|
||||||
(ns arboretum.dengine.kb
|
(ns arboretum.dengine.kb
|
||||||
(:require
|
(:require
|
||||||
arboretum.dengine.case
|
[arboretum.dengine.protocols :refer [Case Feature KB]]
|
||||||
[arboretum.dengine.protocols :refer [Case Feature Kb]]
|
[clojure.pprint :refer [pprint]]
|
||||||
[arboretum.dengine.utils :refer [string->keyword]]
|
[taoensso.timbre :as timbre :refer [log trace debug info warn error fatal report
|
||||||
[clojure.pprint :refer [pprint]])
|
logf tracef debugf infof warnf errorf fatalf reportf
|
||||||
(:import
|
spy]]))
|
||||||
[arboretum.dengine.case CaseImpl]))
|
|
||||||
|
|
||||||
(defrecord KBImpl [cases features]
|
|
||||||
Kb
|
;; (timbre/set-ns-min-level! :info)
|
||||||
|
|
||||||
|
(defn find-case-for [kb c]
|
||||||
|
(cond (satisfies? Case c) c
|
||||||
|
(keyword? c) (-> kb :cases c)
|
||||||
|
:else (throw (ex-info (format "Unexpected value `%s` passed for case" c)
|
||||||
|
{:case c
|
||||||
|
:type (type c)}))))
|
||||||
|
|
||||||
|
(defn find-feature-for [kb f]
|
||||||
|
(cond (satisfies? Feature f) f
|
||||||
|
(keyword? f) (-> kb :features f)
|
||||||
|
:else (throw (ex-info (format "Unexpected value `%s` passed for feature" f)
|
||||||
|
{:case f
|
||||||
|
:type (type f)}))))
|
||||||
|
|
||||||
|
(defn do-is? [kb case-or-id feature-or-id]
|
||||||
|
(let [c (find-case-for kb case-or-id)
|
||||||
|
f (find-feature-for kb feature-or-id)]
|
||||||
|
(-> c :knowledge (:feature-id f) :value)))
|
||||||
|
|
||||||
|
(defrecord KBImpl [^clojure.lang.APersistentMap cases ^clojure.lang.APersistentMap features]
|
||||||
|
KB
|
||||||
(cases [this] (:cases this))
|
(cases [this] (:cases this))
|
||||||
(features [this] (:features this))
|
(features [this] (:features this))
|
||||||
(get-case [this case-id] (-> this :cases case-id))
|
(get-case [this case-id] (-> this :cases case-id))
|
||||||
(get-feature [this feature-id] (-> this :features feature-id))
|
(get-feature [this feature-id] (-> this :features feature-id))
|
||||||
(is? [this case-or-id feature-or-id]
|
(is? [this case-or-id feature-or-id]
|
||||||
(let [c (if (satisfies? Case case-or-id)
|
(do-is? this case-or-id feature-or-id))
|
||||||
case-or-id (-> this :cases case-or-id))
|
(knowledge [this case-or-id feature-or-id]
|
||||||
f (if (satisfies? Feature feature-or-id) feature-or-id
|
(let [c (find-case-for this case-or-id)
|
||||||
(-> this :features feature-or-id))]
|
f (find-feature-for this feature-or-id)]
|
||||||
(-> c (:id f)))))
|
(-> c :knowledge (:feature-id f)))))
|
||||||
|
|
||||||
(def !kb
|
(def !kb
|
||||||
"The one instance of the knowledge base."
|
"The one instance of the knowledge base."
|
||||||
(atom (KBImpl. nil nil)))
|
(atom (KBImpl. {} {})))
|
||||||
|
|
||||||
(defn add-case
|
(defn remember!
|
||||||
"Add a case to the knowledge base with this `name`, and, if supplied, this `id`."
|
"Remember this `value` for the feature with this `feature-id` of the case
|
||||||
([^String name]
|
with this `case-id`. Return the `value`."
|
||||||
(add-case name (string->keyword name (map :id (.cases @!kb)))))
|
[case-id feature-id value]
|
||||||
([^String name ^clojure.lang.Keyword id]
|
;; (log (format "Remembering case `%s`, feature `%s`, value `%s`"
|
||||||
(let [c (CaseImpl. id name {})]
|
;; case-id feature-id value))
|
||||||
(swap! !kb update-in :cases conj c))))
|
(cond
|
||||||
|
(boolean? value) (swap! !kb assoc-in
|
||||||
|
[:cases case-id :knowledge feature-id :value] value)
|
||||||
|
(map? value) (swap! !kb assoc-in
|
||||||
|
[:cases case-id :knowledge feature-id] value)
|
||||||
|
:else (throw (ex-info "Unexpected value?" {:case-id case-id
|
||||||
|
:feature-id feature-id
|
||||||
|
:value value})))
|
||||||
|
value)
|
||||||
|
|
||||||
(defn persist [filepath]
|
(defn persist [filepath]
|
||||||
(let [p (if (empty? filepath) "kb.edn" filepath)]
|
(let [p (if (empty? filepath) "kb.edn" filepath)]
|
||||||
(spit p (with-out-str (pprint @!kb)))))
|
;; (info (format "Persisting knowledge base with %d cases, %d features to `%s`"
|
||||||
(defn restore [filepath]
|
;; (count (:cases @!kb)) (count (:features @!kb)) filepath))
|
||||||
|
(spit p (with-out-str (pprint @!kb)))))
|
||||||
|
|
||||||
|
(defn restore [filepath]
|
||||||
(let [p (if (empty? filepath) "kb.edn" filepath)
|
(let [p (if (empty? filepath) "kb.edn" filepath)
|
||||||
kb (read-string (slurp p))]
|
kb (read-string (slurp p))]
|
||||||
(if (satisfies? Kb kb) (reset! !kb kb)
|
;; (info (format "Read knowledge base with %d cases, %d features from `%s`"
|
||||||
|
;; (count (:cases kb)) (count (:features kb)) filepath))
|
||||||
|
(if (satisfies? KB kb) (reset! !kb kb)
|
||||||
(throw (ex-info "Not a knowledge base?" {:filepath filepath
|
(throw (ex-info "Not a knowledge base?" {:filepath filepath
|
||||||
:kb kb
|
:kb kb
|
||||||
:type (type kb)})))))
|
:type (type kb)})))))
|
|
@ -1,12 +1,81 @@
|
||||||
(ns arboretum.dengine.node
|
(ns arboretum.dengine.node
|
||||||
(:require [arboretum.dengine.protocols :refer [Node]]
|
(:require
|
||||||
[clojure.spec.alpha :as spec]))
|
[arboretum.dengine.kb :refer [!kb]]
|
||||||
|
[arboretum.dengine.protocols :refer [Feature Node]]
|
||||||
|
[clojure.string :as string]))
|
||||||
|
|
||||||
(defrecord NodeImpl [children ^Boolean colour feature ^String fragment]
|
(defrecord NodeImpl [children ^Boolean colour feature ^String fragment]
|
||||||
Node
|
Node
|
||||||
(children [this] "The children of this node" (:children this))
|
(children [this] "The children of this node" (:children this))
|
||||||
(colour ^Boolean [this] "The colour of this node" (:colour this))
|
(colour ^Boolean [this] "The colour of this node" (:colour this))
|
||||||
(feature [this] "The feature at this node" (:feature this))
|
(feature [this] "The feature at this node" (:feature this))
|
||||||
(fragment ^String [this] "The explanation fragment at this node")
|
(fragment ^String [this] "The explanation fragment at this node"
|
||||||
(evaluate [this case-id] )
|
(:fragment this))
|
||||||
)
|
(evaluate [this case-id]
|
||||||
|
(let [c (-> !kb .cases case-id)
|
||||||
|
f (if (satisfies? Feature (:feature this)) (:feature this)
|
||||||
|
(-> !kb .features (:feature this)))
|
||||||
|
value (:value (.knowledge !kb c f))]
|
||||||
|
(when
|
||||||
|
value (if
|
||||||
|
(empty? (:children this))
|
||||||
|
;; If my feature value is true, and I have no
|
||||||
|
;; children, return my colour and my fragment
|
||||||
|
{:value (:colour this)
|
||||||
|
:authority :rule
|
||||||
|
:reason (:fragment this)}
|
||||||
|
;; but if I have some children...
|
||||||
|
(first (remove nil? (map #(.evaluate % c)
|
||||||
|
(:children this)))))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; Here's the algorithm as published in A Graphical Inference Mechanism,
|
||||||
|
;; but it's not real Interlisp code, and I remember there being at least
|
||||||
|
;; one error in it. My memory is that Peter felt real Interlisp wasn't
|
||||||
|
;; sufficiently academic to publish in an academic paper, so he translated
|
||||||
|
;; it into an 'idealised' Lisp, for which no implementation existed.
|
||||||
|
;; However this was based on my Interlisp code, and, as we no longer have
|
||||||
|
;; that, is the best we can currently do.
|
||||||
|
;;
|
||||||
|
;; I have to say, given this is certainly based on my code, it was a long
|
||||||
|
;; time ago and I'm not proud of it.
|
||||||
|
;;
|
||||||
|
;; (DSearch (object feature)
|
||||||
|
;; (LOCAL (node nodesToDo newNodes children stickNodes)
|
||||||
|
;; (SET ’nodesToDo (LIST (GET feature ’dTreeRootNode))
|
||||||
|
;; ;; {origin of the DTree}
|
||||||
|
;; (SET ’stickNodes NIL)
|
||||||
|
;; (LOOP
|
||||||
|
;; (UNTIL (EMPTY? nodesToDo))
|
||||||
|
;; {if no more to do, exit loop}
|
||||||
|
;; (SET’node (POP nodesToDo))
|
||||||
|
;; (SET’children (GET node ’children))
|
||||||
|
;; {get a node from nodesToDo and obtain its children. ..}
|
||||||
|
;; (SET’newNodes NIL)
|
||||||
|
;; ;; {..the children that can be accessed will be newNodes}
|
||||||
|
;; (LOOP
|
||||||
|
;; (UNTIL (EMPTY? children))
|
||||||
|
;; ;; {if all children have been done, exit this loop}
|
||||||
|
;; (SET child (POP children))
|
||||||
|
;; ;; {get the next child}
|
||||||
|
;; (IF (EQUAL
|
||||||
|
;; (DecideFeat
|
||||||
|
;; object
|
||||||
|
;; (GET child 'feature))
|
||||||
|
;; Yes)
|
||||||
|
;; ;; {Run the system to discover whether the feature of the child node is
|
||||||
|
;; ;; true of object}
|
||||||
|
;; (SET newNodes
|
||||||
|
;; (APPEND child newNodes))))
|
||||||
|
;; ;; {..if it is then the node can be accessed and is added to newNodes,
|
||||||
|
;; ;; and loop}
|
||||||
|
;; (IF (EMPTY? newNodes)
|
||||||
|
;; (SET ’stickNodes
|
||||||
|
;; (PUSH child stickNodes))
|
||||||
|
;; {if no new nodes were added the child is a stickNode}
|
||||||
|
;; (ELSE (PUSH newNodes nodesToDo)))))
|
||||||
|
;; ;; {otherwise the newNodes must themselves be examined for
|
||||||
|
;; ;; stickNodes}
|
||||||
|
;; (RETURN (Decide stickNodes))
|
||||||
|
;; {finally, make a decision on the basis of the stickNodes found}
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,14 @@
|
||||||
(defprotocol Case
|
(defprotocol Case
|
||||||
"A case to be decided. It represents an object-in-the-world about
|
"A case to be decided. It represents an object-in-the-world about
|
||||||
which knowledge may be held and enquiries may be made."
|
which knowledge may be held and enquiries may be made."
|
||||||
(id ^Keyword [this] "Returns the `:id` of this case.")
|
(authority [this feature-id] "Returns information about how the value of the
|
||||||
(name ^String [this] "Returns the name associated with this case")
|
feature identified be `feature-id` is known
|
||||||
(knowledge ^APersistentMap [this] "Returns a map whose keys are ids of features
|
about `this` case.
|
||||||
and whose values are the values of those
|
|
||||||
features in this case.")
|
At this stage, one of the keywords `:default`,
|
||||||
(is? ^Boolean [this feature-id] "Returns `true` if feature-id true of `this`,
|
`:inference`, `:user`.")
|
||||||
`false` if it's definitely not true, and
|
(case-id ^Keyword [this] "Returns the `:case-id` of this case.")
|
||||||
`nil` if its truth value is unknown."))
|
(case-name ^String [this] "Returns the name associated with this case"))
|
||||||
|
|
||||||
(defmacro case?
|
(defmacro case?
|
||||||
"Return `true` if this `obj` appears to be a valid case.
|
"Return `true` if this `obj` appears to be a valid case.
|
||||||
|
@ -28,29 +28,42 @@
|
||||||
`(satisfies? Case ~obj))
|
`(satisfies? Case ~obj))
|
||||||
|
|
||||||
(defprotocol Feature
|
(defprotocol Feature
|
||||||
"A feature has three essential elements: a proposition, a default value, and
|
"A feature has three essential elements: a proposition, a default
|
||||||
a method of decision"
|
value, and a method of decision"
|
||||||
(decide ^Boolean [this case] "Returns the default value of my proposition for this `case`.
|
(decide ^Boolean [this case] "Returns the default value of my
|
||||||
|
proposition for this `case`.
|
||||||
|
|
||||||
`case` should be either an object which satisfies the `Case`
|
`case` should be either an object which
|
||||||
protocol or else a keyword, in which case it should be the
|
satisfies the `Case` protocol or else a
|
||||||
|
keyword, in which case it should be the
|
||||||
key of some case in the global `kb`.")
|
key of some case in the global `kb`.")
|
||||||
|
;; it would be a sensible addition for Feature have an additional slot for
|
||||||
|
;; a decision function other than a DTree, so that a value could be fetched
|
||||||
|
;; from some algorithmic or online source. But Cedar didn't have this and
|
||||||
|
;; I am currently just reimplementing Cedar.
|
||||||
(default ^Boolean [this] "Returns the default value of my proposition")
|
(default ^Boolean [this] "Returns the default value of my proposition")
|
||||||
(id ^Keyword [this] "Returns a keywordised representation of the proposition of this feature")
|
(feature-id ^Keyword [this] "Returns a keywordised representation of the proposition
|
||||||
(proposition ^String [this] "Returns the proposition associated with this feature")
|
of this feature")
|
||||||
(rootnode [this] "Returns the root node of the dtree for this feature, if one exists, else `nil`"))
|
(rootnode [this] "Returns the root node of the dtree for this feature, if one
|
||||||
|
exists, else `nil`")
|
||||||
|
(proposition ^String [this] "Returns the proposition associated with this
|
||||||
|
feature"))
|
||||||
|
|
||||||
(defmacro feature?
|
(defmacro feature?
|
||||||
[obj]
|
[obj]
|
||||||
`(satisfies? Feature ~obj))
|
`(satisfies? Feature ~obj))
|
||||||
|
|
||||||
(defprotocol Kb
|
(defprotocol KB
|
||||||
;; "The knowledge base"
|
"A knowledge base, comprising features and means to infer their value
|
||||||
(get-case ^APersistentMap [this ^Keyword id]
|
in a given case, and cases which have features with values."
|
||||||
|
(ask-user ^Boolean [this case-or-id]
|
||||||
|
"Ask the user whether `feature-or-id` is true of the case identified
|
||||||
|
by `case-or-id?` If the user doesn't know, return `nil`.")
|
||||||
|
(get-case ^APersistentMap [this ^Keyword case_id]
|
||||||
"Return the case with this `id` from among my cases,
|
"Return the case with this `id` from among my cases,
|
||||||
if present, else `nil`")
|
if present, else `nil`")
|
||||||
(cases ^APersistentMap [this]
|
(cases ^APersistentMap [this]
|
||||||
"All cases known in this knowledge base, keyed by `:id`")
|
"All cases known in this knowledge base, keyed by `:case-id`")
|
||||||
(get-feature ^APersistentMap [this id-or-prop]
|
(get-feature ^APersistentMap [this id-or-prop]
|
||||||
"Return the feature with this `id` from among my features,
|
"Return the feature with this `id` from among my features,
|
||||||
if present, else `nil`")
|
if present, else `nil`")
|
||||||
|
@ -58,13 +71,24 @@
|
||||||
"All features known in this knowledge base, keyed by `:proposition`")
|
"All features known in this knowledge base, keyed by `:proposition`")
|
||||||
(is? ^Boolean [this case-or-id feature-or-id]
|
(is? ^Boolean [this case-or-id feature-or-id]
|
||||||
"Is the feature identified by `feature-or-id` true of the case identified
|
"Is the feature identified by `feature-or-id` true of the case identified
|
||||||
by `case-or-id?"))
|
by `case-or-id`? If feature is undecided for this case, return `nil`.")
|
||||||
|
(knowledge ^APersistentMap [this case-or-id feature-or-id]
|
||||||
|
"Return whatever is currently known for this `case-or-id` about this
|
||||||
|
`feature-or-id`. If the feature is undecided for this case, return
|
||||||
|
`nil`."))
|
||||||
|
|
||||||
(defprotocol Node
|
(defprotocol Node
|
||||||
"A node in a DTree"
|
"A node in a DTree"
|
||||||
|
;; potentially add a field for `authority`, the authority for this (part of
|
||||||
|
;; the) rule. But, version Cedar didn't have this -- this is really a
|
||||||
|
;; Wildwood feature, and what I'm trying to do here is reimplement Cedar.
|
||||||
(children [this] "The children of this node")
|
(children [this] "The children of this node")
|
||||||
(colour ^Boolean [this] "The colour of this node")
|
(colour ^Boolean [this] "The colour of this node")
|
||||||
(feature [this] "The feature at this node")
|
(feature [this] "The feature at this node")
|
||||||
(fragment ^String [this] "The explanation fragment at this node")
|
(fragment ^String [this] "The explanation fragment at this node")
|
||||||
(evaluate [this case-id] "evaluate this node in the context of the case identified by `case-id`.")
|
(evaluate [this case-id] "evaluate this node in the context of the case
|
||||||
(explain [this case-id] "return an explanation from this node or its children for the case identified by `case-id`"))
|
identified by `case-id`.
|
||||||
|
Returns a map with keys `:value`, `:explanation`
|
||||||
|
and possibly `:authority`.")
|
||||||
|
(explain [this case-id] "return an explanation from this node or its children
|
||||||
|
for the case identified by `case-id`"))
|
|
@ -1,17 +1,17 @@
|
||||||
(ns arboretum.dengine.utils
|
(ns arboretum.dengine.utils
|
||||||
(:require
|
(:require
|
||||||
[clojure.string :refer [lower-case replace]])
|
[clojure.string :as s :refer [lower-case replace]])
|
||||||
(:import
|
(:import
|
||||||
[clojure.lang PersistentHashSet]))
|
[clojure.lang PersistentHashSet]))
|
||||||
|
|
||||||
(defn string->keyword
|
(defn string->keyword
|
||||||
"Produce a keyword based on this `s`. If `others` are supplied, the return
|
"Produce a keyword based on this `s`. If `others` are supplied, the return
|
||||||
value will be different from any of those others."
|
value will be different from any of those others."
|
||||||
([^String s]
|
([^String x]
|
||||||
(let [x (replace
|
(let [x' (s/replace
|
||||||
(replace (lower-case s) #"[^]a-z0-9- ]" "")
|
(s/replace (s/lower-case x) #"[^]a-z0-9- ]" "")
|
||||||
#" " "-")]
|
#" " "-")]
|
||||||
(keyword (if (empty? x) (gensym) x))))
|
(keyword (if (empty? x') (gensym) x'))))
|
||||||
([^String s ^PersistentHashSet others]
|
([^String s ^PersistentHashSet others]
|
||||||
(let [c (string->keyword s)]
|
(let [c (string->keyword s)]
|
||||||
(if (others c) (keyword (gensym (name c))) c))))
|
(if (others c) (keyword (gensym (name c))) c))))
|
||||||
|
|
|
@ -1,20 +1,43 @@
|
||||||
(ns dengine.feature-test
|
(ns dengine.feature-test
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require
|
||||||
[arboretum.dengine.feature :refer :all])
|
[arboretum.dengine.core :refer [add-case! add-feature!]]
|
||||||
(:import [arboretum.dengine.feature FeatureImpl]))
|
[arboretum.dengine.kb :refer [!kb]]
|
||||||
|
[clojure.pprint :refer [pprint]]
|
||||||
|
[clojure.test :refer [deftest is testing]])
|
||||||
|
(:import
|
||||||
|
[arboretum.dengine.kb KBImpl]))
|
||||||
|
|
||||||
|
(def testkb (KBImpl. {}
|
||||||
|
{}))
|
||||||
|
|
||||||
|
(reset! !kb testkb)
|
||||||
|
|
||||||
|
|
||||||
(deftest feature-impl-test
|
(deftest feature-impl-test
|
||||||
(testing "Testing feature implementation"
|
(testing "Testing feature implementation"
|
||||||
(let [f (FeatureImpl. "Is entitled to Widow's Allowance" false nil)]
|
(let [f (add-feature! "is entitled to Widow's Allowance" :is-entitled-to-widows-allowance false nil)
|
||||||
(let [expected :is-entitled-to-widows-allowance
|
c (add-case! "Mrs Nora Trellis" :case_1
|
||||||
actual (.id f)]
|
{:married {:value true
|
||||||
(is (= actual expected)))
|
:authority :user
|
||||||
(let [expected false
|
:reason "I have been told that married is true of Mrs Norah Trellis"}
|
||||||
actual (.default f)]
|
:divorced {:value false
|
||||||
(is (= actual expected)))
|
:authority :default
|
||||||
(let [expected false
|
:reason "I assumed that divorced is false of Mrs Norah Trellis"}
|
||||||
actual (.decide f {})]
|
:is-entitled-to-widows-benefit {:value true
|
||||||
(is (= actual expected) "If we have no knoledge and no decision method, take the defualt."))
|
:authority :rule
|
||||||
(let [expected false
|
:reason "I infered that is-entitled-to-widows-benefit is true of Mrs Norah Trellis"}})]
|
||||||
actual (.decide f {:is-entitled-to-widows-benefit true})]
|
(pprint {:f f
|
||||||
(is (= actual expected) "If we have knoledge, return it.")))))
|
:c c})
|
||||||
|
(let [expected :is-entitled-to-widows-allowance
|
||||||
|
actual (.feature-id f)]
|
||||||
|
(is (= actual expected)))
|
||||||
|
(let [expected false
|
||||||
|
actual (.default f)]
|
||||||
|
(is (= actual expected)))
|
||||||
|
(let [expected false
|
||||||
|
actual (.decide f c)]
|
||||||
|
(is (= actual expected) "If we have no knowledge and no decision method, take the defualt."))
|
||||||
|
(let [expected true
|
||||||
|
actual (.decide (add-feature! "Married" :married false nil) :case_1)]
|
||||||
|
(is (= actual expected) "If we have knowledge, return it."))))
|
||||||
|
)
|
|
@ -1,17 +1,30 @@
|
||||||
(ns dengine.kb-test
|
(ns dengine.kb-test
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require
|
||||||
arboretum.dengine.case
|
[arboretum.dengine.core :refer [add-case!]]
|
||||||
arboretum.dengine.feature
|
[arboretum.dengine.kb :refer :all]
|
||||||
[arboretum.dengine.kb :refer :all])
|
[clojure.pprint :refer [pprint]]
|
||||||
(:import [arboretum.dengine.case CaseImpl]
|
[clojure.test :refer [deftest is testing]])
|
||||||
[arboretum.dengine.feature FeatureImpl]
|
(:import
|
||||||
[arboretum.dengine.kb KBImpl]))
|
[arboretum.dengine.kb KBImpl]))
|
||||||
|
|
||||||
(def testkb (KBImpl. [(CaseImpl. :case_1 "Mrs Nora Trellis" {:married true :divorced false})]
|
(def testkb (KBImpl. {}
|
||||||
[(FeatureImpl. "is entitled to Widow's Benefit" false nil)]))
|
{}))
|
||||||
|
|
||||||
|
(pprint testkb)
|
||||||
|
|
||||||
(reset! !kb testkb)
|
(reset! !kb testkb)
|
||||||
|
|
||||||
|
(add-case! "Mrs Nora Trellis" :case_1
|
||||||
|
{:married {:value true
|
||||||
|
:authority :user
|
||||||
|
:reason "I have been told that married is true of Mrs Norah Trellis"}
|
||||||
|
:divorced {:value false
|
||||||
|
:authority :default
|
||||||
|
:reason "I assumed that divorced is false of Mrs Norah Trellis"}
|
||||||
|
:is-entitled-to-widows-benefit {:value true
|
||||||
|
:authority :rule
|
||||||
|
:reason "I infered that is-entitled-to-widows-benefit is true of Mrs Norah Trellis"}})
|
||||||
|
|
||||||
(deftest access-test
|
(deftest access-test
|
||||||
(testing "Accessing knowledge in the knowledge base"
|
(testing "Accessing knowledge in the knowledge base"
|
||||||
(let [expected nil
|
(let [expected nil
|
||||||
|
@ -22,5 +35,4 @@
|
||||||
(is (= actual expected)))
|
(is (= actual expected)))
|
||||||
(let [expected false
|
(let [expected false
|
||||||
actual (.is? testkb :case_1 :divorced)]
|
actual (.is? testkb :case_1 :divorced)]
|
||||||
(is (= actual expected)))
|
(is (= actual expected)))))
|
||||||
))
|
|
Loading…
Reference in a new issue