diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 27a389c..38940c8 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -1,4 +1,4 @@ -name: Unit Tests +name: Unit Tests on: push: @@ -7,41 +7,36 @@ on: branches: [ "main" ] release: types: [ published ] - workflow_dispatch: {} - + workflow_dispatch: { } + permissions: contents: read actions: read checks: write - + jobs: - build: + build: runs-on: ubuntu-latest - strategy: - max-parallel: 16 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6.0.x - - - name: Restore dependencies - run: dotnet restore + - name: .NET Build + run: dotnet build - - name: Build - run: dotnet build --no-restore - - - name: run - run: | - dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=unit_results.xml" + - name: .NET Test + run: | + dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=unit_results.xml" - - name: Test Report - uses: dorny/test-reporter@v1 - if: success() - with: - name: Unit Tests - path: SVSModel.Tests/unit_results.xml - reporter: java-junit + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() + with: + name: Unit Tests + path: SVSModel.Tests/unit_results.xml + reporter: java-junit \ No newline at end of file diff --git a/SVSModel.Tests/Configuration/CropConfigTests.cs b/SVSModel.Tests/Configuration/CropConfigTests.cs new file mode 100644 index 0000000..d310106 --- /dev/null +++ b/SVSModel.Tests/Configuration/CropConfigTests.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using SVSModel.Configuration; +using SVSModel.Models; +using Xunit; + +namespace SVSModel.Tests.Configuration; + +public class CropConfigTests +{ + private static readonly string CropNameFull = Defaults.CurrentCropNameFull; + private static readonly string EstablishStage = Defaults.EstablishStage; + private static readonly string HarvestStage = Defaults.HarvestStage; + private static readonly double FieldLoss = Defaults.FieldLoss; + private static readonly double MoistureContent = Defaults.MoistureContent; + private static readonly DateTime EstablishDate = Defaults.EstablishDate; + private static readonly DateTime HarvestDate = Defaults.HarvestDate; + private static readonly string ResidueRemoval = Defaults.ResidueRemoval; + private static readonly string ResidueIncorporation = Defaults.ResidueIncorporation; + private static readonly double FieldYield = Defaults.FieldYield; + private static readonly string YieldUnits = Defaults.Units; + private static readonly double? Population = null; + + private readonly Dictionary ExcelInputDict = new() + { + { "CurrentCropNameFull", CropNameFull }, + { "CurrentEstablishStage", EstablishStage }, + { "CurrentHarvestStage", HarvestStage }, + { "CurrentFieldLoss", FieldLoss }, + { "CurrentMoistureContent", MoistureContent }, + { "CurrentEstablishDate", EstablishDate }, + { "CurrentHarvestDate", HarvestDate }, + { "CurrentResidueRemoval", ResidueRemoval }, + { "CurrentResidueIncorporation", ResidueIncorporation }, + { "CurrentSaleableYield", FieldYield }, + { "CurrentYieldUnits", YieldUnits }, + { "CurrentPopulation", Population } + }; + + [Theory] + [InlineData(10.0, "t/ha")] + [InlineData(10_000.0, "kg/ha")] + [InlineData(0.25, "kg/head", 25_000.0)] + public void Test_CropConfig_Excel_Sets_Yield_Correctly(double yield, string units, double? population = null) + { + // Update values in the base dictionary + ExcelInputDict["CurrentSaleableYield"] = yield; + ExcelInputDict["CurrentYieldUnits"] = units; + if (population.HasValue) ExcelInputDict["CurrentPopulation"] = population; + + var cropConfig = new CropConfig(ExcelInputDict, "Current"); + + // Determine the expected yield + var expectedFieldYield = yield * Constants.UnitConversions[units]; + if (population.HasValue) expectedFieldYield = yield * population.Value; + + Assert.Equal(cropConfig.FieldYield, expectedFieldYield); + } + + [Fact] + public void Test_CropConfig_Excel_Sets_Residues_Correctly() + { + var cropConfig = new CropConfig(ExcelInputDict, "Current"); + + var expectedResidueFactRetained = Constants.ResidueFactRetained[ResidueRemoval]; + Assert.Equal(cropConfig.ResidueFactRetained, expectedResidueFactRetained); + + var expectedResidueFactIncorporated = Constants.ResidueIncorporation[ResidueIncorporation]; + Assert.Equal(cropConfig.ResidueFactIncorporated, expectedResidueFactIncorporated); + } + + [Theory] + [InlineData(10.0, "t/ha")] + [InlineData(10_000.0, "kg/ha")] + [InlineData(0.25, "kg/head", 25_000.0)] + public void Test_CropConfig_WebApp_Sets_Yield_Correctly(double yield, string units, double? population = null) + { + var cropConfig = new CropConfig + { + CropNameFull = CropNameFull, + EstablishStage = EstablishStage, + HarvestStage = HarvestStage, + FieldLoss = FieldLoss, + MoistureContent = MoistureContent, + EstablishDate = EstablishDate, + HarvestDate = HarvestDate, + _residueRemoval = ResidueRemoval, + _residueIncorporation = ResidueIncorporation, + _rawYield = yield, + _yieldUnits = units, + _population = population + }; + + // Determine the expected yield + var expectedFieldYield = yield * Constants.UnitConversions[units]; + if (population.HasValue) expectedFieldYield = yield * population.Value; + + Assert.Equal(cropConfig.FieldYield, expectedFieldYield); + } + + [Theory] + [InlineData(10.0, "t/ha")] + [InlineData(10_000.0, "kg/ha")] + [InlineData(0.25, "kg/head", 25_000.0)] + public void Test_CropConfig_Both_Constructors_Match(double yield, string units, double? population = null) + { + var cropConfig = new CropConfig + { + CropNameFull = CropNameFull, + EstablishStage = EstablishStage, + HarvestStage = HarvestStage, + FieldLoss = FieldLoss, + MoistureContent = MoistureContent, + EstablishDate = EstablishDate, + HarvestDate = HarvestDate, + _residueRemoval = ResidueRemoval, + _residueIncorporation = ResidueIncorporation, + _rawYield = yield, + _yieldUnits = units, + _population = population + }; + + ExcelInputDict["CurrentSaleableYield"] = yield; + ExcelInputDict["CurrentYieldUnits"] = units; + if (population.HasValue) ExcelInputDict["CurrentPopulation"] = population; + var cropConfigExcel = new CropConfig(ExcelInputDict, "Current"); + + Assert.Equal(cropConfig.CropNameFull, cropConfigExcel.CropNameFull); + Assert.Equal(cropConfig.EstablishStage, cropConfigExcel.EstablishStage); + Assert.Equal(cropConfig.HarvestStage, cropConfigExcel.HarvestStage); + Assert.Equal(cropConfig.FieldLoss, cropConfigExcel.FieldLoss); + Assert.Equal(cropConfig.MoistureContent, cropConfigExcel.MoistureContent); + Assert.Equal(cropConfig.EstablishDate, cropConfigExcel.EstablishDate); + Assert.Equal(cropConfig.HarvestDate, cropConfigExcel.HarvestDate); + Assert.Equal(cropConfig.FieldYield, cropConfigExcel.FieldYield); + Assert.Equal(cropConfig.ResidueFactIncorporated, cropConfigExcel.ResidueFactIncorporated); + Assert.Equal(cropConfig.ResidueFactRetained, cropConfigExcel.ResidueFactRetained); + } +} diff --git a/SVSModel.Tests/Configuration/FieldConfigTests.cs b/SVSModel.Tests/Configuration/FieldConfigTests.cs new file mode 100644 index 0000000..40e9530 --- /dev/null +++ b/SVSModel.Tests/Configuration/FieldConfigTests.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using SVSModel.Configuration; +using SVSModel.Models; +using Xunit; + +namespace SVSModel.Tests.Configuration; + +public class FieldConfigTests +{ + private static readonly string SoilCategory = Defaults.SoilCategory; + private static readonly string Texture = Defaults.SoilTexture; + private static readonly double PMN = Defaults.PMN; + private static readonly int Splits = Defaults.Splits; + private static readonly double Rocks = 10; + private static readonly string SampleDepth = Defaults.SampleDepth; + private static readonly string PrePlantRain = Defaults.RainPrior; + private static readonly string InCropRain = Defaults.RainDuring; + private static readonly string Irrigation = Defaults.IrrigationApplied; + + private readonly Dictionary ExcelInputDict = new() + { + { "SoilCategory", SoilCategory }, + { "Texture", Texture }, + { "PMN", PMN }, + { "Splits", Splits }, + { "Rocks", Rocks }, + { "SampleDepth", SampleDepth }, + { "PrePlantRain", PrePlantRain }, + { "InCropRain", InCropRain }, + { "Irrigation", Irrigation } + }; + + [Fact] + public void Test_FieldConfig_Excel_Gets_Values_Correctly() + { + var fieldConfig = new FieldConfig(ExcelInputDict); + + Assert.Equal(fieldConfig.Rocks, Rocks / 100); + Assert.Equal(fieldConfig.SampleDepthFactor, Constants.SampleDepthFactor[SampleDepth]); + Assert.Equal(fieldConfig.BulkDensity, Constants.ParticleDensity[SoilCategory] * Constants.Porosity[Texture]); + Assert.Equal(fieldConfig.AWC, 3 * Constants.AWCpct[Texture] * (1 - Rocks / 100)); + Assert.Equal(fieldConfig.PrePlantRainFactor, Constants.PPRainFactors[PrePlantRain]); + Assert.Equal(fieldConfig.InCropRainFactor, Constants.ICRainFactors[InCropRain]); + Assert.Equal(fieldConfig.IrrigationTrigger, Constants.IrrigationTriggers[Irrigation]); + Assert.Equal(fieldConfig.IrrigationRefill, Constants.IrrigationRefill[Irrigation]); + } + + [Fact] + public void Test_FieldConfig_Both_Constructors_Match() + { + var fieldConfig = new FieldConfig + { + SoilCategory = SoilCategory, + SoilTexture = Texture, + PMN = PMN, + Splits = Splits, + _rawRocks = Rocks, + _sampleDepth = SampleDepth, + _prePlantRain = PrePlantRain, + _inCropRain = InCropRain, + _irrigation = Irrigation + }; + + var fieldConfigExcel = new FieldConfig(ExcelInputDict); + + Assert.Equal(fieldConfig.SoilCategory, fieldConfigExcel.SoilCategory); + Assert.Equal(fieldConfig.SoilTexture, fieldConfigExcel.SoilTexture); + Assert.Equal(fieldConfig.Rocks, fieldConfigExcel.Rocks); + Assert.Equal(fieldConfig.SampleDepthFactor, fieldConfigExcel.SampleDepthFactor); + Assert.Equal(fieldConfig.BulkDensity, fieldConfigExcel.BulkDensity); + Assert.Equal(fieldConfig.PMN, fieldConfigExcel.PMN); + Assert.Equal(fieldConfig.Splits, fieldConfigExcel.Splits); + Assert.Equal(fieldConfig.AWC, fieldConfigExcel.AWC); + Assert.Equal(fieldConfig.PrePlantRainFactor, fieldConfigExcel.PrePlantRainFactor); + Assert.Equal(fieldConfig.InCropRainFactor, fieldConfigExcel.InCropRainFactor); + Assert.Equal(fieldConfig.IrrigationTrigger, fieldConfigExcel.IrrigationTrigger); + Assert.Equal(fieldConfig.IrrigationRefill, fieldConfigExcel.IrrigationRefill); + } +} \ No newline at end of file diff --git a/SVSModel.Tests/SVSModel.Tests.csproj b/SVSModel.Tests/SVSModel.Tests.csproj index 0b61f28..c57b5c9 100644 --- a/SVSModel.Tests/SVSModel.Tests.csproj +++ b/SVSModel.Tests/SVSModel.Tests.csproj @@ -1,9 +1,10 @@ - net48 + net8.0 false true + 12 diff --git a/SVSModel.Tests/SVSModelTest.cs b/SVSModel.Tests/SVSModelTest.cs deleted file mode 100644 index b218bd9..0000000 --- a/SVSModel.Tests/SVSModelTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Xunit; -using System; - -namespace SVSModel.Tests -{ - public class SVSModelTest - { - [Fact] - public void Test1() - { - Assert.True(true); - } - - [Fact] - public void Test2() - { - Assert.True(true); - } - } -} diff --git a/SVSModel/Configuration/Config.cs b/SVSModel/Configuration/Config.cs index 9217011..04c0985 100644 --- a/SVSModel/Configuration/Config.cs +++ b/SVSModel/Configuration/Config.cs @@ -10,19 +10,25 @@ public class Config public CropConfig Prior { get; set; } public CropConfig Current { get; set; } public CropConfig Following { get; set; } - - public List Rotation = new List(); - + public List Rotation { get; set; } = []; public FieldConfig Field { get; set; } + /// + /// Constructor used only by external webapp + /// public Config() { } + /// + /// Constructor used only by the Excel model + /// public Config(Dictionary c) { + // Only raw input values should be set in here + Prior = new CropConfig(c, "Prior"); Current = new CropConfig(c, "Current"); Following = new CropConfig(c, "Following"); - Rotation = new List { Prior, Current, Following }; + Rotation = [Prior, Current, Following]; Field = new FieldConfig(c); } } diff --git a/SVSModel/Configuration/Constants.cs b/SVSModel/Configuration/Constants.cs index 275bb28..c2ee142 100644 --- a/SVSModel/Configuration/Constants.cs +++ b/SVSModel/Configuration/Constants.cs @@ -2,10 +2,11 @@ namespace SVSModel.Configuration { - public class Constants + public static class Constants { /// Dictionary containing values for the proportion of maximum DM that occurs at each predefined crop stage - public static readonly Dictionary PropnMaxDM = new Dictionary { + public static readonly Dictionary PropnMaxDM = new() + { { "Seed", 0.0066 }, { "Seedling", 0.015 }, { "Vegetative", 0.5 }, @@ -17,7 +18,8 @@ public class Constants }; /// Dictionary containing values for the proportion of thermal time to maturity that has accumulate at each predefined crop stage - public static readonly Dictionary PropnTt = new Dictionary { + public static readonly Dictionary PropnTt = new() + { { "Seed", 0 }, { "Seedling", 0.16 }, { "Vegetative", 0.5 }, @@ -29,7 +31,7 @@ public class Constants }; /// Dictionary containing conversion from specified units to kg/ha which are the units that the model works in - public static readonly Dictionary UnitConversions = new Dictionary + public static readonly Dictionary UnitConversions = new() { { "t/ha", 1000 }, { "kg/ha", 1.0 }, @@ -37,7 +39,7 @@ public class Constants }; /// Dictionary containing conversion from specified residue treatments to proportoins returned - public static readonly Dictionary ResidueFactRetained = new Dictionary + public static readonly Dictionary ResidueFactRetained = new() { { "None removed", 1.0 }, { "Baled", 0.2 }, @@ -47,7 +49,7 @@ public class Constants }; /// Dictionary containing conversion from specified residue treatments to proportoins returned - public static readonly Dictionary ResidueIncorporation = new Dictionary + public static readonly Dictionary ResidueIncorporation = new() { { "None (Surface)", 0.0 }, { "Part (Cultivate)", 0.5 }, @@ -55,27 +57,27 @@ public class Constants }; /// Dictionary containing conversion from specified rainfall conditions to a factor - public static readonly Dictionary ICRainFactors = new Dictionary + public static readonly Dictionary ICRainFactors = new() { { "Very Wet", 1.7 }, - { "Wet", 1.35}, + { "Wet", 1.35 }, { "Typical", 1.0 }, { "Dry", 0.65 }, { "Very Dry", 0.3 } }; /// Dictionary containing conversion from specified rainfall conditions to a factor - public static readonly Dictionary PPRainFactors = new Dictionary + public static readonly Dictionary PPRainFactors = new() { { "Very Wet", 1.0 }, - { "Wet", 0.95}, + { "Wet", 0.95 }, { "Typical", 0.9 }, { "Dry", 0.6 }, { "Very Dry", 0.3 } }; /// Dictionary containing conversion from specified irrigation method to trigger point factors - public static readonly Dictionary IrrigationTriggers = new Dictionary + public static readonly Dictionary IrrigationTriggers = new() { { "None", 0.0 }, { "Some", 0.4 }, @@ -83,7 +85,7 @@ public class Constants }; /// Dictionary containing conversion from specified irrigation method to refill target factors - public static readonly Dictionary IrrigationRefill = new Dictionary + public static readonly Dictionary IrrigationRefill = new() { { "None", 0.0 }, { "Some", 0.8 }, @@ -91,51 +93,51 @@ public class Constants }; /// Sample depth factor to adjust measurments to equivelent of 30cm measure - public static readonly Dictionary SampleDepthFactor = new Dictionary + public static readonly Dictionary SampleDepthFactor = new() { { "0-15cm", 0.75 }, { "0-30cm", 1 } }; /// Available water capacity % - public static readonly Dictionary AWCpct = new Dictionary + public static readonly Dictionary AWCpct = new() { - {"coarse sand", 5 }, - {"fine sand", 15}, - {"loamy sand", 18}, - {"sandy loam", 23}, - {"sandy clay loam", 16}, - {"loam", 22}, - {"silt loam", 22}, - {"silty clay loam", 20}, - {"clay loam", 18}, - {"silty clay", 20}, - {"clay", 18}, - {"peat", 20} + { "Coarse sand", 5 }, + { "Fine sand", 15 }, + { "Loamy sand", 18 }, + { "Sandy loam", 23 }, + { "Sandy clay loam", 16 }, + { "Loam", 22 }, + { "Silt loam", 22 }, + { "Silty clay loam", 20 }, + { "Clay loam", 18 }, + { "Silty clay", 20 }, + { "Clay", 18 }, + { "Peat", 20 } }; /// The porocity (mm3 pores/mm3 soil volume) of different soil texture classes - public static readonly Dictionary Porosity = new Dictionary + public static readonly Dictionary Porosity = new() { - {"coarse sand", 0.20 }, - {"fine sand", 0.25}, - {"loamy sand", 0.30}, - {"sandy loam", 0.30}, - {"sandy clay loam", 0.16}, - {"loam", 0.40}, - {"silt loam", 0.40}, - {"silty clay loam", 0.43}, - {"clay loam",0.46}, - {"silty clay", 0.45}, - {"clay", 0.50}, - {"peat", 0.50} + { "Coarse sand", 0.20 }, + { "Fine sand", 0.25 }, + { "Loamy sand", 0.30 }, + { "Sandy loam", 0.30 }, + { "Sandy clay loam", 0.16 }, + { "Loam", 0.40 }, + { "Silt loam", 0.40 }, + { "Silty clay loam", 0.43 }, + { "Clay loam", 0.46 }, + { "Silty clay", 0.45 }, + { "Clay", 0.50 }, + { "Peat", 0.50 } }; /// particle bulk density (g/mm3) - public static readonly Dictionary ParticleDensity = new Dictionary + public static readonly Dictionary ParticleDensity = new() { - {"Sedementary", 2.65 }, - {"Volcanic", 1.9 } + { "Sedimentary", 2.65 }, + { "Volcanic", 1.9 } }; } -} +} \ No newline at end of file diff --git a/SVSModel/Configuration/CropConfig.cs b/SVSModel/Configuration/CropConfig.cs index 9cdfe43..f090863 100644 --- a/SVSModel/Configuration/CropConfig.cs +++ b/SVSModel/Configuration/CropConfig.cs @@ -1,76 +1,78 @@ using System; using System.Collections.Generic; +using SVSModel.Models; -namespace SVSModel.Configuration +namespace SVSModel.Configuration; + +/// +/// Class that stores the configuration information in the correct type for a specific crop . +/// I.e constructor takes all config settings as objects and converts them to appropriates types +/// +public class CropConfig { - /// - /// Class that stores the configuration information in the correct type for a specific crop . - /// I.e constructor takes all config settings as objects and converts them to appropriates types - /// - public class CropConfig + // Inputs + public string CropNameFull { get; init; } + public string EstablishStage { get; init; } + public string HarvestStage { get; init; } + public double FieldLoss { get; init; } + public double MoistureContent { get; init; } + public DateTime EstablishDate { get; init; } + public DateTime HarvestDate { get; init; } + public double _rawYield { private get; init; } + public string _yieldUnits { private get; init; } + public double? _population { private get; init; } + public string _residueRemoval { private get; init; } + public string _residueIncorporation { private get; init; } + + // Calculated fields + public double FieldYield { - public string CropNameFull { get; set; } - public string EstablishStage { get; set; } - public string HarvestStage { get; set; } + get + { + if (_yieldUnits == "kg/head") + { + return _rawYield * _population.GetValueOrDefault(); + } - /// - /// Model code is expecting kg/ha, so this field _must_ be in those units - /// - public double FieldYield { get; private set; } + var toKGperHA = Constants.UnitConversions[_yieldUnits ?? Defaults.Units]; - public double FieldLoss { get; set; } - public double DressingLoss { get; set; } - public double MoistureContent { get; set; } - public DateTime EstablishDate { get; set; } - public DateTime HarvestDate { get; set; } - public double ResidueFactRetained { get; set; } - public double ResidueFactIncorporated { get; set; } - public double ResRoot { get; set; } - public double ResStover { get; set; } - public double ResFieldLoss { get; set; } - public double NUptake { get; set; } + return _rawYield * toKGperHA; + } + } + public double ResidueFactRetained => Constants.ResidueFactRetained[_residueRemoval]; + public double ResidueFactIncorporated => Constants.ResidueIncorporation[_residueIncorporation]; - public CropConfig() { } + // Used by model + public double ResRoot { get; set; } + public double ResStover { get; set; } + public double ResFieldLoss { get; set; } + public double NUptake { get; set; } - /// - /// Constructor used only by the Excel model - /// - public CropConfig(Dictionary c, string pos) - { - CropNameFull = c[pos + "CropNameFull"].ToString(); - EstablishStage = c[pos + "EstablishStage"].ToString(); - HarvestStage = c[pos + "HarvestStage"].ToString(); - // UI sends yield in t/ha but model works in kg/ha so convert here - FieldYield = SetYield(Functions.Num(c[pos + "SaleableYield"]), c[pos + "YieldUnits"].ToString(), Functions.Num(c[pos+"Population"])); - FieldLoss = Functions.Num(c[pos + "FieldLoss"]); - MoistureContent = Functions.Num(c[pos + "MoistureContent"]); - EstablishDate = Functions.Date(c[pos + "EstablishDate"]); - HarvestDate = Functions.Date(c[pos + "HarvestDate"]); - ResidueFactRetained = Constants.ResidueFactRetained[c[pos + "ResidueRemoval"].ToString()]; - ResidueFactIncorporated = Constants.ResidueIncorporation[c[pos + "ResidueIncorporation"].ToString()]; - } + /// + /// Constructor used only by external webapp + /// + public CropConfig() { } - /// - /// Call this after initializing Units - /// Converts the raw value to kg/ha for the model code - /// To be used by interfaces outside of the excel sheet - /// - /// The raw value from form - /// The raw units from form - /// The amount of crop when measuring in kg/head - public double SetYield(double rawYield, string rawUnits, double? population) - { - - var toKGperHA = Constants.UnitConversions[rawUnits]; + /// + /// Constructor used only by the Excel model + /// + public CropConfig(Dictionary c, string pos) + { + // Only raw input values should be set in here + + CropNameFull = c[pos + "CropNameFull"].ToString(); + EstablishStage = c[pos + "EstablishStage"].ToString(); + HarvestStage = c[pos + "HarvestStage"].ToString(); + FieldLoss = Functions.Num(c[pos + "FieldLoss"]); + MoistureContent = Functions.Num(c[pos + "MoistureContent"]); + EstablishDate = Functions.Date(c[pos + "EstablishDate"]); + HarvestDate = Functions.Date(c[pos + "HarvestDate"]); - if (rawUnits == "kg/head") - { - return rawYield * population.GetValueOrDefault(); - } - else - { - return rawYield * toKGperHA; - } - } + _residueRemoval = c[pos + "ResidueRemoval"].ToString(); + _residueIncorporation = c[pos + "ResidueIncorporation"].ToString(); + + _rawYield = Functions.Num(c[pos + "SaleableYield"]); + _yieldUnits = c[pos + "YieldUnits"].ToString(); + _population = Functions.Num(c[pos + "Population"]); } } diff --git a/SVSModel/Configuration/DummyChecker.cs b/SVSModel/Configuration/DummyChecker.cs deleted file mode 100644 index f67b340..0000000 --- a/SVSModel/Configuration/DummyChecker.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; - -namespace SVSModel.Configuration -{ - public class Tryout - { - public string message; - public int code; - - // Created a class constructor with multiple parameters - // for some reason - public Tryout(string messageName, int codeNum) - { - message = messageName; - code = codeNum; - } - - } -} - - diff --git a/SVSModel/Configuration/FieldConfig.cs b/SVSModel/Configuration/FieldConfig.cs index 6751628..81bdad6 100644 --- a/SVSModel/Configuration/FieldConfig.cs +++ b/SVSModel/Configuration/FieldConfig.cs @@ -8,41 +8,54 @@ namespace SVSModel.Configuration /// public class FieldConfig { - public double InitialN { get; set; } - public string SoilCategory { get; set; } - public string SoilTexture { get; set; } - public double Rocks { get; set; } - public double SampleDepthFactor { get; set; } - public double BulkDensity { get; set; } - public double PMN { get; set; } - public double Trigger { get; set; } - public double Efficiency { get; set; } - public int Splits { get; set; } - public double AWC { get; set; } - public double PrePlantRainFactor { get; set; } - public double InCropRainFactor { get; set; } - public double IrrigationTrigger { get; set; } - public double IrrigationRefill { get; set; } + // Constants + public const double InitialN = 50; + public const double Trigger = 30; + public const double Efficiency = 1.0; + + // Inputs + public string SoilCategory { get; init; } + public string SoilTexture { get; init; } + public double PMN { get; init; } + public int Splits { get; init; } + public double _rawRocks { internal get; init; } + public string _sampleDepth { internal get; init; } + public string _prePlantRain { internal get; init; } + public string _inCropRain { internal get; init; } + public string _irrigation { internal get; init; } + // Calculated fields + public double Rocks => _rawRocks / 100; + public double SampleDepthFactor => Constants.SampleDepthFactor[_sampleDepth]; + public double BulkDensity => Constants.ParticleDensity[SoilCategory] * Constants.Porosity[SoilTexture]; + public double AWC => 3 * Constants.AWCpct[SoilTexture] * (1 - Rocks); + public double PrePlantRainFactor => Constants.PPRainFactors[_prePlantRain]; + public double InCropRainFactor => Constants.ICRainFactors[_inCropRain]; + public double IrrigationTrigger => Constants.IrrigationTriggers[_irrigation]; + public double IrrigationRefill => Constants.IrrigationRefill[_irrigation]; + + /// + /// Constructor used only by external webapp + /// public FieldConfig() { } + /// + /// Constructor used only by the Excel model + /// public FieldConfig(Dictionary c) { - InitialN = 50; + // Only raw input values should be set in here + SoilCategory = c["SoilCategory"].ToString(); SoilTexture = c["Texture"].ToString(); - Rocks = Functions.Num(c["Rocks"])/100; - SampleDepthFactor = Constants.SampleDepthFactor[c["SampleDepth"].ToString()]; - BulkDensity = Constants.ParticleDensity[c["SoilCategory"].ToString()] * Constants.Porosity[c["Texture"].ToString()]; PMN = Functions.Num(c["PMN"]); - Trigger = 30; - Efficiency = 1.0; Splits = int.Parse(c["Splits"].ToString()); - AWC = 3 * Constants.AWCpct[SoilTexture] * (1-Rocks); - PrePlantRainFactor = Constants.PPRainFactors[c["PrePlantRain"].ToString()]; - InCropRainFactor = Constants.ICRainFactors[c["InCropRain"].ToString()]; - IrrigationRefill = Constants.IrrigationRefill[c["Irrigation"].ToString()]; - IrrigationTrigger = Constants.IrrigationTriggers[c["Irrigation"].ToString()]; + + _rawRocks = Functions.Num(c["Rocks"]); + _sampleDepth = c["SampleDepth"].ToString(); + _prePlantRain = c["PrePlantRain"].ToString(); + _inCropRain = c["InCropRain"].ToString(); + _irrigation = c["Irrigation"].ToString(); } } -} +} \ No newline at end of file diff --git a/SVSModel/Configuration/Functions.cs b/SVSModel/Configuration/Functions.cs index 6f0b4a4..4c56ed4 100644 --- a/SVSModel/Configuration/Functions.cs +++ b/SVSModel/Configuration/Functions.cs @@ -5,7 +5,7 @@ namespace SVSModel.Configuration { - public class Functions + public static class Functions { /// /// calculates the difference between daily values in an array @@ -253,10 +253,16 @@ public static DateTime Date(object configDate) return (DateTime)configDate; } } + + /// + /// Parses a double out of a generic `object` input + /// + /// Parsed double, or zero public static double Num(object configDouble) { - - return Double.Parse(configDouble.ToString()); + var doubleString = configDouble?.ToString() ?? string.Empty; + if (string.IsNullOrWhiteSpace(doubleString)) return 0; + return double.Parse(doubleString); } public static Dictionary ApplyRainfallFactor(Dictionary meanRain, Config config) diff --git a/SVSModel/ModelInterface.cs b/SVSModel/ModelInterface.cs index 0c5d16c..f5aa37d 100644 --- a/SVSModel/ModelInterface.cs +++ b/SVSModel/ModelInterface.cs @@ -23,6 +23,7 @@ public interface IModelInterface /// Model config object, all parameters are required /// A list of objects List GetDailyNBalance(string weatherStation, Dictionary testResults, Dictionary nApplied, Config config); + /// /// Gets the crop data from the data file /// diff --git a/SVSModel/Models/Defaults.cs b/SVSModel/Models/Defaults.cs index 309a9ca..a15d6a7 100644 --- a/SVSModel/Models/Defaults.cs +++ b/SVSModel/Models/Defaults.cs @@ -1,47 +1,45 @@ using System; -namespace SVSModel.Models +namespace SVSModel.Models; + +public static class Defaults { - public class Defaults - { - public static readonly string PriorCropColloquial = "Oat"; - public static readonly string PriorCropEndUse = "Fodder"; - public static readonly string PriorCropType = "General"; - public static readonly string PriorCropNameFull = $"{PriorCropColloquial} {PriorCropEndUse} {PriorCropType}"; - - public static readonly string CurrentCropColloquial = "Oat"; - public static readonly string CurrentCropEndUse = "Fodder"; - public static readonly string CurrentCropType = "General"; - public static readonly string CurrentCropNameFull = $"{CurrentCropColloquial} {CurrentCropEndUse} {CurrentCropType}"; - - public static readonly string NextCropColloquial = "Oat"; - public static readonly string NextCropEndUse = "Fodder"; - public static readonly string NextCropType = "General"; - public static readonly string NextCropNameFull = $"{NextCropColloquial} {NextCropEndUse} {NextCropType}"; - - public static readonly string EstablishStage = "Seed"; - public static readonly string HarvestStage = "EarlyReproductive"; - public static readonly double SaleableYield = 10; - public static readonly string Units = "t/ha"; - - public static readonly double FieldLoss = 0; - public static readonly double MoistureContent = 0; - - public static readonly int GrowingDays = 125; - public static readonly DateTime EstablishDate = DateTime.Today; - public static readonly DateTime HarvestDate = DateTime.Today.AddDays(GrowingDays); - - public static readonly string ResidueRemoval = "None removed"; - public static readonly string ResidueIncorporation = "Full (Plough)"; - - public static readonly string SoilCategory = "Sedementry"; - public static readonly string SoilTexture = "SiltLoam"; - public static readonly string SampleDepth = "0-30cm"; - public static readonly double PMN = 55; - public static readonly int Splits = 3; - public static readonly string RainPrior = "Typical"; - public static readonly string RainDuring = "Typical"; - public static readonly string IrrigationApplied = "None"; - - } + public static readonly string PriorCropColloquial = "Oat"; + public static readonly string PriorCropEndUse = "Fodder"; + public static readonly string PriorCropType = "General"; + public static readonly string PriorCropNameFull = $"{PriorCropColloquial} {PriorCropEndUse} {PriorCropType}"; + + public static readonly string CurrentCropColloquial = "Oat"; + public static readonly string CurrentCropEndUse = "Fodder"; + public static readonly string CurrentCropType = "General"; + public static readonly string CurrentCropNameFull = $"{CurrentCropColloquial} {CurrentCropEndUse} {CurrentCropType}"; + + public static readonly string NextCropColloquial = "Oat"; + public static readonly string NextCropEndUse = "Fodder"; + public static readonly string NextCropType = "General"; + public static readonly string NextCropNameFull = $"{NextCropColloquial} {NextCropEndUse} {NextCropType}"; + + public static readonly string EstablishStage = "Seed"; + public static readonly string HarvestStage = "EarlyReproductive"; + public static readonly double FieldYield = 10; + public static readonly string Units = "t/ha"; + + public static readonly double FieldLoss = 0; + public static readonly double MoistureContent = 0; + + public static readonly int GrowingDays = 125; + public static readonly DateTime EstablishDate = DateTime.Today; + public static readonly DateTime HarvestDate = DateTime.Today.AddDays(GrowingDays); + + public static readonly string ResidueRemoval = "None removed"; + public static readonly string ResidueIncorporation = "Full (Plough)"; + + public static readonly string SoilCategory = "Sedimentary"; + public static readonly string SoilTexture = "Silt loam"; + public static readonly string SampleDepth = "0-30cm"; + public static readonly double PMN = 55; + public static readonly int Splits = 3; + public static readonly string RainPrior = "Typical"; + public static readonly string RainDuring = "Typical"; + public static readonly string IrrigationApplied = "None"; } diff --git a/SVSModel/Models/Fertiliser.cs b/SVSModel/Models/Fertiliser.cs index d926cee..7b153b4 100644 --- a/SVSModel/Models/Fertiliser.cs +++ b/SVSModel/Models/Fertiliser.cs @@ -57,8 +57,8 @@ public static void RemainingFertiliserSchedule( // Set other variables needed to derive fertiliser requirement double CropN = cropN[config.Current.HarvestDate] - cropN[startSchedulleDate]; - double trigger = config.Field.Trigger; - double efficiency = config.Field.Efficiency; + double trigger = FieldConfig.Trigger; + double efficiency = FieldConfig.Efficiency; // Calculate total fertiliser requirement and ammount to be applied at each application double NFertReq = (CropN + trigger) - soilN[startSchedulleDate] - mineralisation - fertToDate; @@ -95,7 +95,7 @@ public static void ApplyExistingFertiliser( DateTime endApplicationDate = config.Following.HarvestDate; //if (testResults.Keys.Count > 0) // startApplicationDate = testResults.Keys.Last().AddDays(1); //If test results specified after establishment that becomes start of schedulling date - double efficiency = config.Field.Efficiency; + double efficiency = FieldConfig.Efficiency; foreach (DateTime d in appliedN.Keys) { if ((d >= startApplicationDate)&&(d <= endApplicationDate)) diff --git a/SVSModel/Models/SoilNitrogen.cs b/SVSModel/Models/SoilNitrogen.cs index 62043e3..fbf6fa3 100644 --- a/SVSModel/Models/SoilNitrogen.cs +++ b/SVSModel/Models/SoilNitrogen.cs @@ -17,8 +17,7 @@ public class SoilNitrogen public static Dictionary InitialBalance( Dictionary uptake, Dictionary residue, - Dictionary som, - Config config) + Dictionary som) { DateTime[] simDates = uptake.Keys.ToArray(); Dictionary soilN = Functions.dictMaker(simDates, new double[simDates.Length]); @@ -26,7 +25,7 @@ public static Dictionary InitialBalance( { if (d == simDates[0]) { - soilN[simDates[0]] = config.Field.InitialN; + soilN[simDates[0]] = FieldConfig.InitialN; } else { diff --git a/SVSModel/README.md b/SVSModel/README.md index 9d82730..6f18842 100644 --- a/SVSModel/README.md +++ b/SVSModel/README.md @@ -3,5 +3,8 @@ First update the `SVSModel.csproj` `` field, then run these commands wi ```bash $ dotnet pack SVSModel/SVSModel.csproj -$ dotnet nuget push SVSModel/bin/Debug/SVSModel.X.X.X.nupkg --source "https://pkgs.dev.azure.com/rezaresystems/48ae16c6-5f20-44a0-ad41-e047c311de0a/_packaging/svs-model-calculator/nuget/v3/index.json" --api-key az --interactive ``` + +```bash +$ dotnet nuget push SVSModel/bin/Release/SVSModel.X.X.X.nupkg --source "https://pkgs.dev.azure.com/rezaresystems/48ae16c6-5f20-44a0-ad41-e047c311de0a/_packaging/svs-model-calculator/nuget/v3/index.json" --api-key az --interactive +``` \ No newline at end of file diff --git a/SVSModel/SVSModel.csproj b/SVSModel/SVSModel.csproj index d015b07..0ce111b 100644 --- a/SVSModel/SVSModel.csproj +++ b/SVSModel/SVSModel.csproj @@ -2,9 +2,10 @@ netstandard2.0 SVSModel - 1.1.0 + 1.1.6 PotatoesNZ A wrapper of the SVS Model code + 12 embedded @@ -116,6 +117,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/SVSModel/Simulation.cs b/SVSModel/Simulation.cs index 05f08ca..60bebcd 100644 --- a/SVSModel/Simulation.cs +++ b/SVSModel/Simulation.cs @@ -80,7 +80,7 @@ public class Simulation Dictionary NSoilOM = SoilOrganic.Mineralisation(RSWC, meanT, config); //Calculate soil N estimated without fertiliser or soil test results - Dictionary SoilN = SoilNitrogen.InitialBalance(NUptake, NResidues, NSoilOM, config); + Dictionary SoilN = SoilNitrogen.InitialBalance(NUptake, NResidues, NSoilOM); //Add fertiliser that has already been applied to the N balance Dictionary LostN = Functions.dictMaker(simDates, new double[simDates.Length]);