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

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

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
# 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

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