Frustratingly close to working, which is another way of saying not working at all.

This commit is contained in:
Simon Brooke 2025-08-13 21:37:43 +01:00
parent 33dcbf3f9c
commit 45f6b972c7
11 changed files with 340 additions and 126 deletions

View file

@ -4,5 +4,6 @@
: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/clojure "1.12.0"]]
[org.clojure/clojure "1.12.0"]
[com.taoensso/timbre "4.10.0"]]
:repl-options {:init-ns arboretum.dengine.core})

View file

@ -1,10 +1,14 @@
(ns arboretum.dengine.case
(:require [arboretum.dengine.protocols :refer [Case]]))
(defrecord CaseImpl [ id name knowledge]
(defrecord CaseImpl [case-id case-name knowledge]
Case
(id [this] :foo);;(:id this))
(name [this] "foo") ;;(or (:name this) (clojure.core/name (:id this))))
(knowledge [this] (:knowledge this))
(is? [this feature-id] ((:knowledge this) feature-id))
)
(authority [this feature-id] (:authority ((:knowledge this) feature-id)))
(case-id [this] (:case-id this))
(case-name [this] (or (:case-name this) (clojure.core/name (:case-id this)))))
(defn make-case
([id name]
(make-case id name {}))
([id name knowledge]
(CaseImpl. id name knowledge)))

View file

@ -1,14 +1,38 @@
(ns arboretum.dengine.core
;; (:require [arboretum.dengine.feature :refer [decide explain]]
;; [arboretum.dengine.protocols :refer [proposition]])
)
(defn foo
"I don't do a whole lot."
[x]
(println x "Hello, World!"))
(:require
[arboretum.dengine.case :refer [make-case]]
[arboretum.dengine.feature :refer [make-feature]]
[arboretum.dengine.kb :refer [!kb]]
[arboretum.dengine.utils :refer [string->keyword]])
(:import [arboretum.dengine.case CaseImpl]))
;; (defn is?
;; [feature case kb]
;; (let [evaluation (decide feature case kb)]
;; ((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)))

View 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)))

View file

