Skip to content

Lua and CadnaA

CadnaA-Lua scripts are plain Lua scripts. Interaction between the script and CadnaA is mediated by the CadnaA global cna object. This object is only accessible when the script file is run from within CadnaA.

CadnaA-Instructions

CadnaA-Lua offers a set of instructions for tasks such as loading, saving and calculation of project files and grids/receiver points as well as a whole lot more (recalculating the limits of the project, etc.). These instructions are all wrapped up into the cna-Namespace, i.e. they are accessible in Lua via cna.INSTRUCTION, e.g.:

cna.calc_grid()
cna.save_grid("test.cnr")
-- print out a string of characters to the dialog
cna.print("message to console")

Accessing objects

Next to these general instructions, the objects of CadnaA are also accessible in CadnaA-Lua. More precisely, the CadnaA object tables are directly accessible and since CadnaA objects are members of these tables, they are accessible as well by using CadnaA table methods such as first_by_id("id") and all("id").

The CadnaA-table-objects can be found in the name space cna.tables, e.g.

cna.tables.pt_src   -- table of point sources
cna.tables.imm      -- table of receivers

There are two identifiers for each table (see Explanation Bit-Field, Explanation Bit-Field). The following examples give an impression in how to access and manipulate CadnaA-objects and project files with CadnaA-Lua.

Example 1: Iteration over every object within a CadnaA-Table

In the following source code snippet, sound pressure levels LP1 at receivers are taken from the table Receiver and the ones being above 70 dB are written into the output window.

Example

Path: LUA_1.lua

local level
local r
for r in cna.tables.imm:all() do
    if r.LP1 > 70 then
        level = level .. r.LP1 .. " dB"
    end
end    
cna.print('levels > 70 are: ' .. level)

The expression tables.imm:all() is an “iterator” for the objects of table cna.tables.imm. Iterators make objects sequentially accessible using loops, as in this case with the for .. in .. do loop. This way it is ensured that every element of the table is being ‘visited’. The variable r is a reference to a CadnaA-object (another way to think of it: it is a row of the CadnaA-table cna.tables.imm).

r is a “reference” to the CadnaA receiver object. This means that manipulations of r are directly applied to “real” object in CadnaA. By using the dot-notation r.ATTRIBUTE the attributes of the object can be read or changed. Attribute names are documented in the CadnaA manual “Attributes, Variables, and Keywords”.

Example 2: Automatic Calculation of Grids

The following script adds a point source q to the project and moves it to a given position to calculate the grid. This process is repeated 400 times and with every step, the point source is moved 25 m on the x axis, thus the resulting 400 numbered grid files show how sound levels changes while the point source has been moved over the distance of 1 km.

Note

The Save Grid command may override existing *.cnr files without warning.

Example

Path: LUA_2.lua

local x
local q = cna.tables.pt_src:append()
q.LW_LI = 100
for x=0,1000,25 do -- x in steps of 25 m
    q.X = 100 + x -- set position
    cna.calc_grid()
    -- save grid (filename contains x-pos)
    cna.save_grid("grid_" .. x .. ".cnr")
    -- saves to the directory containing the LUA-file
end

The loop in detail: After declaring the variable containing the x-position (x) and q (point source) the counter is increased in steps of 25 from 0 to 1000. q is a reference to a new CadnaA object. CadnaA objects can be created by appending the CadnaA table with the method cna.tables.*:append(). For each iteration step, the X attribute of the point source is modified and the grid is calculated and saved. In order to have several resulting files, the filename is generated by the "grid_" .. x .. ".cnr" expression.

In order to save the resulting grids to a specific directory, the file path has to be added to the command cna.save_grid:

cna.save_grid ("D:/CADNA_A/LUA/grid_" .. x .. ". cnr")

As path separator the forward slash must be used as Lua interprets the backward slash (in conjunction with a letter) as a control character.

Example 3: Lissajous-Shapes

The following example shows, how to create several objects in order to manipulate their attributes. The result of this script is a set of point sources that are arranged in the shape of Lissajous-figures. Adjust the example for different shapes and purposes (e.g. the math package contains a random number generator which can be used to distribute sources randomly, etc.).

Example

Path: LUA_3.lua

