diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..d73198fd985df3ed4a7cf516c93af8d4a6df5734
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+    "python.testing.unittestArgs": [
+        "-v",
+        "-s",
+        "./src",
+        "-p",
+        "test_*.py"
+    ],
+    "python.testing.pytestEnabled": false,
+    "python.testing.unittestEnabled": true
+}
\ No newline at end of file
diff --git a/docs/illustrations/vipslogo_512.png b/docs/illustrations/vipslogo_512.png
new file mode 100644
index 0000000000000000000000000000000000000000..f4e416ef1adf3fa60969a5cfb1fd6387ee867ae3
Binary files /dev/null and b/docs/illustrations/vipslogo_512.png differ
diff --git a/src/vipscore_common/data_utils.py b/src/vipscore_common/data_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..10098ecc837c05b0da783010835a099717e787ec
--- /dev/null
+++ b/src/vipscore_common/data_utils.py
@@ -0,0 +1,89 @@
+#!/usr/bin/python3
+"""
+Copyright (c) 2023 NIBIO <http://www.nibio.no/>. 
+
+This file is part of VIPSCore-Python-Common.
+VIPSCore-Python-Common is free software: you can redistribute it and/or modify
+it under the terms of the NIBIO Open Source License as published by 
+NIBIO, either version 1 of the License, or (at your option) any
+later version.
+ 
+VIPSCore-Python-Common is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+NIBIO Open Source License for more details.
+
+You should have received a copy of the NIBIO Open Source License
+along with VIPSCore-Python-Common.  If not, see <http://www.nibio.no/licenses/>.
+"""
+
+"""
+Util methods for data manipulation
+Author: Tor-Einar Skog <tor-einar.skog@nibio.no>
+"""
+
+import numpy as np
+import pandas as pd
+from pandas import DataFrame
+import json
+from entities import *
+
+def get_dataframe_from_weather_observations(weather_observations: list, timezone:datetime.tzinfo) -> DataFrame:
+    """
+    Create a Pandas data frame with timeseries from VIPS weather observations
+    """
+    # Analyze the input data
+    # Which weather parameters are included
+    # Get first and last time_measured
+    # What's the most specific log interval (hour? day?)
+    params = set()
+    start_date = None
+    end_date = None
+    min_log_interval = None
+    for obs in weather_observations:
+        start_date = obs.timeMeasured if start_date is None else obs.timeMeasured if obs.timeMeasured < start_date else start_date
+        end_date = obs.timeMeasured if end_date is None else obs.timeMeasured if obs.timeMeasured > end_date else end_date
+        params.add(obs.elementMeasurementTypeId)
+        if min_log_interval is None:
+            min_log_interval = obs.logIntervalId 
+        elif min_log_interval == WeatherObservation.LOG_INTERVAL_ID_1D and obs.logIntervalId == WeatherObservation.LOG_INTERVAL_ID_1H:
+            min_log_interval = WeatherObservation.LOG_INTERVAL_ID_1H
+    
+    # Start date must be converted to the given timezone
+    tz_start_date = start_date.astimezone(timezone) 
+    # What's the frequency? Hourly? Daily? TODO: Support other log intervals
+    freq = "H" if min_log_interval == 1 else "D"
+    # This creates a datetime_range in the given timezone
+    datetime_range = pd.date_range(tz_start_date,periods = len(weather_observations),freq=freq,tz=timezone)
+    # Create the dataframe with the correct columns
+    df = pd.DataFrame(index=datetime_range, columns=list(params))
+    
+    # Loop through the observations again, adding values to the frame
+    for obs in weather_observations:
+        df.at[obs.timeMeasured, obs.elementMeasurementTypeId] = obs.value
+    
+    return df
+
+
+def get_weather_observations_from_json(weather_data_raw: str) -> list:
+    """
+    Convert a raw json string with VIPS of weather data to a list of WeatherObservation objects
+    """
+    obs_json_list = []
+    #print(weather_data_raw)
+    weather_data_json = json.loads(weather_data_raw)
+    
+    return get_weather_observations_from_json_list(weather_data_json)
+
+def get_weather_observations_from_json_list(weather_data: list) -> list:
+    """
+    Convert a json list of VIPS of weather data to a list of WeatherObservation objects
+    """
+    retval = []
+    for node in weather_data:
+        retval.append(WeatherObservation(**node))
+    return retval
+
+def get_temp_adjusted_for_base(temp: float, base_temp = 0.0) -> float:
+    adjusted = temp - base_temp
+    return max(0.0, adjusted)
\ No newline at end of file
diff --git a/src/vipscore_common/entities.py b/src/vipscore_common/entities.py
index d9be988a1c0a190ed6d5eb4b9b49ad139f10c0be..43b17a8005066978803aabc30bfb046cb018f02f 100755
--- a/src/vipscore_common/entities.py
+++ b/src/vipscore_common/entities.py
@@ -24,8 +24,9 @@ along with VIPSCore-Python-Common.  If not, see <http://www.nibio.no/licenses/>.
 
 from datetime import datetime
 from shapely.geometry import Point, Polygon
