Comparing Promula to Julia

gmADS is built on the Promula platform (Processor for Multi-dimensional Analysis).  This platform has been under development and sustained for use, mainly for large electric utility planning models [1], since 1982.  However, in order for Promula to survive beyond its creators, it must be repackaged as open source and promoted to learning institutions.  We hope these efforts will help Promula gain a following so it may benefit science and discovery for the long term.

Promula has a unique approach to handling the declaration, storage, and manipulation of multi-dimensional numerical data.  It is an excellent fit for implementing models and calculations involving array variables that share dimensions.  Its streamlined notation allows analysts to keep their mind on the model rather than on complex programming syntax.  The system is also very fast because it stores and accesses information in memory as arrays directly rather than as tables, trees, or other structures.

Probably the best way to introduce Promula is by comparison to another popular modeling language, Julia. 

Overview

The document compares Promula and Julia for use in multi-dimensional numerical modeling applications.  These samples were randomly selected from a recent Proof of Concept project to evaluate the feasibility of rewriting a very large Promula model with Julia.  Note, the Promula code uses data stored in a Promula array datastores, the Julia code uses data stored in an HDF5 hierarchical datastores.

Data Structures in Promula

For simplicity, the definition of the data structures used in this comparison is presented in Promula notation:

   Define Set
      EC(43)    'Economic Categories'
      Enduse(6) 'Enduses'
      Tech(10)  'Technology Types'
      Area(25)  'Demand Area'
   End Define Set
   Define Variable
      DOCF(Enduse,Tech,EC,Area) 'Device Operating Cost Fraction ($/Yr/$)'
      DCC(Enduse,Tech,EC,Area)  'Device Capital Cost ($/mmBtu/Yr)'
      MCFU(Enduse,Tech,EC,Area) 'Marginal Cost of Fuel Use ($/mmBtu)'
      DCCR(Enduse,Tech,EC,Area) 'Device Capital Charge Rate ($/Yr/$)'
      ECFP(Enduse,Tech,EC,Area) 'Fuel Price ($/mmBtu)'
      DEE(Enduse,Tech,EC,Area)  'Device Efficiency (Btu/Btu)'
   End Define Variable
   Define Variable  Scratch
      DOMC(Enduse,Tech,EC,Area)    'Device Operating Cost ($/mmBtu)'
   End Define Variable Scratch

The Promula data definition language defines the dimensions of arrays in terms of Sets, not just their sizes.  This has powerful implications for how the arrays behave and interact with each other.

 Julia Data Syntax

For comparison, here is some of the Julia/HDF5 data definition code developed for the POC.  Notice that the relationship between the arrays and their logical dimensions and descriptive labels is only retained as comments.  

const VariableArray{N} = Array{Float64,N} where {N}
const SetArray = Vector{String}
Base.@kwdef struct Data
  db::String
  year::Int
  prior::Int
  next::Int

  Area::SetArray   = ReadDisk(db, "e2020db/Area")
  EC::SetArray     = ReadDisk(db, "$Input/EC")
  Enduse::SetArray = ReadDisk(db, "$Input/Enduse")
  Tech::SetArray   = ReadDisk(db, "$Input/Tech")

  # Device Operating Cost Fraction ($/Yr/$)
  DOCF::VariableArray{4} = ReadDisk(db, "$Input/DOCF", year) # [Enduse, Tech, EC, Area]
  # Device Capital Cost ($/mmBtu/Yr)
  DCC::VariableArray{4} = ReadDisk(db, "$Outpt/DCC", year) # [Enduse, Tech, EC, Area]
  # Marginal Cost of Fuel Use ($/mmBtu)
  MCFU::VariableArray{4} = ReadDisk(db, "$Outpt/MCFU", year) # [Enduse, Tech, EC, Area] 
  # Device Capital Charge Rate ($/Yr/$)
  DCCR::VariableArray{4} = ReadDisk(db, "$Input/DCCR", year) # [Enduse, Tech, EC, Area]
  # Fuel Price ($/mmBtu)
  ECFP::VariableArray{4} = ReadDisk(db, "$Outpt/ECFP", year) # [Enduse, Tech, EC, Area]
  DEE::VariableArray{4} = ReadDisk(db, "$Outpt/DEE", year) # [Enduse, Tech, EC, Area] # Device Efficiency (Btu/Btu)
  # Scratch variables 
  DOMC::VariableArray{4} = zeros(Float64, length(Enduse), length(Tech), length(EC), length(Area)) # [Enduse, Tech, EC, Area] Device Operating Cost ($/mmBtu)

