I realized that building a partial prefix trie could actually have significant benefits if done right.
There are twos key realizations:
- by the time you've computed the set of indices that agree over 4 of the dimensions, the number of remaining indices to compare drops by ~1/16th
- since
Complement[Range[ndim], q]
is sorted, every prefix string will start with either 1, 2, 3, or 4, except for a single term starting with 5. This allows us to compute a significantly smaller number of prefixes and still get the benefit of the pre-computation.
The downside, of course, is that the code to build the prefix trie is more involved.
First we define a function to refine our set of indices based on including the next set of pair-wise equalities. We precompute diffdata
just as Henrik did.
diffdata = Unitize[Subtract[braVecs, ketVecs]];
pullSubtrieInds[trie_, i_] :=
Pick[trie["idx"], diffdata[[i, trie["idx"]]], 0]
Next we have a function to recursively build out most of the trie
constructSubtrie[trie_, i_, depth_, maxDepth_] :=
Module[{newTrie = trie},
newTrie[i] = <|"idx" -> pullSubtrieInds[trie, i]|>;
If[depth < maxDepth,
Do[
newTrie[i] = constructSubtrie[newTrie[i], j, depth + 1, maxDepth];,
{j, i + 1, ndim}
]
];
newTrie
];
and now we add a small modification to add the remaining prefix starting with 5
, which simply doesn't loop as much
addFinalSubtrieElem[trie_, i_, depth_, maxDepth_] :=
Module[{newTrie = trie},
newTrie[i] = <|"idx" -> pullSubtrieInds[trie, i]|>;
If[depth < maxDepth,
newTrie[i] = constructSubtrie[newTrie[i], i + 1, depth + 1, maxDepth];
];
newTrie
];
and then we can build the total trie like
constructSubtrie[trie_, maxDepth_] :=
(*
after doing the complement operation,
we get lists of indices that are at minimum ndim-4 elements long,
so after sorting
they all must contain 1 through 5 at some point and the one with the 5 only \
occuring once, so we just compute that one directly... *)
Module[{newTrie = trie, subtrie},
Do[
newTrie = constructSubtrie[newTrie, i, 1, maxDepth],
{i, 4}
];
(* add on the single set of indices that can start with 5 *)
addFinalSubtrieElem[newTrie, 5, 1, maxDepth]
] ;
The trie itself is a bit slow to build at ndim=25
AbsoluteTiming[
$preindexTrieDepth = 4;
$preindexTrie =
constructSubtrie[<|"idx" -> Range[nstates]|>, $preindexTrieDepth];
][[1]]
2.93388
but could be made faster or slower based on using a larger/smaller $preindexTrieDepth
. We'll still see a payoff in the end.
Next we have a function that actually uses this trie to pull indices
getSubsetDiffs[q_, prefTrie_, prefTrieDepth_] :=
Module[{qComp = Complement[Range[ndim], q], initInds, qRest},
initInds = prefTrie[Sequence @@ qComp[[;; prefTrieDepth]]]["idx"];
qRest = qComp[[1 + prefTrieDepth ;;]];
Pick[initInds, Plus @@ diffdata[[qRest, initInds]], 0]
]
and now some timing comparisons
getSubsetDiffsOG[q_] :=
Random`Private`PositionsOf[
Plus @@ diffdata[[qComp]],
0
]
AbsoluteTiming[
Do[
getSubsetDiffsOG[q],
{q, subsets}
]
][[1]]
40.5721
diffcounts = Total[diffdata];
F = q \[Function]
Random`Private`PositionsOf[
Plus[diffcounts, SparseArray[Partition[q, 1] -> -1, ndim, 0].diffdata], 0];
AbsoluteTiming[
Do[
F[q],
{q, subsets}
]
][[1]]
19.7401
AbsoluteTiming[
Do[
getSubsetDiffs[q, $preindexTrie, $preindexTrieDepth],
{q, subsets}
]
][[1]]
5.81803
which shows a factor of ~7 speedup over the totally naive method and a factor of ~3 speedup over Henrik's approach, after accounting for the time required to build the trie.
40.572074`/(5.818029`+2.93388)
6.97351
19.7401`/(5.818029`+2.93388)
3.39292
Finally to confirm that things are coming out right
Catch[
Do[
If[Total[F[q] - getSubsetDiffs[q, $preindexTrie, $preindexTrieDepth]] > 0,
Throw[False]
],
{q, Subsets[Range[ndim], {4}]}
];
True
]
True
This could also likely be made faster by using Henrik's indexing strategy. I'm currently just doing naive subsampling of diffdata
and using a Pick
call.
Full Code Block
diffdata = Unitize[Subtract[braVecs, ketVecs]];
pullSubtrieInds[trie_, i_] :=
Pick[trie["idx"], diffdata[[i, trie["idx"]]], 0]
ClearAll[constructSubtrie]
constructSubtrie[trie_, i_, depth_, maxDepth_] :=
Module[{newTrie = trie},
newTrie[i] = <|"idx" -> pullSubtrieInds[trie, i]|>;
If[depth < maxDepth,
Do[
newTrie[i] = constructSubtrie[newTrie[i], j, depth + 1, maxDepth];,
{j, i + 1, ndim}
]
];
newTrie
];
addFinalSubtrieElem[trie_, i_, depth_, maxDepth_] :=
Module[{newTrie = trie},
newTrie[i] = <|"idx" -> pullSubtrieInds[trie, i]|>;
If[depth < maxDepth,
newTrie[i] = constructSubtrie[newTrie[i], i + 1, depth + 1, maxDepth];
];
newTrie
];
constructSubtrie[trie_, maxDepth_] :=
(*
after doing the complement operation,
we get lists of indices that are at minimum ndim-4 elements long,
so after sorting
they all must contain 1 through 5 at some point and the one with the 5 only \
occuring once, so we just compute that one directly... *)
Module[{newTrie = trie, subtrie},
Do[
newTrie = constructSubtrie[newTrie, i, 1, maxDepth],
{i, 4}
];
(* add on the single set of indices that can start with 5 *)
addFinalSubtrieElem[newTrie, 5, 1, maxDepth]
] ;
getSubsetDiffs[q_, prefTrie_, prefTrieDepth_] :=
(* we assume sorting *)
Module[{qComp = Complement[Range[ndim], q], initInds, qRest},
initInds = prefTrie[Sequence @@ qComp[[;; prefTrieDepth]]]["idx"];
qRest = qComp[[1 + prefTrieDepth ;;]];
Pick[initInds, Plus @@ diffdata[[qRest, initInds]], 0]
]
Pick[Range[nstates], Total@diffs, 0]
andPick[Range[nstates], Plus@@diffs, 0]
seems to be slightly faster. $\endgroup$ – kglr Mar 3 at 4:38