Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/publish_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ on:
jobs:
build_package:
uses: ./.github/workflows/_build_package.yml
create_release:
name: Create GitHub Release
needs:
- build_package
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download build artifacts
uses: actions/download-artifact@v5
with:
name: dist
path: dist
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: dist/*
# publish_package:
# name: Publish package
# needs:
Expand Down
98 changes: 54 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ print(projects)

### Running a Simulation

The following example runs a non-interactive distributed simulation using a
Spring-Mass-Damper project that has already been configured on the STC platform.
The following example runs a **single non-interactive** distributed simulation
using a Spring-Mass-Damper project that has already been configured on the STC
platform. A simulation is non-interactive when the simulator runs to completion
without user intervention (create → run → finish).

```py
import time

from pystclient.clients import PyStclient, SimulationResults
from pystclient.clients import PyStclient
from pystclient.models import (
LoggingConfiguration,
ModelParameters,
Expand Down Expand Up @@ -119,80 +121,88 @@ client.project.update_log_config(
project.id, LoggingConfiguration(post_plotting=True)
)

# 5 — Create a simulator and wait until it is ready
statuses, ready_future = client.simulator.create(
# 5 — Create a non-interactive distributed simulator and wait until it is ready
statuses, future = client.simulator.create(
SimulationConfig(
project_id=project.id,
parameter_set_names=["Config 1"],
type=SimulationType.DISTRIBUTED,
)
)
simulator_id = statuses[0].id
assert ready_future.result(600), "Simulator did not become ready in time!"

# 6 — Start the simulation with recording enabled
client.simulator.start(simulator_id, record=True)
assert future.result(600), "Simulator did not become ready in time!"

# 7 — Poll until the simulation finishes
# 6 — Poll until the simulation finishes
while not client.simulator.finished(simulator_id):
s = client.simulator.status(simulator_id)
print(f" simulation_time={s.simulation_time} end_time={s.end_time}")
time.sleep(2)
time.sleep(1)

# 8 — End the simulation and collect results
results: SimulationResults | None = client.simulator.end_simulation(simulator_id).result(120)
assert results is not None, "No results returned!"
print(f"Results available — {results.measurement_size()} measurement(s).")
print("Simulation finished.")
```

### Fetching and Displaying Results

Once a simulation has completed and a `SimulationResults` object is available
(see the previous example), you can iterate over the time-series data and
plot it with [matplotlib](https://matplotlib.org/):
Once a simulation has completed, you can retrieve it from the list of
completed simulations and query time-series data for plotting with
[matplotlib](https://matplotlib.org/):

```py
import time

import matplotlib.pyplot as plt

from pystclient.models import QueryVariable
from pystclient.models import MeasurementQuery, QueryVariable, SimulationInfo
from pystclient.types import FmuCausalityType
from pystclient.utils.time import convert_to_timestamp

# Reset the results iterator to start from the beginning
results.reset()
# Wait for the simulation to appear in the completed list
completed_simulations: list[SimulationInfo] = []

# Iterate through all time windows and collect displacement data
while len(completed_simulations) != 1:
completed_simulations = client.project.completed_simulations(
project_id=project.id,
simulator_ids=[simulator_id],
limit=10,
)
time.sleep(5)

sim = completed_simulations[0]
print(f"Simulation {sim.id} name={sim.name} param_set={sim.parameter_set_name}")

# Retrieve measurements and query specific variable data
sim_measurements = client.measurement.measurements(project.id, sim.id)
assert sim_measurements, "No measurements found!"

measurement = sim_measurements[0]
q = MeasurementQuery(
variables=[
QueryVariable(
instance_name="Spring1",
name="dis_yx",
causality=FmuCausalityType.INPUT,
)
],
time_from=0,
time_to=10,
)
results = client.measurement.query(measurement.id, q)

# Plot the displacement data
fig, ax = plt.subplots(figsize=(10, 5))

for query_result in results:
for result in query_result:
if result.signal == "dis_yx":
x = convert_to_timestamp(result.x)
ax.plot(x, result.y, label=f"{result.module} — {result.signal}")
for result in results:
x = convert_to_timestamp(result.x)
ax.plot(x, result.y, label=f"{sim.parameter_set_name}")

ax.set_xlabel("Time [s]")
ax.set_ylabel("Displacement [m]")
ax.set_title("Spring-Mass-Damper — Displacement (dis_yx)")
ax.set_title("Spring-Mass-Damper — Spring Displacement (dis_yx)")
ax.legend()
plt.tight_layout()
plt.show()
```

You can also narrow the query to specific variables using `query_variables`:

```py
results.reset(
query_variables=[
QueryVariable(instance_name="Mass1", name="dis_yx", causality="output"),
]
)

for query_result in results:
for result in query_result:
print(f"{result.module}.{result.signal}: {len(result.y)} data points")
```

> **Tip**: Use `results.step(timedelta(minutes=5))` to change the size of
> each time window when paging through results.

> **See also**: For complete, runnable notebooks check the
> [`examples/`](https://github.com/dnv-opensource/pystclient/tree/main/examples) directory.
Expand Down
77 changes: 55 additions & 22 deletions examples/create_simulation.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@
"source": [
"import time\n",
"\n",
"from pystclient.clients import SimulationResults\n",
"from pystclient.models import SimulationConfig\n",
"from pystclient.types import SimulationType\n",
"\n",
Expand All @@ -115,33 +114,53 @@
"assert future.result(600), \"Simulator did not become ready in time!\"\n",
"print(\"Simulator is ready.\")\n",
"\n",
"# Start the simulation with recording enabled\n",
"assert client.simulator.start(simulator_id, record=True)\n",
"print(\"Simulation started (recording).\")\n",
"\n",
"# Poll until the simulation finishes\n",
"while not client.simulator.finished(simulator_id):\n",
" s = client.simulator.status(simulator_id)\n",
" print(f\" simulation_time={s.simulation_time} end_time={s.end_time}\")\n",
" time.sleep(2)\n",
" time.sleep(1)\n",
"\n",
"print(\"Simulation finished.\")\n",
"print(\"Simulation finished.\")"
],
"id": "65af6192d52560c8",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"cell_type": "markdown",
"source": "### 2.1 - Retrieve completed simulation\n",
"id": "37af90877416c0df"
},
{
"metadata": {},
"cell_type": "code",
"source": [
"from pystclient.models import SimulationInfo\n",
"\n",
"# End the simulation and wait for measurement results\n",
"results_future = client.simulator.end_simulation(simulator_id)\n",
"single_results: SimulationResults | None = results_future.result(120)\n",
"assert isinstance(single_results, SimulationResults), \"No results returned!\"\n",
"print(f\"Results available - {single_results.measurement_size()} measurement(s).\")"
"# Wait for the simulation to appear in the completed list\n",
"completed_simulations: list[SimulationInfo] = []\n",
"\n",
"while len(completed_simulations) != 1:\n",
" completed_simulations = client.project.completed_simulations(\n",
" project_id=project_info.id,\n",
" simulator_ids=[simulator_id],\n",
" limit=10,\n",
" )\n",
" time.sleep(5)\n",
"\n",
"sim = completed_simulations[0]\n",
"print(f\" Simulation {sim.id} name={sim.name} param_set={sim.parameter_set_name}\")"
],
"id": "e54c6b3ba6e91089",
"id": "2b0fa396aa1c8ff0",
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"id": "c44323e06877457f",
"metadata": {},
"source": "### 2.1 - Plot displacement from single simulation\n"
"source": "### 2.2 - Plot displacement from single simulation\n"
},
{
"cell_type": "code",
Expand All @@ -151,19 +170,33 @@
"# pyright: reportUnknownMemberType=false\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from pystclient.models import QueryResult\n",
"from pystclient.models import MeasurementQuery, QueryVariable\n",
"from pystclient.types import FmuCausalityType\n",
"from pystclient.utils.time import convert_to_timestamp\n",
"\n",
"single_results.reset()\n",
"sim = completed_simulations[0]\n",
"sim_measurements = client.measurement.measurements(project_info.id, sim.id)\n",
"assert sim_measurements, \"No measurements found!\"\n",
"\n",
"measurement = sim_measurements[0]\n",
"q = MeasurementQuery(\n",
" variables=[\n",
" QueryVariable(\n",
" instance_name=\"Spring1\",\n",
" name=\"dis_yx\",\n",
" causality=FmuCausalityType.INPUT,\n",
" )\n",
" ],\n",
" time_from=0,\n",
" time_to=10,\n",
")\n",
"results = client.measurement.query(measurement.id, q)\n",
"\n",
"fig, ax = plt.subplots(figsize=(10, 5))\n",
"\n",
"for query_result in single_results:\n",
" for result in query_result:\n",
" result: QueryResult\n",
" if result.signal == \"dis_yx\":\n",
" x = convert_to_timestamp(result.x)\n",
" ax.plot(x, result.y, label=f\"{result.module} - {result.signal}\")\n",
"for result in results:\n",
" x = convert_to_timestamp(result.x)\n",
" ax.plot(x, result.y, label=f\"{sim.parameter_set_name}\")\n",
"\n",
"ax.set_xlabel(\"Time [s]\")\n",
"ax.set_ylabel(\"Displacement [m]\")\n",
Expand Down
24 changes: 12 additions & 12 deletions tests/stc/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ def test_models_by_version_id() -> list[FmuSelect]:
FmuSelect(version=uuid.UUID(x))
for x in (
[
"21B8F9DF-67F5-4A24-9325-45538E2E6EE0", # Spring
"EE1EFF08-8958-495C-ADAF-1156E296524C", # Mass
"830B4F41-BC6C-4A5F-B12D-8F81FB449626", # Damper
]
if any(x in API_ENDPOINT for x in ["dnvgl-osp-api-dev", "localhost", "127.0.0.1"])
else [
"592B1E80-79B3-4E7C-8304-D360B942176D", # Spring
"54B95A5F-0936-4BBD-8011-F3CCF934A2B1", # Mass
"373EA52B-3BF1-4190-AEE2-D28EC37871F2", # Damper
]
if "api.stc.dnv.com" in API_ENDPOINT
else [
"21B8F9DF-67F5-4A24-9325-45538E2E6EE0", # Spring
"EE1EFF08-8958-495C-ADAF-1156E296524C", # Mass
"830B4F41-BC6C-4A5F-B12D-8F81FB449626", # Damper
]
)
]
return fmus
Expand All @@ -88,16 +88,16 @@ def test_models_by_model_id() -> list[FmuSelect]:
FmuSelect(id=uuid.UUID(x))
for x in (
[
"95BD75BD-26DF-47DD-B353-49A460FCF83F", # Spring
"CBEBAEE0-2C4D-4746-B26C-BDAFC6C02247", # Mass
"88EAF9B4-BAAB-449C-97A1-FEE85CDFE38C", # Damper
]
if any(x in API_ENDPOINT for x in ["dnvgl-osp-api-dev", "localhost"])
else [
"869699A2-8945-4D48-B135-AB0AEBD292CA", # Spring
"9D872C71-9589-4171-B2D0-3374CC947F70", # Mass
"617EED59-21BF-438A-8524-0C09C7F8D5FF", # Damper
]
if "api.stc.dnv.com" in API_ENDPOINT
else [
"95BD75BD-26DF-47DD-B353-49A460FCF83F", # Spring
"CBEBAEE0-2C4D-4746-B26C-BDAFC6C02247", # Mass
"88EAF9B4-BAAB-449C-97A1-FEE85CDFE38C", # Damper
]
)
]
return fmus
Expand Down
1 change: 1 addition & 0 deletions tests/stc/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def test_run_simulation(
project_id=test_project.id,
parameter_set_names=["Config 1"],
type=SimulationType.DISTRIBUTED,
is_interactive=True,
)
)
simulator_id = status[0].id
Expand Down
Loading