Beispiel #1
0
def valueRequiringEqualTypes(val, thingA, thingB, typeError='type mismatch'):
    """Return the value, assuming thingA and thingB have the same type."""
    if not needsLazyEvaluation(thingA) and not needsLazyEvaluation(thingB):
        if underlyingType(thingA) is not underlyingType(thingB):
            raise RuntimeParseError(typeError)
        return val
    else:
        return TypeEqualityChecker(val, thingA, thingB, typeError)
Beispiel #2
0
def evaluateRequiringEqualTypes(func, thingA, thingB, typeError='type mismatch'):
	"""Evaluate the func, assuming thingA and thingB have the same type.

	If func produces a lazy value, it should not have any required properties beyond
	those of thingA and thingB."""
	if not needsLazyEvaluation(thingA) and not needsLazyEvaluation(thingB):
		if underlyingType(thingA) is not underlyingType(thingB):
			raise RuntimeParseError(typeError)
		return func()
	else:
		# cannot check the types now; create proxy object to check types after evaluation
		return TypeEqualityChecker(func, thingA, thingB, typeError)
Beispiel #3
0
		def evaluator():
			result = req()
			assert not needsSampling(result)
			if needsLazyEvaluation(result):
				raise InvalidScenarioError(f'requirement on line {line} uses value'
				                           ' undefined outside of object definition')
			return result
Beispiel #4
0
def constructScenarioFrom(namespace):
    """Build a Scenario object from an executed Scenic module."""
    # Extract ego object
    if namespace['_egoObject'] is None:
        raise InvalidScenarioError('did not specify ego object')

    # Extract workspace, if one is specified
    if 'workspace' in namespace:
        workspace = namespace['workspace']
        if not isinstance(workspace, Workspace):
            raise InvalidScenarioError(
                f'workspace {workspace} is not a Workspace')
        if needsSampling(workspace):
            raise InvalidScenarioError('workspace must be a fixed region')
        if needsLazyEvaluation(workspace):
            raise InvalidScenarioError('workspace uses value undefined '
                                       'outside of object definition')
    else:
        workspace = None

    # Create Scenario object
    scenario = Scenario(workspace, namespace['_objects'],
                        namespace['_egoObject'], namespace['_params'],
                        namespace['_externalParams'],
                        namespace['_requirements'],
                        namespace['_requirementDeps'])

    # Prune infeasible parts of the space
    if usePruning:
        pruning.prune(scenario, verbosity=verbosity)

    return scenario
Beispiel #5
0
 def getConstantPolygon(self):
     assert not any(
         needsSampling(c) or needsLazyEvaluation(c) for c in self.corners)
     # TODO refactor???
     corners = [(x, y)
                for x, y in self.corners]  # convert Vectors to tuples
     return shapely.geometry.Polygon(corners)
Beispiel #6
0
def constructScenarioFrom(namespace):
    # extract ego object
    if namespace['_egoObject'] is None:
        raise InvalidScenarioError('did not specify ego object')

    # extract workspace, if one is specified
    if 'workspace' in namespace:
        workspace = namespace['workspace']
        if not isinstance(workspace, Workspace):
            raise InvalidScenarioError(
                f'workspace {workspace} is not a Workspace')
        if needsSampling(workspace):
            raise InvalidScenarioError('workspace must be a fixed region')
        if needsLazyEvaluation(workspace):
            raise InvalidScenarioError('workspace uses value undefined '
                                       'outside of object definition')
    else:
        workspace = None

    scenario = Scenario(workspace, namespace['_objects'],
                        namespace['_egoObject'], namespace['_params'],
                        namespace['_requirements'],
                        namespace['_requirementDeps'])

    if usePruning:
        pruning.prune(scenario, verbosity=verbosity)

    return scenario
Beispiel #7
0
def toTypes(thing, types, typeError='wrong type'):
    """Convert something to any of the given types, printing an error if possible."""
    if needsLazyEvaluation(thing):
        # cannot check the type now; create proxy object to check type after evaluation
        return TypeChecker(thing, types, typeError)
    else:
        return coerceToAny(thing, types, typeError)