-from pydantic import BaseModel, validator, constr
-from typing import Any, Union
+from pydantic import BaseModel,  validator, constr
+from typing import Any, Union, ClassVar
+import pytz
 
 class Result(BaseModel):
     """Represents a set of DSS model result values for a given point in space (Point, Polygon, MultiPolygon) and time (Period or immediate) """
@@ -35,6 +36,14 @@ class Result(BaseModel):
     warning_status: int
     all_values: dict
 
+
+    WARNING_STATUS_NO_WARNING:              ClassVar[int] = 0
+    WARNING_STATUS_NO_WARNING_MISSING_DATA: ClassVar[int] = 1
+    WARNING_STATUS_NO_RISK:                 ClassVar[int] = 2
+    WARNING_STATUS_MINOR_RISK:              ClassVar[int] = 3
+    WARNING_STATUS_HIGH_RISK:               ClassVar[int] = 4
+    
+
     def get_keys(self):
         return set(self.all_values.keys) if self.all_values is not None else set()
 
@@ -54,6 +63,50 @@ class ModelConfiguration(BaseModel):
     model_id: constr(min_length=10, max_length=10)
     config_parameters: dict
 
+    # Can we do this and still serialize the object??
+    def get_config_parameter_as_date(self, param_name: str, required=True) -> datetime:
+        """
+        Input check and typing of date (ISO format)
+        """
+        param_value = self.config_parameters.get(param_name, None)
+        if required and param_value is None:
+            raise ModelConfigurationException("%s is required" % param_name)
+        try:
+            return datetime.fromisoformat(param_value)
+        except ValueError:
+            raise ModelConfigurationException("%s=%s, which is not a valid date." % (param_name, param_value))
+
+    # Can we do this and still serialize the object??
+    def get_config_parameter_as_string(self, param_name: str, required=True) -> datetime:
+        """
+        Input check
+        """
+        param_value = self.config_parameters.get(param_name, None)
+        if required and param_value is None:
+            raise ModelConfigurationException("%s is required" % param_name)
+        return param_value
+    
+    # Can we do this and still serialize the object??
+    def get_config_parameter_as_timezone(self, param_name: str, required=True) -> datetime:
+        """
+        Input check
+        """
+        param_value = self.config_parameters.get(param_name, None)
+        if required and param_value is None:
+            raise ModelConfigurationException("%s is required" % param_name)
+        
+        try:
+            return pytz.timezone(param_value)
+        except pytz.exceptions.UnknownTimeZoneError:
+            raise ModelConfigurationException("%s=%s, which is not a valid timezone." % (param_name, param_value))
+
+
+class ModelConfigurationException(Exception):
+    """
+    Should be raised when there's something wrong with the VIPS model configuration
+    passed to VIPSModel.set_configuration
+    """
+
 class WeatherObservation(BaseModel):
     """Data class for a single weather observation"""
     elementMeasurementTypeId: str
@@ -62,18 +115,19 @@ class WeatherObservation(BaseModel):
     value: float
 
     # Log interval categories
