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
725dda03
Commit
725dda03
authored
Aug 04, 2026
by
Eric Duminil
Browse files
Merge branch 'experimental/csv_export'
parents
0be86308
add17c27
Changes
7
Hide whitespace changes
Inline
Side-by-side
README.md
View file @
725dda03
...
...
@@ -61,6 +61,31 @@ If no project path is specified, workflows are run in a temporary repository und
# Print detected SimStadt installation path and version
simstadt
# See help
simstadt
--help
usage: simstadt
[
-h
]
[
--gui
]
[
--csv-export
]
[
--install
]
[
-d
DESCRIPTION]
[
--destination
DESTINATION]
[
-p
PROJECT_PATH]
[
-f
]
[
-s
PATH]
[
-v
]
[
template]
[
citygml]
simstadt - Python library
for
SimStadt workflows.
positional arguments:
template Template name
(
from SIMSTADT_TEMPLATE_PATH
)
or path to a .flow directory.
citygml Path to the CityGML input file.
options:
-h
,
--help
show this
help
message and
exit
--gui
Launch the SimStadt GUI.
--csv-export
Export CSV from workflowsteps, when available.
--install
Download and
install
the latest SimStadt release to ~/Desktop.
-d
DESCRIPTION,
--description
DESCRIPTION
Human-readable label
for
the result.
--destination
DESTINATION
Workflow folder name
(
default: timestamped random
id
)
.
-p
PROJECT_PATH,
--project-path
PROJECT_PATH
Directory where the workflow folder is created.
-f
,
--files
Show output files after the run.
-s
PATH,
--save
PATH Save result DataFrame to a file
(
.csv or .json
)
.
-v
,
--verbose
Enable debug logging.
# Download and install the latest SimStadt release to ~/Desktop
simstadt
--install
...
...
src/simstadt/cli.py
View file @
725dda03
...
...
@@ -65,6 +65,7 @@ def cmd_run(args: argparse.Namespace) -> None:
description
=
args
.
description
,
destination
=
args
.
destination
,
project_path
=
args
.
project_path
,
csv_export
=
args
.
csv_export
,
)
except
(
FileNotFoundError
,
ValueError
)
as
e
:
print
(
f
"Error:
{
e
}
"
,
file
=
sys
.
stderr
)
...
...
@@ -93,6 +94,7 @@ def build_parser() -> argparse.ArgumentParser:
description
=
"simstadt - Python library for SimStadt workflows."
,
)
parser
.
add_argument
(
"--gui"
,
action
=
"store_true"
,
help
=
"Launch the SimStadt GUI."
)
parser
.
add_argument
(
"--csv-export"
,
action
=
"store_true"
,
help
=
"Export CSV from workflowsteps, when available."
)
parser
.
add_argument
(
"--install"
,
action
=
"store_true"
,
...
...
src/simstadt/results/base.py
View file @
725dda03
...
...
@@ -189,9 +189,9 @@ class SimStadtResults(ABC):
def
get_all_by_extension
(
self
,
ext
:
str
)
->
list
[
Path
]:
return
[
f
for
f
in
sorted
(
self
.
output_files
)
if
f
.
suffix
==
ext
]
def
get_unique_by_extension
(
self
,
ext
:
str
,
filter
:
str
|
None
=
None
)
->
Path
:
def
get_unique_by_extension
(
self
,
ext
:
str
,
*
filter
s
:
str
)
->
Path
:
files_by_extension
=
self
.
get_all_by_extension
(
ext
)
i
f
filter
:
f
or
filter
in
filters
:
files_by_extension
=
[
f
for
f
in
files_by_extension
if
filter
in
f
.
name
]
if
len
(
files_by_extension
)
==
0
:
raise
ValueError
(
f
"Workflow didn't seem to have returned any
{
ext
}
file!"
)
...
...
src/simstadt/runner.py
View file @
725dda03
...
...
@@ -69,8 +69,11 @@ def get_template_path() -> Path:
"No template path found. Set SIMSTADT_TEMPLATE_PATH or create a templates/ directory."
)
def
get_version_and_date
()
->
tuple
[
str
,
str
]:
SIMSTADT_VERSION_PATTERN
=
re
.
compile
(
f
"Launching SimStadt
{
SIMSTADT_VERSION_FORMAT
}
"
)
SIMSTADT_VERSION_PATTERN
=
re
.
compile
(
f
"Launching SimStadt
{
SIMSTADT_VERSION_FORMAT
}
"
)
output
=
get_simstadt_output
(
get_simstadt_folder
())
m
=
SIMSTADT_VERSION_PATTERN
.
search
(
output
)
if
not
m
:
...
...
@@ -78,6 +81,7 @@ def get_version_and_date() -> tuple[str, str]:
version
,
_branch
,
_commit
,
date
=
m
.
groups
()
return
version
,
date
# ---------------------------------------------------------------------------
# Low-level workflow helpers
# ---------------------------------------------------------------------------
...
...
@@ -207,16 +211,22 @@ def get_simstadt_output(path: Path) -> str:
)
return
result
.
stdout
# TODO: Show which workflow is currently run
# TODO: Show absolute paths to listed files?
def
run_simstadt
(
workflow_path
:
Path
,
name
:
str
)
->
str
:
def
run_simstadt
(
workflow_path
:
Path
,
name
:
str
,
csv_export
:
bool
=
False
)
->
str
:
"""Invoke the SimStadt CLI for the given workflow. Returns stdout."""
workflow_path
=
workflow_path
.
resolve
()
with
chdir
(
get_simstadt_folder
()):
log
.
info
(
"Launching %s:"
,
name
)
# NOTE: --csv-export requires SimStadt version >= August 2026
params
=
[
_simstadt_script
(),
str
(
workflow_path
)]
if
csv_export
:
params
.
append
(
"--csv-export"
)
result
=
subprocess
.
run
(
[
_simstadt_script
(),
str
(
workflow_path
)]
,
params
,
text
=
True
,
capture_output
=
True
,
check
=
False
,
...
...
@@ -247,13 +257,17 @@ def run_regionchooser(*params: str) -> str:
return
result
.
stdout
def
run_workflow
(
workflow_path
:
Path
,
citygmls
:
list
[
str
])
->
list
[
Path
]:
def
run_workflow
(
workflow_path
:
Path
,
citygmls
:
list
[
str
],
csv_export
:
bool
=
False
,
)
->
list
[
Path
]:
"""Prepare and run a SimStadt workflow. Returns the list of files written by SimStadt."""
repo_path
=
workflow_path
.
parent
.
parent
_check_paths
(
repo_path
,
workflow_path
)
name
=
_prepare_workflow
(
workflow_path
,
citygmls
)
before
=
_get_all_files
(
workflow_path
)
run_simstadt
(
workflow_path
,
name
)
run_simstadt
(
workflow_path
,
name
,
csv_export
)
after
=
_get_all_files
(
workflow_path
)
return
_compare_written_files
(
repo_path
,
before
,
after
)
...
...
@@ -273,7 +287,7 @@ def _resolve_project(citygml_path: Path, project_path: Path | None) -> tuple[Pat
if
not
citygml_path
.
exists
():
raise
FileNotFoundError
(
f
"
{
citygml_path
}
not found"
)
if
project_path
is
not
None
:
project_path
=
project_path
.
with_suffix
(
'
.proj
'
)
project_path
=
project_path
.
with_suffix
(
"
.proj
"
)
project_path
.
mkdir
(
exist_ok
=
True
,
parents
=
True
)
dest
=
project_path
/
citygml_path
.
name
if
not
dest
.
exists
():
...
...
@@ -300,6 +314,7 @@ def run_workflow_with_citygml(
description
:
str
|
None
=
None
,
destination
:
str
|
None
=
None
,
project_path
:
Path
|
None
=
None
,
csv_export
:
bool
=
False
,
)
->
SimStadtResults
:
"""Copy a workflow template, inject parameters, run SimStadt, and return parsed results.
...
...
@@ -313,6 +328,7 @@ def run_workflow_with_citygml(
- project_path given: copy CityGML there if not already present (takes priority).
- CityGML inside a .proj folder: use parent as project, reference by name.
- Neither: create a temporary repository under /tmp (not auto-cleaned).
csv_export: If available, workflowsteps will export Buildings/Surfaces CSV files.
"""
citygml_path
=
Path
(
citygml_path
)
...
...
@@ -331,7 +347,7 @@ def run_workflow_with_citygml(
workflow_path
=
copy_workflow_from_template
(
template_path
,
resolved_project_path
,
destination
,
replaces
)
output_files
=
run_workflow
(
workflow_path
,
[
citygml_filename
])
output_files
=
run_workflow
(
workflow_path
,
[
citygml_filename
]
,
csv_export
)
if
description
is
None
:
description
=
f
"
{
template_name
}
for
{
citygml_path
.
name
}
"
...
...
tests/conftest.py
View file @
725dda03
...
...
@@ -34,6 +34,7 @@ def test_project_path() -> Path:
# SimStadt mock (replaces subprocess call with fixture file copying)
# ---------------------------------------------------------------------------
def
_find_fixture_flow
(
provider
:
str
)
->
Path
|
None
:
"""Find a .flow directory in test data that matches the given provider and has output files."""
for
flow_dir
in
TEST_REPOSITORY
.
rglob
(
"*.flow"
):
...
...
@@ -43,7 +44,9 @@ def _find_fixture_flow(provider: str) -> Path | None:
root
=
et
.
parse
(
params_xml
).
getroot
()
elem
=
root
.
find
(
".//void[@property='workflowProvider']/object"
)
if
elem
is
not
None
and
elem
.
get
(
"class"
)
==
provider
:
non_params
=
[
f
for
f
in
flow_dir
.
rglob
(
"*"
)
if
f
.
is_file
()
and
f
.
name
!=
"params.xml"
]
non_params
=
[
f
for
f
in
flow_dir
.
rglob
(
"*"
)
if
f
.
is_file
()
and
f
.
name
!=
"params.xml"
]
if
non_params
:
return
flow_dir
return
None
...
...
@@ -53,7 +56,7 @@ def _find_fixture_flow(provider: str) -> Path | None:
def
mock_simstadt
(
monkeypatch
):
"""Mock run_simstadt by copying fixture output files instead of running SimStadt."""
def
_mock
(
workflow_path
:
Path
,
name
:
str
)
->
str
:
def
_mock
(
workflow_path
:
Path
,
name
:
str
,
csv_export
:
bool
=
False
)
->
str
:
root
=
et
.
parse
(
workflow_path
/
"params.xml"
).
getroot
()
elem
=
root
.
find
(
".//void[@property='workflowProvider']/object"
)
provider
=
elem
.
get
(
"class"
)
if
elem
is
not
None
else
None
...
...
@@ -74,13 +77,13 @@ def mock_simstadt(monkeypatch):
# Live fixtures (require SimStadt to be installed)
# ---------------------------------------------------------------------------
@
pytest
.
fixture
(
scope
=
"session"
)
def
simstadt_folder
():
"""Skip the test if SimStadt is not installed."""
from
simstadt.runner
import
get_simstadt_folder
try
:
return
get_simstadt_folder
()
except
ValueError
as
e
:
pytest
.
skip
(
str
(
e
))
tests/test_cli.py
View file @
725dda03
...
...
@@ -91,7 +91,7 @@ def test_run_files_flag(mock_simstadt, run_cli, capsys, tmp_path):
assert
"Mean Uvalue"
in
out
def
_broken_simstadt
(
_workflow_path
:
Path
,
_name
:
str
)
->
str
:
def
_broken_simstadt
(
_workflow_path
:
Path
,
_name
:
str
,
_csv_export
:
bool
=
False
)
->
str
:
raise
ValueError
(
"BOOM!"
)
...
...
tests/test_simulations.py
View file @
725dda03
...
...
@@ -40,6 +40,30 @@ def test_heat_demand():
assert
kpis
[
"Year of construction"
].
value
==
pytest
.
approx
(
1950
,
abs
=
50
)
assert
kpis
[
"Year of construction"
].
unit
==
None
for
step
in
[
"Geometric"
,
"Physics"
,
"Usage"
]:
with
pytest
.
raises
(
ValueError
,
match
=
"Workflow didn't seem to have returned any .csv file!"
):
results
.
get_unique_by_extension
(
'.csv'
,
step
)
@
pytest
.
mark
.
integration
def
test_heat_demand_with_csv
():
results
=
run_workflow_with_citygml
(
TEST_TEMPLATES
/
"01_HeatDemand"
,
TEST_GML
,
project_path
=
TEST_PROJECT
,
description
=
"Heat demand – MiniBuchwald"
,
csv_export
=
True
,
)
for
step
,
suffixes
in
[
(
"Geometric"
,
[
"buildings"
,
"surfaces"
]),
(
"Physics"
,
[
"buildings"
,
"surfaces"
]),
(
"Usage"
,
[
"buildings"
]),
]:
for
suffix
in
suffixes
:
found_csv
=
results
.
get_unique_by_extension
(
'.csv'
,
step
,
suffix
)
assert
found_csv
.
stat
().
st_size
>
0
@
pytest
.
mark
.
integration
def
test_pv
():
...
...
@@ -83,6 +107,7 @@ def test_load_profile():
assert
"Average load"
in
kpis
assert
kpis
[
"Number of buildings"
].
value
>
0
@
pytest
.
mark
.
integration
def
test_energy_grid
():
results
=
run_workflow_with_citygml
(
...
...
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