Beispiel #8
0
	def helper(self, *args, **kwargs):
		if any(needsSampling(arg) for arg in itertools.chain(args, kwargs.values())):
			return VectorMethodDistribution(method, self, args, kwargs)
		elif any(needsLazyEvaluation(arg) for arg in itertools.chain(args, kwargs.values())):
			# see analogous comment in distributionFunction
			return makeDelayedFunctionCall(helper, (self,) + args, kwargs)
		else:
			return method(self, *args, **kwargs)
Beispiel #9
0
def test_distribution_method_encapsulation_lazy():
    vf = VectorField("Foo", lambda pos: 0)
    da = DelayedArgument(set(), lambda context: Options([1, 2]))
    pt = vf.followFrom(Vector(0, 0), da, steps=1)
    assert isinstance(pt, DelayedArgument)
    evpt = valueInContext(pt, {})
    assert not needsLazyEvaluation(evpt)
    assert isinstance(evpt, VectorMethodDistribution)
    assert evpt.method is underlyingFunction(vf.followFrom)
Beispiel #10
0
def test_delayed_call():
    da = DelayedArgument(('blob', ), lambda context:
                         (lambda x, y=1: x * y + context.blob))
    res = da(42, y=2)
    assert isinstance(res, DelayedArgument)
    Thingy = namedtuple('Thingy', 'blob')
    evres = valueInContext(res, Thingy(blob=-7))
    assert not needsLazyEvaluation(evres)
    assert evres == 77
Beispiel #11
0
 def polygon(self):
     position, heading, hw, hh = self.position, self.heading, self.hw, self.hh
     if any(
             needsSampling(c) or needsLazyEvaluation(c)
             for c in (position, heading, hw, hh)):
         return None  # can only convert fixed Regions to Polygons
     corners = RotatedRectangle.makeCorners(position.x, position.y, heading,
                                            hw, hh)
     return shapely.geometry.Polygon(corners)
Beispiel #12
0
def test_delayed_call():
    da = DelayedArgument(('blob', ), lambda context:
                         (lambda x, y=1: x * y + context.blob))
    res = da(42, y=2)
    assert isinstance(res, DelayedArgument)
    context = LazilyEvaluable.makeContext(blob=-7)
    evres = valueInContext(res, context)
    assert not needsLazyEvaluation(evres)
    assert evres == 77
Beispiel #13
0
 def applyTo(self, obj, optionals):
     """Apply specifier to an object, including the specified optional properties."""
     val = self.value.evaluateIn(obj)
     val = toDistribution(val)
     assert not needsLazyEvaluation(val)
     obj._specify(self.property, val)
     for opt in optionals:
         assert opt in self.optionals
         obj._specify(opt, getattr(val, opt))
Beispiel #14
0
	def __init__(self, dependencies):
		deps = []
		props = set()
		for dep in dependencies:
			if needsSampling(dep) or needsLazyEvaluation(dep):
				deps.append(dep)
				props.update(requiredProperties(dep))
		super().__init__(props)
		self._dependencies = tuple(deps)	# fixed order for reproducibility
		self._conditioned = self	# version (partially) conditioned on requirements
Beispiel #15
0
 def handler2(self, *args):
     if needsSampling(self):
         return VectorOperatorDistribution(op, self, args)
     elif any(needsSampling(arg) for arg in args):
         return VectorMethodDistribution(method, self, args, {})
     elif any(needsLazyEvaluation(arg) for arg in args):
         # see analogous comment in distributionFunction
         return makeDelayedFunctionCall(handler2, args, {})
     else:
         return method(self, *args)
Beispiel #16
0
		def helper(*args):
			if needsSampling(instance):
				return VectorOperatorDistribution(op, instance, args)
			elif any(needsSampling(arg) for arg in args):
				return VectorMethodDistribution(method, instance, args, {})
			elif any(needsLazyEvaluation(arg) for arg in args):
				# see analogous comment in distributionFunction
				return makeDelayedFunctionCall(helper, args, {})
			else:
				return wrapped(*args)
