ansys-systemcoupling-core 0.5.0__py3-none-any.whl → 0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ansys-systemcoupling-core might be problematic. Click here for more details.
- ansys/systemcoupling/core/__init__.py +11 -5
- ansys/systemcoupling/core/adaptor/api_23_1/show_plot.py +75 -0
- ansys/systemcoupling/core/adaptor/api_23_1/solution_root.py +7 -1
- ansys/systemcoupling/core/adaptor/api_23_2/show_plot.py +75 -0
- ansys/systemcoupling/core/adaptor/api_23_2/solution_root.py +7 -1
- ansys/systemcoupling/core/adaptor/api_24_1/show_plot.py +75 -0
- ansys/systemcoupling/core/adaptor/api_24_1/solution_root.py +7 -1
- ansys/systemcoupling/core/adaptor/api_24_2/add_participant.py +4 -4
- ansys/systemcoupling/core/adaptor/api_24_2/setup_root.py +1 -1
- ansys/systemcoupling/core/adaptor/api_24_2/show_plot.py +75 -0
- ansys/systemcoupling/core/adaptor/api_24_2/solution_root.py +7 -1
- ansys/systemcoupling/core/adaptor/impl/injected_commands.py +215 -32
- ansys/systemcoupling/core/adaptor/impl/static_info.py +17 -0
- ansys/systemcoupling/core/adaptor/impl/syc_proxy.py +3 -0
- ansys/systemcoupling/core/adaptor/impl/syc_proxy_interface.py +4 -0
- ansys/systemcoupling/core/adaptor/impl/types.py +1 -1
- ansys/systemcoupling/core/charts/chart_datatypes.py +169 -0
- ansys/systemcoupling/core/charts/csv_chartdata.py +299 -0
- ansys/systemcoupling/core/charts/live_csv_datasource.py +87 -0
- ansys/systemcoupling/core/charts/message_dispatcher.py +84 -0
- ansys/systemcoupling/core/charts/plot_functions.py +92 -0
- ansys/systemcoupling/core/charts/plotdefinition_manager.py +303 -0
- ansys/systemcoupling/core/charts/plotter.py +343 -0
- ansys/systemcoupling/core/client/grpc_client.py +6 -1
- ansys/systemcoupling/core/participant/manager.py +25 -9
- ansys/systemcoupling/core/participant/protocol.py +1 -0
- ansys/systemcoupling/core/session.py +4 -4
- ansys/systemcoupling/core/syc_version.py +1 -1
- ansys/systemcoupling/core/util/file_transfer.py +4 -0
- {ansys_systemcoupling_core-0.5.0.dist-info → ansys_systemcoupling_core-0.6.dist-info}/METADATA +11 -10
- {ansys_systemcoupling_core-0.5.0.dist-info → ansys_systemcoupling_core-0.6.dist-info}/RECORD +33 -22
- {ansys_systemcoupling_core-0.5.0.dist-info → ansys_systemcoupling_core-0.6.dist-info}/LICENSE +0 -0
- {ansys_systemcoupling_core-0.5.0.dist-info → ansys_systemcoupling_core-0.6.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from ansys.systemcoupling.core.charts.chart_datatypes import InterfaceInfo, SeriesType
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class DataTransferSpec:
|
|
31
|
+
# It's not ideal, but we have to work in terms of display names for transfers,
|
|
32
|
+
# as that is all we have in the data (the CSV data, at least).
|
|
33
|
+
display_name: str
|
|
34
|
+
show_convergence: bool = True
|
|
35
|
+
show_transfer_values: bool = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class InterfaceSpec:
|
|
40
|
+
name: str
|
|
41
|
+
display_name: str
|
|
42
|
+
transfers: list[DataTransferSpec] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class PlotSpec:
|
|
47
|
+
interfaces: list[InterfaceSpec] = field(default_factory=list)
|
|
48
|
+
plot_time: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
Convergence subplot:
|
|
53
|
+
title - Data Transfer Convergence for <interface disp name>
|
|
54
|
+
x-axis label - Iteration/Time
|
|
55
|
+
y-axis label - RMS Change in Target Value
|
|
56
|
+
|
|
57
|
+
# x-data: []
|
|
58
|
+
y-data: [([], <label=transfer disp name>)]
|
|
59
|
+
|
|
60
|
+
# y-data - we actually want an index
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Transfer values subplot:
|
|
64
|
+
title - <interface display name> - <transfer display name> (<value type>)
|
|
65
|
+
x-axis label: Iteration/time
|
|
66
|
+
y-axis label: <NOT SET>
|
|
67
|
+
|
|
68
|
+
y-data: [([], <label=source|tgt disp name + suffix)]
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class SubplotDefinition:
|
|
75
|
+
"""Various data - mainly title and label strings - used in the rendering of
|
|
76
|
+
a subplot.
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
title: str
|
|
81
|
+
The title of the subplot.
|
|
82
|
+
is_log_y: bool
|
|
83
|
+
Whether the y-axis is logarithmic.
|
|
84
|
+
x_axis_label: str
|
|
85
|
+
The label shown on the x-axis.
|
|
86
|
+
y_axis_label: str
|
|
87
|
+
The label shown on the y-axis. This is an empty string for convergence plots.
|
|
88
|
+
index: int = -1
|
|
89
|
+
The index of this subplot within the list of subplots in the current figure.
|
|
90
|
+
(In `matplotlib` terms it also indexes the corresponding ``Axes`` item in
|
|
91
|
+
the figure.)
|
|
92
|
+
series_labels: list[str] = field(default_factory=list)
|
|
93
|
+
Labels for each series of the subplot.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
title: str
|
|
97
|
+
is_log_y: bool
|
|
98
|
+
x_axis_label: str
|
|
99
|
+
y_axis_label: str
|
|
100
|
+
index: int = -1
|
|
101
|
+
series_labels: list[str] = field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PlotDefinitionManager:
|
|
105
|
+
def __init__(self, spec: PlotSpec):
|
|
106
|
+
self._plot_spec = spec
|
|
107
|
+
self._data_index_map: dict[str, dict[int, tuple[SubplotDefinition, int]]] = {}
|
|
108
|
+
self._conv_subplots: dict[str, SubplotDefinition] = {}
|
|
109
|
+
self._transfer_subplots: dict[tuple[str, str, int], SubplotDefinition] = {}
|
|
110
|
+
self._subplots: list[SubplotDefinition] = []
|
|
111
|
+
self._allocate_subplots()
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def subplots(self) -> list[SubplotDefinition]:
|
|
115
|
+
return self._subplots
|
|
116
|
+
|
|
117
|
+
def subplot_for_data_index(
|
|
118
|
+
self, interface_name: str, data_index: int
|
|
119
|
+
) -> tuple[Optional[SubplotDefinition], int]:
|
|
120
|
+
"""Return the subplot definition, and the line index within the
|
|
121
|
+
subplot, corresponding to a given ``data_index``.
|
|
122
|
+
|
|
123
|
+
The ``data_index`` is a "global" line index for the interface.
|
|
124
|
+
|
|
125
|
+
If there is no subplot corresponding to the provided index,
|
|
126
|
+
return a tuple ``(None, -1)``
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
return self._data_index_map[interface_name][data_index]
|
|
130
|
+
except KeyError:
|
|
131
|
+
return (None, -1)
|
|
132
|
+
|
|
133
|
+
def get_layout(self) -> tuple[int, int]:
|
|
134
|
+
nsubplot = len(self._subplots)
|
|
135
|
+
|
|
136
|
+
if nsubplot == 1:
|
|
137
|
+
ncol = 1
|
|
138
|
+
elif nsubplot < 6:
|
|
139
|
+
ncol = 2
|
|
140
|
+
elif nsubplot < 12:
|
|
141
|
+
ncol = 3
|
|
142
|
+
elif nsubplot < 18:
|
|
143
|
+
ncol = 4
|
|
144
|
+
elif nsubplot < 26:
|
|
145
|
+
ncol = 5
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError(f"Too many subplots requested: {nsubplot}")
|
|
148
|
+
nrow = nsubplot // ncol
|
|
149
|
+
if nsubplot % ncol != 0:
|
|
150
|
+
nrow += 1
|
|
151
|
+
|
|
152
|
+
return (nrow, ncol)
|
|
153
|
+
|
|
154
|
+
def _allocate_subplots(self):
|
|
155
|
+
is_time = self._plot_spec.plot_time
|
|
156
|
+
conv_subplots = {}
|
|
157
|
+
transfer_subplots = {}
|
|
158
|
+
subplots = []
|
|
159
|
+
for interface in self._plot_spec.interfaces:
|
|
160
|
+
conv = SubplotDefinition(
|
|
161
|
+
title=f"Data transfer convergence on {interface.display_name}",
|
|
162
|
+
is_log_y=True,
|
|
163
|
+
x_axis_label="Time" if is_time else "Iteration",
|
|
164
|
+
y_axis_label="RMS Change in target value",
|
|
165
|
+
)
|
|
166
|
+
# Add this now so that it is before transfer values plots but we may end
|
|
167
|
+
# up removing it if none of the transfers add a convergence line to it
|
|
168
|
+
conv_index = len(subplots)
|
|
169
|
+
subplots.append(conv)
|
|
170
|
+
keep_conv = False
|
|
171
|
+
transfer_disambig: dict[str, int] = {}
|
|
172
|
+
for transfer in interface.transfers:
|
|
173
|
+
if transfer.display_name in transfer_disambig:
|
|
174
|
+
transfer_disambig[transfer.display_name] += 1
|
|
175
|
+
else:
|
|
176
|
+
transfer_disambig[transfer.display_name] = 0
|
|
177
|
+
if transfer.show_convergence:
|
|
178
|
+
keep_conv = True
|
|
179
|
+
if transfer.show_transfer_values:
|
|
180
|
+
transfer_value = SubplotDefinition(
|
|
181
|
+
# NB: <VALUETYPE> is a placeholder - substitute later from metadata info
|
|
182
|
+
title=f"{interface.display_name} - {transfer.display_name} (<VALUETYPE>)",
|
|
183
|
+
is_log_y=False,
|
|
184
|
+
x_axis_label="Time" if is_time else "Iteration",
|
|
185
|
+
y_axis_label="",
|
|
186
|
+
)
|
|
187
|
+
transfer_subplots[
|
|
188
|
+
(
|
|
189
|
+
interface.name,
|
|
190
|
+
transfer.display_name,
|
|
191
|
+
transfer_disambig[transfer.display_name],
|
|
192
|
+
)
|
|
193
|
+
] = transfer_value
|
|
194
|
+
subplots.append(transfer_value)
|
|
195
|
+
if keep_conv:
|
|
196
|
+
conv_subplots[interface.name] = conv
|
|
197
|
+
else:
|
|
198
|
+
subplots[conv_index] = None
|
|
199
|
+
# Clean out inactive convergence plots
|
|
200
|
+
self._subplots = [subplot for subplot in subplots if subplot is not None]
|
|
201
|
+
for i, subplot in enumerate(self._subplots):
|
|
202
|
+
subplot.index = i
|
|
203
|
+
self._conv_subplots: dict[str, SubplotDefinition] = conv_subplots
|
|
204
|
+
self._transfer_subplots: dict[tuple[str, str, int], SubplotDefinition] = (
|
|
205
|
+
transfer_subplots
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def set_metadata(self, metadata: InterfaceInfo):
|
|
209
|
+
"""Reconcile the metadata for a single interface with the pre-allocated
|
|
210
|
+
sub-plots and set up any necessary data routing.
|
|
211
|
+
|
|
212
|
+
Typically, this data only starts to be provided once the raw plot data starts
|
|
213
|
+
being generated.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
# If a subset of transfers was specified in the plot spec, this is already
|
|
217
|
+
# implicitly accounted for in self._tranfer_subplots, which will contain only
|
|
218
|
+
# the active ones. However, some additional work has to be done to filter the
|
|
219
|
+
# transfers shown on the convergence subplot and we have to go back to the plot
|
|
220
|
+
# spec to get a list of active transfers.
|
|
221
|
+
active_transfers = []
|
|
222
|
+
for intf in self._plot_spec.interfaces:
|
|
223
|
+
if intf.name == metadata.name:
|
|
224
|
+
active_transfers = [trans.display_name for trans in intf.transfers]
|
|
225
|
+
break
|
|
226
|
+
if not active_transfers:
|
|
227
|
+
# TODO: should this be an exception?
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# map from source data index to corresponding (subplot, line index within subplot)
|
|
231
|
+
data_index_map: dict[int, tuple[SubplotDefinition, int]] = {}
|
|
232
|
+
interface_name = metadata.name
|
|
233
|
+
iconv = 0
|
|
234
|
+
|
|
235
|
+
# Keep a running count of the transfer value lines associated with a given
|
|
236
|
+
# transfer. There will be multiple if the transfer variable has vector
|
|
237
|
+
# and or real/imag components. Note that a transfer is uniquely identified by a
|
|
238
|
+
# pair (transfer_name, int) because transfer names are not guaranteed to be unique.
|
|
239
|
+
# The integer is the "disambiguation_index" the transfer's TransferSeriesInfo.
|
|
240
|
+
transfer_value_line_count: dict[tuple[str, int], int] = {}
|
|
241
|
+
|
|
242
|
+
for transfer in metadata.transfer_info:
|
|
243
|
+
transfer_key = (
|
|
244
|
+
transfer.transfer_display_name,
|
|
245
|
+
transfer.disambiguation_index,
|
|
246
|
+
)
|
|
247
|
+
if transfer.series_type == SeriesType.CONVERGENCE:
|
|
248
|
+
if transfer.transfer_display_name not in active_transfers:
|
|
249
|
+
# We don't want this transfer on the convergence plot
|
|
250
|
+
continue
|
|
251
|
+
# Clear the entry in case transfer names are not unique. (If another transfer
|
|
252
|
+
# with the same name needs plotting, then it should appear as a second entry
|
|
253
|
+
# in active_transfers.)
|
|
254
|
+
active_transfers[
|
|
255
|
+
active_transfers.index(transfer.transfer_display_name)
|
|
256
|
+
] = ""
|
|
257
|
+
|
|
258
|
+
if conv_subplot := self._conv_subplots.get(interface_name):
|
|
259
|
+
data_index_map[transfer.data_index] = (conv_subplot, iconv)
|
|
260
|
+
# Add a new series list to y_data, and label to series_labels
|
|
261
|
+
# Both will be at position iconv of respective lists
|
|
262
|
+
# conv_subplot.y_data.append([])
|
|
263
|
+
conv_subplot.series_labels.append(transfer.transfer_display_name)
|
|
264
|
+
iconv += 1
|
|
265
|
+
else:
|
|
266
|
+
transfer_value_subplot = self._transfer_subplots.get(
|
|
267
|
+
(interface_name, transfer_key[0], transfer_key[1])
|
|
268
|
+
)
|
|
269
|
+
if transfer_value_subplot:
|
|
270
|
+
value_type = (
|
|
271
|
+
"Sum"
|
|
272
|
+
if transfer.series_type == SeriesType.SUM
|
|
273
|
+
else "Weighted Average"
|
|
274
|
+
)
|
|
275
|
+
transfer_value_subplot.title = transfer_value_subplot.title.replace(
|
|
276
|
+
"<VALUETYPE>", value_type
|
|
277
|
+
)
|
|
278
|
+
itransval = transfer_value_line_count.get(transfer_key, 0)
|
|
279
|
+
if not transfer.line_suffixes:
|
|
280
|
+
data_index_map[transfer.data_index] = (
|
|
281
|
+
transfer_value_subplot,
|
|
282
|
+
itransval,
|
|
283
|
+
)
|
|
284
|
+
transfer_value_subplot.series_labels.append(
|
|
285
|
+
transfer.participant_display_name
|
|
286
|
+
)
|
|
287
|
+
itransval += 1
|
|
288
|
+
transfer_value_line_count[transfer_key] = itransval
|
|
289
|
+
else:
|
|
290
|
+
for i, suffix in enumerate(transfer.line_suffixes):
|
|
291
|
+
data_index_map[transfer.data_index + i] = (
|
|
292
|
+
transfer_value_subplot,
|
|
293
|
+
itransval + i,
|
|
294
|
+
)
|
|
295
|
+
transfer_value_subplot.series_labels.append(
|
|
296
|
+
transfer.participant_display_name + suffix
|
|
297
|
+
)
|
|
298
|
+
transfer_value_line_count[transfer_key] = itransval + len(
|
|
299
|
+
transfer.line_suffixes
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# This will be what allows us to update subplot data as new data received
|
|
303
|
+
self._data_index_map[interface_name] = data_index_map
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Callable, Optional, Union
|
|
26
|
+
|
|
27
|
+
from matplotlib.animation import FuncAnimation
|
|
28
|
+
from matplotlib.figure import Figure
|
|
29
|
+
from matplotlib.lines import Line2D
|
|
30
|
+
import matplotlib.pyplot as plt
|
|
31
|
+
|
|
32
|
+
from ansys.systemcoupling.core.charts.chart_datatypes import (
|
|
33
|
+
InterfaceInfo,
|
|
34
|
+
SeriesData,
|
|
35
|
+
TimestepData,
|
|
36
|
+
)
|
|
37
|
+
from ansys.systemcoupling.core.charts.plotdefinition_manager import (
|
|
38
|
+
PlotDefinitionManager,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _process_timestep_data(
|
|
43
|
+
timestep_data: TimestepData,
|
|
44
|
+
) -> tuple[list[int], list[float]]:
|
|
45
|
+
if not timestep_data.timestep:
|
|
46
|
+
return [], []
|
|
47
|
+
|
|
48
|
+
# TODO: for a dynamically updating case, it should be possible to do a partial update.
|
|
49
|
+
time_indexes = [0]
|
|
50
|
+
times = [None]
|
|
51
|
+
curr_step = timestep_data.timestep[0]
|
|
52
|
+
for i, step in enumerate(timestep_data.timestep):
|
|
53
|
+
time = timestep_data.time[i]
|
|
54
|
+
if step == curr_step:
|
|
55
|
+
# Still in the same step, so update
|
|
56
|
+
times[-1] = time
|
|
57
|
+
time_indexes[-1] = i
|
|
58
|
+
else:
|
|
59
|
+
# New step
|
|
60
|
+
times.append(time)
|
|
61
|
+
time_indexes.append(i)
|
|
62
|
+
curr_step = step
|
|
63
|
+
return (time_indexes, times)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _calc_new_ylimits_linear(
|
|
67
|
+
ynew: list[float], old_lim: Optional[tuple[float, float]]
|
|
68
|
+
) -> tuple[float, float]:
|
|
69
|
+
resize_tol = 0.02
|
|
70
|
+
resize_delta = 0.1
|
|
71
|
+
|
|
72
|
+
min_y = min(ynew)
|
|
73
|
+
max_y = max(ynew)
|
|
74
|
+
|
|
75
|
+
# Try to do something reasonable while we still don't have much data.
|
|
76
|
+
# Try to account for case where we don't have much data and range & value both zero.
|
|
77
|
+
data_range = min_y if abs(max_y - min_y) < 1.0e-7 else max_y - min_y
|
|
78
|
+
data_range = abs(data_range)
|
|
79
|
+
if data_range < 1.0e-7:
|
|
80
|
+
delta_limits = 1.0e-7
|
|
81
|
+
else:
|
|
82
|
+
delta_limits = resize_delta * data_range
|
|
83
|
+
|
|
84
|
+
if old_lim is None:
|
|
85
|
+
# First update - force calculation
|
|
86
|
+
old_l = min_y + 1
|
|
87
|
+
old_u = max_y - 1
|
|
88
|
+
else:
|
|
89
|
+
new_l, new_u = old_l, old_u = old_lim
|
|
90
|
+
|
|
91
|
+
if min_y < old_l + resize_tol * data_range:
|
|
92
|
+
new_l = min_y - delta_limits
|
|
93
|
+
if max_y > old_u - resize_tol * data_range:
|
|
94
|
+
new_u = max_y + delta_limits
|
|
95
|
+
|
|
96
|
+
return new_l, new_u
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _calc_new_ylimits_log(
|
|
100
|
+
ynew: list[float], old_lim: Optional[tuple[float, float]]
|
|
101
|
+
) -> tuple[float, float]:
|
|
102
|
+
|
|
103
|
+
min_y = min(ynew)
|
|
104
|
+
max_y = max(ynew)
|
|
105
|
+
|
|
106
|
+
if old_lim is not None:
|
|
107
|
+
new_l, new_u = old_lim
|
|
108
|
+
|
|
109
|
+
if old_lim is None or min_y < old_lim[0]:
|
|
110
|
+
log_l = math.log10(min_y)
|
|
111
|
+
exponent = math.floor(log_l)
|
|
112
|
+
new_l = math.pow(10.0, exponent)
|
|
113
|
+
if old_lim is None or max_y > old_lim[1]:
|
|
114
|
+
log_u = math.log10(max_y)
|
|
115
|
+
exponent = math.ceil(log_u)
|
|
116
|
+
new_u = math.pow(10.0, exponent)
|
|
117
|
+
# We do this in the GUI - is it needed?
|
|
118
|
+
new_u *= 1.0 + 1.0e-7
|
|
119
|
+
|
|
120
|
+
return new_l, new_u
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _update_xy_data(
|
|
124
|
+
time_info: Optional[tuple[list[int], list[float]]],
|
|
125
|
+
x_curr, # ArrayLike
|
|
126
|
+
y_curr, # ArrayLike
|
|
127
|
+
series_data: list[float],
|
|
128
|
+
new_start_index: int,
|
|
129
|
+
) -> tuple[Union[list[float], list[int]], list[float]]:
|
|
130
|
+
|
|
131
|
+
new_total_data_len = new_start_index + len(series_data)
|
|
132
|
+
x_new = []
|
|
133
|
+
y_new = []
|
|
134
|
+
if time_info:
|
|
135
|
+
time_indexes, time_values = time_info
|
|
136
|
+
# IMPORTANT: Assume that time data is updated ahead of any series data
|
|
137
|
+
# This means that (new_data_len - 1) should always be <= maximum known
|
|
138
|
+
# time index.
|
|
139
|
+
# time_indexes[-1] is the latest known 0-based iteration index that belongs
|
|
140
|
+
# to the latest timestep.
|
|
141
|
+
for i, time_iter in enumerate(time_indexes):
|
|
142
|
+
# Straight copy if not overlapping with new data yet
|
|
143
|
+
if time_iter < new_start_index:
|
|
144
|
+
x_new.append(time_values[i])
|
|
145
|
+
y_new.append(y_curr[i])
|
|
146
|
+
|
|
147
|
+
# Don't assume we have data yet for all known timesteps
|
|
148
|
+
elif time_iter < new_total_data_len:
|
|
149
|
+
x_new.append(time_values[i])
|
|
150
|
+
y_new.append(series_data[time_iter - new_start_index])
|
|
151
|
+
else:
|
|
152
|
+
for i in range(new_total_data_len):
|
|
153
|
+
if i < new_start_index:
|
|
154
|
+
x_new.append(x_curr[i])
|
|
155
|
+
y_new.append(y_curr[i])
|
|
156
|
+
else:
|
|
157
|
+
x_new.append(i + 1)
|
|
158
|
+
y_new.append(series_data[i - new_start_index])
|
|
159
|
+
return (x_new, y_new)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# TODO: Only handles one interface at the moment! Generalise to multiple
|
|
163
|
+
class Plotter:
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
mgr: PlotDefinitionManager,
|
|
167
|
+
request_update: Optional[Callable[[], None]] = None,
|
|
168
|
+
):
|
|
169
|
+
self._mgr = mgr
|
|
170
|
+
self._request_update = request_update
|
|
171
|
+
|
|
172
|
+
self._fig: Figure = plt.figure()
|
|
173
|
+
self._subplot_lines: list[list[Line2D]] = []
|
|
174
|
+
self._subplot_limits_set: list[bool] = []
|
|
175
|
+
self._metadata: Optional[InterfaceInfo] = None
|
|
176
|
+
|
|
177
|
+
# Empty if not transient:
|
|
178
|
+
self._times: list[float] = [] # Time value at each time step
|
|
179
|
+
self._time_indexes: list[int] = [] # Iteration to take value at time i from
|
|
180
|
+
|
|
181
|
+
def set_metadata(self, metadata: InterfaceInfo):
|
|
182
|
+
self._metadata = metadata
|
|
183
|
+
self._mgr.set_metadata(metadata)
|
|
184
|
+
# We now have enough information to create the (empty) plots
|
|
185
|
+
self._init_plots()
|
|
186
|
+
|
|
187
|
+
def set_timestep_data(self, timestep_data: TimestepData):
|
|
188
|
+
|
|
189
|
+
if timestep_data.timestep and not self._metadata.is_transient:
|
|
190
|
+
raise RuntimeError("Attempt to set timestep data on non-transient case")
|
|
191
|
+
|
|
192
|
+
self._time_indexes, self._times = _process_timestep_data(timestep_data)
|
|
193
|
+
|
|
194
|
+
def update_line_series(self, series_data: SeriesData):
|
|
195
|
+
"""Update the line series determined by the provided ``series_data`` with the
|
|
196
|
+
incremental data that it contains.
|
|
197
|
+
|
|
198
|
+
The ``series_data`` contains the "start index" in the full series, the index
|
|
199
|
+
to start writing the new data.
|
|
200
|
+
"""
|
|
201
|
+
if not self._metadata:
|
|
202
|
+
raise RuntimeError(
|
|
203
|
+
"Attempt to add series data to plot before metadata provided."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
trans = self._metadata.transfer_info[series_data.transfer_index]
|
|
207
|
+
offset = (
|
|
208
|
+
series_data.component_index
|
|
209
|
+
if series_data.component_index is not None
|
|
210
|
+
else 0
|
|
211
|
+
)
|
|
212
|
+
subplot_defn, subplot_line_index = self._mgr.subplot_for_data_index(
|
|
213
|
+
self._metadata.name, trans.data_index + offset
|
|
214
|
+
)
|
|
215
|
+
if subplot_defn is None:
|
|
216
|
+
# This can happen if the list of plots being show is filtered.
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
subplot_line = self._subplot_lines[subplot_defn.index][subplot_line_index]
|
|
220
|
+
|
|
221
|
+
# We don't assume that all provided series_data are fully up to
|
|
222
|
+
# date with the latest iteration or timestep that pertains globally
|
|
223
|
+
# over all plots.
|
|
224
|
+
|
|
225
|
+
if len(series_data.data) == 0:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
x_curr, y_curr = subplot_line.get_data()
|
|
229
|
+
x_new, y_new = _update_xy_data(
|
|
230
|
+
(self._time_indexes, self._times) if self._times else None,
|
|
231
|
+
x_curr,
|
|
232
|
+
y_curr,
|
|
233
|
+
series_data.data,
|
|
234
|
+
series_data.start_index,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
self.update_limits(subplot_defn.index, subplot_defn.is_log_y, x_new, y_new)
|
|
238
|
+
subplot_line.set_data(x_new, y_new)
|
|
239
|
+
|
|
240
|
+
def update_limits(self, subplot_index, is_log_y, x_new, y_new):
|
|
241
|
+
axes = self._fig.axes[subplot_index]
|
|
242
|
+
|
|
243
|
+
are_limits_initialised = self._subplot_limits_set[subplot_index]
|
|
244
|
+
|
|
245
|
+
old_xlim = (0, 0)
|
|
246
|
+
old_ylim = None
|
|
247
|
+
if are_limits_initialised:
|
|
248
|
+
old_xlim = axes.get_xlim()
|
|
249
|
+
old_ylim = axes.get_ylim()
|
|
250
|
+
|
|
251
|
+
new_ylimits = (
|
|
252
|
+
_calc_new_ylimits_log(y_new, old_ylim)
|
|
253
|
+
if is_log_y
|
|
254
|
+
else _calc_new_ylimits_linear(y_new, old_ylim)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
new_xlimits = old_xlim
|
|
258
|
+
if self._times:
|
|
259
|
+
if x_new[-1] >= old_xlim[1] * 0.95:
|
|
260
|
+
new_xlimits = (0.0, x_new[-1] * 1.1)
|
|
261
|
+
else:
|
|
262
|
+
if len(x_new) >= old_xlim[1] - 1:
|
|
263
|
+
new_xlimits = (1, len(x_new) + 1)
|
|
264
|
+
|
|
265
|
+
axes.set_xlim(new_xlimits)
|
|
266
|
+
axes.set_ylim(new_ylimits)
|
|
267
|
+
|
|
268
|
+
self._subplot_limits_set[subplot_index] = True
|
|
269
|
+
|
|
270
|
+
def close(self):
|
|
271
|
+
if self._fig:
|
|
272
|
+
plt.close(self._fig)
|
|
273
|
+
|
|
274
|
+
def show_plot(self, noblock=False):
|
|
275
|
+
if noblock:
|
|
276
|
+
plt.ion()
|
|
277
|
+
plt.show()
|
|
278
|
+
|
|
279
|
+
def show_animated(self):
|
|
280
|
+
# NB: if using the wait_for_metadata() approach
|
|
281
|
+
# supported by MessageDispatcher, do it here like
|
|
282
|
+
# this (assume the wait function is stored as an
|
|
283
|
+
# attribute):
|
|
284
|
+
#
|
|
285
|
+
# assert self._wait_for_metadata is not None
|
|
286
|
+
# metadata = self._wait_for_metadata()
|
|
287
|
+
# if metadata is not None:
|
|
288
|
+
# self.set_metadata(metadata)
|
|
289
|
+
# else:
|
|
290
|
+
# return
|
|
291
|
+
assert self._request_update is not None
|
|
292
|
+
|
|
293
|
+
self.ani = FuncAnimation(
|
|
294
|
+
self._fig,
|
|
295
|
+
self._update_animation,
|
|
296
|
+
# frames=x_axis_pts,
|
|
297
|
+
save_count=sys.maxsize,
|
|
298
|
+
# init_func=self._init_plots,
|
|
299
|
+
blit=False,
|
|
300
|
+
interval=200,
|
|
301
|
+
repeat=False,
|
|
302
|
+
)
|
|
303
|
+
plt.show()
|
|
304
|
+
|
|
305
|
+
def _update_animation(self, frame: int):
|
|
306
|
+
# print("calling update animation")
|
|
307
|
+
return self._request_update()
|
|
308
|
+
|
|
309
|
+
def _init_plots(self):
|
|
310
|
+
# plt.ion()
|
|
311
|
+
nrow, ncol = self._mgr.get_layout()
|
|
312
|
+
self._fig.subplots(nrow, ncol, gridspec_kw={"hspace": 0.5})
|
|
313
|
+
subplot_defns = self._mgr.subplots
|
|
314
|
+
if len(subplot_defns) != 1 and len(subplot_defns) % 2 == 1:
|
|
315
|
+
self._fig.delaxes(self._fig.axes[-1])
|
|
316
|
+
|
|
317
|
+
# Add labels and legends
|
|
318
|
+
for axes, subplot_defn in zip(self._fig.axes, subplot_defns):
|
|
319
|
+
axes.set_title(subplot_defn.title, fontsize=8)
|
|
320
|
+
if subplot_defn.is_log_y:
|
|
321
|
+
axes.set_yscale("log")
|
|
322
|
+
axes.set_xlabel(subplot_defn.x_axis_label, fontsize=8)
|
|
323
|
+
axes.set_ylabel(subplot_defn.y_axis_label, fontsize=8)
|
|
324
|
+
lines = []
|
|
325
|
+
for label in subplot_defn.series_labels:
|
|
326
|
+
(ln,) = axes.plot([], [], label=label)
|
|
327
|
+
lines.append(ln)
|
|
328
|
+
axes.legend(fontsize=6)
|
|
329
|
+
|
|
330
|
+
# Set arbitrary axes limits
|
|
331
|
+
if self._metadata.is_transient:
|
|
332
|
+
axes.set_xlim(0.0, 1.0)
|
|
333
|
+
else:
|
|
334
|
+
axes.set_xlim(1, 5)
|
|
335
|
+
if subplot_defn.is_log_y:
|
|
336
|
+
axes.set_ylim(1e-20, 1)
|
|
337
|
+
else:
|
|
338
|
+
axes.set_ylim(0, 1.0)
|
|
339
|
+
|
|
340
|
+
# Keep hold of lines so that we can assign data when it's available
|
|
341
|
+
self._subplot_lines.append(lines)
|
|
342
|
+
# The limits on this subplot are essentially unset until we start getting data
|
|
343
|
+
self._subplot_limits_set.append(False)
|
|
@@ -119,6 +119,7 @@ class SycGrpc(object):
|
|
|
119
119
|
working_dir = kwargs.pop("working_dir", None)
|
|
120
120
|
port = kwargs.pop("port", None)
|
|
121
121
|
version = kwargs.pop("version", None)
|
|
122
|
+
start_output = kwargs.pop("start_output", None)
|
|
122
123
|
|
|
123
124
|
if os.environ.get("SYC_LAUNCH_CONTAINER") == "1":
|
|
124
125
|
mounted_from = working_dir if working_dir else "./"
|
|
@@ -137,6 +138,8 @@ class SycGrpc(object):
|
|
|
137
138
|
)
|
|
138
139
|
LOG.debug("...started")
|
|
139
140
|
self._connect(_LOCALHOST_IP, port)
|
|
141
|
+
if start_output:
|
|
142
|
+
self.start_output()
|
|
140
143
|
|
|
141
144
|
def start_container_and_connect(
|
|
142
145
|
self,
|
|
@@ -153,7 +156,7 @@ class SycGrpc(object):
|
|
|
153
156
|
LOG.debug("...started")
|
|
154
157
|
self._connect(_LOCALHOST_IP, port)
|
|
155
158
|
|
|
156
|
-
def start_pim_and_connect(self, version: str = None):
|
|
159
|
+
def start_pim_and_connect(self, version: str = None, start_output: bool = False):
|
|
157
160
|
"""Start PIM-managed instance.
|
|
158
161
|
|
|
159
162
|
Currently for internal use only.
|
|
@@ -171,6 +174,8 @@ class SycGrpc(object):
|
|
|
171
174
|
self.__pim_instance = instance
|
|
172
175
|
channel = instance.build_grpc_channel()
|
|
173
176
|
self._connect(channel=channel)
|
|
177
|
+
if start_output:
|
|
178
|
+
self.start_output()
|
|
174
179
|
|
|
175
180
|
def upload_file(self, *args, **kwargs):
|
|
176
181
|
"""Supports file upload to remote instance.
|