Skip to content

Commit

Permalink
Convergence charts using matplotlib (#312)
Browse files Browse the repository at this point in the history
* initial commit - WIP

* add show_plot injected command

* expand charts unit tests

* rename class

* rename class - add missing files

* improve test coverage, including some refact to facilitate

* add plot to mocksolve case to improve coverage

* in PIM, copy chart CSV data into root working directory to allow file download access

* Rename file

* missed class rename

* fix argument

* fix bug with plot file copy in PIM case

* fix bug with plot file copy in PIM case (2nd attempt)

* fix another issue in plot file copy in PIM case

* temp fix for property query - to be replaced by proper fix from main branch

* terminology change 'diagnostic' -> 'transfer value'

* complete terminology change

* expose filtering of plots

* update 'show_plot' documentation

* fix import

* fix couple of issues with configuring what is show in plots

* some clean up in preparation for publishing PR

* more cleanup
  • Loading branch information
iboyd-ansys authored Jul 2, 2024
1 parent 8a07500 commit 38cbe7c
Show file tree
Hide file tree
Showing 14 changed files with 2,220 additions and 3 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies = [
"pyyaml",
"appdirs>=1.4.0",
"importlib-metadata>=4.0",
"matplotlib>=3.8.2",
]
classifiers = [
"Development Status :: 4 - Beta",
Expand Down
158 changes: 155 additions & 3 deletions src/ansys/systemcoupling/core/adaptor/impl/injected_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,19 @@
# SOFTWARE.

from copy import deepcopy
import os
import random
import time
from typing import Callable, Dict, Optional, Protocol

import ansys.platform.instancemanagement as pypim

from ansys.systemcoupling.core.charts.plot_functions import create_and_show_plot
from ansys.systemcoupling.core.charts.plotdefinition_manager import (
DataTransferSpec,
InterfaceSpec,
PlotSpec,
)
from ansys.systemcoupling.core.native_api import NativeApi
from ansys.systemcoupling.core.participant.manager import ParticipantManager
from ansys.systemcoupling.core.participant.mapdl import MapdlSystemCouplingInterface
Expand All @@ -32,7 +43,7 @@
from .types import Container


# We cannot import Session directly, so define a protocol for typing
# We cannot import Session directly, so define a protocol for typing.
# We mainly use it as a means of accessing the "API roots".
class SessionProtocol(Protocol):
case: Container
Expand Down Expand Up @@ -66,7 +77,6 @@ def get_injected_cmd_map(
"injected commands" that are returned from here are either *additional* commands
that have no counterpart in System Coupling or *overrides* to existing commands
that provide modified or extended behavior.
"""
ret = {}

Expand Down Expand Up @@ -94,6 +104,7 @@ def get_injected_cmd_map(
),
"interrupt": lambda **kwargs: rpc.interrupt(**kwargs),
"abort": lambda **kwargs: rpc.abort(**kwargs),
"show_plot": lambda **kwargs: _show_plot(session, **kwargs),
}

if category == "case":
Expand Down Expand Up @@ -154,8 +165,90 @@ def _wrap_solve(solution: Container, part_mgr: ParticipantManager) -> None:
part_mgr.solve()


def _ensure_file_available(session: SessionProtocol, filepath: str) -> str:
"""If we are in a "PIM" session, copies the file specified by ``filepath``
into the working directory, so that it is available for download.
A suffix is added to the name of the copy to make the name unique, and
the new name is returned.
"""
# Note: it is a general issue with files in a PIM session that they can
# only be uploaded to/downloaded from the root working directory. We
# might want to consider integrating something like this directly into
# the file_transfer module later so that it is more seamless.

if not pypim.is_configured():
return filepath

# Copy file to a unique name in the working directory
file_name = os.path.basename(filepath)
root_name, _, ext = file_name.rpartition(".")
ext = f".{ext}" if ext else ""
new_name = f"{root_name}_{int(time.time())}_{random.randint(1, 10000000)}{ext}"

session._native_api.ExecPythonString(
PythonString=f"import shutil\nshutil.copy('{filepath}', '{new_name}')"
)

session.download_file(new_name, ".")
return new_name


def _show_plot(session: SessionProtocol, **kwargs):
setup = session.setup
working_dir = kwargs.pop("working_dir", ".")
interface_name = kwargs.pop("interface_name", None)
if interface_name is None:
interfaces = setup.coupling_interface.get_object_names()
if len(interfaces) == 0:
return
if len(interfaces) > 1:
raise RuntimeError(
"show_plot() currently only supports a single interface."
)
interface_name = interfaces[0]
interface_object = setup.coupling_interface[interface_name]
interface_disp_name = interface_object.display_name

if (transfer_names := kwargs.pop("transfer_names", None)) is None:
transfer_names = interface_object.data_transfer.get_object_names()

if len(transfer_names) == 0:
return None

transfer_disp_names = [
interface_object.data_transfer[trans_name].display_name
for trans_name in transfer_names
]

show_convergence = kwargs.pop("show_convergence", True)
show_transfer_values = kwargs.pop("show_transfer_values", True)

# TODO : better way to do this?
is_transient = setup.solution_control.time_step_size is not None

file_path = _ensure_file_available(
session, os.path.join(working_dir, "SyC", f"{interface_name}.csv")
)

spec = PlotSpec()
intf_spec = InterfaceSpec(interface_name, interface_disp_name)
spec.interfaces.append(intf_spec)
for transfer in transfer_disp_names:
intf_spec.transfers.append(
DataTransferSpec(
display_name=transfer,
show_convergence=show_convergence,
show_transfer_values=show_transfer_values,
)
)
spec.plot_time = is_transient

return create_and_show_plot(spec, [file_path])


def get_injected_cmd_data() -> list:
"""Gets a list of injected command data in the right form to insert
"""Get a list of injected command data in the right form to insert
at a convenient point in the current processing.
Because the data returned data is always a new copy, it can be manipulated at will.
Expand Down Expand Up @@ -358,4 +451,63 @@ def get_injected_cmd_data() -> list:
pyname: clear_state
isInjected: true
pysyc_internal_name: _clear_state
- name: show_plot
pyname: show_plot
exposure: solution
isInjected: true
isQuery: false
retType: <class 'NoneType'>
doc: |-
Shows plots of transfer values and convergence for data transfers
of a coupling interface.
essentialArgNames:
- interface_name
optionalArgNames:
- transfer_names
- working_dir
- show_convergence
- show_transfer_values
defaults:
- None
- "."
- True
- True
args:
- #!!python/tuple
- interface_name
- pyname: interface_name
Type: <class 'str'>
type: String
doc: |-
Specification of which interface to plot.
- #!!python/tuple
- transfer_names
- pyname: transfer_names
Type: <class 'list'>
type: String List
doc: |-
Specification of which data transfers to plot. Defaults
to ``None``, which means plot all data transfers.
- #!!python/tuple
- working_dir
- pyname: working_dir
Type: <class 'str'>
type: String
doc: |-
Working directory (defaults = ".").
- #!!python/tuple
- show_convergence
- pyname: show_convergence
Type: <class 'bool'>
type: Logical
doc: |-
Whether to show convergence plots (defaults to ``True``).
- #!!python/tuple
- show_transfer_values
- pyname: show_transfer_values
Type: <class 'bool'>
type: Logical
doc: |-
Whether to show transfer value plots (defaults to ``True``).
"""
169 changes: 169 additions & 0 deletions src/ansys/systemcoupling/core/charts/chart_datatypes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional

"""Common data types for the storage of metadata and data for chart series.
All series are assumed to belong to an interface and to a data transfer within the interface
Raw data should be processed into this format from whatever source is available (CSV file
or streamed data, for example) and the actual charts will be built from this.
"""


class SeriesType(Enum):
CONVERGENCE = 1
SUM = 2
WEIGHTED_AVERAGE = 3


@dataclass
class TransferSeriesInfo:
"""Information about the chart series data associated with a data transfer.
Attributes
----------
data_index : int
This is used by the data source processor to associate this information (likely
obtained from heading or other metadata) with the correct data series.
It indexes into the full list of series associated with a given interface.
series_type : SeriesType
The type of line series.
transfer_display_name : str
The display name of the data transfer. This is a primary identifier for data
transfers because CSV data sources do not currently include information about
the underlying data model names of data transfers.
disambiguation_index: int
This should be set to 0, unless there is more than one data transfer with the
same display name. A contiguous range of indexes starting at 0 should be assigned
to the list of data transfers with the same display name.
participant_display_name : str, optional
The display name of the participant. This is required for transfer value series
but not for convergence series.
line_suffixes: list[str]
This should always be empty for convergence series. For transfer value series,
it should contain the suffixes for any component series that exist. That is,
suffixes for complex components, "real" or "imag", and suffixes for vector
components, "x", "y", "z", or a combination of complex and vector component
types. The data indexes for individual components of the transfer are assumed
to be contiguous from ``data_index``.
"""

data_index: int
series_type: SeriesType
transfer_display_name: str
disambiguation_index: int
# Remainder for non-CONVERGENCE series only
participant_display_name: Optional[str] = None
line_suffixes: list[str] = field(default_factory=list)


@dataclass
class InterfaceInfo:
"""Information about the chart series data associated with a single interface.
Attributes
----------
name : str
The name of the coupling interface data model object.
display_name : str, optional
The display name of the interface object. This may be unassigned initially.
is_transient : bool
Whether the data on this interface is associated with a transient analysis.
transfer_info : list[TransferSeriesInfo]
The list of ``TransferSeriesInfo`` associated with this interface.
"""

name: str
display_name: str = "" # TODO: check whether this is actually needed.
is_transient: bool = False
transfer_info: list[TransferSeriesInfo] = field(default_factory=list)


@dataclass
class SeriesData:
"""The plot data for a single chart line series and information to allow
it to be associated with chart metadata.
An instance of this type is assumed to be associated externally with a
single interface.
Attributes
----------
transfer_index : int
Index of the ``TransferSeriesInfo`` metadata for this series within the
``InterfaceInfo`` for the interface this series is associated with.
component_index : int, optional
The component index if this series is one of a set of complex and/or
vector components of the transfer. Otherwise is ``None``.
start_index : int, optional
The starting iteration of the ``data`` field. This defaults to 0 and
only needs to be set to a different value if incremental data, such
as might arise during "live" update of plots, has become available.
data : list[float]
The series data. This is always indexed by iteration. Extract time
step-based data by using a time step to iteration mapping.
"""

transfer_index: int # Index into transfer_info of associated InterfaceInfo
component_index: Optional[int] = None # Component index if applicable

start_index: int = 0 # Use when providing incremental data

data: list[float] = field(default_factory=list)


@dataclass
class InterfaceSeriesData:
"""The series data for given interface.
Attributes
----------
info : InterfaceInfo
The metadata for the interface.
series : list[SeriesData]
The data for all series associated with the interface.
"""

info: InterfaceInfo
series: list[SeriesData] = field(default_factory=list)


@dataclass
class TimestepData:
"""Mappings from iteration to time step and time.
Attributes
----------
timestep : list[int]
Timestep indexes, indexed by iteration. Typically, multiple consecutive
iteration indexes map to the same timestep index.
time: list[float]
Time values, indexed by iteration. Typically, multiple consecutive iteration
indexes map to the same time value.
"""

timestep: list[int] = field(default_factory=list) # iter -> step index
time: list[float] = field(default_factory=list) # iter -> time
Loading

0 comments on commit 38cbe7c

Please sign in to comment.