Beispiel #17
0
		def helper(*args, **kwargs):
			args = tuple(toDistribution(arg) for arg in args)
			kwargs = { name: toDistribution(arg) for name, arg in kwargs.items() }
			if any(needsSampling(arg) for arg in itertools.chain(args, kwargs.values())):
				return MethodDistribution(method, instance, args, kwargs)
			elif any(needsLazyEvaluation(arg)
			         for arg in itertools.chain(args, kwargs.values())):
				# see analogous comment in distributionFunction
				return makeDelayedFunctionCall(helper, args, kwargs)
			else:
				return wrapped(*args, **kwargs)
Beispiel #18
0
		def helper(*args, **kwargs):
			args = tuple(toDistribution(arg) for arg in args)
			kwargs = { name: toDistribution(arg) for name, arg in kwargs.items() }
			if any(needsSampling(arg) for arg in itertools.chain(args, kwargs.values())):
				return FunctionDistribution(wrapped, args, kwargs, support, valueType)
			elif any(needsLazyEvaluation(arg)
			         for arg in itertools.chain(args, kwargs.values())):
				# recursively call this helper (not the original function), since the
				# delayed arguments may evaluate to distributions, in which case we'll
				# have to make a FunctionDistribution
				return makeDelayedFunctionCall(helper, args, kwargs)
			else:
				return wrapped(*args, **kwargs)
Beispiel #19
0
def toDistribution(val):
	"""Wrap Python data types with Distributions, if necessary.

	For example, tuples containing Samplables need to be converted into TupleDistributions
	in order to keep track of dependencies properly.
	"""
	if isinstance(val, (tuple, list)):
		coords = [toDistribution(c) for c in val]
		if any(needsSampling(c) or needsLazyEvaluation(c) for c in coords):
			if isinstance(val, tuple) and hasattr(val, '_fields'):		# namedtuple
				builder = type(val)._make
			else:
				builder = type(val)
			return TupleDistribution(*coords, builder=builder)
	return val
Beispiel #20
0
def require(reqID, req, line, prob=1):
	"""Function implementing the require statement."""
	if evaluatingRequirement:
		raise RuntimeParseError('tried to create a requirement inside a requirement')
	if currentSimulation is not None:	# requirement being evaluated at runtime
		if prob >= 1 or random.random() <= prob:
			result = req()
			assert not needsSampling(result)
			if needsLazyEvaluation(result):
				raise RuntimeParseError(f'requirement on line {line} uses value'
										' undefined outside of object definition')
			if not result:
				raise RejectSimulationException(f'requirement on line {line}')
	else:	# requirement being defined at compile time
		currentScenario._addRequirement(requirements.RequirementType.require,
                                        reqID, req, line, prob)
Beispiel #21
0
    def compile(self, namespace, scenario, syntax=None):
        """Create a closure testing the requirement in the correct runtime state.

        While we're at it, determine whether the requirement implies any relations
        we can use for pruning, and gather all of its dependencies.
        """
        bindings, ego, line = self.bindings, self.egoObject, self.line
        condition = self.condition

        # Check whether requirement implies any relations used for pruning
        if self.ty.constrainsSampling and syntax:
            relations.inferRelationsFrom(syntax, bindings, ego, line)

        # Gather dependencies of the requirement
        deps = set()
        for value in bindings.values():
            if needsSampling(value):
                deps.add(value)
            if needsLazyEvaluation(value):
                raise InvalidScenarioError(
                    f'requirement on line {line} uses value {value}'
                    ' undefined outside of object definition')
        if ego is not None:
            assert isinstance(ego, Samplable)
            deps.add(ego)

        # Construct closure
        def closure(values):
            global evaluatingRequirement, currentScenario
            # rebind any names referring to sampled objects
            for name, value in bindings.items():
                if value in values:
                    namespace[name] = values[value]
            # rebind ego object, which can be referred to implicitly
            boundEgo = None if ego is None else values[ego]
            # evaluate requirement condition, reporting errors on the correct line
            import scenic.syntax.veneer as veneer
            with veneer.executeInRequirement(scenario, boundEgo):
                result = condition()
                assert not needsSampling(result)
                if needsLazyEvaluation(result):
                    raise InvalidScenarioError(
                        f'requirement on line {line} uses value'
                        ' undefined outside of object definition')
            return result

        return CompiledRequirement(self, closure, deps)
