knowledge.py
6.76 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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# 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 StudentKnowledge(object):
# =======================================================================
# methods that update state
# =======================================================================
def __init__(self, deps, state={}):
# graph with topic dependencies shared between all students
self.deps = deps
# state only contains information on unlocked topics
# {'topic_id': {'level':0.5, 'date': datetime}, ...}
self.state = state
# update state level based on the elapsed time (no dependencies... FIXME)
now = datetime.now()
for s in state.values():
dt = now - s['date']
s['level'] *= 0.975 ** dt.days
# compute recommended sequence of topics ['a', 'b', ...]
self.topic_sequence = list(nx.topological_sort(self.deps))
# select a topic to do and initialize questions
# self.start_topic()
# ------------------------------------------------------------------------
# Unlock all topics whose dependencies are satisfied
# ------------------------------------------------------------------------
def unlock_topics(self):
min_level = 0.01 # minimum level to unlock
for topic in self.topic_sequence:
if topic not in self.state: # if locked
pred = self.deps.predecessors(topic)
if all(d in self.state and self.state[d]['level'] > min_level for d in pred): # dependencies done
self.state[topic] = {
'level': 0.0,
'date': datetime.now()
}
logger.debug(f'unlocked {topic}')
# ------------------------------------------------------------------------
# Recommends a topic to practice/learn from the state.
# ------------------------------------------------------------------------
def recommended_topic(self):
return min(self.state.items(), key=lambda x: x[1])[0]
# 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.deps.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.deps.node[topic]['factory']
# if not factory or not unlocked:
# logger.debug(f'Can\'t start topic "{topic}".')
# return
# ------------------------------------------------------------------------
# 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 whose dependencies are done
self.unlock_topics()
if not topic:
topic = self.recommended_topic()
self.current_topic = topic
logger.info(f'Topic set to "{topic}"')
# generate question instances for current topic
factory = self.deps.node[topic]['factory']
questionlist = self.deps.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'Grade = {grade:.2} ({q["ref"]})')
# 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.deps.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):
return [{
'ref': ref,
'name': self.deps.nodes[ref]['name'],
'level': self.state[ref]['level'] if ref in self.state else None
} for ref in self.topic_sequence ]
# ts = []
# for ref in self.topic_sequence:
# ts.append({
# 'ref': ref,
# 'name': self.deps.nodes[ref]['name'],
# 'level': self.state[ref]['level'] if ref in self.state else None
# })
# return ts
# ------------------------------------------------------------------------
def get_topic_progress(self):
return len(self.finished_questions) / (1 + len(self.finished_questions) + len(self.questions))