exploration.main

  • Authors: Peter Mawhorter
  • Consulted:
  • Date: 2022-10-15
  • Purpose: Main API entry points to support the __main__.py script.
   1"""
   2- Authors: Peter Mawhorter
   3- Consulted:
   4- Date: 2022-10-15
   5- Purpose: Main API entry points to support the `__main__.py` script.
   6"""
   7
   8from __future__ import annotations
   9
  10import argparse
  11import pathlib
  12import textwrap
  13import sys
  14import csv
  15import json
  16import time
  17
  18# Resource module not available in Pyodide
  19try:
  20    import resource
  21except Exception:
  22    resource = None  # type: ignore
  23
  24import networkx as nx  # type: ignore
  25
  26from typing import (
  27    Literal, Optional, Union, get_args, TypeAlias, List, Callable, Dict,
  28    Sequence, Any, cast, Tuple, Set, Collection
  29)
  30
  31from . import journal
  32from . import core
  33from . import base
  34from . import analysis
  35from . import parsing
  36
  37
  38#------------#
  39# File input #
  40#------------#
  41
  42SourceType: TypeAlias = Literal[
  43    "graph",
  44    "dot",
  45    "exploration",
  46    "journal",
  47]
  48"""
  49The file types we recognize.
  50"""
  51
  52
  53def determineFileType(filename: str) -> SourceType:
  54    if filename.endswith('.dcg'):
  55        return 'graph'
  56    elif filename.endswith('.dot'):
  57        return 'dot'
  58    elif filename.endswith('.exp'):
  59        return 'exploration'
  60    elif filename.endswith('.exj'):
  61        return 'journal'
  62    else:
  63        raise ValueError(
  64            f"Could not determine the file type of file '{filename}':"
  65            f" it does not end with '.dcg', '.dot', '.exp', or '.exj'."
  66        )
  67
  68
  69def loadDecisionGraph(path: pathlib.Path) -> core.DecisionGraph:
  70    """
  71    Loads a JSON-encoded decision graph from a file. The extension
  72    should normally be '.dcg'.
  73    """
  74    with path.open('r', encoding='utf-8-sig') as fInput:
  75        return parsing.loadCustom(fInput, core.DecisionGraph)
  76
  77
  78def saveDecisionGraph(
  79    path: pathlib.Path,
  80    graph: core.DecisionGraph
  81) -> None:
  82    """
  83    Saves a decision graph encoded as JSON in the specified file. The
  84    file should normally have a '.dcg' extension.
  85    """
  86    with path.open('w', encoding='utf-8') as fOutput:
  87        parsing.saveCustom(graph, fOutput)
  88
  89
  90def loadDotFile(path: pathlib.Path) -> core.DecisionGraph:
  91    """
  92    Loads a `core.DecisionGraph` form the file at the specified path
  93    (whose extension should normally be '.dot'). The file format is the
  94    GraphViz "dot" format.
  95    """
  96    with path.open('r', encoding='utf-8-sig') as fInput:
  97        dot = fInput.read()
  98        try:
  99            return parsing.parseDot(dot)
 100        except parsing.DotParseError:
 101            raise parsing.DotParseError(
 102                "Failed to parse Dot file contents:\n\n"
 103              + dot
 104              + "\n\n(See error above for specific parsing issue.)"
 105            )
 106
 107
 108def saveDotFile(path: pathlib.Path, graph: core.DecisionGraph) -> None:
 109    """
 110    Saves a `core.DecisionGraph` as a GraphViz "dot" file. The file
 111    extension should normally be ".dot".
 112    """
 113    dotStr = parsing.toDot(graph, clusterLevels=[])
 114    with path.open('w', encoding='utf-8') as fOutput:
 115        fOutput.write(dotStr)
 116
 117
 118def loadExploration(path: pathlib.Path) -> core.DiscreteExploration:
 119    """
 120    Loads a JSON-encoded `core.DiscreteExploration` object from the file
 121    at the specified path. The extension should normally be '.exp'.
 122    """
 123    with path.open('r', encoding='utf-8-sig') as fInput:
 124        return parsing.loadCustom(fInput, core.DiscreteExploration)
 125
 126
 127def saveExploration(
 128    path: pathlib.Path,
 129    exploration: core.DiscreteExploration
 130) -> None:
 131    """
 132    Saves a `core.DiscreteExploration` object as JSON in the specified
 133    file. The file extension should normally be '.exp'.
 134    """
 135    with path.open('w', encoding='utf-8') as fOutput:
 136        parsing.saveCustom(exploration, fOutput)
 137
 138
 139def loadJournal(path: pathlib.Path) -> core.DiscreteExploration:
 140    """
 141    Loads a `core.DiscreteExploration` object from a journal file
 142    (extension should normally be '.exj'). Uses the
 143    `journal.convertJournal` function.
 144    """
 145    with path.open('r', encoding='utf-8-sig') as fInput:
 146        return journal.convertJournal(fInput.read())
 147
 148
 149def saveAsJournal(
 150    path: pathlib.Path,
 151    exploration: core.DiscreteExploration
 152) -> None:
 153    """
 154    Saves a `core.DiscreteExploration` object as a text journal in the
 155    specified file. The file extension should normally be '.exj'.
 156
 157    TODO: This?!
 158    """
 159    raise NotImplementedError(
 160        "DiscreteExploration-to-journal conversion is not implemented"
 161        " yet."
 162    )
 163
 164
 165def loadSource(
 166    path: pathlib.Path,
 167    formatOverride: Optional[SourceType] = None
 168) -> Union[core.DecisionGraph, core.DiscreteExploration]:
 169    """
 170    Loads either a `core.DecisionGraph` or a `core.DiscreteExploration`
 171    from the specified file, depending on its file extension (or the
 172    specified format given as `formatOverride` if there is one).
 173    """
 174    if formatOverride is not None:
 175        format = formatOverride
 176    else:
 177        format = determineFileType(str(path))
 178
 179    if format == "graph":
 180        return loadDecisionGraph(path)
 181    if format == "dot":
 182        return loadDotFile(path)
 183    elif format == "exploration":
 184        return loadExploration(path)
 185    elif format == "journal":
 186        return loadJournal(path)
 187    else:
 188        raise ValueError(
 189            f"Unrecognized file format '{format}' (recognized formats"
 190            f" are 'graph', 'exploration', and 'journal')."
 191        )
 192
 193
 194#---------------------#
 195# Analysis tool lists #
 196#---------------------#
 197
 198CSVEmbeddable: TypeAlias = Union[None, bool, str, int, float, complex]
 199"""
 200A type alias for values we're willing to store in a CSV file without
 201coercing them to a string.
 202"""
 203
 204
 205def coerceToCSVValue(result: Any) -> CSVEmbeddable:
 206    """
 207    Coerces any value to one that's embeddable in a CSV file. The
 208    `CSVEmbeddable` types are unchanged, but all other types are
 209    converted to strings via `json.dumps` if possible or `repr` if not.
 210    """
 211    if isinstance(result, get_args(CSVEmbeddable)):
 212        return result
 213    else:
 214        try:
 215            return json.dumps(result)
 216        except Exception:
 217            return repr(result)
 218
 219
 220#---------------#
 221# API Functions #
 222#---------------#
 223
 224def show(
 225    source: pathlib.Path,
 226    formatOverride: Optional[SourceType] = None,
 227    step: int = -1
 228) -> None:
 229    """
 230    Shows the graph or exploration stored in the `source` file. You will
 231    need to have the `matplotlib` library installed. Consider using the
 232    interactive interface provided by the `explorationViewer` module
 233    instead. The file extension is used to determine how to load the data,
 234    although the `--format` option may override this. '.dcg' files are
 235    assumed to be decision graphs in JSON format, '.exp' files are assumed
 236    to be exploration objects in JSON format, and '.exj' files are assumed
 237    to be exploration journals in the default journal format. If the object
 238    that gets loaded is an exploration, the final graph for that
 239    exploration will be displayed, or a specific graph may be selected
 240    using `--step`.
 241    """
 242    obj = loadSource(source, formatOverride)
 243    if isinstance(obj, core.DiscreteExploration):
 244        obj = obj.getSituation(step).graph
 245
 246    import matplotlib.pyplot # type: ignore
 247
 248    # This draws the graph in a new window that pops up. You have to close
 249    # the window to end the program.
 250    nx.draw(obj)
 251    matplotlib.pyplot.show()
 252
 253
 254def transitionStr(
 255    exploration: core.DiscreteExploration,
 256    src: base.DecisionID,
 257    transition: base.Transition,
 258    dst: base.DecisionID
 259) -> str:
 260    """
 261    Given an exploration object, returns a string identifying a
 262    transition, incorporating the final identity strings for the source
 263    and destination.
 264    """
 265    srcId = analysis.finalIdentity(exploration, src)
 266    dstId = analysis.finalIdentity(exploration, dst)
 267    return f"{srcId} → {transition} → {dstId}"
 268
 269
 270def printPerf(analyzerName: str) -> None:
 271    """
 272    Prints performance for the given analyzer to stderr.
 273    """
 274    perf = analysis.ANALYSIS_TIME_SPENT.get(analyzerName)
 275    if perf is None:
 276        raise RuntimeError(
 277            f"Missing analysis perf for {analyzerName!r}."
 278        )
 279    unit = analysis.ALL_ANALYZERS[analyzerName]._unit
 280    call, noC, tc, tw = perf.values()
 281    print(
 282        f"{analyzerName} ({unit}): {call} / {noC} / {tc:.6f} / {tw:.6f}",
 283        file=sys.stderr
 284    )
 285
 286
 287def printMem() -> None:
 288    """
 289    Prints (to stderr) a message about how much memory Python is
 290    currently using overall.
 291    """
 292    if resource is not None:
 293        used = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
 294        print(f"Using {used} memory (bytes or kilobytes, depending on OS)")
 295    else:
 296        print(
 297            f"Can't get memory usage because the resource module is not"
 298            f" available."
 299        )
 300    # TODO: This is apparently kilobytes on linux but bytes on mac?
 301
 302
 303def analyze(
 304    source: pathlib.Path,
 305    destination: Optional[pathlib.Path] = None,
 306    formatOverride: Optional[SourceType] = None,
 307    applyTools: Optional[Collection[str]] = None,
 308    finalOnly: Optional[Collection[str]] = None,
 309    includeAll: bool = False,
 310    profile: bool = False
 311) -> None:
 312    """
 313    Analyzes the exploration stored in the `source` file. The file
 314    extension is used to determine how to load the data, although this
 315    may be overridden by the `--format` option. Normally, '.exp' files
 316    are treated as JSON-encoded exploration objects, while '.exj' files
 317    are treated as journals using the default journal format.
 318
 319    This applies a number of analysis functions to produce a CSV file
 320    showing per-decision-per-step, per-decision, per-step, and
 321    per-exploration metrics. A subset of the available metrics may be
 322    selected by passing a list of strings for the `applyTools` argument.
 323    These strings should be the names of functions in `analysis.py` that
 324    are decorated with `analysis.analyze`. By default, only those not
 325    marked with `analysis.elide` will be included. You can set
 326    `includeAll` to `True` to include all tools, although this is ignored
 327    when `applyTools` is not `None`. `finalOnly` specifies one or more
 328    tools to only run on the final step of the exploration rather than
 329    every step. This only applies to tools whose unit of analysis is
 330    'step', 'stepDecision', or 'stepTransition'. By default those marked
 331    as `finalOnly` in `analysis.py` will be run this way. Tools excluded
 332    via `applyTools` or by default when `includeAll` is false won't be
 333    run even if specified in `finalOnly`. Set `finalOnly` to `False` to
 334    run all selected tools on all steps without having to explicitly
 335    list the tools that would otherwise be restricted by default.
 336
 337    Set `profile` to `True` to gather and report analysis time spent
 338    results (they'll be printed to stdout).
 339
 340    If no output file is specified, the output will be printed out.
 341    """
 342    if profile:
 343        print("Starting analysis with profiling...", file=sys.stderr)
 344        parseStart = time.perf_counter()
 345        printMem()
 346    # Load our source exploration object:
 347    obj = loadSource(source, formatOverride)
 348    if isinstance(obj, core.DecisionGraph):
 349        obj = core.DiscreteExploration.fromGraph(obj)
 350    if profile:
 351        elapsed = time.perf_counter() - parseStart
 352        print(f"Parsed input in {elapsed:.6f}s...", file=sys.stderr)
 353        printMem()
 354
 355    exploration: core.DiscreteExploration = obj
 356
 357    # Set up for profiling
 358    if profile:
 359        analysis.RECORD_PROFILE = True
 360    else:
 361        analysis.RECORD_PROFILE = False
 362
 363    # Figure out which to apply
 364    if applyTools is not None:
 365        toApply: Set[str] = set(applyTools)
 366    else:
 367        toApply = set(analysis.ALL_ANALYZERS.keys())
 368        if not includeAll:
 369            print("ELIDING:", analysis.ELIDE, file=sys.stderr)
 370            toApply -= analysis.ELIDE
 371
 372    if finalOnly is False:
 373        finalOnly = set()
 374    elif finalOnly is None:
 375        finalOnly = analysis.FINAL_ONLY
 376
 377    # Group analyzers by unit
 378    byUnit = analysis.analyzersByUnit(toApply)
 379
 380    # Apply all of the analysis functions (or only just those that are
 381    # selected using applyTools):
 382
 383    wholeRows: List[List[CSVEmbeddable]] = [['Whole exploration metrics:']]
 384    if profile:
 385        print(
 386            "name (unit): calls / non-cached / time (lookups) / time (work)",
 387            file=sys.stderr
 388        )
 389    # One row per analyzer
 390    for ea in byUnit["exploration"]:
 391        wholeRows.append([ea.__name__, coerceToCSVValue(ea(exploration))])
 392        if profile:
 393            printPerf(ea.__name__)
 394
 395    # A few variables for holding pieces we'll assemble
 396    row: List[CSVEmbeddable]
 397    columns: List[CSVEmbeddable]
 398
 399    decisionRows: List[Sequence[CSVEmbeddable]] = [
 400        ['Per-decision metrics:']
 401    ]
 402    # One row per tool; one column per decision
 403    decisionList: List[base.DecisionID] = exploration.allDecisions()
 404    columns = (
 405        cast(List[CSVEmbeddable], ['Metric ↓/Decision →'])
 406      + cast(List[CSVEmbeddable], decisionList)
 407    )
 408
 409    decisionRows.append(columns)
 410    for da in byUnit["decision"]:
 411        row = [da.__name__]
 412        decisionRows.append(row)
 413        for decision in decisionList:
 414            row.append(coerceToCSVValue(da(exploration, decision)))
 415        if profile:
 416            printPerf(da.__name__)
 417
 418    transitionRows: List[Sequence[CSVEmbeddable]] = [
 419        ['Per-transition metrics:']
 420    ]
 421    # One row per tool; one column per decision
 422    transitionList: List[
 423        Tuple[base.DecisionID, base.Transition, base.DecisionID]
 424    ] = exploration.allTransitions()
 425    transitionStrings: List[CSVEmbeddable] = [
 426        transitionStr(exploration, *trans)
 427        for trans in transitionList
 428    ]
 429    columns = (
 430        cast(List[CSVEmbeddable], ['Metric ↓/Transition →'])
 431      + transitionStrings
 432    )
 433    transitionRows.append(columns)
 434    for ta in byUnit["transition"]:
 435        row = [ta.__name__]
 436        transitionRows.append(row)
 437        for transition in transitionList:
 438            row.append(
 439                coerceToCSVValue(ta(exploration, *transition))
 440            )
 441        if profile:
 442            printPerf(ta.__name__)
 443
 444    stepRows: List[Sequence[CSVEmbeddable]] = [
 445        ['Per-step metrics:']
 446    ]
 447    # One row per exploration step; one column per tool
 448    columns = ['Step ↓/Metric →']
 449    stepRows.append(columns)
 450    for step in range(len(exploration)):
 451        row = [step]
 452        stepRows.append(row)
 453        for sa in byUnit["step"]:
 454            if step == 0:
 455                columns.append(sa.__name__)
 456            if sa.__name__ in finalOnly and step != len(exploration) - 1:
 457                row.append("")
 458            else:
 459                row.append(coerceToCSVValue(sa(exploration, step)))
 460
 461    # Print profile results just once after all steps have been analyzed
 462    if profile:
 463        for sa in byUnit["step"]:
 464            printPerf(sa.__name__)
 465
 466    stepwiseRows: List[Sequence[CSVEmbeddable]] = [
 467        ['Per-decision-per-step metrics (one table per metric):']
 468    ]
 469
 470    # For each per-step decision tool; one row per exploration step and
 471    # one column per decision
 472    columns = (
 473        cast(List[CSVEmbeddable], ['Step ↓/Decision →'])
 474      + cast(List[CSVEmbeddable], decisionList)
 475    )
 476    identities = ['Decision names:'] + [
 477        analysis.finalIdentity(exploration, d)
 478        for d in decisionList
 479    ]
 480    for sda in byUnit["stepDecision"]:
 481        stepwiseRows.append([sda.__name__])
 482        stepwiseRows.append(columns)
 483        stepwiseRows.append(identities)
 484        if sda.__name__ in finalOnly:
 485            step = len(exploration) - 1
 486            row = [step]
 487            stepwiseRows.append(row)
 488            for decision in decisionList:
 489                row.append(coerceToCSVValue(sda(exploration, step, decision)))
 490        else:
 491            for step in range(len(exploration)):
 492                row = [step]
 493                stepwiseRows.append(row)
 494                for decision in decisionList:
 495                    row.append(
 496                        coerceToCSVValue(sda(exploration, step, decision))
 497                    )
 498        if profile:
 499            printPerf(sda.__name__)
 500
 501    stepwiseTransitionRows: List[Sequence[CSVEmbeddable]] = [
 502        ['Per-transition-per-step metrics (one table per metric):']
 503    ]
 504
 505    # For each per-step transition tool; one row per exploration step and
 506    # one column per transition
 507    columns = (
 508        cast(List[CSVEmbeddable], ['Step ↓/Transition →'])
 509      + cast(List[CSVEmbeddable], transitionStrings)
 510    )
 511    for sta in byUnit["stepTransition"]:
 512        stepwiseTransitionRows.append([sta.__name__])
 513        stepwiseTransitionRows.append(columns)
 514        if sta.__name__ in finalOnly:
 515            step = len(exploration) - 1
 516            row = [step]
 517            stepwiseTransitionRows.append(row)
 518            for (src, trans, dst) in transitionList:
 519                row.append(
 520                    coerceToCSVValue(sta(exploration, step, src, trans, dst))
 521                )
 522        else:
 523            for step in range(len(exploration)):
 524                row = [step]
 525                stepwiseTransitionRows.append(row)
 526                for (src, trans, dst) in transitionList:
 527                    row.append(
 528                        coerceToCSVValue(
 529                            sta(exploration, step, src, trans, dst)
 530                        )
 531                    )
 532        if profile:
 533            printPerf(sta.__name__)
 534
 535    # Build a grid containing just the non-empty analysis categories, so
 536    # that if you deselect some tools you get a smaller CSV file:
 537    grid: List[Sequence[CSVEmbeddable]] = []
 538    if len(wholeRows) > 1:
 539        grid.extend(wholeRows)
 540    for block in (
 541        decisionRows,
 542        transitionRows,
 543        stepRows,
 544        stepwiseRows,
 545        stepwiseTransitionRows
 546    ):
 547        if len(block) > 1:
 548            if grid:
 549                grid.append([])  # spacer
 550            grid.extend(block)
 551
 552    # Print all profile results at the end
 553    if profile:
 554        print("-"*80, file=sys.stderr)
 555        print("Done with analysis. Time taken:", file=sys.stderr)
 556        print("-"*80, file=sys.stderr)
 557        for aname in analysis.ANALYSIS_TIME_SPENT:
 558            printPerf(aname)
 559        print("-"*80, file=sys.stderr)
 560        printMem()
 561
 562    # Figure out our destination stream:
 563    if destination is None:
 564        outStream = sys.stdout
 565        closeIt = False
 566    else:
 567        outStream = open(destination, 'w')
 568        closeIt = True
 569
 570    # Create a CSV writer for our stream
 571    writer = csv.writer(outStream)
 572
 573    # Write out our grid to the file
 574    try:
 575        writer.writerows(grid)
 576    finally:
 577        if closeIt:
 578            outStream.close()
 579
 580
 581def convert(
 582    source: pathlib.Path,
 583    destination: pathlib.Path,
 584    inputFormatOverride: Optional[SourceType] = None,
 585    outputFormatOverride: Optional[SourceType] = None,
 586    step: int = -1
 587) -> None:
 588    """
 589    Converts between exploration and graph formats. By default, formats
 590    are determined by file extensions, but using the `--format` and
 591    `--output-format` options can override this. The available formats
 592    are:
 593
 594    - '.dcg' A `core.DecisionGraph` stored in JSON format.
 595    - '.dot' A `core.DecisionGraph` stored as a GraphViz DOT file.
 596    - '.exp' A `core.DiscreteExploration` stored in JSON format.
 597    - '.exj' A `core.DiscreteExploration` stored as a journal (see
 598        `journal.JournalObserver`; TODO: writing this format).
 599
 600    When converting a decision graph into an exploration format, the
 601    resulting exploration will have a single starting step containing
 602    the entire specified graph. When converting an exploration into a
 603    decision graph format, only the current graph will be saved, unless
 604    `--step` is used to specify a different step index to save.
 605    """
 606    # TODO journal writing
 607    obj = loadSource(source, inputFormatOverride)
 608
 609    if outputFormatOverride is None:
 610        outputFormat = determineFileType(str(destination))
 611    else:
 612        outputFormat = outputFormatOverride
 613
 614    if outputFormat in ("graph", "dot"):
 615        if isinstance(obj, core.DiscreteExploration):
 616            graph = obj.getSituation(step).graph
 617        else:
 618            graph = obj
 619        if outputFormat == "graph":
 620            saveDecisionGraph(destination, graph)
 621        else:
 622            saveDotFile(destination, graph)
 623    else:
 624        if isinstance(obj, core.DecisionGraph):
 625            exploration = core.DiscreteExploration.fromGraph(obj)
 626        else:
 627            exploration = obj
 628        if outputFormat == "exploration":
 629            saveExploration(destination, exploration)
 630        else:
 631            saveAsJournal(destination, exploration)
 632
 633
 634INSPECTOR_HELP = """
 635Available commands:
 636
 637- 'help' or '?': List commands.
 638- 'done', 'quit', 'q', or 'exit': Quit the inspector.
 639- 'f' or 'follow': Follow the primary decision when changing steps. Also
 640    changes to that decision immediately. Toggles off if on.
 641- 'cd' or 'goto': Change focus decision to the named decision. Cancels
 642    follow mode.
 643- 'ls' or 'list' or 'destinations': Lists transitions at this decision
 644    and their destinations, as well as any mechanisms at this decision.
 645- 'lst' or 'steps': Lists each step of the exploration along with the
 646    primary decision at each step.
 647- 'st' or 'step': Switches to the specified step (an index)
 648- 'n' or 'next': Switches to the next step.
 649- 'p' or 'prev' or 'previous': Switches to the previous step.
 650- 't' or 'take': Change focus decision to the decision which is the
 651    destination of the specified transition at the current focused
 652    decision.
 653- 'prm' or 'primary': Displays the current primary decision.
 654- 'a' or 'active': Lists all currently active decisions
 655- 'u' or 'unexplored': Lists all unexplored transitions at the current
 656    step.
 657- 'x' or 'explorable': Lists all unexplored transitions at the current
 658    step which are traversable based on the current state. (TODO:
 659    make this more accurate).
 660- 'r' or 'reachable': TODO
 661- 'A' or 'all': Lists all decisions at the current step.
 662- 'M' or 'mechanisms': Lists all mechanisms at the current step.
 663"""
 664
 665
 666def inspect(
 667    source: pathlib.Path,
 668    formatOverride: Optional[SourceType] = None
 669) -> None:
 670    """
 671    Inspects the graph or exploration stored in the `source` file,
 672    launching an interactive command line for inspecting properties of
 673    decisions, transitions, and situations. The file extension is used
 674    to determine how to load the data, although the `--format` option
 675    may override this. '.dcg' files are assumed to be decision graphs in
 676    JSON format, '.exp' files are assumed to be exploration objects in
 677    JSON format, and '.exj' files are assumed to be exploration journals
 678    in the default journal format. If the object that gets loaded is a
 679    graph, a 1-step exploration containing just that graph will be
 680    created to inspect. Inspector commands are listed in the
 681    `INSPECTOR_HELP` variable.
 682    """
 683    print(f"Loading exploration from {source!r}...")
 684    # Load our exploration
 685    exploration = loadSource(source, formatOverride)
 686    if isinstance(exploration, core.DecisionGraph):
 687        exploration = core.DiscreteExploration.fromGraph(exploration)
 688
 689    print(
 690        f"Inspecting exploration with {len(exploration)} step(s) and"
 691        f" {len(exploration.allDecisions())} decision(s):"
 692    )
 693    print("('h' for help)")
 694
 695    # Set up tracking variables:
 696    step = len(exploration) - 1
 697    here: Optional[base.DecisionID] = exploration.primaryDecision(step)
 698    graph = exploration.getSituation(step).graph
 699    follow = True
 700
 701    pf = parsing.ParseFormat()
 702
 703    if here is None:
 704        print("Note: There are no decisions in the final graph.")
 705
 706    while True:
 707        # Re-establish the prompt
 708        prompt = "> "
 709        if here is not None and here in graph:
 710            prompt = graph.identityOf(here) + "> "
 711        elif here is not None:
 712            prompt = f"{here} (?)> "
 713
 714        # Prompt for the next command
 715        fullCommand = input(prompt).split()
 716
 717        # Track number of invalid commands so we can quit after 10 in a row
 718        invalidCommands = 0
 719
 720        if len(fullCommand) == 0:
 721            cmd = ''
 722            args = ''
 723        else:
 724            cmd = fullCommand[0]
 725            args = ' '.join(fullCommand[1:])
 726
 727        # Do what the command says
 728        invalid = False
 729        if cmd in ("help", '?'):
 730            # Displays help message
 731            if len(args.strip()) > 0:
 732                print("(help does not accept any arguments)")
 733            print(INSPECTOR_HELP)
 734        elif cmd in ("done", "exit", "quit", "q"):
 735            # Exits the inspector
 736            if len(args.strip()) > 0:
 737                print("(quit does not accept any arguments)")
 738            print("Bye.")
 739            break
 740        elif cmd in ("f", "follow"):
 741            if follow:
 742                follow = False
 743                print("Stopped following")
 744            else:
 745                follow = True
 746                here = exploration.primaryDecision(step)
 747                print(f"Now following at: {graph.identityOf(here)}")
 748        elif cmd in ("cd", "goto"):
 749            # Changes focus to a specific decision
 750            try:
 751                target = pf.parseDecisionSpecifier(args)
 752                target = graph.resolveDecision(target)
 753                here = target
 754                follow = False
 755                print(f"now at: {graph.identityOf(target)}")
 756            except Exception:
 757                print("(invalid decision specifier)")
 758        elif cmd in ("ls", "list", "destinations"):
 759            fromID: Optional[base.AnyDecisionSpecifier] = None
 760            if args.strip():
 761                fromID = pf.parseDecisionSpecifier(args)
 762                fromID = graph.resolveDecision(fromID)
 763            else:
 764                fromID = here
 765
 766            if fromID is None:
 767                print(
 768                    "(no focus decision and no decision specified;"
 769                    " nothing to list; use 'cd' to specify a decision,"
 770                    " or 'all' to list all decisions)"
 771                )
 772            else:
 773                outgoing = graph.destinationsFrom(fromID)
 774                info = graph.identityOf(fromID)
 775                if len(outgoing) > 0:
 776                    print(f"Destinations from {info}:")
 777                    print(graph.destinationsListing(outgoing))
 778                else:
 779                    print("No outgoing transitions from {info}.")
 780        elif cmd in ("lst", "steps"):
 781            total = len(exploration)
 782            print(f"{total} step(s):")
 783            for step in range(total):
 784                pr = exploration.primaryDecision(step)
 785                situ = exploration.getSituation(step)
 786                stGraph = situ.graph
 787                identity = stGraph.identityOf(pr)
 788                print(f"  {step} at {identity}")
 789            print(f"({total} total step(s))")
 790        elif cmd in ("st", "step"):
 791            stepTo = int(args.strip())
 792            if stepTo < 0:
 793                stepTo += len(exploration)
 794            if stepTo < 0:
 795                print(
 796                    f"Invalid step {args!r} (too negative; min is"
 797                    f" {-len(exploration)})"
 798                )
 799            if stepTo >= len(exploration):
 800                print(
 801                    f"Invalid step {args!r} (too large; max is"
 802                    f" {len(exploration) - 1})"
 803                )
 804
 805            step = stepTo
 806            graph = exploration.getSituation(step).graph
 807            if follow:
 808                here = exploration.primaryDecision(step)
 809                print(f"Followed to: {graph.identityOf(here)}")
 810        elif cmd in ("n", "next"):
 811            if step == -1 or step >= len(exploration) - 2:
 812                print("Can't step beyond the last step.")
 813            else:
 814                step += 1
 815                graph = exploration.getSituation(step).graph
 816                if here not in graph:
 817                    here = None
 818            print(f"At step {step}")
 819            if follow:
 820                here = exploration.primaryDecision(step)
 821                print(f"Followed to: {graph.identityOf(here)}")
 822        elif cmd in ("p", "prev"):
 823            if step == 0 or step <= -len(exploration) + 2:
 824                print("Can't step before the first step.")
 825            else:
 826                step -= 1
 827                graph = exploration.getSituation(step).graph
 828                if here not in graph:
 829                    here = None
 830            print(f"At step {step}")
 831            if follow:
 832                here = exploration.primaryDecision(step)
 833                print(f"Followed to: {graph.identityOf(here)}")
 834        elif cmd in ("t", "take"):
 835            if here is None:
 836                print(
 837                    "(no focus decision, so can't take transitions. Use"
 838                    " 'cd' to specify a decision first.)"
 839                )
 840            else:
 841                dest = graph.getDestination(here, args)
 842                if dest is None:
 843                    print(
 844                        f"Invalid transition {args!r} (no destination for"
 845                        f" that transition from {graph.identityOf(here)}"
 846                    )
 847                here = dest
 848        elif cmd in ("prm", "primary"):
 849            pr = exploration.primaryDecision(step)
 850            if pr is None:
 851                print(f"Step {step} has no primary decision")
 852            else:
 853                print(
 854                    f"Primary decision for step {step} is:"
 855                    f" {graph.identityOf(pr)}"
 856                )
 857        elif cmd in ("a", "active"):
 858            active = exploration.getActiveDecisions(step)
 859            print(f"Active decisions at step {step}:")
 860            print(graph.namesListing(active))
 861        elif cmd in ("u", "unexplored"):
 862            unx = analysis.unexploredBranches(graph)
 863            fin = ':' if len(unx) > 0 else '.'
 864            print(f"{len(unx)} unexplored branch(es){fin}")
 865            for frID, unTr in unx:
 866                print(f"take {unTr} at {graph.identityOf(frID)}")
 867        elif cmd in ("x", "explorable"):
 868            ctx = base.genericContextForSituation(
 869                exploration.getSituation(step)
 870            )
 871            unx = analysis.unexploredBranches(graph, ctx)
 872            fin = ':' if len(unx) > 0 else '.'
 873            print(f"{len(unx)} unexplored branch(es){fin}")
 874            for frID, unTr in unx:
 875                print(f"take {unTr} at {graph.identityOf(frID)}")
 876        elif cmd in ("r", "reachable"):
 877            print("TODO: Reachable does not work yet.")
 878        elif cmd in ("A", "all"):
 879            print(
 880                f"There are {len(graph)} decision(s) at step {step}:"
 881            )
 882            for decision in graph.nodes():
 883                print(f"  {graph.identityOf(decision)}")
 884        elif cmd in ("M", "mechanisms"):
 885            count = len(graph.mechanisms)
 886            fin = ':' if count > 0 else '.'
 887            print(
 888                f"There are {count} mechanism(s) at step {step}{fin}"
 889            )
 890            for mID in graph.mechanisms:
 891                where, name = graph.mechanisms[mID]
 892                state = exploration.mechanismState(mID, step=step)
 893                if where is None:
 894                    print(f"  {name!r} (global) in state {state!r}")
 895                else:
 896                    info = graph.identityOf(where)
 897                    print(f"  {name!r} at {info} in state {state!r}")
 898        else:
 899            invalid = True
 900
 901        if invalid:
 902            if invalidCommands >= 10:
 903                print("Too many invalid commands; exiting.")
 904                break
 905            else:
 906                if invalidCommands >= 8:
 907                    print("{invalidCommands} invalid commands so far,")
 908                    print("inspector will stop after 10 invalid commands...")
 909                print(f"Unknown command {cmd!r}...")
 910                invalidCommands += 1
 911                print(INSPECTOR_HELP)
 912        else:
 913            invalidCommands = 0
 914
 915
 916#--------------#
 917# Parser setup #
 918#--------------#
 919
 920parser = argparse.ArgumentParser(
 921    prog="python -m exploration",
 922    description="""\
 923Runs various commands for processing exploration graphs and journals,
 924and for converting between them or displaying them in various formats.
 925"""
 926)
 927subparsers = parser.add_subparsers(
 928    title="commands",
 929    description="The available commands are:",
 930    help="use these with -h/--help for more details"
 931)
 932
 933showParser = subparsers.add_parser(
 934    'show',
 935    help="show an exploration",
 936    description=textwrap.dedent(str(show.__doc__)).strip()
 937)
 938showParser.set_defaults(run="show")
 939showParser.add_argument(
 940    "source",
 941    type=pathlib.Path,
 942    help="The file to load"
 943)
 944showParser.add_argument(
 945    '-f',
 946    "--format",
 947    choices=get_args(SourceType),
 948    help=(
 949        "Which format the source file is in (normally that can be"
 950        " determined from the file extension)."
 951    )
 952)
 953showParser.add_argument(
 954    '-s',
 955    "--step",
 956    type=int,
 957    default=-1,
 958    help="Which graph step to show (when loading an exploration)."
 959)
 960
 961analyzeParser = subparsers.add_parser(
 962    'analyze',
 963    help="analyze an exploration",
 964    description=textwrap.dedent(str(analyze.__doc__)).strip()
 965)
 966analyzeParser.set_defaults(run="analyze")
 967analyzeParser.add_argument(
 968    "source",
 969    type=pathlib.Path,
 970    help="The file holding the exploration to analyze"
 971)
 972analyzeParser.add_argument(
 973    "destination",
 974    default=None,
 975    type=pathlib.Path,
 976    help=(
 977        "The file name where the output should be written (this file"
 978        " will be overwritten without warning)."
 979    )
 980)
 981analyzeParser.add_argument(
 982    '-f',
 983    "--format",
 984    choices=get_args(SourceType),
 985    help=(
 986        "Which format the source file is in (normally that can be"
 987        " determined from the file extension)."
 988    )
 989)
 990analyzeParser.add_argument(
 991    '-a',
 992    "--all",
 993    action='store_true',
 994    help=(
 995        "Whether to include all results or just the default ones. Some"
 996        " of the extended results may cause issues with loading the CSV"
 997        " file in common programs like Excel."
 998    )
 999)