Beispiel #22
0
 def closure(values):
     global evaluatingRequirement, currentScenario
     # rebind any names referring to sampled objects
     for name, value in bindings.items():
         if value in values:
             namespace[name] = values[value]
     # rebind ego object, which can be referred to implicitly
     boundEgo = None if ego is None else values[ego]
     # evaluate requirement condition, reporting errors on the correct line
     import scenic.syntax.veneer as veneer
     with veneer.executeInRequirement(scenario, boundEgo):
         result = condition()
         assert not needsSampling(result)
         if needsLazyEvaluation(result):
             raise InvalidScenarioError(
                 f'requirement on line {line} uses value'
                 ' undefined outside of object definition')
     return result
Beispiel #23
0
    def _toScenario(self, namespace):
        assert self._prepared

        if self._ego is None and self._compose is None:
            msg = 'did not specify ego object'
            modScenarios = namespace['_scenarios']
            if self._dummyNamespace and len(modScenarios) == 1:
                if modScenarios[0]._requiresArguments():
                    msg += (
                        '\n(Note: this Scenic file contains a modular scenario, but it\n'
                        'cannot be used as the top-level scenario since it requires\n'
                        'arguments; so the whole file is being used as a top-level\n'
                        'scenario and needs an ego object.)')
                else:
                    msg += (
                        '\n(Note: this Scenic file contains a modular scenario, but also\n'
                        'other code; so the whole file is being used as a top-level\n'
                        'scenario and needs an ego object.)')
            raise InvalidScenarioError(msg)

        # Extract workspace, if one is specified
        if 'workspace' in namespace:
            workspace = namespace['workspace']
            if not isinstance(workspace, Workspace):
                raise InvalidScenarioError(
                    f'workspace {workspace} is not a Workspace')
            if needsSampling(workspace):
                raise InvalidScenarioError('workspace must be a fixed region')
            if needsLazyEvaluation(workspace):
                raise InvalidScenarioError('workspace uses value undefined '
                                           'outside of object definition')
        else:
            workspace = None

        from scenic.core.scenarios import Scenario
        scenario = Scenario(workspace, self._simulatorFactory, self._objects,
                            self._ego, self._globalParameters,
                            self._externalParameters, self._requirements,
                            self._requirementDeps, self._monitors,
                            self._behaviorNamespaces,
                            self)  # TODO unify these!
        return scenario
Beispiel #24
0
 def closure(values):
     # rebind any names referring to sampled objects
     for name, value in bindings.items():
         if value in values:
             namespace[name] = values[value]
     # rebind ego object, which can be referred to implicitly
     if ego is not None:
         veneer.egoObject = values[ego]
     # evaluate requirement condition, reporting errors on the correct line
     try:
         veneer.evaluatingRequirement = True
         result = req()
         assert not needsSampling(result)
         if needsLazyEvaluation(result):
             raise RuntimeParseError(
                 f'requirement on line {line} uses value'
                 ' undefined outside of object definition')
         return result
     except RuntimeParseError as e:
         cause = e if showInternalBacktrace else None
         raise InterpreterParseError(e, line) from cause
     finally:
         veneer.evaluatingRequirement = False
Beispiel #25
0
	def evaluateIn(self, context):
		"""See `LazilyEvaluable.evaluateIn`."""
		value = super().evaluateIn(context)
		# Check that all dependencies have been evaluated
		assert all(not needsLazyEvaluation(dep) for dep in value._dependencies)
		return value
