Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New structure with new libraries for InitialPop, Production and Dispersal #364

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Prev Previous commit
Next Next commit
[PL] add dispersal around parent plants
  • Loading branch information
mcwimm committed Nov 21, 2024
commit 4a3f5173b015dcc39e44bb4cccb4ed9badb974f3
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<MangaProject>
<random_seed>643879</random_seed>
<resources>
<aboveground>
<type> AsymmetricZOI </type>
<domain>
<x_1> 0 </x_1>
<y_1> 0 </y_1>
<x_2> 22 </x_2>
<y_2> 22 </y_2>
<x_resolution> 22 </x_resolution>
<y_resolution> 22 </y_resolution>
<allow_interpolation>True</allow_interpolation>
</domain>
</aboveground>
<belowground>
<type> Default </type>
</belowground>
</resources>
<population>
<group>
<name> Initial </name>
<species> Avicennia </species>
<vegetation_model_type> Bettina </vegetation_model_type>
<mortality>NoGrowth</mortality>
<domain>
<x_1> 0 </x_1>
<y_1> 0 </y_1>
<x_2> 22 </x_2>
<y_2> 22 </y_2>
</domain>
<initial_population>
<type>FromFile</type>
<filename>Benchmarks/ModuleBenchmarks/PlantModules/Bettina/ag_initial_population.csv</filename>
</initial_population>
<production>
<type>FixedRate</type>
<n_individuals>50</n_individuals>
<per_individual>True</per_individual>
</production>
<dispersal>
<type> Distance2Parent </type>
<distribution>normal</distribution>
<loc>0</loc>
<scale>0.5</scale>
</dispersal>
</group>
</population>
<time_loop>
<type> Simple </type>
<t_start> 0 </t_start>
<t_end> 1e6 </t_end>
<delta_t> 1e6 </delta_t>
<terminal_print>year</terminal_print>
</time_loop>
<visualization>
<type> NONE </type>
</visualization>
<output>
<type> OneFile </type>
<allow_previous_output>True</allow_previous_output>
<output_dir>Benchmarks/ModuleBenchmarks/Population/Distance2Parent/ReferenceFiles/Distance2Parent</output_dir>
<geometry_output> r_stem </geometry_output>
<growth_output> age </growth_output>
</output>
</MangaProject>

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<MangaProject>
<random_seed>643879</random_seed>
<resources>
<aboveground>
<type> AsymmetricZOI </type>
<domain>
<x_1> 0 </x_1>
<y_1> 0 </y_1>
<x_2> 22 </x_2>
<y_2> 22 </y_2>
<x_resolution> 22 </x_resolution>
<y_resolution> 22 </y_resolution>
<allow_interpolation>True</allow_interpolation>
</domain>
</aboveground>
<belowground>
<type> Default </type>
</belowground>
</resources>
<population>
<group>
<name> Initial </name>
<species> Avicennia </species>
<vegetation_model_type> Bettina </vegetation_model_type>
<mortality>NoGrowth</mortality>
<domain>
<x_1> 0 </x_1>
<y_1> 0 </y_1>
<x_2> 22 </x_2>
<y_2> 22 </y_2>
</domain>
<initial_population>
<type>FromFile</type>
<filename>Benchmarks/ModuleBenchmarks/PlantModules/Bettina/ag_initial_population.csv</filename>
</initial_population>
<production>
<type>SizeDependent</type>
<formula>0.3 + 0.01*200*x</formula>
<x_geometry>r_stem</x_geometry>
<log>True</log>
</production>
<dispersal>
<type> Uniform </type>
</dispersal>
</group>
</population>
<time_loop>
<type> Simple </type>
<t_start> 0 </t_start>
<t_end> 2e6 </t_end>
<delta_t> 1e6 </delta_t>
</time_loop>
<visualization>
<type> NONE </type>
</visualization>
<output>
<type> OneTimestepOneFile </type>
<output_times> [2e6] </output_times>
<allow_previous_output>True</allow_previous_output>
<output_dir>Benchmarks/TestOutputs</output_dir>
<geometry_output> r_stem </geometry_output>
</output>
</MangaProject>

5 changes: 3 additions & 2 deletions PopulationLib/Dispersal/Dispersal.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@ def iniDispersal(self, xml_args):
modul_dir=module_dir,
prj_args=xml_args)

