knowledge.py
5.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# python standard library
import random
from datetime import datetime
import logging
# libraries
import networkx as nx
# this project
import questions
# setup logger for this module
logger = logging.getLogger(__name__)
# ----------------------------------------------------------------------------
# kowledge state of each student....??
# ----------------------------------------------------------------------------
class Knowledge(object):
# =======================================================================
# methods that update state
# =======================================================================
def __init__(self, depgraph, state={}, student=''):
self.depgraph = depgraph
self.state = state # {'topic_id': {'level':0.5, 'date': datetime}, ...}
self.student = student
# compute recommended sequence of topics ['a', 'b',...]
self.topic_sequence = nx.topological_sort(self.depgraph)
# select a topic to do and initialize questions
self.start_topic()
# ------------------------------------------------------------------------
# Start a new topic. If not provided, selects the first with level < 0.8
# If all levels > 0.8, will stay in the last one forever...
# ------------------------------------------------------------------------
def start_topic(self, topic=''):
# unlock topics that have satisfied dependencies
unlock_topics = []
for t in self.topic_sequence:
if t not in self.state: # is locked
deps = self.depgraph.predecessors(t)
if all(d in self.state and self.state[d]['level'] > 0.01 for d in deps): # dependencies done
unlock_topics.append(t)
for t in unlock_topics:
self.state[t] = {'level': 0.0, 'date': datetime.now()}
logger.info(f'User "{self.student}" unlocked "{t}"')
# choose topic
if not topic:
for topic in self.topic_sequence:
unlocked = topic in self.state
needs_work = unlocked and self.state[topic]['level'] < 0.8
factory = self.depgraph.node[topic]['factory']
if factory and (not unlocked or needs_work):
break
# use given topic if possible
else:
unlocked = topic in self.state
factory = self.depgraph.node[topic]['factory']
if not factory or not unlocked:
logger.debug(f'User "{self.student}" cannot start topic "{topic}"')
return
self.current_topic = topic
logger.info(f'User "{self.student}" topic set to "{topic}"')
# generate question instances for current topic
questionlist = self.depgraph.node[topic]['questions']
self.questions = [factory[qref].generate() for qref in questionlist]
self.current_question = self.questions.pop(0) # FIXME crashes if questions==[]
self.current_question['start_time'] = datetime.now()
self.finished_questions = []
# ------------------------------------------------------------------------
# returns the current question with correction, time and comments updated
# ------------------------------------------------------------------------
def check_answer(self, answer):
q = self.current_question
q['finish_time'] = datetime.now()
grade = q.correct(answer)
logger.debug(f'User {self.student}: grade = {grade}')
# new question if answer is correct
if grade > 0.999:
self.finished_questions.append(q)
try:
self.current_question = self.questions.pop(0) # FIXME empty?
except IndexError:
self.current_question = None
self.state[self.current_topic] = {
'level': 1.0,
'date': datetime.now()
}
self.start_topic()
else:
self.current_question['start_time'] = datetime.now()
else:
factory = self.depgraph.node[self.current_topic]['factory']
self.questions.append(factory[q['ref']].generate())
return q
# ========================================================================
# pure functions of the state (no side effects)
# ========================================================================
# ------------------------------------------------------------------------
def get_current_question(self):
return self.current_question
# ------------------------------------------------------------------------
def get_current_topic(self):
return self.current_topic
# ------------------------------------------------------------------------
# Return list of tuples (topic, level).
# Levels are in the interval [0, 1] or None if the topic is locked.
# Topics unlocked but not yet done have level 0.0.
# Example: [('topic_A', 0.9), ('topic_B', None), ...]
# ------------------------------------------------------------------------
def get_knowledge_state(self):
ts = []
for t in self.topic_sequence:
if t in self.state:
ts.append((t, self.state[t]['level'])) # already done
else:
ts.append((t, None)) # locked
return ts
# ------------------------------------------------------------------------
def get_topic_progress(self):
return len(self.finished_questions) / (1 + len(self.finished_questions) + len(self.questions))