class ComplexSector(VariableSamplingSet): """ Represents an annular sector in the complex plane from which to sample, based on a given range of modulus and argument. Config: modulus (list): Range for the modulus (default [1,3]) argument (list): Range for the argument (default [0,pi/2]) Usage ===== Sample from the unit circle >>> sect = ComplexSector(modulus=[0,1], argument=[-np.pi,np.pi]) """ schema_config = Schema({ Required('modulus', default=[1, 3]): NumberRange(), Required('argument', default=[0, np.pi / 2]): NumberRange() }) def __init__(self, config=None, **kwargs): """ Configure the class as normal, then set up the modulus and argument parts as RealInterval objects """ super(ComplexSector, self).__init__(config, **kwargs) self.modulus = RealInterval(self.config['modulus']) self.argument = RealInterval(self.config['argument']) def gen_sample(self): """Generates a random sample in the defined annular sector in the complex plane""" return self.modulus.gen_sample() * np.exp( 1j * self.argument.gen_sample())
class ComplexRectangle(VariableSamplingSet): """ Represents a rectangle in the complex plane from which to sample. Config: re (list): Range for the real component (default [1,3]) im (list): Range for the imaginary component (default [1,3]) Usage ===== >>> rect = ComplexRectangle(re=[1,4], im=[-5,0]) """ schema_config = Schema({ Required('re', default=[1, 3]): NumberRange(), Required('im', default=[1, 3]): NumberRange() }) def __init__(self, config=None, **kwargs): """ Configure the class as normal, then set up the real and imaginary parts as RealInterval objects """ super(ComplexRectangle, self).__init__(config, **kwargs) self.re = RealInterval(self.config['re']) self.im = RealInterval(self.config['im']) def gen_sample(self): """Generates a random sample in the defined rectangle in the complex plane""" return self.re.gen_sample() + self.im.gen_sample() * 1j
def schema_config(self): """Define the configuration options for StringGrader""" # Construct the default ItemGrader schema schema = super(StringGrader, self).schema_config # Append options return schema.extend({ Required('strip', default=True): bool, Required('case_sensitive', default=True): bool })
def schema_config(self): """ Defines the default config schema for item graders. Classes that inherit from ItemGrader should extend this schema. """ schema = super(ItemGrader, self).schema_config return schema.extend({ Required('answers', default=tuple()): self.schema_answers, Required('wrong_msg', default=""): str })
def schema_answer(self): """Defines the schema that a fully-specified answer should satisfy.""" return Schema({ Required('expect'): self.schema_expect, Required('grade_decimal', default=1): All(numbers.Number, Range(0, 1)), Required('msg', default=''): str, Required('ok', default='computed'): Any('computed', True, False, 'partial') })
def NumberRange(number_type=Number): """ Schema that allows for a start and stop, or alternatively, a list [start, stop] The type of number can be restricted by specifying number_type=int, for example """ return Schema(Any( { Required('start', default=1): number_type, Required('stop', default=5): number_type }, number_range_alternate(number_type) ))
def schema_config(self): """Define the configuration options for ListGrader""" # Construct the default AbstractGrader schema schema = super(ListGrader, self).schema_config # Append options return schema.extend({ Required('ordered', default=False): bool, Required('subgraders'): Any(AbstractGrader, [AbstractGrader]), Required('grouping', default=[]): [Positive(int)], Required('answers', default=[]): Any(list, (list, )) # Allow for a tuple of lists })
def __init__(self, config=None, **kwargs): super(IntegralGrader, self).__init__(config, **kwargs) self.true_input_positions = self.validate_input_positions( self.config['input_positions']) # The below are copied from FormulaGrader.__init__ # Set up the various lists we use self.functions, self.random_funcs = construct_functions( self.config["whitelist"], self.config["blacklist"], self.config["user_functions"]) self.constants = construct_constants(self.config["user_constants"]) # TODO I would like to move this into construct_constants at some point, # perhaps giving construct_constants and optional argument specifying additional defaults if 'infty' not in self.constants: self.constants['infty'] = float('inf') # Construct the schema for sample_from # First, accept all VariableSamplingSets # Then, accept any list that RealInterval can interpret # Finally, single numbers or tuples of numbers will be handled by DiscreteSet schema_sample_from = Schema({ Required(varname, default=RealInterval()): Any(VariableSamplingSet, All(list, lambda pair: RealInterval(pair)), lambda tup: DiscreteSet(tup)) for varname in self.config['variables'] }) self.config['sample_from'] = schema_sample_from( self.config['sample_from'])
def __init__(self, config=None, **kwargs): """ Validate the Formulagrader's configuration. First, we allow the ItemGrader initializer to construct the function list. We then construct the lists of functions, suffixes and constants. Finally, we refine the sample_from entry. """ super(FormulaGrader, self).__init__(config, **kwargs) # Set up the various lists we use self.functions, self.random_funcs = construct_functions(self.config["whitelist"], self.config["blacklist"], self.config["user_functions"]) self.constants = construct_constants(self.config["user_constants"]) self.suffixes = construct_suffixes(self.config["metric_suffixes"]) # Construct the schema for sample_from # First, accept all VariableSamplingSets # Then, accept any list that RealInterval can interpret # Finally, single numbers or tuples of numbers will be handled by DiscreteSet schema_sample_from = Schema({ Required(varname, default=RealInterval()): Any(VariableSamplingSet, All(list, lambda pair: RealInterval(pair)), lambda tup: DiscreteSet(tup)) for varname in self.config['variables'] }) self.config['sample_from'] = schema_sample_from(self.config['sample_from'])
def schema_config(self): """ Defines the default config schema for abstract graders. Classes that inherit from AbstractGrader should extend this schema. """ return Schema({ Required('debug', default=False): bool # Use to turn on debug output })
class DependentSampler(VariableSamplingSet): """ Represents a variable that depends on other variables. You must initialize with the list of variables this depends on as well as the formula for this variable. Note that only base formulas and suffixes are available for this computation. Usage ===== Specify a single value >>> ds = DependentSampler(depends=['x', 'y', 'z'], formula="sqrt(x^2+y^2+z^2)") """ # Take in an individual or tuple of numbers schema_config = Schema({ Required('depends'): [str], Required('formula'): str, Required('case_sensitive', default=True): bool }) def gen_sample(self): """Return a random entry from the given set""" raise Exception( "DependentSampler must be invoked with compute_sample.") def compute_sample(self, sample_dict): """Compute the value of this sample""" try: result, _ = evaluator(formula=self.config['formula'], case_sensitive=self.config['case_sensitive'], variables=sample_dict, functions=DEFAULT_FUNCTIONS, suffixes=DEFAULT_SUFFIXES) except CalcError: raise ConfigError("Formula error in dependent sampling formula: " + self.config["formula"]) return result
def schema_config(self): """Define the configuration options for NumericalGrader""" # Construct the default FormulaGrader schema schema = super(NumericalGrader, self).schema_config # Modify the default FormulaGrader options return schema.extend({ Required('user_functions', default={}): {Extra: is_callable}, Required('tolerance', default='5%'): Any(PercentageString, NonNegative(Number)), Required('samples', default=1): 1, Required('variables', default=[]): [], Required('sample_from', default={}): {}, Required('failable_evals', default=0): 0 })
def schema_config(self): """Define the configuration options for SingleListGrader""" # Construct the default ItemGrader schema schema = super(SingleListGrader, self).schema_config # Append options, replacing the ItemGrader 'answers' key return schema.extend({ Required('ordered', default=False): bool, Required('length_error', default=False): bool, Required('delimiter', default=','): str, Required('partial_credit', default=True): bool, Required('subgrader'): ItemGrader, Required('answers', default=[]): Any(list, (list, )) # Allow for a tuple of lists })
def schema_config(self): """Define the configuration options for IntegralGrader""" # Construct the default AbstractGrader schema schema = super(IntegralGrader, self).schema_config default_input_positions = { 'lower': 1, 'upper': 2, 'integrand': 3, 'integration_variable': 4 } # Append options return schema.extend({ Required('answers'): { Required('lower'): str, Required('upper'): str, Required('integrand'): str, Required('integration_variable'): str }, Required('input_positions', default=default_input_positions): { Required('lower', default=None): Any(None, Positive(int)), Required('upper', default=None): Any(None, Positive(int)), Required('integrand', default=None): Any(None, Positive(int)), Required('integration_variable', default=None): Any(None, Positive(int)), }, Required('integrator_options', default={'full_output': 1}): { Required('full_output', default=1): 1, Extra: object }, Required('complex_integrand', default=False): bool, # Most of the below are copied from FormulaGrader Required('user_functions', default={}): { Extra: Any(is_callable, [is_callable], FunctionSamplingSet) }, Required('user_constants', default={}): { Extra: Number }, Required('blacklist', default=[]): [str], Required('whitelist', default=[]): [Any(str, None)], Required('tolerance', default='0.01%'): Any(PercentageString, NonNegative(Number)), Required('case_sensitive', default=True): bool, Required('samples', default=1): Positive(int), # default changed to 1 Required('variables', default=[]): [str], Required('sample_from', default={}): dict, Required('failable_evals', default=0): NonNegative(int) })
def schema_config(self): """Define the configuration options for FormulaGrader""" # Construct the default ItemGrader schema schema = super(FormulaGrader, self).schema_config # Append options forbidden_default = "Invalid Input: This particular answer is forbidden" return schema.extend({ Required('user_functions', default={}): {Extra: Any(is_callable, [is_callable], FunctionSamplingSet)}, Required('user_constants', default={}): {Extra: Number}, Required('blacklist', default=[]): [str], Required('whitelist', default=[]): [Any(str, None)], Required('forbidden_strings', default=[]): [str], Required('forbidden_message', default=forbidden_default): str, Required('required_functions', default=[]): [str], Required('tolerance', default='0.01%'): Any(PercentageString, NonNegative(Number)), Required('case_sensitive', default=True): bool, Required('metric_suffixes', default=False): bool, Required('samples', default=5): Positive(int), Required('variables', default=[]): [str], Required('sample_from', default={}): dict, Required('failable_evals', default=0): NonNegative(int) })
class RandomFunction(FunctionSamplingSet): # pylint: disable=too-few-public-methods """ Generates a random well-behaved function on demand. Currently implemented as a sum of trigonometric functions with random amplitude, frequency and phase. You can control the center and amplitude of the resulting oscillations by specifying center and amplitude. Config: input_dim (int): Number of input arguments. 1 is a unary function (default 1) output_dim (int): Number of output dimensions. 1 = scalar, more than 1 is a vector (default 1) num_terms (int): Number of random sinusoid terms to add together (default 3) center (float): Center around which oscillations occur (default 0) amplitude (float): Maximum amplitude of the function (default 10) Usage ===== Generate a random continous function >>> funcs = RandomFunction() By default, the generated functions are R-->R. You can specify the input and output dimensions: >>> funcs = RandomFunction(input_dim=3, output_dim=2) To control the range of the function, specify a center and amplitude. The bounds of the function will be center - amplitude < func(x) < center + amplitude. The following will give oscillations between 0 and 1. >>> funcs = RandomFunction(center=0.5, amplitude=0.5) """ schema_config = Schema({ Required('input_dim', default=1): Positive(int), Required('output_dim', default=1): Positive(int), Required('num_terms', default=3): Positive(int), Required('center', default=0): Number, Required('amplitude', default=10): Positive(Number) }) def gen_sample(self): """ Returns a randomly chosen 'nice' function. The output is a vector with output_dim dimensions: Y^i = sum_{jk} A^i_{jk} sin(B^i_{jk} X_k + C^i_{jk}) i ranges from 1 to output_dim j ranges from 1 to num_terms k ranges from 1 to input_dim """ # Generate arrays of random values for A, B and C output_dim = self.config['output_dim'] input_dim = self.config['input_dim'] num_terms = self.config['num_terms'] # Amplitudes A range from 0.5 to 1 A = np.random.rand(output_dim, num_terms, input_dim) / 2 + 0.5 # Angular frequencies B range from -pi to pi B = 2 * np.pi * (np.random.rand(output_dim, num_terms, input_dim) - 0.5) # Phases C range from 0 to 2*pi C = 2 * np.pi * np.random.rand(output_dim, num_terms, input_dim) def f(*args): """Function that generates the random values""" # Check that the dimensions are correct if len(args) != input_dim: msg = "Expected {} arguments, but received {}".format( input_dim, len(args)) raise ConfigError(msg) # Turn the inputs into an array xvec = np.array(args) # Repeat it into the shape of A, B and C xarray = np.tile(xvec, (output_dim, num_terms, 1)) # Compute the output matrix output = A * np.sin(B * xarray + C) # Sum over the j and k terms # We have an old version of numpy going here, so we can't use # fullsum = np.sum(output, axis=(1, 2)) fullsum = np.sum(np.sum(output, axis=2), axis=1) # Scale and translate to fit within center and amplitude fullsum = fullsum * self.config["amplitude"] / self.config[ "num_terms"] fullsum += self.config["center"] # Return the result return fullsum if output_dim > 1 else fullsum[0] return f