1000analyzeParser.add_argument(
1001    '-p',
1002    "--profile",
1003    action='store_true',
1004    help="Set this to profile time taken by analysis functions."
1005)
1006
1007convertParser = subparsers.add_parser(
1008    'convert',
1009    help="convert an exploration",
1010    description=textwrap.dedent(str(convert.__doc__)).strip()
1011)
1012convertParser.set_defaults(run="convert")
1013convertParser.add_argument(
1014    "source",
1015    type=pathlib.Path,
1016    help="The file holding the graph or exploration to convert."
1017)
1018convertParser.add_argument(
1019    "destination",
1020    type=pathlib.Path,
1021    help=(
1022        "The file name where the output should be written (this file"
1023        " will be overwritten without warning)."
1024    )
1025)
1026convertParser.add_argument(
1027    '-f',
1028    "--format",
1029    choices=get_args(SourceType),
1030    help=(
1031        "Which format the source file is in (normally that can be"
1032        " determined from the file extension)."
1033    )
1034)
1035convertParser.add_argument(
1036    '-o',
1037    "--output-format",
1038    choices=get_args(SourceType),
1039    help=(
1040        "Which format the converted file should be saved as (normally"
1041        " that is determined from the file extension)."
1042    )
1043)
1044convertParser.add_argument(
1045    '-s',
1046    "--step",
1047    type=int,
1048    default=-1,
1049    help=(
1050        "Which graph step to save (when converting from an exploration"
1051        " format to a graph format)."
1052    )
1053)
1054
1055inspectParser = subparsers.add_parser(
1056    'inspect',
1057    help="interactively inspect an exploration",
1058    description=textwrap.dedent(str(inspect.__doc__)).strip()
1059)
1060inspectParser.set_defaults(run="inspect")
1061inspectParser.add_argument(
1062    "source",
1063    type=pathlib.Path,
1064    help="The file holding the graph or exploration to inspect."
1065)
1066inspectParser.add_argument(
1067    '-f',
1068    "--format",
1069    choices=get_args(SourceType),
1070    help=(
1071        "Which format the source file is in (normally that can be"
1072        " determined from the file extension)."
1073    )
1074)
1075
1076def main():
1077    """
1078    Parse options from command line & run appropriate tool.
1079    """
1080    options = parser.parse_args()
1081    if not hasattr(options, "run"):
1082        print("No sub-command specified.")
1083        parser.print_help()
1084        exit(1)
1085    elif options.run == "show":
1086        show(
1087            options.source,
1088            formatOverride=options.format,
1089            step=options.step
1090        )
1091    elif options.run == "analyze":
1092        analyze(
1093            options.source,
1094            destination=options.destination,
1095            formatOverride=options.format,
1096            includeAll=options.all,
1097            profile=options.profile
1098        )
1099    elif options.run == "convert":
1100        convert(
1101            options.source,
1102            options.destination,
1103            inputFormatOverride=options.format,
1104            outputFormatOverride=options.output_format,
1105            step=options.step
1106        )
1107    elif options.run == "inspect":
1108        inspect(
1109            options.source,
1110            formatOverride=options.format
1111        )
1112    else:
1113        raise RuntimeError(
1114            f"Invalid 'run' default value: '{options.run}'."
1115        )
1116
1117
1118if __name__ == "__main__":
1119    main()
SourceType: TypeAlias = Literal['graph', 'dot', 'exploration', 'journal']