Beispiel #26
0
    def generate(self, maxIterations=2000, verbosity=0, feedback=None):
        objects = self.objects

        # choose which custom requirements will be enforced for this sample
        activeReqs = [
            req for req, prob in self.requirements if random.random() <= prob
        ]

        # do rejection sampling until requirements are satisfied
        rejection = True
        iterations = 0
        while rejection is not None:
            if iterations > 0:  # rejected the last sample
                if verbosity >= 2:
                    print(
                        f'  Rejected sample {iterations} because of: {rejection}'
                    )
                if self.externalSampler is not None:
                    feedback = self.externalSampler.rejectionFeedback
            if iterations >= maxIterations:
                raise RuntimeError(
                    f'failed to generate scenario in {iterations} iterations')
            iterations += 1
            try:
                if self.externalSampler is not None:
                    self.externalSampler.sample(feedback)
                sample = Samplable.sampleAll(self.dependencies)
            except RejectionException as e:
                rejection = e
                continue
            rejection = None
            ego = sample[self.egoObject]
            # Normalize types of some built-in properties
            for obj in objects:
                sampledObj = sample[obj]
                assert not needsSampling(sampledObj)
                assert isinstance(sampledObj.position, Vector)
                sampledObj.heading = float(sampledObj.heading)
            # Check built-in requirements
            for i in range(len(objects)):
                vi = sample[objects[i]]
                # Require object to be contained in the workspace/valid region
                container = self.containerOfObject(vi)
                if not container.containsObject(vi):
                    rejection = 'object containment'
                    break
                # Require object to be visible from the ego object
                if vi.requireVisible and vi is not ego and not ego.canSee(vi):
                    rejection = 'object visibility'
                    break
                # Require object to not intersect another object
                for j in range(i):
                    vj = sample[objects[j]]
                    if vi.intersects(vj):
                        rejection = 'object intersection'
                        break
                if rejection is not None:
                    break
            if rejection is not None:
                continue
            # Check user-specified requirements
            for req in activeReqs:
                if not req(sample):
                    rejection = 'user-specified requirement'
                    break

        # obtained a valid sample; assemble a scene from it
        sampledObjects = tuple(sample[obj] for obj in objects)
        sampledParams = {}
        for param, value in self.params.items():
            sampledValue = sample[value] if isinstance(value,
                                                       Samplable) else value
            assert not needsLazyEvaluation(sampledValue)
            sampledParams[param] = sampledValue
        scene = Scene(self.workspace, sampledObjects, ego, sampledParams)
        return scene, iterations
Beispiel #27
0
 def polygon(self):
     if any(
             needsSampling(c) or needsLazyEvaluation(c)
             for c in self.corners):
         return None  # can only convert fixed Regions to Polygons
     return self.getConstantPolygon()
