diff --git a/clientFilesCourse/student_images/.002.png.icloud b/clientFilesCourse/student_images/.002.png.icloud deleted file mode 100644 index 993f5564..00000000 Binary files a/clientFilesCourse/student_images/.002.png.icloud and /dev/null differ diff --git a/courseInstances/Interactive-Graphs/assessments/Interactive_Graph_Examples/infoAssessment.json b/courseInstances/Interactive-Graphs/assessments/Interactive_Graph_Examples/infoAssessment.json new file mode 100644 index 00000000..906e0728 --- /dev/null +++ b/courseInstances/Interactive-Graphs/assessments/Interactive_Graph_Examples/infoAssessment.json @@ -0,0 +1,39 @@ +{ + "uuid": "7046c3d0-567a-4e1d-a352-ce82d031f7ab", + "type": "Homework", + "title": "Interactive Graph Examples", + "set": "Homework", + "number": "1", + "allowAccess": [], + "zones": [ + { + "comment": "These are new questions created for the pl-interactive-graph element", + "questions": [ + { + "id": "pl-interactive-graph-examples/BFS_Example", + "autoPoints": 1 + }, + { + "id": "pl-interactive-graph-examples/DFS_Example", + "autoPoints": 1 + }, + { + "id": "pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example", + "autoPoints": 1 + }, + { + "id": "pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example", + "autoPoints": 1 + }, + { + "id": "pl-interactive-graph-examples/Clickable_Nodes_Dijkstras", + "autoPoints": 1 + }, + { + "id": "pl-interactive-graph-examples/Clickable_Edges_Kruskals", + "autoPoints": 1 + } + ] + } + ] +} diff --git a/courseInstances/Interactive-Graphs/infoCourseInstance.json b/courseInstances/Interactive-Graphs/infoCourseInstance.json new file mode 100644 index 00000000..d4ece3cb --- /dev/null +++ b/courseInstances/Interactive-Graphs/infoCourseInstance.json @@ -0,0 +1,18 @@ +{ + "uuid": "A4C7C4DF-9FF6-493E-BA61-7EB054B97B64", + "longName": "Interactive-Graphs", + "allowAccess": [ + { + "institution": "LTI", + "startDate": "2023-10-23T00:00:01", + "endDate": "2024-06-01T23:59:59" + }, + { + "institution": "Any", + "startDate": "2023-10-23T00:00:01", + "endDate": "2024-06-01T23:59:59", + "uids": [ + ] + } + ] +} diff --git a/elements/.DS_Store b/elements/.DS_Store deleted file mode 100644 index 1d6e8320..00000000 Binary files a/elements/.DS_Store and /dev/null differ diff --git a/elements/pl-interactive-graph/README.md b/elements/pl-interactive-graph/README.md new file mode 100644 index 00000000..c2483554 --- /dev/null +++ b/elements/pl-interactive-graph/README.md @@ -0,0 +1,103 @@ + +# pl-interactive-graph for PrairieLearn + +## Overview +`pl-interactive-graph` is a custom interactive element for PrairieLearn, designed for creating and interacting with graph-based questions. It supports various functionalities like graph traversal, visualization, and interactive node/edge selection. The element is built on top of the existing `pl-graph` element. + +## Usage +To use the `pl-interactive-graph` element in your PrairieLearn course: + +1. **Include the Element in Your Question**: Embed the custom element tag ``in your question HTML file. +2. **Define the Graph**: Specify the graph structure within the tag using your desired graph generation method. Use DOT language to specify your graph, to learn how to use DOT language, navigate to https://graphviz.org/. *Note:* If you would like randomized graphs, you do not need to use the DOT language. +3. **Set Attributes**: Customize the behavior and appearance of the graph using XML attributes. Attributes numbered 9-16 are used for random graphs. If you are not using a random graph, these attributes add nothing. Further, attributes 16 and on are part of the existing `pl-graph` element. The element supports a variety of attributes to cater to different question types and requirements: + + 1. `preserve-ordering`: String. If set to `"True"`, it requires the answer sequence to match exactly. + + 2. `answers`: String. String of an array of node labels representing the correct answer. (Example: '["A","B","C"]' for nodes, '["A--B","B--C"]' for undirected edges and '["A->B","B->C"]' for directed edges) + + 3. `grading`: String. Can be 'bfs', 'dfs' and 'dijkstras' - if selected will automatically grade the answers based on the algorithm + + 4. `partial-credit`: String. If set to `"True"`, it allows partial credit for partially correct sequences. + + 5. `node-fill-color`: String. If set, changes the fill color of selected nodes to that color (default is "red"). + + 6. `edge-fill-color`: String. If set, changes the fill color of selected edges to that color (default is "green"). + + 7. `select-nodes`: Boolean. If set to `True`, it allows the user to click on nodes for selection for interaction. + + 8. `select-edges`: Boolean. If set to `True`, it allows the user to click on edges for selection for interaction. + + 9. `random-graph`: Boolean. If set to `True`, random graphs will be generated. + + 10. `directed-random`: Boolean. If set to `True`, random graphs will be generated with directed edges. + + 11. `min-nodes`: Integer. Defines the minimum number of nodes in a random graph. + + 12. `max-nodes`: Integer. Defines the maximum number of nodes in a random graph. + + 13. `min-edges`: Integer. Defines the minimum number of edges in a random graph. + + 14. `max-edges`: Integer. Defines the maximum number of edges in a random graph. + + 15. `weighted`: Boolean. Specifies if a random graph should have random edge weights. + + 16. `tree`: Boolean. Specifies if the random graph should be a tree. + + 17. `directed`: Boolean. Whether to treat edges in an adjacency matrix as directed or undirected. If set to false, then edges will be rendered as undirected. The input adjacency matrix must be symmetric if this is set to false. + + 18. `connected`: Boolean. Specifies if the random graph should be connected. + + 19. `engine`: String. The rendering engine to use; supports circo, dot, fdp, neato, osage, and twopi. + + 20. `params-name`: String. The the name of a parameter containing the data to use as input. Data type to use depends on params-type attribute. + + 21. `params-name-labels`: String. When using an adjacency matrix, the parameter that contains the labels for each node. + + 22. `params-type`: String. Type of graph representation, e.g., `"adjacency-matrix"` or `"networkx"`. + + 23. `weights`: Boolean. When using an adjacency matrix, whether or not to show the edge weights. By default will automatically show weights for stochastic matrices (when they are not binary 0/1). + + 24. `weights-digits`: Integer. When using an adjacency matrix, how many digits to show for the weights. + + 25. `negative-weights`: Boolean. Whether to recognize negative weights in an adjacency matrix. If set to false, then all weights at most 0 are ignored (not counted as an edge). If set to true, then all weights that are not None are recognized. + + 26. `weights-presentation-type`: String. Number display format for the weights when using an adjacency matrix. If presentation-type is 'sigfig', each number is formatted using the to_precision module to digits significant figures. Otherwise, each number is formatted as {:.{digits}{presentation-type}}. + + 27. `log-warnings`: Boolean. Whether to log warnings that occur during Graphviz rendering. + +Some of the attributes have been inherited from pl-graph, here is more information on those specific inherited attributes: https://prairielearn.readthedocs.io/en/latest/elements/#pl-graph-element + +4. **Modify server.py if Needed**: Determine how would you want to grade the question. To access the order given by the student as the nodes were clicked, you can do student_answer = data["submitted_answers"]["selectedNodes"]. Note: If you have used custom attributes, like preserve-ordering or answers, this part might be different. There is existing autograding if answers are provided in the `` as `` for edges, `` for undirected edges and `` for directed edges as per DOT language. + + +## Description +The students will be presented with a graph of your specified structure and each node and edge will be clickable according to your selection. Students can click the nodes/edges and depending on the element attribute values, the order might matter (they can also unclick nodes). A list of clicked nodes and/or edges in the corresponding order will be shown right under the graph. When the students click "submit" the element will record the clicked nodes and provide them to the backend. The backend has 2 options for grading, one with provision of direct answers through the 'answers' attribute and automatic through the 'grading' attribute, which allows to select of an algorithm to automatically grade the submission for both random and provided graph. + +[Here](https://docs.google.com/presentation/d/1Dr3IpX5KgqjYPDt15EAJK48x462bg-Tt8RRgpj-p_MM/edit?usp=sharing) is our slide deck for the Spring 2024 semester. + +## Suggested Use +This element is not only limited to purely graph traversal questions. Some of the possible problems that could be modelled by this element are (but not limited to): Network Flow, Finite State Machines, Pathfinding Algorithms, etc. + +## Example +Different examples have been included in the questions folder. They are titled `pl-interactive-graph-examples/BFS_Example`, `pl-interactive-graph-examples/DFS_Example`, `pl-interactive-graph-examples/Clickable_Edges_Kruskals`, `pl-interactive-graph-examples/Clickable_Nodes_Dijkstras`, `pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine`, `pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example`, `pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example` and `pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine` + +Here's an example of how you might use `pl-interactive-graph` in a question about graph traversal: + +```html +

