forio Toggle navigation

Model Creation in Julia

When you create a model in Julia, you can use it with the Epicenter platform by:

Creating and Uploading your Model Code

Use the Epicenter user interface to write or upload your model code (.jl files) in the Model folder within your project. This folder must contain all of your model's files, including one file that contains your exposed variables and operations.

Understanding the Environment

Epicenter includes a Julia environment for running your models.

Julia Version

The current version of the Epicenter APIs is v2. With v2, Epicenter uses Julia version 0.5.0.

The previous version of the Epicenter APIs is v1. with v1, Epicenter users Julia version 0.3.0.

(For more information, see the version history of the Epicenter APIs.)

Working with Packages

Epicenter includes standard packages from the current Julia version. Additionally, Epicenter includes the Epicenter package, Epicenter. The Epicenter package (using Epicenter) allows you to save variables and register operations for automatic execution.

If your Julia module uses additional packages, use the standard Pkg.add("PackageName") syntax as needed in your Julia code. However, note that this will load the Julia package every time the end user of your project starts a new session. If this is performance prohibitive for you, contact us for information on upgrading your subscription plan to receive a custom image of Julia for your team, with any special libraries built in.

Working with Sub-folders

When you place your model code in the Model folder, the file in which you expose (export) variables and methods should be at the top level. Other files can be in sub-folders or not, based on how you've structured your model.

Using the Epicenter Package

The Epicenter Julia package, Epicenter ("Epicenter.jl") allows you expose variables and operations (functions) from your model to the Epicenter platform. It also allows you to register operations for automatic execution in some cases.

To use the Epicenter Julia package, simply add using Epicenter to your primary module.

Exposing Variables and Operations to the Epicenter Platform

To expose variables and operations from your model.jl file to the Epicenter platform, you need to:

  • Use the Epicenter Julia package (using Epicenter).

  • Use the keyword export to explicitly expose each variable or operation to the Epicenter platform.

  • List the exposed variables and operations all in one file. This primary file is passed as an argument when you create each run. You can have any number of other files as part of your model, but all of your exposed variables and methods should be listed in just one.

  • Ensure that all exposed variables are global, by using the global keyword. Obviously, this does not mean all variables in your model must be global! However, any variables that end users either view or set directly in the user interface must be global.

  • For any variable that you use in an operation, you need to ensure that exposed variables can be updated in the desired way. For any variable that you use in an operation (function or method), you want to make sure that the most current value of that variable is being used in the method.

    The recommended way to do this is to define your variable as part of a complex type.

    The reasons for requiring a complex type here are largely historical, due to how Epicenter needed to accommodate earlier versions of Julia. We anticipate that additional options will be available in the future.

    As an alternative, you can also create an operation with parameters and use that to set any global variable, regardless of whether it is part of a complex type. However, note that the variable will only be updated when the operation is called.

Saving Variables

When you work with Epicenter, metadata about every "run" — the collection of end user interactions with the project and its model — is automatically saved to the Epicenter backend database. However, specific variables and their values are not saved by default. (See more background on Run Persistence.)