Beispiel #28
0
    def generate(self, maxIterations=2000, verbosity=0, feedback=None):
        """Sample a `Scene` from this scenario.

		Args:
			maxIterations (int): Maximum number of rejection sampling iterations.
			verbosity (int): Verbosity level.
			feedback (float): Feedback to pass to external samplers doing active sampling.
				See :mod:`scenic.core.external_params`.

		Returns:
			A pair with the sampled `Scene` and the number of iterations used.

		Raises:
			`RejectionException`: if no valid sample is found in **maxIterations** iterations.
		"""
        objects = self.objects

        # choose which custom requirements will be enforced for this sample
        activeReqs = [
            req for req in self.initialRequirements
            if random.random() <= req.prob
        ]

        # do rejection sampling until requirements are satisfied
        rejection = True
        iterations = 0
        while rejection is not None:
            if iterations > 0:  # rejected the last sample
                if verbosity >= 2:
                    print(
                        f'  Rejected sample {iterations} because of: {rejection}'
                    )
                if self.externalSampler is not None:
                    feedback = self.externalSampler.rejectionFeedback
            if iterations >= maxIterations:
                raise RejectionException(
                    f'failed to generate scenario in {iterations} iterations')
            iterations += 1
            try:
                if self.externalSampler is not None:
                    self.externalSampler.sample(feedback)
                sample = Samplable.sampleAll(self.dependencies)
            except RejectionException as e:
                rejection = e
                continue
            rejection = None
            ego = sample[self.egoObject]
            # Normalize types of some built-in properties
            for obj in objects:
                sampledObj = sample[obj]
                assert not needsSampling(sampledObj)
                # position, heading
                assert isinstance(sampledObj.position, Vector)
                sampledObj.heading = float(sampledObj.heading)
                # behavior
                behavior = sampledObj.behavior
                if behavior is not None and not isinstance(
                        behavior, veneer.Behavior):
                    raise InvalidScenarioError(
                        f'behavior {behavior} of Object {obj} is not a behavior'
                    )

            # Check built-in requirements
            for i in range(len(objects)):
                vi = sample[objects[i]]
                # Require object to be contained in the workspace/valid region
                container = self.containerOfObject(vi)
                if not container.containsObject(vi):
                    rejection = 'object containment'
                    break
                # Require object to be visible from the ego object
                if vi.requireVisible and vi is not ego and not ego.canSee(vi):
                    rejection = 'object visibility'
                    break
                # Require object to not intersect another object
                for j in range(i):
                    vj = sample[objects[j]]
                    if vi.intersects(vj):
                        rejection = 'object intersection'
                        break
                if rejection is not None:
                    break
            if rejection is not None:
                continue
            # Check user-specified requirements
            for req in activeReqs:
                if not req.satisfiedBy(sample):
                    rejection = f'user-specified requirement (line {req.line})'
                    break

        # obtained a valid sample; assemble a scene from it
        sampledObjects = tuple(sample[obj] for obj in objects)
        sampledParams = {}
        for param, value in self.params.items():
            sampledValue = sample[value]
            assert not needsLazyEvaluation(sampledValue)
            sampledParams[param] = sampledValue
        sampledNamespaces = {}
        for modName, namespace in self.behaviorNamespaces.items():
            sampledNamespace = {
                name: sample[value]
                for name, value in namespace.items()
            }
            sampledNamespaces[modName] = (namespace, sampledNamespace,
                                          namespace.copy())
        alwaysReqs = (veneer.BoundRequirement(req, sample)
                      for req in self.alwaysRequirements)
        terminationConds = (veneer.BoundRequirement(req, sample)
                            for req in self.terminationConditions)
        termSimulationConds = (veneer.BoundRequirement(req, sample)
                               for req in self.terminateSimulationConditions)
        scene = Scene(self.workspace, sampledObjects, ego, sampledParams,
                      alwaysReqs, terminationConds, termSimulationConds,
                      self.monitors, sampledNamespaces, self.dynamicScenario)
        return scene, iterations
