User Tools

Site Tools


02_user_tutorials:04_light_modelling:sensor-grids

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
02_user_tutorials:04_light_modelling:sensor-grids [2025/01/24 15:51] – removed - external edit (Unknown date) 127.0.0.102_user_tutorials:04_light_modelling:sensor-grids [2025/01/24 15:51] (current) – ↷ Page moved from 02_user_tutorials:light_modelling:sensor-grids to 02_user_tutorials:04_light_modelling:sensor-grids tim2
Line 1: Line 1:
 +====== Sensor Grids and Areal Sensors ======
 +
 +Light distributions, micro-light climate, light above canopy, light extinction coefficients, etc. are key parameters for any canopy simulations. To obtain these, areal or gird like sensor arrangements are required. Both can be generated within GroIMP by only a few lines of code.
 +
 +===== Sensor Grids =====
 +
 +Here, a sensor grid is a 3D arrangement of //SensorNodes//, having a defined number of steps/nodes in x, y, and z direction. When placed within a canopy, such a sensor grid is ideal to measure light distributions within different layers of the canopy. Below an example of the light wheat canopy with three layers of sensor nodes (just for illustration reasons, each layer is coloured differently: from bottom to top, red, green, and blue). 
 +
 +{{ :tutorials:SensorGrid_Wheat.png?direct&500 |}}
 +
 +In order to analyse the sensed radiation, e.g., using external tools, it is most probably required to record the location (Cartesian/World coordinates) of each sensor node. One way to do this is to use the Library function //location(Object)// to get the location of the object. The other way is to record the position during the initialization of the elements. Here, the second way is demonstrate in the following. 
 +
 +To record the position of each sensor node it is needed to have some user defined module that provides variables to store the position, e.g., three simple floats, one for each axis. In XL this can be done in the following way that defines a new module //SensorElement// as extension of //SensorNode//:
 +
 +<code java>
 +module SensorElement extends SensorNode() {
 +  float x,y,z;
 +
 +  public SensorElement(float xi, float yi, float zi) {
 +    x = xi;
 +    y = yi;
 +    z = zi;
 +    setRadius(0.1);
 +  }
 +}
 +</code>
 +
 +Using the defined constructor, the position of the //SensorElement// can be parsed to the element. The following code with create a single //SensorElement// at (1, 1, 2).
 +
 +<code java>
 +public void init() [
 + Axiom ==> SensorElement(1, 1, 2);
 +]
 +</code>
 +
 +
 +Now, to construct a sensor grid, we basically just need three nested loops arranging our sensors in a three-dimensional grid. The code to do so could look like this:
 +
 +<code java>
 +{
 +  float xi, yi, zi;
 +}
 +for (int x=0;x<6; x++) (
 +  for (int y=0;y<6; y++) (
 +    for (int z=0;z<3; z++) (
 +      [
 +        {
 +          xi =-2+0.75*x;
 +          yi =-2+0.75*y;
 +          zi =0.5*z; 
 +        }
 +        Null(xi, yi, zi) SensorElement(xi, yi, zi)
 +      ]
 +    )
 +  )
 +)
 +</code>
 +
 +When integrated into a small canopy model of binary trees, the results may look like this (side and top view; Note: the Light source is not totally facing to the middle of the scene, but rather slightly sidewards to get some more interesting distribution pattern):
 +
 +{{ :tutorials:SensorGrid.png?direct&500 |}}
 +
 +Note: The above code works fine as long as the number of sensor nodes is moderate, e.g., below 100. The drawback of the above code it that it connects all sensor nodes to one parent, what, unfortunately, will slow down computation when the number of children is getting too large. If more sensor nodes are required, be advised implementation uses two helper nodes in order to generate an (internal) tree structure instead of sensor nodes. This will nothing change for the user, but will allow easily thousands of sensor nodes if wanted.
 +
 +The 'large number of sensor node save code' would look like given below:
 +<code java>
 +{
 +  float xi, yi, zi;
 +}
 +for (int x=0;x<6; x++) (
 +  [Node
 +  for (int y=0;y<6; y++) (
 +    [Node
 +    for (int z=0;z<3; z++) (
 +      [
 +        {
 +          xi =-2+0.75*x;
 +          yi =-2+0.75*y;
 +          zi =0.5*z; 
 +        }
 +        Null(xi, yi, zi) SensorElement(xi, yi, zi)
 +      ]
 +    ])
 +  ])
 +)
 +</code>
 +
 +
 +The last thing to do in order to make really use of sensor grid is to obtain the sensed radiation for the sensor nodes and also may want to export them for further/external analysis. 
 +
 +<code java>
 +public void simulateLight() {
 +  // perform light calculation
 +  LightModel lm = new LightModel(5000000, 5);
 +  lm.compute();
 +    
 +  //buffer to store the data
 +  StringBuffer sensorData = new StringBuffer();
 +
 +  // see how much was sensed by the sensor
 +  [
 +    x:SensorElement ::> {
 +      sensorData.append(x.x+", "+x.y+",  "+x.z+",  "+lm.getSensedIrradiance(x).integrate()+"\n");
 +    }
 +  ]
 +  
 +  //write the data to a file
 +  BufferedWriter bwr = new BufferedWriter(new FileWriter(new File("SensorGrid.csv")));
 +  bwr.write(sensorData.toString());
 +  bwr.flush();
 +  bwr.close();
 +}
 +</code>
 +
 +
 +The complete code is given below:
 +
 +<code java>
 +//to writing data to a file
 +import java.io.BufferedWriter;
 +import java.io.File;
 +import java.io.FileWriter;
 +import java.io.IOException;
 +
 +const String PATH = "/path/to/your/output/folder/";
 +
 +module SensorElement extends SensorNode() {
 +  float x,y,z;
 +  
 +  public SensorElement(float xi, float yi, float zi) {
 +    x = xi;
 +    y = yi;
 +    z = zi;
 +    setRadius(0.1);
 +  }
 +}
 +
 +
 +module A(float len) extends Sphere(0.1).(setShader(GREEN));
 +
 +protected void init() [
 +  {setSeed(12345);}
 +  Axiom ==>
 + 
 +  //create the sensor grid
 +  {
 +    float xi, yi, zi;
 +  }
 +  for (int x=0;x<6; x++) (
 +    for (int y=0;y<6; y++) (
 +      for (int z=0;z<3; z++) (
 +        [
 +          {
 +            xi =-2+0.75*x;
 +            yi =-2+0.75*y;
 +            zi =0.5*z; 
 +          }
 +          Null(xi, yi, zi) SensorElement(xi, yi, zi)
 +        ]
 +      )
 +    )
 +  )
 +  
 +  //add a lamp
 +  [
 +    Null(0,0,5) RU(170)
 +    LightNode.(setLight(new SpotLight().(
 +      setPower(100),setVisualize(false),
 +      setRaylength(1.5)
 +    )))
 +  ]
 +    
 +  //arrange some plants
 +  for (int i=0;i<25; i++) (
 +    [
 +      Null(random(-2,2),random(-2,2),0)
 +      RH(random(0,360))
 +      Scale(0.5) A(1)
 +    ]
 +  );
 +  {
 +    derive();
 +    for(apply(5)) run();
 +  }
 +]
 +
 +private void run () [
 +  A(x) ==> F(x) [RU(30) RH(90) A(x*0.8)] [RU(-30) RH(90) A(x*0.8)];
 +]
 +
 +public void simulateLight() {
 +  // perform light calculation
 +  LightModel lm = new LightModel(5000000, 5);
 +  lm.compute();
 +    
 +  //buffer to store the data
 +  StringBuffer sensorData = new StringBuffer();
 +
 +  // see how much was sensed by the sensors
 +  [
 +    x:SensorElement ::> {
 +      sensorData.append(x.x+", "+x.y+",  "+x.z+",  "+lm.getSensedIrradiance(x).integrate()+"\n");
 +    }
 +  ]
 +  
 +  //write the data to a file
 +  BufferedWriter bwr = new BufferedWriter(new FileWriter(new File(String.format("%sSensorGrid.csv",PATH))));
 +  bwr.write(sensorData.toString());
 +  bwr.flush();
 +  bwr.close();
 +  
 +  //save a snapshot
 +  makeSnapshot(String.format("%sSensorGrid.png",PATH));
 +}
 +</code>
 +
 +
 +The output of the model will be a database (CSV-file), containing the x, y, and z location and the sensed radiation for each of them. 
 +
 +{{ :tutorials:dataTab.png?direct&250 |}}
 +
 +And when loaded and 3d interpolated using external tools, we nicely can visualize the the 3d light distribution within the canopy.
 +
 +{{ :tutorials:twoD_interpol.png?direct&500 |}}
 +
 +
 +===== Areal Sensors =====
 +
 +In a very similar way, areal sensors can be defined. Instead of //SensorNode// objects, simple flat Box objects are used. Since "real physical" objects are interfering with the 3d scene - what //SensorNodes// are not doing - the here defined type of areal sensors cannot be used within a canopy. Instead they are used for instance on the ground or behind a canopy.
 +
 +In the following, a //SensorElement// is defined as a Box with a black shader. Black to simply absorb any incoming colour.
 +
 +<code java>
 +module SensorElement(float x, float y) extends Box(0.001,dX,dY).(setShader(BLACK));
 +</code>
 +
 +The sensor elements are arranged in 2d to a closed surface using two nested loops. To make things more general, the dimension (x and y) of the grid as well as the resolution of the singles sensor elements can be defined. 
 +
 +
 +<code java>
 +//dimension of wall elements
 +const float WIDTH_X = 1.0;
 +const float WIDTH_Y = 1.0;
 +
 +//dimension of sensor elements
 +const float dX = 0.05;
 +const float dY = 0.05;
 +//number sensor elements
 +const int nX_SE = (int)Math.round(WIDTH_X/dX);
 +const int nY_SE = (int)Math.round(WIDTH_Y/dY);
 +
 +
 +protected void init()[ 
 +  Axiom ==> 
 +    //area sensor on the ground
 +    {
 +      float x,y;
 +    }
 +    [
 +      for(int i:(0:nY_SE)) (
 +        [
 +          Null(0,0,0) //dummy node to prevent having too many nodes on one parent node
 +          {
 +            y = 0.5*WIDTH_Y-i*dY;
 +          }
 +          for(int p:(0:nX_SE))  (
 +            [
 +              {
 +                x = 0.5*WIDTH_X -p*dX;
 +              }
 +              Null(x, y, 0) //move to position 
 +              SensorElement(x,y)
 +            ]
 +          )
 +        ]
 +      )
 +    ];
 +]
 +</code>
 +
 +Depending on the resolution, the numberer of single sensor elements that build the areal sensor, the resulting absorption pattern will change. Obviously, with decreasing element size, the resolution increases and the absorbed pattern will become sharper. The image below uses from left to right sensor element dimension of 0.2x0.2, 0.1x0.1, 0.05x0.05, and 0.005x0.005 meter. Taking the one by one meter areal sensor size, this leads to 36 (1m/0.2m=5; (5+1)*(5+1)=25) up to 40401 (1m/0.0005m=200; (200+1)*(200+1)=40401) sensor elements.
 +
 +{{ :tutorials:ArealSensor.png?direct&500 |}}
 +
 +Queering the //SensorElements// and exporting the data follows precisely the way of the sensor grid. One small difference, of course, is that for visible objects the //getSensedIrradiance// function, as it is used for //SensorNodes//, cannot be applied. Instead, the //getAbsorbedPower// function needs to be called.
 +
 +Additionally, in this example, a //ColorGradient// is applied to colour the sensor elements according to their absorbed radiation. In order to define the right range for the //ColorGradient// instance, the minimal and maximal absorption values needs to be measured first. This is done whenever the //simulateLight// function is called and printed to the console window. The obtained values needs to be set to the //ColorGradient//, consequently, the colouring will be right from the second run one and needs to be redone whenever ether the dimension or the resolution of the areal sensor is changed.
 +
 +<code java>
 +//the colour map for the nice colour gradient
 +const ColorGradient colorMap = new ColorGradient("jet",0.033,0.26, graphState()); //set the min and max values here!
 +
 +public void simulateLight() {
 +...
 +
 +  // see how much was sensed by the sensors
 +  float sumAbsorbedPower = 0;
 +  float min = 10000;
 +  float max = -10000;
 +  [
 +    x:SensorElement ::> {
 +      float absPower = lm.getAbsorbedPower(x).integrate();
 +      if (absPower<min) { min = absPower; }
 +      if (absPower>max) { max = absPower; };
 +      sumAbsorbedPower += absPower;
 +      x.setShader(new RGBAShader(colorMap.getColor(absPower)));
 +    }
 +  ]
 +  println("min/max/total = "+min+", "+max+", "+sumAbsorbedPower);
 +]
 +</code>
 +
 +Further, a ColorBar is inserted to provide some visual reference. It used the defined colour map as input and visualizes it.
 +
 +<code java>
 +protected void init() [
 + ...
 + 
 +  Axiom ==> 
 + ...
 +    // the colour bar as reference
 +    [
 +      Null(-0.75,0,0) Scale(0.3,0.3,0.75) ColorBar(colorMap, true)// colour map, labelling
 +    ]
 +...
 +;
 +]
 +</code>
 +
 +
 +The full code is given below:
 +
 +<code java>
 +import java.awt.Color;
 +
 +//for writing data to a file
 +import java.io.BufferedWriter;
 +import java.io.File;
 +import java.io.FileWriter;
 +import java.io.IOException;
 +
 +
 +//dimension of wall elements
 +const float WIDTH_X = 1.0;
 +const float WIDTH_Y = 1.0;
 +
 +//dimension of sensor elements
 +const float dX = 0.05;
 +const float dY = 0.05;
 +//number sensor elements
 +const int nX_SE = (int)Math.round(WIDTH_X/dX);
 +const int nY_SE = (int)Math.round(WIDTH_Y/dY);
 +
 +const String PATH = "/path/to/your/output/folder/";
 +
 +//the colour map for the nice colour gradient
 +const ColorGradient colorMap = new ColorGradient("jet",0.033,0.26, graphState());
 +
 +//a single sensor element
 +module SensorElement(float x, float y) extends Box(0.001,dX,dY).(setShader(BLACK));
 +
 +
 +////////////////////////////////////////////////////////////////////////////////
 +module A(float len) extends Sphere(0.025).(setShader(GREEN));
 +
 +protected void init() [
 +  {
 +    clearConsole();
 +    println("Number of sensor elements = "+(nX_SE+1)*(nY_SE+1));
 +  }
 +  
 +  Axiom ==> 
 +    //area sensor on the ground
 +    {
 +      float x,y;
 +    }
 +    [
 +    for(int i:(0:nY_SE)) (
 +      [
 +        Null(0,0,0)
 +        {
 +          y = 0.5*WIDTH_Y-i*dY;
 +        }
 +        for(int p:(0:nX_SE))  (
 +          [
 +            {
 +              x = 0.5*WIDTH_X -p*dX;
 +            }
 +            Null(x, y, 0) //move to position 
 +            SensorElement(x,y)
 +          ]
 +        )
 +      ]
 +    )
 +    ]
 +    
 +    //just a plane on the ground
 +    [
 +      M(-0.01) Box(0.001,2,2).(setShader(new RGBAShader(0.1,0.3,0)))
 +    ]
 +    
 +    // the colour bar as reference
 +    [
 +      Null(-0.75,0,0) Scale(0.3,0.3,0.75) ColorBar(colorMap, true)// colour map, labelling
 +    ]
 +    
 +    //add a lamp
 +    [
 +      Null(-0.5,-0.5,2) RH(-45) RU(160)
 +      LightNode.(setLight(new DirectionalLight().(
 +        setPowerDensity(100),setVisualize(false),
 +        setRaylength(1.5)
 +      )))
 +    ]
 +    
 +    // add the plant 'seed'
 +    A(0.25);
 +    
 +  {
 +    derive();
 +    for(apply(5)) run();
 +  }
 +]
 +
 +private void run () [
 +  A(x) ==> F(x,0.02) [RU(30) RH(90) A(x*0.8)] [RU(-30) RH(90) A(x*0.8)];
 +]
 +////////////////////////////////////////////////////////////////////////////////
 +
 +
 +public void simulateLight() {
 +  // perform light calculation
 +  LightModel lm = new LightModel(5000000, 5);
 +  lm.compute();
 +
 +  //buffer to store the data
 +  StringBuffer sensorData = new StringBuffer();
 +    
 +  // see how much was sensed by the sensors
 +  float sumAbsorbedPower = 0;
 +  float min = 10000;
 +  float max = -10000;
 +  [
 +    x:SensorElement ::> {
 +      float absPower = lm.getAbsorbedPower(x).integrate();
 +      if (absPower<min) { min = absPower; }
 +      if (absPower>max) { max = absPower; };
 +      sumAbsorbedPower += absPower;
 +      x.setShader(new RGBAShader(colorMap.getColor(absPower)));
 +      sensorData.append(x.x+", "+x.y+",  "+absPower+"\n");
 +    }
 +  ]
 +  println("min/max/total = "+min+", "+max+", "+sumAbsorbedPower);
 +  repaintView3D();
 +  
 +  //write the data to a file
 +  BufferedWriter bwr = new BufferedWriter(new FileWriter(new File(String.format("%sSensorGround_%.3f_%.3f.csv",PATH,dX,dY))));
 +  bwr.write(sensorData.toString());
 +  bwr.flush();
 +  bwr.close();
 +  
 +  //save a snapshot
 +  makeSnapshot(String.format("%sSensorGround_%.3f_%.3f.png",PATH,dX,dY));
 +}
 +</code>
 +
 +
 +Using the above code and adding two more areal sensors along the XZ- and YZ-plane, we can get visualizations as the following of an adult tomato plant:
 +
 +{{ :tutorials:AreaSensorBox.png?direct&500 |}}
 +