def getPositions(self, number_of_plants):
def getPositions(self, number_of_plants, plants):
"""
Get plant positions from selected dispersal module.
Args:
number_of_plants (int or list of ints): total number of plants to be added to the system or a list of
numbers to be added per existing plant.
plants (dict): plant object, see ``pyMANGA.PopulationLib.PopManager.Plant``
Returns:
dict
"""
return self.dispersal.getPositions(number_of_plants)
return self.dispersal.getPositions(number_of_plants, plants)

def setModelDomain(self, x1, x2, y1, y2):
"""
Expand Down
74 changes: 74 additions & 0 deletions PopulationLib/Dispersal/Distance2Parent/Distance2Parent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Description

Dispersal module that defines the location new plants based on the parents position.

New plants are randomly distributed around the parent tree.
The probability density function is defined by the user.

The size (geometry) and attributes of a plant are taken from the species file (see ``pyMANGA.PopulationLib.Species``).

# Usage

```xml
<dispersal>
<type> Distance2Parent </type>
<distribution>normal</distribution>
<scale>0.5</scale>
<loc>0</loc>
</dispersal>
```

# Attributes

- ``type`` (string): "Random"
- ``distribution`` (str): probability density function, i.e., 'normal', 'uniform', 'exponential'.
- ``scale`` (float): (optional) Distribution parameter. See [numpy random generator](https://numpy.org/doc/stable/reference/random/legacy.html) for specification.
- ``loc`` (float): (optional) Distribution parameter. See [numpy random generator](https://numpy.org/doc/stable/reference/random/legacy.html) for specification.

# Value

see ``pyMANGA.PopulationLib.Dispersal``

# Details
## Purpose

Define the location of new plants added to the model based on the position of the parent plant.

## Process overview
#### getPositions

The position of new seedlings is calculated depending on the parent plant (`xp`, `yp`).
Therefore, the distance between the parent and `N` its seedlings is calculated based on a probability density function (``getDistances``).
Based on the distance (`dist2parent`) the seedlings are randomly distributed around the plant by generating a random ``angle`` (uniform distribution) and using

``python
angle = np.random.uniform(low=0, high=2 * np.pi, size=N)
x_new = xp + dist2parent * np.sin(angle)
y_new = yp + dist2parent * np.cos(angle)
``

#### getDistances

The distance between the parent plant and seedlings is based on a probability density function using the [numpy random generator](https://numpy.org/doc/stable/reference/random/legacy.html).
Therefore, N random numbers are drawn from the chosen distribution, where N is the number of new seedlings produced by the each plant.

N is determined in ``pyMANGA.PopulationLib.Production``.

## Application & Restrictions

- This module needs seed production per individual (see ``pyMANGA.PopulationLib.Production``).

# References

-

# Author(s)

Marie-Christin Wimmler


# See Also

``pyMANGA.PopulationLib.Production``,
``pyMANGA.PopulationLib.Dispersal``,
``pyMANGA.PopulationLib.Species``
96 changes: 96 additions & 0 deletions PopulationLib/Dispersal/Distance2Parent/Distance2Parent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import numpy as np
from ProjectLib import helpers as helpers


class Distance2Parent:
"""
Distance2Parent dispersal module
"""
def __init__(self, xml_args):
"""
Args:
xml_args (lxml.etree._Element): dispersal module specifications from project file tags
"""
self.xml_args = xml_args
self.getInputParameters(args=xml_args)

def getInputParameters(self, args):
tags = {
"prj_file": args,
"required": ["type", "distribution"],
"optional": ["scale", "loc", "high"]
}
myself = super(Distance2Parent, self)
helpers.getInputParameters(myself, **tags)

self.type = self.type.lower()
if not hasattr(self, "loc"):
self.loc = 0

def getPositions(self, number_of_plants, plants):
"""
Return positions of new plants, which are drawn from a user-defined distribution.
Args:
number_of_plants (int or list of int): number of plants that will be added to the model
plants (dict): plant object, see ``pyMANGA.PopulationLib.PopManager.Plant``
Returns:
dict
"""
if np.isscalar(number_of_plants):
print("ERROR: it seems <production><per_individual> is False but needs to be True.")
exit()

self.xi, self.yi = [], []
for i in range(len(plants)):
plant = plants[i]
xp, yp = plant.getPosition()
n = int(number_of_plants[i])
dist2parent = self.getDistances(number_of_plants=n)
angle = np.random.uniform(low=0, high=2 * np.pi, size=n)
self.xi.append(xp + dist2parent * np.sin(angle))
self.yi.append(yp + dist2parent * np.cos(angle))

# Flatten lists
x = np.array(self.xi).flat
y = np.array(self.yi).flat

# Drop seeds that are outside the model domain
idx = np.where((x < self.x_1) | (x > self.x_2) | (y < self.y_1) | (y > self.y_2))
x = np.delete(x, idx)
y = np.delete(y, idx)

plant_positions = {"x": x, "y": y}
return plant_positions

def getDistances(self, number_of_plants):
"""
Return distances between seedlings and parent plant.
Args:
number_of_plants (int): number of plants that will be added to the model
Returns:
list
"""
try:
if self.distribution in "normal":
return np.random.normal(loc=self.loc, scale=self.scale, size=number_of_plants)

if self.distribution in "uniform":
return np.random.uniform(size=number_of_plants, high=self.high)

if self.distribution in "exponential":
return np.random.exponential(scale=self.scale, size=number_of_plants)
except AttributeError:
print("ERROR: A parameter defining the probability density function is missing. "
"Check the documentation of <dispersal>.")
exit()

def setModelDomain(self, x1, x2, y1, y2):
"""
Adds model domain boundaries to the object.
Args:
x1 (float): x-coordinate of left bottom border of grid
x2 (float): x-coordinate of right bottom border of grid
y1 (float): y-coordinate of left top border of grid
y2 (float): y-coordinate of right top border of grid
"""
helpers.setModelDomain(self, x1, x2, y1, y2)
4 changes: 4 additions & 0 deletions PopulationLib/Dispersal/Distance2Parent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
.. include:: ./Distance2Parent.md
"""
from .Distance2Parent import Distance2Parent
2 changes: 1 addition & 1 deletion PopulationLib/Dispersal/Uniform/Uniform.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Description