require('math')
local N = 10000
for x = 1,N do
    local q = cna.tables.pt_src:append()
    -- q is the new source
    local f = 2*math.pi / N
    q.X = 50 + 250*math.sin(x/f) 
    q.Y = 500 + 250*math.cos(x/f*3)
    q.ID = "source" .. N  -- assign an ID
    -- assign level in dB
    q.LW_LI = 90 + math.random() * 20 - 10
    -- create and assign a memo text variable
    -- "objectindex" with content x
    q.memo_var["objektindex"] =  x 
   q.memo_var.objektindex =  x -- alternative way of referencing the variable
end

In this example, negative x coordinates are generated. Therefore we want to adjust the limits of the project:

cna.limit.calc()

The Lua math library is in name space math and offers basic mathematical operations for trigonometry and others (math.sqrt, math.sqrt, math.abs, math.exp, math.log, math.log10 etc.; see the Lua documentation for details).

Access to memo text variables is possible with the memo_var field. Memo text variables are available as attributes of memo_var.

Example 4: Iterating over active objects

In example 1 it was shown how to iterate over all objects of a table. The following example shows, how iterations over subsets of the table’s objects are possible, namely (1) over active objects only and (2) over objects having a specific ID.

Example

Path: LUA_4_1.lua

local L = ""
local r
-- just looking for active receivers
for r in cna.tables.imm:all{active_only=true} do
    if r.LP1 > 70 then
        L= L .. r.LP1 .. " dB"
    end
end
cna.print("L > 70: " .. L)

Example

Path: LUA_4_2.lua

local L = ""
local r
-- looking for actice rcvrs AND with ID=R_01
for r in cna.tables.imm:all{active_only=true, id="R_01"} do
    if r.LP1 > 70 then
        L = L .. r.LP1 .. " dB"
    end
