Skip to content

Running a model

An online queue is used to coordinate the task of formulating and solving mathematical programs. Users submit parameters to the queue which creates a new 'job'. A pool of 'workers' monitor this queue for new jobs. If a worker is available (i.e. it is not currently processing a job) it will pick the job off the queue and then formulate and solve a mathematical program using the inputs provided. The worker then posts the optimisation model's results back to the queue, and marks the job as 'finished'. Results can then be accessed by the user.

This notebook describes how to submit a case file parameters and retrieve results from the queue.

Imports and authentication

import os
import json

import requests
import xmltodict
import pandas as pd

import IPython.display as display

First specify the base URL for API calls.

# Base URL endpoint for the Dispatch API
base_url = 'http://nemde-api-host:8080/api/v1/'

Load case file

Case files contain parameters used by the NEMDE when determining dispatch targets for generators and loads. The API uses selected inputs from these case files when formulating a mathematical program that approximates the NEMDE's operation (see the parameter reference page for a description of the inputs used).

Each case file is identified by a case ID, e.g. 20201101001, which has the following format:

{year}{month}{day}{interval_id}

Interval IDs range from 001-288 and are used to identify each 5 minute interval within a 24 hour period.

As each trading day begins at 4.00am, the case ID 20201101001 corresponds to the trading interval which starts at 2020-11-01 04:00:00 and ends at 2020-11-01 04:05:00. When NEMDE is run at approximately 04:00:00 it produces dispatch targets that generators and loads should meet at 04:05:00. The following table illustrates the mapping between case IDs and the start and end times for selected dispatch intervals.

Case ID Interval start Interval end
20201101001 2020-11-01 04:00:00 2020-11-01 04:05:00
20201101002 2020-11-01 04:05:00 2020-11-01 04:10:00
20201101003 2020-11-01 04:10:00 2020-11-01 04:15:00
... ... ...
20201101287 2020-11-02 03:50:00 2020-11-02 03:55:00
20201101288 2020-11-02 03:55:00 2020-11-02 04:00:00

The following function loads a case file in XML format and converts it to a Python dictionary.

def convert_casefile(path_to_file):
    """Load a historical NEMDE case file and convert it to a dict"""

    # Read case file contents
    with open(path_to_file, 'r') as f:
        casefile = f.read()

    # Force these nodes to always return lists
    force_list = ('Trade', 'TradeTypePriceStructure',)

    return xmltodict.parse(casefile, force_list=force_list)

casefile = convert_casefile('../../data/NEMSPDOutputs_2021040100100.loaded')

Case file components

Case files describe the state of the system at the start of a dispatch interval, and also include forecasts for demand and intermittent generation at the end of the interval. A nested data structure, in this case a Python dictionary, organises data into logical components.

Those familiar with NEMDE case files in XML format may recognise the following data structure. In fact, the dictionary is obtained by converting a NEMDE case file in XML format to a Python dictionary. See this tutorial to learn how to convert your own NEMDE XML files into a format that can be consumed by the Dispatch API.

At the dictionary's root there is a single key, NEMSPDCaseFile:

casefile.keys()
odict_keys(['NEMSPDCaseFile'])

The dictionary can be traversed by 'getting' the value for a given key, in this case NEMSPDCaseFile, and looking at its constituent components. Here we can see there are three nested keys:

casefile.get('NEMSPDCaseFile').keys()
odict_keys(['NemSpdInputs', 'NemSpdOutputs', 'SolutionAnalysis'])
Key Description
NemSpdInputs Parameters describing the system's state
NemSpdOutputs NEMDE solution for each trader (generator / load), generic constraint, interconnector, and region
SolutionAnalysis Price setting results

While NEMDE case files provide a convenient data structure describing parameters used to set dispatch targets, there are a limitations associated with their design. For instance, some parameters are duplicated, while others may be ignored. Users seeking to modify case files should consult the parameter reference page to see which parameters can be meaningfully modified when using the Dispatch API.

Submitting a job

For now let's run the case file without modifying any of its components. The body of the request is simply a dictionary with "casefile" as the key, and the case file dictionay as its corresponding value:

body = {"casefile": casefile}

A POST request is submitted to https://dispatch.envector.com/api/v1/jobs/create

The response contains information pertaining to the newly created job, including a job ID which will be used when querying results once they become available.

def submit_casefile(base_url, casefile):
    """Submit case file to the job queue"""

    # Construct request body and URL
    body = {'casefile': casefile}
    url = base_url + 'jobs/create'

    # Send job to queue and return job meta data
    response = requests.post(url=url, json=body)

    return response.json()


# Submit job and inspect meta data
job_info = submit_casefile(base_url=base_url, casefile=casefile)
job_info
{'job_id': '77b379d4-26e6-401b-8152-ca2e5482c658',
 'created_at': '2021-08-07T13:38:20.741817Z',
 'enqueued_at': '2021-08-07T13:38:20.884427Z',
 'timeout': 180,
 'status': 'queued',
 'label': None}

A pool of workers monitor the queue to which the job is posted. If a worker is available it will formulate and run the optimisation model using the inputs provided. Results are then posted back to the queue for retrieval by the user.

The following URLs become available once a job has been submitted, allowing users to check the job's status, examine job results, or delete the job:

URL Description
http://nemde-api-host/api/v1/jobs/{job_id}/status Get job status
http://nemde-api-host/api/v1/jobs/{job_id}/results Get job results
http://nemde-api-host/api/v1/jobs/{job_id}/delete Delete job

