Skip to content

Commit 4bf081d

Browse files
authored
Merge pull request #311 from mitodl/sibling_sampling
Rearranging sibling sampling
2 parents 7294a80 + c806676 commit 4bf081d

File tree

5 files changed

+62
-24
lines changed

5 files changed

+62
-24
lines changed

mitxgraders/formulagrader/formulagrader.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import six
99
from voluptuous import Schema, Required, Any, All, Invalid, Length
1010
from mitxgraders.comparers import equality_comparer
11-
from mitxgraders.sampling import schema_user_functions_no_random
11+
from mitxgraders.sampling import schema_user_functions_no_random, DependentSampler
1212
from mitxgraders.exceptions import MissingInput
1313
from mitxgraders.baseclasses import ItemGrader
1414
from mitxgraders.helpers.calc import evaluator, DEFAULT_VARIABLES
@@ -131,7 +131,7 @@ def reset_default_comparer(cls):
131131
cls.set_default_comparer(equality_comparer)
132132

133133
@staticmethod
134-
def eval_and_validate_comparer_params(scoped_eval, comparer_params, siblings_eval):
134+
def eval_and_validate_comparer_params(scoped_eval, comparer_params):
135135
"""
136136
Evaluate the comparer_params, and make sure they contain no references
137137
to empty siblings.
@@ -149,11 +149,6 @@ def eval_and_validate_comparer_params(scoped_eval, comparer_params, siblings_eva
149149
for param in comparer_params]
150150
# results is a list of (value, EvalMetaData) pairs
151151
comparer_params_eval = [value for value, _ in results]
152-
used_variables = set().union(*[used.variables_used for _, used in results])
153-
154-
for variable in used_variables:
155-
if variable in siblings_eval and np.isnan(siblings_eval[variable]):
156-
raise MissingInput('Cannot grade answer, a required input is missing.')
157152

158153
return comparer_params_eval
159154

@@ -290,11 +285,13 @@ def gen_evaluations(self, comparer_params, student_input, sibling_formulas,
290285
comparer_params_evals = []
291286
student_evals = []
292287

293-
# Create a list of instructor variables to remove from student evaluation
288+
# Create a list of instructor and sibling variables to remove from student evaluation
289+
sibling_vars = [key for key in sibling_formulas]
294290
var_blacklist = []
295291
for var in self.config['instructor_vars']:
296292
if var in var_samples[0]:
297293
var_blacklist.append(var)
294+
var_blacklist += sibling_vars
298295

299296
for i in range(self.config['samples']):
300297
# Update the functions and variables listings with this sample
@@ -309,33 +306,21 @@ def scoped_eval(expression,
309306
return evaluator(expression, variables, functions, suffixes, max_array_dim,
310307
allow_inf=self.config['allow_inf'])
311308

312-
# Compute the sibling values, and add them to varlist
313-
siblings_eval = {
314-
key: scoped_eval(sibling_formulas[key])[0]
315-
for key in sibling_formulas
316-
}
317-
varlist.update(siblings_eval)
318-
319309
# Compute expressions
320-
comparer_params_eval = self.eval_and_validate_comparer_params(
321-
scoped_eval, comparer_params, siblings_eval)
310+
comparer_params_eval = self.eval_and_validate_comparer_params(scoped_eval, comparer_params)
322311
comparer_params_evals.append(comparer_params_eval)
323312

324313
# Before performing student evaluation, scrub the sibling and instructor
325314
# variables so that students can't use them
326-
for key in siblings_eval:
327-
del varlist[key]
328315
for key in var_blacklist:
329316
del varlist[key]
330317

331318
student_eval, meta = scoped_eval(student_input)
332319
student_evals.append(student_eval)
333320

334-
# TODO: Remove this if statement
335321
if self.config['debug']:
336322
# Put the siblings and instructor variables back in for the debug output
337323
varlist.update(var_samples[i])
338-
varlist.update(siblings_eval)
339324
self.log_eval_info(i, varlist, funclist,
340325
comparer_params_eval=comparer_params_eval,
341326
student_eval=student_eval)
@@ -347,8 +332,15 @@ def raw_check(self, answer, student_input, **kwargs):
347332

348333
# Extract sibling formulas to allow for sampling
349334
siblings = kwargs.get('siblings', None)
335+
# Find sibling variables used in comparer parameters
350336
comparer_params = answer['expect']['comparer_params']
351337
required_siblings = self.get_used_vars(comparer_params)
338+
# Add in any sibling variables used in DependentSamplers
339+
samplers = [self.config['sample_from'][x]
340+
for x in self.config['sample_from']
341+
if isinstance(self.config['sample_from'][x], DependentSampler)]
342+
sampler_vars = sum((x.config['depends'] for x in samplers), [])
343+
required_siblings = list(set(required_siblings).union(set(sampler_vars)))
352344
# required_siblings might include some extra variable names, but no matter
353345
sibling_formulas = self.get_sibling_formulas(siblings, required_siblings)
354346

mitxgraders/helpers/math_helpers.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
from voluptuous import Schema, Required, Any, All, Length, Coerce
1717

1818
from mitxgraders.baseclasses import ItemGrader
19-
from mitxgraders.exceptions import InvalidInput, ConfigError
19+
from mitxgraders.exceptions import InvalidInput, ConfigError, MissingInput
2020
from mitxgraders.comparers import CorrelatedComparer
21-
from mitxgraders.sampling import (VariableSamplingSet, RealInterval, DiscreteSet,
21+
from mitxgraders.sampling import (VariableSamplingSet, RealInterval, DiscreteSet, DependentSampler,
2222
gen_symbols_samples, construct_functions,
2323
construct_constants, construct_suffixes,
2424
schema_user_functions,
@@ -537,6 +537,18 @@ def gen_var_and_func_samples(self, *args):
537537

538538
# Generate the variable list
539539
variables, sample_from_dict = self.generate_variable_list(expressions)
540+
541+
# Check if a dictionary of sibling variables has been provided, and sample those too
542+
for entry in args:
543+
if isinstance(entry, dict):
544+
if all([k.startswith('sibling_') for k in entry]):
545+
# This is a sibling dictionary. Add it to the list of variables to sample.
546+
for k in entry:
547+
variables.append(k)
548+
if entry[k] == '':
549+
raise MissingInput('Cannot grade answer, a required input is missing.')
550+
sample_from_dict[k] = DependentSampler(formula=entry[k])
551+
break
540552

541553
# Generate the samples
542554
var_samples = gen_symbols_samples(variables,
@@ -583,7 +595,7 @@ def generate_variable_list(self, expressions):
583595
(full_string, head) = match.groups()
584596
variable_list.append(full_string)
585597
sample_from_dict[full_string] = sample_from_dict[head]
586-
598+
587599
return variable_list, sample_from_dict
588600

589601
def log_comparison_info(self, comparer, comparer_results):

tests/formulagrader/test_formulagrader.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,36 @@ def test_fg_evals_numbered_variables_in_siblings():
688688
# The second call should provide only the first sibling formula
689689
assert results[1] == {'sibling_1': 'x_{0}+1'}
690690

691+
def test_siblings_in_dependent_samplers():
692+
grader = ListGrader(
693+
answers=['1', 'x'],
694+
subgraders=[
695+
FormulaGrader(),
696+
FormulaGrader(variables=['x'], sample_from={'x': DependentSampler(formula='sibling_1+1')})
697+
],
698+
ordered=True
699+
)
700+
701+
submission = ['1', '2']
702+
expected_result = {
703+
'overall_message': '',
704+
'input_list': [
705+
{'ok': True, 'grade_decimal': 1, 'msg': ''},
706+
{'ok': True, 'grade_decimal': 1, 'msg': ''},
707+
]
708+
}
709+
assert grader(None, submission) == expected_result
710+
711+
submission = ['2', '3']
712+
expected_result = {
713+
'overall_message': '',
714+
'input_list': [
715+
{'ok': False, 'grade_decimal': 0, 'msg': ''},
716+
{'ok': True, 'grade_decimal': 1, 'msg': ''},
717+
]
718+
}
719+
assert grader(None, submission) == expected_result
720+
691721
def test_ng_config():
692722
"""Test that the NumericalGrader config bars unwanted entries"""
693723
expect = r"not a valid value for dictionary value @ data\[u?'failable_evals'\]. Got 1"

tests/helpers/calc/test_expressions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,6 @@ def test_min_max():
278278

279279
def test_large_numbers():
280280
assert evaluator('x', variables={'x': 10 ** 30})[0] == 1e30
281+
282+
def test_nan():
283+
assert np.isnan(evaluator("x^2", {'x': float('nan')}, {}, {})[0])

tests/test_listgrader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ def test_siblings_passed_to_subgrader_check_if_ordered_and_subgrader_list():
588588
for _, kwargs in check2.call_args_list:
589589
assert kwargs['siblings'] == siblings
590590

591+
591592
def test_grouping_unordered_different_lengths():
592593
"""Test that an error is raised if unordered groupings use different numbers of inputs"""
593594
msg = "Groups must all be the same length when unordered"

0 commit comments

Comments
 (0)