Skip to content

Commit 9122de0

Browse files
authored
Merge pull request #52 from iterorganization/feature/muscle3-actor
Muscle3 actor
2 parents f605139 + a9ec29c commit 9122de0

File tree

12 files changed

+463
-26
lines changed

12 files changed

+463
-26
lines changed

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Manual
2121
cli
2222
tendencies
2323
yaml_format
24+
muscle3
2425

2526
.. toctree::
2627
:caption: API docs

docs/source/muscle3.rst

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
MUSCLE3 IMAS Actor
2+
==================
3+
4+
The waveform editor includes an actor that can be included in an IMAS `MUSCLE3
5+
<https://muscle3.readthedocs.io/>`__ simulation.
6+
7+
.. caution::
8+
The IMAS MUSCLE3 actor requires the following packages:
9+
10+
- `muscle3 <https://pypi.org/project/muscle3>`__
11+
- `imas_core <https://git.iter.org/projects/IMAS/repos/al-core/browse>`__ which is
12+
not (yet) publicly available.
13+
14+
15+
This page assumes you are familiar with MUSCLE3 and IMAS coupled simulations.
16+
17+
18+
Actor details
19+
-------------
20+
21+
The actor expects messages on a single input port. We take the timestamp of the
22+
message and evaluate all waveforms at that moment in time. These waveforms are
23+
stored in their respective IDSs and sent on the respective (connected) output port.
24+
25+
.. code-block:: yaml
26+
:caption: Example ``implementations`` section for running the waveform-editor actor
27+
28+
implementations:
29+
waveform_actor:
30+
executable: waveform-editor
31+
args: actor
32+
33+
Available settings
34+
''''''''''''''''''
35+
36+
- ``waveforms`` (mandatory): indicate the (full) path to the waveform configuration.
37+
38+
39+
Input ports (``F_INIT``)
40+
''''''''''''''''''''''''
41+
42+
The actor has one input port. The name can be chosen freely in the workflow yMMSL (see
43+
example below).
44+
45+
The actor will stop with a ``RuntimeError`` when there are no input ports, or when there
46+
are multiple input ports declared.
47+
48+
49+
Output ports (``O_F``)
50+
'''''''''''''''''''''''
51+
52+
The actor can have one output port per IDS that is defined in the waveform
53+
configuration. Output ports must be named ``<ids_name>_out`` or ``<ids_name>``.
54+
55+
The actor will stop with a ``RuntimeError`` when an output port is connected for which
56+
there is no corresponding waveform defined. For below example, the actor would report an
57+
error when the ``waveforms.yaml`` doesn't contain waveforms for either the
58+
``ec_launchers`` IDS or the ``nbi`` IDS.
59+
60+
61+
Example
62+
-------
63+
64+
The following yMMSL shows an example coupling for a hypothetical ``controller`` actor
65+
with the waveform-editor actor. N.B. ``__PATH__`` is a placeholder which should be
66+
replaced with the full path to the files.
67+
68+
.. literalinclude:: ../../tests/muscle3_integration/coupling.ymmsl.in
69+
:language: yaml
70+
:caption: coupling.ymmsl.in
71+
72+
The corresponding waveform configuration is shown below:
73+
74+
.. literalinclude:: ../../tests/muscle3_integration/waveforms.yaml
75+
:language: yaml
76+
:caption: waveforms.yaml
77+

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ dependencies = [
2929
dynamic = ["version"]
3030

3131
[project.optional-dependencies]
32-
all = ["waveform-editor[dev,linting,test,docs]"]
32+
all = ["waveform-editor[muscle3,dev,linting,test,docs]"]
33+
muscle3 = [
34+
"muscle3",
35+
]
3336
dev = [
3437
"waveform-editor[test,linting]",
3538
# useful to run panel in --dev mode
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
coupling.ymmsl
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import imas
2+
import numpy as np
3+
from libmuscle import Instance, Message
4+
from ymmsl import Operator
5+
6+
7+
def controller():
8+
"""Dummy controller actor for demonstrating the waveform actor."""
9+
instance = Instance(
10+
ports={
11+
Operator.O_I: ["time_out"],
12+
Operator.S: ["ec_launchers_in", "nbi_in"],
13+
}
14+
)
15+
16+
factory = imas.IDSFactory("4.0.0")
17+
18+
while instance.reuse_instance():
19+
for time in np.linspace(0, 50, 20):
20+
# The data of this message is ignored by the waveform-actor, only the
21+
# timestamp is relevant:
22+
instance.send("time_out", Message(time))
23+
24+
# Receive waveform input
25+
msg = instance.receive("ec_launchers_in")
26+
ec_launchers = factory.new("ec_launchers")
27+
ec_launchers.deserialize(msg.data)
28+
29+
msg = instance.receive("nbi_in")
30+
nbi = factory.new("nbi")
31+
nbi.deserialize(msg.data)
32+
33+
# An actual actor would do something with the received data now:
34+
...
35+
36+
37+
if __name__ == "__main__":
38+
controller()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
ymmsl_version: v0.1
2+
3+
model:
4+
name: example_coupling_with_waveform_actor
5+
6+
components:
7+
controller:
8+
implementation: controller
9+
ports:
10+
o_i: time_out
11+
s:
12+
- ec_launchers_in
13+
- nbi_in
14+
15+
waveform_actor:
16+
implementation: waveform_actor
17+
ports:
18+
# The name of the input port can be freely chosen:
19+
f_init: time_in
20+
# Names of the output port are "<ids_name>_out":
21+
o_f:
22+
- ec_launchers_out
23+
- nbi_out
24+
25+
conduits:
26+
controller.time_out: waveform_actor.time_in
27+
waveform_actor.ec_launchers_out: controller.ec_launchers_in
28+
waveform_actor.nbi_out: controller.nbi_in
29+
30+
settings:
31+
# Mandatory setting for the waveform actor: the waveform configuration:
32+
waveform_actor.waveforms: __PATH__/waveforms.yaml
33+
34+
resources:
35+
controller:
36+
threads: 1
37+
waveform_actor:
38+
threads: 1
39+
40+
implementations:
41+
controller:
42+
executable: python
43+
args: __PATH__/controller.py
44+
45+
waveform_actor:
46+
executable: waveform-editor
47+
args: actor
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import subprocess
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
pytest.importorskip("libmuscle")
7+
pytest.importorskip("imas_core")
8+
9+
10+
def test_muscle3_integration(tmp_path, monkeypatch):
11+
# Prepare yMMSL file:
12+
curpath = Path(__file__).parent
13+
ymmsl_in = curpath / "coupling.ymmsl.in"
14+
ymmsl_out = curpath / "coupling.ymmsl"
15+
ymmsl_out.write_text(ymmsl_in.read_text().replace("__PATH__", str(curpath)))
16+
17+
# Start workflow and check that it completes successfully
18+
subprocess.run(
19+
["muscle_manager", "--start-all", str(ymmsl_out)],
20+
cwd=tmp_path,
21+
check=True,
22+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Very simple waveform configuration as an example for the MUSCLE3 IMAS actor
2+
globals:
3+
dd_version: 4.0.0
4+
5+
ec_launchers:
6+
ec_launchers/beam(1)/power_launched/data:
7+
# Ramp up to 50 kW
8+
- {type: linear, to: 50e3, duration: 10}
9+
# Flat top, constant at 50 kW
10+
- {type: constant, duration: 30}
11+
# Ramp down
12+
- {type: linear, to: 0, duration: 10}
13+
14+
nbi:
15+
# Note that the actor would give an error if we do not include this waveform:
16+
# the yMMSL file expects an output for the NBI IDS, so we must define
17+
# waveforms for this IDS.
18+
nbi/unit(1)/power_launched/data:
19+
- {type: constant, value: 0} # NBI is turned off for this configuration

tests/test_muscle3.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import imas
2+
import numpy as np
3+
import pytest
4+
5+
# libmuscle and ymmsl are optional dependencies, so may not be installed
6+
libmuscle = pytest.importorskip("libmuscle")
7+
ymmsl = pytest.importorskip("ymmsl")
8+
9+
# This cannot be imported if libmuscle is not available
10+
from waveform_editor.muscle3 import waveform_actor # noqa: E402
11+
12+
# imas_core is required for IDS serialize, unfortunately this means we cannot run these
13+
# tests in github Actions yet..
14+
pytest.importorskip("imas_core")
15+
16+
17+
WAVEFORM_YAML = """
18+
ec_launchers:
19+
beams:
20+
ec_launchers/beam(1)/phase/angle: 1
21+
ec_launchers/beam(2)/phase/angle: 2
22+
ec_launchers/beam(3)/phase/angle: 3
23+
ec_launchers/beam(4)/power_launched/data:
24+
- {to: 8.33e5, duration: 20}
25+
- {type: constant, duration: 20}
26+
- {duration: 25, to: 0}
27+
globals:
28+
dd_version: 4.0.0
29+
"""
30+
TIMES = [1, 21, 50]
31+
VALUES_PER_TIME = [8.33e5 / 20, 8.33e5, 8.33e5 * 15 / 25]
32+
33+
YMMSL = """
34+
ymmsl_version: v0.1
35+
36+
model:
37+
name: test_waveform_actor
38+
39+
components:
40+
time_generator:
41+
implementation: time_generator
42+
waveform_actor:
43+
implementation: waveform_actor
44+
waveform_validator:
45+
implementation: waveform_validator
46+
47+
conduits:
48+
time_generator.output: waveform_actor.input
49+
waveform_actor.ec_launchers_out: waveform_validator.ec_launchers_in
50+
51+
settings:
52+
waveform_actor.waveforms: {waveform_yaml}
53+
"""
54+
55+
56+
def time_generator():
57+
instance = libmuscle.Instance({ymmsl.Operator.O_I: ["output"]})
58+
59+
while instance.reuse_instance():
60+
for t in TIMES:
61+
instance.send("output", libmuscle.Message(t))
62+
63+
64+
def waveform_validator():
65+
instance = libmuscle.Instance({ymmsl.Operator.F_INIT: ["ec_launchers_in"]})
66+
67+
i = 0
68+
while instance.reuse_instance():
69+
msg = instance.receive("ec_launchers_in")
70+
assert msg.timestamp == TIMES[i]
71+
72+
ids = imas.IDSFactory("4.0.0").ec_launchers()
73+
ids.deserialize(msg.data)
74+
75+
assert np.array_equal(ids.time, [TIMES[i]])
76+
assert len(ids.beam) == 4
77+
assert np.array_equal(ids.beam[0].phase.angle, [1])
78+
assert np.array_equal(ids.beam[1].phase.angle, [2])
79+
assert np.array_equal(ids.beam[2].phase.angle, [3])
80+
assert np.allclose(ids.beam[3].power_launched.data, [VALUES_PER_TIME[i]])
81+
82+
i += 1
83+
assert i == len(TIMES)
84+
85+
86+
# Running `os.fork()` after `import pandas` triggers this warning...
87+
# It doesn't seem to be an issue (and not relevant in production where muscle_manager
88+
# will start the actor in a standalone process), so we'll ignore this warning:
89+
@pytest.mark.filterwarnings("ignore:.*use of fork():DeprecationWarning")
90+
def test_muscle3(tmp_path, monkeypatch):
91+
monkeypatch.chdir(tmp_path)
92+
waveform_yaml = (tmp_path / "waveform.yml").resolve()
93+
waveform_yaml.write_text(WAVEFORM_YAML)
94+
configuration = ymmsl.load(YMMSL.format(waveform_yaml=waveform_yaml))
95+
implementations = {
96+
"time_generator": time_generator,
97+
"waveform_actor": waveform_actor,
98+
"waveform_validator": waveform_validator,
99+
}
100+
libmuscle.runner.run_simulation(configuration, implementations)

0 commit comments

Comments
 (0)