Overview of the NEM
The National Electricity Market (NEM) operates on one of the world’s longest interconnected power systems. It covers around 40,000 km of transmission lines and cables, supplying a population exceeding 23 million.
The NEM is a wholesale market where electricity is traded across five interconnected regions: Queensland (QLD), New South Wales (NSW), Victoria (VIC), South Australia (SA), and Tasmania (TAS). Each region acts as a separate pricing zone, and prices can diverge when the transmission lines (interconnectors) between them become congested.
Simple usage
This package handles the download of data from the NEMWEB Archive and the parsing of the data into a System from PowerSystems.jl. At the moment only a simple zonal network model is available (a nodal model with physical lines is a work-in-progress), which allows to model a simple economic dispatch. By default, the package will create a hidden folder .aem_cache in the home directory, and save the data into parquet files.
begin
using AustralianElectricityMarkets
using TidierDB
using Dates
using PowerSystems
using DataFrames
using Chain
using CairoMakie, AlgebraOfGraphics
end
# Initialise a connection to manage the market data via duckdb
db = aem_connect(duckdb());
# Download data from AEMO's NEMWEB's Monthly archive
date_range = Date(2025, 1, 1):Date(2025, 1, 2)
# Download the data from the monthly archive, saving them locally
# in parquet files.
# Only the data requirements for a RegionalNetworkConfiguration are downloaded.
tables = table_requirements(RegionalNetworkConfiguration())
map(tables) do table
fetch_table_data(table, date_range)
end;2026-02-01 06:10:34 [info ] Creating cache directory at /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Creating temp cache directory at /tmp/.nemweb_temp
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for INTERCONNECTOR 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for INTERCONNECTOR 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 76.81it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for INTERCONNECTORCONSTRAINT 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for INTERCONNECTORCONSTRAINT 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 454.47it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for DISPATCHREGIONSUM 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for DISPATCHREGIONSUM 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 257.67it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for DUDETAIL 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for DUDETAIL 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 363.58it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for DUDETAILSUMMARY 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for DUDETAILSUMMARY 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 205.28it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for STATION 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for STATION 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 859.31it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for STATIONOPERATINGSTATUS 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for STATIONOPERATINGSTATUS 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 885.25it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for GENUNITS 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for GENUNITS 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 767.63it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for DUALLOC 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for DUALLOC 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 686.24it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for BIDDAYOFFER_D 2025 / 1
2026-02-01 06:10:34 [info ] Data already exists for BIDDAYOFFER_D 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 118.20it/s]
2026-02-01 06:10:34 [info ] Set cache directory to /home/runner/.nemweb_cache
2026-02-01 06:10:34 [info ] Set filesystem to local
2026-02-01 06:10:34 [info ] Populating database with data from 2025-01-01 00:00:00 to 2025-01-01 00:00:00
0%| | 0/1 [00:00<?, ?it/s]2026-02-01 06:10:34 [info ] Checking if data already exists for BIDPEROFFER_D 2025 / 1
2026-02-01 06:10:35 [info ] Data already exists for BIDPEROFFER_D 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 1.30it/s]
100%|██████████| 1/1 [00:00<00:00, 1.30it/s]Once the data is downloaded, a few utility functions allow direct parsing of key quantities, such as a table of all units registered in the NEM, or the region zonal operational demand
# read all units
units = read_units(db)
capacity_by_fuel = @chain units begin
select(:DUID, :REGISTEREDCAPACITY, :CO2E_ENERGY_SOURCE, :REGIONID)
groupby([:CO2E_ENERGY_SOURCE, :REGIONID])
combine(:REGISTEREDCAPACITY => (x -> sum(x) / 1000) => :installed_capacity_gw)
dropmissing
sort!(:installed_capacity_gw)
# Ignore marginal sources
subset!(:installed_capacity_gw => ByRow(>=(0.1)))
end
spec = mapping(
:REGIONID,
:installed_capacity_gw => "Installed capacity [GW]",
stack = :CO2E_ENERGY_SOURCE,
color = :CO2E_ENERGY_SOURCE => "Fuel source"
) * visual(
BarPlot, alpha = 0.8,
)
fig = data(capacity_by_fuel) * spec
draw(
fig,
figure = (;
title = "Installed capacity in the National Electricity Market by energy source",
),
scales(Color = (; palette = from_continuous(:Paired_12)))
)
Read the demand for each state. In the NEM, this is referred to as Operational Demand, which is the demand met by local scheduled and semi-scheduled generation, plus net imports from other regions. It excludes "behind-the-meter" rooftop PV, which instead appears as a reduction in operational demand.
demand = read_demand(db)
begin
spec = data(
subset(demand, :SETTLEMENTDATE => ByRow(x -> (Date(2025, 1, 20) <= x <= Date(2025, 1, 31))))
)
spec *= mapping(:SETTLEMENTDATE => "Date", :TOTALDEMAND => "Total Demand [MW]", color = :REGIONID => "State")
spec *= visual(Lines)
figure_options = (;
title = "Regional demand in each Australian state",
subtitle = "Period of January 20th 2025 to January 31st 2025",
)
draw(spec; figure = figure_options)
end
Australia has a fairly high penetration of PV, which at times exceed a state's total demand for electricity (in SA in particular).
begin
spec = data(
subset(demand, :SETTLEMENTDATE => ByRow(x -> (Date(2025, 1, 20) <= x <= Date(2025, 1, 31))))
)
spec *= mapping(:SETTLEMENTDATE => "Date", :SS_SOLAR_AVAILABILITY => "Total Demand [MW]", color = :REGIONID => "State")
spec *= visual(Lines)
figure_options = (;
title = "Solar generation from semi-scheduled units per state",
subtitle = "Period of January 20th 2025 to January 31st 2025",
)
draw(spec; figure = figure_options)
end
Integration with PowerSystems.jl
The main purpose of this package is to provide the data required for instantiating systems with PowerSystems.jl and leverage the ecosystem of power systems modelling developed by NREL-Sienna.
# Instantiate the system
sys = nem_system(db, RegionalNetworkConfiguration())| System | |
| Property | Value |
|---|---|
| Name | |
| Description | |
| System Units Base | SYSTEM_BASE |
| Base Power | 100.0 |
| Base Frequency | 60.0 |
| Num Components | 602 |
| Static Components | |
| Type | Count |
|---|---|
| ACBus | 12 |
| Arc | 12 |
| Area | 6 |
| AreaInterchange | 8 |
| EnergyReservoirStorage | 31 |
| HydroDispatch | 84 |
| Line | 14 |
| PowerLoad | 6 |
| RenewableDispatch | 222 |
| ThermalStandard | 199 |
| TransmissionInterface | 8 |
Users can then interact with the system with all PowerSystems utilities
# Explore the thermal generators
thermal = @chain get_components(ThermalGen, sys) |> DataFrame begin
select!([:name, :fuel, :prime_mover_type, :base_power])
groupby(:fuel)
combine(:base_power => sum => :installed_capacity_mw)
end| Row | fuel | installed_capacity_mw |
|---|---|---|
| ThermalF… | Float64 | |
| 1 | PowerSystems.ThermalFuelsModule.ThermalFuels.NATURAL_GAS = 18 | 9710.0 |
| 2 | PowerSystems.ThermalFuelsModule.ThermalFuels.COAL = 1 | 21371.0 |
| 3 | PowerSystems.ThermalFuelsModule.ThermalFuels.OTHER_GAS = 19 | 1329.0 |
| 4 | PowerSystems.ThermalFuelsModule.ThermalFuels.AG_BYPRODUCT = 21 | 480.0 |
| 5 | PowerSystems.ThermalFuelsModule.ThermalFuels.DISTILLATE_FUEL_OIL = 9 | 841.0 |
Get information for a specific unit
gen = get_component(ThermalGen, sys, "JLA02")ThermalStandard: JLA02:
name: JLA02
available: true
status: true
bus: ACBus: VIC1_GEN_BUS
active_power: 0.0
reactive_power: 0.0
rating: 0.51
active_power_limits: (min = 0.0, max = 0.6499999916553497)
reactive_power_limits: nothing
ramp_limits: (up = 0.23000000149011612, down = 0.23000000149011612)
operation_cost: PowerSystems.ThermalGenerationCost composed of variable: InfrastructureSystems.FuelCurve{InfrastructureSystems.LinearCurve}
base_power: 51.0
time_limits: (up = 8.0, down = 8.0)
must_run: false
prime_mover_type: PowerSystems.PrimeMoversModule.PrimeMovers.CC = 4
fuel: PowerSystems.ThermalFuelsModule.ThermalFuels.NATURAL_GAS = 18
services: 0-element Vector{PowerSystems.Service}
time_at_status: 10000.0
dynamic_injector: nothing
ext: Dict{String, Any}("postcode" => "3000", "station_name" => "Jeeralang \"A\" Power Station", "station_id" => "JEERA")
InfrastructureSystems.SystemUnitsSettings:
base_value: 100.0
unit_system: InfrastructureSystems.UnitSystemModule.UnitSystem.SYSTEM_BASE = 0
has_supplemental_attributes: false
has_time_series: falseCheck the prime mover type
get_prime_mover_type(gen)PowerSystems.PrimeMoversModule.PrimeMovers.CC = 4Check the rating in natural units
with_units_base(sys, "NATURAL_UNITS") do
get_rating(gen)
end51.0