{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "<script async src=\"https://www.googletagmanager.com/gtag/js?id=UA-59152712-8\"></script>\n", "<script>\n", " window.dataLayer = window.dataLayer || [];\n", " function gtag(){dataLayer.push(arguments);}\n", " gtag('js', new Date());\n", "\n", " gtag('config', 'UA-59152712-8');\n", "</script>\n", "\n", "# Unit Testing Functions Reference\n", "\n", "## Author: Kevin Lituchy\n", "\n", "## Introduction:\n", "This module contains in-depth explanations of all the functions in `UnitTesting`.If you have not already, please read through the Jupyter notebook [tutorial](../Tutorial-UnitTesting.ipynb) for unit testing. This will contain in-depth information on all functions used for unit testing, not a high-level user tutorial. With examples, the default module that will be used is `UnitTesting/Test_UnitTesting/test_module.py`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='toc'></a>\n", "\n", "# Table of Contents\n", "$$\\label{toc}$$\n", "\n", "This module is organized as follows:\n", "\n", "1. [`Non-interactive Files`](#non_interactive_files)\n", " 1. [`failed_tests`](#failed_tests)\n", " 1. [`standard_constants`](#standard_constants)\n", "1. [`run_NRPy_UnitTests`](#run_NRPy_UnitTests)\n", "1. [`create_test`](#create_test)\n", " 1. [`setup_trusted_values_dict`](#setup_trusted_values_dict)\n", " 1. [`RepeatedTimer`](#RepeatedTimer)\n", "1. [`run_test`](#run_test)\n", " 1. [`evaluate_globals`](#evaluate_globals)\n", " 1. [`cse_simplify_and_evaluate_sympy_expressions`](#cse_simplify_and_evaluate_sympy_expressions)\n", " 1. [`expand_variable_dict`](#expand_variable_dict)\n", " 1. [`get_variable_dimension`](#get_variable_dimension)\n", " 1. [`flatten`](#flatten)\n", " 1. [`form_string`](#form_string)\n", " 1. [`increment_counter`](#increment_counter)\n", " 1. [`calculate_value`](#calculate_value)\n", " 1. [`create_dict_string`](#create_dict_string)\n", " 1. [`first_time_print`](#first_time_print)\n", " 1. [`calc_error`](#calc_error)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='non_interactive_files'></a>\n", "\n", "# `Non-interactive Files` \\[Back to [top](#toc)\\]\n", "$$\\label{non_interactive_files}$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='failed_tests'></a>\n", "\n", "## `failed_tests` \\[Back to [top](#toc)\\]\n", "$$\\label{failed_tests}$$\n", "\n", "[`failed_tests.txt`](../../edit/UnitTesting/failed_tests.txt) is a simple text file that keeps track of which tests\n", "failed. Line 1 is by default 'Failures: '. The subsequent lines tell the\n", "user which test functions in which test files failed in the following\n", "format: `[test file path]: [test function]`\n", "\n", "Example:\n", "\n", "Say that the test function `test_module_for_testing_no_gamma()` failed.\n", "Then we'd expect `failed_tests.txt` to be the following:\n", "\n", "```\n", "Failures:\n", "\n", "UnitTesting/Test_UnitTesting/test_module.py: test_module_for_testing_no_gamma\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='standard_constants'></a>\n", "\n", "## `standard_constants` \\[Back to [top](#toc)\\]\n", "$$\\label{standard_constants}$$\n", "\n", "[`standard_constants.py`](../../edit/UnitTesting/standard_constants.py) stores test-wide information that the user can\n", "modify to impact the numerical result for their globals. It currently\n", "only has one field, `precision`, which determines how precise the values\n", "for the globals are. It is by default set to `30`, which we've\n", "determined to be a reasonable amount. This file has the ability to be\n", "expanded upon in the future, but it is currently minimal." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='run_NRPy_UnitTests'></a>\n", "\n", "\n", "# `run_NRPy_UnitTests` \\[Back to [top](#toc)\\]\n", "$$\\label{run_NRPy_UnitTests}$$\n", "\n", "[`run_NRPy_UnitTests.sh`](../../edit/UnitTesting/run_NRPy_UnitTests.sh) is a bash script that acts as the hub for\n", "running tests -- it's where the user specifies the tests they'd like to\n", "be run. It keeps track of which tests failed by interacting with\n", "[failed_tests](#failed_tests), giving the user easily readable output\n", "from the file. It also has the option to automatically rerun the tests\n", "that failed in `DEBUG` mode if the boolean `rerun_if_fail` is `true`.\n", "\n", "The script is run with the following syntax:\n", "\n", "```\n", "./UnitTesting/run_NRPy_UnitTests.sh [python interpreter]\n", "```\n", "\n", "This of course assumes that the user is in the nrpy directory; the user\n", "simply has to specify the path from their current directory to the bash\n", "file.\n", " \n", "Examples of `python interpreter` are `python` and `python3`.\n", "\n", "The script first lets the user know if they forgot to pass a python\n", "interpreter. Then if they didn't, it prints some baseline information\n", "about Python variables: `PYTHONPATH`, `PYTHONEXEC`, and `PYTHONEXEC`\n", "version.\n", "\n", "`failed_tests.txt` is then overwritten with the default information.\n", "This makes it so that each subsequent test call has a unique list of the\n", "tests that passed; it wouldn't make sense to store this information.\n", "\n", "The user can then change the boolean `rerun_if_fail` if need be. Next,\n", "the user can add tests using the `add_test` function. The syntax is as\n", "follows: `add_test [path to test file]`\n", "\n", "Example:\n", "\n", "```\n", "add_test UnitTesting/Test_UnitTesting/test_module.py\n", "```\n", "\n", "Finally, the bash script will read any failures from `failed_tests.txt`\n", "and, if `rerun_if_fail` is `true`, rerun those tests. It lastly prints\n", "which tests failed in the same format as `failed_tests.txt`, and if no\n", "tests failed, a success message." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='create_test'></a>\n", "\n", "# `create_test` \\[Back to [top](#toc)\\]\n", "$$\\label{create_test}$$\n", "\n", "[`create_test`](../../edit/UnitTesting/create_test.py) is a function that takes the following user-supplied\n", "information: \n", "\n", "- `module`, the module to be tested \n", "- `module_name`, the name of the module \n", "- `function_and_global_dict`, a dictionary whose keys are functions and whose values are lists of globals \n", "\n", "It uses this information to generate a test file that is automatically run by the bash\n", "script; this test file does all the heavy lifting in calling the\n", "function, getting expressions for all the globals, evaluating the\n", "expressions to numerical values, and storing the values in the proper\n", "trusted_values_dict.\n", "\n", "`create_test` additionally takes optional arguments `logging_level` and\n", "`initialization_string_dict`, which respectively determine the desired\n", "level of output (think verbosity) and run some Python code prior calling\n", "the specified function. Usage is as following:\n", "\n", "```\n", "module = 'BSSN.BrillLindquist'\n", "\n", "module_name = 'BrillLindquist'\n", "\n", "function_and_global_dict = {'BrillLindquist(ComputeADMGlobalsOnly = True)': ['alphaCart', 'betaCartU', 'BCartU', 'gammaCartDD', 'KCartDD']}\n", "\n", "create_test(module, module_name, function_and_global_dict)\n", "```\n", "\n", "The way to think of this is that the module to be tested is\n", "BSSN.BrillLindquist. The module_name is how you refer to this module --\n", "it's a bit arbitrary, so whether you prefer BrillLindquist or bl, it\n", "won't change the computation. The function_and_global_dict contains\n", "entry 'BrillLindquist(ComputeADMGlobalsOnly = True)', which is the\n", "function that gets called in the module. It's value in the dictionary is\n", "a list of globals that get created when this function gets called.\n", "\n", "Now let's add the optional arguments into the same example:\n", "\n", "```\n", "module = 'BSSN.BrillLindquist'\n", "\n", "module_name = 'BrillLindquist'\n", "\n", "function_and_global_dict = {'BrillLindquist(ComputeADMGlobalsOnly = True)': ['alphaCart', 'betaCartU', 'BCartU', 'gammaCartDD', 'KCartDD']}\n", "\n", "logging_level = 'DEBUG'\n", "\n", "initialization_string_dict = {'BrillLindquist(ComputeADMGlobalsOnly = True)': 'print(\"example\")\\nprint(\"Hello world!\")'}\n", "\n", "create_test(module, module_name, function_and_global_dict, logging_level=logging_level, initialization_string_dict=initialization_string_dict)\n", "```\n", "\n", "Now when create_test runs, the user will be given much more output due\n", "to the logging_level; additionally, the user-specified print will occur\n", "due to initialization_string_dict.\n", "\n", "You may now be wondering why we use dictionaries to store this data\n", "instead of simply having separate variables `function`, `global_list`,\n", "and `initialization_string`. This is where some of the power of this\n", "testing method lies: we can test multiple functions and their globals\n", "with ease! In other words, function_and_global_dict can contain multiple\n", "entries, each a specific function call with its own associated list of\n", "globals. Since not every function being tested must have an associated\n", "initialization_string, we make an entry for each function optional. An\n", "example is as follows:\n", "\n", "```\n", "module = 'BSSN.BrillLindquist'\n", "\n", "module_name = 'BrillLindquist'\n", "\n", "function_and_global_dict = {'BrillLindquist(ComputeADMGlobalsOnly = True)': ['alphaCart', 'betaCartU', 'BCartU', 'gammaCartDD', 'KCartDD'],\n", " 'BrillLindquist(ComputeADMGlobalsOnly = False)': ['alphaCart', 'betaCartU', 'BCartU', 'gammaCartDD', 'KCartDD']}\n", "\n", "logging_level = 'DEBUG'\n", "\n", "initialization_string_dict = {'BrillLindquist(ComputeADMGlobalsOnly = True)': 'print(\"example\")\\nprint(\"Hello world!\")'}\n", "\n", "create_test(module, module_name, function_and_global_dict, logging_level=logging_level, initialization_string_dict=initialization_string_dict)\n", "```\n", "\n", "Both instances will be called separately, with their own globals. The\n", "print statements will only be called in the first function, since there\n", "is no associated initialization_string for the second function as well.\n", "\n", "An important note when using `create_test` is that all arguments are\n", "**strings**. This includes the module, module_name, function, each\n", "global in the list of globals, logging level, and initialization_string.\n", "The reason for making these fields strings is that when setting\n", "module_name, for example, there doesn't exist anything in Python with\n", "the name BrillLindquist. So, we wrap it in a string. This is true of\n", "every input. Be careful with the dicts and lists, however: their\n", "arguments are strings, they aren't themselves strings.\n", "\n", "So what does this funciton actually do at a lower level? First, `create_test` makes sure that all user arguments are of the correct type, failing if any are incorrect.\n", "\n", "It then loops through every function in `function_and_global_dict`, and creates `file_string`, a string that represents a unit test file that will be executed. Along with the current function comes its global list, and initialization string if it has one.\n", "\n", "Next, the contents from [run_test](#run_test) is copied into `file_string` so that the test will be automatically run, and so the user can easily see their unique file contents in case of an error.\n", "\n", "A file is then created which houses the `file_string`, and is what gets called with a bash script. The file exists in the directory of the test being run, and is deleted upon test success, but left for the user to inspect upon test failure. `file_string` has code to call [setup_trusted_values_dict](#setup_trusted_values_dict), initialize [RepeatedTimer](#RepeatedTimer), and runs [run_test](#run_test) in order to ensure that the test will run correctly.\n", "\n", "Finally, the test file is called using [cmdline_helper](../../edit/cmdline_helper.py), which runs the test and does everything described in [run_test](#run_test).\n", "\n", "Once `run_test` finishes, either the test failed or the test succeeded. `file_string` has additonal code to create a `success.txt` file if the test passes, and do nothing otherwise. So, `create_test` looks for the existence of `success.txt`. If it exists, we delete it and create_test has finished for the current function and moves on to the next function. Otherwise, if `success.txt` doesn't exist, an exception is thrown to be caught later.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='setup_trusted_values_dict'></a>\n", "\n", "## `setup_trusted_values_dict` \\[Back to [top](#toc)\\]\n", "$$\\label{setup_trusted_values_dict}$$\n", "\n", "[`setup_trusted_values_dict`](../../edit/UnitTesting/setup_trusted_values_dict.py) takes in a path to a test directory `path`,\n", "and checks whether or not a `trusted_values_dict.py` exists in the test\n", "directory. If it does exist, the function does nothing. If it doesn't\n", "exist, `setup_trusted_values_dict` creates the file\n", "`trusted_values_dict.py` in the test directory. In then writes the\n", "following default code into the file:\n", "\n", "```\n", "from mpmath import mpf, mp, mpc\n", "from UnitTesting.standard_constants import precision\n", "\n", "mp.dps = precision\n", "trusted_values_dict = {}\n", "\n", "```\n", "\n", "The default code allows the unit test to properly interact with and\n", "write to the file." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='RepeatedTimer'></a>\n", "\n", "## `RepeatedTimer` \\[Back to [top](#toc)\\]\n", "$$\\label{RepeatedTimer}$$\n", "\n", "[`RepeatedTimer`](../../edit/UnitTesting/RepeatedTimer.py) is a class that allows the user to automatically run some function every `n` seconds. We specifically use the class for timed output, where we print every five minutes in order to prevent time-outs in Travis CI; if Travis CI doesn't receive any output for too long, the build automatically fails.\n", "Due to some NRPy modules taking a long time due to their sheer size, we want to ensure that the build doesn't time-out simply due to it taking a long time." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='run_test'></a>\n", "\n", "# `run_test` \\[Back to [top](#toc)\\]\n", "$$\\label{run_test}$$\n", "\n", "[`run_test`](../../edit/UnitTesting/run_test.py) acts as the hub for an individual unit test. It takes in all\n", "the module-wide information, and goes through all the steps of\n", "determining whether the test passed or failed by calling many\n", "sub-functions. A fundamentally important part of `run_test` is the\n", "notion of `self`; `self` stores a test's information (i.e. `module`,\n", "`module_name`, etc.) to be able to easily pass information to and make\n", "assertions in sub-functions. When `self` is referenced, simply think\n", "\"information storage\".\n", "\n", "`run_test` begins by importing the `trusted_values_dict` of the current\n", "module being tested; since `setup_trusted_values_dict` is called before\n", "`run_test`, we know it exists.\n", "\n", "`run_test` then determines if the current function/module is being done\n", "for the first time based off the existence of the proper entry in\n", "`trusted_values_dict`, and stores this boolean in `first_time`. \n", "\n", "[`evaluate_globals`](#evaluate_globals) is then run in order to generate\n", "the SymPy expressions for each global being tested. \n", "\n", "Next,\n", "[`cse_simplify_and_evaluate_sympy_expressions`](#cse_simplify_and_evaluate_sympy_expressions)\n", "is called to turn each SymPy expression for each global into a random, yet predictable/repeatable, number. \n", "\n", "The next step depends on the value of `first_time`: if `first_time` is `True`, then [`first_time_print`](#first_time_print) is run to print the result both to the console and to the `trusted_values_dict.py`. Otherwise, if `first_time` is `False`, [`calc_error`](#calc_error) is called in order to compare the calculated values and the trusted values for the current module/function. If an error was found, the difference is printed and the code exits. Otherwise, the module completes and returns.\n", "\n", "On it's own, `run_test` doesn't do much -- it's the subfunctions called by `run_test` that do the heavy lifting in terms of formatting, printing, calculating, etc." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='evaluate_globals'></a>\n", "\n", "## `evaluate_globals` \\[Back to [top](#toc)\\]\n", "$$\\label{evaluate_globals}$$\n", "\n", "[`evaluate_globals`](../../edit/UnitTesting/evaluate_globals.py) takes in `self` and uses the following attributes:\n", "\n", "- `self.module`\n", "- `self.module_name`\n", "- `self.initialization_string`\n", "- `self.function`\n", "- `self.global_list`\n", "\n", "`evaluate_globals` first imports `self.module` as a module object, instead of a simple string. It next runs `self.initialization_string`, then creates string of execution `string_exec` to be called; this string of execution calls `self.function` on `self.module`, then gets the SymPy expressions for all globals defined in `self.global_list` and returns a dictionary containing all the globals and their expressions." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='cse_simplify_and_evaluate_sympy_expressions'></a>\n", "\n", "## `cse_simplify_and_evaluate_sympy_expressions` \\[Back to [top](#toc)\\]\n", "$$\\label{cse_simplify_and_evaluate_sympy_expressions}$$\n", "\n", "\n", "[`cse_simplify_and_evaluate_sympy_expressions`](../../edit/UnitTesting/cse_simplify_and_evaluate_sympy_expressions.py) takes in `self` and uses the following attributes:\n", "\n", "- `self.variable-dict`\n", "\n", "`cse_simplify_and_evaluate_sympy_expressions` uses SymPy's cse algorithm to efficiently calculate a value for each expression by assigning a random, yet predictable and consistent, number to each variable in the variable dictionary.\n", "\n", "`cse_simplify_and_evaluate_sympy_expressions` first calls [`expand_variable_dict`](#expand_variable_dict) on `self.variable_dict` to expand the variable dictionary. This makes it much easier for the user to figure out which variables differ, if any.\n", "Basic example:\n", "\n", "```\n", "variable_dict = {'betaU': [0, 10, 400]}\n", "expand_variable_dict(variable_dict) --> {'betaU[0]': 0, 'betaU[1]: 10, 'betaU[2]': 400}\n", "\n", "```\n", "\n", "This may seem trivial and unnecessary for a small example, but once the tensors get to a higher rank, such as `GammahatUDDdD`, which is rank 5 by NRPy naming convention, it's easy to see why the expansion is necessary for user clarity.\n", "\n", "Next, `cse_simplify_and_evaluate_sympy_expressions` loops through each variable in the expanded variable dict and adds that variable's free symbols to a set containing all free symbols from all variables. 'Free symbols' are simply the SymPy symbols that make up a variable.\n", "\n", "Once we get this set of free symbols, we know we have every symbol that will be referenced when substituting in values. So, we assign each symbol in this set to a random value. To ensure that this remains predictable and consistent, we do the following:\n", "\n", "- Create a string representation of the symbol\n", "- Use Python's hashlib module to hash the string\n", "- Turn the hash value into a hex number\n", "- Turn the hex number into an decimal number\n", "- Pass the decimal number into Python's random module as the seed\n", "- Assign the next random number in the given seed to the symbol\n", "\n", "This method ensures a symbol will always be assigned the same random value throughout Python instances, operating systems, and Python versions. If the symbol is `M_PI` or `M_SQRT1_2`, however, we want to assign them to their exact values; they should be given their true values of `pi` and `1/sqrt(2)`, respectively.\n", "\n", "\n", "`cse_simplify_and_evaluate_sympy_expressions` then loops through the expanded variable dictionary in order to calculate a value for each variable. In order to optimize the calculation for massive expressions, we use SymPy's cse ([common subexpression elimination](https://en.wikipedia.org/wiki/Common_subexpression_elimination)) algorithm to greatly improve the speed of calculation. What this algorithm does in essence is factor out common subexpressions (i.e. `a**2`) by storing them in their own variables (i.e. `x0 = a**2`) so that any time `a**2` shows up, we don't have to recalculate the value of the subexpression; we instead just replace it with `x0`. This is a seemingly small optimization, but as NRPy expressions can become massive, the small efficiencies add up and make the calculation orders of magnitude faster.\n", "\n", "Once the cse algorithm optimizes the calculation for a given variable, we now calculate a numerical result for the variable using [`calculate_value`](#calculate_value) and store it in a calculated dictionary. For numerical results near zero, we double check the calculation at a higher precision to see if the value should truly be zero.\n", "\n", "Finally, after repeating this process for each variable, we return the calculated dictionary which stores numerical values for each variable.\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='expand_variable_dict'></a>\n", "\n", "### `expand_variable_dict` \\[Back to [top](#toc)\\]\n", "$$\\label{expand_variable_dict}$$\n", "\n", "[`expand_variable_dict`](../../edit/UnitTesting/cse_simplify_and_evaluate_sympy_expressions.py) takes in a variable dictionary `variable_dict` , and returns an expanded variable dictionary. 'expanded' refers to taking all tensors in `variable_dict` and breaking them down into their subcomponents according to the indices of the tensor. This is best illustrated with an example:\n", "\n", "```\n", "variable_dict = {'alpha': 0, 'betaU': [3.14, 1, -42]}\n", "expand_variable_dict(variable_dict) --> {'alpha': 0, 'betaU[0]': 3.14, 'betaU[1]': 1, 'betaU[2]' = -42}\n", "\n", "variable_dict = {'aDD': [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}\n", "expanded_variable_dict(variable_dict) --> {'aDD[0][0]': 1, 'aDD[0][1]': 2, 'aDD[0][2]': 3, 'aDD[1][0]': 4, 'aDD[1][1]': 5, 'aDD[1][2]': 6, 'aDD[2][0]': 7, 'aDD[2][1]': 8, 'aDD[2][2]': 9}\n", "```\n", "\n", "As we can see, `expand_variable_dict` breaks up tensors into their indices. While this may not be obviously useful for low-rank tensors, high-rank tensors become overwhelmingly difficult to debug, and as such their expansion greatly eases the debugging process.\n", "\n", "`expand_variable_dict` starts out by looping through each variable in `variable_dict`. For each variable, it first gets the dimension of the variable's expression list using [`get_variable_dimension`](#get_variable_dimension) -- this is equivalent to getting the rank of the tensor.\n", "\n", "`expand_variable_dict` then initializes a counter of the correct dimension; the counter represents the current index of the expression list.\n", "\n", "Next, `expand_variable_dict` flattens the expression list into a rank-1 tensor using [`flatten`](#flatten); this makes it simple to iterate through. \n", "\n", "The flattened expression list is then iterated through, and each expression in the expression list is stored in a result dictionary `result_dict`. To get the key for the entry into `result_dict`, we pass the current variable and the counter into ['form_string'](#form_string), which gives us a string representation of an index of a variable. The value for the respective key is simply the current expression.\n", "\n", "The counter is then incremented using [`increment_counter`](#increment_counter) to ensure a consistent naming scheme.\n", "\n", "This process is repeated for all variables, and finally `result_dict` is returned." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='get_variable_dimension'></a>\n", "\n", "### `get_variable_dimension` \\[Back to [top](#toc)\\]\n", "$$\\label{get_variable_dimension}$$\n", "\n", "[`get_variable_dimension`](../../edit/UnitTesting/cse_simplify_and_evaluate_sympy_expressions.py) takes in a tensor `tensor` and returns a tuple containing the rank of `tensor` and the dimension of `tensor`. It does this by looping through the first element of the tensor until it's rank-0. Then the number of times it had to loop is the rank of the tensor. The dimension of the tensor is simply the length of the rank-1 tensor. Example:\n", "\n", "```\n", "tensor = 42\n", "get_variable_dimension(tensor) --> 0, 1\n", "\n", "tensor = [1, 2, 3, 4, 5]\n", "get_variable_dimension(tensor) --> 1, 5\n", "\n", "tensor = [[1, 2], [3, 4]] --> 2, 2\n", "get_variable_dimension(tensor) --> 2, 2\n", "```\n", "\n", "`get_variable_dimension` assumes that `tensor` has the same dimension at all ranks.\n", "\n", "This means it's square, or it's dimension could be written `N x N x ... x N`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='flatten'></a>\n", "\n", "### `flatten` \\[Back to [top](#toc)\\]\n", "$$\\label{flatten}$$\n", "\n", "[`flatten`](../../edit/UnitTesting/cse_simplify_and_evaluate_sympy_expressions.py) takes in a list `l` and an optional argument the represents already flattened list `fl`. The user need not pay attention to `fl`: it's simply used for the recursive call, so the user should use `flatten` with only `l` as an argument. It 'unwraps' each element of `l` by recursively calling `flatten` while the current index of the current element is of type `list`. Once it's not of type list, append it to `fl`, and once every element has been flattened, return `fl`. Example:\n", "\n", "```\n", "l = [1, 2, 3]\n", "flatten(l) --> [1, 2, 3]\n", "\n", "l = [[1, 2], [3, 4]]\n", "flatten(l) --> [1, 2, 3, 4]\n", "\n", "l = [[[[[2], 3], 4, [5, 6], 7]], 8]\n", "flatten(l) --> [2, 3, 4, 5, 6, 7, 8]\n", "```\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='form_string'></a>\n", "\n", "### `form_string` \\[Back to [top](#toc)\\]\n", "$$\\label{form_string}$$\n", "\n", "[`form_string`](../../edit/UnitTesting/cse_simplify_and_evaluate_sympy_expressions.py) takes in a variable `var` and a counter `counter` and returns the string representation of the `counter`'th index of `var`. Example:\n", "\n", "```\n", "var = 'alpha'\n", "counter = [0, 0]\n", "form_string(var, counter) --> 'alpha[0][0]'\n", "\n", "var = 'gammauDDdD'\n", "counter = [2, 3, 4, 1, 0]\n", "form_string(var_counter) --> 'gammauDDdD[2][3][4][1][0]'\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='increment_counter'></a>\n", "\n", "### `increment_counter` \\[Back to [top](#toc)\\]\n", "$$\\label{increment_counter}$$\n", "\n", "[`increment_counter`](../../edit/UnitTesting/cse_simplify_and_evaluate_sympy_expressions.py) takes in a counter `counter` and a length `length` and returns a new counter which is equivalent to (`counter` + 1) in base `length`.\n", "\n", "`increment_counter` loops through `counter` in reverse, adds `1` to the last digit, and then checks if any digit is now equal to `length` -- if it is, increment the next digit as well, and so on.\n", "\n", "It then returns the un-reversed new counter which represents an iteration of `counter`.\n", "\n", "Example:\n", "\n", "```\n", "counter = [0, 0]\n", "length = 2\n", "increment_counter(counter, length) -> [0, 1]\n", "\n", "counter = [0, 1]\n", "length = 2\n", "increment_counter(counter, length) -> [1, 0]\n", "\n", "counter = [2, 3, 4, 4, 4]\n", "length = 5\n", "increment_counter(counter, length) -> [2, 4, 0, 0, 0]\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='calculate_value'></a>\n", "\n", "### `calculate_value` \\[Back to [top](#toc)\\]\n", "$$\\label{calculate_value}$$\n", "\n", "[`calculate_value`](../../edit/UnitTesting/cse_simplify_and_evaluate_sympy_expressions.py) takes in a dictionary of free symbols `free_symbols_dict`, the outputs from SymPy's cse algorithm `replaced` and `reduced`, and an optional argument `precision_factor` which is `1` by default.\n", "\n", "`calculate_value` first sets `reduced` to `reduced[0]` -- this is to remove extraneous output from the cse algorithm. It then sets the precision to `standard_constants.precision * precision_factor`. This is why `precision_factor` is optional and defaults to `1` -- most of the time we want the standard precision. \n", "\n", "`calculate_value` next loops through `replaced`. `replaced` is a dictionary whose keys are new expressions and whose values are old expressions; in essence, we set the new expression to the value we get by calculating the old expression. This is equivalent to calculating numerical values for all the cse optimizations instead of expressions.\n", "\n", "Now that we have values for all the new expressions, we can substitute these into the overall expression given to us by `reduced`. After doing this, we finally have a value for our expression that we arrived at as optimally as possible.\n", "\n", "Finally, we store this value as an `mpf` -- if it is actually complex, we store it as a `mpc`. The precision is then set back to `standard_constants.precision` before the final value is returned." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='create_dict_string'></a>\n", "\n", "## `create_dict_string` \\[Back to [top](#toc)\\]\n", "$$\\label{create_dict_string}$$\n", "\n", "[`create_dict_string`](../../edit/UnitTesting/cse_simplify_and_evaluate_sympy_expressions.py) takes in a dictonary of values and returns a well-formatted string representation of the dictionary. It ensures that a consistent, repeatable output is returned from two identical dictionaries. \n", "\n", "`create_dict_string` sorts the dictionary of values, loops through each (key, value) pair of the dictionary, and creates a string based on the type of the value (mpf, mpc, or other). By doing this, it creates a string representation of the dictionary of values that can be printed to a `trusted_values-dict`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='first_time_print'></a>\n", "\n", "## `first_time_print` \\[Back to [top](#toc)\\]\n", "$$\\label{first_time_print}$$\n", "\n", "[`first_time_print`](../../edit/UnitTesting/first_time_print.py) takes in `self` and a boolean, `write`, and uses the following attributes:\n", "\n", "- `self.module_name`\n", "- `self.trusted_values_dict_name`\n", "- `self.calculated_dict`\n", "- `self.path`\n", "\n", "`first_time_print` prints the string that should be copied into your `trusted_values_dict` to the console. If `write` is `True`, it additionally automatically appends the string to the `trusted_values_dict.py`.\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<a id='calc_error'></a>\n", "\n", "## `calc_error` \\[Back to [top](#toc)\\]\n", "$$\\label{calc_error}$$\n", "\n", "[`calc_error`](../../edit/UnitTesting/calc_error.py) takes in `self` and uses the following attributes:\n", "\n", "- `self.calculated_dict`\n", "- `self.trusted_values_dict_entry`\n", "- `self.module_name`\n", "\n", "`calc_error` loops through each variable in `self.calculated_dict` and `self.trusted_values_dict_entry`, and compares the variables' values to ensure that no variable differs.\n", "\n", "`calc_error` first makes sure that `self.calculated_dict` and `self.trusted_values_dict_entry` have the same entries; that is, they contain values for the same variables. If any variables differ, those variables are printed and the function returns `False`.\n", "\n", "If the dictionaries have the same entries, we must compare the two values for a given variable. We compare the values by seeing by how many decimal places the values differ; if they differ by more than (`standard_constants.precision` / 2) decimal places, then we consider the values to be different. The reason for this method of comparison is the inconsistency of floating point calculations; if we simply checked for equality, we'd consistently run into error when in reality the values differed by a minimal amount -- say `2 * 10 ** -30` -- which is close enough to have arisen from the same calculation.\n", "\n", "We repeat this process for each variable, storing the variables who differed in a list. After each variable has been checked, we see if the list of differing variables is empty. If it's empty, no variables differed -- so the test passed -- and we return `True`. Otherwise, if it's not empty, we print the contents of the list -- the variables that differed. We additionally print a new `trusted_values_dict` entry (similar to [first_time_print](#first_time_print) that the user can look at and determine if it's correct or not. The function then returns `False`, as the test failed." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.3" } }, "nbformat": 4, "nbformat_minor": 2 }