...
End


Calculations Comparison: MarginalCostOfFuelUsage

Promula

Define Procedure MarginalCostOfFuelUsage
    DOMC=DOCF*DCC
    MCFU=DCCR*DCC+DOMC+ECFP/DEE
    Write Disk(MCFU)
End Procedure MarginalCostOfFuelUsage
(145 characters)

The Promula code above users implicit subscripting wherein arrays dimensioned by the same sets are automatically indexed in sync during calculations.

Julia

function MarginalCostOfFuelUsage(data::Data)
  (; db, year) = data
  (; Area, EC, Tech, Enduse) = data
  (; DOMC, DOCF, DCC, MCFU, DCCR, ECFP, DEE) = data

  for area in Select(Area), ec in Select(EC), tech in Select(Tech), enduse in Select(Enduse)
    DOMC[enduse, tech, ec, area] = DOCF[enduse, tech, ec, area] * DCC[enduse, tech, ec, area]
  end

  for area in Select(Area), ec in Select(EC), tech in Select(Tech), enduse in Select(Enduse)
    MCFU[enduse, tech, ec, area] =
      DCCR[enduse, tech, ec, area] * DCC[enduse, tech, ec, area] +
      DOMC[enduse, tech, ec, area] +
      ECFP[enduse, tech, ec, area] * finite_inverse(DEE[enduse, tech, ec, area])
  end
  WriteDisk(db, "$Outpt/MCFU", year, MCFU)
end
(699 characters)

The Julia code depends on a Select and WriteDisk macros to facilitate accessing arrays that are logically indexed by sets and stored in HDF5.

Calculations Comparison: MShare

Promula

Define Procedure MShare
   MAW=exp(MMSM0+LN(MSMM)+MVF*LN((MCFU/Inflation/PEE)/(MCFU0/Infla0/PEE0)))
   TMAW(EU,EC,Area)=sum(TE)(MAW(EU,TE,EC,Area))
   MMSF=MAW/TMAW
   Write Disk(MMSF)
End Procedure MShare
(200 characters)

Julia

function MShare(data::Data)
  (; db, year) = data
  (; Area, EC, Tech, Enduse) = data
  (; MAW, MMSM0, MSMM, MVF, MCFU, Inflation, Inflation0, PEE, MCFU0, PEE0, TMAW, MMSF) = data

  for area in Select(Area), ec in Select(EC), tech in Select(Tech), enduse in Select(Enduse)
    MAW[enduse, tech, ec, area] = finite_exp(
      (MMSM0[enduse, tech, ec, area] + finite_log(MSMM[enduse, tech, ec, area])) +
      MVF[enduse, tech, ec, area] * finite_log(
        (MCFU[enduse, tech, ec, area] * finite_inverse(Inflation[area]) * finite_inverse(PEE[enduse, tech, ec, area])) * finite_inverse((
          MCFU0[enduse, tech, ec, area] *
          finite_inverse(Inflation0[area]) *
          finite_inverse(PEE0[enduse, tech, ec, area])
        )),
      ),
    )
  end

  for area in Select(Area), ec in Select(EC), enduse in Select(Enduse)
    TMAW[enduse, ec, area] = sum(MAW[enduse, tech, ec, area] for tech in Select(Tech))
  end

  for area in Select(Area), ec in Select(EC), tech in Select(Tech), enduse in Select(Enduse)
    MMSF[enduse, tech, ec, area] = MAW[enduse, tech, ec, area] * finite_inverse(TMAW[enduse, ec, area])
  end
  # Write Disk(MMSF)
  WriteDisk(db, "$Outpt/MMSF", year, MMSF)
end
(1,174 characters)


[1]: Promula has been used to develop the Energy 2020 planning model for over 30 years.  Visit https://www.energy2020.com/ to learn more.