-    LOG_INTERVAL_ID_1M = 8;
-    LOG_INTERVAL_ID_5M = 9;
-    LOG_INTERVAL_ID_10M = 6;
-    LOG_INTERVAL_ID_15M = 5;
-    LOG_INTERVAL_ID_30M = 4;
-    LOG_INTERVAL_ID_1H = 1;
-    LOG_INTERVAL_ID_3H = 3;
-    LOG_INTERVAL_ID_6H = 7;
-    LOG_INTERVAL_ID_1D = 2;
+    LOG_INTERVAL_ID_1M: ClassVar[int] = 8
+    LOG_INTERVAL_ID_5M: ClassVar[int] = 9
+    LOG_INTERVAL_ID_10M: ClassVar[int] = 6
+    LOG_INTERVAL_ID_15M: ClassVar[int] = 5
+    LOG_INTERVAL_ID_30M: ClassVar[int] = 4
+    LOG_INTERVAL_ID_1H: ClassVar[int] = 1
+    LOG_INTERVAL_ID_3H: ClassVar[int] = 3
+    LOG_INTERVAL_ID_6H: ClassVar[int] = 7
+    LOG_INTERVAL_ID_1D: ClassVar[int] = 2
 
     @validator("timeMeasured")
     def ensure_timezone(cls, v):
         if v.tzinfo is None or v.tzinfo.utcoffset(v) is None:
             raise ValueError("%s must be timezone aware" % v)
         return v
+
diff --git a/src/vipscore_common/reference_model.py b/src/vipscore_common/reference_model.py
index 54d3c3229df6c6b43f1747693b495153dfe6a652..da96f082adeae5eadb306ed3b63315223ee5dcd5 100755
--- a/src/vipscore_common/reference_model.py
+++ b/src/vipscore_common/reference_model.py
@@ -20,25 +20,69 @@ along with VIPSCore-Python-Common.  If not, see <http://www.nibio.no/licenses/>.
 
 from vips_model import VIPSModel
 from entities import Result, ModelConfiguration, WeatherObservation
+import data_utils
+import numpy as np
 
 class ReferenceModel(VIPSModel):
+    """
+        A reference implementation of the VIPSModel abstract class. Showcasing core functionality
+        and best practices
+    """
 
     MODEL_ID        = "REFERENCEM"
     COPYRIGHT       = "(c) 2023 NIBIO"
+
+    THRESHOLD_LOW = 100.0
+    THRESHOLD_MEDIUM = 300.0
+    THRESHOLD_HIGH = 500.0
  
     def set_configuration(self, model_configuration: ModelConfiguration):
         if not isinstance(model_configuration, ModelConfiguration):
             raise ValueError("%s is not a ModelConfiguration object" % model_configuration)
         if model_configuration.model_id != ReferenceModel.MODEL_ID:
             raise ValueError("%s is not the correct model ID!" % model_configuration.model_id)
-        self.sowing_date = model_configuration.config_parameters["sowingDate"]
-        self.observations = []
-        for node in model_configuration.config_parameters["observations"]:
-            self.observations.append(WeatherObservation(**node))
+        
+        # Input data check
+        self.sowing_date = model_configuration.get_config_parameter_as_date("sowingDate")
+        self.timezone = model_configuration.get_config_parameter_as_timezone("timeZone")
+        # Weather data is turned into Pandas dataframe
+        self.df = data_utils.get_dataframe_from_weather_observations(
+                data_utils.get_weather_observations_from_json_list(
+                    model_configuration.config_parameters["observations"]
+                    ),
+                self.timezone
+            )
+
+    
+
+    def determine_warning_status(self, TMDD: float) -> int:
+        if TMDD < ReferenceModel.THRESHOLD_MEDIUM:
+            return  Result.WARNING_STATUS_NO_WARNING
+        if TMDD < ReferenceModel.THRESHOLD_HIGH:
+            return Result.WARNING_STATUS_MINOR_RISK
+        else:
+            return Result.WARNING_STATUS_HIGH_RISK
+
+        
     
     def get_result(self) -> list[Result]:
         """Get the results as a list of Result objects (TODO ref)"""
-        pass
+        
+        # Calculate day degrees from sowingDate and as far as weather data goes
+        # Adjusting for base temperature
+        self.df["TMContrib"] = self.df["TM"].apply(data_utils.get_temp_adjusted_for_base, args=(5,))
+        # Aggregating the day degrees
+        self.df["TMDD"] = self.df["TMContrib"].cumsum()
+        # Adding the thresholds to the data frame
+        self.df["THRESHOLD_LOW"] = ReferenceModel.THRESHOLD_LOW
+        self.df["THRESHOLD_MEDIUM"] = ReferenceModel.THRESHOLD_MEDIUM
+        self.df["THRESHOLD_HIGH"] = ReferenceModel.THRESHOLD_HIGH
+        self.df["WARNING_STATUS"] = self.df["TMDD"].apply(self.determine_warning_status)
+        print(self.df)
+        # For each day: check accumulated day-degrees and decide warning status
+
+
+
 
     @property
     def model_id(self) -> str:
