diff --git a/Tutorial.md b/Tutorial.md new file mode 100644 index 0000000..fd6e5bf --- /dev/null +++ b/Tutorial.md @@ -0,0 +1,56 @@ +# Tutorial of the configuration + +This tutorial explains how the configuration work on the model. User, modeler and developer interact with the configuration. The user update the file, the modeler build the file and the developer use config to init the model and run it. + +## User + +The user interacts only with the configuration file, not with Python code. +The user can modify the YAML and JSON file. He can change the values, the parameters and also comment. +The file can be opened with a text editor or on Visual Studio Code. +The configuration file is written in YAML or JSON and contains sections, parameters inside each section. + +For example, in HydroRoot with simulation and planting as sections with their parameters: +simulation: + sdate: '2012-08-01 00:00:00' + edate: '2012-08-04 23:00:00' + latitude: 43.61 + longitude: 3.87 + elevation: 44.0 + tzone: Europe/Paris + output_index: '' + unit_scene_length: cm + hydraulic_structure: true + negligible_shoot_resistance: false + energy_budget: true +planting: + spacing_between_rows: 3.6 + spacing_on_row: 1 + row_angle_with_south: 140.0 + +The user can modify only the values, never the parameter names but can never change the name of parameters. He can change latitude 43.61 to 41 for example. The user can also comment, comments begin with #. JSON doesn't support comment, if the user want to comment, he have to use YAML. + +## Modeler + +The modeler constructs the configuration. A Config is a Python dictionary that stores sections and parameters. A Config is built from a list of ModelUnit objects. + +For example: +p_sdate = Parameter("sdate", "2012-08-01 00:00:00") +p_edate = Parameter("edate", "2012-08-04 23:00:00") +p_lat = Parameter("latitude", 43.61) +p_longitude = Parameter("longitude", 3.87) +p_elevation = Parameter("elevation", 44.0) +p_tzone = Parameter("tzone", "Europe/Paris") +p_output_index = Parameter("output_index", "") +p_unit_scene_length = Parameter("unit_scene_length", "cm") +p_hydraulic_structure = Parameter("hydraulic_structure", True) +p_negligible_shoot_resistance = Parameter("negligible_shoot_resistance", False) +p_energy_budget = Parameter("energy_budget", True) +parameters = [p_sdate, p_edate, p_lat, p_longitude, p_elevation, p_tzone, p_output_index, p_unit_scene_length, p_hydraulic_structure, p_negligible_shoot_resistance, p_energy_budget] +simulation = ModelUnit('simulation', parameters) +config = Config([simulation]) + +The modeler can also load and dump a configuration file as config.dump("params.yml") or config.dump("params.json"). The modeler can also add new sections, for example *config.add_section(planting)*. + +## Developer + +The developer receives a config object. Config unherite from a dict, the developer can access to the parameters value. He can initialize the model and call run(). \ No newline at end of file diff --git a/conda/meta.yaml b/conda/meta.yaml index 91a6421..aecb512 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -25,12 +25,16 @@ build: requirements: host: - python + - ruamel.yaml + - pyyaml {% for dep in build_deps %} - {{ dep }} {% endfor %} run: - python + - ruamel.yaml + - pyyaml {% for dep in deps + conda_deps %} - {{ dep }} {% endfor %} diff --git a/pyproject.toml b/pyproject.toml index 63bfc18..58da666 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dependencies = [ "configobj", "six", "setuptools", + "pyyaml", ] [tool.setuptools.dynamic] diff --git a/src/openalea/core/Documentation.ipynb b/src/openalea/core/Documentation.ipynb new file mode 100644 index 0000000..aca80a2 --- /dev/null +++ b/src/openalea/core/Documentation.ipynb @@ -0,0 +1,420 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "16c62968-51e4-4d0a-8305-874f666d5180", + "metadata": {}, + "source": [ + "# Tutorial" + ] + }, + { + "cell_type": "markdown", + "id": "04b358d7-3d1d-4c05-b03e-c431732b960d", + "metadata": {}, + "source": [ + "## User" + ] + }, + { + "cell_type": "markdown", + "id": "12ef5489-ac5c-431a-9c6b-695c998f35fc", + "metadata": {}, + "source": [ + "The user interacts only with the configuration file, not with Python code.\n", + "\n", + "1) Open the configuration file, in YAML or JSON format.\n", + " \n", + "The user can open the file in Visual Studio Code or any other text editor. For example:\n", + "```yaml\n", + "# description: Simulations parameters\n", + "simulation:\n", + "# description: Start date of the simulation\n", + "# param_type: datetime\n", + " sdate: '2012-08-01 00:00:00'\n", + "# description: End date of the simulation\n", + "# param_type: datetime\n", + " edate: '2012-08-04 23:00:00'\n", + "# unit: degrees\n", + "# param_type: float\n", + " latitude: 43.61\n", + "# description: Longitude of the simulation\n", + "# unit: degrees\n", + "# param_type: float\n", + " longitude: 3.87\n", + "# description: Elevation of the simulation\n", + "# unit: meters\n", + "# param_type: float\n", + " elevation: 44.0\n", + "# description: Time zone\n", + "# param_type: string\n", + " tzone: Europe/Paris\n", + "# description: Output index\n", + "# param_type: string\n", + " output_index: ''\n", + "# description: Unit of scene length\n", + "# param_type: string\n", + " unit_scene_length: cm\n", + "# description: Hydraulic structure\n", + "# param_type: boolean\n", + " hydraulic_structure: true\n", + "# description: Negligible short resistance\n", + "# param_type: boolean\n", + " negligible_shoot_resistance: false\n", + "# description: Energy budget\n", + "# param_type: boolean\n", + " energy_budget: true\n", + "# description: Planting section\n", + "planting:\n", + "# description: Distance between planting rows\n", + "# unit: meters\n", + "# param_type: float\n", + " spacing_between_rows: 3.6\n", + "# description: Spacing on the row\n", + "# unit: meters\n", + "# param_type: float\n", + " spacing_on_row: 1\n", + "# description: Row angle with south\n", + "# unit: degrees\n", + "# param_type: float\n", + " row_angle_with_south: 140.0\n", + "# description: Phenology section\n", + "phenology:\n", + "# description: Emergence date\n", + "# param_type: datetime\n", + " emdate: '2012-04-01 00:00:00'\n", + "# description: Base temperature\n", + "# unit: degrees\n", + "# param_type: float\n", + " t_base: 10.0\n", + "# description: Mtg api section\n", + "mtg_api:\n", + "# description: Collar label\n", + "# param_type: string\n", + " collar_label: inT\n", + "# description: Leaf label prefix\n", + "# param_type: string\n", + " leaf_lbl_prefix: L\n", + "# description: Stem label prefix\n", + "# param_type: list\n", + " stem_lbl_prefix:\n", + " - in\n", + " - Pet\n", + " - cx\n", + "# description: Numerical section\n", + "numerical_resolution:\n", + "# description: Maximum number of iterations\n", + "# param_type: integer\n", + " max_iter: 100\n", + "# description: Psi step\n", + "# param_type: float\n", + " psi_step: 1.0\n", + "# description: Psi error threshold\n", + "# unit: seconds\n", + " psi_error_threshold: 0.05\n", + "# description: Time step\n", + "# param_type: float\n", + " t_step: 1.0\n", + "# description: Time error threshold\n", + "# unit: float\n", + " t_error_threshold: 0.02\n", + "\n", + " ```\n", + "\n", + "Simulation and planting are sections. *sdate, edate, latitude* and all the parameters under the simulation and planting sections are parameters. All lines starting with # are comments.\n", + "In the description, we should explain what the parameter and the section do. Each parameter can include optional comments such as unit and param_type." + ] + }, + { + "cell_type": "markdown", + "id": "20e21ed0-70ba-4d5a-aa69-399abefe6ccf", + "metadata": {}, + "source": [ + "2) Modify a value.\n", + "\n", + "The user can change any value directly in the JSON or YAML file. \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "58b1b25d-4682-48e9-9290-c861dbb99fb2", + "metadata": {}, + "source": [ + "## Modeler" + ] + }, + { + "cell_type": "markdown", + "id": "4c397ddd-3f07-47dd-8e22-92ed0e89f6fd", + "metadata": {}, + "source": [ + "The modeler define the configuration object from submodels. A model is composed of sub-models, each sub-model return a ModelUnitConfig. Each modelUnitConfig contains a list of parameters. A Config is a Python dictionary that stores sections and their parameters. A Config is built from a list of ModelUnit objects. \n" + ] + }, + { + "cell_type": "markdown", + "id": "6d68dfd5-305b-4c53-ab42-d2ed14a9cad4", + "metadata": {}, + "source": [ + "In the config, we have 3 classes: Parameter, ModelUnit and Config. " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "dd4c69a0-dc54-4473-9e4f-8227e1a4e766", + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from openalea.core.config import Config, ModelUnit, Parameter" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "0e1d4f73-b9b3-40fe-b816-83807abe71f3", + "metadata": {}, + "outputs": [], + "source": [ + "from ruamel.yaml import YAML\n", + "from config import to_commented_map, add_unit_comment, add_comment2\n" + ] + }, + { + "cell_type": "markdown", + "id": "437eeae0-46fd-42e0-8298-c91bfcc2b484", + "metadata": {}, + "source": [ + "The Modeler can create new parameters using instances of the Parameter class. A ModelUnit is a dictionary that contains a list of Parameter instances.\n", + "For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "597d2dce-4f57-4f9e-b78e-0555ebcca90e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'simulation': {'sdate': '2012-08-01 00:00:00', 'edate': '2012-08-04 23:00:00', 'latitude': 43.61, 'longitude': 3.87, 'elevation': 44.0, 'tzone': 'Europe/Paris', 'output_index': '', 'unit_scene_length': 'cm', 'hydraulic_structure': True, 'negligible_shoot_resistance': False, 'energy_budget': True}}]\n" + ] + } + ], + "source": [ + "p_sdate = Parameter(\"sdate\", \"2012-08-01 00:00:00\", \"Start date of the simulation\", None, \"datetime\")\n", + "p_edate = Parameter(\"edate\", \"2012-08-04 23:00:00\", \"End date of the simulation\", None, \"datetime\")\n", + "p_lat = Parameter(\"latitude\", 43.61, None, \"degrees\", \"float\")\n", + "p_longitude = Parameter(\"longitude\", 3.87, \"Longitude of the simulation\", \"degrees\", \"float\")\n", + "p_elevation = Parameter(\"elevation\", 44.0, \"Elevation of the simulation\", \"meters\", \"float\")\n", + "p_tzone = Parameter(\"tzone\", \"Europe/Paris\", \"Time zone\", None, \"string\")\n", + "p_output_index = Parameter(\"output_index\", \"\", \"Output index\", None, \"string\")\n", + "p_unit_scene_length = Parameter(\"unit_scene_length\", \"cm\", \"Unit of scene length\", None, \"string\")\n", + "p_hydraulic_structure = Parameter(\"hydraulic_structure\", True, \"Hydraulic structure\", None, \"boolean\")\n", + "p_negligible_shoot_resistance = Parameter(\"negligible_shoot_resistance\", False, \"Negligible short resistance\", None, \"boolean\")\n", + "p_energy_budget = Parameter(\"energy_budget\", True, \"Energy budget\", None, \"boolean\")\n", + "parameters = [p_sdate, p_edate, p_lat, p_longitude, p_elevation, p_tzone, p_output_index, p_unit_scene_length, p_hydraulic_structure, p_negligible_shoot_resistance, p_energy_budget]\n", + "simulation = ModelUnit(\"simulation\", parameters, \"Simulations parameters\")\n", + "config = Config([simulation])\n", + "print(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "54e7bc9d-13c6-4cf6-883a-e2fc2cb43f8d", + "metadata": {}, + "outputs": [], + "source": [ + "p_spacing_between_rows = Parameter(\"spacing_between_rows\", 3.6, \"Distance between planting rows\", \"meters\", \"float\")\n", + "p_spacing_on_row = Parameter(\"spacing_on_row\", 1, \"Spacing on the row\", \"meters\", \"float\")\n", + "p_row_angle_with_south = Parameter(\"row_angle_with_south\", 140.0, \"Row angle with south\", \"degrees\", \"float\")\n", + "parameters = [p_spacing_between_rows, p_spacing_on_row, p_row_angle_with_south]\n", + "planting = ModelUnit('planting', parameters, \"Planting section\")" + ] + }, + { + "cell_type": "markdown", + "id": "3cdbc30e-fa0f-4a90-955f-dc5ca97c919d", + "metadata": {}, + "source": [ + "Add new sections." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "2f1b072b-be43-4a5c-a8e3-c063616620bf", + "metadata": {}, + "outputs": [], + "source": [ + "config.add_section(planting)" + ] + }, + { + "cell_type": "markdown", + "id": "3be19f2e-5d4f-4f0c-84c4-34ab56ffda4c", + "metadata": {}, + "source": [ + "The modeler can also load and dump a configuration file." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "e8b5003a-44eb-4193-b16b-421c7657199c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'simulation': {'sdate': '2012-08-01 00:00:00', 'edate': '2012-08-04 23:00:00', 'latitude': 43.61, 'longitude': 3.87, 'elevation': 44.0, 'tzone': 'Europe/Paris', 'output_index': '', 'unit_scene_length': 'cm', 'hydraulic_structure': True, 'negligible_shoot_resistance': False, 'energy_budget': True}}, {'planting': {'spacing_between_rows': 3.6, 'spacing_on_row': 2.5, 'row_angle_with_south': 140.0}}]\n", + "New value :\n", + "2.5\n" + ] + } + ], + "source": [ + "from ruamel.yaml import YAML\n", + "\n", + "config.dump(\"config.yml\")\n", + "config[\"planting\"][\"spacing_on_row\"] = 2.5\n", + "config.dump(\"config.yml\")\n", + "new_config = config.load(\"config.yml\")\n", + "print(new_config)\n", + "print(\"New value :\")\n", + "print(config[\"planting\"][\"spacing_on_row\"])\n" + ] + }, + { + "cell_type": "markdown", + "id": "eeecb6c8-317e-49a2-a34b-d750db0b94f8", + "metadata": {}, + "source": [ + "## Developer" + ] + }, + { + "cell_type": "markdown", + "id": "8e72f695-9284-44e6-be1a-45bf0ad02747", + "metadata": {}, + "source": [ + "The developer receives a config object. Config inherits from a dict, so the developer can access parameters values directly. He can initialize the model and run it.\n", + "For example, to access the value of a parameter:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "d6dd0b34-e9f1-4a27-9a62-d4110f202a76", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2012-08-01 00:00:00\n" + ] + } + ], + "source": [ + "value = config[\"simulation\"][\"sdate\"]\n", + "print(value)" + ] + }, + { + "cell_type": "markdown", + "id": "787d6ed0-1bb2-40a8-b1ed-ec1f3149994e", + "metadata": {}, + "source": [ + "The developer can also access a specific section." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "29f4704b-7c30-44f6-836c-ecb7d13f9af7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'sdate': '2012-08-01 00:00:00', 'edate': '2012-08-04 23:00:00', 'latitude': 43.61, 'longitude': 3.87, 'elevation': 44.0, 'tzone': 'Europe/Paris', 'output_index': '', 'unit_scene_length': 'cm', 'hydraulic_structure': True, 'negligible_shoot_resistance': False, 'energy_budget': True}\n" + ] + } + ], + "source": [ + "section = config[\"simulation\"]\n", + "print(section)" + ] + }, + { + "cell_type": "markdown", + "id": "945f3dec-e769-4d26-8e71-5772a710ac54", + "metadata": {}, + "source": [ + "The developer can use the config API, the developer can get all sections of the configuration which returns a list of section names. He can also access all parameters of a specific section. Each parameter has a default value and and the developer can check whether its current value has been modified or not." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "196c68b8-d369-44d0-a591-8398fa6912b9", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['simulation', 'planting']\n", + "['sdate', 'edate', 'latitude', 'longitude', 'elevation', 'tzone', 'output_index', 'unit_scene_length', 'hydraulic_structure', 'negligible_shoot_resistance', 'energy_budget']\n" + ] + } + ], + "source": [ + "sections = config.get_sections()\n", + "print(sections)\n", + "\n", + "parameters = config.get_parameters('simulation')\n", + "print(parameters)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/openalea/core/config.py b/src/openalea/core/config.py new file mode 100644 index 0000000..d0e5686 --- /dev/null +++ b/src/openalea/core/config.py @@ -0,0 +1,293 @@ +# -*- python -*- +# +# OpenAlea.Core +# +# Copyright 2006-2026 CIRAD - INRAE - inria +# +# File author(s): Leticia Napolitano +# Christophe Pradal +# Fabrice Bauget +# +# Distributed under the Cecill-C License. +# See accompanying file LICENSE.txt or copy at +# http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html +# +# OpenAlea WebSite : http://openalea.gforge.inria.fr +# +############################################################################## +"""Configuration of OpenAlea models""" + +__license__ = "Cecill-C" + +import yaml +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap +import json +from openalea.core.singleton import Singleton +from openalea.core.observer import Observed +from pathlib import Path +from dataclasses import dataclass, asdict +import io +from ruamel.yaml import YAML + +def _load_json(filename: str): + """ + Load a configuration from a JSON file. + :param filename: the JSON configuration file. + """ + + file = Path(filename) + with file.open() as f: + data = json.load(f) + return data + +def _load_yml(filename: str): + """ + Load a configuration from a YAML file. + :param filename: the YAML configuration file. + """ + + file = Path(filename) + with file.open() as f: + data = yaml.load(f, Loader=yaml.SafeLoader) + return data + +def _dump_yml(data, filename: str, sort_keys=False): + """ + Dump configuration to a YAML file. + :param data: configuration data to write. + :param filename: YAML file path. + :param sort_keys: sort keys. + """ + + file = Path(filename) + with file.open("w") as f: + yaml.dump(data, f, sort_keys=sort_keys) + +def _dump_json(data, filename: str): + """ + Dump configuration to a JSON file. + :param data: configuration data to write. + :param filename: JSON file path. + """ + + file = Path(filename) + with file.open("w") as f: + json.dump(data, f, indent=4) + +def to_commented_map(obj): + """ + Convert a Python dictionary into a ruamel.yaml CommentedMap. + :param obj: dictionary. + """ + + if isinstance(obj, dict): + cm = CommentedMap() + for k, v in obj.items(): + cm[k] = to_commented_map(v) + return cm + else: + return obj + +def add_unit_comment(cm, model_unit_configs): + """ + Add comments above each ModelUnit section. + The comments are generated from optional ModelUnit fields: description, unit, param_type, uid, and uri. + :param cm: CommentedMap. + :param model_unit_configs: List of ModelUnit objects. + """ + + fields = ["description", "unit", "param_type", "uid", "uri"] + + for unit in model_unit_configs: + comments = [] + + for field in fields: + value = getattr(unit, field, None) + if value is not None: + comments.append(f"{field}: {value}") + + if comments: + cm.yaml_set_comment_before_after_key( + unit.name, + before="\n".join(comments) + ) + + +def add_comment2(cm, model_unit_configs): + """ + Add comments to each parameter. Comments are generated from optional Parameter fields: + description, unit, param_type, uid, and uri. + If the value is None, this function doesn't add a new comment. If the value is not None, we add the comment. + :param cm: CommentedMap. + :param model_unit_configs: List of ModelUnit objects. + """ + + param_index = {} + for unit in model_unit_configs: + param_index[unit.name] = {} + for p in unit.parameters: + param_index[unit.name][p.name] = p + + for section_name, section in cm.items(): + for param_name in section: + param = param_index.get(section_name, {}).get(param_name) + fields = ["description", "unit", "param_type", "uid", "uri"] + comments = [] + if param: + for field in fields: + value = getattr(param, field) + if value is not None: + comments.append(f"{field}: {value}") + if comments: + section.yaml_set_comment_before_after_key( + param_name, + before="\n".join(comments) + ) + + +@dataclass +class Parameter: + """ + Represent a configuration parameter. + The dataclass Parameter has two mandatory parameters name and value. The others parameters are optional to the configuration, if + we add them this would be commented. + The parameter can be converted to dictionary. + """ + name: str + value: any + description : any = None + unit : any = None + param_type : any = None + uid : any = None + uri : any = None + + def __to_dict__(self): + return {self.name: self.value} + + def __str__(self): + return ( + f"name={self.name}, value={self.value}" + ) + + +class ModelUnit(dict): + """ + Represent a configuration sectiion with a name a list of parameters. + :param name: Name of the section. + :param parameters: List of Parameter objects. + :param description: Optional description of the section. + """ + def __init__(self, name, parameters: list, description=None, unit=None, param_type=None, uid=None, uri=None): + super().__init__({name: {p.name: p.value for p in parameters}}) + self.name = name + self.parameters = parameters + self.description = description + self.uid = uid + self.uri = uri + + def __str__(self): + return f"name={self.name}, parameters={dict(self)}" + + +class Config(dict): + """ + Configuration of OpenAlea models. + A Config object is a dictionary that contains a list of ModelUnit. A config can load, dump, add new sections, new sections, + the quantity of units. + :param model_unit_configs: List of ModelUnit objects. + """ + + def __init__(self, model_unit_configs: list): + """ + Initialize the configuration with a list of unit configurations. + """ + + super().__init__() + self.model_unit_configs = model_unit_configs + self.params_comments = {} + self.section_comments = {} + + for unit in model_unit_configs: + self.update(unit) + + def __len__(self): + return len(self.model_unit_configs) + + + def __str__(self): + return f"{self.model_unit_configs}" + + def add_section(self, unit): + self.model_unit_configs.append(unit) + self.update(unit) + + + def load(self, filename: str): + """ + Load a configuration file (YAML or JSON) and return a new Config. + :param filename: configuration file. + """ + + extension = filename.split(".")[-1] + + if extension in ("yml", "yaml"): + yaml = YAML() + with open(filename, "r") as f: + data = yaml.load(f) + + elif extension == "json": + data = _load_json(filename) + + units = [] + + for unit_name, params in data.items(): + parameters = [] + for param_name, param_value in params.items(): + p = Parameter(name=param_name, value=param_value) + parameters.append(p) + + units.append(ModelUnit(unit_name, parameters)) + + return Config(units) + + + def dump(self, filename: str): + """ + Dump the configuration to a YAML or JSON file. + :param filename: filename path. + """ + + extension = filename.split(".")[-1] + + if extension in ("yml", "yaml"): + yaml = YAML() + cm = to_commented_map(self) + + add_comment2(cm, self.model_unit_configs) + add_unit_comment(cm, self.model_unit_configs) + + with open(filename, "w") as f: + yaml.dump(cm, f) + + elif extension == "json": + _dump_json(dict(self), filename) + + def get_sections(self): + """ + Return the list of section names in the configuration. + """ + + return list(self.keys()) + + def get_parameters(self, section_name): + """ + Return the list of parameter names of a section. + :param section_name: Name of the section. + """ + + if section_name not in self: + raise KeyError(f"Section '{section_name}' doesn't exist.") + return list(self[section_name].keys()) + + diff --git a/test/params.json b/test/params.json new file mode 100644 index 0000000..9d76a63 --- /dev/null +++ b/test/params.json @@ -0,0 +1,40 @@ +{ + "simulation": { + "sdate": "2012-08-01 00:00:00", + "edate": "2012-08-04 23:00:00", + "latitude": 43.61, + "longitude": 3.87, + "elevation": 44.0, + "tzone": "Europe/Paris", + "output_index": "", + "unit_scene_length": "cm", + "hydraulic_structure": true, + "negligible_shoot_resistance": false, + "energy_budget": true + }, + "planting": { + "spacing_between_rows": 3.6, + "spacing_on_row": 1, + "row_angle_with_south": 140.0 + }, + "phenology": { + "emdate": "2012-04-01 00:00:00", + "t_base": 10.0 + }, + "mtg_api": { + "collar_label": "inT", + "leaf_lbl_prefix": "L", + "stem_lbl_prefix": [ + "in", + "Pet", + "cx" + ] + }, + "numerical_resolution": { + "max_iter": 100, + "psi_step": 1.0, + "psi_error_threshold": 0.05, + "t_step": 1.0, + "t_error_threshold": 0.02 + } +} \ No newline at end of file diff --git a/test/params.yml b/test/params.yml new file mode 100644 index 0000000..adf5415 --- /dev/null +++ b/test/params.yml @@ -0,0 +1,91 @@ +# description: Simulations parameters +simulation: +# description: Start date of the simulation +# param_type: datetime + sdate: '2012-08-01 00:00:00' +# description: End date of the simulation +# param_type: datetime + edate: '2012-08-04 23:00:00' +# unit: degrees +# param_type: float + latitude: 43.61 +# description: Longitude of the simulation +# unit: degrees +# param_type: float + longitude: 3.87 +# description: Elevation of the simulation +# unit: meters +# param_type: float + elevation: 44.0 +# description: Time zone +# param_type: string + tzone: Europe/Paris +# description: Output index +# param_type: string + output_index: '' +# description: Unit of scene length +# param_type: string + unit_scene_length: cm +# description: Hydraulic structure +# param_type: boolean + hydraulic_structure: true +# description: Negligible short resistance +# param_type: boolean + negligible_shoot_resistance: false +# description: Energy budget +# param_type: boolean + energy_budget: true +# description: Planting section +planting: +# description: Distance between planting rows +# unit: meters +# param_type: float + spacing_between_rows: 3.6 +# description: Spacing on the row +# unit: meters +# param_type: float + spacing_on_row: 1 +# description: Row angle with south +# unit: degrees +# param_type: float + row_angle_with_south: 140.0 +# description: Phenology section +phenology: +# description: Emergence date +# param_type: datetime + emdate: '2012-04-01 00:00:00' +# description: Base temperature +# unit: degrees +# param_type: float + t_base: 10.0 +# description: Mtg api section +mtg_api: +# description: Collar label +# param_type: string + collar_label: inT +# description: Leaf label prefix +# param_type: string + leaf_lbl_prefix: L +# description: Stem label prefix +# param_type: list + stem_lbl_prefix: + - in + - Pet + - cx +# description: Numerical section +numerical_resolution: +# description: Maximum number of iterations +# param_type: integer + max_iter: 100 +# description: Psi step +# param_type: float + psi_step: 1.0 +# description: Psi error threshold +# unit: seconds + psi_error_threshold: 0.05 +# description: Time step +# param_type: float + t_step: 1.0 +# description: Time error threshold +# unit: float + t_error_threshold: 0.02 diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..e089049 --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,107 @@ +from openalea.core.config import Parameter, ModelUnit, Config +import yaml +import json + +def hydroshoot_simulation_config(): + + p_sdate = Parameter("sdate", "2012-08-01 00:00:00", "Start date of the simulation", None, "datetime") + p_edate = Parameter("edate", "2012-08-04 23:00:00", "End date of the simulation", None, "datetime") + p_lat = Parameter("latitude", 43.61, None, "degrees", "float") + p_longitude = Parameter("longitude", 3.87, "Longitude of the simulation", "degrees", "float") + p_elevation = Parameter("elevation", 44.0, "Elevation of the simulation", "meters", "float") + p_tzone = Parameter("tzone", "Europe/Paris", "Time zone", None, "string") + p_output_index = Parameter("output_index", "", "Output index", None, "string") + p_unit_scene_length = Parameter("unit_scene_length", "cm", "Unit of scene length", None, "string") + p_hydraulic_structure = Parameter("hydraulic_structure", True, "Hydraulic structure", None, "boolean") + p_negligible_shoot_resistance = Parameter("negligible_shoot_resistance", False, "Negligible short resistance", None, "boolean") + p_energy_budget = Parameter("energy_budget", True, "Energy budget", None, "boolean") + parameters = [p_sdate, p_edate, p_lat, p_longitude, p_elevation, p_tzone, p_output_index, p_unit_scene_length, p_hydraulic_structure, p_negligible_shoot_resistance, p_energy_budget] + simulation = ModelUnit("simulation", parameters, "Simulations parameters") + + print(simulation) + + return simulation + +def hydroshoot_planting_config(): + p_spacing_between_rows = Parameter("spacing_between_rows", 3.6, "Distance between planting rows", "meters", "float") + p_spacing_on_row = Parameter("spacing_on_row", 1, "Spacing on the row", "meters", "float") + p_row_angle_with_south = Parameter("row_angle_with_south", 140.0, "Row angle with south", "degrees", "float") + + parameters = [p_spacing_between_rows, p_spacing_on_row, p_row_angle_with_south] + planting = ModelUnit('planting', parameters, "Planting section") + + return planting + +def hydroshoot_phenology_config(): + p_emdate = Parameter("emdate", "2012-04-01 00:00:00", "Emergence date", None, "datetime") + p_t_base = Parameter("t_base", 10.0, "Base temperature", "degrees", "float") + + parameters = [p_emdate, p_t_base] + phenology = ModelUnit('phenology', parameters, "Phenology section") + + return phenology + +def hydroshoot_mtg_api_config(): + p_collar_label = Parameter("collar_label", "inT", "Collar label", None, "string") + p_leaf_lbl_prefix = Parameter("leaf_lbl_prefix", "L", "Leaf label prefix", None, "string") + p_stem_lbl_prefix = Parameter("stem_lbl_prefix", ["in", "Pet", "cx"], "Stem label prefix", None, "list") + + parameters = [p_collar_label, p_leaf_lbl_prefix, p_stem_lbl_prefix] + mtg_api = ModelUnit("mtg_api", parameters, "Mtg api section") + + return mtg_api + +def hydroshoot_numerical_resolution_config(): + p_max_iter = Parameter("max_iter", 100, "Maximum number of iterations", None, "integer") + p_psi_step = Parameter("psi_step", 1.0, "Psi step", None, "float") + p_psi_error_threshold = Parameter("psi_error_threshold", 0.05, "Psi error threshold", "seconds") + p_t_step = Parameter("t_step", 1.0, "Time step", None, "float") + p_t_error_threshold = Parameter("t_error_threshold", 0.02, "Time error threshold", "float") + + parameters = [p_max_iter, p_psi_step, p_psi_error_threshold, p_t_step, p_t_error_threshold] + numerical_resolution = ModelUnit("numerical_resolution", parameters, "Numerical section") + + return numerical_resolution + + +def test_config_hs(): + unit = hydroshoot_simulation_config() + config = Config([unit]) + config.dump("params4.yml") + + assert(len(config) == 1) + assert(len(config['simulation'])==11) + config.section_comments["simulation"] = "Simulation" + planting = hydroshoot_planting_config() + config.add_section(planting) + config.dump("params.yml") + + assert len(config) == 2 + assert len(config["planting"]) == 3 + + phenology = hydroshoot_phenology_config() + config.add_section(phenology) + mtg_api = hydroshoot_mtg_api_config() + config.add_section(mtg_api) + config.dump("params.yml") + + numerical_resolution = hydroshoot_numerical_resolution_config() + config.add_section(numerical_resolution) + config.dump("params.yml") + config.dump("params.json") + + config.load("params.yml") + + assert len(config) == 5 + assert len(config["planting"]) == 3 + value = config["simulation"]["sdate"] + print('value ' + value) + section = config["simulation"] + print('section') + print(section) + + +test_config_hs() + + + diff --git a/test/toto.json b/test/toto.json new file mode 100644 index 0000000..6b3b979 --- /dev/null +++ b/test/toto.json @@ -0,0 +1,10 @@ +{ + "unit1": { + "p1": 1, + "p2": "2" + }, + "unit2": { + "p1": 3, + "p2": "4" + } +}