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
Hide whitespace changes
Inline
Side-by-side
.gitignore
View file @
ea249156
...
...
@@ -11,3 +11,5 @@ wheels/
# Legacy reference code (not part of the library)
old/
.cache
src/simstadtpy/runner.py
View file @
ea249156
...
...
@@ -221,6 +221,7 @@ def run_workflow_with_citygml(
replaces
:
dict
|
None
=
None
,
description
:
str
|
None
=
None
,
destination
:
str
|
None
=
None
,
project_path
:
Path
|
None
=
None
,
)
->
SimStadtResults
:
"""Copy a workflow template, inject parameters, run SimStadt, and return parsed results.
...
...
@@ -230,6 +231,9 @@ def run_workflow_with_citygml(
replaces: Optional dict of XML-fragment regex replacements for params.xml.
description: Human-readable label for the result object.
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
):
template_path
=
get_template_path
()
/
template
...
...
@@ -240,9 +244,21 @@ def run_workflow_with_citygml(
if
destination
is
None
:
destination
=
random_id
()
+
"_"
+
template_name
.
rsplit
(
"_"
,
1
)[
-
1
]
project_path
=
citygml_path
.
parent
if
project_path
is
None
:
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
)
output_files
=
run_workflow
(
workflow_path
,
[
citygml_
path
.
name
])
output_files
=
run_workflow
(
workflow_path
,
[
citygml_
ref
])
if
description
is
None
:
description
=
f
"
{
template_name
}
for
{
citygml_path
.
name
}
"
...
...
tests/conftest.py
View file @
ea249156
import
os
from
pathlib
import
Path
import
pytest
...
...
@@ -31,10 +30,3 @@ def simstadt_folder():
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
*.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
or SIMSTADTPY_TEST_PROJECT is not set."""
"""Live integration tests for SimStadt workflows. Skipped if SimStadt is not installed."""
from
pathlib
import
Path
import
pytest
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
]:
...
...
@@ -12,162 +20,47 @@ def to_dict(values: list[KPI]) -> dict[str, KPI]:
@
pytest
.
mark
.
live
def
test_pv
(
live_project_path
):
citygml
=
live_project_path
/
"Grombühl_mini.gml"
meteonorm
=
"Wuerzburg-hour.csv"
min_roof_insolation
=
800
########################
# 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"
,
def
test_heat_demand
(
simstadt_folder
):
results
=
run_workflow_with_citygml
(
TEST_TEMPLATES
/
"01_HeatDemand"
,
TEST_GML
,
project_path
=
TEST_PROJECT
,
description
=
"Heat demand – MiniBuchwald"
,
)
kpis
=
to_dict
(
results
.
kpis
)
assert
"Heated area"
in
kpis
assert
"Yearly Heating demand"
in
kpis
assert
"Yearly
Cool
ing demand"
in
kpis
assert
kpis
[
"
Y
ea
r of construction
"
].
value
>
190
0
assert
kpis
[
"Yearly
Heat
ing demand"
].
value
>
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
assert
tree_kpis
[
"Yearly Cooling demand"
].
value
<
shadow_kpis
[
"Yearly Cooling demand"
].
value
*
0.95
########################
# Refurbishment #
########################
refurb_results
=
heatdemand_simulation
(
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
,
@
pytest
.
mark
.
live
def
test_pv
(
simstadt_folder
):
results
=
run_workflow_with_citygml
(
TEST_TEMPLATES
/
"03_PV"
,
TEST_GML
,
project_path
=
TEST_PROJECT
,
description
=
"PV – MiniBuchwald"
,
)
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
refurb_kpis
[
"Mean Uvalue"
].
value
<
tree_kpis
[
"Mean Uvalue"
].
value
*
0.50
assert
"PV potential yield"
in
kpis
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
def
test_greenwater
(
live_project_path
):
citygml_with_trees
=
live_project_path
/
"Grombühl_mini_with_trees.gml"
meteonorm
=
"Wuerzburg-hour.csv"
results
=
greenwater_simulation
(
citygml_with_trees
,
meteonorm
,
irrigation_ratio
=
1.0
)
def
test_load_profile
(
simstadt_folder
):
results
=
run_workflow_with_citygml
(
TEST_TEMPLATES
/
"02_LoadProfile"
,
TEST_GML
,
project_path
=
TEST_PROJECT
,
description
=
"Load profile – MiniBuchwald"
,
)
kpis
=
to_dict
(
results
.
kpis
)
assert
"Rain"
in
kpis
assert
"Evaporative cooling"
in
kpis
assert
"Irrigation"
in
kpis
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
)
assert
"Number of buildings"
in
kpis
assert
"Total load [MWh]"
in
kpis
or
any
(
"load"
in
k
.
lower
()
for
k
in
kpis
)
assert
kpis
[
"Number of buildings"
].
value
>
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