@@ -91,8 +135,8 @@ class ReferenceModel(VIPSModel):
         Gray   status (warning status == 0): Warning not applicable
         Blue   status (warning status == 1): Missing data
         Green  status (warning status == 2): No risk. Sleep well
-        Yellow status (warning status == 3): Medium risk. Be on the alert, inspect your field
-        Red status    (warning status == 4): High risk. When the going gets tough, the tough get going
+        Yellow status (warning status == 3): The day-degree hreshold for medium risk has been passed. Be on the alert, inspect your field
+        Red status    (warning status == 4): The day-degree threshold for high risk has been passed. When the going gets tough, the tough get going
         """
 
     def get_model_usage(self, language = VIPSModel.default_language) -> str:
diff --git a/src/vipscore_common/tests/test_data_utils.py b/src/vipscore_common/tests/test_data_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..09dab010aa0f266db0bf92d6cd21a780a5319577
--- /dev/null
+++ b/src/vipscore_common/tests/test_data_utils.py
@@ -0,0 +1,31 @@
+import unittest
+
+
+import data_utils
+from reference_model import *
+from entities import *
+import pytz
+
+class TestDataUtils(unittest.TestCase):
+    def test_get_weather_observations_from_json(self):
+        """
+        The method can convert a json dict to a list of WeatherObservation objects
+        """
+        with open ("src/vipscore_common/tests/weather_data_2015_NO_aas_TMD.json") as f:
+            result = data_utils.get_weather_observations_from_json(f.read())
+            # Length should be > 0
+            self.assertGreater(len(result), 0)
+            # All items should be a WeatherObservation class
+            for item in result:
+                self.assertIsInstance(item, WeatherObservation)
+    
+    def test_getdataframe_from_weather_observations(self):
+        """
+        Lorem Ipsum osv
+        """
+        with open ("src/vipscore_common/tests/weather_data_2015_NO_aas_TMD.json") as f:
+            weather_observations = data_utils.get_weather_observations_from_json(f.read())
+            result = data_utils.get_dataframe_from_weather_observations(weather_observations, pytz.timezone("Europe/Oslo"))
+            self.assertIsNotNone(result)
+            start_date = datetime.fromisoformat("2015-03-01T00:00:00+01:00")
+            self.assertEqual(1.41025,result.at[start_date,"TM"])
diff --git a/src/vipscore_common/tests/test_reference_model.py b/src/vipscore_common/tests/test_reference_model.py
index 9732894028a296ef615c80baf9eff8bbc8115bc5..161a04b6738cc19ffaae8df4844f550a43c8ef10 100644
--- a/src/vipscore_common/tests/test_reference_model.py
+++ b/src/vipscore_common/tests/test_reference_model.py
@@ -6,15 +6,16 @@ from reference_model import *
 from entities import *
 
 def get_model_configuration():
-        with open ("src/vipscore_common/tests/weather_data_2015_NO_aas_TMD.json") as f:
-            weather_observations = json.load(f)
-            return ModelConfiguration(
-                model_id=ReferenceModel.MODEL_ID,
-                config_parameters={
-                    "sowingDate": "2022-03-01",
-                    "observations": weather_observations
-                }
-            )
+    with open ("src/vipscore_common/tests/weather_data_2015_NO_aas_TMD.json") as f:
+        weather_observations = json.load(f)
+        return ModelConfiguration(
+            model_id=ReferenceModel.MODEL_ID,
+            config_parameters={
+                "sowingDate": "2022-03-01",
+                "timeZone" : "Europe/Oslo",
+                "observations": weather_observations
+            }
+        )
 
 class TestReferenceModel(unittest.TestCase):
     def test_set_configuration(self):
@@ -25,6 +26,14 @@ class TestReferenceModel(unittest.TestCase):
         instance = ReferenceModel()
         instance.set_configuration(get_model_configuration())
 
+    def test_get_result(self):
+        """
+        We get a series of results from the calculation
+        """
+        instance = ReferenceModel()
+        instance.set_configuration(get_model_configuration())
+        result_list = instance.get_result()
+
     def test_get_model_id(self):
          """
          The model returns the correct ID