Skip to content

Commit b4b34a8

Browse files
authored
Merge pull request #22 from dots-energy/update-template-to-use-code-gen
Update template to use code gen
2 parents a8044a3 + f26788d commit b4b34a8

File tree

12 files changed

+358
-236
lines changed

12 files changed

+358
-236
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ jobs:
3434
- name: Test with unittest
3535
run: |
3636
cd test
37-
python -m unittest discover -s ./ -p 'Test*.py'
37+
python -m unittest discover -s ./ -p 'test*.py'

Dockerfile

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
FROM python:3.9.0
2-
# If needed you can use the official python image (larger memory size)
3-
#FROM python:3.9.0
1+
FROM python:3.13
42

53
RUN mkdir /app/
64
WORKDIR /app
75

86
COPY src/<<INSERT_FOLDER_NAME>> src/<<INSERT_FOLDER_NAME>>
9-
COPY requirements.txt ./
107
COPY pyproject.toml ./
118
COPY README.md ./
12-
RUN pip install -r requirements.txt
139
RUN pip install ./
1410

15-
ENTRYPOINT python3 src/<<INSERT_FOLDER_NAME>>/<<INSERT_MAIN_PYTHON_FILENAME>>.py
11+
ENTRYPOINT python3 src/<<INSERT_FOLDER_NAME>>/<<INSERT_IMPLEMENTATION_PYTHON_FILENAME>>.py

README.md

Lines changed: 2 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,4 @@
11
# Dots Calculation service template
2-
A template repository for DOTs-helics calculation services.
3-
To create a new calculation service follow the following steps (For a more detailed explenation see below):
4-
1. Create a new github repository based on this template.
5-
2. Edit the `EConnection.py` based upon your needs i.e. define the correct calculations for the calculation service.
6-
3. Edit the `TestTemplate.py` to test your calculations indepedently in a python unit test.
7-
4. Replace the placeholders in the `Dockerfile`. The foldername should match the one that is in the src folder.
2+
A template repository for DOTs-helics calculation services.
83

