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.

Files changed (33) hide show
  1. ansys/systemcoupling/core/__init__.py +11 -5
  2. ansys/systemcoupling/core/adaptor/api_23_1/show_plot.py +75 -0
  3. ansys/systemcoupling/core/adaptor/api_23_1/solution_root.py +7 -1
  4. ansys/systemcoupling/core/adaptor/api_23_2/show_plot.py +75 -0
  5. ansys/systemcoupling/core/adaptor/api_23_2/solution_root.py +7 -1
  6. ansys/systemcoupling/core/adaptor/api_24_1/show_plot.py +75 -0
  7. ansys/systemcoupling/core/adaptor/api_24_1/solution_root.py +7 -1
  8. ansys/systemcoupling/core/adaptor/api_24_2/add_participant.py +4 -4
  9. ansys/systemcoupling/core/adaptor/api_24_2/setup_root.py +1 -1
  10. ansys/systemcoupling/core/adaptor/api_24_2/show_plot.py +75 -0
  11. ansys/systemcoupling/core/adaptor/api_24_2/solution_root.py +7 -1
  12. ansys/systemcoupling/core/adaptor/impl/injected_commands.py +215 -32
  13. ansys/systemcoupling/core/adaptor/impl/static_info.py +17 -0
  14. ansys/systemcoupling/core/adaptor/impl/syc_proxy.py +3 -0
  15. ansys/systemcoupling/core/adaptor/impl/syc_proxy_interface.py +4 -0
  16. ansys/systemcoupling/core/adaptor/impl/types.py +1 -1
  17. ansys/systemcoupling/core/charts/chart_datatypes.py +169 -0
  18. ansys/systemcoupling/core/charts/csv_chartdata.py +299 -0
  19. ansys/systemcoupling/core/charts/live_csv_datasource.py +87 -0
  20. ansys/systemcoupling/core/charts/message_dispatcher.py +84 -0
  21. ansys/systemcoupling/core/charts/plot_functions.py +92 -0
  22. ansys/systemcoupling/core/charts/plotdefinition_manager.py +303 -0
  23. ansys/systemcoupling/core/charts/plotter.py +343 -0
  24. ansys/systemcoupling/core/client/grpc_client.py +6 -1
  25. ansys/systemcoupling/core/participant/manager.py +25 -9
  26. ansys/systemcoupling/core/participant/protocol.py +1 -0
  27. ansys/systemcoupling/core/session.py +4 -4
  28. ansys/systemcoupling/core/syc_version.py +1 -1
  29. ansys/systemcoupling/core/util/file_transfer.py +4 -0
  30. {ansys_systemcoupling_core-0.5.0.dist-info → ansys_systemcoupling_core-0.6.dist-info}/METADATA +11 -10
  31. {ansys_systemcoupling_core-0.5.0.dist-info → ansys_systemcoupling_core-0.6.dist-info}/RECORD +33 -22
  32. {ansys_systemcoupling_core-0.5.0.dist-info → ansys_systemcoupling_core-0.6.dist-info}/LICENSE +0 -0
  33. {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.