The file types we recognize.

def determineFileType(filename: str) -> Literal['graph', 'dot', 'exploration', 'journal']:
54def determineFileType(filename: str) -> SourceType:
55    if filename.endswith('.dcg'):
56        return 'graph'
57    elif filename.endswith('.dot'):
58        return 'dot'
59    elif filename.endswith('.exp'):
60        return 'exploration'
61    elif filename.endswith('.exj'):
62        return 'journal'
63    else:
64        raise ValueError(
65            f"Could not determine the file type of file '{filename}':"
66            f" it does not end with '.dcg', '.dot', '.exp', or '.exj'."
67        )
def loadDecisionGraph(path: pathlib.Path) -> exploration.core.DecisionGraph:
70def loadDecisionGraph(path: pathlib.Path) -> core.DecisionGraph:
71    """
72    Loads a JSON-encoded decision graph from a file. The extension
73    should normally be '.dcg'.
74    """
75    with path.open('r', encoding='utf-8-sig') as fInput:
76        return parsing.loadCustom(fInput, core.DecisionGraph)

Loads a JSON-encoded decision graph from a file. The extension should normally be '.dcg'.

def saveDecisionGraph(path: pathlib.Path, graph: exploration.core.DecisionGraph) -> None:
79def saveDecisionGraph(
80    path: pathlib.Path,
81    graph: core.DecisionGraph
82) -> None:
83    """
84    Saves a decision graph encoded as JSON in the specified file. The
85    file should normally have a '.dcg' extension.
86    """
87    with path.open('w', encoding='utf-8') as fOutput:
88        parsing.saveCustom(graph, fOutput)

