123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623(*****************************************************************************)(* *)(* MIT License *)(* Copyright (c) 2022 Nomadic Labs <contact@nomadic-labs.com> *)(* *)(* 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. *)(* *)(*****************************************************************************)openCsiropenOptimizer_helpers(** The optimizer simplifies a constraint system, producing an equivalent one
with fewer constraints in essentially three ways:
- 1. {!next_gate_selectors} [qlg], [qrg] and/or [qog], which
lead to a more compact representation of sums (linear combinations).
- 2. {!shared_wires} among different constraints.
- 3. {!compacting_blocks} which operate on independent wires.
{1 Optimization Rules}
{2:next_gate_selectors Introducing next-gate selectors}
In a system with 3-wires architecture, adding (in a linear combination) [n]
elements requires [n-1] constraints. For example, computing
[out = 1 + 2 x1 + 4 x2 + 8 x3 + 16 x4 + 32 x5] can be implemented in 4
constraints as follows:
{[
{ a = x1; b = x2; c = aux1; identity = [ql 2; qr 4; qo -1; qc 1] };
{ a = x3; b = x4; c = aux2; identity = [ql 8; qr 16; qo -1] };
{ a = x5; b = aux1; c = aux3; identity = [ql 32; qr 1; qo -1] };
{ a = aux2; b = aux3; c = out; identity = [ql 1; qr 1; qo -1] };
]}
Instead, by using next-gate selectors, a linear combination of [n] terms
can be implemented in [(n-1)/2] constraints. In this case, just two:
{[
{ a = x1; b = x2; c = out; identity = [ql 2; qr 4; qlg 8; qrg 16; qog 32; qo -1; qc 1] };
{ a = x3; b = x4; c = x5; identity = [] };
]}
{2:shared_wires Reusing shared-wires}
Remarkably, two linear combinations that involve some common wires can be
implemented more efficiently if they are handled together.
For example, [out1 = 5 x + 3 y + 9 z] and [out2 = 2 x - 3 y + 7 t] can be
implemented with 3 constraints (instead of 4) as follows:
{[
{ a = z; c = out1; identity = [ql 9; qlg 5; qrg 3; qog -1] };
{ a = x; b = y; c = out2; identity = [ql 2; qr -3; qrg 7; qog -1] };
{ b = t; ; identity = [] };
]}
Furthermore, observe how, conveniently, some wires in the above constraints
are unbound, which can lead to our next optimization.
{2:compacting_blocks Compacting blocks}
Consider a gate which only takes one wire as input, e.g. for computing
[out3 = 3 w + 1], implemented with constraint:
{[{ a = w; c = out3; identity = [ql 3; qo -1; qc 1] }]}
This constraint can be merged with our previous block, which only uses
wire b in its last constraint and has unbound selectors for it:
{[
{ a = z; c = out1; identity = [ql 9; qlg 5; qrg 3; qog -1] };
{ a = x; b = y; c = out2; identity = [ql 2; qr -3; qrg 7; qog -1] };
{ a = w; b = t; c = out3; identity = [ql 3; qo -1; qc 1] };
]}
{1 Algorithm}
The optimization proceeds in several steps:
1. Collect information about all linear constraints/gates (see {!is_linear}),
including the number of occurrences of their output wire among the circuit.
2. Inline any {!type:linear_computation} whose output is used only once
among the circuit, in the computation where it is used (only if the latter
is also linear).
3. Transform any non-inlined linear computation into a {!type:pseudo_block}
and join pseudo blocks that share at least a wire (preferably two wires).
4. Transform pseudo blocks to actual blocks (also considering the non-linear
computations that were left aside) and join blocks that operate on
independent wires together. *)letnb_wires_arch=3typeconstr=Csir.CS.raw_constrainttypescalar=Csir.Scalar.t[@@derivingrepr](* The goal of blocks and pseudo-blocks is to keep together constraints that
depend on each other, e.g. those using next selectors. *)typeblock=constrarray(* coeff * wire *)typeterm=scalar*int[@@derivingrepr]typetrace_info={free_wires:intlist;assignments:(int*termlist)list;}[@@derivingrepr](** Type to store the trace_updater information.
[free_wires] represents a list of indices of wires that have been
removed from the system and have not been used again for auxiliary
computations. If a free wire [w] is used to store an auxiliary linear
computation on some [terms], it is removed from [free_wires] and the pair
[(v, terms)] is added to the list of [assignments]. *)typepseudo_constr={pc_head:termlist;pc_body:termlist;pc_tail:termlist;pc_const:Scalar.t;}(** A pseudo constraint represents an equation between an arbitrary number of
terms. In particular, adding all terms from its head, body, tail and its
constant term should produce 0.
The length of pc_head and pc_tail should never exceed [nb_wires_arch]. *)typepseudo_block=pseudo_constrlist(** A pseudo block is a list of pseudo constraints that have been groupped
in order to apply the {!shared_wires} heuristic.Alcotest
The constraints in a pseudo block satisfy (by construction) the invariant
the pc_head of a constraint starts with the pc_tail of the previous one
(when considering the term variables, coefficients may be different).
Note that the pc_head may contain extra terms that do not appear in the
previous pc_tail.
This invariant is important for [block_of_pseudo_block] to be correct *)(** [join_list ~join scope x xs] joins [x] with another element among the first
[scope] elements of the list of candidates [xs], or returns [None] if no
union was possible. This function may not preserve the order in [xs] *)letjoin_list:join:('a->'a->'aoption)->int->'a->'alist->'alistoption=fun~joinscopexxs->letrecauxcntseen_xs=function|[]->None|x'::xs->(ifcnt>=scopethenNoneelsematchjoinxx'with|Someunion->Some(List.rev_appendseen_xs(union::xs))|None->(matchjoinx'xwith|Someunion->Some(List.rev_appendseen_xs(union::xs))|None->aux(cnt+1)(x'::seen_xs)xs))inaux0[]xs(** [combine_quadratic ~join xs] takes a list of elements [xs] and returns
a (potentially) shorter list [xs'] where some of the elements have been
combined according to [join]. [join x x'] is [None] if [x] and [x'] cannot
be combined and it is [Some union] otherwise.
This algorithm tries to combine every element of [xs] with any other element
by greedily trying all possible [n choose 2] different pairings of elements,
where [n] is the length of [xs]. *)letcombine_quadratic:scope:int->join:('a->'a->'aoption)->'alist->'alist=fun~scope~joingroups->letrecauxseen_xs=function|[]->seen_xs|x::xs->(matchjoin_listscope~joinxxswith|None->aux(x::seen_xs)xs|Somexs->auxseen_xsxs)inaux[]groups(** Similar to [combine_quadratic], but more efficient.
Takes a list of [top_candidates] that will each be try to be combined
with [rest]. This function may not preserve the order in [rest] *)letcombine_quadratic_efficient~scope~jointop_candidatesrest=letrecauxseentoprest=matchtopwith|[]->List.rev_append(combine_quadratic~scope~joinseen)rest|x::xs->(matchjoin_listscope~joinxrestwith|None->aux(x::seen)xsrest|Somerest->auxseenxsrest)inaux[]top_candidatesrest(** Concatenates two lists of terms, merging terms with the same wire identifier
(by adding their coefficients) *)letadd_terms:termlist->termlist->termlist=funterms1terms2->(* [aux] requires that the terms be first sorted wrt the wire identifier *)letrecauxterms(q,w)=function|[]->(q,w)::terms|(q',w')::rest->ifw=w'thenauxterms(Scalar.addqq',w)restelseaux((q,w)::terms)(q',w')restinletcompare(_,w1)(_,w2)=Int.comparew2w1inletsorted=List.sortcompare(List.rev_appendterms1terms2)inmatchsortedwith[]->[]|_->aux[](List.hdsorted)(List.tlsorted)(** [inline w ~from ~into linear] takes an array of linear constraints
(a linear constraint is represented by list of terms), solves for wire
[w] in constraint [linear.(from)] and substitutes wire [w] by the result
in constraint [linear.(into)]. [linear.(from)] is left empty.
This routine should only be called if [w] does not appear in any other
constraint. In that case, constraint [linear.(from)] can then be removed
and wire [w] is said to be "free". *)letinlinew~from~intolinear:unit=letequals_wt=w=sndtinlet(q1,_),ts1=find_and_removeequals_wlinear.(from)inlet(q2,_),ts2=find_and_removeequals_wlinear.(into)inletf(q,w)=(Scalar.(negateq*q2/q1),w)inlinear.(from)<-[];linear.(into)<-add_terms(List.mapfts1)ts2moduleIMap=Map.Make(Int)(** The keys are wire indices and the values lists of constraints indices, such
that the wire appears in each of the constraints. *)(* TODO: sometimes for clarity I like to make the type of the values monomorphic
This module can also be called Wire_occurrences *)moduleISet=Set.Make(Int)(** Auxiliary function used in [inline_linear].
[increase_occurrences map i terms] updates [map] by adding [i] to the
list of occurrences of wire [w] for every [(_, w)] in [terms]. *)letincrease_occurrencesmapiterms=List.fold_left(funmap(_,w)->IMap.updatew(funo->Some(i::Option.value~default:[]o))map)mapterms(** Creates a pseudo block with all terms in the body except for the wire -1
which is set as constant. *)letpseudo_block_of_termsterms=(* The list of terms contains no duplicates with respect to the wire
identifier [w], thanks to function [add_terms]. *)letqcs,terms=List.partition(fun(_,w)->w<0)termsinletqc=matchqcswith|[]->Scalar.zero|[(q,-1)]->q|_->(* Two different terms should not have the same second component *)assertfalsein[{pc_head=[];pc_body=terms;pc_tail=[];pc_const=qc}](** Returns a list of linear gates that have possibly been inlined, the list of
wires that were inlined (and thus freed) and the list of unchanged non-linear
gates. The wires selected for inlining must:
- appear in exactly two linear gates of size one
- not be inputs
The inlined linear gates are returned as pseudo blocks because they may need
to be split again to fit 3 wires. *)letinline_linear~nb_inputsgates=letlinear,non_linear=List.partition_map(fungate->ifArray.lengthgate=1&&CS.is_linear_raw_constrgate.(0)thenLeftgate.(0)elseRightgate)gatesinletlinear=Array.(mapCS.linear_terms@@of_listlinear)inletnon_linear_wires=List.concat_mapCS.gate_wiresnon_linear|>ISet.of_listin(* Associate each wire to the indices (of array [linear]) of the constraints
where the wire appears. The only wires considered must not:
- be a circuit input
- appear in a non-linear gate. *)letwire_occurrences=Array.fold_left(fun(map,i)gate->(increase_occurrencesmapigate,succi))(IMap.empty,0)linear|>fst|>IMap.filter(funw_->w>nb_inputs&&(not@@ISet.memwnon_linear_wires))in(* During the process of inlining, some wire occurrences will point to
constraints that have been removed (inlined).
In [pointers] we carry the information of where they were inlined into. *)letpointers=refIMap.emptyinletrecupdated_pointerocc=matchIMap.find_optocc!pointerswith|Somep->letp'=updated_pointerpin(* Create a direct connection between [occ] and [p'] *)pointers:=IMap.addoccp'!pointers;p'|None->occin(* If a wire occurs only on two constraints, we remove one
of them, "inlining" it in the other, the wire disappears *)letfree_wires,removable_constrs=IMap.fold(funwoccs(free_wires,removable_constrs)->matchoccswith|[occ]->(* We could raise an exception saying that wire [w] is unnecessary *)letocc=updated_pointeroccin(w::free_wires,occ::removable_constrs)|[occ1;occ2]->letfrom=updated_pointerocc1inletinto=updated_pointerocc2ininlinew~from~intolinear;(* References to [from] should refer to [into] instead *)pointers:=IMap.addfrominto!pointers;(w::free_wires,removable_constrs)|_->(free_wires,removable_constrs))wire_occurrences([],[])in(* Drop removable constraints (only now that the inlining has finished) *)List.iter(func->linear.(c)<-[])removable_constrs;letlinear=List.filter(funts->ts<>[])(Array.to_listlinear)in(* Sort free_vars to reach allocation order. Is this really needed? *)letfree_wires=List.sortInt.comparefree_wiresinletpseudo_blocks=List.mappseudo_block_of_termslinearin(pseudo_blocks,non_linear,free_wires)(** [place_over_pseudo_block ~perfectly b1 b2] tries to join pseudo block [b1]
on top of [b2], i.e., link the last pseudo constraint of [b1] with the
first pseudo constraint of [b2]. It returns an optional argument containing
the combined pseudo block if the combination was successful.
[perfectly] is a Boolean argument that specifies whether only "perfect"
matches should be accepted. In this context, "perfect" means that the two
constraints to be combined share exactly [nb_wires_arch] wires. *)letplace_over_pseudo_block~perfectlyb1b2=letb1_rest,b1_k=split_lastb1inletb2_0,b2_rest=split_firstb2inifb1_k.pc_tail<>[]||b2_0.pc_head<>[]thenNoneelseletb1_body=List.mapsndb1_k.pc_bodyinletb2_body=List.mapsndb2_0.pc_bodyinletcommon_body=list_intersection~equal:(=)b1_bodyb2_bodyin(* Given that the head of [b2_0] is empty, we can place [nb_wires_arch]
terms on it. The total room depends on how occupied its tail is *)letb2_free_room=(2*nb_wires_arch)-List.lengthb2_0.pc_tailin(* We will pick [n] common terms between the bodies of [b1_k] and [b2_0].
If the body of [b2_0] fits completely into its head, we do not need
to leave an extra space for an auxiliary variable *)letn=ifList.lengthb2_body<=b2_free_room&&List.lengthb2_body=nb_wires_archthennb_wires_archelsenb_wires_arch-1inletcommon=rev_first_n~ncommon_body|>ISet.of_listinlettight=ISet.cardinalcommon=nb_wires_archinifperfectly&¬tightthenNoneelseifnb_wires_arch-ISet.cardinalcommon>1thenNoneelseletsplit_body=List.partition(fun(_,w)->ISet.memwcommon)inletqcommon1,body1=split_bodyb1_k.pc_bodyinletqcommon2,body2=split_bodyb2_0.pc_bodyin(* We sort them so that they have the same order *)letcompare_terms(_,w1)(_,w2)=Int.comparew1w2inletqcommon1=List.sortcompare_termsqcommon1inletqcommon2=List.sortcompare_termsqcommon2inletpc1={b1_kwithpc_tail=qcommon1;pc_body=body1}inletpc2={b2_0withpc_head=qcommon2;pc_body=body2}inSome(b1_rest@(pc1::pc2::b2_rest))letcombine_pseudo_blocks~scope~perfectlypseudo_blocks=lettry_both_waysb1b2=matchplace_over_pseudo_block~perfectlyb1b2with|None->place_over_pseudo_block~perfectlyb2b1|Someunion->Someunionincombine_quadratic~scope~join:try_both_wayspseudo_blocksletlinear_combination~this~nextqc=letpad=List.init(nb_wires_arch-List.lengththis)(fun_->0)inletwires=List.mapsndthis@padinletcombinel1l2=List.combine(list_sub(List.lengthl2)l1)l2inletthis_sels=combineCS.this_constr_linear_selectors(List.mapfstthis)inletnext_sels=combineCS.next_constr_linear_selectors(List.mapfstnext)inCS.mk_linear_constr(wires,(("qc",qc)::this_sels)@next_sels)letdummy_linear_combinationterms=letpad=List.init(nb_wires_arch-List.lengthterms)(fun_->0)inCS.mk_linear_constr(List.mapsndterms@pad,[])(** Given a pseudo block, i.e., a list of pseudo constraints, transform it into
a block. Thanks to the invariant that the pc_head of a pseudo constraint
starts with the pc_tail of the previous one, it is enough to add a constraint
for each pc_head (and encode each pc_tail with next-gate selectors).
Note that pc_bodies may need to be freed through auxiliary variables. *)letblock_of_pseudo_blocktrace_infopseudo_block=letpseudo_block=Array.of_listpseudo_blockinArray.fold_left(fun(trace_info,block,residue_pseudo_blocks,i)b->(* We need to move all terms from b.pc_body to its head or tail.
If there are more terms that those that can be moved, we will
need an auxiliary variable, to be put in the head *)(* TODO: select heuristically what to place and where, e.g., leave
for the residue the terms that are more frequent among the circuit *)letis_last=i=Array.lengthpseudo_block-1inletfree_room=nb_wires_arch-List.lengthb.pc_headinletfree_room=free_room+ifis_lastthennb_wires_arch-List.lengthb.pc_tailelse0inletaux_var_needed=List.lengthb.pc_body>free_roominletn=ifaux_var_neededthennb_wires_arch-1elsenb_wires_archinlethead,pending=complete~nb.pc_headb.pc_bodyinlettail,pending=ifis_lastthencomplete~n:nb_wires_archb.pc_tailpendingelse(b.pc_tail,pending)inlettrace_info,head,residues=ifnotaux_var_neededthen(assert(pending=[]);(trace_info,head,[]))elseletv,free_wires=split_firsttrace_info.free_wiresinletassignments=(v,pending)::trace_info.assignmentsinassert(List.lengthhead<nb_wires_arch);lethead=head@[(Scalar.one,v)]inlettrace_info={free_wires;assignments}inletresidues=[pseudo_block_of_terms@@((Scalar.(negateone),v)::pending)]in(trace_info,head,residues)in(* The next constraint will have a head that starts with this tail *)letconstr1=linear_combination~this:head~next:tailb.pc_constinletconstrs=ifis_last&&tail<>[]then[constr1;dummy_linear_combinationtail]else[constr1]in(trace_info,block@constrs,residues@residue_pseudo_blocks,succi))(trace_info,[],[],0)pseudo_block|>fun(trace_info,block,residue_pseudo_blocks,_)->(trace_info,Array.of_listblock,residue_pseudo_blocks)(** This function takes a list of pseudo blocks, combines them if possible and
converts them into blocks. This conversion may lead to residue pseudo blocks
(if auxiliary variables were involved to free a pc_body).
It then applies the same process on the residue pseudo blocks.
Observe that it is relatively rare that a residue pseudo block is produced
after the convertion. Consequently the second iteration of this function
will be among many fewer pseudo blocks and it quickly reaches termination *)letblocks_of_pseudo_blocks~scopefree_wirespseudo_blocks=letreciteration(trace_info,blocks,pseudo_blocks)=matchpseudo_blockswith|[]->(trace_info,blocks)|_->letjoined=pseudo_blocks|>combine_pseudo_blocks~scope~perfectly:true|>combine_pseudo_blocks~scope~perfectly:falseinList.fold_left(fun(trace_info,blocks,new_pseudoblks)pb->lettrace_info,block,new_pseudo=block_of_pseudo_blocktrace_infopbin(trace_info,block::blocks,List.rev_appendnew_pseudonew_pseudoblks))(trace_info,blocks,[])joined|>iterationinlettrace_info={free_wires;assignments=[]}initeration(trace_info,[],pseudo_blocks)(** [place_over_block ~perfectly pb pb'] tries to join block [b1] on top
of [b2]. It works only if the last constraint of [b1] has no selectors.
[perfectly] returns only matches that have no empty selectors (no holes).
It is relevant for a heuristic where we first want to find perfect matches
and then any match.
The joining is done by
- finding a permutation that makes the wires of b1.last and b2.first match
(being relaxed about unset wires)
- checking if the permutation if perfect and failing if required
- renaming the selectors of b1.last
- renaming the next selectors of b1.second_last
Example:
[ (1 1 1) [ql:a qlg:a]
(3 2 -1) [] ]
[ (2 3 4) [ql:a]
(1 1 1) [ql:a] ]
becomes
[ (1 1 1) [ql:a qlg:b]
(2 3 4) [ql:a]
(1 1 1) [ql:a] ]
*)letplace_over_block:perfectly:bool->block->block->blockoption=fun~perfectlyb1b2->letb1_k=b1.(Array.lengthb1-1)inifb1_k.sels<>[]thenNoneelseletwires1=CS.wires_of_constr_ib1(Array.lengthb1-1)inletwires2=CS.wires_of_constr_ib20inmatchadaptwires1wires2with|None->None|Someperm->letwires1=permute_listpermwires1inletcombined=List.map2(funxy->ifx>=0thenxelsey)wires1wires2inletis_perfect=List.for_all(funx->x>=0)combinedinifperfectly&¬is_perfectthenNoneelseletrenamen=letselectors_assoc=List.combine(permute_listpermCS.next_constr_linear_selectors)CS.next_constr_linear_selectorsinassert(List.lengthselectors_assoc=nb_wires_arch);matchList.find_opt(fun(s,_)->s=n)selectors_assocwith|Some(_,s')->s'|None->ninletb1_k_1=b1.(Array.lengthb1-2)inletsels_prev=List.map(fun(n,q)->(renamen,q))b1_k_1.selsinleta,b,c=(* TODO: Revisit [type raw_constraint] and how to represent unused
wires. Should the conversion from -1 to 0 be done here? *)matchList.map(max0)combinedwith|[a;b;c]->(a,b,c)|_->assertfalseinletb2_0=b2.(0)inletb2_rest=Array.subb21(Array.lengthb2-1)inb1.(Array.lengthb1-1)<-{a;b;c;sels=b2_0.sels;label="hyb"::b2_0.label};b1.(Array.lengthb1-2)<-{b1_k_1withsels=sels_prev};Some(Array.appendb1b2_rest)letcombine_blocks~perfectlyblocks=letblocks_with_empty_selectors,rest=letopenCSinList.partition(funb->b.(Array.lengthb-1).sels=[])blocksincombine_quadratic_efficient~join:(place_over_block~perfectly)blocks_with_empty_selectorsrest(** Takes {!type:trace_info} and returns a function that updates a given
trace to make it compatible with the optimized constraints. *)lettrace_updater:trace_info->scalararray->scalararray=funtrace_infotrace->letsum~trace=List.fold_left(funs(coeff,i)->Scalar.(adds@@mulcoefftrace.(i)))Scalar.zeroinList.iter(fun(i,terms)->trace.(i)<-sum~traceterms)(* Iterate over the assignments reversed to ensure that variables
are assigned in the correct order, in case they depend on each other. *)(List.revtrace_info.assignments);trace(** Takes a list of raw_constraints and returns an equivalent constraint system
with potentially fewer constraints.
As a second output, it returns the necessary information to build a function
that updates the trace to make it compatible with the new constraints *)letoptimize:nb_inputs:int->CS.gatelist->CS.gatelist*trace_info=fun~nb_inputsgates->letlinear_pseudo_blocks,non_linear,free_wires=inline_linear~nb_inputsgatesinletscope=ifList.compare_length_withnon_linear10_000>0then10else100inlettrace_info,linear_blocks=blocks_of_pseudo_blocks~scopefree_wireslinear_pseudo_blocksinletcs=List.rev_appendnon_linearlinear_blocks|>combine_blocks~scope~perfectly:true(* we shuffle the blocks to mix linear with non-linear ones and this
way allow for more likely combinations; we use an arbitrary fixed seed
for determinism *)|>shuffle_list~seed:1031|>combine_blocks~scope~perfectly:falsein(cs,trace_info)