From ebdf523ef6865ea5553aff2e89e335ce43b1647e Mon Sep 17 00:00:00 2001 From: Tung Anh Vu <tung@kam.mff.cuni.cz> Date: Sun, 31 Oct 2021 18:08:32 +0100 Subject: [PATCH] (a,b)-tree experiment --- 05-ab_experiment/cpp/Makefile | 22 ++ 05-ab_experiment/cpp/ab_experiment.cpp | 389 +++++++++++++++++++++++ 05-ab_experiment/cpp/random.h | 59 ++++ 05-ab_experiment/python/Makefile | 15 + 05-ab_experiment/python/ab_experiment.py | 259 +++++++++++++++ 05-ab_experiment/task.md | 81 +++++ 6 files changed, 825 insertions(+) create mode 100644 05-ab_experiment/cpp/Makefile create mode 100644 05-ab_experiment/cpp/ab_experiment.cpp create mode 100644 05-ab_experiment/cpp/random.h create mode 100644 05-ab_experiment/python/Makefile create mode 100755 05-ab_experiment/python/ab_experiment.py create mode 100644 05-ab_experiment/task.md diff --git a/05-ab_experiment/cpp/Makefile b/05-ab_experiment/cpp/Makefile new file mode 100644 index 0000000..967fad4 --- /dev/null +++ b/05-ab_experiment/cpp/Makefile @@ -0,0 +1,22 @@ +STUDENT_ID ?= PLEASE_SET_STUDENT_ID + +.PHONY: test +test: ab_experiment + @rm -rf out && mkdir out + @for test in insert min random ; do \ + for mode in '2-3' '2-4' ; do \ + echo t-$$test-$$mode ; \ + ./ab_experiment $$test $(STUDENT_ID) $$mode >out/t-$$test-$$mode ; \ + done ; \ + done + +INCLUDE ?= . +CXXFLAGS=-std=c++11 -O2 -Wall -Wextra -g -Wno-sign-compare -I$(INCLUDE) + +ab_experiment: ab_tree.h ab_experiment.cpp $(INCLUDE)/random.h + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $^ -o $@ + +.PHONY: clean +clean:: + rm -f ab_experiment + rm -rf out diff --git a/05-ab_experiment/cpp/ab_experiment.cpp b/05-ab_experiment/cpp/ab_experiment.cpp new file mode 100644 index 0000000..31b520e --- /dev/null +++ b/05-ab_experiment/cpp/ab_experiment.cpp @@ -0,0 +1,389 @@ +#include <algorithm> +#include <functional> +#include <string> +#include <utility> +#include <vector> +#include <iostream> +#include <cmath> + +#include "ab_tree.h" +#include "random.h" + +using namespace std; + +void expect_failed(const string& message) { + cerr << "Test error: " << message << endl; + exit(1); +} + +/* + * A modified Splay tree for benchmarking. + * + * We inherit the implementation of operations from the Tree class + * and extend it by keeping statistics on the number of splay operations + * and the total number of rotations. Also, if naive is turned on, + * splay uses only single rotations. + * + * Please make sure that your Tree class defines the rotate() and splay() + * methods as virtual. + */ + +class BenchmarkingABTree : public ab_tree { +public: + int num_operations; + int num_struct_changes; + + BenchmarkingABTree(int a, int b) : ab_tree(a,b) + { + reset(); + } + + void reset() + { + num_operations = 0; + num_struct_changes = 0; + } + + pair<ab_node*, int> split_node(ab_node *node, int size) override + { + num_struct_changes++; + return ab_tree::split_node(node, size); + } + + void insert(int key) override + { + num_operations++; + ab_tree::insert(key); + } + + // Return the average number of rotations per operation. + double struct_changes_per_op() + { + if (num_operations > 0) + return (double) num_struct_changes / num_operations; + else + return 0; + } + + // Delete key from the tree. Does nothing if the key is not in the tree. + void remove(int key){ + num_operations += 1; + + // Find the key to be deleted + ab_node *node = root; + int i; + bool found = node->find_branch(key, i); + while(!found){ + node = node->children[i]; + if (!node) return; // Key is not in the tree + found = node->find_branch(key, i); + } + + // If node is not a leaf, we need to swap the key with its successor + if (node->children[0] != nullptr){ // Only leaves have nullptr as children + // Successor is leftmost key in the right subtree of key + ab_node *succ = min(node->children[i+1]); + swap(node->keys[i], succ->keys[0]); + node = succ; + } + + // Now run the main part of the delete + remove_leaf(key, node); + } + +private: + // Main part of the remove + void remove_leaf(int key, ab_node* node) + { + EXPECT(node != nullptr, "Trying to delete key from nullptr"); + EXPECT(node->children[0] == nullptr, "Leaf's child must be nullptr"); + + while(1){ + // Find the key in the node + int key_position; + bool found = node->find_branch(key, key_position); + EXPECT(found, "Trying to delete key that is not in the node."); + + // Start with the deleting itself + node->keys.erase(node->keys.cbegin() + key_position); + node->children.erase(node->children.cbegin() + key_position + 1); + + // No underflow means we are done + if (node->children.size() >= a) return; + + // Root may underflow, but cannot have just one child (unless tree is empty) + if (node == root){ + if ((node->children.size() == 1) && (root->children[0] != nullptr)){ + ab_node *old_root = root; + root = root->children[0]; + root->parent = nullptr; + delete_node(old_root); + } + return; + } + + ab_node *brother; + int separating_key_pos; + bool tmp; + tie(brother, separating_key_pos, tmp) = get_brother(node); + int separating_key = node->parent->keys[separating_key_pos]; + + // First check whether we can steal brother's child + if (brother->children.size() > a){ + steal_child(node); + return; + } + + // If the brother is too small, we merge with him and propagate the delete + node = merge_node(node); + node = node->parent; + key = separating_key; + key_position = separating_key_pos; + } + } + + // Return the leftmost node of a subtree rooted at node. + ab_node* min(ab_node *node) + { + EXPECT(node != nullptr, "Trying to search for minimum of nullptr"); + while (node->children[0]) { + node = node->children[0]; + } + return node; + } + + // Return the left brother if it exists, otherwise return right brother. + // Returns tuple (brother, key_position, is_left_brother), where + // key_position is a position of the key that separates node and brother in their parent. + tuple<ab_node*, int, bool> get_brother(ab_node* node) + { + ab_node *parent = node->parent; + EXPECT(parent != nullptr, "Node without parent has no brother"); + + // Find node in parent's child list + int i; + for(i = 0; i < parent->children.size(); ++i){ + ab_node *c = parent->children[i]; + if (c == node) break; + } + EXPECT(i < parent->children.size(), "Node is not inside its parent"); + + if (i == 0){ + return make_tuple(parent->children[1], 0, false); + } + else{ + return make_tuple(parent->children[i - 1], i - 1, true); + } + } + + // Transfer one child from node's left brother to the node. + // If node has no left brother, use right brother instead. + void steal_child(ab_node* node) + { + ab_node *brother; + int separating_key_pos; + bool is_left_brother; + tie(brother, separating_key_pos, is_left_brother) = get_brother(node); + int separating_key = node->parent->keys[separating_key_pos]; + + EXPECT(brother->children.size() > a, "Stealing child causes underflow in brother!"); + EXPECT(node->children.size() < b, "Stealing child causes overflow in the node!"); + + // We steal either from front or back + int steal_position, target_position; + if (is_left_brother){ + steal_position = brother->children.size()-1; + target_position = 0; + } + else{ + steal_position = 0; + target_position = node->children.size(); + } + // Steal the child + ab_node *stolen_child = brother->children[steal_position]; + if (stolen_child != nullptr){ + stolen_child->parent = node; + } + node->children.insert(node->children.cbegin() + target_position, stolen_child); + brother->children.erase(brother->children.cbegin() + steal_position); + + // List of keys is shorter than list of children + if (is_left_brother) steal_position -= 1; + else target_position -= 1; + + // Update keys + node->keys.insert(node->keys.cbegin() + target_position, separating_key); + node->parent->keys[separating_key_pos] = brother->keys[steal_position]; + brother->keys.erase(brother->keys.cbegin() + steal_position); + } + +public: + // Merge node with its left brother and destroy the node. Must not cause overflow! + // Returns result of the merge. + // If node has no left brother, use right brother instead. + ab_node* merge_node(ab_node* node){ + num_struct_changes += 1; + + ab_node *brother; + int separating_key_pos; + bool is_left_brother; + tie(brother, separating_key_pos, is_left_brother) = get_brother(node); + int separating_key = node->parent->keys[separating_key_pos]; + + // We swap brother and node if necessary so that the node is always on the right + if (!is_left_brother) swap(brother, node); + + for (auto c: node->children) + brother->children.push_back(c); + brother->keys.push_back(separating_key); + for (auto k: node->keys) + brother->keys.push_back(k); + + EXPECT(brother->children.size() <= b, "Merge caused overflow!"); + + // Update parent pointers in non-leaf + if (brother->children[0] != nullptr){ + for (auto c : brother->children) + c->parent = brother; + } + + delete_node(node); + return brother; + } +}; + +int a, b; +RandomGen *rng; // Random generator object + +// An auxiliary function for generating a random permutation. +vector<int> random_permutation(int n) +{ + vector<int> perm; + for (int i=0; i<n; i++) + perm.push_back(i); + for (int i=0; i<n-1; i++) + swap(perm[i], perm[i + rng->next_range(n-i)]); + return perm; +} + +void test_insert() +{ + for (int e=32; e<=64; e++) { + int n = (int) pow(2, e/4.); + BenchmarkingABTree tree = BenchmarkingABTree(a,b); + + vector<int> perm = random_permutation(n); + for (int x : perm) + tree.insert(x); + + cout << n << " " << tree.struct_changes_per_op() << endl; + } +} + +void test_random() +{ + for (int e=32; e<=64; e++) { + int n = (int) pow(2, e/4.); + BenchmarkingABTree tree = BenchmarkingABTree(a,b); + + // We keep track of elements present and not present in the tree + vector<int> elems; + vector<int> anti_elems; + elems.reserve(n); + anti_elems.reserve(n+1); + + for (int x = 0; x < 2*n; x+=2){ + tree.insert(x); + elems.push_back(x); + } + + for (int i = -1; i <2*n + 1; i+=2) + anti_elems.push_back(i); + + for (int i=0; i<n; i++){ + int r, x; + // Delete random element + r = rng->next_range(elems.size()); + x = elems[r]; + tree.remove(x); + elems.erase(elems.cbegin() + r); + anti_elems.push_back(x); + + // Insert random "anti-element" + r = rng->next_range(anti_elems.size()); + x = anti_elems[r]; + tree.insert(x); + elems.push_back(x); + anti_elems.erase(anti_elems.cbegin() + r); + } + + cout << n << " " << tree.struct_changes_per_op() << endl; + } +} + +void test_min() +{ + for (int e=32; e<=64; e++) { + int n = (int) pow(2, e/4.); + BenchmarkingABTree tree = BenchmarkingABTree(a,b); + + for (int x = 0; x < n; x++) + tree.insert(x); + + for (int i=0; i<n; i++){ + tree.remove(0); + tree.insert(0); + } + + cout << n << " " << tree.struct_changes_per_op() << endl; + } +} + +vector<pair<string, function<void()>>> tests = { + { "insert", test_insert }, + { "random", test_random }, + { "min", test_min }, +}; + +int main(int argc, char **argv) +{ + if (argc != 4) { + cerr << "Usage: " << argv[0] << " <test> <student-id> (2-3|2-4)" << endl; + return 1; + } + + string which_test = argv[1]; + string id_str = argv[2]; + string mode = argv[3]; + + try { + rng = new RandomGen(stoi(id_str)); + } catch (...) { + cerr << "Invalid student ID" << endl; + return 1; + } + + a = 2; + if (mode == "2-3") + b = 3; + else if (mode == "2-4") + b = 4; + else + { + cerr << "Last argument must be either '2-3' or '2-4'" << endl; + return 1; + } + + for (const auto& test : tests) { + if (test.first == which_test) + { + cout.precision(12); + test.second(); + return 0; + } + } + cerr << "Unknown test " << which_test << endl; + return 1; + + return 0; +} diff --git a/05-ab_experiment/cpp/random.h b/05-ab_experiment/cpp/random.h new file mode 100644 index 0000000..7d18ab6 --- /dev/null +++ b/05-ab_experiment/cpp/random.h @@ -0,0 +1,59 @@ +#define DS1_RANDOM_H + +#include <cstdint> + +/* + * This is the xoroshiro128+ random generator, designed in 2016 by David Blackman + * and Sebastiano Vigna, distributed under the CC-0 license. For more details, + * see http://vigna.di.unimi.it/xorshift/. + * + * Rewritten to C++ by Martin Mares, also placed under CC-0. + */ + +class RandomGen { + uint64_t state[2]; + + uint64_t rotl(uint64_t x, int k) + { + return (x << k) | (x >> (64 - k)); + } + + public: + // Initialize the generator, set its seed and warm it up. + RandomGen(unsigned int seed) + { + state[0] = seed * 0xdeadbeef; + state[1] = seed ^ 0xc0de1234; + for (int i=0; i<100; i++) + next_u64(); + } + + // Generate a random 64-bit number. + uint64_t next_u64(void) + { + uint64_t s0 = state[0], s1 = state[1]; + uint64_t result = s0 + s1; + s1 ^= s0; + state[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14); + state[1] = rotl(s1, 36); + return result; + } + + // Generate a random 32-bit number. + uint32_t next_u32(void) + { + return next_u64() >> 11; + } + + // Generate a number between 0 and range-1. + unsigned int next_range(unsigned int range) + { + /* + * This is not perfectly uniform, unless the range is a power of two. + * However, for 64-bit random values and 32-bit ranges, the bias is + * insignificant. + */ + return next_u64() % range; + } +}; + diff --git a/05-ab_experiment/python/Makefile b/05-ab_experiment/python/Makefile new file mode 100644 index 0000000..48e36ae --- /dev/null +++ b/05-ab_experiment/python/Makefile @@ -0,0 +1,15 @@ +STUDENT_ID ?= PLEASE_SET_STUDENT_ID + +.PHONY: test +test: ab_experiment.py ab_tree.py + @rm -rf out && mkdir out + @for test in insert min random ; do \ + for mode in '2-3' '2-4' ; do \ + echo t-$$test-$$mode ; \ + ./ab_experiment.py $$test $(STUDENT_ID) $$mode >out/t-$$test-$$mode ; \ + done ; \ + done + +.PHONY: clean +clean:: + rm -rf out __pycache__ diff --git a/05-ab_experiment/python/ab_experiment.py b/05-ab_experiment/python/ab_experiment.py new file mode 100755 index 0000000..bc116e3 --- /dev/null +++ b/05-ab_experiment/python/ab_experiment.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 + +import sys +import random + +from ab_tree import ABTree + +class BenchmarkingABTree(ABTree): + """A modified ABTree for benchmarking. + + We inherit the implementation of operations from the ABTree class + and extend it by delete operation and by keeping statistics on the number + of operations and the total number of structural changes. + """ + def __init__(self, a, b): + ABTree.__init__(self, a, b) + self.reset() + + def reset(self): + """ Reset statistics """ + self.num_operations = 0 + self.num_struct_changes = 0 + + def struct_changes_per_op(self): + """Return the average number of struct. changes per operation.""" + if self.num_operations > 0: + return self.num_struct_changes / self.num_operations + else: + return 0 + + def insert(self, key): + self.num_operations += 1 + ABTree.insert(self, key) + + def split_node(self, node, size): + self.num_struct_changes += 1 + return ABTree.split_node(self, node, size) + + def remove(self, key): + """ Delete key from the tree. Does nothing if the key is not in the tree. """ + self.num_operations += 1 + + # Find the key to be deleted + node = self.root + found, i = node.find_branch(key) + while not found: + node = node.children[i] + if not node: return # Key is not in the tree + found, i = node.find_branch(key) + + # If node is not a leaf, we need to swap the key with its successor + if node.children[0] is not None: # Only leaves have None as children + # Successor is leftmost key in the right subtree of key + succ = self._min(node.children[i+1]) + node.keys[i], succ.keys[0] = succ.keys[0], node.keys[i] + node = succ + + # Now run the main part of the delete + self._remove_leaf(key, node) + + def _remove_leaf(self, key, node): + """ Main part of the delete. + """ + assert node is not None, "Trying to delete key from None" + assert node.children[0] is None, "Leaf's child must be None" + + while True: + # Find the key in the node + found, key_position = node.find_branch(key) + assert found, "Trying to delete key that is not in the node." + + # Start with the deleting itself + del node.keys[key_position] + del node.children[key_position + 1] + + # No underflow means we are done + if len(node.children) >= self.a: return + + # Root may underflow, but cannot have just one child (unless tree is empty) + if node == self.root: + if (len(node.children) == 1) and (self.root.children[0] is not None): + self.root = self.root.children[0] + self.root.parent = None + return + + brother, separating_key_pos, _ = self._get_brother(node) + separating_key = node.parent.keys[separating_key_pos] + + # First check whether we can steal brother's child + if len(brother.children) > self.a: + self._steal_child(node) + return + + # If the brother is too small, we merge with him and propagate the delete + node = self.merge_node(node) + node, key, key_position = node.parent, separating_key, separating_key_pos + + def _min(self, node): + """ Return the leftmost node of a subtree rooted at node.""" + assert node is not None + while node.children[0] is not None: + node = node.children[0] + return node + + def _get_brother(self, node): + """ Return the left brother if it exists, otherwise return right brother. + returns tuple (brother, key_position, is_left_brother), where + key_position is a position of the key that separates node and brother in their parent. + """ + parent = node.parent + assert parent is not None, "Node without parent has no brother" + + # Find node in parent's child list + i = 0 + for c in parent.children: + if c is node: break + else: i += 1 + assert i < len(parent.children), "Node is not inside its parent" + + if i == 0: + return parent.children[1], 0, False + else: + return parent.children[i - 1], i - 1, True + + def _steal_child(self, node): + """ Transfer one child from node's left brother to the node. + If node has no left brother, use right brother instead. + """ + brother, separating_key_pos, is_left_brother = self._get_brother(node) + separating_key = node.parent.keys[separating_key_pos] + + assert len(brother.children) > self.a, "Stealing child causes underflow in brother!" + assert len(node.children) < self.b, "Stealing child causes overflow in the node!" + + # We steal either from front or back + if is_left_brother: + steal_position = len(brother.children)-1 + target_position = 0 + else: + steal_position = 0 + target_position = len(node.children) + # Steal the child + stolen_child = brother.children[steal_position] + if stolen_child is not None: + stolen_child.parent = node + node.children.insert(target_position, stolen_child) + del brother.children[steal_position] + + # List of keys is shorter than list of children + if is_left_brother: + steal_position -= 1 + else: + target_position -= 1 + # Update keys + node.keys.insert(target_position, separating_key) + node.parent.keys[separating_key_pos] = brother.keys[steal_position] + del brother.keys[steal_position] + + def merge_node(self, node): + """ Merge node with its left brother and destroy the node. Must not cause overflow! + + Returns result of the merge. + If node has no left brother, use right brother instead. + """ + self.num_struct_changes += 1 + + brother, separating_key_pos, is_left_brother = self._get_brother(node) + separating_key = node.parent.keys[separating_key_pos] + + # We swap brother and node if necessary so that the node is always on the right + if not is_left_brother: + brother, node = node, brother + + brother.children.extend(node.children) + brother.keys.append(separating_key) + brother.keys.extend(node.keys) + + assert len(brother.children) <= self.b, "Merge caused overflow!" + + # Update parent pointers in non-leaf + if brother.children[0] is not None: + for c in brother.children: + c.parent = brother + return brother + +def test_insert(): + for exp in range(32, 64): + n = int(2**(exp/4)) + tree = BenchmarkingABTree(a, b) + + for elem in random.sample(range(n), n): + tree.insert(elem) + + print(n, tree.struct_changes_per_op()) + +def test_random(): + for exp in range(32, 64): + n = int(2**(exp/4)) + tree = BenchmarkingABTree(a, b) + + for elem in range(0, 2*n, 2): + tree.insert(elem) + + # We keep track of elements present and not present in the tree + elems = list(range(0, n, 2)) + anti_elems = list(range(-1, 2*n+1, 2)) + + for _ in range(n): + # Delete random element + elem = random.choice(elems) + tree.remove(elem) + elems.remove(elem) + anti_elems.append(elem) + + # Insert random "anti-element" + elem = random.choice(anti_elems) + tree.insert(elem) + elems.append(elem) + anti_elems.remove(elem) + + print(n, tree.struct_changes_per_op()) + +def test_min(): + for exp in range(32, 64): + n = int(2 ** (exp / 4)) + tree = BenchmarkingABTree(a, b) + + for i in range(n): + tree.insert(i) + + for _ in range(n): + tree.remove(0) + tree.insert(0) + + print(n, tree.struct_changes_per_op()) + +tests = { + "min": test_min, + "insert": test_insert, + "random": test_random, +} + +if __name__ == '__main__': + if len(sys.argv) == 4: + test, student_id = sys.argv[1], sys.argv[2] + a = 2 + if sys.argv[3] == "2-3": + b = 3 + elif sys.argv[3] == "2-4": + b = 4 + else: + raise ValueError("Last argument must be either '2-3' or '2-4'") + random.seed(student_id) + if test in tests: + tests[test]() + else: + raise ValueError("Unknown test {}".format(test)) + else: + raise ValueError("Usage: {} <test> <student-id> (2-3|2-4)".format(sys.argv[0])) diff --git a/05-ab_experiment/task.md b/05-ab_experiment/task.md new file mode 100644 index 0000000..663f54e --- /dev/null +++ b/05-ab_experiment/task.md @@ -0,0 +1,81 @@ +## Goal + +The goal of this assignment is to evaluate your implementation of (a,b)-trees +experimentally and compare performance of (2,3) and (2,4)-trees. + +You are given a test program (`ab_experiment`) which is used to evaluate your +implementation of the previous assignment. The test program auguments your implementation +by implementing a `remove` method and it performs the following experiments: + +- _Insert test:_ Insert _n_ elements in random order. +- _Min test:_ Insert _n_ elements sequentially and then _n_ times repeat: remove the minimal + element in the tree and then insert it back. +- _Random test:_ Insert _n_ elements sequentially and then _n_ times repeat: remove random + element from the tree and then insert random element into the tree. Removed element is + always present in the tree and inserted element is always *not* present in the tree. + + +The program tries each experiment with different values of _n_. In each try, +it prints the average number of _structural changes_ per operation. Structural change is +either a node split (in insert) or merging of two nodes (in delete). + +You should perform these experiments and write a report, which contains the following +plots of the measured data. Each plot should show the dependence of the average +number of structural changes on the set size _n_. + +- The insert test: one curve for (2,3) tree, one for (2,4) tree. +- The min test: one curve for (2,3) tree, one for (2,4) tree. +- The random test: one curve for (2,3) tree, one for (2,4) tree. + +The report should discuss the experimental results and try to explain the observed +behavior using theory from the lectures. (If you want, you can carry out further +experiments to gain better understanding of the data structure and include these +in the report. This is strictly optional.) + +You should submit a PDF file with the report (and no source code). +You will get 1 temporary point upon submission if the file is syntantically correct; +proper points will be assigned later. + +## Test program + +The test program is given three arguments: +- The name of the test (`insert`, `min`, `random`). +- The random seed: you should use the last 2 digits of your student ID (you can find + it in the Study Information System – just click on the Personal data icon). Please + include the random seed in your report. +- The type of the tree to test (`2-3` or `2-4`). + +The output of the program contains one line per experiment, which consists of _n_ and the +average number of structural changes. + +## Your implementation + +Please use your implementation from the previous exercise. Methods `split_node(...)` +and `insert()` will be augmented by the test program. If you are performing +a node splits directly instead of using `split_node(...)` method, you +need to adjust the `BenchmarkingABTree` class accordingly. + +## Hints + +The following tools can be useful for producing nice plots: +- [pandas](https://pandas.pydata.org/) +- [matplotlib](https://matplotlib.org/) +- [gnuplot](http://www.gnuplot.info/) + +A quick checklist for plots: +- Is there a caption explaining what is plotted? +- Are the axes clearly labelled? Do they have value ranges and units? +- Have you mentioned that this axis has logarithmic scale? (Logarithmic graphs + are more fitting in some cases, but you should tell.) +- Is it clear which curve means what? +- Is it clear what are the measured points and what is an interpolated + curve between them? +- Are there any overlaps? (E.g., the most interesting part of the curve + hidden underneath a label?) + +In your discussion, please distinguish the following kinds of claims. +It should be always clear which is which: +- Experimental results (i.e., the raw data you obtained from the experiments) +- Theoretical facts (i.e., claims we have proved mathematically) +- Your hypotheses (e.g., when you claim that the graph looks like something is true, + but you are not able to prove rigorously that it always holds) -- GitLab