9-
## Creating a calculation in the calculation service
10-
The initial `EConnection.py` defines two calculations inside the `EConnection` calculation service. These calculations are called `EConnectionDispatch` and `EConnectionSchedule` respectively. Let's take a look at the definition of the first calculation:
11-
12-
```python
13-
class CalculationServiceEConnection(HelicsSimulationExecutor):
14-
15-
def __init__(self):
16-
super().__init__()
17-
18-
subscriptions_values = [
19-
SubscriptionDescription(esdl_type="PVInstallation",
20-
input_name="PV_Dispatch",
21-
input_unit="W",
22-
input_type=h.HelicsDataType.DOUBLE)
23-
]
24-
25-
publication_values = [
26-
PublicationDescription(global_flag=True,
27-
esdl_type="EConnection",
28-
output_name="EConnectionDispatch",
29-
output_unit="W",
30-
data_type=h.HelicsDataType.DOUBLE)
31-
]
32-
33-
e_connection_period_in_seconds = 60
34-
35-
calculation_information = HelicsCalculationInformation(
36-
time_period_in_seconds=e_connection_period_in_seconds,
37-
offset=0,
38-
uninterruptible=False,
39-
wait_for_current_time_update=False,
40-
terminate_on_error=True,
41-
calculation_name="EConnectionDispatch",
42-
inputs=subscriptions_values,
43-
outputs=publication_values,
44-
calculation_function=self.e_connection_dispatch
45-
)
46-
self.add_calculation(calculation_information)
47-
48-
publication_values = [
49-
PublicationDescription(True, "EConnection", "Schedule", "W", h.HelicsDataType.VECTOR)
50-
]
51-
52-
e_connection_period_in_seconds = 21600
53-
54-
calculation_information_schedule = HelicsCalculationInformation(e_connection_period_in_seconds, 0, False, False, True, "EConnectionSchedule", [], publication_values, self.e_connection_da_schedule)
55-
self.add_calculation(calculation_information_schedule)
56-
57-
def init_calculation_service(self, energy_system: esdl.EnergySystem):
58-
LOGGER.info("init calculation service")
59-
for esdl_id in self.simulator_configuration.esdl_ids:
60-
LOGGER.info(f"Example of iterating over esdl ids: {esdl_id}")
61-
62-
def e_connection_dispatch(self, param_dict : dict, simulation_time : datetime, time_step_number : TimeStepInformation, esdl_id : EsdlId, energy_system : EnergySystem):
63-
ret_val = {}
64-
single_dispatch_value = get_single_param_with_name(param_dict, "PV_Dispatch") # returns the first value in param dict with "PV_Dispatch" in the key name
65-
all_dispatch_values = get_vector_param_with_name(param_dict, "PV_Dispatch") # returns all the values as a list in param_dict with "PV_Dispatch" in the key name
66-
ret_val["EConnectionDispatch"] = sum(single_dispatch_value)
67-
self.influx_connector.set_time_step_data_point(esdl_id, "EConnectionDispatch", simulation_time, ret_val["EConnectionDispatch"])
68-
return ret_val
69-
```
70-
71-
First, you can see a list called `subscriptions_values`. This list defines the inputs of the calculation. In this case it hase one input that is supposed to come from the ESDL type called `PVInstallation` as specified by the `esdl_type` parameter. Next, the `input_name` parameter describes the name of the value that the `PVInstallation` produces. Finally, the `input_unit` and `input_type` describe the input's unit and type respectively.
72-
73-
The second list describes the publications or outputs of the calculation. There are two parameters of a publication description that are worth mentioning: `global_flag` this needs to be set to `True` and will be removed in later versions of this template, and, `esdl_type` this is the esdl type of the calculation service that you are defining in this case `EConnection`.
74-
75-
After the definition of the subscription and publication values the actual calculation is defined by instantiating `HelicsCalculationInformation`. The first 5 parameters are related to a helics federate confguration and therefore the reader is reffered to the [helics documentation](https://docs.helics.org/en/latest/references/configuration_options_reference.html#broker_init_string--null).
76-
77-
Finally, the the last line adds the calculation to the calculation service and ensures that it is executed during a running co-simulation. Every added calculation will become a [helics federate](https://docs.helics.org/en/latest/user-guide/fundamental_topics/helics_terminology.html) with their own timing parameters as defined in the `calculation_information`. To get an idea of how helics timing works have a look at this [page](https://docs.helics.org/en/latest/user-guide/fundamental_topics/timing_configuration.html) of the helics documentation.
78-
79-
When the simulation starts there will be an initialization stage and a calculating stage. In the initialization phase a calculation service can initialize variables that are required in the calculation stage. This can be done by adjusting the `init_calculation_service` function. The esdl that is associated with the simulated scenario is given as a parameter to this function.
80-
81-
In the calculation phase the calculation functions are called periodically for each simulated esdl entity. The `esdl_id` of the simulated entity is passed as a parameter to the calculation function. New inputs can be read and new outputs can be generated. An example of getting inputs, returning outputs and writing to the influx db can be found in the example above.
82-
83-
## Getting inputs from a calculation service
84-
The input parameters provided by other calculation services are provided by the `param_dict` parameter in the calculation. Wheneve the calculation function `e_connection_dispatch` is called, the param dict for the calculation `e_connection_dispatch` could look like:
85-
86-
```
87-
param_dict = {
88-
"PVInstallation/PV_Dispatch/1f60ceb9-9708-4d89-b079-482abc1408ea" : 5,
89-
"PVInstallation/PV_Dispatch/468f4332-4306-4b74-a5c2-eb8a7aa0a8d9" : 3,
90-
}
91-
```
92-
93-
This would mean that the associated esdl entity is connected to two `PVInstallation` entities with id `1f60ceb9-9708-4d89-b079-482abc1408ea` and `468f4332-4306-4b74-a5c2-eb8a7aa0a8d9` respectively. There are two ways to retrieve the values from the dictionary. First, by the python way of retrieving values from a dictiononary i.e. `param_dict[key]` this would require know the keys of dictionary.
94-
The second option is to use the helper functions in `dots_infrastructure.CalculationServiceHelperFunctions` (as shown in the above example). The function `get_single_param_with_name` will get the first value in `param_dict` with a specific input name. In the above example the input called `PV_Dispatch` is fetched and thus the function will return the vaule `5`. The other function to help retrieve values is called `get_vector_param_with_name` and will return all the values with a specific `input_name` as list. In this example it wil return the list `[5, 3]`.
95-
96-
## Testing a calculation service
97-
98-
1. Create a new python virtual environment
99-
2. Install dependencies `pip install -r requirements.txt`
100-
3. Install package `pip install -e .`
101-
4. Run `cd test`
102-
5. Run `python -m unittest discover -s ./ -p 'Test*.py'`
103-
104-
## Building a docker image such that it can be used
105-
106-
1. Adjust `<<ImageName>>` to the name of the calculation service's image in the file `.github/workflows/publish-image.yml`
107-
2. Push your changes to a new branch
108-
3. Create a pull request
109-
4. A github action will now run building the calculation service as a docker image and pushing it to the registry, as long as the pull request is not merged in the main branch the version number will be `test`
110-
5. When finished complete the pull request and a new docker image will be built and pushed with version number `latest`
111-
6. Change the visibility of the package to public, follow the steps detail [here](https://docs.github.com/en/enterprise-server@3.12/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility#configuring-visibility-of-packages-for-an-organization).
4+
Find the instructions on how to use this template in `template-instructions.md`.

code_gen.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
from typing import List
3+
from dots_infrastructure.code_gen.code_gen import CodeGenerator
4+
import json
5+
import os
6+
from dataclasses import dataclass
7+
8+
@dataclass
9+
class FindReplace:
10+
find : str
11+
replace : str
12+
13+
def replace_string_in_file(file_path, find_replace : List[FindReplace]):
14+
if os.path.exists(file_path):
15+
with open(file_path, 'r') as file:
16+
filedata = file.read()
17+
18+
for item in find_replace:
19+
filedata = filedata.replace(item.find, item.replace)
20+
21+
with open(file_path, 'w') as file:
22+
file.write(filedata)
23+
24+
code_generator = CodeGenerator()
25+
with open("input.json", "r") as input_file:
26+
input_data = input_file.read()
27+
28+
parsed_input_data = json.loads(input_data)
29+
cs_name = code_generator.camel_case(parsed_input_data["name"])
30+
cs_python_name = code_generator.get_python_name(cs_name)
31+
cs_python_base_class_name = code_generator.get_base_class_name(cs_name)
32+
33+
if os.path.exists("src/ExampleCalculationService"):
34+
os.rename("src/ExampleCalculationService", f"src/{cs_name}")
35+
36+
if os.path.exists(f"src/{cs_name}/calculation_service_test.py"):
37+
os.rename(f"src/{cs_name}/calculation_service_test.py", f"src/{cs_name}/{cs_python_name}.py")
38+
39+
if os.path.exists("test/test_template.py"):
40+
os.rename("test/test_template.py", f"test/test_{cs_python_name}.py")
41+
42+
code_generator.code_gen(input=input_data, code_output_dir=f"src/{cs_name}", documentation_ouput_dir="docs")
43+
44+
replace_string_in_file('pyproject.toml', [FindReplace('ExampleCalculationService', cs_name)])
45+
replace_string_in_file(f'src/{cs_name}/{cs_python_name}.py', [FindReplace('CalculationServiceTest', cs_name), FindReplace('CalculationServiceTestBase', f'{cs_name}Base')])
46+
replace_string_in_file(f'test/test_{cs_python_name}.py', [FindReplace('CalculationServiceTest', cs_name), FindReplace('calculation_service_test', cs_python_name), FindReplace('ExampleCalculationService', cs_name)] )
47+
replace_string_in_file('Dockerfile', [FindReplace('<<INSERT_FOLDER_NAME>>', cs_name), FindReplace('<<INSERT_IMPLEMENTATION_PYTHON_FILENAME>>', cs_python_name)] )

input.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "test",
3+
"esdl_type" : "EConnection",
4+
"description" : "this is a test description",
5+
"relevant_links" : [
6+
{
7+
"name" : "test link",
8+
"url" : "https://example.com/test",
9+
"description" : "this is a test link"
10+
},
11+
{
12+
"name" : "another test link",
13+
"url" : "https://example.com/anothertest",
14+
"description" : "this is another link"
15+
}
16+
],
17+
"calculations" : [
18+
{
19+
"name": "test calculation",
20+
"description" : "test",
21+
"time_period_in_seconds" : 900,
22+
"offset_in_seconds" : 0,
23+
"inputs" : [
24+
{
25+
"name" : "input1",
26+
"esdl_type" : "PVInstallation",
27+
"data_type" : "DOUBLE",
28+
"description" : "input 1 description",
29+
"unit" : "K"
30+
}
31+
],
32+
"outputs" : [
33+
{
34+
"name" : "output1",
35+
"data_type" : "DOUBLE",
36+
"description" : "output 1 description",
37+
"unit" : "W"
38+
},
39+
{
40+
"name" : "output2",
41+
"data_type" : "DOUBLE",
42+
"description" : "output 2 description",
43+
"unit" : "W"
44+
}
45+
]
46+
},
47+
{
48+
"name": "test calculation 2",
49+
"description" : "test",
50+
"time_period_in_seconds" : 900,
51+
"offset_in_seconds" : 100,
52+
"inputs" : [
53+
],
54+
"outputs" : [
55+
{
56+
"name" : "output3",
57+
"data_type" : "DOUBLE",
58+
"description" : "output 3 description",
59+
"unit" : "W"
60+
}
61+
]
62+
}
63+
]
64+
}

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ classifiers = [
1616
"License :: OSI Approved :: MIT License",
1717
"Operating System :: OS Independent",
1818
]
19+
dependencies = [
20+
'helics==3.6.1',
21+
'dots_infrastructure==0.4.9'
22+
]
1923

2024
[project.urls]
2125
Homepage = "https://github.com/EES-TUe/Dots-calculation-service-template"

requirements.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/ExampleCalculationService/EConnection.py

Lines changed: 0 additions & 76 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from datetime import datetime
2+
from esdl import esdl
3+
import helics as h
4+
from dots_infrastructure.DataClasses import EsdlId, TimeStepInformation
5+
from dots_infrastructure.CalculationServiceHelperFunctions import get_single_param_with_name, get_vector_param_with_name
6+
from dots_infrastructure.Logger import LOGGER
7+
from esdl import EnergySystem
8+
9+
class CalculationServiceTest(CalculationServiceTestBase):
10+
11+
def init_calculation_service(self, energy_system: esdl.EnergySystem):
12+
LOGGER.info("init calculation service")
13+
for esdl_id in self.simulator_configuration.esdl_ids:
14+
LOGGER.info(f"Example of iterating over esdl ids: {esdl_id}")
15+
16+
def test_calculation(self, param_dict : dict, simulation_time : datetime, time_step_number : TimeStepInformation, esdl_id : EsdlId, energy_system : EnergySystem):
17+
ret_val = {}
18+
single_input1_value = get_single_param_with_name(param_dict, "input1") # returns the first value in param dict with "PV_Dispatch" in the key name
19+
all_input1_values = get_vector_param_with_name(param_dict, "input1") # returns all the values as a list in param_dict with "PV_Dispatch" in the key name
20+
ret_val["output1"] = single_input1_value
21+
ret_val["output2"] = sum(all_input1_values)
22+
self.influx_connector.set_time_step_data_point(esdl_id, "EConnectionDispatch", simulation_time, ret_val["EConnectionDispatch"])
23+
return ret_val
24+
25+
def test_calculation_2(self, param_dict : dict, simulation_time : datetime, time_step_number : TimeStepInformation, esdl_id : EsdlId, energy_system : EnergySystem):
26+
ret_val = {}
27+
ret_val["output3"] = 3.0
28+
return ret_val
29+
30+
if __name__ == "__main__":
31+
32+
helics_simulation_executor = CalculationServiceTest()
33+
helics_simulation_executor.start_simulation()
34+
helics_simulation_executor.stop_simulation()

0 commit comments

Comments
 (0)