Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change style of DAG generated in dag_drawer() #13253

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e6e5caa
Added 'custom' style that exposes graphviz customization functions fo…
emilkovacev Oct 1, 2024
3000fb8
Added type hints to function calls
emilkovacev Oct 1, 2024
b92bfb2
Added docs
emilkovacev Oct 1, 2024
fd6a8a9
Adds tests for dag_drawer
emilkovacev Oct 1, 2024
9ca881a
Fixed invalid exceptions caught by test
emilkovacev Oct 1, 2024
68cd6f3
ran formatter and linter on code
emilkovacev Oct 1, 2024
f0c8336
Merge branch 'main' into main
emilkovacev Oct 1, 2024
a8a5c6a
Formatted code and test to abide by pylint standards
emilkovacev Oct 2, 2024
4bae3af
Merge branch 'main' of github.com:emilkovacev/qiskit
emilkovacev Oct 2, 2024
42680e1
Fix typo in docs for graph_attr kwarg
emilkovacev Oct 2, 2024
a450cbc
placed functionality into stylesheets
emilkovacev Oct 9, 2024
a32493a
Added error correction to dag visualization
emilkovacev Oct 9, 2024
7f29944
rewrote tests to work with new stylesheet method
emilkovacev Oct 9, 2024
59870e2
Added color templates for DAGs
emilkovacev Oct 9, 2024
c9ddc22
Re-format docs for dag_visualization
emilkovacev Oct 9, 2024
37fdee4
reformatted dag_visualization.py
emilkovacev Oct 9, 2024
7f118bb
Ran linting
emilkovacev Oct 9, 2024
617d0ea
Added artifacts for docs to recognize stylesheets
emilkovacev Oct 9, 2024
ceab704
Fixed errors to match original invocation of dag_drawer()
emilkovacev Oct 9, 2024
616c67e
Fixed small import formatting issue
emilkovacev Oct 9, 2024
379c4e1
Adds release notes for PR
emilkovacev Oct 12, 2024
1079dd8
Merge branch 'main' of github.com:Qiskit/qiskit
emilkovacev Jan 17, 2025
dfba4dd
Modified DagDependency code to match previous impl
emilkovacev Jan 20, 2025
5064f2c
Apply suggestions from code review
emilkovacev Jan 20, 2025
2071945
Merge branch 'main' of github.com:Qiskit/qiskit
emilkovacev Jan 20, 2025
6a985c3
Merge branch 'main' of github.com:emilkovacev/qiskit
emilkovacev Jan 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include requirements.txt
recursive-include qiskit/qasm/libs *.inc
include qiskit/VERSION.txt
include qiskit/visualization/circuit/styles/*.json
include qiskit/visualization/dag/styles/*.json
recursive-include qiskit/providers/fake_provider/backends_v1 *.json

# Include the tests files.
Expand Down
13 changes: 13 additions & 0 deletions qiskit/visualization/dag/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2014.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Empty init for data directory."""
211 changes: 211 additions & 0 deletions qiskit/visualization/dag/dagstyle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2019.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Matplotlib circuit visualization style."""

from __future__ import annotations

import json
import os
from typing import Any
from pathlib import Path
from warnings import warn
from qiskit.visualization import exceptions


class StyleDict(dict):
"""A dictionary for graphviz styles.

Defines additional abbreviations for key accesses, such as allowing
``"ec"`` instead of writing ``"edgecolor"``.
"""

VALID_FIELDS = {
"name",
"fontsize",
"bgcolor",
"dpi",
"pad",
"nodecolor",
"inputnodecolor",
"inputnodefontcolor",
"outputnodecolor",
"outputnodefontcolor",
"opnodecolor",
"opnodefontcolor",
"edgecolor",
"qubitedgecolor",
"clbitedgecolor",
}

ABBREVIATIONS = {
"nc": "nodecolor",
"ic": "inputnodecolor",
"if": "inputnodefontcolor",
"oc": "outputnodecolor",
"of": "outputnodefontcolor",
"opc": "opnodecolor",
"opf": "opnodefontcolor",
"ec": "edgecolor",
"qec": "qubitedgecolor",
"clc": "clbitedgecolor",
}

def __setitem__(self, key: Any, value: Any) -> None:
# allow using field abbreviations
if key in self.ABBREVIATIONS:
key = self.ABBREVIATIONS[key]

if key not in self.VALID_FIELDS:
warn(
f"style option ({key}) is not supported",
UserWarning,
2,
)
return super().__setitem__(key, value)

def __getitem__(self, key: Any) -> Any:
# allow using field abbreviations
if key in self.ABBREVIATIONS:
key = self.ABBREVIATIONS[key]

return super().__getitem__(key)

def update(self, other):
super().update((key, value) for key, value in other.items())


class DefaultStyle:
"""Creates a Default Style dictionary

The style dict contains numerous options that define the style of the
output circuit visualization. The style dict is used by the `mpl` or
`latex` output. The options available in the style dict are defined below:

Attributes:
name (str): The name of the style.
fontsize (str): The font size to use for text.
bgcolor (str): The color name to use for the background ('red', 'green', etc.).
nodecolor (str): The color to use for all nodes.
dpi (int): The DPI to use for the output image.
pad (int): A number to adjust padding around output
graph.
inputnodecolor (str): The color to use for incoming wire nodes. Overrides
nodecolor for those nodes.
inputnodefontcolor (str): The font color to use for incoming wire nodes.
Overrides nodecolor for those nodes.
outputnodecolor (str): The color to use for output wire nodes. Overrides
nodecolor for those nodes.
outputnodefontcolor (str): The font color to use for output wire nodes.
Overrides nodecolor for those nodes.
opnodecolor (str): The color to use for Instruction nodes. Overrides
nodecolor for those nodes.
opnodefontcolor (str): The font color to use for Instruction nodes.
Overrides nodecolor for those nodes.

qubitedgecolor (str): The edge color for qubits. Overrides edgecolor for these edges.
clbitedgecolor (str): The edge color for clbits. Overrides edgecolor for these edges.
"""

def __init__(self):
default_style_dict = "color.json"
path = Path(__file__).parent / "styles" / default_style_dict

with open(path, "r") as infile:
default_style = json.load(infile)

# set shortcuts, such as "ec" for "edgecolor"
self.style = StyleDict(**default_style)


def load_style(style: dict | str = "color") -> StyleDict:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we instead be able to provide a file or directory here too? Since whenever we provide a string it searches for one. Perhaps we should be able to specify a specific Path or file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, that seems a lot cleaner. This function definition was taken from the load_style() function in qiskit/visualization/circuit/qcstyle.py, should that be changed too or is that outside of the scope for this PR?

(I also noticed that the docstring here needs updating, I'll fix that too)

"""Utility function to load style from json files.

Args:
style: Depending on the type, this acts differently:

* If a string, it can specify a supported style name (such
as "iqp" or "clifford"). It can also specify the name of
a custom color scheme stored as JSON file. This JSON file
_must_ specify a complete set of colors.
* If a dictionary, it may specify the style name via a
``{"name": "<desired style>"}`` entry. If this is not given,
the default style will be used. The remaining entries in the
dictionary can be used to override certain specs.
E.g. ``{"name": "iqp", "ec": "#FF0000"}`` will use the ``"iqp"``
color scheme but set the edgecolor to red.

Returns:
A tuple containing the style as dictionary and the default font ratio.
"""

# determine the style name which could also be inside a dictionary, like
# style={"name": "clifford", <other settings...>}
if isinstance(style, dict):
style_name = style.get("name", "color")
elif isinstance(style, str):
if style.endswith(".json"):
style_name = style[:-5]
else:
style_name = style
else:
raise exceptions.VisualizationError("Invalid style {style}")
emilkovacev marked this conversation as resolved.
Show resolved Hide resolved

if style_name in ["color"]:
current_style = DefaultStyle().style
emilkovacev marked this conversation as resolved.
Show resolved Hide resolved
else:
# Search for file in 'styles' dir, and then the current directory
style_name = style_name + ".json"
style_paths = []

default_path = Path(__file__).parent / "styles" / style_name
style_paths.append(default_path)

# check current directory
cwd_path = Path("") / style_name
style_paths.append(cwd_path)

for path in style_paths:
# expand ~ to the user directory and check if the file exists
exp_user = path.expanduser()
if os.path.isfile(exp_user):
try:
with open(exp_user) as infile:
json_style = json.load(infile)

current_style = StyleDict(json_style)
break
except json.JSONDecodeError as err:
warn(
f"Could not decode JSON in file '{path}': {str(err)}. "
"Will use default style.",
UserWarning,
2,
)
break
except (OSError, FileNotFoundError):
warn(
f"Error loading JSON file '{path}'. Will use default style.",
UserWarning,
2,
)
break
Comment on lines +165 to +201
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This here can get a little convoluted. While providing a string name for a style that might be located at the styles directory is a good idea. We should be able to directly provide an instance of File or Path so that we don't need to perform this search actively and the user can just provide the Path themselves.

Copy link
Contributor Author

@emilkovacev emilkovacev Nov 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, similar to before this code uses the same logic as the other instance of load_style() in qiskit/visualization/circuit/qcstyle.py, but with small changes to accommodate for the different use case.

else:
raise exceptions.VisualizationError(f"Invalid style {style}")

# if the style is a dictionary, update the defaults with the new values
# this _needs_ to happen after loading by name to cover cases like
# style = {"name": "bw", "edgecolor": "#FF0000"}
if isinstance(style, dict):
current_style.update(style)

return current_style
13 changes: 13 additions & 0 deletions qiskit/visualization/dag/styles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2014.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Empty init for data directory."""
6 changes: 6 additions & 0 deletions qiskit/visualization/dag/styles/color.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "color",
"inputnodecolor": "green",
"outputnodecolor": "red",
"opnodecolor": "lightblue"
}
6 changes: 6 additions & 0 deletions qiskit/visualization/dag/styles/plain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "plain",
"inputnodecolor": "white",
"outputnodecolor": "white",
"opnodecolor": "white"
}
Loading