Unsaved variables are only available in the current session. (A session lasts for a certain amount of time after the end user has stopped interacting with your project. This amount is set in the Model Session Timeout, in your project's Settings.)

Sometimes this is the behavior you want: often you only need your model to perform calculations and analyses in the current session.

For other simulations or models, you want end users to be able to review information from previous runs, or to resume working with a run they were using earlier. In these cases, you need to save model variables to the Epicenter backend database.

To save variables from a run to the Epicenter platform's backend database from your Julia model:

  1. At the start of your Julia model, make sure to use the Epicenter.jl library.

     using Epicenter
  2. In your Julia model, when you want to save a variable, add the line record(key).

    For example:

     record(:year)

    saves the variable year. Any previous value of year is overwritten once the persistence operation is complete.

    As another example,

     record(:run_results, length(run_results))

    saves the variable run_results[length(run_results)]. Because Julia arrays are 1-based, length(run_results) refers to the last element of the run_results array. So this example is only persisting the last element of the run_results array, and the example is never overwriting anything. Part of your model design needs to be careful thought about what you do and do not need to keep, and what is and is not ok to overwrite.

Notes:

  • Once you've added a record() call that saves a variable, you'll need to make sure the call is executed in your model. For example, if the call is inside an operation (function), you could add the operation to a component (such as a button) in your user interface using Flow.js. Or, you could call the operation directly.

  • You cannot save null values to dicts with record().

Registering Operations for Automatic Execution

Optionally, you can create operations (functions) in your model, then register them so that they are automatically called when a new run is created.

This registration is done using the subscribe function in the Epicenter.jl module. There are three opportunities for registration:

For example, in your Julia model code, you would have:

# the function to initialize the model, can be named anything
initializeModel function()
    ...
end

# the function to reset the model, can be named anything
resetModel function()
    ...
end

# the function to call after a snapshot restore, can be named anything
restoreModel function()
    ...
end

Then, in your primary model file (e.g. MyModel.jl), you register these operations using the subscribe function from Epicenter.jl:

# in MyModel.jl

using Epicenter

Epicenter.subscribe("initialize", initializeModel)
Epicenter.subscribe("reset", resetModel)
Epicenter.subscribe("restore", restoreModel)

Creating the User Interface

Epicenter provides several different APIs, at different levels of abstraction, so you can connect your model's variables and operations with your UI. (Read more about the various options for this.)

Once your model is complete, you can create the user interface for your project.

Accessing and Updating Variables

Once you've decided how to structure your model and appropriately exposed and recorded variables, you need to connect those model variables to your project's user interface.

Note: If your variable is part of a module, remember to include the name of the module when you access the variable. You cannot directly update (PATCH) variables that are part of modules, however, you can update them call calling an operation.

Scalars

To update or reference a scalar variable, use the name of the variable.

For example, suppose in your Julia model you have

Price = 10

If you are using Flow.js, in your project's user interface file this looks like:

<!-- update Price -->
<input type="text" data-f-bind="Price" />

<!-- display Price -->
<span data-f-bind="Price"></span>

And if you are using the Run API, this looks like:

<!-- update Price -->
curl -X PATCH \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables' \
    --header 'Content-Type:application/json' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9' \
    --data '{"Price": 15}'

<!-- display Price; returns 15 -->
curl -G \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables/Price' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9'

Arrays

To update or reference an array element, use square brackets and the index of the array: [n].

For example, suppose in your Julia model you have

salesByQuarter = [100, 110, 120, 130]

If you are using Flow.js, by default the final value of the array is selected. You can also specify a particular element:

<!-- salesByQuarter, update last element of the array -->
<input data-f-bind="salesByQuarter"></input>

<!-- salesByQuarter, display second element of the array -->
<span data-f-bind="salesByQuarter[2]"></span>

You can list values of an array (that is, your variable over all steps) using the data-f-foreach attribute. You can also use converters to access common array elements (first, last, previous, etc.).

And if you are using the Run API, you need to include the array index. This looks like:

<!--- update salesByQuarter[3] -->
curl -X PATCH \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables' \
    --header 'Content-Type:application/json' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9' \
    --data '{"salesByQuarter[3]": 220}'

<!-- update salesByQuarter entire array: must be done one element at a time -->
curl -X PATCH \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables' \
    --header 'Content-Type:application/json' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9' \
    --data '{"salesByQuarter[1]": 500,  "salesByQuarter[2]": 600, "salesByQuarter[3]": 700, "salesByQuarter[4]": 800}'

<!-- display salesByQuarter[2]; returns 600 -->
curl -G \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables/salesByQuarter[2]' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9'

<!-- display salesByQuarter; returns the entire array: 
    [ 500, 600, 700, 800 ]
-->
curl -G \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables/salesByQuarter' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9'        

Tuples

To reference tuples, use square brackets and the index of the tuple element: [n]. You cannot update tuples; they are immutable in Julia.

For example, suppose in your Julia model you have:

sampleTuple = (10,9,8)

If you are using Flow.js, you can set or reference the value for a single tuple element using data-f-bind:

<!-- display sampleTuple[1]; returns 1 -->
<span data-f-bind="sampleTuple[1]"></span>

If you want to display the entire tuple, use the data-f-foreach attribute instead:

<!-- displays a list, one list item for each tuple element
<ul data-f-foreach="sampleTuple">
    <li></li>
</ul>

See more details on using data-f-foreach.

And if you are using the Run API, this looks like:

<!-- display sampleTuple[1]; returns 1 -->
curl -G \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables/sampleTuple[1]' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9'

<!-- display sampleTuple; returns the entire tuple:
    [ 10, 9, 8 ]
-->
curl -G \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables/sampleTuple' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9'

Objects

To update or reference a field in an object, use dot notation: . followed by the name of the field.

For example, suppose in your Julia model you have the object:

type mgrType
  num_reports::Int
  name::String
  region::String
  mgrType(; num_reports = 0, name = "", region = "") = new(num_reports,name, region)
end

salesMgr = mgrType()
  salesMgr.num_reports = 2
  salesMgr.name = "John Smith"
  salesMgr.region = "West"

If you are using Flow.js, this looks like:

<!-- display salesMgr.region; returns West -->
<span data-f-bind="salesMgr.region"></span>

<!-- update salesMgr.region -->
<input type="text" data-f-bind="salesMgr.region" />

And if you are using the Run API, this looks like:

<!-- returns "West" -->
curl -G \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables/salesMgr.region' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9'

<!-- 
    returns the entire salesMgr object: 
    { "num_reports": 2, "name": "John Smith", "region": "West" }
-->
curl -G \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables/salesMgr' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9'

<!-- update salesMgr.region -->
curl -X PATCH \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables' \
    --header 'Content-Type:application/json' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9' \
    --data '{"salesMgr.region": "South"}'

<!-- update salesMgr, all fields -->
curl -X PATCH \
    'https://api.forio.com/v2/run/acme-simulations/sample-julia-model/000001578cc265b4bd87f480f70557988008/variables' \
    --header 'Content-Type:application/json' \
    --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9' \
    --data '{"salesMgr.region": "South", "salesMgr.num_reports": 3, "salesMgr.name": "Jane Doe" }'

Note that because the variable salesMgr is of a defined type in your Julia model, you cannot add additional fields to it; you can only update existing fields.

IMPORTANT: Regardless of how you reference model variables, remember that the best way to update variables is using objects and operations, to ensure that variables can be updated and kept in sync as desired. If you are using Flow.js, consider calling an operation (e.g. with data-f-on-click) rather than using an input element with data-f-bind.

Calling Operations

You can call any operation (function) in that is exposed (using export) in your Julia model from your project's user interface.

For example, in Flow.js, add the operation to an element on the page, or call it when the run is created:

<button data-f-on-click="advanceMyModel(1)">Advance One Step</button>

In addition to the operations that you have defined yourself, you can also call the operation reset():

  • This operation takes no arguments.

  • This operation returns a new run, that is, a new instantiation of your model for an end user to play with. (Remember that a run is a collection of interactions with a model.) For example, this is useful if your end user needs to "start over" to create a new scenario within your project.

  • Note that the reset() operation can ONLY be called from your project's user interface if you are using Flow.js. (Under the hood, this is not an operation as defined for the RESTful Run API. If you are building your project's user interface with the Epicenter RESTful APIs, you should just create a new run instead of calling reset().)