You are provided a graph below. Click on the nodes in the order they will be selected if we run Breadth-First Search (BFS) algorithm on this graph. You can see the order of your clicked list in a list under the graph and you are allowed to unclick and deselect nodes at any point. Press "Save & Grade" to submit your answer.

+ + + digraph G { + A -> B; + A -> C; + B -> D; + B -> E; + C -> F; + C -> G; + E -> H; + } + + + + + diff --git a/elements/pl-interactive-graph/info.json b/elements/pl-interactive-graph/info.json new file mode 100644 index 00000000..564c7245 --- /dev/null +++ b/elements/pl-interactive-graph/info.json @@ -0,0 +1,6 @@ +{ + "controller": "interactive-graphs.py", + "dependencies": { + "elementStyles": ["interactive-graphs-css.css"] + } + } \ No newline at end of file diff --git a/elements/pl-interactive-graph/interactive-graphs-css.css b/elements/pl-interactive-graph/interactive-graphs-css.css new file mode 100644 index 00000000..4c4e2c72 --- /dev/null +++ b/elements/pl-interactive-graph/interactive-graphs-css.css @@ -0,0 +1,38 @@ +.pl-graph { + display: flex; + flex-direction: column; /* Stack vertically */ + align-items: center; /* Center-align the items */ + overflow-x: auto; + width: 100%; + order: 1; +} + +.pl-graph > svg { + margin-top: 1rem; /* Add space between the list and the graph */ +} + +#selectedNodeList { + width: 100%; /* List takes full width */ + padding: 1rem; + box-sizing: border-box; + background-color: #f0f0f0; + border-radius: 5px; + margin-bottom: 1rem; /* Space before the graph */ + order: -1; /* Makes the list come before the graph */ + visibility: hidden; +} +#selectedEdgeList { + width: 100%; /* List takes full width */ + padding: 1rem; + box-sizing: border-box; + background-color: #f0f0f0; + border-radius: 5px; + margin-bottom: 1rem; /* Space before the graph */ + order: -1; /* Makes the list come before the graph */ + visibility: hidden; + +} + +.edge text { + dy: 200px; /* Adjust this value as needed to increase spacing */ +} \ No newline at end of file diff --git a/elements/pl-interactive-graph/interactive-graphs.py b/elements/pl-interactive-graph/interactive-graphs.py new file mode 100644 index 00000000..8bcbfdfa --- /dev/null +++ b/elements/pl-interactive-graph/interactive-graphs.py @@ -0,0 +1,647 @@ +import warnings +import random +import lxml.html +import networkx as nx +import numpy as np +import string +import heapq +import prairielearn as pl +import pygraphviz +from collections import deque + + +#Default pl-interactive-graph +PRESERVE_ORDERING=True +ANSWERS = [] +PARTIAL_CREDIT = False +ENGINE_DEFAULT = "dot" +# Legacy default +PARAMS_NAME_MATRIX_DEFAULT = None +PARAMS_NAME_DEFAULT = None +PARAMS_NAME_LABELS_DEFAULT = None +PARAMS_TYPE_DEFAULT = "adjacency-matrix" +WEIGHTS_DEFAULT = None +WEIGHTS_DIGITS_DEFAULT = 2 +WEIGHTS_PRESENTATION_TYPE_DEFAULT = "f" +NEGATIVE_WEIGHTS_DEFAULT = False +DIRECTED_DEFAULT = True +LOG_WARNINGS_DEFAULT = True +AUTOGRADING = None + + +def graphviz_from_networkx( + element: lxml.html.HtmlElement, data: pl.QuestionData +) -> str: + input_param_name = pl.get_string_attrib(element, "params-name") + + networkx_graph = pl.from_json(data["params"][input_param_name]) + + G = nx.nx_agraph.to_agraph(networkx_graph) + + return G.string() + +def graphviz_from_adj_matrix( + element: lxml.html.HtmlElement, data: pl.QuestionData +) -> str: + # Legacy input with passthrough + input_param_matrix = pl.get_string_attrib( + element, "params-name-matrix", PARAMS_NAME_DEFAULT + ) + input_param_name = pl.get_string_attrib(element, "params-name", input_param_matrix) + + # Exception to make typechecker happy. + if input_param_name is None: + raise ValueError('"params-name" is a required attribute.') + + input_label = pl.get_string_attrib( + element, "params-name-labels", PARAMS_NAME_LABELS_DEFAULT + ) + negative_weights = pl.get_boolean_attrib( + element, "negative-weights", NEGATIVE_WEIGHTS_DEFAULT + ) + + mat = np.array(pl.from_json(data["params"][input_param_name])) + show_weights = pl.get_boolean_attrib( + element, "weights", WEIGHTS_DEFAULT + ) # by default display weights for stochastic matrices + digits = pl.get_integer_attrib( + element, "weights-digits", WEIGHTS_DIGITS_DEFAULT + ) # if displaying weights how many digits to round to + presentation_type = pl.get_string_attrib( + element, "weights-presentation-type", WEIGHTS_PRESENTATION_TYPE_DEFAULT + ).lower() + directed = pl.get_boolean_attrib(element, "directed", DIRECTED_DEFAULT) + + label = None + if input_label is not None: + label = np.array(pl.from_json(data["params"][input_label])) + + # Sanity checking + + if mat.shape[0] != mat.shape[1]: + raise ValueError( + f'Non-square adjacency matrix "{input_param_name}" of size ({mat.shape[0]}, {mat.shape[1]}) given as input.' + ) + + if label is not None: + mat_label = label + if mat_label.shape[0] != mat.shape[0]: + raise ValueError( + f'Dimension {mat_label.shape[0]} of the label "{input_label}"' + f'is not consistent with the dimension {mat.shape[0]} of the matrix "{input_param_name}".' + ) + else: + mat_label = range(mat.shape[1]) + + if not directed and not np.allclose(mat, mat.T): + raise ValueError( + f'Input matrix "{input_param_name}" must be symmetric if rendering is set to be undirected.' + ) + + # Auto detect showing weights if any of the weights are not 1 or 0 + + if show_weights is None: + show_weights = any(x not in {0, 1} for x in mat.flatten()) + + # Create pygraphviz graph representation + + G = pygraphviz.AGraph(directed=directed) + G.add_nodes_from(mat_label) + + for in_node, row in zip(mat_label, mat): + for out_node, x in zip(mat_label, row): + # If showing negative weights, show every entry that is not None + # Otherwise, only show positive weights + if x is None or (not negative_weights and x <= 0.0): + continue + + if show_weights: + G.add_edge( + out_node, + in_node, + label=pl.string_from_2darray( + x, presentation_type=presentation_type, digits=digits + ), + ) + else: + G.add_edge(out_node, in_node) + + return G.string() + +def generate_graph(min_nodes, max_nodes, min_edges, max_edges, directed=False, weighted=False, tree=False): + # Step 1: Determine the number of nodes + n = random.randint(min_nodes, max_nodes) + + # Step 2: Ensure that the graph has at least n-1 edges if min_edges is 0 + if min_edges == 0: + min_edges = n - 1 + # Set a reasonable upper limit on edges if max_edges is 0 + if max_edges == 0: + max_edges = round(n * 1.5) + + # Step 3: Generate the graph + if tree: + G = nx.random_tree(n, create_using=nx.DiGraph() if directed else nx.Graph()) + if directed: + G = nx.DiGraph([(u, v) for u, v in G.edges()]) + else: + # Generate a connected non-tree graph by ensuring a spanning tree first + G = nx.random_tree(n, create_using=nx.DiGraph() if directed else nx.Graph()) + + # Add additional edges to meet the minimum and maximum constraints + m = random.randint(min_edges, max_edges if not directed else n * (n - 1) // 2) + + existing_edges = set(G.edges()) + remaining_edges = m - len(existing_edges) + possible_edges = set() + + for u in range(n): + for v in range(u + 1, n): + if directed: + possible_edges.add((u, v)) + possible_edges.add((v, u)) + else: + possible_edges.add((u, v)) + + # Add remaining edges randomly from available pairs + available_edges = list(possible_edges - existing_edges) + additional_edges = random.sample(available_edges, remaining_edges) + + for u, v in additional_edges: + G.add_edge(u, v) + + # Step 4: Add weights if necessary + if weighted: + for (u, v) in G.edges(): + G.edges[u, v]['weight'] = random.randint(1, 10) + + # Step 5: Relabel nodes to ASCII labels + ascii_labels = list(string.ascii_letters[:n]) + mapping = {i: ascii_labels[i] for i in range(n)} + G = nx.relabel_nodes(G, mapping) + + return G + +def prepare(element_html: str, data: pl.QuestionData) -> None: + optional_attribs = [ + "preserve-ordering", + "answers", + "grading", + "partial-credit", + "node-fill-color", + "edge-fill-color", + "select-nodes", + "select-edges", + "random-graph", + "directed-random", + "min-nodes", + "max-nodes", + "min-edges", + "max-edges", + "weighted", + "tree", + "engine", + "directed", + "params-name-matrix", + "params-name", + "weights", + "weights-digits", + "weights-presentation-type", + "params-name-labels", + "params-type", + "negative-weights", + "log-warnings", + ] + + # Load attributes from extensions if they have any + extensions = pl.load_all_extensions(data) + for extension in extensions.values(): + if hasattr(extension, "optional_attribs"): + optional_attribs.extend(extension.optional_attribs) + + element = lxml.html.fragment_fromstring(element_html) + pl.check_attribs(element, required_attribs=[], optional_attribs=optional_attribs) + +def render(element_html: str, data: pl.QuestionData) -> str: + + # Get attribs + element = lxml.html.fragment_fromstring(element_html) + engine = pl.get_string_attrib(element, "engine", ENGINE_DEFAULT) + log_warnings = pl.get_boolean_attrib(element, "log-warnings", LOG_WARNINGS_DEFAULT) + node_fill_color = pl.get_string_attrib(element, "node-fill-color", "red") + edge_fill_color = pl.get_string_attrib(element, "edge-fill-color", "red") + + select_nodes = pl.get_string_attrib(element, "select-nodes", "True") + select_edges = pl.get_string_attrib(element, "select-edges", True) + random_graph = pl.get_string_attrib(element, "random-graph", False) + grading = pl.get_string_attrib(element, "grading", None) + + #if random_graph is true, generate a random graph instead of using existing functions + if random_graph=="True": + min_nodes = pl.get_integer_attrib(element, "min-nodes", 5) + max_nodes = pl.get_integer_attrib(element, "max-nodes", 10) + min_edges = pl.get_integer_attrib(element, "min-edges", 0) + max_edges = pl.get_integer_attrib(element, "max-nodes", 0) + directed_random = pl.get_boolean_attrib(element, "directed-random", False) + + weighted = pl.get_boolean_attrib(element, "weighted", False) + tree = pl.get_boolean_attrib(element, "tree", False) + + networkx_graph = generate_graph(min_nodes, max_nodes, min_edges, max_edges, directed_random, weighted, tree) + agraph = nx.nx_agraph.to_agraph(networkx_graph) + if weighted: + for edge in agraph.edges(): + u, v = edge + edge.attr['label'] = networkx_graph[u][v]['weight'] + + graphviz_data = agraph.to_string() + else: + # Original logic to choose between networkx and adjacency matrix based on input_type + matrix_backends = { + "adjacency-matrix": graphviz_from_adj_matrix, + "networkx": graphviz_from_networkx, + } + #load color + # Load all extensions + extensions = pl.load_all_extensions(data) + for extension in extensions.values(): + matrix_backends.update(extension.backends) + # Legacy input with passthrough + input_param_matrix = pl.get_string_attrib( + element, "params-name-matrix", PARAMS_NAME_DEFAULT + ) + input_param_name = pl.get_string_attrib(element, "params-name", input_param_matrix) + + input_type = pl.get_string_attrib(element, "params-type", PARAMS_TYPE_DEFAULT) + + if len(str(element.text)) == 0 and input_param_name is None: + raise ValueError( + "No graph source given! Must either define graph in HTML or provide source in params." + ) + + if input_param_name is not None: + if input_type in matrix_backends: + graphviz_data = matrix_backends[input_type](element, data) + else: + raise ValueError(f'Unknown graph type "{input_type}".') + else: + # Read the contents of this element as the data to render + # we dump the string to json to ensure that newlines are + # properly encoded + graphviz_data = element.text + translated_dotcode = pygraphviz.AGraph(string=graphviz_data) + translated_dotcode_string=translated_dotcode.string().replace('\"\"', "G") + + #print(translated_dotcode_string) + + #stored_graph_data = translated_dotcode.string.split(" ")[:split] + #graph_data = translated_dotcode_string.split(" ")[split:] + + graph_data = translated_dotcode_string + with warnings.catch_warnings(): + # Only apply ignore filter if we enable hiding warnings + if not log_warnings: + warnings.simplefilter("ignore") + svg = translated_dotcode.draw(format="svg", prog=engine).decode( + "utf-8", "strict" + ) + #print(graph_data) + javascript_function = f""" + + + +
+
+ + """ + return f'
{svg}
{javascript_function}' + +def grade(element_html, data): + + element = lxml.html.fragment_fromstring(element_html) + #select_nodes = pl.from_json(element.get("select-nodes")) + + select_nodes = pl.get_string_attrib(element, "select-nodes", "False") + select_edges = pl.get_string_attrib(element, "select-edges", "False") + + grading = pl.get_string_attrib(element, "grading", None) + + if select_nodes == "True": + + if len(data["submitted_answers"]["selectedNodes"]) < 3: + data["partial_scores"]["score"] = { + "score": 0, + "weight": 1, + "feedback": "no nodes selected", + } + + return data + + + if select_edges == "True": + + if len(data["submitted_answers"]["selectedEdges"]) < 3: + data["partial_scores"]["score"] = { + "score": 0, + "weight": 1, + "feedback": "no edges selected", + } + + return data + + + if select_nodes == "True": + user_selected_nodes = eval(data["submitted_answers"]["selectedNodes"]) + + + if select_edges == "True": + user_selected_edges = eval(data["submitted_answers"]["selectedEdges"]) + + # Use 'submitted_answers' instead of data["submitted_answers"] + score = 0 + + ######### Helper ######### + random_graph = data["submitted_answers"]["random-graph"] + graph = pygraphviz.AGraph(string=random_graph) + ########################## + + + + correct_answer = eval(pl.from_json(element.get("answers", "[]"))) + preserve_ordering = pl.from_json(element.get("preserve-ordering")) + partial_credit = pl.from_json(element.get("partial-credit")) + + if grading == 'dfs': + correct_answer = dfs_agraph(graph, graph.nodes()[0]) + + if grading == 'bfs': + correct_answer = bfs_agraph(graph, graph.nodes()[0]) + + if grading == 'dijkstras': + correct_answer = dijkstra_agraph(graph, graph.nodes()[0]) + + + if select_nodes == "True": + if preserve_ordering != "True": + for i in range(len(user_selected_nodes)): + if user_selected_nodes[i] in correct_answer: + score += 1 + else: + for i in range(len(correct_answer)): + if i < len(user_selected_nodes) and correct_answer[i] == user_selected_nodes[i]: + score += 1 + + if partial_credit != "True": + if score != len(correct_answer): + score = 0 + else: + score = 1 + else: + score = score/len(correct_answer) + data["partial_scores"]["score"] = { + "score": score, + "weight": 1 + } + + + if select_edges == "True": + if preserve_ordering != "True": + for i in range(len(user_selected_edges)): + if user_selected_edges[i] in correct_answer: + score += 1 + else: + for i in range(len(correct_answer)): + if i < len(user_selected_edges) and correct_answer[i] == user_selected_edges[i]: + score += 1 + + if partial_credit != "True": + if score != len(correct_answer): + score = 0 + else: + score = 1 + else: + score = score/len(correct_answer) + data["partial_scores"]["score"] = { + "score": score, + "weight": 1 + } + + + return data + + + + + + +##### Always need to run this ##### +def string_to_graph(string_graph): + random_graph = data["submitted_answers"]["random-graph"] + graph = pygraphviz.AGraph(string=string_graph) + return graph + + + + + +############################################################################ +############################ Algorithms #################################### +############################################################################ + + +def dfs_agraph(agraph, start): + visited = set() # Set of visited nodes + stack = [start] # Stack for DFS + order = [] # Order of visited nodes + + while stack: + node = stack.pop() + if node not in visited: + visited.add(node) + order.append(node) + # Get successors of the node in reverse order to maintain the correct order when popped from stack + stack.extend(reversed([n for n in agraph.successors(node) if n not in visited])) + + return order + + +def bfs_agraph(agraph, start_node): + + visited = set([start_node]) # Set of visited nodes + queue = deque([start_node]) # Queue for BFS + order = [] # Order of visited nodes + + while queue: + # Dequeue a node from queue + current_node = queue.popleft() + order.append(current_node) + + # Visit all the unvisited neighbors + for neighbor in agraph.successors(current_node): + if neighbor not in visited: + visited.add(neighbor) + queue.append(neighbor) + + return order + + + +def dijkstra_agraph(agraph, start_node): + + # Initialize distances from start_node to infinity, except for start_node itself which is 0 + distances = {node: float('inf') for node in agraph.nodes()} + distances[start_node] = 0 + + # Priority queue to select the node with the smallest distance + pq = [(0, start_node)] + + # Set and list to record the order of visited nodes + visited_set = set() + visited_order = [] + + while pq: + # Pop the node with the smallest distance + current_distance, current_node = heapq.heappop(pq) + + # Skip if this node has already been visited + if current_node in visited_set: + continue + + # Mark the current node as visited + visited_set.add(current_node) + visited_order.append(current_node) + + # Explore the neighbors of the current node + for neighbor in agraph.successors(current_node): + edge = agraph.get_edge(current_node, neighbor) + + try: + # Extract the edge's label, which contains the weight as a string + label = edge.attr.get('label') + # If the label exists and is not None, convert it to float. Otherwise, use default weight. + weight = float(label) if label is not None else 1.0 + except ValueError: + # In case the label cannot be converted to float, use a default weight. + weight = 1.0 + + # Calculate new distance to the neighboring node + distance = current_distance + weight + + # If the new distance is shorter, update the path and distances + if distance < distances[neighbor]: + distances[neighbor] = distance + # Use a tuple of (distance, neighbor) to maintain a min heap based on distance + heapq.heappush(pq, (distance, neighbor)) + + return visited_order + + diff --git a/questions/.DS_Store b/questions/.DS_Store deleted file mode 100644 index 10757731..00000000 Binary files a/questions/.DS_Store and /dev/null differ diff --git a/questions/pl-interactive-graph-examples/BFS_Example/example.png b/questions/pl-interactive-graph-examples/BFS_Example/example.png new file mode 100644 index 00000000..6b2befa2 Binary files /dev/null and b/questions/pl-interactive-graph-examples/BFS_Example/example.png differ diff --git a/questions/pl-interactive-graph-examples/BFS_Example/info.json b/questions/pl-interactive-graph-examples/BFS_Example/info.json new file mode 100644 index 00000000..38394f86 --- /dev/null +++ b/questions/pl-interactive-graph-examples/BFS_Example/info.json @@ -0,0 +1,9 @@ +{ + "uuid": "b1d3d28d-dd2f-4729-832e-a0b0b5553329", + "title": "BFS Example (Direct Autograding)", + "topic": "Traversals", + "tags": [ + "Fa23" + ], + "type": "v3" +} diff --git a/questions/pl-interactive-graph-examples/BFS_Example/question.html b/questions/pl-interactive-graph-examples/BFS_Example/question.html new file mode 100644 index 00000000..8db54df6 --- /dev/null +++ b/questions/pl-interactive-graph-examples/BFS_Example/question.html @@ -0,0 +1,17 @@ +

You are provided a graph below. Click on the nodes in the order they will be selected if we run Breath-First Search (BFS) algorithm on this graph. You can see the order of your clicked list in a list under the graph and you are allowed to unclick and deselect nodes at any point. Press "Save & Grade" to submit your answer.

+ + + digraph G { + A -> B; + A -> C; + B -> D; + B -> E; + C -> F; + C -> G; + E -> H; + } + + + + + diff --git a/questions/pl-interactive-graph-examples/BFS_Example/server.py b/questions/pl-interactive-graph-examples/BFS_Example/server.py new file mode 100644 index 00000000..e69de29b diff --git a/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/example.png b/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/example.png new file mode 100644 index 00000000..63c6e7f7 Binary files /dev/null and b/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/example.png differ diff --git a/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/info.json b/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/info.json new file mode 100644 index 00000000..a736fb04 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/info.json @@ -0,0 +1,9 @@ +{ + "uuid": "67d91bb0-5489-4101-a5d9-94a9fd4dd0ef", + "title": "Kruskals Example (Direct Autograding)", + "topic": "Traversals", + "tags": [ + "Fa23" + ], + "type": "v3" +} diff --git a/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/question.html b/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/question.html new file mode 100644 index 00000000..c7653d90 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/question.html @@ -0,0 +1,22 @@ +

You are provided a graph below. Click on the edges in the order they will be selected if we run Kruskal's algorithm on this graph. You will be able to see the edges you select in the apropriate order in a list under the graph. You can also unclick and deselect individual edges. Press "Save & Grade" to submit your answer.

+ + + graph G { + A -- B [label="4"]; + A -- D [label="1"]; + B -- C [label="2"]; + B -- E [label="5"]; + C -- F [label="3"]; + D -- E [label="7"]; + D -- G [label="8"]; + E -- H [label="6"]; + F -- I [label="4"]; + G -- H [label="2"]; + H -- I [label="7"]; + } + + + + + + diff --git a/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/server.py b/questions/pl-interactive-graph-examples/Clickable_Edges_Kruskals/server.py new file mode 100644 index 00000000..e69de29b diff --git a/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/example.png b/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/example.png new file mode 100644 index 00000000..4f9e8b28 Binary files /dev/null and b/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/example.png differ diff --git a/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/info.json b/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/info.json new file mode 100644 index 00000000..fe5e6ff0 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/info.json @@ -0,0 +1,9 @@ +{ + "uuid": "38e1318c-967a-4506-a518-da7fe80cf249", + "title": "Dijkstras Example (Direct Autograding)", + "topic": "Traversals", + "tags": [ + "Fa23" + ], + "type": "v3" +} diff --git a/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/question.html b/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/question.html new file mode 100644 index 00000000..ef2f8084 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/question.html @@ -0,0 +1,24 @@ +

You are provided a graph below. Click on the nodes in the order they will be selected if we run Dijkstra's algorithm starting at node A. + This question refers to the order nodes will be popped from the queue.

+ + + digraph { + A + B + C + D + E + F + A -> B [label=5] + A -> C [label=3] + B -> C [label=2] + B -> D [label=6] + C -> D [label=7] + C -> E [label=4] + D -> F [label=1] + E -> F [label=8] + E -> D [label=2] + } + + + diff --git a/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/server.py b/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/server.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Clickable_Nodes_Dijkstras/server.py @@ -0,0 +1 @@ + diff --git a/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/example.png b/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/example.png new file mode 100644 index 00000000..0a66d305 Binary files /dev/null and b/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/example.png differ diff --git a/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/info.json b/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/info.json new file mode 100644 index 00000000..59e469e0 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/info.json @@ -0,0 +1,9 @@ +{ + "uuid": "7937E504-47E9-42DB-A947-27D4091488D6", + "title": "Finite State Machines Example", + "topic": "Traversals", + "tags": [ + "Fa23" + ], + "type": "v3" +} diff --git a/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/question.html b/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/question.html new file mode 100644 index 00000000..b66c4de7 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/question.html @@ -0,0 +1,48 @@ +

+ Consider an FSM representing an automated library system with a specific operational flow. The system is designed to handle a unique sequence of tasks each day, involving book checkouts, returns, catalog updates, and member services. Each transition between states involves an action with an associated weight. +

+
    +
  1. The day starts with processing book returns.
  2. +
  3. After handling returns, the system updates the catalog.
  4. +
  5. If there are new members, their registration is processed next; otherwise, the system moves directly to restocking returned books.
  6. +
  7. After dealing with new members or restocking, the system addresses member queries.
  8. +
  9. The day ends with event planning and closing tasks.
  10. + +

    Note: In your answer, do not try to click on start or end, just the states in between are needed. The order does not matter, and there is no partial credit.

    +
+ + + + + + + digraph automated_library_system { + + size="10,5" + edge [fontsize=10, fontcolor=blue, fontname="Helvetica"] + node [shape = circle,fontsize=12, fontname="Helvetica"] + + // States + A -> B [ label = "Check-in (weight: 2)" ]; + A -> C [ label = "New Member (weight: 3)" ]; + B -> D [ label = "Catalog Update (weight: 2)" ]; + C -> D [ label = "Book Checkout (weight: 4)" ]; + C -> E [ label = "Member Query (weight: 3)" ]; + D -> F [ label = "Restock (weight: 1)" ]; + E -> G [ label = "Event Planning (weight: 5)" ]; + F -> G [ label = "Closing Tasks (weight: 2)" ]; + + // Special States + node [shape = doublecircle]; + A; G; + + // Start and End Points + node [shape = record, height=.1]; + start [label="Start", shape=box, fillcolor=lightgreen]; + start -> A; + end [label="End", shape=Msquare]; + G -> end; + } + + + \ No newline at end of file diff --git a/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/server.py b/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/server.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Clickable_Nodes_Finite_State_Machine/server.py @@ -0,0 +1 @@ + diff --git a/questions/pl-interactive-graph-examples/DFS_Example/example.png b/questions/pl-interactive-graph-examples/DFS_Example/example.png new file mode 100644 index 00000000..7552121d Binary files /dev/null and b/questions/pl-interactive-graph-examples/DFS_Example/example.png differ diff --git a/questions/pl-interactive-graph-examples/DFS_Example/info.json b/questions/pl-interactive-graph-examples/DFS_Example/info.json new file mode 100644 index 00000000..5cf6ac03 --- /dev/null +++ b/questions/pl-interactive-graph-examples/DFS_Example/info.json @@ -0,0 +1,9 @@ +{ + "uuid": "3887ad38-b4bd-4b46-9f46-81a8d018dd1b", + "title": "DFS Example (Direct Autograding)", + "topic": "Traversals", + "tags": [ + "Fa23" + ], + "type": "v3" +} diff --git a/questions/pl-interactive-graph-examples/DFS_Example/question.html b/questions/pl-interactive-graph-examples/DFS_Example/question.html new file mode 100644 index 00000000..5a5de46c --- /dev/null +++ b/questions/pl-interactive-graph-examples/DFS_Example/question.html @@ -0,0 +1,20 @@ +

You are provided a graph below. Click on the nodes in the order they will be selected if we run Depth-First Search (DFS) algorithm on this graph. You can see the order of your clicked list in a list under the graph and you are allowed to unclick and deselect nodes at any point. Press "Save & Grade" to submit your answer.

+ + + graph G { + A -- B; + A -- C; + B -- D; + B -- E; + C -- F; + C -- G; + E -- H; + E -- I; + E -- J; + } + + + + + + diff --git a/questions/pl-interactive-graph-examples/DFS_Example/server.py b/questions/pl-interactive-graph-examples/DFS_Example/server.py new file mode 100644 index 00000000..e69de29b diff --git a/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/example.png b/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/example.png new file mode 100644 index 00000000..7df75dee Binary files /dev/null and b/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/example.png differ diff --git a/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/info.json b/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/info.json new file mode 100644 index 00000000..30166865 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/info.json @@ -0,0 +1,9 @@ +{ + "uuid": "EF8DBB87-4B0B-4BC8-8549-BBC5DD9EF5A5", + "title": "Random Graph BFS (Algorithm Autograding)", + "topic": "Traversals", + "tags": [ + "Fa23" + ], + "type": "v3" +} diff --git a/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/question.html b/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/question.html new file mode 100644 index 00000000..daccba1a --- /dev/null +++ b/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/question.html @@ -0,0 +1,9 @@ +

+ What is the Breadth-First Search traversal order of this algorithm? Click the nodes in the order they are selected and click submit. There is partial credit on this problem.

+ + + graph G { + + } + + \ No newline at end of file diff --git a/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/server.py b/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/server.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Random_Graph_BFS_Autograding_Example/server.py @@ -0,0 +1 @@ + diff --git a/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/example.png b/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/example.png new file mode 100644 index 00000000..cde51aa4 Binary files /dev/null and b/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/example.png differ diff --git a/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/info.json b/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/info.json new file mode 100644 index 00000000..c4b4ede5 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/info.json @@ -0,0 +1,9 @@ +{ + "uuid": "35c69c07-7a78-49b6-84aa-6f1630b41f85", + "title": "Random Graph DFS (Algorithm Autograding)", + "topic": "Traversals", + "tags": [ + "Fa23" + ], + "type": "v3" +} diff --git a/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/question.html b/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/question.html new file mode 100644 index 00000000..10649391 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/question.html @@ -0,0 +1,9 @@ +

+ What is the Breadth-First Search traversal order of this algorithm? Click the nodes in the order they are selected and click submit. There is partial credit on this problem.

+ + + graph G { + + } + + \ No newline at end of file diff --git a/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/server.py b/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/server.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/questions/pl-interactive-graph-examples/Random_Graph_DFS_Autograding_Example/server.py @@ -0,0 +1 @@ +