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 networkx as nx # type: ignore 16 17from typing import ( 18 Literal, Optional, Union, get_args, TypeAlias, List, Callable, Dict, 19 Sequence 20) 21 22from . import journal 23from . import core 24from . import base 25from . import analysis 26from . import parsing 27 28 29#------------# 30# File input # 31#------------# 32 33SourceType: TypeAlias = Literal[ 34 "graph", 35 "dot", 36 "exploration", 37 "journal", 38] 39""" 40The file types we recognize. 41""" 42 43 44def determineFileType(filename: str) -> SourceType: 45 if filename.endswith('.dcg'): 46 return 'graph' 47 elif filename.endswith('.dot'): 48 return 'dot' 49 elif filename.endswith('.exp'): 50 return 'exploration' 51 elif filename.endswith('.exj'): 52 return 'journal' 53 else: 54 raise ValueError( 55 f"Could not determine the file type of file '{filename}':" 56 f" it does not end with '.dcg', '.dot', '.exp', or '.exj'." 57 ) 58 59 60def loadDecisionGraph(path: pathlib.Path) -> core.DecisionGraph: 61 """ 62 Loads a JSON-encoded decision graph from a file. The extension 63 should normally be '.dcg'. 64 """ 65 with path.open('r', encoding='utf-8-sig') as fInput: 66 return parsing.loadCustom(fInput, core.DecisionGraph) 67 68 69def saveDecisionGraph( 70 path: pathlib.Path, 71 graph: core.DecisionGraph 72) -> None: 73 """ 74 Saves a decision graph encoded as JSON in the specified file. The 75 file should normally have a '.dcg' extension. 76 """ 77 with path.open('w', encoding='utf-8') as fOutput: 78 parsing.saveCustom(graph, fOutput) 79 80 81def loadDotFile(path: pathlib.Path) -> core.DecisionGraph: 82 """ 83 Loads a `core.DecisionGraph` form the file at the specified path 84 (whose extension should normally be '.dot'). The file format is the 85 GraphViz "dot" format. 86 """ 87 with path.open('r', encoding='utf-8-sig') as fInput: 88 dot = fInput.read() 89 try: 90 return parsing.parseDot(dot) 91 except parsing.DotParseError: 92 raise parsing.DotParseError( 93 "Failed to parse Dot file contents:\n\n" 94 + dot 95 + "\n\n(See error above for specific parsing issue.)" 96 ) 97 98 99def saveDotFile(path: pathlib.Path, graph: core.DecisionGraph) -> None: 100 """ 101 Saves a `core.DecisionGraph` as a GraphViz "dot" file. The file 102 extension should normally be ".dot". 103 """ 104 dotStr = parsing.toDot(graph, clusterLevels=[]) 105 with path.open('w', encoding='utf-8') as fOutput: 106 fOutput.write(dotStr) 107 108 109def loadExploration(path: pathlib.Path) -> core.DiscreteExploration: 110 """ 111 Loads a JSON-encoded `core.DiscreteExploration` object from the file 112 at the specified path. The extension should normally be '.exp'. 113 """ 114 with path.open('r', encoding='utf-8-sig') as fInput: 115 return parsing.loadCustom(fInput, core.DiscreteExploration) 116 117 118def saveExploration( 119 path: pathlib.Path, 120 exploration: core.DiscreteExploration 121) -> None: 122 """ 123 Saves a `core.DiscreteExploration` object as JSON in the specified 124 file. The file extension should normally be '.exp'. 125 """ 126 with path.open('w', encoding='utf-8') as fOutput: 127 parsing.saveCustom(exploration, fOutput) 128 129 130def loadJournal(path: pathlib.Path) -> core.DiscreteExploration: 131 """ 132 Loads a `core.DiscreteExploration` object from a journal file 133 (extension should normally be '.exj'). Uses the 134 `journal.convertJournal` function. 135 """ 136 with path.open('r', encoding='utf-8-sig') as fInput: 137 return journal.convertJournal(fInput.read()) 138 139 140def saveAsJournal( 141 path: pathlib.Path, 142 exploration: core.DiscreteExploration 143) -> None: 144 """ 145 Saves a `core.DiscreteExploration` object as a text journal in the 146 specified file. The file extension should normally be '.exj'. 147 148 TODO: This?! 149 """ 150 raise NotImplementedError( 151 "DiscreteExploration-to-journal conversion is not implemented" 152 " yet." 153 ) 154 155 156def loadSource( 157 path: pathlib.Path, 158 formatOverride: Optional[SourceType] = None 159) -> Union[core.DecisionGraph, core.DiscreteExploration]: 160 """ 161 Loads either a `core.DecisionGraph` or a `core.DiscreteExploration` 162 from the specified file, depending on its file extension (or the 163 specified format given as `formatOverride` if there is one). 164 """ 165 if formatOverride is not None: 166 format = formatOverride 167 else: 168 format = determineFileType(str(path)) 169 170 if format == "graph": 171 return loadDecisionGraph(path) 172 if format == "dot": 173 return loadDotFile(path) 174 elif format == "exploration": 175 return loadExploration(path) 176 elif format == "journal": 177 return loadJournal(path) 178 else: 179 raise ValueError( 180 f"Unrecognized file format '{format}' (recognized formats" 181 f" are 'graph', 'exploration', and 'journal')." 182 ) 183 184 185#---------------------# 186# Analysis tool lists # 187#---------------------# 188 189AnalysisResult: TypeAlias = Union[None, bool, str, int, float, complex] 190""" 191A type alias for values we're willing to accept as analysis results. 192These are going to be written to a CSV file that we want to be 193human-readable. 194""" 195 196STEPWISE_DECISION_ANALYSIS_TOOLS: Dict[ 197 str, 198 Callable[[base.Situation, base.DecisionID], AnalysisResult] 199] = { 200 "actionCount": analysis.analyzeGraph(analysis.countActionsAtDecision), 201 "branchCount": analysis.analyzeGraph(analysis.countBranches) 202} 203""" 204The analysis functions to apply to each decision in each step when 205analyzing an exploration, and the names for each. 206""" 207 208STEP_ANALYSIS_TOOLS: Dict[ 209 str, 210 Callable[[base.Situation], AnalysisResult] 211] = { 212 "currentDecision": analysis.currentDecision, 213 "unexploredCount": analysis.countAllUnexploredBranches, 214 "traversableUnexploredCount": analysis.countTraversableUnexploredBranches, 215 "meanActions": analysis.meanOfResults( 216 analysis.perDecision( 217 analysis.analyzeGraph(analysis.countActionsAtDecision) 218 ) 219 ), 220 "meanBranches": analysis.meanOfResults( 221 analysis.perDecision( 222 analysis.analyzeGraph(analysis.countBranches) 223 ) 224 ), 225} 226""" 227The analysis functions to apply to each step when analyzing an 228exploration, and the names for each. 229""" 230 231DECISION_ANALYSIS_TOOLS: Dict[ 232 str, 233 Callable[ 234 [core.DiscreteExploration, base.DecisionID], 235 AnalysisResult 236 ] 237] = { 238 "identity": analysis.lastIdentity, 239 "isVisited": lambda e, d: d in e.allVisitedDecisions(), 240 "revisitCount": analysis.countRevisits, 241} 242""" 243The analysis functions to apply once to each decision in an exploration, 244and the names for each. 245""" 246 247WHOLE_ANALYSIS_TOOLS: Dict[ 248 str, 249 Callable[[core.DiscreteExploration], AnalysisResult] 250] = { 251 "stepCount": lambda e: len(e), 252 "finalDecisionCount": lambda e: len(e.getSituation().graph), 253 "meanRevisits": analysis.meanOfResults( 254 analysis.perExplorationDecision( 255 analysis.countRevisits, 256 mode="visited" 257 ) 258 ) 259} 260""" 261The analysis functions to apply to entire explorations, and the names 262for each. 263""" 264 265 266#---------------# 267# API Functions # 268#---------------# 269 270def show( 271 source: pathlib.Path, 272 formatOverride: Optional[SourceType] = None, 273 step: int = -1 274) -> None: 275 """ 276 Shows the graph or exploration stored in the `source` file. You will 277 need to have the `matplotlib` library installed. Consider using the 278 interactive interface provided by the `explorationViewer` module 279 instead. The file extension is used to determine how to load the data, 280 although the `--format` option may override this. '.dcg' files are 281 assumed to be decision graphs in JSON format, '.exp' files are assumed 282 to be exploration objects in JSON format, and '.exj' files are assumed 283 to be exploration journals in the default journal format. If the object 284 that gets loaded is an exploration, the final graph for that 285 exploration will be displayed, or a specific graph may be selected 286 using `--step`. 287 """ 288 obj = loadSource(source, formatOverride) 289 if isinstance(obj, core.DiscreteExploration): 290 obj = obj.getSituation(step).graph 291 292 import matplotlib.pyplot # type: ignore 293 294 # This draws the graph in a new window that pops up. You have to close 295 # the window to end the program. 296 nx.draw(obj) 297 matplotlib.pyplot.show() 298 299 300def analyze( 301 source: pathlib.Path, 302 formatOverride: Optional[SourceType] = None, 303 destination: Optional[pathlib.Path] = None, 304 applyTools: Optional[List[str]] = None 305) -> None: 306 """ 307 Analyzes the exploration stored in the `source` file. The file 308 extension is used to determine how to load the data, although this 309 may be overridden by the `--format` option. Normally, '.exp' files 310 are treated as JSON-encoded exploration objects, while '.exj' files 311 are treated as journals using the default journal format. 312 313 This applies a number of analysis functions to produce a CSV file 314 showing per-decision-per-step, per-decision, per-step, and 315 per-exploration metrics. A subset of the available metrics may be 316 selected by passing a list of strings for the `applyTools` argument. 317 See the `STEPWISE_DECISION_ANALYSIS_TOOLS`, `STEP_ANALYSIS_TOOLS`, 318 `DECISION_ANALYSIS_TOOLS`, and `WHOLE_ANALYSIS_TOOLS` dictionaries 319 for tool names. 320 321 If no output file is specified, the output will be printed out. 322 """ 323 # Load our source exploration object: 324 obj = loadSource(source, formatOverride) 325 if isinstance(obj, core.DecisionGraph): 326 obj = core.DiscreteExploration.fromGraph(obj) 327 328 exploration: core.DiscreteExploration = obj 329 330 # Apply all of the analysis functions (or only just those that are 331 # selected using applyTools): 332 333 wholeRows: List[List[AnalysisResult]] = [['Whole exploration metrics:']] 334 # One row per tool 335 for tool in WHOLE_ANALYSIS_TOOLS: 336 if (applyTools is None) or (tool in applyTools): 337 wholeRows.append( 338 [tool, WHOLE_ANALYSIS_TOOLS[tool](exploration)] 339 ) 340 341 decisionRows: List[Sequence[AnalysisResult]] = [ 342 ['Per-decision metrics:'] 343 ] 344 # One row per tool; one column per decision 345 decisionList = exploration.allDecisions() 346 columns = ['Metric ↓/Decision →'] + decisionList 347 decisionRows.append(columns) 348 for tool in DECISION_ANALYSIS_TOOLS: 349 if (applyTools is None) or (tool in applyTools): 350 row: List[AnalysisResult] = [tool] 351 decisionRows.append(row) 352 for decision in decisionList: 353 row.append( 354 DECISION_ANALYSIS_TOOLS[tool](exploration, decision) 355 ) 356 357 stepRows: List[Sequence[AnalysisResult]] = [ 358 ['Per-step metrics:'] 359 ] 360 # One row per exploration step; one column per tool 361 columns = ['Step ↓/Metric →'] 362 stepRows.append(columns) 363 for i, situation in enumerate(exploration): 364 row = [i] 365 stepRows.append(row) 366 for tool in STEP_ANALYSIS_TOOLS: 367 if (applyTools is None) or (tool in applyTools): 368 if i == 0: 369 columns.append(tool) 370 row.append(STEP_ANALYSIS_TOOLS[tool](situation)) 371 372 stepwiseRows: List[Sequence[AnalysisResult]] = [ 373 ['Per-decision-per-step, metrics (one table per metric):'] 374 ] 375 # For each tool; one row per exploration step and one column per 376 # decision 377 decisionList = exploration.allDecisions() 378 columns = ['Step ↓/Decision →'] + decisionList 379 identities = ['Decision names:'] + [ 380 analysis.lastIdentity(exploration, d) 381 for d in decisionList 382 ] 383 for tool in STEPWISE_DECISION_ANALYSIS_TOOLS: 384 if (applyTools is None) or (tool in applyTools): 385 stepwiseRows.append([tool]) 386 stepwiseRows.append(columns) 387 stepwiseRows.append(identities) 388 for i, situation in enumerate(exploration): 389 row = [i] 390 stepwiseRows.append(row) 391 for decision in decisionList: 392 row.append( 393 STEPWISE_DECISION_ANALYSIS_TOOLS[tool]( 394 situation, 395 decision 396 ) 397 ) 398 399 # Build a grid containing just the non-empty analysis categories, so 400 # that if you deselect some tools you get a smaller CSV file: 401 grid: List[Sequence[AnalysisResult]] = [] 402 if len(wholeRows) > 1: 403 grid.extend(wholeRows) 404 for block in decisionRows, stepRows, stepwiseRows: 405 if len(block) > 1: 406 if grid: 407 grid.append([]) # spacer 408 grid.extend(block) 409 410 # Figure out our destination stream: 411 if destination is None: 412 outStream = sys.stdout 413 closeIt = False 414 else: 415 outStream = open(destination, 'w') 416 closeIt = True 417 418 # Create a CSV writer for our stream 419 writer = csv.writer(outStream) 420 421 # Write out our grid to the file 422 try: 423 writer.writerows(grid) 424 finally: 425 if closeIt: 426 outStream.close() 427 428 429def convert( 430 source: pathlib.Path, 431 destination: pathlib.Path, 432 inputFormatOverride: Optional[SourceType] = None, 433 outputFormatOverride: Optional[SourceType] = None, 434 step: int = -1 435) -> None: 436 """ 437 Converts between exploration and graph formats. By default, formats 438 are determined by file extensions, but using the `--format` and 439 `--output-format` options can override this. The available formats 440 are: 441 442 - '.dcg' A `core.DecisionGraph` stored in JSON format. 443 - '.dot' A `core.DecisionGraph` stored as a GraphViz DOT file. 444 - '.exp' A `core.DiscreteExploration` stored in JSON format. 445 - '.exj' A `core.DiscreteExploration` stored as a journal (see 446 `journal.JournalObserver`; TODO: writing this format). 447 448 When converting a decision graph into an exploration format, the 449 resulting exploration will have a single starting step containing 450 the entire specified graph. When converting an exploration into a 451 decision graph format, only the current graph will be saved, unless 452 `--step` is used to specify a different step index to save. 453 """ 454 # TODO journal writing 455 obj = loadSource(source, inputFormatOverride) 456 457 if outputFormatOverride is None: 458 outputFormat = determineFileType(str(destination)) 459 else: 460 outputFormat = outputFormatOverride 461 462 if outputFormat in ("graph", "dot"): 463 if isinstance(obj, core.DiscreteExploration): 464 graph = obj.getSituation(step).graph 465 else: 466 graph = obj 467 if outputFormat == "graph": 468 saveDecisionGraph(destination, graph) 469 else: 470 saveDotFile(destination, graph) 471 else: 472 if isinstance(obj, core.DecisionGraph): 473 exploration = core.DiscreteExploration.fromGraph(obj) 474 else: 475 exploration = obj 476 if outputFormat == "exploration": 477 saveExploration(destination, exploration) 478 else: 479 saveAsJournal(destination, exploration) 480 481 482INSPECTOR_HELP = """ 483Available commands: 484 485- 'help' or '?': List commands. 486- 'done', 'quit', 'q', or 'exit': Quit the inspector. 487- 'f' or 'follow': Follow the primary decision when changing steps. Also 488 changes to that decision immediately. Toggles off if on. 489- 'cd' or 'goto': Change focus decision to the named decision. Cancels 490 follow mode. 491- 'ls' or 'list' or 'destinations': Lists transitions at this decision 492 and their destinations, as well as any mechanisms at this decision. 493- 'lst' or 'steps': Lists each step of the exploration along with the 494 primary decision at each step. 495- 'st' or 'step': Switches to the specified step (an index) 496- 'n' or 'next': Switches to the next step. 497- 'p' or 'prev' or 'previous': Switches to the previous step. 498- 't' or 'take': Change focus decision to the decision which is the 499 destination of the specified transition at the current focused 500 decision. 501- 'prm' or 'primary': Displays the current primary decision. 502- 'a' or 'active': Lists all currently active decisions 503- 'u' or 'unexplored': Lists all unexplored transitions at the current 504 step. 505- 'x' or 'explorable': Lists all unexplored transitions at the current 506 step which are traversable based on the current state. (TODO: 507 make this more accurate). 508- 'r' or 'reachable': TODO 509- 'A' or 'all': Lists all decisions at the current step. 510- 'M' or 'mechanisms': Lists all mechanisms at the current step. 511""" 512 513 514def inspect( 515 source: pathlib.Path, 516 formatOverride: Optional[SourceType] = None 517) -> None: 518 """ 519 Inspects the graph or exploration stored in the `source` file, 520 launching an interactive command line for inspecting properties of 521 decisions, transitions, and situations. The file extension is used 522 to determine how to load the data, although the `--format` option 523 may override this. '.dcg' files are assumed to be decision graphs in 524 JSON format, '.exp' files are assumed to be exploration objects in 525 JSON format, and '.exj' files are assumed to be exploration journals 526 in the default journal format. If the object that gets loaded is a 527 graph, a 1-step exploration containing just that graph will be 528 created to inspect. Inspector commands are listed in the 529 `INSPECTOR_HELP` variable. 530 """ 531 print(f"Loading exploration from {source!r}...") 532 # Load our exploration 533 exploration = loadSource(source, formatOverride) 534 if isinstance(exploration, core.DecisionGraph): 535 exploration = core.DiscreteExploration.fromGraph(exploration) 536 537 print( 538 f"Inspecting exploration with {len(exploration)} step(s) and" 539 f" {len(exploration.allDecisions())} decision(s):" 540 ) 541 print("('h' for help)") 542 543 # Set up tracking variables: 544 step = len(exploration) - 1 545 here: Optional[base.DecisionID] = exploration.primaryDecision(step) 546 graph = exploration.getSituation(step).graph 547 follow = True 548 549 pf = parsing.ParseFormat() 550 551 if here is None: 552 print("Note: There are no decisions in the final graph.") 553 554 while True: 555 # Re-establish the prompt 556 prompt = "> " 557 if here is not None and here in graph: 558 prompt = graph.identityOf(here) + "> " 559 elif here is not None: 560 prompt = f"{here} (?)> " 561 562 # Prompt for the next command 563 fullCommand = input(prompt).split() 564 565 # Track number of invalid commands so we can quit after 10 in a row 566 invalidCommands = 0 567 568 if len(fullCommand) == 0: 569 cmd = '' 570 args = '' 571 else: 572 cmd = fullCommand[0] 573 args = ' '.join(fullCommand[1:]) 574 575 # Do what the command says 576 invalid = False 577 if cmd in ("help", '?'): 578 # Displays help message 579 if len(args.strip()) > 0: 580 print("(help does not accept any arguments)") 581 print(INSPECTOR_HELP) 582 elif cmd in ("done", "exit", "quit", "q"): 583 # Exits the inspector 584 if len(args.strip()) > 0: 585 print("(quit does not accept any arguments)") 586 print("Bye.") 587 break 588 elif cmd in ("f", "follow"): 589 if follow: 590 follow = False 591 print("Stopped following") 592 else: 593 follow = True 594 here = exploration.primaryDecision(step) 595 print(f"Now following at: {graph.identityOf(here)}") 596 elif cmd in ("cd", "goto"): 597 # Changes focus to a specific decision 598 try: 599 target = pf.parseDecisionSpecifier(args) 600 target = graph.resolveDecision(target) 601 here = target 602 follow = False 603 print(f"now at: {graph.identityOf(target)}") 604 except Exception: 605 print("(invalid decision specifier)") 606 elif cmd in ("ls", "list", "destinations"): 607 fromID: Optional[base.AnyDecisionSpecifier] = None 608 if args.strip(): 609 fromID = pf.parseDecisionSpecifier(args) 610 fromID = graph.resolveDecision(fromID) 611 else: 612 fromID = here 613 614 if fromID is None: 615 print( 616 "(no focus decision and no decision specified;" 617 " nothing to list; use 'cd' to specify a decision," 618 " or 'all' to list all decisions)" 619 ) 620 else: 621 outgoing = graph.destinationsFrom(fromID) 622 info = graph.identityOf(fromID) 623 if len(outgoing) > 0: 624 print(f"Destinations from {info}:") 625 print(graph.destinationsListing(outgoing)) 626 else: 627 print("No outgoing transitions from {info}.") 628 elif cmd in ("lst", "steps"): 629 total = len(exploration) 630 print(f"{total} step(s):") 631 for step in range(total): 632 pr = exploration.primaryDecision(step) 633 situ = exploration.getSituation(step) 634 stGraph = situ.graph 635 identity = stGraph.identityOf(pr) 636 print(f" {step} at {identity}") 637 print(f"({total} total step(s))") 638 elif cmd in ("st", "step"): 639 stepTo = int(args.strip()) 640 if stepTo < 0: 641 stepTo += len(exploration) 642 if stepTo < 0: 643 print( 644 f"Invalid step {args!r} (too negative; min is" 645 f" {-len(exploration)})" 646 ) 647 if stepTo >= len(exploration): 648 print( 649 f"Invalid step {args!r} (too large; max is" 650 f" {len(exploration) - 1})" 651 ) 652 653 step = stepTo 654 graph = exploration.getSituation(step).graph 655 if follow: 656 here = exploration.primaryDecision(step) 657 print(f"Followed to: {graph.identityOf(here)}") 658 elif cmd in ("n", "next"): 659 if step == -1 or step >= len(exploration) - 2: 660 print("Can't step beyond the last step.") 661 else: 662 step += 1 663 graph = exploration.getSituation(step).graph 664 if here not in graph: 665 here = None 666 print(f"At step {step}") 667 if follow: 668 here = exploration.primaryDecision(step) 669 print(f"Followed to: {graph.identityOf(here)}") 670 elif cmd in ("p", "prev"): 671 if step == 0 or step <= -len(exploration) + 2: 672 print("Can't step before the first step.") 673 else: 674 step -= 1 675 graph = exploration.getSituation(step).graph 676 if here not in graph: 677 here = None 678 print(f"At step {step}") 679 if follow: 680 here = exploration.primaryDecision(step) 681 print(f"Followed to: {graph.identityOf(here)}") 682 elif cmd in ("t", "take"): 683 if here is None: 684 print( 685 "(no focus decision, so can't take transitions. Use" 686 " 'cd' to specify a decision first.)" 687 ) 688 else: 689 dest = graph.getDestination(here, args) 690 if dest is None: 691 print( 692 f"Invalid transition {args!r} (no destination for" 693 f" that transition from {graph.identityOf(here)}" 694 ) 695 here = dest 696 elif cmd in ("prm", "primary"): 697 pr = exploration.primaryDecision(step) 698 if pr is None: 699 print("Step {step} has no primary decision") 700 else: 701 print( 702 f"Primary decision for step {step} is:" 703 f" {graph.identityOf(pr)}" 704 ) 705 elif cmd in ("a", "active"): 706 active = exploration.getActiveDecisions(step) 707 print(f"Active decisions at step {step}:") 708 print(graph.namesListing(active)) 709 elif cmd in ("u", "unexplored"): 710 unx = analysis.unexploredBranches(graph) 711 fin = ':' if len(unx) > 0 else '.' 712 print(f"{len(unx)} unexplored branch(es){fin}") 713 for frID, unTr in unx: 714 print(f"take {unTr} at {graph.identityOf(frID)}") 715 elif cmd in ("x", "explorable"): 716 ctx = base.genericContextForSituation( 717 exploration.getSituation(step) 718 ) 719 unx = analysis.unexploredBranches(graph, ctx) 720 fin = ':' if len(unx) > 0 else '.' 721 print(f"{len(unx)} unexplored branch(es){fin}") 722 for frID, unTr in unx: 723 print(f"take {unTr} at {graph.identityOf(frID)}") 724 elif cmd in ("r", "reachable"): 725 print("TODO: Reachable does not work yet.") 726 elif cmd in ("A", "all"): 727 print( 728 f"There are {len(graph)} decision(s) at step {step}:" 729 ) 730 for decision in graph.nodes(): 731 print(f" {graph.identityOf(decision)}") 732 elif cmd in ("M", "mechanisms"): 733 count = len(graph.mechanisms) 734 fin = ':' if count > 0 else '.' 735 print( 736 f"There are {count} mechanism(s) at step {step}{fin}" 737 ) 738 for mID in graph.mechanisms: 739 where, name = graph.mechanisms[mID] 740 state = exploration.mechanismState(mID, step=step) 741 if where is None: 742 print(f" {name!r} (global) in state {state!r}") 743 else: 744 info = graph.identityOf(where) 745 print(f" {name!r} at {info} in state {state!r}") 746 else: 747 invalid = True 748 749 if invalid: 750 if invalidCommands >= 10: 751 print("Too many invalid commands; exiting.") 752 break 753 else: 754 if invalidCommands >= 8: 755 print("{invalidCommands} invalid commands so far,") 756 print("inspector will stop after 10 invalid commands...") 757 print(f"Unknown command {cmd!r}...") 758 invalidCommands += 1 759 print(INSPECTOR_HELP) 760 else: 761 invalidCommands = 0 762 763 764#--------------# 765# Parser setup # 766#--------------# 767 768parser = argparse.ArgumentParser( 769 prog="python -m exploration", 770 description="""\ 771Runs various commands for processing exploration graphs and journals, 772and for converting between them or displaying them in various formats. 773""" 774) 775subparsers = parser.add_subparsers( 776 title="commands", 777 description="The available commands are:", 778 help="use these with -h/--help for more details" 779) 780 781showParser = subparsers.add_parser( 782 'show', 783 help="show an exploration", 784 description=textwrap.dedent(str(show.__doc__)).strip() 785) 786showParser.set_defaults(run="show") 787showParser.add_argument( 788 "source", 789 type=pathlib.Path, 790 help="The file to load" 791) 792showParser.add_argument( 793 '-f', 794 "--format", 795 choices=get_args(SourceType), 796 help=( 797 "Which format the source file is in (normally that can be" 798 " determined from the file extension)." 799 ) 800) 801showParser.add_argument( 802 '-s', 803 "--step", 804 type=int, 805 default=-1, 806 help="Which graph step to show (when loading an exploration)." 807) 808 809analyzeParser = subparsers.add_parser( 810 'analyze', 811 help="analyze an exploration", 812 description=textwrap.dedent(str(analyze.__doc__)).strip() 813) 814analyzeParser.set_defaults(run="analyze") 815analyzeParser.add_argument( 816 "source", 817 type=pathlib.Path, 818 help="The file holding the exploration to analyze" 819) 820analyzeParser.add_argument( 821 "destination", 822 default=None, 823 type=pathlib.Path, 824 help=( 825 "The file name where the output should be written (this file" 826 " will be overwritten without warning)." 827 ) 828) 829analyzeParser.add_argument( 830 '-f', 831 "--format", 832 choices=get_args(SourceType), 833 help=( 834 "Which format the source file is in (normally that can be" 835 " determined from the file extension)." 836 ) 837) 838 839convertParser = subparsers.add_parser( 840 'convert', 841 help="convert an exploration", 842 description=textwrap.dedent(str(convert.__doc__)).strip() 843) 844convertParser.set_defaults(run="convert") 845convertParser.add_argument( 846 "source", 847 type=pathlib.Path, 848 help="The file holding the graph or exploration to convert." 849) 850convertParser.add_argument( 851 "destination", 852 type=pathlib.Path, 853 help=( 854 "The file name where the output should be written (this file" 855 " will be overwritten without warning)." 856 ) 857) 858convertParser.add_argument( 859 '-f', 860 "--format", 861 choices=get_args(SourceType), 862 help=( 863 "Which format the source file is in (normally that can be" 864 " determined from the file extension)." 865 ) 866) 867convertParser.add_argument( 868 '-o', 869 "--output-format", 870 choices=get_args(SourceType), 871 help=( 872 "Which format the converted file should be saved as (normally" 873 " that is determined from the file extension)." 874 ) 875) 876convertParser.add_argument( 877 '-s', 878 "--step", 879 type=int, 880 default=-1, 881 help=( 882 "Which graph step to save (when converting from an exploration" 883 " format to a graph format)." 884 ) 885) 886 887if __name__ == "__main__": 888 options = parser.parse_args() 889 if options.run == "show": 890 show( 891 options.source, 892 formatOverride=options.format, 893 step=options.step 894 ) 895 elif options.run == "analyze": 896 analyze( 897 options.source, 898 formatOverride=options.format, 899 destination=options.destination 900 ) 901 elif options.run == "convert": 902 convert( 903 options.source, 904 options.destination, 905 inputFormatOverride=options.format, 906 outputFormatOverride=options.output_format, 907 step=options.step 908 ) 909 else: 910 raise RuntimeError( 911 f"Invalid 'run' default value: '{options.run}'." 912 )
The file types we recognize.
45def determineFileType(filename: str) -> SourceType: 46 if filename.endswith('.dcg'): 47 return 'graph' 48 elif filename.endswith('.dot'): 49 return 'dot' 50 elif filename.endswith('.exp'): 51 return 'exploration' 52 elif filename.endswith('.exj'): 53 return 'journal' 54 else: 55 raise ValueError( 56 f"Could not determine the file type of file '{filename}':" 57 f" it does not end with '.dcg', '.dot', '.exp', or '.exj'." 58 )
61def loadDecisionGraph(path: pathlib.Path) -> core.DecisionGraph: 62 """ 63 Loads a JSON-encoded decision graph from a file. The extension 64 should normally be '.dcg'. 65 """ 66 with path.open('r', encoding='utf-8-sig') as fInput: 67 return parsing.loadCustom(fInput, core.DecisionGraph)
Loads a JSON-encoded decision graph from a file. The extension should normally be '.dcg'.
70def saveDecisionGraph( 71 path: pathlib.Path, 72 graph: core.DecisionGraph 73) -> None: 74 """ 75 Saves a decision graph encoded as JSON in the specified file. The 76 file should normally have a '.dcg' extension. 77 """ 78 with path.open('w', encoding='utf-8') as fOutput: 79 parsing.saveCustom(graph, fOutput)
Saves a decision graph encoded as JSON in the specified file. The file should normally have a '.dcg' extension.
82def loadDotFile(path: pathlib.Path) -> core.DecisionGraph: 83 """ 84 Loads a `core.DecisionGraph` form the file at the specified path 85 (whose extension should normally be '.dot'). The file format is the 86 GraphViz "dot" format. 87 """ 88 with path.open('r', encoding='utf-8-sig') as fInput: 89 dot = fInput.read() 90 try: 91 return parsing.parseDot(dot) 92 except parsing.DotParseError: 93 raise parsing.DotParseError( 94 "Failed to parse Dot file contents:\n\n" 95 + dot 96 + "\n\n(See error above for specific parsing issue.)" 97 )
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.
100def saveDotFile(path: pathlib.Path, graph: core.DecisionGraph) -> None: 101 """ 102 Saves a `core.DecisionGraph` as a GraphViz "dot" file. The file 103 extension should normally be ".dot". 104 """ 105 dotStr = parsing.toDot(graph, clusterLevels=[]) 106 with path.open('w', encoding='utf-8') as fOutput: 107 fOutput.write(dotStr)
Saves a core.DecisionGraph
as a GraphViz "dot" file. The file
extension should normally be ".dot".
110def loadExploration(path: pathlib.Path) -> core.DiscreteExploration: 111 """ 112 Loads a JSON-encoded `core.DiscreteExploration` object from the file 113 at the specified path. The extension should normally be '.exp'. 114 """ 115 with path.open('r', encoding='utf-8-sig') as fInput: 116 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'.
119def saveExploration( 120 path: pathlib.Path, 121 exploration: core.DiscreteExploration 122) -> None: 123 """ 124 Saves a `core.DiscreteExploration` object as JSON in the specified 125 file. The file extension should normally be '.exp'. 126 """ 127 with path.open('w', encoding='utf-8') as fOutput: 128 parsing.saveCustom(exploration, fOutput)
Saves a core.DiscreteExploration
object as JSON in the specified
file. The file extension should normally be '.exp'.
131def loadJournal(path: pathlib.Path) -> core.DiscreteExploration: 132 """ 133 Loads a `core.DiscreteExploration` object from a journal file 134 (extension should normally be '.exj'). Uses the 135 `journal.convertJournal` function. 136 """ 137 with path.open('r', encoding='utf-8-sig') as fInput: 138 return journal.convertJournal(fInput.read())
Loads a core.DiscreteExploration
object from a journal file
(extension should normally be '.exj'). Uses the
journal.convertJournal
function.
141def saveAsJournal( 142 path: pathlib.Path, 143 exploration: core.DiscreteExploration 144) -> None: 145 """ 146 Saves a `core.DiscreteExploration` object as a text journal in the 147 specified file. The file extension should normally be '.exj'. 148 149 TODO: This?! 150 """ 151 raise NotImplementedError( 152 "DiscreteExploration-to-journal conversion is not implemented" 153 " yet." 154 )
Saves a core.DiscreteExploration
object as a text journal in the
specified file. The file extension should normally be '.exj'.
TODO: This?!
157def loadSource( 158 path: pathlib.Path, 159 formatOverride: Optional[SourceType] = None 160) -> Union[core.DecisionGraph, core.DiscreteExploration]: 161 """ 162 Loads either a `core.DecisionGraph` or a `core.DiscreteExploration` 163 from the specified file, depending on its file extension (or the 164 specified format given as `formatOverride` if there is one). 165 """ 166 if formatOverride is not None: 167 format = formatOverride 168 else: 169 format = determineFileType(str(path)) 170 171 if format == "graph": 172 return loadDecisionGraph(path) 173 if format == "dot": 174 return loadDotFile(path) 175 elif format == "exploration": 176 return loadExploration(path) 177 elif format == "journal": 178 return loadJournal(path) 179 else: 180 raise ValueError( 181 f"Unrecognized file format '{format}' (recognized formats" 182 f" are 'graph', 'exploration', and 'journal')." 183 )
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).
A type alias for values we're willing to accept as analysis results. These are going to be written to a CSV file that we want to be human-readable.
The analysis functions to apply to each decision in each step when analyzing an exploration, and the names for each.
The analysis functions to apply to each step when analyzing an exploration, and the names for each.
The analysis functions to apply once to each decision in an exploration, and the names for each.
The analysis functions to apply to entire explorations, and the names for each.
271def show( 272 source: pathlib.Path, 273 formatOverride: Optional[SourceType] = None, 274 step: int = -1 275) -> None: 276 """ 277 Shows the graph or exploration stored in the `source` file. You will 278 need to have the `matplotlib` library installed. Consider using the 279 interactive interface provided by the `explorationViewer` module 280 instead. The file extension is used to determine how to load the data, 281 although the `--format` option may override this. '.dcg' files are 282 assumed to be decision graphs in JSON format, '.exp' files are assumed 283 to be exploration objects in JSON format, and '.exj' files are assumed 284 to be exploration journals in the default journal format. If the object 285 that gets loaded is an exploration, the final graph for that 286 exploration will be displayed, or a specific graph may be selected 287 using `--step`. 288 """ 289 obj = loadSource(source, formatOverride) 290 if isinstance(obj, core.DiscreteExploration): 291 obj = obj.getSituation(step).graph 292 293 import matplotlib.pyplot # type: ignore 294 295 # This draws the graph in a new window that pops up. You have to close 296 # the window to end the program. 297 nx.draw(obj) 298 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
.
301def analyze( 302 source: pathlib.Path, 303 formatOverride: Optional[SourceType] = None, 304 destination: Optional[pathlib.Path] = None, 305 applyTools: Optional[List[str]] = None 306) -> None: 307 """ 308 Analyzes the exploration stored in the `source` file. The file 309 extension is used to determine how to load the data, although this 310 may be overridden by the `--format` option. Normally, '.exp' files 311 are treated as JSON-encoded exploration objects, while '.exj' files 312 are treated as journals using the default journal format. 313 314 This applies a number of analysis functions to produce a CSV file 315 showing per-decision-per-step, per-decision, per-step, and 316 per-exploration metrics. A subset of the available metrics may be 317 selected by passing a list of strings for the `applyTools` argument. 318 See the `STEPWISE_DECISION_ANALYSIS_TOOLS`, `STEP_ANALYSIS_TOOLS`, 319 `DECISION_ANALYSIS_TOOLS`, and `WHOLE_ANALYSIS_TOOLS` dictionaries 320 for tool names. 321 322 If no output file is specified, the output will be printed out. 323 """ 324 # Load our source exploration object: 325 obj = loadSource(source, formatOverride) 326 if isinstance(obj, core.DecisionGraph): 327 obj = core.DiscreteExploration.fromGraph(obj) 328 329 exploration: core.DiscreteExploration = obj 330 331 # Apply all of the analysis functions (or only just those that are 332 # selected using applyTools): 333 334 wholeRows: List[List[AnalysisResult]] = [['Whole exploration metrics:']] 335 # One row per tool 336 for tool in WHOLE_ANALYSIS_TOOLS: 337 if (applyTools is None) or (tool in applyTools): 338 wholeRows.append( 339 [tool, WHOLE_ANALYSIS_TOOLS[tool](exploration)] 340 ) 341 342 decisionRows: List[Sequence[AnalysisResult]] = [ 343 ['Per-decision metrics:'] 344 ] 345 # One row per tool; one column per decision 346 decisionList = exploration.allDecisions() 347 columns = ['Metric ↓/Decision →'] + decisionList 348 decisionRows.append(columns) 349 for tool in DECISION_ANALYSIS_TOOLS: 350 if (applyTools is None) or (tool in applyTools): 351 row: List[AnalysisResult] = [tool] 352 decisionRows.append(row) 353 for decision in decisionList: 354 row.append( 355 DECISION_ANALYSIS_TOOLS[tool](exploration, decision) 356 ) 357 358 stepRows: List[Sequence[AnalysisResult]] = [ 359 ['Per-step metrics:'] 360 ] 361 # One row per exploration step; one column per tool 362 columns = ['Step ↓/Metric →'] 363 stepRows.append(columns) 364 for i, situation in enumerate(exploration): 365 row = [i] 366 stepRows.append(row) 367 for tool in STEP_ANALYSIS_TOOLS: 368 if (applyTools is None) or (tool in applyTools): 369 if i == 0: 370 columns.append(tool) 371 row.append(STEP_ANALYSIS_TOOLS[tool](situation)) 372 373 stepwiseRows: List[Sequence[AnalysisResult]] = [ 374 ['Per-decision-per-step, metrics (one table per metric):'] 375 ] 376 # For each tool; one row per exploration step and one column per 377 # decision 378 decisionList = exploration.allDecisions() 379 columns = ['Step ↓/Decision →'] + decisionList 380 identities = ['Decision names:'] + [ 381 analysis.lastIdentity(exploration, d) 382 for d in decisionList 383 ] 384 for tool in STEPWISE_DECISION_ANALYSIS_TOOLS: 385 if (applyTools is None) or (tool in applyTools): 386 stepwiseRows.append([tool]) 387 stepwiseRows.append(columns) 388 stepwiseRows.append(identities) 389 for i, situation in enumerate(exploration): 390 row = [i] 391 stepwiseRows.append(row) 392 for decision in decisionList: 393 row.append( 394 STEPWISE_DECISION_ANALYSIS_TOOLS[tool]( 395 situation, 396 decision 397 ) 398 ) 399 400 # Build a grid containing just the non-empty analysis categories, so 401 # that if you deselect some tools you get a smaller CSV file: 402 grid: List[Sequence[AnalysisResult]] = [] 403 if len(wholeRows) > 1: 404 grid.extend(wholeRows) 405 for block in decisionRows, stepRows, stepwiseRows: 406 if len(block) > 1: 407 if grid: 408 grid.append([]) # spacer 409 grid.extend(block) 410 411 # Figure out our destination stream: 412 if destination is None: 413 outStream = sys.stdout 414 closeIt = False 415 else: 416 outStream = open(destination, 'w') 417 closeIt = True 418 419 # Create a CSV writer for our stream 420 writer = csv.writer(outStream) 421 422 # Write out our grid to the file 423 try: 424 writer.writerows(grid) 425 finally: 426 if closeIt: 427 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.
See the STEPWISE_DECISION_ANALYSIS_TOOLS
, STEP_ANALYSIS_TOOLS
,
DECISION_ANALYSIS_TOOLS
, and WHOLE_ANALYSIS_TOOLS
dictionaries
for tool names.
If no output file is specified, the output will be printed out.
430def convert( 431 source: pathlib.Path, 432 destination: pathlib.Path, 433 inputFormatOverride: Optional[SourceType] = None, 434 outputFormatOverride: Optional[SourceType] = None, 435 step: int = -1 436) -> None: 437 """ 438 Converts between exploration and graph formats. By default, formats 439 are determined by file extensions, but using the `--format` and 440 `--output-format` options can override this. The available formats 441 are: 442 443 - '.dcg' A `core.DecisionGraph` stored in JSON format. 444 - '.dot' A `core.DecisionGraph` stored as a GraphViz DOT file. 445 - '.exp' A `core.DiscreteExploration` stored in JSON format. 446 - '.exj' A `core.DiscreteExploration` stored as a journal (see 447 `journal.JournalObserver`; TODO: writing this format). 448 449 When converting a decision graph into an exploration format, the 450 resulting exploration will have a single starting step containing 451 the entire specified graph. When converting an exploration into a 452 decision graph format, only the current graph will be saved, unless 453 `--step` is used to specify a different step index to save. 454 """ 455 # TODO journal writing 456 obj = loadSource(source, inputFormatOverride) 457 458 if outputFormatOverride is None: 459 outputFormat = determineFileType(str(destination)) 460 else: 461 outputFormat = outputFormatOverride 462 463 if outputFormat in ("graph", "dot"): 464 if isinstance(obj, core.DiscreteExploration): 465 graph = obj.getSituation(step).graph 466 else: 467 graph = obj 468 if outputFormat == "graph": 469 saveDecisionGraph(destination, graph) 470 else: 471 saveDotFile(destination, graph) 472 else: 473 if isinstance(obj, core.DecisionGraph): 474 exploration = core.DiscreteExploration.fromGraph(obj) 475 else: 476 exploration = obj 477 if outputFormat == "exploration": 478 saveExploration(destination, exploration) 479 else: 480 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 (seejournal.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.
515def inspect( 516 source: pathlib.Path, 517 formatOverride: Optional[SourceType] = None 518) -> None: 519 """ 520 Inspects the graph or exploration stored in the `source` file, 521 launching an interactive command line for inspecting properties of 522 decisions, transitions, and situations. The file extension is used 523 to determine how to load the data, although the `--format` option 524 may override this. '.dcg' files are assumed to be decision graphs in 525 JSON format, '.exp' files are assumed to be exploration objects in 526 JSON format, and '.exj' files are assumed to be exploration journals 527 in the default journal format. If the object that gets loaded is a 528 graph, a 1-step exploration containing just that graph will be 529 created to inspect. Inspector commands are listed in the 530 `INSPECTOR_HELP` variable. 531 """ 532 print(f"Loading exploration from {source!r}...") 533 # Load our exploration 534 exploration = loadSource(source, formatOverride) 535 if isinstance(exploration, core.DecisionGraph): 536 exploration = core.DiscreteExploration.fromGraph(exploration) 537 538 print( 539 f"Inspecting exploration with {len(exploration)} step(s) and" 540 f" {len(exploration.allDecisions())} decision(s):" 541 ) 542 print("('h' for help)") 543 544 # Set up tracking variables: 545 step = len(exploration) - 1 546 here: Optional[base.DecisionID] = exploration.primaryDecision(step) 547 graph = exploration.getSituation(step).graph 548 follow = True 549 550 pf = parsing.ParseFormat() 551 552 if here is None: 553 print("Note: There are no decisions in the final graph.") 554 555 while True: 556 # Re-establish the prompt 557 prompt = "> " 558 if here is not None and here in graph: 559 prompt = graph.identityOf(here) + "> " 560 elif here is not None: 561 prompt = f"{here} (?)> " 562 563 # Prompt for the next command 564 fullCommand = input(prompt).split() 565 566 # Track number of invalid commands so we can quit after 10 in a row 567 invalidCommands = 0 568 569 if len(fullCommand) == 0: 570 cmd = '' 571 args = '' 572 else: 573 cmd = fullCommand[0] 574 args = ' '.join(fullCommand[1:]) 575 576 # Do what the command says 577 invalid = False 578 if cmd in ("help", '?'): 579 # Displays help message 580 if len(args.strip()) > 0: 581 print("(help does not accept any arguments)") 582 print(INSPECTOR_HELP) 583 elif cmd in ("done", "exit", "quit", "q"): 584 # Exits the inspector 585 if len(args.strip()) > 0: 586 print("(quit does not accept any arguments)") 587 print("Bye.") 588 break 589 elif cmd in ("f", "follow"): 590 if follow: 591 follow = False 592 print("Stopped following") 593 else: 594 follow = True 595 here = exploration.primaryDecision(step) 596 print(f"Now following at: {graph.identityOf(here)}") 597 elif cmd in ("cd", "goto"): 598 # Changes focus to a specific decision 599 try: 600 target = pf.parseDecisionSpecifier(args) 601 target = graph.resolveDecision(target) 602 here = target 603 follow = False 604 print(f"now at: {graph.identityOf(target)}") 605 except Exception: 606 print("(invalid decision specifier)") 607 elif cmd in ("ls", "list", "destinations"): 608 fromID: Optional[base.AnyDecisionSpecifier] = None 609 if args.strip(): 610 fromID = pf.parseDecisionSpecifier(args) 611 fromID = graph.resolveDecision(fromID) 612 else: 613 fromID = here 614 615 if fromID is None: 616 print( 617 "(no focus decision and no decision specified;" 618 " nothing to list; use 'cd' to specify a decision," 619 " or 'all' to list all decisions)" 620 ) 621 else: 622 outgoing = graph.destinationsFrom(fromID) 623 info = graph.identityOf(fromID) 624 if len(outgoing) > 0: 625 print(f"Destinations from {info}:") 626 print(graph.destinationsListing(outgoing)) 627 else: 628 print("No outgoing transitions from {info}.") 629 elif cmd in ("lst", "steps"): 630 total = len(exploration) 631 print(f"{total} step(s):") 632 for step in range(total): 633 pr = exploration.primaryDecision(step) 634 situ = exploration.getSituation(step) 635 stGraph = situ.graph 636 identity = stGraph.identityOf(pr) 637 print(f" {step} at {identity}") 638 print(f"({total} total step(s))") 639 elif cmd in ("st", "step"): 640 stepTo = int(args.strip()) 641 if stepTo < 0: 642 stepTo += len(exploration) 643 if stepTo < 0: 644 print( 645 f"Invalid step {args!r} (too negative; min is" 646 f" {-len(exploration)})" 647 ) 648 if stepTo >= len(exploration): 649 print( 650 f"Invalid step {args!r} (too large; max is" 651 f" {len(exploration) - 1})" 652 ) 653 654 step = stepTo 655 graph = exploration.getSituation(step).graph 656 if follow: 657 here = exploration.primaryDecision(step) 658 print(f"Followed to: {graph.identityOf(here)}") 659 elif cmd in ("n", "next"): 660 if step == -1 or step >= len(exploration) - 2: 661 print("Can't step beyond the last step.") 662 else: 663 step += 1 664 graph = exploration.getSituation(step).graph 665 if here not in graph: 666 here = None 667 print(f"At step {step}") 668 if follow: 669 here = exploration.primaryDecision(step) 670 print(f"Followed to: {graph.identityOf(here)}") 671 elif cmd in ("p", "prev"): 672 if step == 0 or step <= -len(exploration) + 2: 673 print("Can't step before the first step.") 674 else: 675 step -= 1 676 graph = exploration.getSituation(step).graph 677 if here not in graph: 678 here = None 679 print(f"At step {step}") 680 if follow: 681 here = exploration.primaryDecision(step) 682 print(f"Followed to: {graph.identityOf(here)}") 683 elif cmd in ("t", "take"): 684 if here is None: 685 print( 686 "(no focus decision, so can't take transitions. Use" 687 " 'cd' to specify a decision first.)" 688 ) 689 else: 690 dest = graph.getDestination(here, args) 691 if dest is None: 692 print( 693 f"Invalid transition {args!r} (no destination for" 694 f" that transition from {graph.identityOf(here)}" 695 ) 696 here = dest 697 elif cmd in ("prm", "primary"): 698 pr = exploration.primaryDecision(step) 699 if pr is None: 700 print("Step {step} has no primary decision") 701 else: 702 print( 703 f"Primary decision for step {step} is:" 704 f" {graph.identityOf(pr)}" 705 ) 706 elif cmd in ("a", "active"): 707 active = exploration.getActiveDecisions(step) 708 print(f"Active decisions at step {step}:") 709 print(graph.namesListing(active)) 710 elif cmd in ("u", "unexplored"): 711 unx = analysis.unexploredBranches(graph) 712 fin = ':' if len(unx) > 0 else '.' 713 print(f"{len(unx)} unexplored branch(es){fin}") 714 for frID, unTr in unx: 715 print(f"take {unTr} at {graph.identityOf(frID)}") 716 elif cmd in ("x", "explorable"): 717 ctx = base.genericContextForSituation( 718 exploration.getSituation(step) 719 ) 720 unx = analysis.unexploredBranches(graph, ctx) 721 fin = ':' if len(unx) > 0 else '.' 722 print(f"{len(unx)} unexplored branch(es){fin}") 723 for frID, unTr in unx: 724 print(f"take {unTr} at {graph.identityOf(frID)}") 725 elif cmd in ("r", "reachable"): 726 print("TODO: Reachable does not work yet.") 727 elif cmd in ("A", "all"): 728 print( 729 f"There are {len(graph)} decision(s) at step {step}:" 730 ) 731 for decision in graph.nodes(): 732 print(f" {graph.identityOf(decision)}") 733 elif cmd in ("M", "mechanisms"): 734 count = len(graph.mechanisms) 735 fin = ':' if count > 0 else '.' 736 print( 737 f"There are {count} mechanism(s) at step {step}{fin}" 738 ) 739 for mID in graph.mechanisms: 740 where, name = graph.mechanisms[mID] 741 state = exploration.mechanismState(mID, step=step) 742 if where is None: 743 print(f" {name!r} (global) in state {state!r}") 744 else: 745 info = graph.identityOf(where) 746 print(f" {name!r} at {info} in state {state!r}") 747 else: 748 invalid = True 749 750 if invalid: 751 if invalidCommands >= 10: 752 print("Too many invalid commands; exiting.") 753 break 754 else: 755 if invalidCommands >= 8: 756 print("{invalidCommands} invalid commands so far,") 757 print("inspector will stop after 10 invalid commands...") 758 print(f"Unknown command {cmd!r}...") 759 invalidCommands += 1 760 print(INSPECTOR_HELP) 761 else: 762 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.