From 0a2d454ff5a107bca26670ea313f23a391c4edf6 Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Mon, 26 Jul 2021 16:49:42 +0200 Subject: [PATCH 01/11] Reset versions for develop branch --- CHANGELOG.md | 2 ++ CITATION.cff | 4 ++-- CMakeLists.txt | 2 +- codemeta.json | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2258ba44..5c11ed41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog][] by Olivier Lacan, and this project a [Keep a Changelog]: [Semantic Versioning]: +## [Unreleased] + ## [1.1.1] - 2021-07-26 ### Fixed diff --git a/CITATION.cff b/CITATION.cff index 957e89d5..10ce0194 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -10,9 +10,9 @@ authors: affiliation: Senckenberg Biodiversity and Climate Research Centre orcid: https://orcid.org/0000-0003-4925-7248 title: "Modular Megafauna Model" -version: 1.1.1 +version: 0.0.0 # This DOI represents all versions, and will always resolve to the latest one. doi: 10.5281/zenodo.4710254 -date-released: 2021-07-26 +date-released: license: LGPL-3.0-or-later repository-code: https://github.com/wtraylor/modular_megafauna_model diff --git a/CMakeLists.txt b/CMakeLists.txt index 8378554b..5f06f587 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required (VERSION 3.10) project ("Modular Megafauna Model" # This version must match exactly the Git tag! # Compare the "check_version" job in ".gitlab-ci.yml". - VERSION 1.1.1 + VERSION 0.0.0 DESCRIPTION "A physiological, dynamic herbivore simulator in C++." LANGUAGES CXX ) diff --git a/codemeta.json b/codemeta.json index 9b396b3b..c62b7779 100644 --- a/codemeta.json +++ b/codemeta.json @@ -5,10 +5,10 @@ "codeRepository": "git+https://github.com/wtraylor/modular_megafauna_model.git", "contIntegration": "https://gitlab.com/wtraylor/modular_megafauna_model/pipelines", "datePublished": "2021-04-26", - "dateModified": "2021-07-26", + "dateModified": "", "issueTracker": "https://github.com/wtraylor/modular_megafauna_model/issues/", "name": "Modular Megafauna Model (MMM)", - "version": "1.1.1", + "version": "0.0.0", "identifier": "10.5281/zenodo.4710254. ", "description": "Extendable, dynamic, process-based herbivore simulator that can be integrated into a dynamic vegetation model. Written as a C++ library.\n", "applicationCategory": "Ecological Modeling", From 31998df2be852b9693b073850e233642ea7a53a6 Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 09:06:55 +0200 Subject: [PATCH 02/11] Plot only existing files in demo_results.Rmd close gh-39 --- CHANGELOG.md | 4 ++ tools/demo_simulator/demo_results.Rmd | 55 +++++++++++++-------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c11ed41..d802ac02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ The format is based on [Keep a Changelog][] by Olivier Lacan, and this project a ## [Unreleased] +### Added +- `demo_results.Rmd` now only tries to plot those files that are available [#39] + ## [1.1.1] - 2021-07-26 ### Fixed @@ -282,6 +285,7 @@ The format is based on [Keep a Changelog][] by Olivier Lacan, and this project a [#31]: https://github.com/wtraylor/modular_megafauna_model/issues/31 [#32]: https://github.com/wtraylor/modular_megafauna_model/issues/32 [#34]: https://github.com/wtraylor/modular_megafauna_model/issues/34 +[#39]: https://github.com/wtraylor/modular_megafauna_model/issues/39 [#41]: https://github.com/wtraylor/modular_megafauna_model/issues/41 [#43]: https://github.com/wtraylor/modular_megafauna_model/issues/43 [#44]: https://github.com/wtraylor/modular_megafauna_model/issues/44 diff --git a/tools/demo_simulator/demo_results.Rmd b/tools/demo_simulator/demo_results.Rmd index c69fb6fc..42818457 100644 --- a/tools/demo_simulator/demo_results.Rmd +++ b/tools/demo_simulator/demo_results.Rmd @@ -6,10 +6,16 @@ SPDX-License-Identifier: CC0-1.0 # Modular Megafauna Output -```{r include = FALSE} +```{r setup, include = FALSE} library(ggplot2) library(tidyr) +# "timestep" is the output interval: a day, a year, (roughly) a month, or a decade. +get_timestep <- function(df){ + unique_years.v <- sort(unique(df$year)) + timestep <- abs(unique_years.v[2] - unique_years.v[1]) +} + # READ TABLES read_generic_table <- function(filename){ @@ -43,33 +49,15 @@ read_per_hft_per_forage_table <- function(filename){ gather(tbl, key = HFT, value = VALUE, !all_of(fixed_vars)) } -# Read individual tables - -available.tbl <- read_per_forage_type_table("available_forage.tsv") -available.tbl$VALUE <- available.tbl$VALUE * 10^-6 # convert kg/km² to kg/m² - -bodyfat.tbl <- read_per_hft_table("body_fat.tsv") - -eaten.tbl <- read_per_hft_per_forage_table("eaten_forage_per_ind.tsv") - -eaten_nitrogen.tbl <- read_per_hft_table("eaten_nitrogen_per_ind.tsv") - -mass.tbl <- read_per_hft_table("mass_density.tsv") -mass.tbl$VALUE <- mass.tbl$VALUE / 100 # convert kg/km² to kg/ha - -inddens.tbl <- read_per_hft_table("individual_density.tsv") -inddens.tbl$VALUE <- inddens.tbl$VALUE / 100 # convert ind/km² to ind/ha - -# `timestep` is the output interval: a day, a year, (roughly) a month, or a decade. -unique_years.v <- sort(unique(mass.tbl$year)) -timestep <- abs(unique_years.v[2] - unique_years.v[1]) - # The first of each month as Julian day: for drawing month axis labels. julian_month_days <- julian(as.Date(paste(1:12, "1 1970"), "%m %d %Y")) ``` -```{r echo = FALSE} +```{r mass_density, echo = FALSE, eval = file.exists("mass_density.tsv")} +mass.tbl <- read_per_hft_table("mass_density.tsv") +mass.tbl$VALUE <- mass.tbl$VALUE / 100 # convert kg/km² to kg/ha # We plot the points not in the beginning of each time step, but in the middle. +timestep <- get_timestep(mass.tbl) ggplot(mass.tbl, aes(x = year + timestep / 2, y = VALUE, color = HFT)) + stat_summary(fun = mean, geom = "line") + coord_cartesian(xlim = c(min(mass.tbl$year), max(mass.tbl$year) + timestep)) + @@ -90,8 +78,11 @@ ggplot(mass.tbl, aes(x = year + timestep / 2, y = VALUE, color = HFT)) + ) ``` -```{r echo = FALSE} +```{r individual_density, echo = FALSE, eval = file.exists("individual_density.tsv")} +inddens.tbl <- read_per_hft_table("individual_density.tsv") +inddens.tbl$VALUE <- inddens.tbl$VALUE / 100 # convert ind/km² to ind/ha # We plot the points not in the beginning of each time step, but in the middle. +timestep <- get_timestep(inddens.tbl) ggplot(inddens.tbl, aes(x = year + timestep / 2, y = VALUE, color = HFT)) + stat_summary(fun = mean, geom = "line") + coord_cartesian( @@ -114,8 +105,10 @@ ggplot(inddens.tbl, aes(x = year + timestep / 2, y = VALUE, color = HFT)) + ) ``` -```{r echo = FALSE} +```{r eaten_forage, echo = FALSE, eval = file.exists("eaten_forage_per_ind.tsv")} # Timeline with annual means +eaten.tbl <- read_per_hft_per_forage_table("eaten_forage_per_ind.tsv") +timestep <- get_timestep(eaten.tbl) ggplot( eaten.tbl, aes(x = year + timestep / 2, y = VALUE, color = HFT, linetype = forage_type) @@ -150,8 +143,11 @@ if ("day" %in% names(eaten.tbl)) { } ``` -```{r echo = FALSE} +```{r available_forage, echo = FALSE, eval = file.exists("available_forage.tsv")} # Timeline with annual means +available.tbl <- read_per_forage_type_table("available_forage.tsv") +available.tbl$VALUE <- available.tbl$VALUE * 10^-6 # convert kg/km² to kg/m² +timestep <- get_timestep(available.tbl) ggplot(available.tbl, aes(x = year + timestep / 2, y = VALUE, color = forage_type)) + stat_summary(fun = mean, geom = "line") + coord_cartesian(xlim = c(min(available.tbl$year), max(available.tbl$year) + timestep)) + @@ -180,7 +176,8 @@ if ("day" %in% names(available.tbl)) { ``` -```{r echo = FALSE} +```{r body_fat, echo = FALSE, eval = file.exists("body_fat.tsv")} +bodyfat.tbl <- read_per_hft_table("body_fat.tsv") if ("day" %in% names(bodyfat.tbl)) { bodyfat.tbl %>% ggplot(aes(x = day, y = VALUE, color = HFT)) + @@ -210,7 +207,9 @@ if ("day" %in% names(bodyfat.tbl)) { } ``` -```{r echo = FALSE} +```{r eaten_nitrogen, echo = FALSE, eval = file.exists("eaten_nitrogen_per_ind.tsv")} +eaten_nitrogen.tbl <- read_per_hft_table("eaten_nitrogen_per_ind.tsv") +timestep <- get_timestep(eaten_nitrogen.tbl) ggplot(eaten_nitrogen.tbl, aes(x = year + timestep / 2, y = VALUE, color = HFT)) + stat_summary(fun = mean, geom = "line") + coord_cartesian( From 30565b3221040b7212a87900ee6eb111271819e3 Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 09:21:33 +0200 Subject: [PATCH 03/11] Allow one_hft_per_habitat only for "Cohorts" --- CHANGELOG.md | 3 +++ src/Fauna/parameters.cpp | 7 +++++++ src/Fauna/world.cpp | 1 + src/Fauna/world.test.cpp | 1 + src/Fauna/world_constructor.cpp | 3 ++- 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d802ac02..cec5e354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ The format is based on [Keep a Changelog][] by Olivier Lacan, and this project a ### Added - `demo_results.Rmd` now only tries to plot those files that are available [#39] +### Fixed +- Allow `simulation.one_hft_per_habitat` only for `herbivore_type = "Cohort"` + ## [1.1.1] - 2021-07-26 ### Fixed diff --git a/src/Fauna/parameters.cpp b/src/Fauna/parameters.cpp index 8c51b285..362b1bcb 100644 --- a/src/Fauna/parameters.cpp +++ b/src/Fauna/parameters.cpp @@ -22,6 +22,13 @@ bool Parameters::is_valid(std::string& messages) const { //------------------------------------------------------------ // add new checks in alphabetical order + if (one_hft_per_habitat && (herbivore_type != HerbivoreType::Cohort)) { + stream << "'simulation.one_hft_per_habitat' is only defined for " + "'simulation.herbivore_type = \"cohort\"'." + << std::endl; + is_valid = false; + } + if (herbivore_establish_interval < 0) { stream << "herbivore_establish_interval must be >=0" << std::endl; is_valid = false; diff --git a/src/Fauna/world.cpp b/src/Fauna/world.cpp index aab8c7bc..58bef776 100644 --- a/src/Fauna/world.cpp +++ b/src/Fauna/world.cpp @@ -172,6 +172,7 @@ void World::simulate_day(const Date& date, const bool do_herbivores) { // differ. Compare the habitat count against HFT count only if necessary. const int habitat_count = get_habitat_count_per_agg_unit(); if (get_params().one_hft_per_habitat && + (get_params().herbivore_type == HerbivoreType::Cohort) && (habitat_count % get_hfts().size() != 0)) throw std::logic_error( "Fauna::World::simulate_day() " diff --git a/src/Fauna/world.test.cpp b/src/Fauna/world.test.cpp index 1709b68b..8fc34fd3 100644 --- a/src/Fauna/world.test.cpp +++ b/src/Fauna/world.test.cpp @@ -168,6 +168,7 @@ TEST_CASE("FAUNA::World", "") { SECTION("Habitat count as multiple of HFT count") { std::shared_ptr params(new Parameters); params->one_hft_per_habitat = true; + REQUIRE(params->herbivore_type == HerbivoreType::Cohort); World world(params, HFTLIST); REQUIRE(HFTLIST->size() > 1); diff --git a/src/Fauna/world_constructor.cpp b/src/Fauna/world_constructor.cpp index dbd3aa31..4b99ec24 100644 --- a/src/Fauna/world_constructor.cpp +++ b/src/Fauna/world_constructor.cpp @@ -38,7 +38,8 @@ PopulationList* WorldConstructor::create_populations( assert(!get_hftlist().empty()); if (get_params().herbivore_type == HerbivoreType::Cohort) { - if (get_params().one_hft_per_habitat) { + if (get_params().one_hft_per_habitat && + (get_params().herbivore_type == HerbivoreType::Cohort)) { // Create only one HFT, i.e. one population. const int hft_idx = habitat_ctr_in_agg_unit % get_hftlist().size(); assert(hft_idx >= 0); From aa1ec3c08e505cf5f802ae0684da3ff3f218941f Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 09:36:06 +0200 Subject: [PATCH 04/11] Check if HFTs were correctly created in habitats --- src/Fauna/world.test.cpp | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Fauna/world.test.cpp b/src/Fauna/world.test.cpp index 8fc34fd3..401c32b0 100644 --- a/src/Fauna/world.test.cpp +++ b/src/Fauna/world.test.cpp @@ -10,10 +10,12 @@ */ #include "world.h" #include "catch.hpp" +#include "cohort_population.h" #include "date.h" #include "dummy_habitat.h" #include "dummy_hft.h" #include "parameters.h" +#include "simulation_unit.h" using namespace Fauna; TEST_CASE("FAUNA::World", "") { @@ -188,6 +190,17 @@ TEST_CASE("FAUNA::World", "") { std::shared_ptr(new DummyHabitat("3"))); } CHECK_NOTHROW(world.simulate_day(Date(0, 0), true)); + // Check that all HFT populations are created. + for (const auto& hft : *HFTLIST) { + int hft_ctr = 0; + for (const auto& sim_unit : world.get_sim_units()) { + REQUIRE(sim_unit.get_populations().size() == 1); + const CohortPopulation* pop = + (CohortPopulation*)sim_unit.get_populations().front().get(); + if (pop->get_hft() == *hft) hft_ctr++; + } + CHECK(hft_ctr == 3); // This HFT once in each of the 3 agg. units + } } SECTION("Habitat count == 2 * HFT count") { @@ -201,10 +214,21 @@ TEST_CASE("FAUNA::World", "") { std::shared_ptr(new DummyHabitat("3"))); } CHECK_NOTHROW(world.simulate_day(Date(0, 0), true)); + // Check that all HFT populations are created. + for (const auto& hft : *HFTLIST) { + int hft_ctr = 0; + for (const auto& sim_unit : world.get_sim_units()) { + REQUIRE(sim_unit.get_populations().size() == 1); + const CohortPopulation* pop = + (CohortPopulation*)sim_unit.get_populations().front().get(); + if (pop->get_hft() == *hft) hft_ctr++; + } + CHECK(hft_ctr == 2 * 3); // HFT 2 times in each of the 3 agg. units + } } SECTION("Habitat count == 3 * HFT count") { - // Create 3 aggregation units with 3 habitats for each HFT. + // Create 4 aggregation units with 3 habitats for each HFT. for (int i = 0; i < 3 * HFTLIST->size(); i++) { world.create_simulation_unit( std::shared_ptr(new DummyHabitat("1"))); @@ -212,8 +236,21 @@ TEST_CASE("FAUNA::World", "") { std::shared_ptr(new DummyHabitat("2"))); world.create_simulation_unit( std::shared_ptr(new DummyHabitat("3"))); + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("4"))); } CHECK_NOTHROW(world.simulate_day(Date(0, 0), true)); + // Check that all HFT populations are created. + for (const auto& hft : *HFTLIST) { + int hft_ctr = 0; + for (const auto& sim_unit : world.get_sim_units()) { + REQUIRE(sim_unit.get_populations().size() == 1); + const CohortPopulation* pop = + (CohortPopulation*)sim_unit.get_populations().front().get(); + if (pop->get_hft() == *hft) hft_ctr++; + } + CHECK(hft_ctr == 3 * 4); // HFT 3 times in each of the 4 agg. units + } } SECTION("Habitat count == 1") { From 0905fd22994e2ed437bcd6438b820e60626534e9 Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 09:52:01 +0200 Subject: [PATCH 05/11] Add unit test for one_hft_per_habitat == false --- src/Fauna/world.test.cpp | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/Fauna/world.test.cpp b/src/Fauna/world.test.cpp index 401c32b0..3092e4b3 100644 --- a/src/Fauna/world.test.cpp +++ b/src/Fauna/world.test.cpp @@ -167,6 +167,91 @@ TEST_CASE("FAUNA::World", "") { } } + SECTION("Every HFT in every habitat") { + REQUIRE(PARAMS->herbivore_type == HerbivoreType::Cohort); + REQUIRE(PARAMS->one_hft_per_habitat == false); + World world(PARAMS, HFTLIST); + REQUIRE(HFTLIST->size() > 1); + + SECTION("1 habitat, 1 aggregation unit") { + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("1"))); + CHECK_NOTHROW(world.simulate_day(Date(0, 0), true)); + // Check all HFTs are in each habitat. + for (const auto& sim_unit : world.get_sim_units()) { + REQUIRE(sim_unit.get_populations().size() == HFTLIST->size()); + for (const auto& hft : *HFTLIST) { + bool hft_found = false; + for (const auto& pop : sim_unit.get_populations()) + hft_found |= (((CohortPopulation*)pop.get())->get_hft() == *hft); + CHECK(hft_found); + } + } + } + + SECTION("2 habitats, 1 aggregation unit") { + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("1"))); + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("1"))); + CHECK_NOTHROW(world.simulate_day(Date(0, 0), true)); + // Check all HFTs are in each habitat. + for (const auto& sim_unit : world.get_sim_units()) { + REQUIRE(sim_unit.get_populations().size() == HFTLIST->size()); + for (const auto& hft : *HFTLIST) { + bool hft_found = false; + for (const auto& pop : sim_unit.get_populations()) + hft_found |= (((CohortPopulation*)pop.get())->get_hft() == *hft); + CHECK(hft_found); + } + } + } + + SECTION("1 habitats, 2 aggregation unit") { + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("1"))); + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("2"))); + CHECK_NOTHROW(world.simulate_day(Date(0, 0), true)); + // Check all HFTs are in each habitat. + for (const auto& sim_unit : world.get_sim_units()) { + REQUIRE(sim_unit.get_populations().size() == HFTLIST->size()); + for (const auto& hft : *HFTLIST) { + bool hft_found = false; + for (const auto& pop : sim_unit.get_populations()) + hft_found |= (((CohortPopulation*)pop.get())->get_hft() == *hft); + CHECK(hft_found); + } + } + } + + SECTION("2 habitats, 3 aggregation unit") { + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("1"))); + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("1"))); + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("2"))); + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("2"))); + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("3"))); + world.create_simulation_unit( + std::shared_ptr(new DummyHabitat("3"))); + CHECK_NOTHROW(world.simulate_day(Date(0, 0), true)); + // Check all HFTs are in each habitat. + for (const auto& sim_unit : world.get_sim_units()) { + REQUIRE(sim_unit.get_populations().size() == HFTLIST->size()); + for (const auto& hft : *HFTLIST) { + bool hft_found = false; + for (const auto& pop : sim_unit.get_populations()) + hft_found |= (((CohortPopulation*)pop.get())->get_hft() == *hft); + CHECK(hft_found); + } + } + } + } + SECTION("Habitat count as multiple of HFT count") { std::shared_ptr params(new Parameters); params->one_hft_per_habitat = true; From 045843cf90f0abbeeef2fd5bc5d488f47a1b9161 Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 09:53:16 +0200 Subject: [PATCH 06/11] Remove redundant check for herbivore_type = cohort --- src/Fauna/world_constructor.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Fauna/world_constructor.cpp b/src/Fauna/world_constructor.cpp index 4b99ec24..dbd3aa31 100644 --- a/src/Fauna/world_constructor.cpp +++ b/src/Fauna/world_constructor.cpp @@ -38,8 +38,7 @@ PopulationList* WorldConstructor::create_populations( assert(!get_hftlist().empty()); if (get_params().herbivore_type == HerbivoreType::Cohort) { - if (get_params().one_hft_per_habitat && - (get_params().herbivore_type == HerbivoreType::Cohort)) { + if (get_params().one_hft_per_habitat) { // Create only one HFT, i.e. one population. const int hft_idx = habitat_ctr_in_agg_unit % get_hftlist().size(); assert(hft_idx >= 0); From 5d1d44e29ab76d6c205793e7e984b01d9ebc0a5c Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 10:01:20 +0200 Subject: [PATCH 07/11] Fix non-functional one_hft_per_habitat The counter for how many habitats were already created in an aggregation unit was not correctly incremented because I compared char* with each other. --- CHANGELOG.md | 1 + src/Fauna/world.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cec5e354..c8bb0627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog][] by Olivier Lacan, and this project a ### Fixed - Allow `simulation.one_hft_per_habitat` only for `herbivore_type = "Cohort"` +- Issue that only one HFT got created if `simulation.one_hft_per_habitat = true` ## [1.1.1] - 2021-07-26 diff --git a/src/Fauna/world.cpp b/src/Fauna/world.cpp index 58bef776..bda7defc 100644 --- a/src/Fauna/world.cpp +++ b/src/Fauna/world.cpp @@ -75,10 +75,12 @@ void World::create_simulation_unit(std::shared_ptr habitat) { int habitat_ctr = 0; // Find the number of habitats already created in this aggregation unit. - for (const auto& sim_unit : sim_units) - if (sim_unit.get_habitat().get_aggregation_unit() == - habitat->get_aggregation_unit()) - habitat_ctr++; + const std::string this_agg_unit(habitat->get_aggregation_unit()); + for (const auto& sim_unit : sim_units) { + const std::string existing_agg_unit( + sim_unit.get_habitat().get_aggregation_unit()); + if (this_agg_unit == existing_agg_unit) habitat_ctr++; + } PopulationList* populations = world_constructor->create_populations(habitat_ctr); assert(populations); From 0902c6b14181732fed5470059fd2e915991419eb Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 10:33:06 +0200 Subject: [PATCH 08/11] Make SimpleHabitat::settings const There is no reason to change the settings during simulation --- tools/demo_simulator/simple_habitat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/demo_simulator/simple_habitat.h b/tools/demo_simulator/simple_habitat.h index 03df9618..1111befd 100644 --- a/tools/demo_simulator/simple_habitat.h +++ b/tools/demo_simulator/simple_habitat.h @@ -68,7 +68,7 @@ class SimpleHabitat : public Habitat { private: const std::string aggregation_unit; - SimpleHabitat::Parameters settings; + const SimpleHabitat::Parameters settings; /// Air temperature in °C, as read from /// \ref SimpleHabitat::Parameters::air_temperature. From e2f7c84bad13ca9b3153f1c129d8a5a4586b7477 Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 10:34:28 +0200 Subject: [PATCH 09/11] =?UTF-8?q?Remove=20TODO=20note=20that=20I=20don?= =?UTF-8?q?=E2=80=99t=20understand=20anymore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Fauna/world.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Fauna/world.cpp b/src/Fauna/world.cpp index bda7defc..809c0cb2 100644 --- a/src/Fauna/world.cpp +++ b/src/Fauna/world.cpp @@ -233,8 +233,6 @@ void World::simulate_day(const Date& date, const bool do_herbivores) { if (do_herbivores) days_since_last_establishment++; // Create function object to delegate all simulations for this day to. - // TODO: Create function object only once per day and for all simulation - // units. SimulateDay simulate_day(date.get_julian_day(), sim_unit, feed_herbivores); // Call the function object. From 6f69336a5e8ba9768c85b4f16366a76e127b7459 Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 10:52:22 +0200 Subject: [PATCH 10/11] Tidy up Fauna::World Remove World::is_activated() and dummy constructor World::World() because they are not used at all. --- include/Fauna/world.h | 15 ++------------- src/Fauna/world.cpp | 25 ++++++++++++------------- src/Fauna/world.test.cpp | 20 ++++++++++---------- 3 files changed, 24 insertions(+), 36 deletions(-) diff --git a/include/Fauna/world.h b/include/Fauna/world.h index 180fda25..f55defc0 100644 --- a/include/Fauna/world.h +++ b/include/Fauna/world.h @@ -70,14 +70,6 @@ class World { World(const std::shared_ptr params, const std::shared_ptr hftlist); - /// Constructor: Create deactivated `World` object. - /** - * Even if the megafauna model should be completely deactivated, there might - * be a need to create a "dummy" \ref World instance. An object created with - * this constructor will not simulate anything. - */ - World(); - /// Default destructor. ~World(); @@ -115,9 +107,6 @@ class World { */ const std::list& get_sim_units() const { return sim_units; } - /// Whether this \ref World object has been created with parameters and HFTs. - const bool is_activated() const { return activated; } - /// Iterate through all simulation units and perform simulation for this day. /** * This is the central access point to start the perform the herbivore @@ -166,8 +155,8 @@ class World { */ Output::WriterInterface* construct_output_writer() const; - /// Whether the whole model is active or not. - const bool activated; + /// Whether this object is going to simulate or just lint an instruction file. + const SimMode mode = SimMode::Simulate; /// Whether the habitat counts per aggregation unit have been checked. /** diff --git a/src/Fauna/world.cpp b/src/Fauna/world.cpp index 809c0cb2..8bb5d420 100644 --- a/src/Fauna/world.cpp +++ b/src/Fauna/world.cpp @@ -45,23 +45,27 @@ Output::WriterInterface* World::construct_output_writer() const { } World::World(const std::string instruction_filename, const SimMode mode) - : activated(true), + : mode(mode), insfile(read_instruction_file(instruction_filename)), days_since_last_establishment(get_params().herbivore_establish_interval), output_aggregator(new Output::Aggregator()), output_writer(mode == SimMode::Lint ? NULL : construct_output_writer()), world_constructor(new WorldConstructor(insfile.params, get_hfts())) {} -World::World() : activated(false) {} - World::World(const std::shared_ptr params, const std::shared_ptr hftlist) - : activated(true), - insfile({hftlist, params}), + : insfile({hftlist, params}), days_since_last_establishment(get_params().herbivore_establish_interval), output_aggregator(new Output::Aggregator()), output_writer(construct_output_writer()), - world_constructor(new WorldConstructor(insfile.params, get_hfts())) {} + world_constructor(new WorldConstructor(insfile.params, get_hfts())) { + if (params.get() == NULL) + throw std::invalid_argument( + "Fauna::World::World() The argument 'params' is NULL."); + if (hftlist.get() == NULL) + throw std::invalid_argument( + "Fauna::World::World() The argument 'hftlist' is NULL."); +} // The destructor must be implemented here in the source file, where the // forward-declared types are complete. @@ -71,7 +75,7 @@ void World::create_simulation_unit(std::shared_ptr habitat) { if (habitat == NULL) throw std::invalid_argument( "World::create_simulation_unit(): Pointer to habitat is NULL."); - if (!activated) return; + if (mode != SimMode::Simulate) return; int habitat_ctr = 0; // Find the number of habitats already created in this aggregation unit. @@ -102,11 +106,6 @@ const HftList& World::get_hfts() const { } const Parameters& World::get_params() const { - if (!activated) - throw std::logic_error( - "Fauna::World::get_params() " - "The megafauna model was created without an instruction file. " - "Parameters are not available."); if (!insfile.params) throw std::logic_error( "Fauna::World::get_params() " @@ -166,7 +165,7 @@ World::InsfileContent World::read_instruction_file( } void World::simulate_day(const Date& date, const bool do_herbivores) { - if (!activated) return; + if (mode != SimMode::Simulate) return; // Sanity checks for simulation units if (!simulation_units_checked) { diff --git a/src/Fauna/world.test.cpp b/src/Fauna/world.test.cpp index 3092e4b3..9a135769 100644 --- a/src/Fauna/world.test.cpp +++ b/src/Fauna/world.test.cpp @@ -19,23 +19,23 @@ using namespace Fauna; TEST_CASE("FAUNA::World", "") { - SECTION("Dummy Constructor") { - World w; - REQUIRE(!w.is_activated()); - CHECK_THROWS(w.get_params()); - CHECK_THROWS(w.create_simulation_unit(NULL)); - CHECK_NOTHROW(w.simulate_day(Date(1, 2), true)); - CHECK_NOTHROW(w.simulate_day(Date(1, 2), false)); - } - static const std::shared_ptr PARAMS(new Parameters); static const std::shared_ptr HFTLIST(create_hfts(3, *PARAMS)); + + CHECK_THROWS(World(NULL, NULL)); + CHECK_THROWS(World(NULL, HFTLIST)); + CHECK_THROWS(World(PARAMS, NULL)); + REQUIRE_NOTHROW(World(PARAMS, HFTLIST)); + CHECK_THROWS(World(PARAMS, HFTLIST).create_simulation_unit(NULL)); + CHECK_NOTHROW(World(PARAMS, HFTLIST).get_params()); + CHECK_NOTHROW(World(PARAMS, HFTLIST).simulate_day(Date(1, 2), true)); + CHECK_NOTHROW(World(PARAMS, HFTLIST).simulate_day(Date(1, 2), false)); + SECTION("Unit test constructor") { CHECK_THROWS(World(NULL, NULL)); CHECK_THROWS(World(PARAMS, NULL)); CHECK_THROWS(World(NULL, HFTLIST)); CHECK_NOTHROW(World(PARAMS, HFTLIST)); - REQUIRE(World(PARAMS, HFTLIST).is_activated()); REQUIRE(World(PARAMS, HFTLIST).get_sim_units().empty()); } From 73313227e454aa892b6ee23d3017d61c01bdff7c Mon Sep 17 00:00:00 2001 From: Wolfgang Traylor Date: Tue, 27 Jul 2021 11:55:31 +0200 Subject: [PATCH 11/11] Fix output if one_hft_per_habitat==true Fauna::Output::CombinedData::merge() had a bad for loop to check the HFTs/groups from the to-be-merged other object. This way the data from the `other` object always got dropped. --- CHANGELOG.md | 2 +- src/Fauna/Output/combined_data.cpp | 24 +++++++++++---------- src/Fauna/Output/combined_data.test.cpp | 28 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8bb0627..95538758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ The format is based on [Keep a Changelog][] by Olivier Lacan, and this project a ### Fixed - Allow `simulation.one_hft_per_habitat` only for `herbivore_type = "Cohort"` -- Issue that only one HFT got created if `simulation.one_hft_per_habitat = true` +- Issue that only one HFT got created and outputted if `simulation.one_hft_per_habitat = true` ## [1.1.1] - 2021-07-26 diff --git a/src/Fauna/Output/combined_data.cpp b/src/Fauna/Output/combined_data.cpp index dbe62ff3..1b2ce0bb 100644 --- a/src/Fauna/Output/combined_data.cpp +++ b/src/Fauna/Output/combined_data.cpp @@ -33,23 +33,25 @@ CombinedData& CombinedData::merge(const CombinedData& other) { // HERBIVORE DATA // Merge herbivore data for each output group. + // Groups can be for instance HFTs. typedef std::map GroupMap; - // First, create empty HerbivoreData objects for all groups that are not - // yet present in this object. - for (const auto& itr : hft_data) { - // create new object if group not found. + // First, create empty HerbivoreData objects for all HFTs/groups from the + // *other* object that are not yet present in *this* object. + for (const auto& itr : other.hft_data) { + // Create new, empty object if the group is new to this object. if (!hft_data.count(itr.first)) hft_data[itr.first] = HerbivoreData(); } - // Second, merge all herbivore data. + // Now we have all groups in this->hft_data, and we can merge for each + // group/HFT. for (auto& itr : hft_data) { - // try to find this group in the other object - GroupMap::const_iterator found = other.hft_data.find(itr.first); - - // If the other object doesn’t contain this HFT, we use an empty - // object. - HerbivoreData other_herbi_data; + // If an HFT/group in this->hft_data is not in other.hft_data, we create a + // temporary empty HerbivoreData object. + const auto& found = other.hft_data.find(itr.first); + HerbivoreData other_herbi_data; // Temporary empty object for now. + // If we did find the HFT/group in the other object, we use that for + // merging. if (found != other.hft_data.end()) other_herbi_data = found->second; // Let the class HerbivoreData do the actual merge. diff --git a/src/Fauna/Output/combined_data.test.cpp b/src/Fauna/Output/combined_data.test.cpp index 49490af3..352dca16 100644 --- a/src/Fauna/Output/combined_data.test.cpp +++ b/src/Fauna/Output/combined_data.test.cpp @@ -69,6 +69,7 @@ TEST_CASE("Fauna::Output::CombinedData") { // merge with equal weight c1.datapoint_count = c2.datapoint_count = 3; + // Merge c2 into c1. The HFTs in c2 are a *subset* of those in c1. c1.merge(c2); CHECK(c1.datapoint_count == 6); @@ -90,6 +91,33 @@ TEST_CASE("Fauna::Output::CombinedData") { Approx(3.0)); // weighted by inddens, but only one data point } + // This is the same as before, but merging the other way round. + SECTION("merge() and create new HFT record") { + // merge with equal weight + c1.datapoint_count = c2.datapoint_count = 3; + + // Merge c1 into c2. Now there is one new HFT coming from c1. + c2.merge(c1); + + CHECK(c2.datapoint_count == 6); + + CHECK(c2.habitat_data.available_forage.grass.get_mass() == Approx(1.5)); + // c1 now has data for both HFTs + REQUIRE(c2.hft_data.size() == 2); + // in HFT 0, two datapoints are merged + CHECK(c2.hft_data[hfts[0].get()->name].inddens == + Approx((1.0 + 2.0) / 2.0)); // normal average + CHECK( + c2.hft_data[hfts[0].get()->name].expenditure == + Approx((1.0 * 1.0 + 2.0 * 2.0) / (1.0 + 2.0))); // weighted by inddens + + // in HFT 1, one datapoint is merged with zero values + CHECK(c2.hft_data[hfts[1].get()->name].inddens == + Approx((0.0 + 3.0) / 2.0)); // normal average + CHECK(c2.hft_data[hfts[1].get()->name].expenditure == + Approx(3.0)); // weighted by inddens, but only one data point + } + SECTION("merge() with different data point counts") { c1.datapoint_count = 1; c2.datapoint_count = 2;