5.6. TestGeneration

module TestGeneration;
from StudentInterface import Student;
from TestGrading import Mean,Median,Mode,GradedQuestion;
from TestTaking import AnsweredQuestion;
from TestAdminister import Roster;
from QuestionModule import Question,Topic,Author,Difficulty,Time,ClassName,Type,Date,Year, ID;
from QuestionBankModule import QuestionBank,LocalQuestionBank,SharedQuestionBank;

export Test,Wizard,TestName,TotalPoints,TotalTime,PointsPossible,TestQuestion,TotalGrade,ClassTaken,Section,QuestionNumber;

(*-------------------- BEGIN MATT DeVORE'S SECTION --------------------*)

object Weight is integer
    description: (* Weight indicates the amount of representation a Topic
                        or question Type receives in a test. If a Topic has a
                        high weight, it will be touched upon more in the
                        generated test than a Topic with a lower weight. *);
end;

(* enum type: GenerationType *)
object GenerationType is
    components: manual:ManualGeneration or automatic:AutomaticGeneration;
    description: (* Represents one of the two generation methods available:
                ManualGeneration or AutomaticGeneration. This is a
                        parameter entered by the user in the first step of
                        generation. *);
end GenerationType;

object ManualGeneration is
    components: ;
     description: (* This is one of the two available methods that
                        teachers can use to generate a
                        test. ManualGeneration indicates that
                        the generated test will initially have no
                        questions, and that every question must be
                        searched for manually by the instructor and
                        inserted using the Edit Test interface. *);
end ManualGeneration;

object AutomaticGeneration is
    components: ;
    description: (* This is one of the two available methods that
                        teachers can use to generate a
                        test. AutomaticGeneration requires the teacher
                        to enter various criteria which allow the
                        software to automatically find a pool of
                        questions that satisfies the teacher's
                        needs. *);
end AutomaticGeneration;

(* enum type: Direction *)
object Direction is
    components: for:Forward or back:Backward;
    description: (*
        A Direction is equal to one of the two directions the user may
        be moving while walking between steps in the Test Generation
        Wizard. Forward indicates a clicking of the Next button.
        Backward indicates a clicking of the Previous button. *);
end Direction;
object Forward; object Backward;

object LowerLimit is
    components: integer;
    description: (*
        The lower limit of a Range. Ranges are entered in the fourth
        step of the Wizard. *);
end LowerLimit;

object UpperLimit is
    components: integer;
    description: (*
        The upper limit of a Range. Ranges are entered in the fourth
        step of the Wizard. *);
end UpperLimit;

object Range is
    components: lower:LowerLimit and upper:UpperLimit;
    description: (*
        Represents one of three ranges that the user
                enters in the fourth step of the Wizard.
        Each of the three Ranges are conveyed through
        three different object types that are defined
        simply "is Range": AllowedDifficulties, AverageDifficulty,
        and EstimatedTimeLengthOfTest. *);
end Range;

object AllowedDifficulties is Range;
object AverageDifficulty is Range;
object EstimatedTimeLengthOfTest is Range;

obj Class is
    components: roster:Roster, classname:ClassName, Quarter, Year, Instructor;
end Class;
obj CourseNumber is integer;
obj Section is integer;
obj Quarter is string;
obj Instructor is string;
object Comments is string;
object Grade is integer;
object CreatedDate is Date;
object ModifiedDate is Date;
object LastUsedDate is Date;
object Notes is string;
object EstimatedTimeTaken is real;
object SuccessRate is real;
object DateTaken is Date;
object ClassTaken is Class;
object TotalTime is integer;
object PointsPossible is integer;
object TotalPoints is PointsPossible;
object TotalGrade is integer;
object TestName is string;
object IsAvailable is boolean;
object QuestionNumber is integer;
object TestQuestion inherits from Question
    components: qt:Type, points:PointsPossible, num:QuestionNumber;
end TestQuestion;
object Test is
    components: name:TestName, totalTime:TotalTime, tp:TotalPoints, tg:TotalGrade,qs:TestQuestion*,aq:AnsweredQuestion*,gq:GradedQuestion*,
        Student, TotalGrade, DateTaken, Class, ClassTaken, Mean,Median,Mode, ia:IsAvailable;
    description: (*
        In this module, represents a generated test. However, in other modules, it may
        also represent a taken or graded test.*);
    operations: AddQuestion, RemoveQuestion, EditQuestion, MoveUp, MoveDown;
end Test;

object CriteriaRanges is
    components: allowedDiff:AllowedDifficulties,
        averageDiff:AverageDifficulty and
        estTimeLength:EstimatedTimeLengthOfTest;
    description: (*
        This object represents the three ranges that are entered during
        step four of the Wizard. Note that these ranges may not be
        blank. *);
end CriteriaRanges;

object WizardTopic is
    components: topic:Topic and weight:Weight;
    description: (*
        Contains both a Topic and that Topic's Weight. The Weight
        value is the number displayed in the Advanced Weight Specification
        dialog next to the Topic indicated in this object.*);
end WizardTopic;

object WizardType is
    components: qt:Type and weight:Weight;
    description: (*
        Contains both a Type and that Type's Weight.
        The Weight value is the number displayed in the Advanced Weight
        Specification dialog next to the Type indicated in
        this object. *);
end WizardType;

object CurrentStep is integer;
object FinishedFlag is boolean;
object IncludedClasses is ClassName*;
object IncludedTypes is WizardType*;
object IncludedTopics is WizardTopic*;

object Wizard is
    components: step:CurrentStep, finished:FinishedFlag, name:TestName,
         genType:GenerationType, classes:IncludedClasses,
        topics:IncludedTopics, types:IncludedTypes and
        critRanges:CriteriaRanges;
    operations: Step1, Step2, Step3, Step4, Finish, SmallestMarginOfError;
end Wizard;

(* The sole purpose of the StepX operations is to plant the user-given values
   for various criteria into the Wizard object as the Wizard proceeds. *)

operation Step1 is
    inputs: wiz:Wizard and testName:TestName and genType:GenerationType;
    outputs: wiz':Wizard;
    precondition: wiz.step = 1 and #testName > 0;
    postcondition:
        (wiz'.step = 2 and wiz'.name = testName);
    description: (* Plants the user-given values for generation criteria
                        that were entered in Step 1.  These values will be in
                        the output Wizard object. *);
end Step1;

operation Step2 is
    inputs: dir:Direction and wiz:Wizard and classes:ClassName*;
    outputs: wiz':Wizard;
    precondition: wiz.step = 2;
    postcondition:
        if (dir?for) then
            (wiz'.step = 3 and wiz'.classes = classes)
        else
            wiz'.step = 1;
    description: (* Plants the user-given values for generation criteria
                        that were entered in Step 2.  These values will be in
                        the output Wizard object. This is needed whenever the
                        user leaves Step 2 to go to Step 3 or Step 1. *);
end Step2;

operation Step3 is
    inputs: dir:Direction and wiz:Wizard and wizTops:WizardTopic*;
    outputs: wiz':Wizard;
    precondition: wiz.step = 3;
    postcondition:
        if (dir?for) then
            (wiz'.step = 4 and wiz'.topics = wizTops)
        else
            wiz'.step = 2;
    description: (* Plants the user-given values for generation criteria
                        that were entered in Step 3.  These values will be in
                        the output Wizard object. This is needed whenever the
                        user leaves Step 3 to go to Step 4 or Step 2. *);
end Step3;

operation Step4 is
    inputs: dir:Direction and wiz:Wizard and types:WizardType* and critRanges:CriteriaRanges;
    outputs: wiz':Wizard;
    precondition: wiz.step = 4;
    postcondition:
        if (dir?for) then
            (wiz'.finished = true and wiz'.step = 5 and wiz'.types = types
             and wiz'.critRanges = critRanges)
        else
            wiz'.step = 3;
    description: (* Plants the user-given values for generation criteria
                        that were entered in Step 4.  These values will be in
                        the output Wizard object. This is needed whenever the
                        user leaves Step 4 to go to Step 3 or to Finish the
                        generation. *);
end Step4;

operation Finish is
    inputs: wiz:Wizard, bank:TestQuestion*;
    outputs: test:Test;
    precondition: wiz.finished;
    postcondition:
        (* In case of manual generation, just return a blank test. *)
        (wiz.genType?manual and #(test.qs) = 0)
        or
        (let topicList = GetTopics (wiz.topics);
        let typeList = GetTypes (wiz.types);
        let error = SmallestMarginOfError (wiz, test);

        (* Margin of error cannot be so small so as to render the
           weights meaningless. *)
        error <= MinTypeWeight (wiz.types) and
        error <= MinTopicWeight (wiz.topics) and

        (* Only classes that have been specified for inclusion may be
           represented on the test. Same goes for topics and question
           types. *)
        HasValidClasses (test.qs, wiz.classes) and
        HasValidTopics (test.qs, topicList) and
        HasValidTypes (test.qs, typeList) and

        (* The user weights must correctly apply to the test
           questions. *)
        TopicWeightsMatch (wiz.topics, test.qs, error) and
        TypeWeightsMatch (wiz.types, test.qs, error) and

        (* The three ranges entered in step four must be adhered to. *)
        FitsRange (wiz.critRanges.allowedDiff,
               MinDifficulty (test.qs)) and

        FitsRange (wiz.critRanges.allowedDiff,
               MaxDifficulty (test.qs)) and

        FitsRange (wiz.critRanges.averageDiff,
               AverageDifficultyOfQuestions (test.qs)) and

        FitsRange (wiz.critRanges.estTimeLength,
               TotalTimeOfQuestions (test.qs)) and

        (* Test name must match the one entered exactly. *)
        test.name = wiz.name and

        (* Every question included in the test must be in the local
           question bank. *)
        forall (q in test.qs) (q in bank))

        or

        (* If the above doesn't work, try making the test again, but of
           a shorter length. *)
        (test = Finish (LowerMinimumTime(wiz), bank));
    description: (*
        This finishes the Test Generation Wizard by
        returning a new test that follows the given criteria
                that was entered in the Generation Wizard. These criteria
                are stored in the Wizard object. *);
end Finish;

operation SmallestMarginOfError is
    inputs: wiz:Wizard, test:Test;
    outputs: error:integer;
    precondition: wiz.finished;
    postcondition:
        TopicWeightsMatch (wiz.topics, test.qs, error) and
        TypeWeightsMatch (wiz.types, test.qs, error) and
        error >= 0 and
        not (exists (smallerError: integer)
            ((smallerError < error)
            and (TopicWeightsMatch (wiz.topics, test.qs, smallerError))
            and (TypeWeightsMatch (wiz.types, test.qs, smallerError))));
    description: (*
        This is not a user-visible operation, but can only be expressed
        through pre- and post-conditions. It returns the smallest margin
        of error required in order to make the given questions fit the
        user-entered weights. *);
end SmallestMarginOfError;

(* LowerMinimumTime: returns the same Wizard object, but the lower limit of the
   Estimated Length in Minutes range is decremented by one minute. This is used
   so that if the software cannot generate a test based on the given criteria,
   it reduces the amount of time required for the test to consume, and attempts
   generation again. See 2.2.1.2, subsection Weights Values Flexibility. *)
function LowerMinimumTime (wiz:Wizard) -> (Wizard)
        = {wiz.step, wiz.finished, wiz.name, wiz.genType,
           wiz.classes, wiz.topics,
           {wiz.critRanges.averageDiff,
                    wiz.critRanges.allowedDiff,
                {wiz.critRanges.estTimeLength.lower - 1,
                     wiz.critRanges.estTimeLength.upper}}};

function Min (x:integer, y:integer) -> (integer) = if (x < y) then x else y;
function Max (x:integer, y:integer) -> (integer) = if (x > y) then x else y;
function MinReal (x:real, y:real) -> (real) = if (x < y) then x else y;
function MaxReal (x:real, y:real) -> (real) = if (x > y) then x else y;

function TotalTimeOfQuestions (coll:TestQuestion*) -> (real) =
    if (#coll = 0)
    then 0
    else (coll[1].time + TotalTimeOfQuestions (coll - coll[1]))
end TotalTime;

(* Utility functions used for post/preconditions in the Finish operation *)

(* PointTotal: adds up the point value of every single question in the list. *)
function PointTotal (coll:TestQuestion*) -> (integer) =
    if (#coll = 0)
    then 0
    else (coll[1].points + PointTotal (coll - coll[1]))
end PointTotal;

(* Difficulty Total: adds up the difficulty rating of every single question in
   the list, for the purposes of finding the mean. *)
function DifficultyTotal (coll:TestQuestion*) -> (integer) =
    if (#coll = 0) then 0 else (coll[1].difficulty + DifficultyTotal (coll - coll[1]))
end DifficultyTotal;

(* AverageDifficultyOfQuestions: finds the mean difficulty rating of the
   questions in the list. *)
function AverageDifficultyOfQuestions (coll:TestQuestion*) -> (real) =
    DifficultyTotal(coll) / #coll
end AverageDifficultyOfQuestions;

(* Returns the lowest difficulty rating out of all the given questions. *)
function MinDifficulty (coll:TestQuestion*) -> (integer) =
    if (1 = #coll)
    then coll[1].difficulty
    else Min(coll[1].difficulty, MinDifficulty (coll - coll[1]))
end MinDifficulty;

(* Returns the highest difficulty rating out of all the given questions. *)
function MaxDifficulty (coll:TestQuestion*) -> (integer) =
    if (1 = #coll)
    then coll[1].difficulty
    else Max(coll[1].difficulty, MaxDifficulty (coll - coll[1]))
end MaxDifficulty;

(* Indicates the number of times that a particular topic is touched upon by
   any question, given a list of questions. *)
function TopicRepresentation (coll:TestQuestion*, what:Topic) -> (integer) =
    (let thisCount = if (what in coll[1].topics) then 1 else 0;
    if (1 = #coll)
    then thisCount
    else (thisCount + TopicRepresentation (coll - coll[1], what)))
end TopicRepresentation;

(* Finds all the questions of a given Type, then adds up their point
   values. *)
function TypeRepresentation (coll:TestQuestion*, what:Type) -> (integer) =
    (let thisCount = if (coll[1].qt = what) then coll[1].points else 0;
    if (1 = #coll)
    then thisCount
    else (thisCount + TypeRepresentation (coll - coll[1], what)))
end TypeRepresentation;

(* Returns true iff the question list has questions from a particular set of
   classes. If validClasses has CSC101 and CSC102, then the Question list
   can only have questions pertaining to those classes. *)
function HasValidClasses (coll:TestQuestion*, validClasses:ClassName*) -> (boolean) =
    (forall (q in coll)
        q.cn in validClasses)
end HasValidClasses;

(* Similar to the HasValidClasses function, but works on topics. *)
function HasValidTopics (coll:TestQuestion*, validTopics:Topic*) -> (boolean) =
    forall (q in coll)
        forall (t in q.topics)
            t in validTopics
end HasValidTopics;

function HasValidTypes (coll:TestQuestion*, validTypes:Type*) -> (boolean) =
    forall (q in coll)
        q.qt in validTypes
end HasValidTypes;

(* Given a list of WizardTopics, returns a list of Topics. This has the effect
   of  cutting away the weight values (see the WizardTopic object). *)
function GetTopics (coll:WizardTopic*) -> (Topic*) =
    if (0 = #coll)
    then empty
    else (GetTopics (coll - coll[1]) + coll[1].topic)
end GetTopics;

(* Given a list of WizardTypes, returns a list of Types. This has the effect
   of cutting away the weight values. (see the WizardType object).*)
function GetTypes (coll:WizardType*) -> (Type*) =
    if (0 = #coll)
    then empty
    else (GetTypes (coll - coll[1]) + coll[1].qt)
end GetTypes;

function FitsRange (r:Range, v:integer) -> (boolean) = ((v >= r.lower) and (v <= r.upper));

(* The ...WeightsMatch functions make sure the given weights match the representation
   of authors and topics in the given question list. A don't care weight is given by zero,
   so if any weight is non-positive, then that means the weights don't matter. *)
function TopicWeightsMatch (tops:WizardTopic*, qs:TestQuestion*, error:integer) -> (boolean) =
    exists (top in tops)
        top.weight <= 0
    or
    exists (factor:real)
        forall (top in tops)
            FitsRange ({-error, +error}, top.weight * factor - TopicRepresentation (qs, top.topic))
end TopicWeightsMatch;

function TypeWeightsMatch (types:WizardType*, qs:TestQuestion*, error:integer) -> (boolean) =
    exists (type in types)
        type.weight <= 0
    or
    exists (factor:real)
        forall (type in types)
            FitsRange ({-error, +error}, type.weight * factor - TypeRepresentation (qs, type.qt))
end TypeWeightsMatch;

(* Returns the weight of the lowest-weighted Type. *)
function MinTypeWeight (types:WizardType*) -> (real) =
    if (1 = #types)
    then types[1].weight
    else MinReal (types[1].weight, MinTypeWeight(types - types[1]))
end MinTypeWeight;

(* Returns the weight of the lowest-weighted Topic. *)
function MinTopicWeight (topics:WizardTopic*) -> (real) =
    if (1 = #topics)
    then topics[1].weight
    else MinReal (topics[1].weight, MinTopicWeight(topics - topics[1]))
end MinTopicWeight;

(*-------------------- END MATT DeVORE'S SECTION --------------------*)


(*-------------------- BEGIN Ben Williams's section------------------*)
operation AddQuestion is
    inputs: t:Test, q:Question, p:PointsPossible;
    outputs: t':Test;
    precondition: (* The given question is not in the given input  test. *)
        (not (exists (q' in t.qs) q'.id = q.id));
    postcondition:
        (* The given question is in the output test. *)
        (exists (newq in t'.qs) q.id = newq.id and newq.points = p) and
        (*
         * A question is in the output test if and only if it is the new
         * question or if it is in the input test.
         *)
        (forall (q':TestQuestion)
            (q' in t'.qs) iff ((q'.id = q.id) or (q' in t.qs)))
        and
        (*
         * There exists a test question in the output test that is the new
         * question with the same id, number of points, and is at the end
         * of the test.
         *)
        (exists (tq in t'.qs)
            (tq.id = q.id) and (tq.points = p) and (tq.num = #(t.qs) + 1));
    description: (*
        AddQuestion adds the given question to the given test, which in
        turn produces an updated test containing that question with the
        given number of points.
    *);
end AddQuestion;

operation RemoveQuestion is
    inputs: id:ID, t:Test;
    outputs: t':Test;
    precondition: (* The given question is in the given input test. *)
        ((exists (q' in t.qs) q'.id = id));
    postcondition:
        (* The given question is not in the output test. *)
        (not (exists (q' in t'.qs) (q'.id = id)))
        and
        (*
         * A question is in the output test if and only if it is not
         * the given question to be removed and if it is in the input
         *test.
         *)
        (forall (r':TestQuestion)
            (r' in t'.qs) iff ((r'.id != id) and (r' in t.qs)));
    description: (*
        RemoveQuestion removes the Question with the matching given ID
        from the given test, which in turn produces an updated Test
        without a question matching the given ID.
    *);
end RemoveQuestion;

operation EditQuestion
    inputs:  t:Test, id:ID, tq:TestQuestion;
    outputs:  t':Test;
    precondition: (* The question with the given id exists in the test. *)
        (exists (q in t.qs) (q.id = id) and (q.type = tq.type));
    postcondition:
        (* The new question is in the test *)
        tq in t.qs
        and
        (*
         *  The number of the editied question is the same as the
         *  question was before it was edited(replaced)
         *)
        (exists (q' in t.qs) (q'.id = id) and (q'.num = tq.num))
        and
        (*
         *  A question is only in the test if it was in the input test
         *  and is not the original question that will be edited or if
         *  it is the edited version of the the question that was changed.
         *)
        (forall (s':TestQuestion)
            (s' in t'.qs) iff (((s'.id != id) and (s' in t.qs)) or (s'.id = tq.id)));
    description: (*
        EditQuestion finds the test question on the given test with an ID
        matching the given ID.  This test question is replaced with the
        given updated or edited question and put back in the Test,
        which is then returned.
    *);
end EditQuestion;

operation MoveUp is
    inputs: t:Test, id:ID;
    outputs: t':Test;
    precondition:
        (*
         * The question to be moved up does not exist at the top
         * of the list (in the 1st question spot)
         *)
        (exists (q' in t.qs) (q'.id = id) and q'.num != 1);
    postcondition:
        (*
         * The number is moved up into the position 1 above it and the
         * number in that place is moved down to where the number to be
         * moved originally was.
         *)
        ((exists (q' in t.qs)
         (exists (q'' in t'.qs)
            (q'.id = id) and (q''.id = id) and (q''.num = q'.num - 1)
        and
        ((exists (r' in t.qs)
         (exists (r'' in t'.qs)
            (r'.id = r''.id) and (r'.num = q''.num) and (r''.num = q'.num)))))));
    description: (*
        * MoveUp moves the question matching the input id up one spot in the
        * tests order of questions.
    *);
end MoveUp;

operation MoveDown is
    inputs: t:Test, id:ID;
    outputs: t':Test;
    precondition:
        (*
         * The question to be moved up does not exist at the top
         * of the list (in the 1st question spot)
         *)
        (exists (q' in t.qs) ((q'.id = id) and (q'.num != #(t.qs))));
    postcondition:
        (*
         * The number is moved up into the position 1 above it and the
         * number in that place is moved down to where the number to be
         * moved originally was.
         *)
        ((exists (q' in t.qs)
         (exists (q'' in t'.qs)
            (q'.id = id) and (q''.id = id) and (q''.num = q'.num + 1)
        and   
        ((exists (r' in t.qs)
         (exists (r'' in t'.qs)
            (r'.id = r''.id) and (r'.num = q''.num) and (r''.num = q'.num)))))));
    description: (*
        * MoveDown moves the question matching the input id down one spot in the
        * tests order of questions.
    *);
end MoveDown;

(*-------------------- END Ben Williams's section--------------------*)

end TestGeneration;


Prev:
5.5. TestAdminister

Up:
5. Formal Specifications
Next:
5.7. TestGrading