# -*- coding: utf-8 -*-
# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""Generator of the JSON file defining the coupling structure used by the N2 chart."""
from __future__ import unicode_literals
import json
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
List,
Mapping,
Optional,
Sequence,
Tuple,
Union,
)
from jinja2 import Template
if TYPE_CHECKING:
from gemseo.core.coupling_structure import DependencyGraph
from gemseo.core.discipline import MDODiscipline
[docs]class N2JSON(object):
"""The JSON structure to be used by the D3.js-based N2 chart."""
_DEFAULT_GROUP_TEMPLATE = "Group {}"
_DEFAULT_WEAKLY_COUPLED_DISCIPLINES = "Weakly coupled disciplines"
def __init__(
self,
graph, # type: DependencyGraph
): # type: (...) -> None
"""
Args:
graph: The dependency graph.
"""
self._graph = graph
self.__disciplines = list(graph.disciplines)
data = {}
self.__disciplines_names = self._get_disciplines_names()
couplings = self._graph.get_disciplines_couplings()
groups, n_groups, data["children"] = self._compute_groups(
self.__disciplines_names
)
variables_sizes = self._compute_variables_sizes()
data["nodes"], data["groups"] = self._create_nodes(
groups,
variables_sizes,
self.__disciplines_names,
n_groups,
data["children"],
)
data["links"] = self._create_links(
couplings,
len(data["nodes"]),
variables_sizes,
self.__disciplines_names,
n_groups,
)
data["disciplines"] = self._create_groups_menu_html(
self.__disciplines_names, data["children"], data["groups"]
)
self.__json = json.dumps(data, sort_keys=True)
def __str__(self):
return self.__json
@staticmethod
def _create_variables_html(
names, # type: Iterable[str]
variables_sizes=None, # type: Optional[Mapping[str,int]]
): # type: (...) -> str
"""Generate the HTML representation of variables from their names and sizes.
Args:
names: The names of the variables.
variables_sizes: The sizes of the variables.
If None, display only the names.
Return:
The HTML representation of the sorted variables.
"""
variables = [
{
"name": name,
"size": None
if variables_sizes is None
else variables_sizes.get(name, 1),
}
for name in sorted(names)
]
return Template(
"<div align='center'>"
" <table>"
" {%- for variable in variables %}"
" <tr>"
" <td>{{ variable.name }}</td>"
" {%- if variable.size is not none -%}"
" <td>({{ variable.size }})</td>"
" {%- endif %}"
" </tr>"
" {%- endfor %}"
" </table>"
"</div>"
).render(variables=variables)
@classmethod
def _create_coupling_html(
cls,
source, # type: str
destination, # type: str
coupling_names, # type: Iterable[str]
variables_sizes, # type: Mapping[str,int]
): # type: (...) -> str
"""Generate the HTML representation of a bi-disciplinary coupling.
Args:
source: The name of the source discipline.
destination: The name of the destination discipline.
coupling_names: The names of the coupling variables.
variables_sizes: The sizes of the variables.
Returns:
The HTML block describing this bi-disciplinary coupling.
"""
return Template(
(
"<h2 style='text-align: center;'>"
"<span style='color: gray;'>Coupling</span></br>"
"<span style='color: gray;'>from</span> {{ source }}<br/>"
"<span style='color: gray;'>to</span> {{ destination }}</h2>"
"{{ coupling_variables }}"
)
).render(
source=source,
destination=destination,
coupling_variables=cls._create_variables_html(
coupling_names, variables_sizes
),
)
@classmethod
def _create_discipline_html(
cls,
discipline, # type: MDODiscipline
variables_sizes, # type: Mapping[str,int]
): # type: (...) -> str
"""Generate the HTML representation of a discipline.
Args:
discipline: The discipline.
variables_sizes: The sizes of the variables.
Returns:
The HTML block describing the discipline.
"""
html_inputs_names = cls._create_variables_html(
discipline.get_input_data_names(), variables_sizes
)
html_outputs_names = cls._create_variables_html(
discipline.get_output_data_names(), variables_sizes
)
return Template(
(
"<h2 style='text-align: center;'>{{ discipline }}</h2>"
"<h3 style='text-align: center;'>Inputs</h3>"
"{{ inputs }}"
"<h3 style='text-align: center;'>Outputs</h3>"
"{{ outputs }}"
)
).render(
discipline=discipline.name,
inputs=html_inputs_names,
outputs=html_outputs_names,
)
@classmethod
def _create_group_html(
cls,
group, # type: int
disciplines, # type: Sequence[str]
n_groups, # type: int
children, # type: Sequence[Sequence[int]]
): # type: (...) -> str
"""Generate the HTML representation of a group of disciplines.
Args:
group: The index of the group of disciplines.
disciplines: The names of the disciplines.
n_groups: The number of groups.
children: The indices of the disciplines for the different groups.
Returns:
The HTML block describing the group of disciplines.
"""
disciplines = [disciplines[child - n_groups] for child in children[group]]
return Template(
"<h2 style='text-align: center;'>{{ group }}</h2>{{ disciplines }}"
).render(
group=cls._DEFAULT_GROUP_TEMPLATE.format(group),
disciplines=cls._create_variables_html(disciplines),
)
@staticmethod
def _create_groups_menu_html(
disciplines, # type: Sequence[str]
children, # type: Sequence[Sequence[int]]
groups, # type: Sequence[str]
): # type: (...) -> str
"""Generate the HTML representation of the right menu related to the groups.
Args:
disciplines: The names of the disciplines.
children: The indices of the disciplines for the different groups.
groups: The names of the groups.
Returns:
The HTML block used to collapse, expand and visualize groups.
"""
data = []
n_groups = len(groups)
for group_index, disciplines_indices in enumerate(children):
disciplines_names = [
disciplines[discipline_index - n_groups]
for discipline_index in disciplines_indices
]
data.append(
{
"disciplines": sorted(disciplines_names),
"group_index": group_index,
"group_name": groups[group_index],
}
)
return Template(
"<h2>Group the disciplines</h2>"
"<div style='overflow:scroll; height:39em;'>"
" <ul id='myUL'>"
" {%- for datum in data %}"
" <li>"
" <div class='caret'>"
" <span id='group_name_{{ datum.group_index }}' contenteditable='true' class='group' onblur='change_group_name(this,{{ datum.group_index }});'>{{ datum.group_name }}</span>" # noqa: B950
" {%- if datum.group_index != 0 %}"
" <input type='checkbox' onclick='expand_collapse_group({{ datum.group_index }},svg)'/>" # noqa: B950
" {%- endif %}"
" </div>"
" <ul class='nested'>"
" {%- for discipline in datum.disciplines %}"
" <li>{{ discipline }}</li>"
" {%- endfor %}"
" </ul>"
" </li>"
" {%- endfor %}"
" </ul>"
"</div>"
).render(data=data)
def _create_links(
self,
couplings, # type: Iterable[Tuple[MDODiscipline,MDODiscipline,Sequence[str]]]
n_nodes, # type: int
variables_sizes, # type: Mapping[str,int]
disciplines, # type: Sequence[str]
n_groups, # type: int
): # type: (...) -> List[Dict[str,Union[int,str]]]
"""Create the links.
Args:
couplings: The couplings.
n_nodes: The number of nodes.
variables_sizes: The sizes of the variables.
disciplines: The names of the disciplines.
n_groups: The number of groups.
Returns:
The existing links between disciplines,
defined by a source discipline
a target discipline,
a value quantifying the degree of this relationship,
and an HTML representation.
"""
links = []
for link in couplings:
source, target, variables = link
if variables:
source_index = n_groups + disciplines.index(source.name)
target_index = n_groups + disciplines.index(target.name)
links.append(
{
"source": source_index,
"target": target_index,
"value": len(variables),
"description": self._create_coupling_html(
source.name, target.name, variables, variables_sizes
),
}
)
for index in range(n_nodes):
links.append(
{
"source": index,
"target": index,
"value": 1,
"description": "",
}
)
return links
def _create_nodes(
self,
group, # type: Mapping[str,int],
variables_sizes, # type: Mapping[str,int],
disciplines, # type: Sequence[str]
n_groups, # type: int
children, # type: Sequence[Sequence[int]]
): # type: (...) -> Tuple[List[Dict[str,Union[int,str,bool]]],List[str]]
"""Create the nodes representing either a discipline or a disciplines group.
Args:
group: The indices of the groups to which the disciplines belong.
variables_sizes: The sizes of the variables.
disciplines: The names of the disciplines.
n_groups: The number of groups.
children: The indices of the disciplines for the different groups.
Returns:
The existing nodes,
defined by a name,
a group index,
an HTML representation
a target discipline,
a value quantifying the degree of this relationship,
an HTML representation,
and whether the node represents a group.
"""
disciplines_nodes = [
{
"name": discipline.name,
"group": group[discipline.name],
"description": self._create_discipline_html(
discipline, variables_sizes
),
"is_group": False,
}
for discipline in self.__disciplines
]
groups_nodes = [
{
"name": self._DEFAULT_GROUP_TEMPLATE.format(group_index),
"group": group_index,
"description": self._create_group_html(
group_index, disciplines, n_groups, children
),
"is_group": True,
}
for group_index in range(n_groups)
]
groups_names = [
self._DEFAULT_GROUP_TEMPLATE.format(group_index)
for group_index in range(1, n_groups)
]
groups_names = [self._DEFAULT_WEAKLY_COUPLED_DISCIPLINES] + groups_names
return groups_nodes + disciplines_nodes, groups_names
def _compute_groups(
self,
disciplines, # type: Sequence[str]
): # type: (...) -> Tuple[Dict[str,int],int,List[List[int]]]
"""Compute the groups and the children.
Args:
disciplines: The names of the disciplines
Returns:
The groups to which the disciplines belong,
the number of groups,
and the indices of the disciplines for the different groups.
"""
children = [[]]
n_groups = 1
groups = {}
for parallel_tasks in self._graph.get_execution_sequence():
for components in parallel_tasks:
if len(components) > 1:
n_groups += 1
children.append([])
for component in components:
index = disciplines.index(component.name)
children[-1].append(index)
groups[component.name] = n_groups - 1
else:
index = disciplines.index(components[0].name)
groups[components[0].name] = 0
children[0].append(index)
indices = []
new_children = []
index = 0
for group in children:
new_children.append([])
for child in group:
new_children[-1].append(index)
index += 1
indices.append(child)
self.__disciplines = [self.__disciplines[index] for index in indices]
self.__disciplines_names = [
self.__disciplines_names[index] for index in indices
]
new_children = [[child + n_groups for child in group] for group in new_children]
return groups, n_groups, new_children
def _compute_variables_sizes(self): # type: (...) -> Dict[str,int]
"""Compute the sizes of the coupling variables.
Returns:
The names of the coupling variables bound to their sizes.
"""
variables_sizes = {}
for discipline in self.__disciplines:
for name in discipline.get_input_data_names():
variables_sizes[name] = len(discipline.default_inputs.get(name, [1]))
return variables_sizes
def _get_disciplines_names(self): # type: (...) -> List[str]
"""Return the names of the disciplines."""
return [discipline.name for discipline in self.__disciplines]