Best Practice for Lookup Table Definition

Hello,

I am looking for some guidance on what the idiomatic way to define a small database of objects/materials might be. For example, this might pertain to a small set of materials and their associated material properties, or, as shown below, a table of battery cells available for selection and use in a larger analysis. There might be anywhere from 3-30 elements in a small database like this and they are small enough to be written (and later extended) by hand.

I am looking for advice on both the creation/definition of these lookup tables / databases and the form they end up taking (I am totally flexible to go in any direction at the moment). The dictionary of structs created below works perfectly fine but seems for some reason to be missing out on the similarity of the objects within the dictionary.

In the end this database definition ideally has these characteristics, roughly in order of importance:

  • Follows Julia best-practices, this is largely a learning experience for myself and partially for additional users. I am trying to move some of these larger system analyses from Matlab to Julia and would like to be able to make the case for potential future Julia use to colleagues
  • Ease of use. It should be straightforward to select an element/row by name and pass all of its properties to a function
  • Accessible (both readable and extendable) by additional users who likely are not familiar with Julia
  • Traceable to a specification, so the numbers seen in the database correlate to numbers available on the spec sheet and any wrangling to fit the struct fields is visible and obvious. This wrangling may be slightly different for each added element depending on the information available in the specification.
  • Single file that defines the entire database
  • Flexible to various datatypes. For example, the energy and power fields in the example below might be 2 elements, they might be 20. This is tough to represent in spreadsheet databases like Excel or csv.
  • Fast during runtime
  • It would be great if this file had the look/feel of a static configuration file. I have a feeling this might directly contradict the above desired characteristics.
module BatteryCell

using Unitful.DefaultSymbols
using Unitful: hr

export LUT

abstract type AbstractCell end

ED(A::AbstractCell) = A.energy / A.volume
PD(A::AbstractCell) = A.power / A.volume
#...other heuristics available

struct BatteryCell <: AbstractCell
    volume::typeof(1.0m^3)
    voltage::typeof(1.0V)
    power::Vector{typeof(1.0W)}
    energy::Vector{typeof(1.0J)}
    #...a few more fields here
end

cyl_vol(l,d) = l * π * (d/2)^2
pris_vol(l,w,h) = l * w * h

#Make lookup table for use
LUT = Dict{String,BatteryCell}()

#Now add cell definitions in one at a time? Is it more idiomatic to wrap all this in a function call?
# LFP_26650 https://www.batteryspace.com/lifepo4-26650-rechargeable-cell-3-2v-3300-mah-19-8a-rate-10wh---un38-3-passed-ndgr.aspx
len = 65.5mm
dia = 26.3mm
vol = cyl_vol(len,dia)
volt = 3.2V
power = volt * [0,   0.2, 0.5, 1,   2,   3   ]hr^(-1) * 3200mA*hr #W from provided c-rates
energy = volt * [3250,3250,3200,3150,3100,3050]mA*hr
LUT["LFP_26650"] = BatteryCell(vol,mass,volt,resist,cost,power,energy)

# LFP_18650 https://www.batteryspace.com/lifepo4-18650-rechargeable-cell-3-2v-1500-mah-8-4a-rate-4-32wh-ul-listed-un38-3-passed-ndgr.aspx
len = 65.5mm
dia = 18.6mm
vol = cyl_vol(len,dia)
volt = 3.2V
power = volt *  [0,   0.2, 0.5, 1,   2,   3   ]hr^(-1) * 3200mA*hr #W from provided c-rates
energy = volt * [1500,1500,1450,1400,1400,1400]mA*hr       
LUT["LFP_18650"] = BatteryCell(vol,mass,volt,resist,cost,power,energy)

# LFP_G100 https://www.batteryspace.com/prod-specs/5907-100Ah.pdf
len = 140mm
wid = 62mm
height = 243mm
vol = pris_vol(len,wid,height)
volt = 3.2V
power = [3.2, 3.2, 3.2, 3.0]V .* [0, 0.5, 1, 3]hr^(-1) * 100A*hr
energy = [3.2, 3.2, 3.2, 3.0]V .* [110, 110, 110, 100]A*hr
LUT["LFP_G100"] = BatteryCell(vol,mass,volt,resist,cost,power,energy)

#...10ish more candidate battery cells...

end

I realize there are a ton of different ways to get this done, each with their own pros/cons. Thank you all for any advice you may have to offer!

1 Like

Using a dictionary with structs as values, as you have done, is entirely reasonable. What do you mean by “but seems for some reason to be missing out on the similarity of the objects within the dictionary”?

First, I would go easy on the globals and just define an (outer) constructor that figures out vol and power from the rest of the fields with extra information (whenever applicable).

Then make a Dict with simply the syntax

Dict(["LFP_26650" => BatteryCell(len = 65.5mm,
                                 dia = 26.3mm,
                                 volt = 3.2V,
                                 ...),
      ...
      ])

For 30+ items, I think a dictionary is fine.

To expand on the solution above, I would perhaps define a macro in order to build BatteryCell instances more easily:

# Minimal example
struct BatteryCell
    vol
end
macro battery_cell(exprs)
    display(exprs)
    quote
        let
            $exprs
            BatteryCell(vol)
        end
    end
end

# @battery_cell begin
#    len = 65.5
#    dia = 26.3
#    vol = len * π * dia^2 / 4
# end
#
# expands roughly to:
#    let
#        begin
#            var"#49#len" = 65.5
#            var"#50#dia" = 26.3
#            var"#51#vol" = (var"#49#len" * Main.Ď€ * var"#50#dia" ^ 2) / 4
#        end
#        Main.BatteryCell(var"#51#vol")
#    end

Then the LUT can be defined in a “code is data, data is code” way:

const INSTRUMENTS = Dict(
    :LFP_26650 => @battery_cell(begin
                               len = 65.5
                               dia = 26.3
                               vol = len * pi * dia^2 / 4
                               end),

    :LFP_G100  => @battery_cell(begin
                               len = 140.
                               wid = 62.
                               hgt = 243.
                               vol = len * wid * hgt
                               end)
)