Saves a decision graph encoded as JSON in the specified file. The file should normally have a '.dcg' extension.

def loadDotFile(path: pathlib.Path) -> exploration.core.DecisionGraph:
 91def loadDotFile(path: pathlib.Path) -> core.DecisionGraph:
 92    """
 93    Loads a `core.DecisionGraph` form the file at the specified path
 94    (whose extension should normally be '.dot'). The file format is the
 95    GraphViz "dot" format.
 96    """
 97    with path.open('r', encoding='utf-8-sig') as fInput:
 98        dot = fInput.read()
 99        try:
100            return parsing.parseDot(dot)
101        except parsing.DotParseError:
102            raise parsing.DotParseError(
103                "Failed to parse Dot file contents:\n\n"
104              + dot
105              + "\n\n(See error above for specific parsing issue.)"
106            )

Loads a core.DecisionGraph form the file at the specified path (whose extension should normally be '.dot'). The file format is the GraphViz "dot" format.

def saveDotFile(path: pathlib.Path, graph: exploration.core.DecisionGraph) -> None:
109def saveDotFile(path: pathlib.Path, graph: core.DecisionGraph) -> None:
110    """
111    Saves a `core.DecisionGraph` as a GraphViz "dot" file. The file
112    extension should normally be ".dot".
113    """
114    dotStr = parsing.toDot(graph, clusterLevels=[])
115    with path.open('w', encoding='utf-8') as fOutput:
116        fOutput.write(dotStr)

