{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "active-ipynb",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "try:\n",
    "    from openmdao.utils.notebook_utils import notebook_mode  # noqa: F401\n",
    "except ImportError:\n",
    "    !python -m pip install openmdao[notebooks]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(reports-system)=\n",
    "# Reports System\n",
    "\n",
    "## Default Usage\n",
    "\n",
    "OpenMDAO has a reports system which will generate reports when you run your model. To see a list of the\n",
    "reports that are run by default, along with a brief description of each, use the command:\n",
    "\n",
    "```\n",
    "openmdao list_reports -d\n",
    "```\n",
    "\n",
    "Running the same command without the `-d` will list all reports that are available.\n",
    "\n",
    "Here is a table with more info about these default commands.\n",
    "\n",
    "| Report Name      | Description                                                  | When Run                       | Doc link                                                           |\n",
    "|------------------|--------------------------------------------------------------|--------------------------------|--------------------------------------------------------------------|\n",
    "| `inputs`         | Interactive table of model inputs                            | after `Problem.final_setup`    |                                                                     |\n",
    "| `n2`             | Interactive, graphical view of the model                     | after `Problem.final_setup`    | [N2 diagram](../model_visualization/n2_basics/n2_basics.ipynb)     |\n",
    "| `optimizer`      | Summary of results of an optimization run                    | after `Drive._post_run`        | [optimization report](optimization_report.ipynb)  \n",
    "| `scaling`        | Information on design variables, objectives, and constraints | after `Driver._compute_totals` | [scaling report](../model_visualization/view_scaling_report.ipynb) |\n",
    "| `total_coloring` | Metadata associated with the creation of the coloring        | after `Driver._get_coloring`   |                                                                    |\n",
    "\n",
    "\n",
    "Note that the reports are only run the first time the method is called on a given instance. For example, the scaling report is only run after the first call to `Driver._compute_totals`.\n",
    "\n",
    "These reports will, by default, be put into a subfolder of `reports` named after the `Problem` name. \n",
    "For example, if your model has a single Problem and its default name, `problem1`, was not changed by the user, the reports will be placed in a directory named `reports/problem1`.\n",
    "\n",
    "The user has control over the reports system by setting some environment variables which are described below.\n",
    "\n",
    "## Controlling which reports are generated\n",
    "\n",
    "There are two ways to control which reports are generated on which models.\n",
    "\n",
    "(features:reports:report_system:controlling_reporting_using_an_environment_variable)=\n",
    "### Controlling reporting using an environment variable\n",
    "To turn off OpenMDAO's default report generation, the user should set the `OPENMDAO_REPORTS`\n",
    "environment variable to any one of these five case-insensitive values: '0', 'false', 'no', 'off', or 'none'.\n",
    "\n",
    "The user can also have more fine-grained control over what reports are generated by setting this environment variable to a string that lists the reports to run. For example\n",
    "\n",
    "1. OPENMDAO_REPORTS=n2\n",
    "\n",
    "2. OPENMDAO_REPORTS=n2,scaling\n",
    "\n",
    "3. OPENMDAO_REPORTS=none\n",
    "\n",
    "If `OPENMDAO_REPORTS` is unset, the default reports will be generated. \n",
    "\n",
    "The user can also explicitly turn on all available reports by setting `OPENMDAO_REPORTS` to 'all'.\n",
    "\n",
    "### Controlling reporting using the Problem reports argument \n",
    "The user can also control what reports are generated using the `reports` argument to `Problem`.\n",
    "\n",
    "If `reports` is the default of `_UNDEFINED` or `True`, the `OPENMDAO_REPORTS` environment variable determines reporting.\n",
    "\n",
    "If the value for `reports` is `None` or `False`, it will disable all reports on that `Problem`.\n",
    "\n",
    "Otherwise, `reports` may be a list of names or a single comma-delimited string of the names of the reports to run for that Problem.\n",
    "\n",
    "Overall, `OPENMDAO_REPORTS` represents the global default list of reports, and individual Problems can either use that default list or override it by specifying their own list explicitly."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(reports_system:directory)=\n",
    "## Setting the directory for the reports\n",
    "\n",
    "As of OpenMDAO 3.35.0, reports are placed within a \"reports\" directory under the problem's output directory.\n",
    "By default, this directory is `f'{problem_name}_out'` in the current working directory.\n",
    "\n",
    "These directories are separated to ensure that data from multiple OpenMDAO executions does not collide.\n",
    "A side-effect of this is that a users disk space can become cluttered with output files over time.\n",
    "There are two ways to address this:\n",
    "\n",
    "1. The user has the option of setting a common output directory for ALL problems using the `OPENMDAO_WORKDIR` environment variable. Thus there is a single directory that contains all OpenMDAO output data.\n",
    "\n",
    "2. The [openmdao clean](om-command-clean) or `openmdao.api.clean_outputs` function can be used to remove these directories.\n",
    "\n",
    "As an alternative to setting `OPENMDAO_WORKDIR` to set the top level output directory for all problems, a user can set `work_dir` as an option of Problem in order to set the top level output directory under which that problem's `'{problem_name}_out'` directory will be placed. If provided, the value of the `work_dir` option takes precedence.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Support for subproblems\n",
    "\n",
    "The reporting system supports subproblems. For example, if a model has two Problems and they have the names `problem1` and `problem2`, the reporting system will put the reports into the subfolders, `problem1` and `problem2` under the top level reports directory, which defaults to `./reports` or is specified by the value of `OPENMDAO_REPORTS_DIR`.\n",
    "\n",
    "## Adding user defined reports\n",
    "\n",
    "The user can register a user defined report using the `register_report` function.\n",
    "\n",
    "```{eval-rst}\n",
    "    .. autofunction:: openmdao.utils.reports_system.register_report\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "Here is a script to show how this is done. Note that the function that is called to generate the actual report must have as its first argument the OpenMDAO object instance that owns the method where the report will be run. Currently, the only option is a `Problem` or `Driver` instance. The full file path of the report will be determined based on `OPENMDAO_REPORTS_DIR` and the name of the enclosing `Problem` instance.  The directory where the report will be written can\n",
    "be obtained by calling the `get_reports_dir` method on the instance passed into the reporting function.\n",
    "\n",
    "Note that `register_report` only adds a report type to the global reports registry.  In order to activate the report so that it actually generates output, it must appear in either `OPENMDAO_REPORTS` or must be passed in the `Problem` reports arg as shown below.\n",
    "\n",
    "After this script is run, there will be a file named `user_report.txt` in the directory `problem1`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "import os\n",
    "os.environ['OPENMDAO_REPORTS'] = '1'  # Need to run reports in this notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import pathlib\n",
    "import openmdao.api as om\n",
    "from openmdao.test_suite.components.paraboloid import Paraboloid\n",
    "\n",
    "user_report_filename = 'user_report.txt'\n",
    "\n",
    "def user_defined_report(prob):\n",
    "    path = pathlib.Path(prob.get_reports_dir()).joinpath(user_report_filename)\n",
    "    with open(path, \"w\") as f:\n",
    "        f.write(f\"Do some reporting on the Problem, {prob._name}\\n\")\n",
    "\n",
    "# Run the report before the call to Problem.setup\n",
    "om.register_report(\"user_report\", user_defined_report, \"This report is user defined\",\n",
    "                   'Problem', 'setup', 'post')\n",
    "\n",
    "prob = om.Problem(reports='user_report')\n",
    "model = prob.model\n",
    "\n",
    "model.add_subsystem('p1', om.IndepVarComp('x', 0.0), promotes=['x'])\n",
    "model.add_subsystem('p2', om.IndepVarComp('y', 0.0), promotes=['y'])\n",
    "model.add_subsystem('comp', Paraboloid(), promotes=['x', 'y', 'f_xy'])\n",
    "\n",
    "model.add_design_var('x', lower=0.0, upper=1.0)\n",
    "model.add_design_var('y', lower=0.0, upper=1.0)\n",
    "model.add_objective('f_xy')\n",
    "\n",
    "prob.driver = om.ScipyOptimizeDriver()\n",
    "\n",
    "prob.setup()\n",
    "prob.run_driver()\n",
    "prob.cleanup()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "try:\n",
    "    om.unregister_report('user_report')\n",
    "except KeyError:\n",
    "    pass\n",
    "\n",
    "import pathlib\n",
    "path = pathlib.Path(prob.get_reports_dir()).joinpath(user_report_filename)\n",
    "assert path.is_file(),\"Could not find problem report\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are some advanced cases where it may be necessary to register more then one hook for a report. This happens currently with the 'n2' report, which may run after `setup()` if there are errors before or during setup, or after `final_setup()` if there are no errors.  In cases where multiple hooks are necessary, multiple calls to `register_report_hook` should be used instead of a single call to `register_report`.  The signature for `register_report_hook` is:\n",
    "\n",
    "```{eval-rst}\n",
    "    .. autofunction:: openmdao.utils.reports_system.register_report_hook\n",
    "        :noindex:\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Creating a report plugin\n",
    "\n",
    "What if you've developed an OpenMDAO-related python package and would like anyone using that package\n",
    "to have access to a new report type that you've defined within that package?  You can do this by\n",
    "creating an `openmdao_report` plugin using the steps outlined below.  This example will use the\n",
    "existing `summary` report plugin as an example.\n",
    "\n",
    "1) Create the function that generates your report. For example:\n",
    "\n",
    "    ```python\n",
    "    def _summary_report(prob):\n",
    "        path = str(pathlib.Path(prob.get_reports_dir()).joinpath('summary.html'))\n",
    "        s = StringIO()\n",
    "        config_summary(prob, s)\n",
    "        with open(path, 'w') as f:\n",
    "            f.write(text2html(s.getvalue()))\n",
    "    ```\n",
    "\n",
    "    Note that your report function must take as an argument an instance of the type that contains the\n",
    "    method where your report is registered to run.  For example, in the `register_report` call below,\n",
    "    it refers to the `final_setup` method of `Problem`, so our instance in this case must be an \n",
    "    instance of `Problem`.  Also, note that the function calls `prob.get_reports_dir()` to get the\n",
    "    location of the reports directory where the report shouild be written.  In addition, the\n",
    "    string generated from calling `config_summary` is plain text, so it is wrapped in html using the\n",
    "    `text2html` function to make it easily viewable in a browser, and therefore accessible using the\n",
    "    `openmdao view_reports` command.\n",
    "\n",
    "2) Create a function that will register your new report type.  For example:\n",
    "\n",
    "    ```python\n",
    "    def _summary_report_register():\n",
    "        register_report('summary', _summary_report, 'Model summary', 'Problem', 'final_setup', 'post')\n",
    "    ```\n",
    "\n",
    "    The call above will result in a report type called `summary` that triggers at the end of\n",
    "    `Problem.final_setup`.\n",
    "\n",
    "\n",
    "3) Create an entry point in your package's `setup.py` file.  In this example, our report\n",
    "registry function is found in the `openmdao.devtools.debug.py` file, so we'll update the \n",
    "`entry_point` arg in the call to `setup` in our `setup.py` file as follows:\n",
    "\n",
    "    ```python\n",
    "        entry_points = {\n",
    "            \n",
    "            # other entry point groups here...\n",
    "            \n",
    "            'openmdao_report': [\n",
    "                \n",
    "                # other report entry points here...\n",
    "                \n",
    "                'summary_report=openmdao.devtools.debug:_summary_report_register',\n",
    "            ],\n",
    "        }\n",
    "    ```\n",
    "\n",
    "    As shown above, report entry points are part of the `openmdao_report` entry point group.\n",
    "    \n",
    "    Note that the name for your entry point needs to be unique. Here, we use the name \"summary_report\" because\n",
    "    an entry point named \"summary\" already exists for one of the built-in reports.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Viewing existing reports\n",
    "\n",
    "As mentioned earlier, the `openmdao view_reports` command provides an easy way to view all of the\n",
    "reports that have been generated, either for an entire script or just for a specific Problem \n",
    "instance.  To view only reports for a specific problem, use\n",
    "\n",
    "```\n",
    "openmdao view_reports probname\n",
    "```\n",
    "\n",
    "where `probname` is the name of the problem of interest."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Listing available reports\n",
    "\n",
    "As mentioned before, the user can list available reports using the `openmdao list_reports` command.\n",
    "\n",
    "It can also be done programmatically, using the  `list_reports` function.\n",
    "\n",
    "```{eval-rst}\n",
    "    .. autofunction:: openmdao.utils.reports_system.list_reports\n",
    "        :noindex:\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "om.list_reports()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "os.environ['OPENMDAO_REPORTS'] = '0'"
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Tags",
  "kernelspec": {
   "display_name": "py311",
   "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.11.4"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
