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 22 commits into
base: main
Choose a base branch
from

Conversation

emilkovacev
Copy link
Contributor

@emilkovacev emilkovacev commented Oct 1, 2024

Summary

Closes #13106

Adds non-breaking interface to dag_drawer() to allow Qiskit users to customize the colors of DAGs.

Details and comments

I created stylesheets for dag_drawer(), that works similarly to how circuit_drawer does.

Here is an example:

from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.converters import circuit_to_dag
from qiskit.visualization import dag_drawer
 

q = QuantumRegister(3, 'q')
c = ClassicalRegister(3, 'c')
circ = QuantumCircuit(q, c)
circ.h(q[0])
circ.cx(q[0], q[1])
circ.measure(q[0], c[0])
circ.rz(0.5, q[1]).c_if(c, 2)

dag = circuit_to_dag(circ)

style = {
    # "fontsize": 12,
    # "bgcolor": "white",
    # "dpi": 10,
    # "pad": 0,
    # "nodecolor": "green",
    "inputnodecolor": "pink",
    # "inputnodefontcolor": "white",
    "outputnodecolor": "lightblue",
    # "outputnodefontcolor": "white",
    "opnodecolor": "red",
    # "opnodefontcolor": "white",
    # "edgecolor": "black",
    # "qubitedgecolor": "red",
    # "clbitedgecolor": "black",
}

dag_drawer(dag, style=style)

The code above generates this DAG:

Screenshot 2024-10-09 at 11 39 46 AM

@emilkovacev emilkovacev requested review from nonhermitian and a team as code owners October 1, 2024 21:25
@qiskit-bot qiskit-bot added the Community PR PRs from contributors that are not 'members' of the Qiskit repo label Oct 1, 2024
@qiskit-bot
Copy link
Collaborator

Thank you for opening a new pull request.

Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient.

While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone.

One or more of the following people are relevant to this code:

@coveralls
Copy link

coveralls commented Oct 1, 2024

Pull Request Test Coverage Report for Build 12821657503

Details

  • 18 of 167 (10.78%) changed or added relevant lines in 2 files are covered.
  • 22 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.09%) to 88.829%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/visualization/dag/dagstyle.py 16 63 25.4%
qiskit/visualization/dag_visualization.py 2 104 1.92%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/lex.rs 3 91.48%
qiskit/visualization/dag_visualization.py 7 9.64%
crates/qasm2/src/parse.rs 12 96.69%
Totals Coverage Status
Change from base Build 12813106471: -0.09%
Covered Lines: 79457
Relevant Lines: 89449

💛 - Coveralls

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

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

Thanks for getting this started. I left an inline comment about the high level interface. I'm a bit concerned with how low level the current interface is and that it would require understanding how graphviz is used internally by dag_drawer

qiskit/visualization/dag_visualization.py Outdated Show resolved Hide resolved
@emilkovacev emilkovacev requested a review from mtreinish October 13, 2024 03:06
@raynelfss raynelfss added this to the 1.3.0 milestone Oct 31, 2024
@raynelfss raynelfss removed the Community PR PRs from contributors that are not 'members' of the Qiskit repo label Oct 31, 2024
@raynelfss raynelfss added Changelog: New Feature Include in the "Added" section of the changelog mod: visualization qiskit.visualization mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library and removed mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library labels Oct 31, 2024
@raynelfss raynelfss self-assigned this Oct 31, 2024
Copy link
Contributor

@raynelfss raynelfss left a comment

Choose a reason for hiding this comment

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

This looks great, thank you for these additions. I wonder whether we should consider using FileSpecifier (such as File, Path, etc) as an alternative input for load_style(). I feel like it might benefit from not having to check whether the file exists or not. See the comments in my review.

else:
style_name = style
else:
raise exceptions.VisualizationError("Invalid style {style}")
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this might have been an f-string?

Suggested change
raise exceptions.VisualizationError("Invalid style {style}")
raise exceptions.VisualizationError(f"Invalid style {style}")

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)

Comment on lines +163 to +164
if style_name in ["color"]:
current_style = DefaultStyle().style
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe compere the strings directly here?

Suggested change
if style_name in ["color"]:
current_style = DefaultStyle().style
if style_name == "color":
current_style = DefaultStyle().style

Comment on lines +165 to +201
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
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.

Comment on lines -167 to -173
if node.name == "barrier":
n["style"] = "filled"
n["fillcolor"] = "grey"
elif getattr(node.op, "_directive", False):
n["style"] = "filled"
n["fillcolor"] = "red"
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why these two conditions were removed here?

@raynelfss raynelfss modified the milestones: 1.3.0, 2.0.0 Nov 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: visualization qiskit.visualization
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Allow for changing colors in dag circuit drawer
5 participants