Saves a core.DecisionGraph as a GraphViz "dot" file. The file extension should normally be ".dot".

def loadExploration(path: pathlib.Path) -> exploration.core.DiscreteExploration:
119def loadExploration(path: pathlib.Path) -> core.DiscreteExploration:
120    """
121    Loads a JSON-encoded `core.DiscreteExploration` object from the file
122    at the specified path. The extension should normally be '.exp'.
123    """
124    with path.open('r', encoding='utf-8-sig') as fInput:
125        return parsing.loadCustom(fInput, core.DiscreteExploration)

Loads a JSON-encoded core.DiscreteExploration object from the file at the specified path. The extension should normally be '.exp'.

def saveExploration( path: pathlib.Path, exploration: exploration.core.DiscreteExploration) -> None:
128def saveExploration(
129    path: pathlib.Path,
130    exploration: core.DiscreteExploration
131) -> None:
132    """
133    Saves a `core.DiscreteExploration` object as JSON in the specified
134    file. The file extension should normally be '.exp'.
135    """
136    with path.open('w', encoding='utf-8') as fOutput:
137        parsing.saveCustom(exploration, fOutput)

Saves a core.DiscreteExploration object as JSON in the specified file. The file extension should normally be '.exp'.

def loadJournal(path: pathlib.Path) -> exploration.core.DiscreteExploration:
140def loadJournal(path: pathlib.Path) -> core.DiscreteExploration:
141    """
142    Loads a `core.DiscreteExploration` object from a journal file
143    (extension should normally be '.exj'). Uses the
144    `journal.convertJournal` function.
145    """
146    with path.open('r', encoding='utf-8-sig') as fInput:
147        return journal.convertJournal(fInput.read())

Loads a core.DiscreteExploration object from a journal file (extension should normally be '.exj'). Uses the journal.convertJournal function.

def saveAsJournal( path: pathlib.Path, exploration: exploration.core.DiscreteExploration) -> None:
150def saveAsJournal(
151    path: pathlib.Path,
152    exploration: core.DiscreteExploration
153) -> None:
154    """
155    Saves a `core.DiscreteExploration` object as a text journal in the
156    specified file. The file extension should normally be '.exj'.
157
158    TODO: This?!
159    """
160    raise NotImplementedError(
161        "DiscreteExploration-to-journal conversion is not implemented"
162        " yet."
163    )

Saves a core.DiscreteExploration object as a text journal in the specified file. The file extension should normally be '.exj'.

TODO: This?!

def loadSource( path: pathlib.Path, formatOverride: Optional[Literal['graph', 'dot', 'exploration', 'journal']] = None) -> Union[exploration.core.DecisionGraph, exploration.core.DiscreteExploration]:
166def loadSource(
167    path: pathlib.Path,
168    formatOverride: Optional[SourceType] = None
169) -> Union[core.DecisionGraph, core.DiscreteExploration]:
170    """
171    Loads either a `core.DecisionGraph` or a `core.DiscreteExploration`
172    from the specified file, depending on its file extension (or the
173    specified format given as `formatOverride` if there is one).
174    """
175    if formatOverride is not None:
176        format = formatOverride
177    else:
178        format = determineFileType(str(path))
179
180    if format == "graph":
181        return loadDecisionGraph(path)
182    if format == "dot":
183        return loadDotFile(path)
184    elif format == "exploration":
185        return loadExploration(path)
186    elif format == "journal":
187        return loadJournal(path)
188    else:
189        raise ValueError(
190            f"Unrecognized file format '{format}' (recognized formats"
191            f" are 'graph', 'exploration', and 'journal')."
192        )

Loads either a core.DecisionGraph or a core.DiscreteExploration from the specified file, depending on its file extension (or the specified format given as formatOverride if there is one).

CSVEmbeddable: TypeAlias = Union[NoneType, bool, str, int, float, complex]

A type alias for values we're willing to store in a CSV file without coercing them to a string.

def coerceToCSVValue(result: Any) -> Union[NoneType, bool, str, int, float, complex]:
206def coerceToCSVValue(result: Any) -> CSVEmbeddable:
207    """
208    Coerces any value to one that's embeddable in a CSV file. The
209    `CSVEmbeddable` types are unchanged, but all other types are
210    converted to strings via `json.dumps` if possible or `repr` if not.
211    """
212    if isinstance(result, get_args(CSVEmbeddable)):
213        return result
214    else:
215        try:
216            return json.dumps(result)
217        except Exception:
218            return repr(result)

Coerces any value to one that's embeddable in a CSV file. The CSVEmbeddable types are unchanged, but all other types are converted to strings via json.dumps if possible or repr if not.

def show( source: pathlib.Path, formatOverride: Optional[Literal['graph', 'dot', 'exploration', 'journal']] = None, step: int = -1) -> None:
225def show(
226    source: pathlib.Path,
227    formatOverride: Optional[SourceType] = None,
228    step: int = -1
229) -> None:
230    """
231    Shows the graph or exploration stored in the `source` file. You will
232    need to have the `matplotlib` library installed. Consider using the
233    interactive interface provided by the `explorationViewer` module
234    instead. The file extension is used to determine how to load the data,
235    although the `--format` option may override this. '.dcg' files are
236    assumed to be decision graphs in JSON format, '.exp' files are assumed
237    to be exploration objects in JSON format, and '.exj' files are assumed
238    to be exploration journals in the default journal format. If the object
239    that gets loaded is an exploration, the final graph for that
240    exploration will be displayed, or a specific graph may be selected
241    using `--step`.
242    """
243    obj = loadSource(source, formatOverride)
244    if isinstance(obj, core.DiscreteExploration):
245        obj = obj.getSituation(step).graph
246
247    import matplotlib.pyplot # type: ignore
248
249    # This draws the graph in a new window that pops up. You have to close
250    # the window to end the program.
251    nx.draw(obj)
252    matplotlib.pyplot.show()

Shows the graph or exploration stored in the source file. You will need to have the matplotlib library installed. Consider using the interactive interface provided by the explorationViewer module instead. The file extension is used to determine how to load the data, although the --format option may override this. '.dcg' files are assumed to be decision graphs in JSON format, '.exp' files are assumed to be exploration objects in JSON format, and '.exj' files are assumed to be exploration journals in the default journal format. If the object that gets loaded is an exploration, the final graph for that exploration will be displayed, or a specific graph may be selected using --step.

def transitionStr( exploration: exploration.core.DiscreteExploration, src: int, transition: str, dst: int) -> str:
255def transitionStr(
256    exploration: core.DiscreteExploration,
257    src: base.DecisionID,
258    transition: base.Transition,
259    dst: base.DecisionID
260) -> str:
261    """
262    Given an exploration object, returns a string identifying a
263    transition, incorporating the final identity strings for the source
264    and destination.
265    """
266    srcId = analysis.finalIdentity(exploration, src)
267    dstId = analysis.finalIdentity(exploration, dst)
268    return f"{srcId} → {transition} → {dstId}"

Given an exploration object, returns a string identifying a transition, incorporating the final identity strings for the source and destination.

def printPerf(analyzerName: str) -> None:
271def printPerf(analyzerName: str) -> None:
272    """
273    Prints performance for the given analyzer to stderr.
274    """
275    perf = analysis.ANALYSIS_TIME_SPENT.get(analyzerName)
276    if perf is None:
277        raise RuntimeError(
278            f"Missing analysis perf for {analyzerName!r}."
279        )
280    unit = analysis.ALL_ANALYZERS[analyzerName]._unit
281    call, noC, tc, tw = perf.values()
282    print(
283        f"{analyzerName} ({unit}): {call} / {noC} / {tc:.6f} / {tw:.6f}",
284        file=sys.stderr
285    )

Prints performance for the given analyzer to stderr.

def printMem() -> None:
288def printMem() -> None:
289    """
290    Prints (to stderr) a message about how much memory Python is
291    currently using overall.
292    """
293    if resource is not None:
294        used = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
295        print(f"Using {used} memory (bytes or kilobytes, depending on OS)")
296    else:
297        print(
298            f"Can't get memory usage because the resource module is not"
299            f" available."
300        )
301    # TODO: This is apparently kilobytes on linux but bytes on mac?

Prints (to stderr) a message about how much memory Python is currently using overall.

