diff --git a/README.md b/README.md index 32f1ba5406e30f3fe51a672105ecf9e58aa58467..28fda9ffba796f69a43b6ddccc74429c3b28382f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,13 @@ Last edit: 2023-02-21 Since VIPS consists of multiple subsystems, this project is a starting point for VIPS documentation, describing the system as a whole. Documentation to each subsystem is linked from here. ## Further reading +(Read the introduction below first...) +* [Implementing models in VIPS - introduction](./model_implementation.md) +* [Implementing models in VIPS - using Java](./model_development_java.md) +* [Implementing models in VIPS - using Python](./model_development_python.md) * [VIPS Weather data specification](./weather_data.md) +* [VIPS model input data specification](./input_data.md) +* [VIPS model result data specification](./result_data.md) ## Introduction to VIPS ### What is VIPS? diff --git a/illustrations/funguspilosusflavis_scaffold.png b/illustrations/funguspilosusflavis_scaffold.png new file mode 100644 index 0000000000000000000000000000000000000000..659a1fc33e7774f36f73cf7fcce11bcd8d7d3314 Binary files /dev/null and b/illustrations/funguspilosusflavis_scaffold.png differ diff --git a/model_development_java.md b/model_development_java.md index b21795748e0e55833ef9780d6dddf520f7a88085..43e6e577bdeab38e460f48451bbcbc39cac2cf35 100644 --- a/model_development_java.md +++ b/model_development_java.md @@ -3,7 +3,7 @@ # Developing a model using Java This page builds on [A step-by-step introduction to implementing prediction models in VIPS](model_implementation.md) -What you need to develop a VIPS model is basically: +What you need to develop a VIPS model is: - A decent coding environment (IDE) like NetBeans, Eclipse or IntelliJ. @@ -11,7 +11,8 @@ What you need to develop a VIPS model is basically: - A testing framework like Junit. This is normally bundled with your IDE (see above) -The normal workflow is that you have some [correctly formatted weather data](weather_data.md) in a file that you put on the project's +The normal workflow is that you have some [correctly formatted weather data](weather_data.md) +in a file that you put on the project's classpath, you mix this with the other configuration data and develop the model based on these input data. You must have one main class that implements the Model interface, which is available in the VIPSCommon.jar @@ -23,8 +24,7 @@ VIPSCore server (TODO: Document this) ## Implementing a forecasting model - -In this project, we are going to implement a forecasting model for a +We are going to implement a forecasting model for a virtual fungus called «Fungus pilosus flavis» (please bear with me, any phytopatologists who might read this). Let\'s say that it there is a forecasting model for it that states that @@ -84,12 +84,11 @@ Now you should be ready to code. Now the class is ready to be programmed and do some calculations. -[]{#anchor-8}Create the method for finding when 500 day degrees has been passed -------------------------------------------------------------------------------- +#### Create the method for finding when 500 day degrees has been passed To find out when 500 day degrees (since some date) have passed, you need the mean temperature of each day. All weather observations in VIPS are -represented by an instance of the object WeatherObservation. This object +represented by an instance of the class WeatherObservation. This class has a few important properties: - ElementMeasurementTypeId: Rain, mean temperature, leaf wetness etc. @@ -100,13 +99,15 @@ has a few important properties: We need a list of one WeatherObservation with mean temperature per day. So we could start by writing this method: -[]{#anchor-9}[]{#anchor-10}public Date -getDateWhenDayDegreeLimitHasPassed(List\<WeatherObservation\> +```java +public Date +getDateWhenDayDegreeLimitHasPassed(List<WeatherObservation> observations) { } +``` NetBeans complains, because it can\'t find the definition of WeatherObservation. Click on the light bulb and select «Add import for @@ -121,67 +122,38 @@ method does not return anything yet. So a simple approach could be: Sample code for this could be: -*public Date -getDateWhenDayDegreeLimitHasPassed(List\<WeatherObservation\> -observations){* - -// Make sure the observations are in chronological order - -Collections.sort(observations); - -// Initalize the day degree counter - -Double dayDegrees = 0.0; - -// Iterate through the list of observations - -for(WeatherObservation obs:observations) - -{ - -// Make sure it\'s only daily temperature observations that are used - -if(obs.getLogIntervalId() - -.equals(WeatherObservation.LOG\_INTERVAL\_ID\_1D) - -&& obs.getElementMeasurementTypeId() - -.equals(WeatherElements.TEMPERATURE\_MEAN)) - -{ - -// Add to dayDegree sum - -dayDegrees += obs.getValue(); - -// If threshold is reached, return the date of the current temperature - -// measurement - -if(dayDegrees \>= 500.0) - -{ - -return obs.getTimeMeasured(); - -} - -} - -} - -// We have finished looping through the observations, and dayDegrees has - -// not passed 500. So we can\'t return a Date, we must return NULL -(nothing) +```java +public Date getDateWhenDayDegreeLimitHasPassed(List<WeatherObservation> observations){ + + // Make sure the observations are in chronological order + Collections.sort(observations); + // Initalize the day degree counter + Double dayDegrees = 0.0; + // Iterate through the list of observations + for(WeatherObservation obs:observations) + { + // Make sure it's only daily temperature observations that are used + if(obs.getLogIntervalId().equals(WeatherObservation.LOG_INTERVAL_ID_1D) + && obs.getElementMeasurementTypeId().equals(WeatherElements.TEMPERATURE_MEAN)) + { + // Add to dayDegree sum + dayDegrees += obs.getValue(); + // If threshold is reached, return the date of the current temperature + // measurement + if(dayDegrees >= 500.0) + { + return obs.getTimeMeasured(); + } -return null; + } + // We have finished looping through the observations, and dayDegrees has + // not passed 500. So we can\'t return a Date, we must return NULL(nothing) + return null; } +``` -[]{#anchor-11}Creating the method to calculate the infection risk ------------------------------------------------------------------ +#### Create the method to calculate the infection risk We can operate on hourly weather data for leaf wetness and calculate the infection risk. Data in will be a list of weather observations (leaf @@ -191,62 +163,38 @@ key, and the infection risk as value. So for instance for 24th July 2014 An example of a solution can be: -public Map\<Date, Integer\> getInfectionRisk(List\<WeatherObservation\> +```java +public Map<Date, Integer> getInfectionRisk(List<WeatherObservation> observations) - { - -// Create the map with dates and infection risk values - -Map\<Date, Integer\> riskMap = new HashMap\<\>(); - -// Make sure the observations are in chronological order - -Collections.sort(observations); - -// Counter for consecutive hours of leaf wetness - -Integer consecutiveHoursOfLeafWetness = 0; - -// Loop through the list of observations - -for(WeatherObservation obs:observations) - -{ - -// We define a lower threshold for leaf wetnes to be 10mins/hour - -if(obs.getValue() \> 10.0) - -{ - -// Leaf wetness registered, add to consecutive hours counter - -consecutiveHoursOfLeafWetness++; - -} - -else - -{ - -// No leaf wetness, reset counter - -consecutiveHoursOfLeafWetness = 0; - -} - -// We set the risk value - -riskMap.put(obs.getTimeMeasured(), consecutiveHoursOfLeafWetness \* 2); - + // Create the map with dates and infection risk values + Map<Date, Integer> riskMap = new HashMap<>(); + // Make sure the observations are in chronological order + Collections.sort(observations); + // Counter for consecutive hours of leaf wetness + Integer consecutiveHoursOfLeafWetness = 0; + // Loop through the list of observations + for(WeatherObservation obs:observations) + { + // We define a lower threshold for leaf wetnes to be 10mins/hour + if(obs.getValue() > 10.0) + { + // Leaf wetness registered, add to consecutive hours counter + consecutiveHoursOfLeafWetness++; + } + else + { + // No leaf wetness, reset counter + consecutiveHoursOfLeafWetness = 0; + } + // We set the risk value + riskMap.put(obs.getTimeMeasured(), consecutiveHoursOfLeafWetness * 2); + } + // Return the map with all values + return riskMap; } -// Return the map with all values - -return riskMap; - -} +``` []{#anchor-12}How can we be sure that these methods work? Testing to the rescue ------------------------------------------------------------------------------- @@ -418,14 +366,14 @@ could be: // Initialize the list of results List<Result> results = new ArrayList<>(); - // Ensure that the observations are in chronoligical order + // Ensure that the observations are in chronological order Collections.sort(this.observations); // Which date did day degree sum exceed 500? Date dayDegreeLimitReachedDate = this.getDateWhenDayDegreeLimitHasPassed(this.observations); // Get infection risk for the whole period - Map<Date, Integer\> uncontrolledInfectionRisk = this.getInfectionRisk(this.observations); + Map<Date, Integer> uncontrolledInfectionRisk = this.getInfectionRisk(this.observations); // Get all dates from the map of infection risk List<Date> dateList = new ArrayList(uncontrolledInfectionRisk.keySet()); @@ -437,8 +385,8 @@ could be: // Create a new result object Result result = new ResultImpl(); // Set the timestamp on it - result.setResultValidTime(currentDate); - // If we're after the date of day degree sum \> 500, use the infectionrisk + result.setValidTimeStart(currentDate); + // If we're after the date of day degree sum > 500, use the infectionrisk if(currentDate.compareTo(dayDegreeLimitReachedDate) >= 0) { // Set infection risk diff --git a/model_development_python.md b/model_development_python.md index bc0abf67269c07f5207901e183fc02cd81926ff0..260d3a8d5e8d4203a547b985fd2ab5cee1eca9b9 100644 --- a/model_development_python.md +++ b/model_development_python.md @@ -1,4 +1,529 @@ <img src="illustrations/vipslogo_512.png" alt="VIPS Logo" height="250"/> # Developing a model using Python -This page builds on [A step-by-step introduction to implementing prediction models in VIPS](model_implementation.md) \ No newline at end of file +This page builds on [A step-by-step introduction to implementing prediction models in VIPS](model_implementation.md) + +## Preparations +The tools you need to develop a VIPS model in Python are: + +* A coding tool, such as an IDE(Integrated Development Environment). +Common choices for Python are: + * [Eclipse](https://www.eclipse.org/) + * [PyCharm](https://www.jetbrains.com/pycharm/) + * [MS Visual Studio Code](https://code.visualstudio.com/) +* [VIPSCore-Python-Common](https://gitlab.nibio.no/VIPS/vipscore-python-common), a Python package +that contains the shared tools and classes for VIPS models. + +You should be familiar with +* The Python programming language, version 3 +* Using Python's [venv](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment) + +## Workflow example +The normal workflow is +1) that you have some [correctly formatted weather data](weather_data.md) +in a file that you put on the project's classpath, +2) you mix this with the other configuration data and +3) develop the model based on these input data. +4) You must have one main class that +extends the VIPSModel Abstract Base Class, which is available in the +[VIPSCore-Python-Common](https://gitlab.nibio.no/VIPS/vipscore-python-common) +package. +5) The test framework can be used to test single methods that are +part of the algorithms or you can test the complete model. +6) When you're happy with how the model works you can test deploy it to the +VIPSCore server (TODO: Document this) + +We will take this step-by-step below + +## Implementing a forecasting model +We are going to implement a forecasting model for a +virtual fungus called «Fungus pilosus flavis» (please bear with me, any +phytopatologists who might read this). Let\'s say that it there is a +forecasting model for it that states that + +- There is no infection risk until you have reached 500 day degrees + (celcius) +- After that, the risk multiplies by 2 for each consecutive hour of + leaf wetness (starting at 1 on the first hour). When reaching the + threshold of 24, there is serious risk of infection, and measures + should be taken. + +We'll be using [MS Visual Studio Code](https://code.visualstudio.com/) +for this example, but the process should be transferable to other IDEs. + +### Creating a new project for the forecasting model +We are going to create a Python package. There is a very good post about how to +do this in [RealPython](https://realpython.com/pypi-publish-python-package/), and [Python's own documentation](https://packaging.python.org/en/latest/tutorials/packaging-projects/) is also good (and a bit shorter/simpler) + +Start with creating a folder named e.g. `FungusPilosisFlavisModel`. Enter it, +and activate git inside, like this (or however you normally do it!): + +```bash +$ git init +``` +It is recommended to create a `.gitignore` file with [this standard content](https://github.com/github/gitignore/blob/main/Python.gitignore): + +Create and activate a Python virtualenv for this project. This can be done outside or inside the project folder, just make sure that your IDE knows where to find it. If you create it inside the folder, the IDE may autodiscover it and suggest that it will use it as the default virtualenv in this project. + +```bash +$ python3 -m venv venv +$ source venv/bin/activate +(venv)$ +``` + +Open the folder with your IDE, and add these files and folders + +<img src="illustrations/funguspilosusflavis_scaffold.png" alt="Package scaffolding"/> + +Below you'll find example contents of the `pyproject.toml` file. Make sure to edit the +data that are specific for your project, such as + +* name +* version +* authors +* dependencies + +``` +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "vips_fungus_pilosus_flavis_model" +version = "0.0.1" +description = "Example VIPS model, showcasing functionality" +readme = "README.md" +authors = [{ name="Foo Barson", email="tor-einar.skog@nibio.no" }] +license = { file = "LICENSE"} +classifiers = [ + "Programming Language :: Python :: 3", + "License :: GNU Affero GPL v3", + "Operating System :: OS Independent", +] +dependencies = [ + "shapely", + "pydantic", + "pytz", + "pandas", + "vipscore_common @ git+https://gitlab.nibio.no/VIPS/vipscore-python-common.git@0.2.1" +] + +requires-python = ">=3.9" + +[project.optional-dependencies] +dev = ["pytest"] + +``` + +### Make the package editable locally +Create and activate a Python virtualenv for this project. This can be done outside or inside the project folder, just make sure that your IDE knows where to find it. If you create it inside the folder, the IDE may autodiscover it and suggest that it will use it as the default virtualenv in this project. + +```bash +# Create virtualenv +$ python3 -m venv venv +$ source venv/bin/activate +(venv)$ +# Install your package in editable mode inside your virtualenv so that you can develop and test it +(venv)$ python -m pip install -e . +``` + +### Create the main VIPSModel class +In `/src`, create a folder named e.g. `vips_fungus_pilosus_flavis_model` +Inside of this folder, create the main module `fungus_pilosus_flavis_model.py`. Then, add this contents to the file: + +```python +from vipscore_common.vips_model import VIPSModel +from vipscore_common.entities import Result, ModelConfiguration, WeatherObservation, WeatherElements +from vipscore_common.data_utils import * + + + +class FungusPilosisFlavisModel(VIPSModel): + """ + This is the result of a VIPS Model implementation class + """ + + MODEL_ID = "FUNGUSPILO" + COPYRIGHT = "(c) 2023 ACME Industries" +``` + +Make sure you add the `__init__.py` file in the same folder as your module + +#### Create the method for finding when 500 day degrees has been passed +To find out when 500 day degrees (since some date) have passed, you need +the mean temperature of each day. All weather observations in VIPS are +represented by an instance of the class WeatherObservation. This class +has a few important properties: + +- ElementMeasurementTypeId: Rain, mean temperature, leaf wetness etc. +- TimeMeasured +- LogInterval: Hourly, Daily, Monthly measurement +- Value: the numerical value of the weather observation + +We need a list of one WeatherObservation with mean temperature per day. A simple approach could be: + +1. Loop through all the WeatherObservation objects, and add the value + to the total day degree sum as we do so +2. When the threshold of 500 has been reached, return the date of that + WeatherObservation object. + +So we could start by writing this method: + +```python + + THRESHOLD = 500.0 + + def get_date_when_day_degree_limit_has_passed(self, observations: list): + # Initalize the day degree counter + day_degrees = 0.0 + # Iterate through the list of observations + # !! Assuming the observations list is sorted chronologically!! + for observation in observations: + # Make sure it's only daily temperature observations that are used + if observation.logIntervalId == WeatherObservation.LOG_INTERVAL_ID_1D and observation.elementMeasurementTypeId == WeatherElements.TEMPERATURE_MEAN: + # Add to day_degree_sum + day_degrees = day_degrees + observation.value + # If threshold is reached, return the date of the current temperature measurement + if day_degrees >= self.THRESHOLD: + return observation.timeMeasured + # We have finished looping through the observations, and dayDegrees has + # not passed 500. So we can't return a Date, we must return None(nothing) + return None +``` + +**IMPORTANT NOTE**: These kinds of operations are better solved using Pandas, but +for people unfamiliar with using Pandas, we stick with the simplest form of Python. + +#### Start testing +Now that we have a method, we need to start testing. We will use [pytest](https://docs.pytest.org/). Start by adding a test module named `test_fungus_pilasus_flavis_model.py` in the `tests` folder, and add imports and a method declaration: + +```python +import datetime, pytz +import unittest +from src.vips_fungus_pilosus_flavis_model.fungus_pilosus_flavis_model import * + +class TestFungusPilasusFlavisModel(unittest.TestCase): + def test_get_date_when_day_degree_limit_has_passed(self): + # TODO: Get observations list + observations = None + # Instantiate the model + instance = FungusPilosisFlavisModel() + result = instance.get_date_when_day_degree_limit_has_passed(observations) + expected_date = datetime(2016, 5, 25, 22, 0, tzinfo=pytz.timezone("UTC")) + self.assertEquals(result, expected_date) +``` + +As you can tell, we lack a couple of important parts here: +* The list of weather observations (`observations`) +* The expected date +We need to get hold of weather data: +* Mean daily temperature +* Hourly leaf wetness +Test data can be obtained from NIBIO's Norwegian Agromet service. So to get daily temperature values for a period, you can run: + +https://lmt.nibio.no/services/rest/vips/getdata/forecastfallback?weatherStationId=5&elementMeasurementTypes[]=TM&logInterval=1d&startDate=2016-03-01&startTime=00&endDate=2016-09-30&endTime=00&timeZone=Europe/Oslo + +Save the returned results as e.g. `tests/tm.json` + +Now we add this helper method to the test class: + +```python +import vipscore_common.data_utils + +def get_temperature_data(): + with open("tests/tm.json") as f: + return get_weather_observations_from_json(f.read()) +``` + +...and we call it from the test method: + +```python + + + +class TestFungusPilasusFlavisModel(unittest.TestCase): + def test_get_date_when_day_degree_limit_has_passed(self): + # Get observations list + observations = get_temperature_data() + # Instantiate the model + instance = FungusPilosisFlavisModel() + result = instance.get_date_when_day_degree_limit_has_passed(observations) + + self.assertEqual(result, expected_date) + +``` + + +Let's see if we can get this to work. Install pytest and run: + +```bash +(venv) $ pip install pytest +(venv) $ pytest +``` + +If everything else is correctly set up, this will fail with the following error message: + +``` +============================================================ test session starts ============================================================ +platform linux -- Python 3.10.6, pytest-7.2.1, pluggy-1.0.0 +rootdir: /home/treinar/nextcloud/MaDiPHS/workshop_2023-02/FungusPilosusFlavisModel +collected 1 item + +tests/test_fungus_pilasus_flavis_model.py F [100%] + +================================================================= FAILURES ================================================================== +________________________________ TestFungusPilasusFlavisModel.test_get_date_when_day_degree_limit_has_passed ________________________________ + +self = <tests.test_fungus_pilasus_flavis_model.TestFungusPilasusFlavisModel testMethod=test_get_date_when_day_degree_limit_has_passed> + + def test_get_date_when_day_degree_limit_has_passed(self): + # TODO: Get observations list + observations = None + # Instantiate the model +> instance = FungusPilosisFlavisModel() +E TypeError: Can't instantiate abstract class FungusPilosisFlavisModel with abstract methods copyright, get_model_description, get_model_name, get_model_usage, get_result, get_warning_status_interpretation, license, model_id, sample_config, set_configuration + +tests/test_fungus_pilasus_flavis_model.py:15: TypeError +========================================================== short test summary info ========================================================== +FAILED tests/test_fungus_pilasus_flavis_model.py::TestFungusPilasusFlavisModel::test_get_date_when_day_degree_limit_has_passed - TypeError: Can't instantiate abstract class FungusPilosisFlavisModel with abstract methods copyright, get_model_description, get_model_n... +============================================================= 1 failed in 0.63s ============================================================= + +``` + +pytest fails because we have not implemented any of the abstract methods of VIPSModel. So let's do that, but only by passing them all + +```python + def set_configuration(self, model_configuration: ModelConfiguration): + """ + Set the configuration object (with all its possible parameters) + Must be done before you call get_result + """ + pass + + def get_result(self) -> list[Result]: + """Get the results as a list of Result objects (TODO ref)""" + pass + + @property + def model_id(self) -> str: + """10-character ID of the model. Must be unique (at least in the current system)""" + pass + + @property + def sample_config(self) -> dict: + """A sample configuration in JSON format (TODO check relation with Dict)""" + pass + + @property + def license(self) -> str: + """Returns the license for this piece of software""" + pass + + @property + def copyright(self) -> str: + """Name of person/organization that holds the copyright, and contact information""" + pass + + def get_model_name(self, language: str) -> str: + """Returns the model name in the specified language (<a href="http://www.loc.gov/standards/iso639-2/php/English_list.php">ISO-639-2</a>)""" + pass + + + def get_model_description(self, language: str) -> str: + """Returns the model description in the specified language (<a href="http://www.loc.gov/standards/iso639-2/php/English_list.php">ISO-639-2</a>)""" + pass + + def get_warning_status_interpretation(self, language: str) -> str: + """How to interpret the warning status (red-yellow-green, what does it mean?) in the specified language (<a href="http://www.loc.gov/standards/iso639-2/php/English_list.php">ISO-639-2</a>)""" + pass + + def get_model_usage(self, language: str) -> str: + """Technical manual for this model, in the specified language (<a href="http://www.loc.gov/standards/iso639-2/php/English_list.php">ISO-639-2</a>)""" + pass +``` + +Running `pytest` again should make the tests pass. + +#### Create the method to calculate the infection risk +We can operate on hourly weather data for leaf wetness and calculate the +infection risk. Data in will be a list of weather observations (leaf +wetness, hourly). Output data will be a dictionary with timestamp as +key, and the infection risk as value. So for instance for 24th July 2014 +14:00 UTC there will be only one value. + +To get hourly leaf wetness values for the same period and location, request: + +https://lmt.nibio.no/services/rest/vips/getdata/forecastfallback?weatherStationId=5&elementMeasurementTypes[]=BT&logInterval=1h&startDate=2016-03-01&startTime=00&endDate=2016-09-30&endTime=00&timeZone=Europe/Oslo + +An example of a solution can be: + +```python + def get_infection_risk(self, observations:list): + # Create the map with dates and infection risk values + risk_map = {} + + # Counter for consecutive hours of leaf wetness + consecutive_hours_with_leaf_wetness = 0 + + # !! Assuming the observations list is sorted chronologically!! + # Loop through the list of observations + for observation in observations: + # We define a lower threshold for leaf wetnes to be 10mins/hour + if observation.value > 10.0: + # Leaf wetness registered, add to consecutive hours counter + consecutive_hours_with_leaf_wetness = consecutive_hours_with_leaf_wetness + 1 + else: + # No leaf wetness, reset counter + consecutive_hours_with_leaf_wetness = 0 + # We set the risk value + risk_map[observation.timeMeasured] = consecutive_hours_with_leaf_wetness * 2 + # Return the map with all values + return risk_map +``` +**Exercise: Write a test for get_infection_risk()** + +And to get hourly leaf wetness values for the same period and location, request: + +https://lmt.nibio.no/services/rest/vips/getdata/forecastfallback?weatherStationId=5&elementMeasurementTypes[]=BT&logInterval=1h&startDate=2016-03-01&startTime=00&endDate=2016-09-30&endTime=00&timeZone=Europe/Oslo + +### Putting it together + + +We now have the most important methods created (and successfully +tested). What we need to do now is to get data in (set configuration, +get weather data etc) and get the results out in the expected format. + +#### Data in + +Input data are sendt in a large lump called a ModelConfiguration. It\'s +a key based store of many different kind of objects: Numbers, strings, dates, WeatherObservations. This +configuration object is sent to the model through the method +set_configuration. So to get the weather data, we need to extract them +from the configuration object in that method. An example of how to do +this is as follows: + +First, at the top of the class, declare the object that holds the +weather data: + +```python + observations = None +``` + +This list will stay empty (NULL) until set_configuration does something +about it. So let's do that, e.g.: + +```python + def set_configuration(self, model_configuration: ModelConfiguration): + """ + Set the configuration object (with all its possible parameters) + Must be done before you call get_result + """ + # Get the observation list, using the data_utils helper module + self.observations = get_weather_observations_from_json_list(model_configuration.config_parameters["observations"]) +``` + +So now we have the weather data in a list, and we can start using them + +#### Data out + +Data out are sent as a list of Result objects. The method to get the +data is called `get_result()`, surprisingly. An example of this method +could be: + +```python + + CONTROLLED_INFECTION_RISK = "CONTROLLED_INFECTION_RISK" + + def get_result(self) -> list[Result]: + """Get the results as a list of Result objects (TODO ref)""" + # Initialize the list of results + results = [] + # !! Assuming the observations list is sorted chronologically!! TODO Sort algorithm + # Which date did day degree sum exceed 500? + day_degree_limit_reach_date = self.get_date_when_day_degree_limit_has_passed(self.observations) + + # Get infection risk for the whole period + uncontrolled_infection_risk = self.get_infection_risk(self.observations) + # Get all dates from the map of infection risk + date_list = list(uncontrolled_infection_risk.keys()) + date_list.sort() + + for current_date in date_list: + result = Result( + validTimeStart=current_date, + validTimeEnd=None, + warningStatus=0 # Temporary, set it later + ) + + # If we're after the date of day degree sum > 500, use the infectionrisk + if current_date >= day_degree_limit_reach_date: + # Set infection risk + result.set_value(self.MODEL_ID, self.CONTROLLED_INFECTION_RISK, "%s" % uncontrolled_infection_risk[current_date]) + else: + # Set infection risk to 0 + result.set_value(self.MODEL_ID, self.CONTROLLED_INFECTION_RISK, "0") + + # Set the warning status + # If controlled infection risk < 64, status is NO RISK + # Otherwise it's HIGH RISK + result.warning_status = Result.WARNING_STATUS_NO_RISK if uncontrolled_infection_risk[current_date] <64 else Result.WARNING_STATUS_HIGH_RISK + results.append(result) + return results +``` + +Now it's time to test the methods. We + +```python + def test_get_result(self): + """ + We get an infection risk of 10 at a certain point in the time series + """ + tm_obs = get_temperature_data() + lw_obs = get_lw_data() + + observations = tm_obs + lw_obs + + instance = FungusPilosisFlavisModel() + model_config = ModelConfiguration( + model_id = instance.MODEL_ID, + config_parameters = {"observations": observations} + ) + instance.set_configuration(model_config) + + results = instance.get_result() + + self.assertIsNotNone(results) + + self.assertEqual(int(results[5094].get_value(instance.MODEL_ID,instance.CONTROLLED_INFECTION_RISK)),10) +``` + +#### Implementing the meta information methods +So, now you have a forecasting model that produces the expected results. +When this model is deployed to the VIPS core runtime, it is discovered +automatically and added to the list of available models. In order for +other systems (like VIPSLogic or another client) to be able to query and +show information about the model, it needs to implement the methods that +provide documentation: + +- get_model_name() - the name of the model. For instance «Fungus pilosus + flavis model» +- get_license() - Open Source? Proprietary? Your pick +- get_copyright() - For instance «(c) 2014 Bioforsk» +- get_model_description() - Detailed description of how the model works, + from a biological perspective +- get_model_usage() - How to configure the model (what parameters are + needed, what values may they have and so on) +- get_sample_config() - A sample JSON configuration file. + +Most of these methods have two versions: One takes language into +account, one doesn't. Translation in model documentation is part of a +presently unwritten chapter. For now, you can do this if you want as a +general pattern: + +```python + def get_model_name(self, language = VIPSModel.default_language) -> str: + """Returns the model name in the specified language (<a href="http://www.loc.gov/standards/iso639-2/php/English_list.php">ISO-639-2</a>)""" + return "Fungus pilosus flavis model" +``` + +The [Reference Model](https://gitlab.nibio.no/VIPS/models/python/referencemodel) contains examples for translation and how to include images in the description text. \ No newline at end of file