diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index c0b5b64..586df6b 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -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: diff --git a/README.md b/README.md index d889913..a6c3e1c 100644 --- a/README.md +++ b/README.md @@ -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, @@ -119,8 +121,8 @@ 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"], @@ -128,71 +130,79 @@ statuses, ready_future = client.simulator.create( ) ) 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. diff --git a/examples/create_simulation.ipynb b/examples/create_simulation.ipynb index 9b8de34..f6965de 100644 --- a/examples/create_simulation.ipynb +++ b/examples/create_simulation.ipynb @@ -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", @@ -115,25 +114,45 @@ "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 }, @@ -141,7 +160,7 @@ "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", @@ -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", diff --git a/tests/stc/conftest.py b/tests/stc/conftest.py index dfce3cb..91eea1b 100644 --- a/tests/stc/conftest.py +++ b/tests/stc/conftest.py @@ -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 @@ -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 diff --git a/tests/stc/test_simulation.py b/tests/stc/test_simulation.py index 8e99e88..83aafd5 100644 --- a/tests/stc/test_simulation.py +++ b/tests/stc/test_simulation.py @@ -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