@ -1,35 +1,25 @@
(ns arboretum.dengine.feature
(:require [arboretum.dengine.kb :refer [!kb]]
[arboretum.dengine.protocols :refer [Case Feature Node]]
[arboretum.dengine.utils :refer [string->keyword]]
[clojure.spec.alpha :as spec]))
(:require
[arboretum.dengine.dengine :refer [decide-feat]]
[arboretum.dengine.protocols :refer [Feature]]
[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
(decide [this c] (let [v ((.id this) c)]
(cond (boolean? v) v
(:rootnode this) (dengine (:rootnode this) c)
:else (:default this))))
(decide [this c] (decide-feat this c))
(default [this] (:default this))
(id [this] (string->keyword (:proposition this)))
(feature-id [this] (string->keyword (:proposition this)))
(rootnode [this] (:rootnode 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)))

View file

@ -1,44 +1,78 @@
(ns arboretum.dengine.kb
(:require
arboretum.dengine.case
[arboretum.dengine.protocols :refer [Case Feature Kb]]
[arboretum.dengine.utils :refer [string->keyword]]
[clojure.pprint :refer [pprint]])
(:import
[arboretum.dengine.case CaseImpl]))
[arboretum.dengine.protocols :refer [Case Feature KB]]
[clojure.pprint :refer [pprint]]
[taoensso.timbre :as timbre :refer [log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy]]))
(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))
(features [this] (:features this))
(get-case [this case-id] (-> this :cases case-id))
(get-feature [this feature-id] (-> this :features feature-id))
(is? [this case-or-id feature-or-id]
(let [c (if (satisfies? Case case-or-id)
case-or-id (-> this :cases case-or-id))
f (if (satisfies? Feature feature-or-id) feature-or-id
(-> this :features feature-or-id))]
(-> c (:id f)))))
(do-is? this case-or-id feature-or-id))
(knowledge [this case-or-id feature-or-id]
(let [c (find-case-for this case-or-id)
f (find-feature-for this feature-or-id)]
(-> c :knowledge (:feature-id f)))))
(def !kb
"The one instance of the knowledge base."
(atom (KBImpl. nil nil)))
(atom (KBImpl. {} {})))
(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 (map :id (.cases @!kb)))))
([^String name ^clojure.lang.Keyword id]
(let [c (CaseImpl. id name {})]
(swap! !kb update-in :cases conj c))))
(defn remember!
"Remember this `value` for the feature with this `feature-id` of the case
with this `case-id`. Return the `value`."
[case-id feature-id value]
;; (log (format "Remembering case `%s`, feature `%s`, value `%s`"
;; case-id feature-id value))
(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]
(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`"
;; (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)
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
:kb kb
:type (type kb)})))))
:kb kb
:type (type kb)})))))

View file

@ -1,12 +1,81 @@
(ns arboretum.dengine.node
(:require [arboretum.dengine.protocols :refer [Node]]
[clojure.spec.alpha :as spec]))
(:require
[arboretum.dengine.kb :refer [!kb]]
[arboretum.dengine.protocols :refer [Feature Node]]
[clojure.string :as string]))
(defrecord NodeImpl [children ^Boolean colour feature ^String fragment]
Node
Node
(children [this] "The children of this node" (:children this))
(colour ^Boolean [this] "The colour of this node" (:colour this))
(feature [this] "The feature at this node" (:feature this))
(fragment ^String [this] "The explanation fragment at this node")
(evaluate [this case-id] )
)
(fragment ^String [this] "The explanation fragment at this node"
(: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}
;; (SETnode (POP nodesToDo))
;; (SETchildren (GET node children))
;; {get a node from nodesToDo and obtain its children. ..}
;; (SETnewNodes 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}

View file

@ -10,14 +10,14 @@
(defprotocol Case
"A case to be decided. It represents an object-in-the-world about
which knowledge may be held and enquiries may be made."
(id ^Keyword [this] "Returns the `:id` of this case.")
(name ^String [this] "Returns the name associated with this case")
(knowledge ^APersistentMap [this] "Returns a map whose keys are ids of features
and whose values are the values of those
features in this case.")
(is? ^Boolean [this feature-id] "Returns `true` if feature-id true of `this`,
`false` if it's definitely not true, and
`nil` if its truth value is unknown."))
(authority [this feature-id] "Returns information about how the value of the
feature identified be `feature-id` is known
about `this` case.
At this stage, one of the keywords `:default`,
`:inference`, `:user`.")
(case-id ^Keyword [this] "Returns the `:case-id` of this case.")
(case-name ^String [this] "Returns the name associated with this case"))
(defmacro case?
"Return `true` if this `obj` appears to be a valid case.
@ -28,29 +28,42 @@
`(satisfies? Case ~obj))
(defprotocol Feature
"A feature has three essential elements: a proposition, a default value, and
a method of decision"
(decide ^Boolean [this case] "Returns the default value of my proposition for this `case`.
"A feature has three essential elements: a proposition, a default
value, and a method of decision"
(decide ^Boolean [this case] "Returns the default value of my
proposition for this `case`.
`case` should be either an object which satisfies the `Case`
protocol or else a keyword, in which case it should be the
`case` should be either an object which
satisfies the `Case` protocol or else a
keyword, in which case it should be the
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")
(id ^Keyword [this] "Returns a keywordised representation of the proposition of this feature")
(proposition ^String [this] "Returns the proposition associated with this feature")
(rootnode [this] "Returns the root node of the dtree for this feature, if one exists, else `nil`"))
(feature-id ^Keyword [this] "Returns a keywordised representation of the proposition
of this feature")
(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?
[obj]
`(satisfies? Feature ~obj))
(defprotocol Kb
;; "The knowledge base"
(get-case ^APersistentMap [this ^Keyword id]
(defprotocol KB
"A knowledge base, comprising features and means to infer their value
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,
if present, else `nil`")
(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]
"Return the feature with this `id` from among my features,
if present, else `nil`")
@ -58,13 +71,24 @@
"All features known in this knowledge base, keyed by `:proposition`")
(is? ^Boolean [this case-or-id feature-or-id]
"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
"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")
(colour ^Boolean [this] "The colour of this node")
(feature [this] "The feature 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`.")
(explain [this case-id] "return an explanation from this node or its children for the case identified by `case-id`"))
(evaluate [this case-id] "evaluate this node in the context of the case
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`"))

View file

@ -1,17 +1,17 @@
(ns arboretum.dengine.utils
(:require
[clojure.string :refer [lower-case replace]])
[clojure.string :as s :refer [lower-case replace]])
(:import
[clojure.lang PersistentHashSet]))
(defn string->keyword
"Produce a keyword based on this `s`. If `others` are supplied, the return
value will be different from any of those others."
([^String s]
(let [x (replace
(replace (lower-case s) #"[^]a-z0-9- ]" "")
([^String x]
(let [x' (s/replace
(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]
(let [c (string->keyword s)]
(if (others c) (keyword (gensym (name c))) c))))

View file

@ -1,20 +1,43 @@
(ns dengine.feature-test
(:require [clojure.test :refer [deftest is testing]]
[arboretum.dengine.feature :refer :all])
(:import [arboretum.dengine.feature FeatureImpl]))
(:require
[arboretum.dengine.core :refer [add-case! add-feature!]]
[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
(testing "Testing feature implementation"
(let [f (FeatureImpl. "Is entitled to Widow's Allowance" false nil)]
(let [expected :is-entitled-to-widows-allowance
actual (.id f)]
(is (= actual expected)))
(let [expected false
actual (.default f)]
(is (= actual expected)))
(let [expected false
actual (.decide f {})]
(is (= actual expected) "If we have no knoledge and no decision method, take the defualt."))
(let [expected false
actual (.decide f {:is-entitled-to-widows-benefit true})]
(is (= actual expected) "If we have knoledge, return it.")))))
(testing "Testing feature implementation"
(let [f (add-feature! "is entitled to Widow's Allowance" :is-entitled-to-widows-allowance false nil)
c (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"}})]
(pprint {:f f
: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."))))
)

View file

@ -1,17 +1,30 @@
(ns dengine.kb-test
(:require [clojure.test :refer [deftest is testing]]
arboretum.dengine.case
arboretum.dengine.feature
[arboretum.dengine.kb :refer :all])
(:import [arboretum.dengine.case CaseImpl]
[arboretum.dengine.feature FeatureImpl]
[arboretum.dengine.kb KBImpl]))
(:require
[arboretum.dengine.core :refer [add-case!]]
[arboretum.dengine.kb :refer :all]
[clojure.pprint :refer [pprint]]
[clojure.test :refer [deftest is testing]])
(:import
[arboretum.dengine.kb KBImpl]))
(def testkb (KBImpl. [(CaseImpl. :case_1 "Mrs Nora Trellis" {:married true :divorced false})]
[(FeatureImpl. "is entitled to Widow's Benefit" false nil)]))
(def testkb (KBImpl. {}
{}))
(pprint 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
(testing "Accessing knowledge in the knowledge base"
(let [expected nil
@ -22,5 +35,4 @@
(is (= actual expected)))
(let [expected false
actual (.is? testkb :case_1 :divorced)]
(is (= actual expected)))
))
(is (= actual expected)))))