def analyze( source: pathlib.Path, destination: Optional[pathlib.Path] = None, formatOverride: Optional[Literal['graph', 'dot', 'exploration', 'journal']] = None, applyTools: Optional[Collection[str]] = None, finalOnly: Optional[Collection[str]] = None, includeAll: bool = False, profile: bool = False) -> None:
304def analyze(
305    source: pathlib.Path,
306    destination: Optional[pathlib.Path] = None,
307    formatOverride: Optional[SourceType] = None,
308    applyTools: Optional[Collection[str]] = None,
309    finalOnly: Optional[Collection[str]] = None,
310    includeAll: bool = False,
311    profile: bool = False
312) -> None:
313    """
314    Analyzes the exploration stored in the `source` file. The file
315    extension is used to determine how to load the data, although this
316    may be overridden by the `--format` option. Normally, '.exp' files
317    are treated as JSON-encoded exploration objects, while '.exj' files
318    are treated as journals using the default journal format.
319
320    This applies a number of analysis functions to produce a CSV file
321    showing per-decision-per-step, per-decision, per-step, and
322    per-exploration metrics. A subset of the available metrics may be
323    selected by passing a list of strings for the `applyTools` argument.
324    These strings should be the names of functions in `analysis.py` that
325    are decorated with `analysis.analyze`. By default, only those not
326    marked with `analysis.elide` will be included. You can set
327    `includeAll` to `True` to include all tools, although this is ignored
328    when `applyTools` is not `None`. `finalOnly` specifies one or more
329    tools to only run on the final step of the exploration rather than
330    every step. This only applies to tools whose unit of analysis is
331    'step', 'stepDecision', or 'stepTransition'. By default those marked
332    as `finalOnly` in `analysis.py` will be run this way. Tools excluded
333    via `applyTools` or by default when `includeAll` is false won't be
334    run even if specified in `finalOnly`. Set `finalOnly` to `False` to
335    run all selected tools on all steps without having to explicitly
336    list the tools that would otherwise be restricted by default.
337
338    Set `profile` to `True` to gather and report analysis time spent
339    results (they'll be printed to stdout).
340
341    If no output file is specified, the output will be printed out.
342    """
343    if profile:
344        print("Starting analysis with profiling...", file=sys.stderr)
345        parseStart = time.perf_counter()
346        printMem()
347    # Load our source exploration object:
348    obj = loadSource(source, formatOverride)
349    if isinstance(obj, core.DecisionGraph):
350        obj = core.DiscreteExploration.fromGraph(obj)
351    if profile:
352        elapsed = time.perf_counter() - parseStart
353        print(f"Parsed input in {elapsed:.6f}s...", file=sys.stderr)
354        printMem()
355
356    exploration: core.DiscreteExploration = obj
357
358    # Set up for profiling
359    if profile:
360        analysis.RECORD_PROFILE = True
361    else:
362        analysis.RECORD_PROFILE = False
363
364    # Figure out which to apply
365    if applyTools is not None:
366        toApply: Set[str] = set(applyTools)
367    else:
368        toApply = set(analysis.ALL_ANALYZERS.keys())
369        if not includeAll:
370            print("ELIDING:", analysis.ELIDE, file=sys.stderr)
371            toApply -= analysis.ELIDE
372
373    if finalOnly is False:
374        finalOnly = set()
375    elif finalOnly is None:
376        finalOnly = analysis.FINAL_ONLY
377
378    # Group analyzers by unit
379    byUnit = analysis.analyzersByUnit(toApply)
380
381    # Apply all of the analysis functions (or only just those that are
382    # selected using applyTools):
383
384    wholeRows: List[List[CSVEmbeddable]] = [['Whole exploration metrics:']]
385    if profile:
386        print(
387            "name (unit): calls / non-cached / time (lookups) / time (work)",
388            file=sys.stderr
389        )
390    # One row per analyzer
391    for ea in byUnit["exploration"]:
392        wholeRows.append([ea.__name__, coerceToCSVValue(ea(exploration))])
393        if profile:
394            printPerf(ea.__name__)
395
396    # A few variables for holding pieces we'll assemble
397    row: List[CSVEmbeddable]
398    columns: List[CSVEmbeddable]
399
400    decisionRows: List[Sequence[CSVEmbeddable]] = [
401        ['Per-decision metrics:']
402    ]
403    # One row per tool; one column per decision
404    decisionList: List[base.DecisionID] = exploration.allDecisions()
405    columns = (
406        cast(List[CSVEmbeddable], ['Metric ↓/Decision →'])
407      + cast(List[CSVEmbeddable], decisionList)
408    )
409
410    decisionRows.append(columns)
411    for da in byUnit["decision"]:
412        row = [da.__name__]
413        decisionRows.append(row)
414        for decision in decisionList:
415            row.append(coerceToCSVValue(da(exploration, decision)))
416        if profile:
417            printPerf(da.__name__)
418
419    transitionRows: List[Sequence[CSVEmbeddable]] = [
420        ['Per-transition metrics:']
421    ]
422    # One row per tool; one column per decision
423    transitionList: List[
424        Tuple[base.DecisionID, base.Transition, base.DecisionID]
425    ] = exploration.allTransitions()
426    transitionStrings: List[CSVEmbeddable] = [
427        transitionStr(exploration, *trans)
428        for trans in transitionList
429    ]
430    columns = (
431        cast(List[CSVEmbeddable], ['Metric ↓/Transition →'])
432      + transitionStrings
433    )
434    transitionRows.append(columns)
435    for ta in byUnit["transition"]:
436        row = [ta.__name__]
437        transitionRows.append(row)
438        for transition in transitionList:
439            row.append(
440                coerceToCSVValue(ta(exploration, *transition))
441            )
442        if profile:
443            printPerf(ta.__name__)
444
445    stepRows: List[Sequence[CSVEmbeddable]] = [
446        ['Per-step metrics:']
447    ]
448    # One row per exploration step; one column per tool
449    columns = ['Step ↓/Metric →']
450    stepRows.append(columns)
451    for step in range(len(exploration)):
452        row = [step]
453        stepRows.append(row)
454        for sa in byUnit["step"]:
455            if step == 0:
456                columns.append(sa.__name__)
457            if sa.__name__ in finalOnly and step != len(exploration) - 1:
458                row.append("")
459            else:
460                row.append(coerceToCSVValue(sa(exploration, step)))
461
462    # Print profile results just once after all steps have been analyzed
463    if profile:
464        for sa in byUnit["step"]:
465            printPerf(sa.__name__)
466
467    stepwiseRows: List[Sequence[CSVEmbeddable]] = [
468        ['Per-decision-per-step metrics (one table per metric):']
469    ]
470
471    # For each per-step decision tool; one row per exploration step and
472    # one column per decision
473    columns = (
474        cast(List[CSVEmbeddable], ['Step ↓/Decision →'])
475      + cast(List[CSVEmbeddable], decisionList)
476    )
477    identities = ['Decision names:'] + [
478        analysis.finalIdentity(exploration, d)
479        for d in decisionList
480    ]
481    for sda in byUnit["stepDecision"]:
482        stepwiseRows.append([sda.__name__])
483        stepwiseRows.append(columns)
484        stepwiseRows.append(identities)
485        if sda.__name__ in finalOnly:
486            step = len(exploration) - 1
487            row = [step]
488            stepwiseRows.append(row)
489            for decision in decisionList:
490                row.append(coerceToCSVValue(sda(exploration, step, decision)))
491        else:
492            for step in range(len(exploration)):
493                row = [step]
494                stepwiseRows.append(row)
495                for decision in decisionList:
496                    row.append(
497                        coerceToCSVValue(sda(exploration, step, decision))
498                    )
499        if profile:
500            printPerf(sda.__name__)
501
502    stepwiseTransitionRows: List[Sequence[CSVEmbeddable]] = [
503        ['Per-transition-per-step metrics (one table per metric):']
504    ]
505
506    # For each per-step transition tool; one row per exploration step and
507    # one column per transition
508    columns = (
509        cast(List[CSVEmbeddable], ['Step ↓/Transition →'])
510      + cast(List[CSVEmbeddable], transitionStrings)
511    )
512    for sta in byUnit["stepTransition"]:
513        stepwiseTransitionRows.append([sta.__name__])
514        stepwiseTransitionRows.append(columns)
515        if sta.__name__ in finalOnly:
516            step = len(exploration) - 1
517            row = [step]
518            stepwiseTransitionRows.append(row)
519            for (src, trans, dst) in transitionList:
520                row.append(
521                    coerceToCSVValue(sta(exploration, step, src, trans, dst))
522                )
523        else:
524            for step in range(len(exploration)):
525                row = [step]
526                stepwiseTransitionRows.append(row)
527                for (src, trans, dst) in transitionList:
528                    row.append(
529                        coerceToCSVValue(
530                            sta(exploration, step, src, trans, dst)
531                        )
532                    )
533        if profile:
534            printPerf(sta.__name__)
535
536    # Build a grid containing just the non-empty analysis categories, so
537    # that if you deselect some tools you get a smaller CSV file:
538    grid: List[Sequence[CSVEmbeddable]] = []
539    if len(wholeRows) > 1:
540        grid.extend(wholeRows)
541    for block in (
542        decisionRows,
543        transitionRows,
544        stepRows,
545        stepwiseRows,
546        stepwiseTransitionRows
547    ):
548        if len(block) > 1:
549            if grid:
550                grid.append([])  # spacer
551            grid.extend(block)
552
553    # Print all profile results at the end
554    if profile:
555        print("-"*80, file=sys.stderr)
556        print("Done with analysis. Time taken:", file=sys.stderr)
557        print("-"*80, file=sys.stderr)
558        for aname in analysis.ANALYSIS_TIME_SPENT:
559            printPerf(aname)
560        print("-"*80, file=sys.stderr)
561        printMem()
562
563    # Figure out our destination stream:
564    if destination is None:
565        outStream = sys.stdout
566        closeIt = False
567    else:
568        outStream = open(destination, 'w')
569        closeIt = True
570
571    # Create a CSV writer for our stream
572    writer = csv.writer(outStream)
573
574    # Write out our grid to the file
575    try:
576        writer.writerows(grid)
577    finally:
578        if closeIt:
579            outStream.close()

Analyzes the exploration stored in the source file. The file extension is used to determine how to load the data, although this may be overridden by the --format option. Normally, '.exp' files are treated as JSON-encoded exploration objects, while '.exj' files are treated as journals using the default journal format.

This applies a number of analysis functions to produce a CSV file showing per-decision-per-step, per-decision, per-step, and per-exploration metrics. A subset of the available metrics may be selected by passing a list of strings for the applyTools argument. These strings should be the names of functions in analysis.py that are decorated with analysis.analyze. By default, only those not marked with analysis.elide will be included. You can set includeAll to True to include all tools, although this is ignored when applyTools is not None. finalOnly specifies one or more tools to only run on the final step of the exploration rather than every step. This only applies to tools whose unit of analysis is 'step', 'stepDecision', or 'stepTransition'. By default those marked as finalOnly in analysis.py will be run this way. Tools excluded via applyTools or by default when includeAll is false won't be run even if specified in finalOnly. Set finalOnly to False to run all selected tools on all steps without having to explicitly list the tools that would otherwise be restricted by default.

