Skip to content

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.

julia
using AustralianElectricityMarkets
using TidierDB
using Dates
using PowerSystems
using DataFrames
using Chain
using CairoMakie, AlgebraOfGraphics

db = aem_connect(duckdb());
date_range = Date(2025, 1, 1):Date(2025, 1, 2)

tables = table_requirements(RegionalNetworkConfiguration())
map(tables) do table
    fetch_table_data(table, date_range)
end;
<frozen importlib._bootstrap>:488: Warning: OpenSSL 3's legacy provider failed to load. Legacy algorithms will not be available. If you need those algorithms, check your OpenSSL configuration.
2026-03-09 05:22:37 [info     ] Creating cache directory at /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Creating temp cache directory at /tmp/.nemweb_temp
2026-03-09 05:22:37 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Set filesystem to local       
2026-03-09 05:22:37 [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-03-09 05:22:37 [info     ] Checking if data already exists for INTERCONNECTOR 2025 / 1
2026-03-09 05:22:37 [info     ] Data already exists for INTERCONNECTOR 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 206.58it/s]
2026-03-09 05:22:37 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Set filesystem to local       
2026-03-09 05:22:37 [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-03-09 05:22:37 [info     ] Checking if data already exists for INTERCONNECTORCONSTRAINT 2025 / 1
2026-03-09 05:22:37 [info     ] Data already exists for INTERCONNECTORCONSTRAINT 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 524.48it/s]
2026-03-09 05:22:37 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Set filesystem to local       
2026-03-09 05:22:37 [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-03-09 05:22:37 [info     ] Checking if data already exists for DISPATCHREGIONSUM 2025 / 1
2026-03-09 05:22:37 [info     ] Data already exists for DISPATCHREGIONSUM 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 217.47it/s]
2026-03-09 05:22:37 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Set filesystem to local       
2026-03-09 05:22:37 [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-03-09 05:22:37 [info     ] Checking if data already exists for DUDETAIL 2025 / 1
2026-03-09 05:22:37 [info     ] Data already exists for DUDETAIL 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 363.33it/s]
2026-03-09 05:22:37 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Set filesystem to local       
2026-03-09 05:22:37 [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-03-09 05:22:37 [info     ] Checking if data already exists for DUDETAILSUMMARY 2025 / 1
2026-03-09 05:22:37 [info     ] Data already exists for DUDETAILSUMMARY 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 184.92it/s]
2026-03-09 05:22:37 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Set filesystem to local       
2026-03-09 05:22:37 [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-03-09 05:22:37 [info     ] Checking if data already exists for STATION 2025 / 1
2026-03-09 05:22:37 [info     ] Data already exists for STATION 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 732.89it/s]
2026-03-09 05:22:37 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Set filesystem to local       
2026-03-09 05:22:37 [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-03-09 05:22:37 [info     ] Checking if data already exists for STATIONOPERATINGSTATUS 2025 / 1
2026-03-09 05:22:37 [info     ] Data already exists for STATIONOPERATINGSTATUS 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 779.47it/s]
2026-03-09 05:22:37 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:37 [info     ] Set filesystem to local       
2026-03-09 05:22:38 [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-03-09 05:22:38 [info     ] Checking if data already exists for GENUNITS 2025 / 1
2026-03-09 05:22:38 [info     ] Data already exists for GENUNITS 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 581.90it/s]
2026-03-09 05:22:38 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:38 [info     ] Set filesystem to local       
2026-03-09 05:22:38 [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-03-09 05:22:38 [info     ] Checking if data already exists for DUALLOC 2025 / 1
2026-03-09 05:22:38 [info     ] Data already exists for DUALLOC 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 573.46it/s]
2026-03-09 05:22:38 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:38 [info     ] Set filesystem to local       
2026-03-09 05:22:38 [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-03-09 05:22:38 [info     ] Checking if data already exists for BIDDAYOFFER_D 2025 / 1
2026-03-09 05:22:38 [info     ] Data already exists for BIDDAYOFFER_D 2025 / 1, skipping download. Use force_new=True to overwrite.
100%|██████████| 1/1 [00:00<00:00, 86.65it/s]
2026-03-09 05:22:38 [info     ] Set cache directory to /home/runner/.nemdb_cache
2026-03-09 05:22:38 [info     ] Set filesystem to local       
2026-03-09 05:22:38 [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-03-09 05:22:38 [info     ] Checking if data already exists for BIDPEROFFER_D 2025 / 1
2026-03-09 05:22:38 [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.34it/s]100%|██████████| 1/1 [00:00<00:00,  1.34it/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.

julia
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)
    subset!(:installed_capacity_gw => ByRow(>=(0.1))) ## Ignore marginal sources
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.

julia
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).

julia
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.

julia
sys = nem_system(db, RegionalNetworkConfiguration())
System
PropertyValue
Name
Description
System Units BaseSYSTEM_BASE
Base Power100.0
Base Frequency60.0
Num Components602
Static Components
TypeCount
ACBus12
Arc12
Area6
AreaInterchange8
EnergyReservoirStorage31
HydroDispatch84
Line14
PowerLoad6
RenewableDispatch222
ThermalStandard199
TransmissionInterface8

Users can then interact with the system with all PowerSystems utilities.

julia
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
5×2 DataFrame
Rowfuelinstalled_capacity_mw
ThermalF…Float64
1PowerSystems.ThermalFuelsModule.ThermalFuels.NATURAL_GAS = 189710.0
2PowerSystems.ThermalFuelsModule.ThermalFuels.COAL = 121371.0
3PowerSystems.ThermalFuelsModule.ThermalFuels.OTHER_GAS = 191329.0
4PowerSystems.ThermalFuelsModule.ThermalFuels.AG_BYPRODUCT = 21480.0
5PowerSystems.ThermalFuelsModule.ThermalFuels.DISTILLATE_FUEL_OIL = 9841.0

Get information for a specific unit.

julia
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: false

Check the prime mover type.

julia
get_prime_mover_type(gen)
PowerSystems.PrimeMoversModule.PrimeMovers.CC = 4

Check the rating in natural units.

julia
with_units_base(sys, "NATURAL_UNITS") do
    get_rating(gen)
end
51.0