Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
SimStadt
simstadtpy
Commits
ea249156
Commit
ea249156
authored
Mar 12, 2026
by
Eric Duminil
Browse files
Test simulations too
parent
304acfb6
Changes
6
Show whitespace changes
Inline
Side-by-side
.gitignore
View file @
ea249156
...
@@ -11,3 +11,5 @@ wheels/
...
@@ -11,3 +11,5 @@ wheels/
# Legacy reference code (not part of the library)
# Legacy reference code (not part of the library)
old/
old/
.cache
src/simstadtpy/runner.py
View file @
ea249156
...
@@ -221,6 +221,7 @@ def run_workflow_with_citygml(
...
@@ -221,6 +221,7 @@ def run_workflow_with_citygml(
replaces
:
dict
|
None
=
None
,
replaces
:
dict
|
None
=
None
,
description
:
str
|
None
=
None
,
description
:
str
|
None
=
None
,
destination
:
str
|
None
=
None
,
destination
:
str
|
None
=
None
,
project_path
:
Path
|
None
=
None
,
)
->
SimStadtResults
:
)
->
SimStadtResults
:
"""Copy a workflow template, inject parameters, run SimStadt, and return parsed results.
"""Copy a workflow template, inject parameters, run SimStadt, and return parsed results.
...
@@ -230,6 +231,9 @@ def run_workflow_with_citygml(
...
@@ -230,6 +231,9 @@ def run_workflow_with_citygml(
replaces: Optional dict of XML-fragment regex replacements for params.xml.
replaces: Optional dict of XML-fragment regex replacements for params.xml.
description: Human-readable label for the result object.
description: Human-readable label for the result object.
destination: Output workflow folder name; defaults to a timestamped random id.
destination: Output workflow folder name; defaults to a timestamped random id.
project_path: Directory where the workflow folder is created. Defaults to
citygml_path.parent. When set to a different directory, citygml_path is
passed as an absolute path so SimStadt can locate it.
"""
"""
if
isinstance
(
template
,
str
):
if
isinstance
(
template
,
str
):
template_path
=
get_template_path
()
/
template
template_path
=
get_template_path
()
/
template
...
@@ -240,9 +244,21 @@ def run_workflow_with_citygml(
...
@@ -240,9 +244,21 @@ def run_workflow_with_citygml(
if
destination
is
None
:
if
destination
is
None
:
destination
=
random_id
()
+
"_"
+
template_name
.
rsplit
(
"_"
,
1
)[
-
1
]
destination
=
random_id
()
+
"_"
+
template_name
.
rsplit
(
"_"
,
1
)[
-
1
]
if
project_path
is
None
:
project_path
=
citygml_path
.
parent
project_path
=
citygml_path
.
parent
citygml_ref
=
citygml_path
.
name
else
:
repo_path
=
project_path
.
parent
try
:
citygml_ref
=
str
(
citygml_path
.
relative_to
(
repo_path
))
except
ValueError
as
e
:
raise
ValueError
(
f
"CityGML file
{
citygml_path
}
is not under the repository root
{
repo_path
}
. "
"Place the GML file inside the repository directory or omit project_path."
)
from
e
workflow_path
=
copy_workflow_from_template
(
template_path
,
project_path
,
destination
,
replaces
)
workflow_path
=
copy_workflow_from_template
(
template_path
,
project_path
,
destination
,
replaces
)
output_files
=
run_workflow
(
workflow_path
,
[
citygml_
path
.
name
])
output_files
=
run_workflow
(
workflow_path
,
[
citygml_
ref
])
if
description
is
None
:
if
description
is
None
:
description
=
f
"
{
template_name
}
for
{
citygml_path
.
name
}
"
description
=
f
"
{
template_name
}
for
{
citygml_path
.
name
}
"
...
...
tests/conftest.py
View file @
ea249156
import
os
from
pathlib
import
Path
from
pathlib
import
Path
import
pytest
import
pytest
...
@@ -31,10 +30,3 @@ def simstadt_folder():
...
@@ -31,10 +30,3 @@ def simstadt_folder():
pytest
.
skip
(
str
(
e
))
pytest
.
skip
(
str
(
e
))
@
pytest
.
fixture
(
scope
=
"session"
)
def
live_project_path
(
simstadt_folder
):
"""Skip the test if SIMSTADTPY_TEST_PROJECT is not set."""
project
=
os
.
environ
.
get
(
"SIMSTADTPY_TEST_PROJECT"
)
if
not
project
:
pytest
.
skip
(
"SIMSTADTPY_TEST_PROJECT env var not set"
)
return
Path
(
project
)
tests/data/.gitignore
View file @
ea249156
*.png
*.png
*.proj
tests/data/
GMLs
/MiniBuchwald.gml
→
tests/data/
TestRepo
/MiniBuchwald.gml
View file @
ea249156
File moved
tests/test_simulations.py
View file @
ea249156
"""Live integration tests for SimStadt workflows. Skipped if SimStadt is not installed
"""Live integration tests for SimStadt workflows. Skipped if SimStadt is not installed."""
or SIMSTADTPY_TEST_PROJECT is not set."""
from
pathlib
import
Path
import
pytest
import
pytest
from
simstadtpy.results
import
KPI
from
simstadtpy.results
import
KPI
from
simstadtpy.workflows
import
greenwater_simulation
,
heatdemand_simulation
,
photovoltaic_simulation
from
simstadtpy.runner
import
run_workflow_with_citygml
TEST_DATA
=
Path
(
__file__
).
parent
/
"data"
TEST_TEMPLATES
=
TEST_DATA
/
"Templates"
# SimStadt resolves GML paths relative to the repository root (project_path.parent).
# With project in TestRepo/Temp.proj, repo root = TestRepo/ → GML must live there.
TEST_GML
=
TEST_DATA
/
"TestRepo"
/
"MiniBuchwald.gml"
TEST_PROJECT
=
TEST_DATA
/
"TestRepo"
/
"Temp.proj"
def
to_dict
(
values
:
list
[
KPI
])
->
dict
[
str
,
KPI
]:
def
to_dict
(
values
:
list
[
KPI
])
->
dict
[
str
,
KPI
]:
...
@@ -12,162 +20,47 @@ def to_dict(values: list[KPI]) -> dict[str, KPI]:
...
@@ -12,162 +20,47 @@ def to_dict(values: list[KPI]) -> dict[str, KPI]:
@
pytest
.
mark
.
live
@
pytest
.
mark
.
live
def
test_pv
(
live_project_path
):
def
test_heat_demand
(
simstadt_folder
):
citygml
=
live_project_path
/
"Grombühl_mini.gml"
results
=
run_workflow_with_citygml
(
meteonorm
=
"Wuerzburg-hour.csv"
TEST_TEMPLATES
/
"01_HeatDemand"
,
min_roof_insolation
=
800
TEST_GML
,
project_path
=
TEST_PROJECT
,
########################
description
=
"Heat demand – MiniBuchwald"
,
# No shadow, no tree #
########################
results
=
photovoltaic_simulation
(
citygml
,
meteonorm_filename
=
meteonorm
,
with_shadows
=
False
,
min_roof_insolation
=
min_roof_insolation
,
)
pv_kpis
=
to_dict
(
results
.
kpis
)
assert
"PV potential yield"
in
pv_kpis
assert
"Roof area for PV"
in
pv_kpis
assert
"PV specific yield"
in
pv_kpis
assert
abs
(
pv_kpis
[
"PV specific yield"
].
value
-
1_100
)
<=
200
########################
# No shadow, more roofs #
########################
more_pv_kpis
=
to_dict
(
photovoltaic_simulation
(
citygml
,
meteonorm_filename
=
meteonorm
,
pv_share_flat
=
80
,
pv_share_tilted
=
80
,
with_shadows
=
False
,
min_roof_insolation
=
min_roof_insolation
,
).
kpis
)
assert
more_pv_kpis
[
"PV potential nominal power"
].
value
>
pv_kpis
[
"PV potential nominal power"
].
value
*
1.5
assert
more_pv_kpis
[
"PV potential yield"
].
value
>
pv_kpis
[
"PV potential yield"
].
value
*
1.5
assert
abs
(
more_pv_kpis
[
"Irradiance in module plane"
].
value
-
pv_kpis
[
"Irradiance in module plane"
].
value
)
<=
1
assert
abs
(
more_pv_kpis
[
"Roof area for PV"
].
value
-
pv_kpis
[
"Roof area for PV"
].
value
)
<=
100
########################
# Shadow, no tree #
########################
shadow_results
=
photovoltaic_simulation
(
citygml
,
meteonorm_filename
=
meteonorm
,
with_shadows
=
True
,
min_roof_insolation
=
min_roof_insolation
,
)
shadow_pv_kpis
=
to_dict
(
shadow_results
.
kpis
)
assert
pv_kpis
[
"PV potential nominal power"
].
value
==
pytest
.
approx
(
shadow_pv_kpis
[
"PV potential nominal power"
].
value
)
assert
shadow_pv_kpis
[
"PV potential yield"
].
value
<
pv_kpis
[
"PV potential yield"
].
value
assert
shadow_pv_kpis
[
"Irradiance in module plane"
].
value
<
pv_kpis
[
"Irradiance in module plane"
].
value
########################
# Shadow, trees #
########################
tree_results
=
photovoltaic_simulation
(
live_project_path
/
"Grombühl_mini_with_comically_large_trees.gml"
,
meteonorm_filename
=
meteonorm
,
with_shadows
=
True
,
min_roof_insolation
=
min_roof_insolation
,
)
tree_pv_kpis
=
to_dict
(
tree_results
.
kpis
)
assert
tree_pv_kpis
[
"PV potential nominal power"
].
value
<
shadow_pv_kpis
[
"PV potential nominal power"
].
value
*
0.8
assert
tree_pv_kpis
[
"PV potential yield"
].
value
<
shadow_pv_kpis
[
"PV potential yield"
].
value
*
0.8
assert
tree_pv_kpis
[
"Irradiance in module plane"
].
value
<
shadow_pv_kpis
[
"Irradiance in module plane"
].
value
@
pytest
.
mark
.
live
def
test_heat_demand
(
live_project_path
):
citygml
=
live_project_path
/
"Grombühl_mini.gml"
meteonorm
=
live_project_path
/
"Wuerzburg-hour.csv"
########################
# No shadow, no tree #
########################
results
=
heatdemand_simulation
(
citygml
,
meteonorm_filename
=
str
(
meteonorm
),
with_shadows
=
False
,
calculation_mode
=
"HEATING_AND_COOLING"
,
)
)
kpis
=
to_dict
(
results
.
kpis
)
kpis
=
to_dict
(
results
.
kpis
)
assert
"Heated area"
in
kpis
assert
"Heated area"
in
kpis
assert
"Yearly Heating demand"
in
kpis
assert
"Yearly Heating demand"
in
kpis
assert
"Yearly
Cool
ing demand"
in
kpis
assert
kpis
[
"Yearly
Heat
ing demand"
].
value
>
0
assert
kpis
[
"
Y
ea
r of construction
"
].
value
>
190
0
assert
kpis
[
"
H
ea
ted area
"
].
value
>
0
########################
# Shadow, no tree #
########################
shadow_results
=
heatdemand_simulation
(
citygml
,
meteonorm_filename
=
str
(
meteonorm
),
with_shadows
=
True
,
calculation_mode
=
"HEATING_AND_COOLING"
,
)
shadow_kpis
=
to_dict
(
shadow_results
.
kpis
)
assert
shadow_kpis
[
"Yearly Heating demand"
].
value
>
kpis
[
"Yearly Heating demand"
].
value
*
1.02
assert
shadow_kpis
[
"Yearly Cooling demand"
].
value
<
kpis
[
"Yearly Cooling demand"
].
value
*
0.95
########################
# Shadow, trees #
########################
tree_results
=
heatdemand_simulation
(
live_project_path
/
"Grombühl_mini_with_trees.gml"
,
meteonorm_filename
=
str
(
meteonorm
),
with_shadows
=
True
,
calculation_mode
=
"HEATING_AND_COOLING"
,
)
tree_kpis
=
to_dict
(
tree_results
.
kpis
)
assert
tree_kpis
[
"Yearly Heating demand"
].
value
>
shadow_kpis
[
"Yearly Heating demand"
].
value
*
1.01
@
pytest
.
mark
.
live
assert
tree_kpis
[
"Yearly Cooling demand"
].
value
<
shadow_kpis
[
"Yearly Cooling demand"
].
value
*
0.95
def
test_pv
(
simstadt_folder
):
results
=
run_workflow_with_citygml
(
########################
TEST_TEMPLATES
/
"03_PV"
,
# Refurbishment #
TEST_GML
,
########################
project_path
=
TEST_PROJECT
,
refurb_results
=
heatdemand_simulation
(
description
=
"PV – MiniBuchwald"
,
live_project_path
/
"Grombühl_mini_with_trees.gml"
,
meteonorm_filename
=
str
(
meteonorm
),
with_shadows
=
True
,
calculation_mode
=
"HEATING_AND_COOLING"
,
refurbishment_ratio
=
1.0
,
)
)
refurb_
kpis
=
to_dict
(
refurb_
results
.
kpis
)
kpis
=
to_dict
(
results
.
kpis
)
assert
refurb_kpis
[
"Yearly Heating demand"
].
value
<
tree_kpis
[
"Yearly Heating demand"
].
value
*
0.50
assert
"PV potential yield"
in
kpis
assert
refurb_kpis
[
"Mean Uvalue"
].
value
<
tree_kpis
[
"Mean Uvalue"
].
value
*
0.50
assert
"Roof area for PV"
in
kpis
assert
kpis
[
"PV potential yield"
].
value
>
0
assert
kpis
[
"Roof area for PV"
].
value
>
0
@
pytest
.
mark
.
live
@
pytest
.
mark
.
live
def
test_greenwater
(
live_project_path
):
def
test_load_profile
(
simstadt_folder
):
citygml_with_trees
=
live_project_path
/
"Grombühl_mini_with_trees.gml"
results
=
run_workflow_with_citygml
(
meteonorm
=
"Wuerzburg-hour.csv"
TEST_TEMPLATES
/
"02_LoadProfile"
,
TEST_GML
,
results
=
greenwater_simulation
(
citygml_with_trees
,
meteonorm
,
irrigation_ratio
=
1.0
)
project_path
=
TEST_PROJECT
,
description
=
"Load profile – MiniBuchwald"
,
)
kpis
=
to_dict
(
results
.
kpis
)
kpis
=
to_dict
(
results
.
kpis
)
assert
"Rain"
in
kpis
assert
"Number of buildings"
in
kpis
assert
"Evaporative cooling"
in
kpis
assert
"Total load [MWh]"
in
kpis
or
any
(
"load"
in
k
.
lower
()
for
k
in
kpis
)
assert
"Irrigation"
in
kpis
assert
kpis
[
"Number of buildings"
].
value
>
0
assert
kpis
[
"Rain"
].
rounded_value
==
pytest
.
approx
(
458
)
more_results
=
greenwater_simulation
(
citygml_with_trees
,
meteonorm
,
irrigation_ratio
=
0.0
)
more_kpis
=
to_dict
(
more_results
.
kpis
)
assert
"Rain"
in
more_kpis
assert
"Evaporative cooling"
in
more_kpis
assert
"Irrigation"
in
more_kpis
assert
kpis
[
"Irrigation"
].
value
>
500
assert
more_kpis
[
"Irrigation"
].
rounded_value
==
pytest
.
approx
(
0
)
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment