Skip to content
Tyler edited this page Aug 24, 2023 · 4 revisions

Basic Concepts in AnyLogic <-> Python Communication

This section covers the primary ways of communicating between your AnyLogic model and Python. Many of the same concepts are also covered (and made into a hands-on tutorial) by the provided example model "Basic Functionality (Interactive)", downloadable from the releases page.

Overview: Pypeline gives you the ability to communicate with Python interactively (i.e., like in a standard REPL shell) or non-interactively (i.e., in a one-off script mode).

Note that the usage instructions below are written using the most verbose workflow in order to most fully explain all the concepts. There are more direct and compact ways to accomplish the same tasks (the recommended workflow) which utilize "shortcut" variants of the functions; these are detailed in their own section.

Interactive Communication

Each instance of a (assumed properly configured) PyCommunicator object within your model has an independent interactive Python environment associated with it. Within this environment, you can execute Python code in the same way as if you opened a command prompt and launched the Python interpreter in Interactive Mode (read more: Python documentation). Communication to and from this environment from your model happens through 2 basic functions:

  • run: used for 1-directional commands for Python to execute (e.g., imports, variable declarations/updates)

  • runResults: used for 2-directional statements for Python to evaluate and return the results of (e.g., retrieving variable values, function outputs)

Each take an argument of one or more strings. When multiple strings are passed at once, they are treated as being on their own line. This is useful for tasks like importing multiple libraries in one call or writing loops. Some generic examples are shown in the images below:

Basic usage-Run example 1

Basic usage-Run example 2

Note: for multi-lined code, it does not matter how many spaces/tabs are used to indent; it only matters that the indentation is consistent (e.g., a double indentation should be twice the number of spaces/tabs as the first).

With this specific usage, both run and runResults return a special type called Attempt; handling and parsing the output is discussed in the relevant section below.

Non-Interactive Communication

By "non-interactive", this is referring to the 'traditional' way of using Python - that is, when you have a Python file that is written to be executed possibly with some arguments, to perform some action and possibly output some result. Afterwards, whatever variables or other Python objects were accessed are removed from memory. Pypeline allows you to run any script, passing your desired arguments (if any), and retrieving the output back into a type usable by your AnyLogic model. This is achieved by using one of two functions:

  • runFile: used to run a Python script that may or may not output some result

  • runFileHeadless: same as 'runFile', but executes in parallel to your running model; intended when executing long-running scripts that you don't immediately need the result of

If calling either one of these through an instance of the PyCommunicator object (i.e., non-statically), the script is executed independently from the active interactive environment, so they will have no influence or access to it.

Similar to the interactive-related functions, using the inputs as described here will cause runFile to return Attempt type object; runFileHeadless returns a FutureAttempt object. Both are described in the section below.

Example:

Attempt a = pyCommunicator.runFile("adder.py", "--num1", 3, "--num2", 6);
// ... process ...

Parsing the outputs

For run, runResults, and runFile, usage with the default inputs (i.e., code to execute or objects to pass) returns an object of a custom type called Attempt. This type has two primary functions:

  1. isSuccessful()
  • Returns a boolean representing whether the Python call executed without errors
  1. getFeedback([Class])
  • Returns the result or response of the call to Python
  • Calling without any arguments will return a String. If you know the data type of the response in advance, you can optionally pass the class - in place of "[Class]").
  • When commands fail to execute, the error message will be contained in the feedback
  • If using the run command, there will only be any feedback if the command failed to execute

The following is a (very verbose) example of getting a random number between 1 and 6 in Python and retrieving the result in the model, with some error checking at each stage of the communication:

attempt example 1

Note: If you have PyCommunicator's "Throw error on failed attempt" property enabled, you can alternatively just assume it will work and troubleshoot if you receive an error when running the model:

attempt example 2


Specifically for runFileHeadless, it returns an object of the custom type FutureAttempt. Of note are the following functions:

  • isDone()
    • Returns a boolean whether the script has finished running
  • get([long duration, TimeUnits units])
    • Returns an Attempt with the result of the executed
    • If called without any arguments, the Attempt object will have any outputs up until the time you called this function
    • If called with a duration and TimeUnits constant, it will wait that amount of real world time for the script to end before returning the current status (e.g., get(1, SECOND))

Notable function variants (including recommended workflow)

Most functions available have multiple "variants" (i.e., different number and type of arguments that you can pass) to provide shortcuts or other more convenient way of achieving a desired action.

run

For run, if you're only executing a single line of code that you're looking to pass some objects from your model with, you can use the following variant:

  • run(String codeFmt, Object... objs)
    • code should be a string with one or more formatting placeholder strings (%s)
    • objs should have as many objects, as subsequent arguments, for each placeholder you provided.

Using this is helpful to avoid having to do a lot of string concatenation.

In the following example, pressing the button will assign 3 variables in Python (note: ignore the newlines/tabs; they're for readability)

run alt example

runResults

For runResults, one variant allows you to do the exact same thing as described above with run. Both additionally and separately, you can choose to bypass the need to parse an Attempt object by having the expected return class (i.e., type) as the first argument:

  • runResults(Class cls, String code)
  • runResults(Class cls, String codeFmt, Object... objs)

Examples:

int roll = pyCommunicator.runResults(int.class, "random.randint(1,6)");
// imagine the model has two int variables called 'lower' and 'upper'
int roll = pyCommunicator.runResults(int.class, "random.randint(%s,%s)", lower, upper);

runFile

For runFile, you can pass the expected return type as the first argument to bypass the need to assign/parse an Attempt object:

  • runFile(Class cls, Object... args)

Example:

double result = pyCommunicator.runResults(double.class, "my_adder.py", "--num1", 3, "--num2", 6);
// ... process the result ...