Set profile to True to gather and report analysis time spent results (they'll be printed to stdout).

If no output file is specified, the output will be printed out.

def convert( source: pathlib.Path, destination: pathlib.Path, inputFormatOverride: Optional[Literal['graph', 'dot', 'exploration', 'journal']] = None, outputFormatOverride: Optional[Literal['graph', 'dot', 'exploration', 'journal']] = None, step: int = -1) -> None:
582def convert(
583    source: pathlib.Path,
584    destination: pathlib.Path,
585    inputFormatOverride: Optional[SourceType] = None,
586    outputFormatOverride: Optional[SourceType] = None,
587    step: int = -1
588) -> None:
589    """
590    Converts between exploration and graph formats. By default, formats
591    are determined by file extensions, but using the `--format` and
592    `--output-format` options can override this. The available formats
593    are:
594
595    - '.dcg' A `core.DecisionGraph` stored in JSON format.
596    - '.dot' A `core.DecisionGraph` stored as a GraphViz DOT file.
597    - '.exp' A `core.DiscreteExploration` stored in JSON format.
598    - '.exj' A `core.DiscreteExploration` stored as a journal (see
599        `journal.JournalObserver`; TODO: writing this format).
600
601    When converting a decision graph into an exploration format, the
602    resulting exploration will have a single starting step containing
603    the entire specified graph. When converting an exploration into a
604    decision graph format, only the current graph will be saved, unless
605    `--step` is used to specify a different step index to save.
606    """
607    # TODO journal writing
608    obj = loadSource(source, inputFormatOverride)
609
610    if outputFormatOverride is None:
611        outputFormat = determineFileType(str(destination))
612    else:
613        outputFormat = outputFormatOverride
614
615    if outputFormat in ("graph", "dot"):
616        if isinstance(obj, core.DiscreteExploration):
617            graph = obj.getSituation(step).graph
618        else:
619            graph = obj
620        if outputFormat == "graph":
621            saveDecisionGraph(destination, graph)
622        else:
623            saveDotFile(destination, graph)
624    else:
625        if isinstance(obj, core.DecisionGraph):
626            exploration = core.DiscreteExploration.fromGraph(obj)
627        else:
628            exploration = obj
629        if outputFormat == "exploration":
630            saveExploration(destination, exploration)
631        else:
632            saveAsJournal(destination, exploration)

Converts between exploration and graph formats. By default, formats are determined by file extensions, but using the --format and --output-format options can override this. The available formats are:

  • '.dcg' A core.DecisionGraph stored in JSON format.
  • '.dot' A core.DecisionGraph stored as a GraphViz DOT file.
  • '.exp' A core.DiscreteExploration stored in JSON format.
  • '.exj' A core.DiscreteExploration stored as a journal (see journal.JournalObserver; TODO: writing this format).

When converting a decision graph into an exploration format, the resulting exploration will have a single starting step containing the entire specified graph. When converting an exploration into a decision graph format, only the current graph will be saved, unless --step is used to specify a different step index to save.

INSPECTOR_HELP = "\nAvailable commands:\n\n- 'help' or '?': List commands.\n- 'done', 'quit', 'q', or 'exit': Quit the inspector.\n- 'f' or 'follow': Follow the primary decision when changing steps. Also\n changes to that decision immediately. Toggles off if on.\n- 'cd' or 'goto': Change focus decision to the named decision. Cancels\n follow mode.\n- 'ls' or 'list' or 'destinations': Lists transitions at this decision\n and their destinations, as well as any mechanisms at this decision.\n- 'lst' or 'steps': Lists each step of the exploration along with the\n primary decision at each step.\n- 'st' or 'step': Switches to the specified step (an index)\n- 'n' or 'next': Switches to the next step.\n- 'p' or 'prev' or 'previous': Switches to the previous step.\n- 't' or 'take': Change focus decision to the decision which is the\n destination of the specified transition at the current focused\n decision.\n- 'prm' or 'primary': Displays the current primary decision.\n- 'a' or 'active': Lists all currently active decisions\n- 'u' or 'unexplored': Lists all unexplored transitions at the current\n step.\n- 'x' or 'explorable': Lists all unexplored transitions at the current\n step which are traversable based on the current state. (TODO:\n make this more accurate).\n- 'r' or 'reachable': TODO\n- 'A' or 'all': Lists all decisions at the current step.\n- 'M' or 'mechanisms': Lists all mechanisms at the current step.\n"
def inspect( source: pathlib.Path, formatOverride: Optional[Literal['graph', 'dot', 'exploration', 'journal']] = None) -> None:
667def inspect(
668    source: pathlib.Path,
669    formatOverride: Optional[SourceType] = None
670) -> None:
671    """
672    Inspects the graph or exploration stored in the `source` file,
673    launching an interactive command line for inspecting properties of
674    decisions, transitions, and situations. The file extension is used
675    to determine how to load the data, although the `--format` option
676    may override this. '.dcg' files are assumed to be decision graphs in
677    JSON format, '.exp' files are assumed to be exploration objects in
678    JSON format, and '.exj' files are assumed to be exploration journals
679    in the default journal format. If the object that gets loaded is a
680    graph, a 1-step exploration containing just that graph will be
681    created to inspect. Inspector commands are listed in the
682    `INSPECTOR_HELP` variable.
683    """
684    print(f"Loading exploration from {source!r}...")
685    # Load our exploration
686    exploration = loadSource(source, formatOverride)
687    if isinstance(exploration, core.DecisionGraph):
688        exploration = core.DiscreteExploration.fromGraph(exploration)
689
690    print(
691        f"Inspecting exploration with {len(exploration)} step(s) and"
692        f" {len(exploration.allDecisions())} decision(s):"
693    )
694    print("('h' for help)")
695
696    # Set up tracking variables:
697    step = len(exploration) - 1
698    here: Optional[base.DecisionID] = exploration.primaryDecision(step)
699    graph = exploration.getSituation(step).graph
700    follow = True
701
702    pf = parsing.ParseFormat()
703
704    if here is None:
705        print("Note: There are no decisions in the final graph.")
706
707    while True:
708        # Re-establish the prompt
709        prompt = "> "
710        if here is not None and here in graph:
711            prompt = graph.identityOf(here) + "> "
712        elif here is not None:
713            prompt = f"{here} (?)> "
714
715        # Prompt for the next command
716        fullCommand = input(prompt).split()
717
718        # Track number of invalid commands so we can quit after 10 in a row
719        invalidCommands = 0
720
721        if len(fullCommand) == 0:
722            cmd = ''
723            args = ''
724        else:
725            cmd = fullCommand[0]
726            args = ' '.join(fullCommand[1:])
727
728        # Do what the command says
729        invalid = False
730        if cmd in ("help", '?'):
731            # Displays help message
732            if len(args.strip()) > 0:
733                print("(help does not accept any arguments)")
734            print(INSPECTOR_HELP)
735        elif cmd in ("done", "exit", "quit", "q"):
736            # Exits the inspector
737            if len(args.strip()) > 0:
738                print("(quit does not accept any arguments)")
739            print("Bye.")
740            break
741        elif cmd in ("f", "follow"):
742            if follow:
743                follow = False
744                print("Stopped following")
745            else:
746                follow = True
747                here = exploration.primaryDecision(step)
748                print(f"Now following at: {graph.identityOf(here)}")
749        elif cmd in ("cd", "goto"):
750            # Changes focus to a specific decision
751            try:
752                target = pf.parseDecisionSpecifier(args)
753                target = graph.resolveDecision(target)
754                here = target
755                follow = False
756                print(f"now at: {graph.identityOf(target)}")
757            except Exception:
758                print("(invalid decision specifier)")
759        elif cmd in ("ls", "list", "destinations"):
760            fromID: Optional[base.AnyDecisionSpecifier] = None
761            if args.strip():
762                fromID = pf.parseDecisionSpecifier(args)
763                fromID = graph.resolveDecision(fromID)
764            else:
765                fromID = here
766
767            if fromID is None:
768                print(
769                    "(no focus decision and no decision specified;"
770                    " nothing to list; use 'cd' to specify a decision,"
771                    " or 'all' to list all decisions)"
772                )
773            else:
774                outgoing = graph.destinationsFrom(fromID)
775                info = graph.identityOf(fromID)
776                if len(outgoing) > 0:
777                    print(f"Destinations from {info}:")
778                    print(graph.destinationsListing(outgoing))
779                else:
780                    print("No outgoing transitions from {info}.")
781        elif cmd in ("lst", "steps"):
782            total = len(exploration)
783            print(f"{total} step(s):")
784            for step in range(total):
785                pr = exploration.primaryDecision(step)
786                situ = exploration.getSituation(step)
787                stGraph = situ.graph
788                identity = stGraph.identityOf(pr)
789                print(f"  {step} at {identity}")
790            print(f"({total} total step(s))")
791        elif cmd in ("st", "step"):
792            stepTo = int(args.strip())
793            if stepTo < 0:
794                stepTo += len(exploration)
795            if stepTo < 0:
796                print(
797                    f"Invalid step {args!r} (too negative; min is"
798                    f" {-len(exploration)})"
799                )
800            if stepTo >= len(exploration):
801                print(
802                    f"Invalid step {args!r} (too large; max is"
803                    f" {len(exploration) - 1})"
804                )
805
806            step = stepTo
807            graph = exploration.getSituation(step).graph
808            if follow:
809                here = exploration.primaryDecision(step)
810                print(f"Followed to: {graph.identityOf(here)}")
811        elif cmd in ("n", "next"):
812            if step == -1 or step >= len(exploration) - 2:
813                print("Can't step beyond the last step.")
814            else:
815                step += 1
816                graph = exploration.getSituation(step).graph
817                if here not in graph:
818                    here = None
819            print(f"At step {step}")
820            if follow:
821                here = exploration.primaryDecision(step)
822                print(f"Followed to: {graph.identityOf(here)}")
823        elif cmd in ("p", "prev"):
824            if step == 0 or step <= -len(exploration) + 2:
825                print("Can't step before the first step.")
826            else:
827                step -= 1
828                graph = exploration.getSituation(step).graph
829                if here not in graph:
830                    here = None
831            print(f"At step {step}")
832            if follow:
833                here = exploration.primaryDecision(step)
834                print(f"Followed to: {graph.identityOf(here)}")
835        elif cmd in ("t", "take"):
836            if here is None:
837                print(
838                    "(no focus decision, so can't take transitions. Use"
839                    " 'cd' to specify a decision first.)"
840                )
841            else:
842                dest = graph.getDestination(here, args)
843                if dest is None:
844                    print(
845                        f"Invalid transition {args!r} (no destination for"
846                        f" that transition from {graph.identityOf(here)}"
847                    )
848                here = dest
849        elif cmd in ("prm", "primary"):
850            pr = exploration.primaryDecision(step)
851            if pr is None:
852                print(f"Step {step} has no primary decision")
853            else:
854                print(
855                    f"Primary decision for step {step} is:"
856                    f" {graph.identityOf(pr)}"
857                )
858        elif cmd in ("a", "active"):
859            active = exploration.getActiveDecisions(step)
860            print(f"Active decisions at step {step}:")
861            print(graph.namesListing(active))
862        elif cmd in ("u", "unexplored"):
863            unx = analysis.unexploredBranches(graph)
864            fin = ':' if len(unx) > 0 else '.'
865            print(f"{len(unx)} unexplored branch(es){fin}")
866            for frID, unTr in unx:
867                print(f"take {unTr} at {graph.identityOf(frID)}")
868        elif cmd in ("x", "explorable"):
869            ctx = base.genericContextForSituation(
870                exploration.getSituation(step)
871            )
872            unx = analysis.unexploredBranches(graph, ctx)
873            fin = ':' if len(unx) > 0 else '.'
874            print(f"{len(unx)} unexplored branch(es){fin}")
875            for frID, unTr in unx:
876                print(f"take {unTr} at {graph.identityOf(frID)}")
877        elif cmd in ("r", "reachable"):
878            print("TODO: Reachable does not work yet.")
879        elif cmd in ("A", "all"):
880            print(
881                f"There are {len(graph)} decision(s) at step {step}:"
882            )
883            for decision in graph.nodes():
884                print(f"  {graph.identityOf(decision)}")
885        elif cmd in ("M", "mechanisms"):
886            count = len(graph.mechanisms)
887            fin = ':' if count > 0 else '.'
888            print(
889                f"There are {count} mechanism(s) at step {step}{fin}"
890            )
891            for mID in graph.mechanisms:
892                where, name = graph.mechanisms[mID]
893                state = exploration.mechanismState(mID, step=step)
894                if where is None:
895                    print(f"  {name!r} (global) in state {state!r}")
896                else:
897                    info = graph.identityOf(where)
898                    print(f"  {name!r} at {info} in state {state!r}")
899        else:
900            invalid = True
901
902        if invalid:
903            if invalidCommands >= 10:
904                print("Too many invalid commands; exiting.")
905                break
906            else:
907                if invalidCommands >= 8:
908                    print("{invalidCommands} invalid commands so far,")
909                    print("inspector will stop after 10 invalid commands...")
910                print(f"Unknown command {cmd!r}...")
911                invalidCommands += 1
912                print(INSPECTOR_HELP)
913        else:
914            invalidCommands = 0

Inspects the graph or exploration stored in the source file, launching an interactive command line for inspecting properties of decisions, transitions, and situations. The file extension is used to determine how to load the data, although the --format option may override this. '.dcg' files are assumed to be decision graphs in JSON format, '.exp' files are assumed to be exploration objects in JSON format, and '.exj' files are assumed to be exploration journals in the default journal format. If the object that gets loaded is a graph, a 1-step exploration containing just that graph will be created to inspect. Inspector commands are listed in the INSPECTOR_HELP variable.

parser = ArgumentParser(prog='python -m exploration', usage=None, description='Runs various commands for processing exploration graphs and journals,\nand for converting between them or displaying them in various formats.\n', formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)
subparsers = _SubParsersAction(option_strings=[], dest='==SUPPRESS==', nargs='A...', const=None, default=None, type=None, choices={'show': ArgumentParser(prog='python -m exploration show', usage=None, description="Shows the graph or exploration stored in the `source` file. You will\nneed to have the `matplotlib` library installed. Consider using the\ninteractive interface provided by the `explorationViewer` module\ninstead. The file extension is used to determine how to load the data,\nalthough the `--format` option may override this. '.dcg' files are\nassumed to be decision graphs in JSON format, '.exp' files are assumed\nto be exploration objects in JSON format, and '.exj' files are assumed\nto be exploration journals in the default journal format. If the object\nthat gets loaded is an exploration, the final graph for that\nexploration will be displayed, or a specific graph may be selected\nusing `--step`.", formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True), 'analyze': ArgumentParser(prog='python -m exploration analyze', usage=None, description="Analyzes the exploration stored in the `source` file. The file\nextension is used to determine how to load the data, although this\nmay be overridden by the `--format` option. Normally, '.exp' files\nare treated as JSON-encoded exploration objects, while '.exj' files\nare treated as journals using the default journal format.\n\nThis applies a number of analysis functions to produce a CSV file\nshowing per-decision-per-step, per-decision, per-step, and\nper-exploration metrics. A subset of the available metrics may be\nselected by passing a list of strings for the `applyTools` argument.\nThese strings should be the names of functions in `analysis.py` that\nare decorated with `analysis.analyze`. By default, only those not\nmarked with `analysis.elide` will be included. You can set\n`includeAll` to `True` to include all tools, although this is ignored\nwhen `applyTools` is not `None`. `finalOnly` specifies one or more\ntools to only run on the final step of the exploration rather than\nevery step. This only applies to tools whose unit of analysis is\n'step', 'stepDecision', or 'stepTransition'. By default those marked\nas `finalOnly` in `analysis.py` will be run this way. Tools excluded\nvia `applyTools` or by default when `includeAll` is false won't be\nrun even if specified in `finalOnly`. Set `finalOnly` to `False` to\nrun all selected tools on all steps without having to explicitly\nlist the tools that would otherwise be restricted by default.\n\nSet `profile` to `True` to gather and report analysis time spent\nresults (they'll be printed to stdout).\n\nIf no output file is specified, the output will be printed out.", formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True), 'convert': ArgumentParser(prog='python -m exploration convert', usage=None, description="Converts between exploration and graph formats. By default, formats\nare determined by file extensions, but using the `--format` and\n`--output-format` options can override this. The available formats\nare:\n\n- '.dcg' A `core.DecisionGraph` stored in JSON format.\n- '.dot' A `core.DecisionGraph` stored as a GraphViz DOT file.\n- '.exp' A `core.DiscreteExploration` stored in JSON format.\n- '.exj' A `core.DiscreteExploration` stored as a journal (see\n `journal.JournalObserver`; TODO: writing this format).\n\nWhen converting a decision graph into an exploration format, the\nresulting exploration will have a single starting step containing\nthe entire specified graph. When converting an exploration into a\ndecision graph format, only the current graph will be saved, unless\n`--step` is used to specify a different step index to save.", formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True), 'inspect': ArgumentParser(prog='python -m exploration inspect', usage=None, description="Inspects the graph or exploration stored in the `source` file,\nlaunching an interactive command line for inspecting properties of\ndecisions, transitions, and situations. The file extension is used\nto determine how to load the data, although the `--format` option\nmay override this. '.dcg' files are assumed to be decision graphs in\nJSON format, '.exp' files are assumed to be exploration objects in\nJSON format, and '.exj' files are assumed to be exploration journals\nin the default journal format. If the object that gets loaded is a\ngraph, a 1-step exploration containing just that graph will be\ncreated to inspect. Inspector commands are listed in the\n`INSPECTOR_HELP` variable.", formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)}, required=False, help='use these with -h/--help for more details', metavar=None)
showParser = ArgumentParser(prog='python -m exploration show', usage=None, description="Shows the graph or exploration stored in the `source` file. You will\nneed to have the `matplotlib` library installed. Consider using the\ninteractive interface provided by the `explorationViewer` module\ninstead. The file extension is used to determine how to load the data,\nalthough the `--format` option may override this. '.dcg' files are\nassumed to be decision graphs in JSON format, '.exp' files are assumed\nto be exploration objects in JSON format, and '.exj' files are assumed\nto be exploration journals in the default journal format. If the object\nthat gets loaded is an exploration, the final graph for that\nexploration will be displayed, or a specific graph may be selected\nusing `--step`.", formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)
analyzeParser = ArgumentParser(prog='python -m exploration analyze', usage=None, description="Analyzes the exploration stored in the `source` file. The file\nextension is used to determine how to load the data, although this\nmay be overridden by the `--format` option. Normally, '.exp' files\nare treated as JSON-encoded exploration objects, while '.exj' files\nare treated as journals using the default journal format.\n\nThis applies a number of analysis functions to produce a CSV file\nshowing per-decision-per-step, per-decision, per-step, and\nper-exploration metrics. A subset of the available metrics may be\nselected by passing a list of strings for the `applyTools` argument.\nThese strings should be the names of functions in `analysis.py` that\nare decorated with `analysis.analyze`. By default, only those not\nmarked with `analysis.elide` will be included. You can set\n`includeAll` to `True` to include all tools, although this is ignored\nwhen `applyTools` is not `None`. `finalOnly` specifies one or more\ntools to only run on the final step of the exploration rather than\nevery step. This only applies to tools whose unit of analysis is\n'step', 'stepDecision', or 'stepTransition'. By default those marked\nas `finalOnly` in `analysis.py` will be run this way. Tools excluded\nvia `applyTools` or by default when `includeAll` is false won't be\nrun even if specified in `finalOnly`. Set `finalOnly` to `False` to\nrun all selected tools on all steps without having to explicitly\nlist the tools that would otherwise be restricted by default.\n\nSet `profile` to `True` to gather and report analysis time spent\nresults (they'll be printed to stdout).\n\nIf no output file is specified, the output will be printed out.", formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)
convertParser = ArgumentParser(prog='python -m exploration convert', usage=None, description="Converts between exploration and graph formats. By default, formats\nare determined by file extensions, but using the `--format` and\n`--output-format` options can override this. The available formats\nare:\n\n- '.dcg' A `core.DecisionGraph` stored in JSON format.\n- '.dot' A `core.DecisionGraph` stored as a GraphViz DOT file.\n- '.exp' A `core.DiscreteExploration` stored in JSON format.\n- '.exj' A `core.DiscreteExploration` stored as a journal (see\n `journal.JournalObserver`; TODO: writing this format).\n\nWhen converting a decision graph into an exploration format, the\nresulting exploration will have a single starting step containing\nthe entire specified graph. When converting an exploration into a\ndecision graph format, only the current graph will be saved, unless\n`--step` is used to specify a different step index to save.", formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)
inspectParser = ArgumentParser(prog='python -m exploration inspect', usage=None, description="Inspects the graph or exploration stored in the `source` file,\nlaunching an interactive command line for inspecting properties of\ndecisions, transitions, and situations. The file extension is used\nto determine how to load the data, although the `--format` option\nmay override this. '.dcg' files are assumed to be decision graphs in\nJSON format, '.exp' files are assumed to be exploration objects in\nJSON format, and '.exj' files are assumed to be exploration journals\nin the default journal format. If the object that gets loaded is a\ngraph, a 1-step exploration containing just that graph will be\ncreated to inspect. Inspector commands are listed in the\n`INSPECTOR_HELP` variable.", formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)
def main():
1077def main():
1078    """
1079    Parse options from command line & run appropriate tool.
1080    """
1081    options = parser.parse_args()
1082    if not hasattr(options, "run"):
1083        print("No sub-command specified.")
1084        parser.print_help()
1085        exit(1)
1086    elif options.run == "show":
1087        show(
1088            options.source,
1089            formatOverride=options.format,
1090            step=options.step
1091        )
1092    elif options.run == "analyze":
1093        analyze(
1094            options.source,
1095            destination=options.destination,
1096            formatOverride=options.format,
1097            includeAll=options.all,
1098            profile=options.profile
1099        )
1100    elif options.run == "convert":
1101        convert(
1102            options.source,
1103            options.destination,
1104            inputFormatOverride=options.format,
1105            outputFormatOverride=options.output_format,
1106            step=options.step
1107        )
1108    elif options.run == "inspect":
1109        inspect(
1110            options.source,
1111            formatOverride=options.format
1112        )
1113    else:
1114        raise RuntimeError(
1115            f"Invalid 'run' default value: '{options.run}'."
1116        )

Parse options from command line & run appropriate tool.