Beispiel #29
0
def storeScenarioStateIn(namespace, requirementSyntax, filename):
    # extract created Objects
    namespace['_objects'] = tuple(veneer.allObjects)
    namespace['_egoObject'] = veneer.egoObject

    # extract global parameters
    namespace['_params'] = veneer.globalParameters
    for name, value in veneer.globalParameters.items():
        if needsLazyEvaluation(value):
            raise InvalidScenarioError(
                f'parameter {name} uses value {value}'
                ' undefined outside of object definition')

    # extract requirements and create proper closures
    requirements = veneer.pendingRequirements
    finalReqs = veneer.inheritedReqs
    requirementDeps = set(
    )  # things needing to be sampled to evaluate the requirements
    namespace['_requirements'] = finalReqs
    namespace['_requirementDeps'] = requirementDeps

    def makeClosure(req, bindings, ego, line):
        def closure(values):
            # rebind any names referring to sampled objects
            for name, value in bindings.items():
                if value in values:
                    namespace[name] = values[value]
            # rebind ego object, which can be referred to implicitly
            if ego is not None:
                veneer.egoObject = values[ego]
            # evaluate requirement condition, reporting errors on the correct line
            try:
                veneer.evaluatingRequirement = True
                result = req()
                assert not needsSampling(result)
                if needsLazyEvaluation(result):
                    raise RuntimeParseError(
                        f'requirement on line {line} uses value'
                        ' undefined outside of object definition')
                return result
            except RuntimeParseError as e:
                cause = e if showInternalBacktrace else None
                raise InterpreterParseError(e, line) from cause
            finally:
                veneer.evaluatingRequirement = False

        return closure

    for reqID, (req, bindings, ego, line, prob) in requirements.items():
        # Check whether requirement implies any relations used for pruning
        reqNode = requirementSyntax[reqID]
        relations.inferRelationsFrom(reqNode, bindings, ego, line)
        # Gather dependencies of the requirement
        for value in bindings.values():
            if needsSampling(value):
                requirementDeps.add(value)
            if needsLazyEvaluation(value):
                raise InvalidScenarioError(
                    f'requirement on line {line} uses value {value}'
                    ' undefined outside of object definition')
        if ego is not None:
            assert isinstance(ego, Samplable)
            requirementDeps.add(ego)
        # Construct closure
        finalReqs.append((makeClosure(req, bindings, ego, line), prob))
Beispiel #30
0
def storeScenarioStateIn(namespace, requirementSyntax, lineMap, filename):
    """Post-process an executed Scenic module, extracting state from the veneer."""
    # Extract created Objects
    namespace['_objects'] = tuple(veneer.allObjects)
    namespace['_egoObject'] = veneer.egoObject

    # Extract global parameters
    namespace['_params'] = veneer.globalParameters
    for name, value in veneer.globalParameters.items():
        if needsLazyEvaluation(value):
            raise InvalidScenarioError(
                f'parameter {name} uses value {value}'
                ' undefined outside of object definition')

    # Extract external parameters
    namespace['_externalParams'] = tuple(veneer.externalParameters)

    # Extract requirements, scan for relations used for pruning, and create closures
    requirements = veneer.pendingRequirements
    finalReqs = veneer.inheritedReqs
    requirementDeps = set(
    )  # things needing to be sampled to evaluate the requirements
    namespace['_requirements'] = finalReqs
    namespace['_requirementDeps'] = requirementDeps

    def makeClosure(req, bindings, ego, line):
        """Create a closure testing the requirement in the correct runtime state."""
        def evaluator():
            result = req()
            assert not needsSampling(result)
            if needsLazyEvaluation(result):
                raise InvalidScenarioError(
                    f'requirement on line {line} uses value'
                    ' undefined outside of object definition')
            return result

        def closure(values):
            # rebind any names referring to sampled objects
            for name, value in bindings.items():
                if value in values:
                    namespace[name] = values[value]
            # rebind ego object, which can be referred to implicitly
            if ego is not None:
                veneer.egoObject = values[ego]
            # evaluate requirement condition, reporting errors on the correct line
            try:
                veneer.evaluatingRequirement = True
                result = executePythonFunction(evaluator, lineMap, filename)
            finally:
                veneer.evaluatingRequirement = False
            return result

        return closure

    for reqID, (req, bindings, ego, line, prob) in requirements.items():
        # Check whether requirement implies any relations used for pruning
        reqNode = requirementSyntax[reqID]
        relations.inferRelationsFrom(reqNode, bindings, ego, line, lineMap)
        # Gather dependencies of the requirement
        for value in bindings.values():
            if needsSampling(value):
                requirementDeps.add(value)
            if needsLazyEvaluation(value):
                raise InvalidScenarioError(
                    f'requirement on line {line} uses value {value}'
                    ' undefined outside of object definition')
        if ego is not None:
            assert isinstance(ego, Samplable)
            requirementDeps.add(ego)
        # Construct closure
        finalReqs.append((makeClosure(req, bindings, ego, line), prob))