end
cna.print("L > 70: " .. L

Example 5: Iteration over an ObjectTree group – pattern matching

Furthermore, you can iterate over a group of objects (i.e. not the whole table). This works by supplying a regular expression pattern for an ID-string which is then matched with the object’s ID:

Example

Path: LUA_5.lua

local L = ""
local r
for r in tables.imm:all<div id="!01!*"/> do
    if r.LP1 > 70 then
        L = L .. r.LP1 .. " dB"
    end
end
cna.print("L > 70: " .. L)

The pattern expressions are like the ones that you can use in CadnaA dialogs.

Example 6: Accessing polygons and polygon vertices

Several CadnaA objects have polygon-data, e.g. buildings, roads, auxiliary polygons etc. CadnaA-Lua makes these polygons accessible with the poly attribute. Very much like for the CadnaA table objects, CadnaA-Lua offers iterators for polygon points.

Furthermore, commands for simplifying the object‘s geometry (Simplify Geo) or calculation of areas and lengths are available. The following feature shows how polygons are handled using an auxiliary polygon auxpoly. The file start.cna contains an auxiliary polygon with ID "aux1":

Example

Path: LUA_6_1.lua

local auxpoly = cna.tables.div:first_by_id("aux1")
-- polygon data is available via `poly`
-- Shortcut: pp is declared to be a reference to auxpoly.poly

local pp = auxpoly.poly     
cna.print("Number of polygon vertices: " .. pp:num_vertices())

-- alternative way of putting it:
cna.print("Num. vertices with # operator: " .. #pp)  
cna.print("length 2d " .. pp:length2d())
cna.print("length 3d " .. pp:length3d())
cna.print("area 2d " .. pp:area2d())
cna.print("area 3d " .. pp:area3d())

-- Iteration over all polygon points
local p
for p in pp:all() do
    cna.print(p) -- print out point
    -- manually repeat what cna.print(p) does by
    -- accessing the point data itself
    cna.print(string.format("test [%f,%f,%f; G=%f]", p[1], p[2], p[3], p[4])) 
    -- or
    cna.print(string.format("test [%f,%f,%f; G=%f]", p.x, p.y, p.z, p.g)) 
end

Example

Path: LUA_6_2.lua

-- apply 'simplify geometry':
local del_vertices = pp:simplify_geometry(1.0)
cna.print("Number of vertices removed" .. del_vertices)

cna.print("Num. vertices with # operator: " .. #pp) 
cna.print("length 2d " .. pp:length2d())
cna.print("length 3d " .. pp:length3d())
cna.print("area 2d " .. pp:area2d())
cna.print("area 3d " .. pp:area3d())

Example 7: Insert and define polygon vertices

The following source code creates an auxiliary polygon and inserts vertices into it. The vertices are located on a circle with a radius of 10 m around the origin (0, 0). The second for-Loop iterates over the polygon vertices and inserts more points using a 5 m radius. The result is a star-shaped auxiliary polygon.

Example

Path: LUA_7.lua

local auxpoly = cna.tables.Div:append()
auxpoly.ID = "test"

local N = 100
local i

-- Append N vertices
for i=1,N do
      -- append and set coordinate
 local p = auxpoly.poly:append() 
 p.x = 10*math.cos(i/N*2*math.pi)
 p.y = 10*math.sin(i/N*2*math.pi)
 p.z = 0
end

-- assertions help to write save code. If the 
-- expression within assert(...) evaluates to false
-- => Lua reports an error
assert(N == #auxpoly.poly)

i=0
for pp in auxpoly.poly:all() do
 i = i + 1
 local p = auxpoly.poly:insert_after(pp)
 p.x = 5*math.cos((i+1)/N*2*math.pi)
 p.y = 5*math.sin((i+1)/N*2*math.pi)
end

Example 8: Lua-side calculation

This example shall demonstrate the power of Lua as an extension language that transcends from a mere collection of commands to built interesting new things.

Motivation: It is known, that with some noise calculation procedures, the sound pressure level at two receivers being close to each may differ quite substantially. With these procedures, the local level has a lower spatial accuracy than with other procedures. In order to evaluate this kind of scattering of results, the sound pressure level is calculated for small changes of a parameter (e.g. using the source coordinate x) using a Lua script. To this end, we calculate the noise level of N receivers that are all within a 1 m range around the receiver‘s original location. For those N noise levels, we want to calculate the arithmetic mean μ and the standard deviation σ. These two values used to quantify the noise level distribution, are calculated by the two following equations:

Now, since we want CadnaA-Lua to perform these calculations, a Lua-function has to be written:

Example

Path: LUA_8.lua

-- mean and stddev of values t = {x1, x2, ...., x_(#t)}
local function calc_mean_stddev(t) 
  local s = 0 -- accumulator for sum
  local i
  for i=1,#t do  
    s = s + t[i]
    end
  local mean = s / #t

  -- calculate stddev
  local sum_sqr =0 -- accumulator for sum of squares
  for i=1,#t do
    sum_sqr = sum_sqr +  math.pow(t[i]-mean,2)
  end
  local stddev =  math.sqrt(1/(#t - 1) * sum_sqr)

  -- return two values 
  return mean, stddev
end

Now, we want a specific receiver to act as our “original”. We choose a receiver with ID “i1”, this receiver will be moved during the execution of the script. Now we can just use cna.tables.imm:append() to attach a new receiver to our project, however, when run multiple times we would have the problem of adding more and more receivers on top. Therefore, in order to avoid this, we use Lua’s or operator. In an expression a or b, a is evaluated, if it is a true-ish value, it is the result of the expression, if it is false-ish however, the second operand b is evaluated. Consequently, the following line will retrieve a receiver with ID “i1” if it exists, or append a new receiver to the project and assign the ID “i1” to it.

local imm = cna.tables.imm:first_by_id("i1") or cna.tables.imm:append()
imm.ID = "i1"

Now some basic properties of the receiver are set (change this according to your test project).

imm.X = 100
imm.Y = 100
imm.H = 4

We choose to look at 10 locations all across the map and perform 10 receiver-variations at each of those locations. The following variables simplify changing those numbers later on:

local N=10 -- Number of mean values 
local M=10 -- Number of values for calculating the mean

The current x coordinate is stored as we need it to calculate the intermediate x positions.

local x0 = imm.X

Now, we iterate over positions on a large scale (10 meters distance) (this is the for loop over i) setting up an array data to hold the noise levels which are about to be calculated. Then, we loop over the local variations (for loop over j), calculate an x coordinate which is a local variation within in an 1 m interval. The calculation for our specific receiver is started and the noise levels are written to the data array. Upon having calculated all local variations, the mean values and the standard deviations of the means are calculated and written to the log window.

for i=0,10*N-1,10 do
  local j
  local data = {}
  -- local variations within one meter
  for j = 1,M do
    imm.X = x0 - i + j*1/M
    cna.calc_all_imm()
    data[j] = imm.LP1  
    end 
  local x_c = x0 - i + 0.5
  local mean, stddev = calc_mean_stddev(data)
  cna.print(string.format(
      "around %f; mean %f +/- %f",x_c, mean, stddev))
end

This example could be run in a simple project containing roads assessing the level differences when applying RLS-90 and NMPB08 prediction models, for example.