For example, if the job ID is 04c66262-6144-444d-98bf-00c21cb955dd, the URL to get the job's status would be:

http://nemde-api-host/api/v1/jobs/04c66262-6144-444d-98bf-00c21cb955dd/status

Let's check the job's status.

def check_job_status(base_url, job_id):
    """Check job status given a job ID"""

    url = base_url + f'jobs/{job_id}/status'

    return requests.get(url=url).json()


# Check job status
job_id = job_info.get('job_id')
check_job_status(base_url=base_url, job_id=job_id)
{'job_id': '77b379d4-26e6-401b-8152-ca2e5482c658',
 'status': 'started',
 'created_at': '2021-08-07T13:38:20.741817',
 'enqueued_at': '2021-08-07T13:38:20.884427',
 'started_at': '2021-08-07T13:38:21.221078',
 'ended_at': None,
 'timeout': 180,
 'label': None}

We can see a worker has started to process the job. It typically takes 30s for a worker to complete a job once started. After waiting a short period we can check the status again.

check_job_status(base_url=base_url, job_id=job_id)
{'job_id': '77b379d4-26e6-401b-8152-ca2e5482c658',
 'status': 'finished',
 'created_at': '2021-08-07T13:38:20.741817',
 'enqueued_at': '2021-08-07T13:38:20.884427',
 'started_at': '2021-08-07T13:38:21.386579',
 'ended_at': '2021-08-07T13:39:12.951867',
 'timeout': 180,
 'label': None}

Retrieving results

Once the job has finished we can access its results.

def get_job_results(base_url, job_id):
    """Extract job results from the queue"""

    url = base_url + f'jobs/{job_id}/results'   
    response = requests.get(url=url)

    return response.json()


# Get job results from the queue
results = get_job_results(base_url=base_url, job_id=job_id)

Note: completed jobs are only retained in the queue for 2 hours, at which point the job (and results) are deleted.

The value corresponding to the results key contains the solution reported by the worker. Let's use Pandas to examine the output.

region_solution = results.get('results').get('output').get('RegionSolution')

# Convert to markdown to display results
region_solution_md = pd.DataFrame(region_solution).to_markdown(index=False)
display.Markdown(region_solution_md)
@RegionID @CaseID @Intervention @EnergyPrice @DispatchedGeneration @DispatchedLoad @FixedDemand @NetExport @SurplusGeneration @R6Dispatch @R60Dispatch @R5Dispatch @R5RegDispatch @L6Dispatch @L60Dispatch @L5Dispatch @L5RegDispatch @ClearedDemand
NSW1 20210401001 0 38.2172 5127.54 200 6330.79 -1403.26 0 261 251 141 88 91 142 67 33 6576.14
QLD1 20210401001 0 32.25 6278.72 0 5322.13 956.591 0 75 53 107.915 23.2 0 0 0 59.875 5362.13
SA1 20210401001 0 37.2465 810.756 10 1065.3 -264.539 0 142.829 77.5837 121 54.8 137 69 86 57.125 1077.47
TAS1 20210401001 0 31.7702 1224.95 0 991.98 232.971 0 33 82.2457 0 49 56.166 152.495 50 50 996.513
VIC1 20210401001 0 34.4678 4729.57 0 4134.2 595.377 0 105 153 98 5 25 31 52.5805 10 4159.28
interconnector_solution = results.get('results').get('output').get('InterconnectorSolution')

# Convert to markdown to display results
interconnector_solution_md = pd.DataFrame(interconnector_solution).to_markdown(index=False)
display.Markdown(interconnector_solution_md)
@InterconnectorID @CaseID @Intervention @Flow @Losses @Deficit
N-Q-MNSP1 20210401001 0 -97 5.32865 0
NSW1-QLD1 20210401001 0 -819.585 65.3249 0
T-V-MNSP1 20210401001 0 228.438 4.53231 0
V-S-MNSP1 20210401001 0 63 0.102114 0
V-SA 20210401001 0 203.717 8.27106 0
VIC1-NSW1 20210401001 0 532.012 24.9029 0
trader_solution = results.get('results').get('output').get('TraderSolution')

# Convert to markdown to display results
trader_solution_md = pd.DataFrame(trader_solution).head().to_markdown(index=False)
display.Markdown(trader_solution_md)
@TraderID @CaseID @Intervention @EnergyTarget @R6Target @R60Target @R5Target @R5RegTarget @L6Target @L60Target @L5Target @L5RegTarget @R6Violation @R60Violation @R5Violation @R5RegViolation @L6Violation @L60Violation @L5Violation @L5RegViolation @RampUpRate @RampDnRate @FSTargetMode
AGLHAL 20210401001 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 720 720 0
AGLSOM 20210401001 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 480 480 0
ANGAST1 20210401001 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 840 840 nan
APD01 20210401001 0 0 31 90 45 0 0 0 0 0 0 0 0 0 0 0 0 0 nan nan nan
ARWF1 20210401001 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1200 600 nan

Summary

This tutorial demonstrates the basic functionality of the Dispatch API. Two key components have been introduced: the ability to interact with historical case files, and methods that facilitate interaction with an online queue. Future tutorials will discuss how to modify case files, perform scenario analyses, and also introduce more advanced workflows using additional Dispatch API features.