Dispersal module that defines the location of initial and new plants.
Dispersal module that defines the location new plants.

New plants are randomly distributed in the model domain.

Expand Down
6 changes: 3 additions & 3 deletions PopulationLib/Dispersal/Uniform/Uniform.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Uniform:
def __init__(self, xml_args):
"""
Args:
xml_args (lxml.etree._Element): distribution module specifications from project file tags
xml_args (lxml.etree._Element): dispersal module specifications from project file tags
"""
self.xml_args = xml_args
self.getInputParameters(args=xml_args)
Expand All @@ -22,11 +22,11 @@ def getInputParameters(self, args):
myself = super(Uniform, self)
helpers.getInputParameters(myself, **tags)

def getPositions(self, number_of_plants):
def getPositions(self, number_of_plants, plants):
"""
Return positions of new plants, which are drawn from a uniform distribution.
Args:
number_of_plants (int): number of plants that will be added to the model
number_of_plants (int or list of int): number of plants that will be added to the model
Returns:
dict
"""
Expand Down
6 changes: 3 additions & 3 deletions PopulationLib/Dispersal/Weighted/Weighted.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Weighted:
def __init__(self, xml_args):
"""
Args:
xml_args (lxml.etree._Element): distribution module specifications from project file tags
xml_args (lxml.etree._Element): dispersal module specifications from project file tags
"""
self.xml_args = xml_args
self.getInputParameters(args=xml_args)
Expand Down Expand Up @@ -48,13 +48,13 @@ def readWeightsFile(self):
if np.max(self.weights) > 1:
print("WARNING: dispersal weights are > 1.")

def getPositions(self, number_of_plants):
def getPositions(self, number_of_plants, plants):
"""
Return positions of new plants, which are drawn from a weighted uniform distribution.
Credits: https://stackoverflow.com/a/15205104
Credits: http://www.sciencedirect.com/science/article/pii/S002001900500298X
Args:
number_of_plants (int): number of plants that will be added to the model
number_of_plants (int or list of int): number of plants that will be added to the model
Returns:
dict
"""
Expand Down
3 changes: 2 additions & 1 deletion PopulationLib/InitialPop/Random/Random.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def getPositions(self):
dict
"""
plant_positions = Uniform.getPositions(self=self,
number_of_plants=self.n_individuals)
number_of_plants=self.n_individuals,
plants={})
return plant_positions

def setModelDomain(self, x1, x2, y1, y2):
Expand Down
Loading
Loading