-
Notifications
You must be signed in to change notification settings - Fork 11
feat: add new Subcircuit #1289
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
base: main
Are you sure you want to change the base?
feat: add new Subcircuit #1289
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1289 +/- ##
==========================================
- Coverage 79.53% 78.76% -0.77%
==========================================
Files 160 161 +1
Lines 20571 21293 +722
Branches 19605 20327 +722
==========================================
+ Hits 16361 16772 +411
- Misses 3227 3527 +300
- Partials 983 994 +11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
acl-cqc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't quite followed all of this but most of it seems good, @lmondada I am proposing that I can do the alterations if you thumbs up / down my comments? Tho there are one or two that I haven't understood i.e. questions that perhaps you can answer!
| // Public API exports | ||
| pub use flow::{DefaultResourceFlow, ResourceFlow, UnsupportedOp}; | ||
| pub use scope::{ResourceScope, ResourceScopeConfig}; | ||
| pub use scope::ResourceScope; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there good reason to drop this public export?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, feel free to leave it :)
| } | ||
|
|
||
| /// Get the port of node on the given resource path. | ||
| /// Get the ports of node with the given opvalue in the given direction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with the given [CircuitUnit], I guess, but I'm more puzzled - why might there be more than one?
get_resource_port handles the linear case, and limits to at-most-one.
So I presume this means you might have multiple classical ports with the same unit....ah, if you are asking for inputs, and multiple inputs are wired from the same classical port? (But it'll never be >1 for Direction::Outgoing?)
Is that right? If so I can just add a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is exactly the point. For copyable types, you may have more than one incoming port connected to the same value (identified by (Node, OutgoingPort))
| .expect("linear resource") | ||
| } | ||
|
|
||
| /// Get the resource ID at the given port of the given node. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self - so this has just moved
| /// Create a new ResourceId. | ||
| /// | ||
| /// This method should only be called by ResourceAllocator and tests. | ||
| /// ResourceIds should typically be obtained from [`ResourceAllocator`]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll put this in some #[cfg(test)]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
|
|
||
| /// All copyable wires on the ports of `node` in the given direction. | ||
| pub fn get_copyable_wires( | ||
| pub fn all_copyable_wires( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I note this is called only once, with Direction::Outgoing, for which case it is exactly the same as get_copyable_ports.
Whereas all_copyable_wires(...., Direction::Incoming), which is never used, there is nothing to tell you which port of node the specified Wire is connected to...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I suggest either dropping this one (have just get_copyable_ports), or have two methods, one for inputs returning (IncomingPort, Wire)s and one for outputs returning just Wires.
(There is only one case where you pass a non-constant direction to get_copyable_ports and I am minded to rewrite that...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes good spot, this is redundant. I agree that get_copyable_ports is sufficient.
| if other_inputs.iter().flatten().any(|(n, p)| { | ||
| circuit | ||
| .get_circuit_unit(*n, *p) | ||
| .is_none_or(|u| u.is_resource()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean is_some_and here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise we are just gonna error if any of them is_none...that might be what we want?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think this is correct: None would mean that the port is not valid (or that there is something wrong with circuit). The error could be refined to "resource or unknown input found after copyable inputs", or something like that. Technically, you could even panic if get_circuit_unit is None, because that means we have an invalid port somehow...
| .peeking_take_while(|&(node, port)| { | ||
| circuit | ||
| .get_circuit_unit(node, port) | ||
| .is_some_and(|unit| unit.is_resource()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here and elsewhere - change circ.get_circuit_unit(n, p).is_some_and(|unit| unit.is_resource()) to circ.get_resource_id(n,p),is_some() ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, it's a bit shorter, so might be a good idea. I find the current version quite readable, but choose the one that you prefer :)
| let mut subcircuit = Self::try_from_resource_nodes(resource_nodes, circuit)?; | ||
|
|
||
| // Now adjust the boundary of subcircuit to match the subgraph | ||
| let (resource_inputs, copyable_inputs) = parse_input_boundary(subgraph, circuit)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hang on - do we expect linears to precede copyables for an arbitrary SiblingSubgraph??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes -- parse_input_boundary will return an error if that is not the case. This is for round-tripping: any SiblingSubgraph that is constructed from a Subcircuit will be of this form. By only accepting SiblingSubgraphs that satisfy this constraint, we make sure we never shuffle the boundary and thus surprise the user.
Admittedly this behaviour must be added to the docstring!! Good spot, thank you :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that it would be easy to add a method to SiblingSubgraph to explicitly shuffle the boundary to satisfy this condition.
| .collect_vec(); | ||
| for (node, port) in remove_inputs { | ||
| subcircuit | ||
| .try_remove_copyable_input(node, port, circuit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gonna think about this. I'm worried some removals might fail because you haven't done the additions yet / in the right order or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the comment I added at the definition site of try_remove_copyable_input should help. This is safe because all ports in remove_inputs should be covered by other inputs (added when subcircuit.input_copyable was extended above). These other ports are not removed (and would fail to be removed if attempted). So the order in which these "redundant" (covered) inputs are removed is not important.
| /// | ||
| /// This is possible when the input can be expressed as a function of other | ||
| /// inputs or computations within the subcircuit. | ||
| pub fn try_remove_copyable_input( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is pretty tough. Haven't got my head round it yet (apologies, I think I'm still less than 100% well)...I'm puzzled how any input was an input if it can be expressed as a function of the other inputs - that sounds nonconvex? I would think removing an input would require either
- extending the subgraph outwards (to incorporate the node computing the old/removed input, thus gaining that new node's inputs, and also removing any other inputs that were outputs of the same new node)
- shrinking the subgraph inwards, i.e. we lose the nodes that consumed that the old/removed input, and add inputs equal to the outputs of the nodes that were lost
However, I think you may be addressing something much more of a special case, in which case - do we want this to be pub?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your intuition is correct. More specifically, the behaviour here is close to your first bullet point. Let me explain.
First of all, note that this only makes sense for classical inputs. In that case, it might be that a port is an input AND the input P is "covered" by other inputs, i.e. any path from a node outside of the subcircuit to P contains another input port (in the past of P).
This is the only case where removing the input P will be successful---and is what this method attempts to do.
Should this be public? Probably not, as I would not expect that users would have to modify the boundary with this level of detail. At the same time, it should never do something invalid, so the user shouldn't be able to "shoot themselves in the foot" if they used it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be helpful if I rewrote this function, with more comments and possibly breaking some stuff into helper functions?
| #[derive(Debug, Clone, PartialEq)] | ||
| pub struct Subcircuit<N: HugrNode = hugr::Node> { | ||
| /// The resource intervals making up the resource part of the subcircuit | ||
| intervals: Vec<Interval<N>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need the ordering, right - we can't change this to a Map<ResourceId, Interval>?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ordering defines the order of the boundary in SiblingSubgraph, and ultimate the rewrites. It is important because circuit rewrites map the i-th input/output of the subgraph to the i-th input/output of the replacement circuit.
That being said, you could use an IndexMap? That would both preserve the ordering as well as being more explicit about the map relationship.
| intervals: Vec<Interval<N>>, | ||
| /// The subcircuit inputs that are copyable values. | ||
| /// | ||
| /// The copyable expressions within the subcircuit can be computed on the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this! 👍 👍 Storing all the CopyableExprs would entail duplicating all the shared nodes, and indeed, storing all the nodes on the way to the output boundary. This is much better!
|
Hi Alan! Thank you so much, I'll have a look and answer tomorrow, hope that's okay!! Sorry for the delay |
lmondada
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey Alan!
Thank you so much for your work!! I think I should have answered all your comments and help clarify things where it was unclear.
Note that I cannot "apply" any of your change suggestions, as I only have read access to the repo. So please go ahead and apply them yourself.
Do you have a clear idea how you want to proceed? If you estimate that making the changes required to this PR is a lot of work, I'm happy to pick up a part of that. Just let me know exactly what you'd like me to fix.
Ping me here or in email whenever!
|
|
||
| /// All copyable wires on the ports of `node` in the given direction. | ||
| pub fn get_copyable_wires( | ||
| pub fn all_copyable_wires( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes good spot, this is redundant. I agree that get_copyable_ports is sufficient.
| .get_copyable_ports(n, Direction::Incoming) | ||
| .map(|p| p.as_incoming().expect("incoming port")) | ||
| { | ||
| let Some(expr) = self.get_copyable_expression(n, p, circuit) else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good! I don't think I can apply the change somehow, so you go ahead :)
| let mut was_changed = false; | ||
|
|
||
| for dir in Direction::BOTH { | ||
| let copyable_ports = circuit.get_copyable_ports(node, dir); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!!
|
|
||
| impl<H: HugrView> ResourceScope<H> { | ||
| fn is_convex(&self, _subcircuit: &Subcircuit<H::Node>) -> bool { | ||
| unimplemented!("is_convex is not yet implemented") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, absolutely. I will submit a PR with the implementation of this next! See also the deactivated tests (that will be reactivated asap)
| /// a [`SiblingSubgraph`] needs to be constructed (see | ||
| /// [`Subcircuit::try_to_subgraph`] and [`Subcircuit::validate_subgraph`]). | ||
| /// | ||
| /// [`Subcircuit`] distinguishes between "pure copyable" nodes, which has |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, please apply :)
| subgraph: &SiblingSubgraph<N>, | ||
| circuit: &ResourceScope<impl HugrView<Node = N>>, | ||
| ) -> Result<(Vec<(N, IncomingPort)>, IncomingPorts<N>), InvalidSubcircuit<N>> { | ||
| let mut inp_iter = subgraph.incoming_ports().iter().peekable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're probably right. I didn't try as I wasn't convinced it was worth the effort, but if you have an idea how to do it, please go ahead!
| Ok((resource_inputs, other_inputs)) | ||
| } | ||
|
|
||
| fn parse_output_boundary<N: HugrNode>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's correct!
| .peeking_take_while(|&(node, port)| { | ||
| circuit | ||
| .get_circuit_unit(node, port) | ||
| .is_some_and(|unit| unit.is_resource()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, it's a bit shorter, so might be a good idea. I find the current version quite readable, but choose the one that you prefer :)
| } | ||
|
|
||
| fn extend_unique<E: PartialEq>(vec: &mut Vec<E>, other: impl IntoIterator<Item = E>) { | ||
| let other_unique = other.into_iter().filter(|x| !vec.contains(x)).collect_vec(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could replace the vector of copyable inputs/outputs with an IndexSet? That should preserve ordering and have cheap dedup.
That being said, I expect most subcircuits to have <10 classical inputs/outputs, so I was not worried about the cost here. The asymptotic would only ever be an issue if we have huge subcircuits in the future...
| let mut out_iter = subgraph.outgoing_ports().iter().copied().peekable(); | ||
|
|
||
| let resource_outputs = out_iter | ||
| .peeking_take_while(|&(node, port)| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that plan
Part two of Luca's #1273, following #1288.
I've dropped the
as_const_value,as_const_f64andget_const_inputsmethods for now as they are only used in a test, so all that will be in the third part