Skip to content

Commit 69bec43

Browse files
authored
Adding "MaxDestroyerEvents" as a step spec to WolframModel (C++) (#619)
## Changes * Closes #611. * Adds the `"MaxDestroyerEvents"` (per expression) to `WolframModel` as a step specification: ```wl WolframModel[{{1, 2}, {2, 3}} -> {{2, 3}, {2, 4}, {3, 4}, {2, 1}}, {{1, 1}, {1, 1}}, <|"MaxEvents" -> 5, "MaxDestroyerEvents" -> 2|>, "EventSelectionFunction" -> "MultiwaySpacelike"] ``` ## Comments * This works as follows: The causal graph keeps track of how many destroyer events each expression has (`expressionsIDsToDestroyerEventsCount_`). During evolution, we check if the destroyer event count of an expression (`causalGraph_.destroyerEventsCount(expressionID)`) exceeds the maximum allowed (`maxDestroyerEvents_`), and if it does, we eliminate that expression and matches related to that expression from the matcher. * Missing tests, both for C++ and WL. * Missing documentation. ## Examples * Notice how `"MaxDestroyerEvents"` is not a stopping condition, but behaves similar to an event selection function: ```wl In[] := Table[ WolframModel[ {{1, 2}, {2, 3}} -> {{2, 3}, {2, 4}, {3, 4}, {2, 1}}, {{1, 1}, {1, 1}}, <|"MaxEvents" -> 5, "MaxDestroyerEvents" -> maxDestroyerEvents|>, "EventSelectionFunction" -> "MultiwaySpacelike" ]["ExpressionsEventsGraph", VertexLabels -> "Index"], {maxDestroyerEvents, {1, 2, Infinity}}] // GraphicsRow ``` ![MaxDestroyerEventsComparison](https://user-images.githubusercontent.com/40190339/109845600-52cc1600-7c1b-11eb-97ec-70992ca65f13.png) * This is also supported for `"EventSelectionFunction" -> None`: ```wl In[] := Table[ WolframModel[ {{1, 2}, {2, 3}} -> {{2, 3}, {2, 4}, {3, 4}, {2, 1}}, {{1, 1}, {1, 1}}, <|"MaxEvents" -> 5, "MaxDestroyerEvents" -> maxDestroyerEvents|>, "EventSelectionFunction" -> None ]["ExpressionsEventsGraph", VertexLabels -> "Index"], {maxDestroyerEvents, {1, 2, 3, Infinity}}] // GraphicsRow ``` ![MaxDestroyerEventsComparison2](https://user-images.githubusercontent.com/40190339/109846386-25339c80-7c1c-11eb-88df-2ce339e35f2b.png) * `"MaxDestroyerEvent" -> 1`, for any `"EventSelectionFunction"`, behaves effectively as a singleway system: ```wl In[] := Table[ WolframModel[ {{1, 2}, {2, 3}} -> {{2, 3}, {2, 4}, {3, 4}, {2, 1}}, {{1, 1}, {1, 1}}, <|"MaxEvents" -> 5, "MaxDestroyerEvents" -> 1|>, "EventSelectionFunction" -> esf ]["ExpressionsEventsGraph", VertexLabels -> "Index"], {esf, {"GlobalSpacelike", "MultiwaySpacelike", None}}] // GraphicsRow ``` ![MaxDestroyerEventsComparison3](https://user-images.githubusercontent.com/40190339/109846710-75aafa00-7c1c-11eb-8a59-85ca7243d5e4.png) <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/maxitg/setreplace/619) <!-- Reviewable:end -->
1 parent 714251d commit 69bec43

File tree

11 files changed

+244
-64
lines changed

11 files changed

+244
-64
lines changed

Documentation/SymbolsAndFunctions/WolframModelAndWolframModelEvolutionObject/WolframModelAndWolframModelEvolutionObject.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ In[] := WolframModel[{{1, 2, 3}, {4, 5, 6}, {2, 5}, {5, 2}} ->
332332
All possible keys in that association are:
333333

334334
* `"MaxEvents"`: limit the number of individual replacements (in the [`SetReplace`](../SetReplace.md) function meaning).
335+
* `"MaxDestroyerEvents"`: limits the number of destroyer events per expression. That is, when the destroyer events count
336+
of a given expression reaches the maximum number allowed, no further match will involve said expression.
335337
* `"MaxGenerations"`: limit the number of generations (steps in [`SetReplaceAll`](../SetReplace.md) meaning), same as
336338
specifying steps directly as a number in `WolframModel`.
337339
* `"MaxVertices"`: limit the number of vertices in the *final* state only (the total count throughout evolution might be
@@ -349,6 +351,10 @@ attempt to match edges with generations over the limit. Therefore unlike, i.e.,
349351
the evolution immediately once the limit-violating event is attempted, `"MaxGenerations"` would keep "filling in" events
350352
for as long as possible until no further matches within allowed generations are possible.
351353

354+
Similarly, the evolution does not terminate when one expression reaches the `"MaxDestroyerEvents"`, but instead the
355+
expression is removed from future matches. Evolution continues until there are no more expressions whose destroyer
356+
events count is less than the maximum allowed.
357+
352358
It is also possible to set the step count to `Automatic`, in which case `WolframModel` tries to automatically pick a
353359
number of steps that showcases the evolution without taking too long. It stops the evolution sooner if the state grows
354360
quickly:

Kernel/setSubstitutionSystem$cpp.m

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,16 +116,17 @@
116116
7 -> $Aborted
117117
|>;
118118

119-
systemTypeCode[eventSelectionFunction_] := Boole[multiwayEventSelectionFunctionQ[eventSelectionFunction]];
119+
(* GlobalSpacelike is syntactic sugar for "EventSelectionFunction" -> "MultiwaySpacelike", "MaxDestroyerEvents" -> 1 *)
120+
121+
maxDestroyerEvents[_, $globalSpacelike] = 1;
122+
maxDestroyerEvents[Automatic | _ ? MissingQ | Infinity, _] = $maxInt64;
123+
maxDestroyerEvents[n_, _] := n;
120124

121125
(* 0 -> All
122126
1 -> Spacelike *)
123127

124-
(* GlobalSpacelike is set to All because all concurrently matched expressions are always spacelike in that case,
125-
and All is much faster to evaluate. *)
126-
127128
eventSelectionCodes[eventSelectionFunction_, ruleCount_] :=
128-
ConstantArray[eventSelectionFunction /. {$globalSpacelike -> 0, None -> 0, $spacelike -> 1}, ruleCount];
129+
ConstantArray[eventSelectionFunction /. {None -> 0, ($globalSpacelike | $spacelike) -> 1}, ruleCount];
129130

130131
$orderingFunctionCodes = <|
131132
$sortedExpressionIDs -> 0,
@@ -167,7 +168,7 @@
167168
encodeNestedLists[List @@@ mappedRules],
168169
eventSelectionCodes[eventSelectionFunction, Length[canonicalRules]],
169170
encodeNestedLists[mappedSet],
170-
systemTypeCode[eventSelectionFunction],
171+
maxDestroyerEvents[stepSpec[$maxDestroyerEvents], eventSelectionFunction],
171172
Catenate[Replace[eventOrderingFunction, $orderingFunctionCodes, {2}]],
172173
Replace[eventDeduplication, $eventDeduplicationCodes],
173174
RandomInteger[{0, $maxUInt32}]

Kernel/setSubstitutionSystem.m

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
PackageScope["$stepSpecKeys"]
1919
PackageScope["$maxEvents"]
2020
PackageScope["$maxGenerationsLocal"]
21+
PackageScope["$maxDestroyerEvents"]
2122
PackageScope["$maxFinalVertices"]
2223
PackageScope["$maxFinalVertexDegree"]
2324
PackageScope["$maxFinalExpressions"]
@@ -88,6 +89,7 @@
8889
(* local means the evolution will keep running until no further matches can be made exceeding the max generation.
8990
This might result in a different evolution order. *)
9091
$maxGenerationsLocal -> "MaxGenerations",
92+
$maxDestroyerEvents -> "MaxDestroyerEvents",
9193
(* these are any level-2 expressions in the set, not just atoms. *)
9294
$maxFinalVertices -> "MaxVertices",
9395
$maxFinalVertexDegree -> "MaxVertexDegree",
@@ -96,6 +98,7 @@
9698
$stepSpecNamesInErrorMessage = <|
9799
$maxEvents -> "number of replacements",
98100
$maxGenerationsLocal -> "number of generations",
101+
$maxDestroyerEvents -> "number of destroyer events",
99102
$maxFinalVertices -> "number of vertices",
100103
$maxFinalVertexDegree -> "vertex degree",
101104
$maxFinalExpressions -> "number of edges"|>;
@@ -233,7 +236,7 @@
233236
"SameInputSetIsomorphicOutputs" -> $sameInputSetIsomorphicOutputs
234237
|>;
235238

236-
parseParameterValue[caller_, name_, value_, association_] /; MemberQ[Keys[association], value] := association[value];
239+
parseParameterValue[caller_, name_, value_, association_] /; KeyMemberQ[association, value] := association[value];
237240

238241
General::invalidParameterValue = "`1` `2` should be one of `3`.";
239242

Tests/WolframModel.wlt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,65 @@
355355
{4, 12, "MaxEvents"}
356356
],
357357

358+
(*** MaxDestroyerEvents ***)
359+
With[{rule = {{1, 2}, {2, 3}} -> {{2, 3}, {2, 4}, {3, 4}, {2, 1}}, init = {{1, 1}, {1, 1}}},
360+
{
361+
VerificationTest[
362+
WolframModel[
363+
rule,
364+
init,
365+
<|"MaxEvents" -> 5, "MaxDestroyerEvents" -> 5|>,
366+
"EventSelectionFunction" -> "GlobalSpacelike"],
367+
WolframModel[
368+
rule,
369+
init,
370+
<|"MaxEvents" -> 5, "MaxDestroyerEvents" -> 1|>,
371+
"EventSelectionFunction" -> "GlobalSpacelike"]
372+
],
373+
374+
VerificationTest[
375+
WolframModel[rule, init, <|"MaxEvents" -> 5|>],
376+
WolframModel[
377+
rule,
378+
init,
379+
<|"MaxEvents" -> 5, "MaxDestroyerEvents" -> 1|>,
380+
"EventSelectionFunction" -> "MultiwaySpacelike"]
381+
],
382+
383+
VerificationTest[
384+
WolframModel[
385+
rule,
386+
init,
387+
<|"MaxEvents" -> 5, "MaxDestroyerEvents" -> 0|>,
388+
"EventSelectionFunction" -> "MultiwaySpacelike"]["GenerationsCount"],
389+
{0, 0}
390+
]
391+
}
392+
],
393+
394+
VerificationTest[
395+
WolframModel[
396+
{{1, 2}, {2, 3}} -> {{2, 3}, {2, 4}, {3, 4}, {2, 1}},
397+
{{1, 1}, {1, 1}},
398+
<|"MaxEvents" -> 5, "MaxDestroyerEvents" -> 2|>,
399+
"EventSelectionFunction" -> "MultiwaySpacelike"]["EdgeDestroyerEventsIndices"],
400+
{{1, 2}, {1, 2}, {3, 4}, {3, 5}, {4}, {5}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}
401+
],
402+
403+
VerificationTest[
404+
WolframModel[
405+
{{1, 2}, {2, 3}} -> {{2, 3}, {2, 4}, {3, 4}, {2, 1}},
406+
{{1, 1}, {1, 1}},
407+
<|"MaxEvents" -> 5, "MaxDestroyerEvents" -> 3|>,
408+
"EventSelectionFunction" -> "MultiwaySpacelike"]["EdgeDestroyerEventsIndices"],
409+
{{1, 2}, {1, 2}, {3, 4, 5}, {3}, {4}, {5}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}
410+
],
411+
412+
testUnevaluated[
413+
WolframModel[1 -> 2, {1}, <|"MaxEvents" -> 5, "MaxDestroyerEvents" -> -1|>],
414+
{WolframModel::invalidSteps}
415+
],
416+
358417
(*** MaxVertices ***)
359418

360419
Table[With[{method = method, $simpleGrowingRule = {{1, 2}} -> {{1, 3}, {3, 2}}, $simpleGrowingInit = {{1, 1}}}, {

libSetReplace/Event.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class CausalGraph::Implementation {
1010
// the first event is the "fake" initialization event
1111
std::vector<Event> events_;
1212
std::vector<EventID> expressionIDsToCreatorEvents_;
13+
std::vector<uint64_t> expressionIDsToDestroyerEventsCount_;
1314

1415
// needed to return the largest generation in O(1)
1516
Generation largestGeneration_ = 0;
@@ -34,6 +35,7 @@ class CausalGraph::Implementation {
3435
std::vector<ExpressionID> addEvent(const RuleID ruleID,
3536
const std::vector<ExpressionID>& inputExpressions,
3637
const int outputExpressionsCount) {
38+
incrementDestroyerEventsCount(inputExpressions);
3739
const auto newExpressions = createExpressions(events_.size(), outputExpressionsCount);
3840
const Generation generation = newEventGeneration(inputExpressions);
3941
events_.push_back({ruleID, inputExpressions, newExpressions, generation});
@@ -88,13 +90,22 @@ class CausalGraph::Implementation {
8890
return SeparationType::Spacelike;
8991
}
9092

93+
uint64_t destroyerEventsCount(const ExpressionID id) { return expressionIDsToDestroyerEventsCount_[id]; }
94+
9195
private:
9296
std::vector<ExpressionID> createExpressions(const EventID creatorEvent, const int count) {
9397
const size_t beginIndex = expressionIDsToCreatorEvents_.size();
9498
expressionIDsToCreatorEvents_.insert(expressionIDsToCreatorEvents_.end(), count, creatorEvent);
99+
expressionIDsToDestroyerEventsCount_.insert(expressionIDsToDestroyerEventsCount_.end(), count, 0);
95100
return idsRange(beginIndex, expressionIDsToCreatorEvents_.size());
96101
}
97102

103+
void incrementDestroyerEventsCount(const std::vector<ExpressionID>& inputExpressions) {
104+
for (const auto& id : inputExpressions) {
105+
++expressionIDsToDestroyerEventsCount_[id];
106+
}
107+
}
108+
98109
static std::vector<ExpressionID> idsRange(const ExpressionID beginIndex, const ExpressionID endIndex) {
99110
std::vector<ExpressionID> result;
100111
result.reserve(endIndex - beginIndex);
@@ -167,4 +178,8 @@ Generation CausalGraph::largestGeneration() const { return implementation_->larg
167178
SeparationType CausalGraph::expressionsSeparation(const ExpressionID first, const ExpressionID second) const {
168179
return implementation_->expressionsSeparation(first, second);
169180
}
181+
182+
uint64_t CausalGraph::destroyerEventsCount(const ExpressionID id) const {
183+
return implementation_->destroyerEventsCount(id);
184+
}
170185
} // namespace SetReplace

libSetReplace/Event.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ class CausalGraph {
8585
*/
8686
SeparationType expressionsSeparation(ExpressionID first, ExpressionID second) const;
8787

88+
/** @brief Number of destroyer events per expression.
89+
*/
90+
uint64_t destroyerEventsCount(ExpressionID id) const;
91+
8892
private:
8993
class Implementation;
9094
std::shared_ptr<Implementation> implementation_;

libSetReplace/Set.cpp

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Set::Implementation {
1717

1818
// Determines the limiting conditions for the evaluation.
1919
StepSpecification stepSpec_ = {0, 0, 0, 0, 0}; // don't evolve unless asked to.
20-
const SystemType systemType_;
20+
const uint64_t maxDestroyerEvents_;
2121
TerminationReason terminationReason_ = TerminationReason::NotTerminated;
2222

2323
std::unordered_map<ExpressionID, AtomsVector> expressions_;
@@ -40,14 +40,14 @@ class Set::Implementation {
4040
public:
4141
Implementation(const std::vector<Rule>& rules,
4242
const std::vector<AtomsVector>& initialExpressions,
43-
const SystemType& systemType,
43+
const uint64_t maxDestroyerEvents,
4444
const Matcher::OrderingSpec& orderingSpec,
4545
const Matcher::EventDeduplication& eventDeduplication,
4646
const unsigned int randomSeed)
4747
: Implementation(
4848
rules,
4949
initialExpressions,
50-
systemType,
50+
maxDestroyerEvents,
5151
orderingSpec,
5252
eventDeduplication,
5353
randomSeed,
@@ -98,18 +98,6 @@ class Set::Implementation {
9898

9999
// At this point, we are committed to modifying the set.
100100

101-
// This goes first, as if the system type is invalid, we want to fail before modifying anything else.
102-
if (systemType_ == SystemType::Singleway) {
103-
matcher_.removeMatchesInvolvingExpressions(match->inputExpressions);
104-
atomsIndex_.removeExpressions(match->inputExpressions);
105-
destroyedExpressionsCount_ += match->inputExpressions.size();
106-
updateAtomDegrees(&atomDegrees_, match->inputExpressions, -1);
107-
} else if (systemType_ == SystemType::Multiway) {
108-
matcher_.deleteMatch(match);
109-
} else {
110-
throw Error::InvalidSystemType;
111-
}
112-
113101
// Name newly created atoms as well, now all atoms in the output are explicitly named.
114102
const auto namedRuleOutputs = nameAnonymousAtoms(explicitRuleOutputs);
115103

@@ -118,12 +106,36 @@ class Set::Implementation {
118106

119107
addExpressions(outputExpressionIDs, namedRuleOutputs);
120108

109+
if (maxDestroyerEvents_ == 1) {
110+
matcher_.removeMatchesInvolvingExpressions(match->inputExpressions);
111+
atomsIndex_.removeExpressions(match->inputExpressions);
112+
// The following only make sense for singleway systems.
113+
destroyedExpressionsCount_ += match->inputExpressions.size();
114+
updateAtomDegrees(&atomDegrees_, match->inputExpressions, -1);
115+
} else if (maxDestroyerEvents_ == static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) {
116+
matcher_.deleteMatch(match);
117+
} else {
118+
// Only remove expressions whose destroyer events count reached the maximum.
119+
matcher_.deleteMatch(match);
120+
std::vector<ExpressionID> inputExpressionsToRemove;
121+
for (const auto& id : match->inputExpressions) {
122+
if (causalGraph_.destroyerEventsCount(id) >= maxDestroyerEvents_) {
123+
inputExpressionsToRemove.push_back(id);
124+
}
125+
}
126+
matcher_.removeMatchesInvolvingExpressions(inputExpressionsToRemove);
127+
atomsIndex_.removeExpressions(inputExpressionsToRemove);
128+
}
129+
121130
return 1;
122131
}
123132

124133
int64_t replace(const StepSpecification stepSpec, const std::function<bool()>& shouldAbort) {
125134
updateStepSpec(stepSpec);
126135
int64_t count = 0;
136+
if (maxDestroyerEvents_ == 0) {
137+
return count;
138+
}
127139
while (true) {
128140
if (replaceOnce(shouldAbort)) {
129141
++count;
@@ -156,17 +168,17 @@ class Set::Implementation {
156168
const std::vector<Event>& events() const { return causalGraph_.events(); }
157169

158170
private:
159-
Implementation(std::vector<Rule> rules,
171+
Implementation(const std::vector<Rule>& rules,
160172
const std::vector<AtomsVector>& initialExpressions,
161-
const SystemType& systemType,
173+
const uint64_t maxDestroyerEvents,
162174
const Matcher::OrderingSpec& orderingSpec,
163175
const Matcher::EventDeduplication& eventDeduplication,
164176
const unsigned int randomSeed,
165177
const GetAtomsVectorFunc& getAtomsVector,
166178
const GetExpressionsSeparationFunc& getExpressionsSeparation)
167-
: rules_(rules),
168-
systemType_(systemType),
169-
causalGraph_(static_cast<int>(initialExpressions.size()), separationTrackingMethod(systemType, rules)),
179+
: rules_(optimizeRules(rules, maxDestroyerEvents)),
180+
maxDestroyerEvents_(maxDestroyerEvents),
181+
causalGraph_(static_cast<int>(initialExpressions.size()), separationTrackingMethod(maxDestroyerEvents, rules)),
170182
atomsIndex_(getAtomsVector),
171183
matcher_(rules_,
172184
&atomsIndex_,
@@ -185,6 +197,24 @@ class Set::Implementation {
185197
addExpressions(causalGraph_.allExpressionIDs(), initialExpressions);
186198
}
187199

200+
std::vector<Rule> optimizeRules(const std::vector<Rule>& rules, uint64_t maxDestroyerEvents) {
201+
if (maxDestroyerEvents == 1) {
202+
// The real optimization happens later when we call separationTrackingMethod(1, rules) by setting
203+
// SeparationTrackingMethod to None.
204+
// EventSelectionFunction is set to All in each rule to prevent breaking: SeparationTrackingMethod::None causes
205+
// isSpacelikeSeparated(...) to be always false for any expression pair, thus no new event whose rule is only
206+
// applied when expressions are spacelike separated would occur.
207+
std::vector<Rule> newRules;
208+
newRules.reserve(rules.size());
209+
for (const auto& rule : rules) {
210+
newRules.push_back(Rule{rule.inputs, rule.outputs, EventSelectionFunction::All});
211+
}
212+
return newRules;
213+
} else {
214+
return rules;
215+
}
216+
}
217+
188218
Atom incrementNextAtom() {
189219
if (nextAtom_ == std::numeric_limits<Atom>::max()) {
190220
throw Error::AtomCountOverflow;
@@ -223,7 +253,7 @@ class Set::Implementation {
223253
unindexedExpressions_.clear();
224254
}
225255

226-
bool isMultiway() const { return systemType_ != SystemType::Singleway; }
256+
bool isMultiway() const { return maxDestroyerEvents_ > 1; }
227257

228258
TerminationReason willExceedAtomLimits(const std::vector<AtomsVector>& explicitRuleInputs,
229259
const std::vector<AtomsVector>& explicitRuleOutputs) const {
@@ -349,9 +379,10 @@ class Set::Implementation {
349379
return smallestSoFar;
350380
}
351381

352-
static CausalGraph::SeparationTrackingMethod separationTrackingMethod(const SystemType systemType,
382+
static CausalGraph::SeparationTrackingMethod separationTrackingMethod(const uint64_t maxDestroyerEvents,
353383
const std::vector<Rule>& rules) {
354-
if (systemType == SystemType::Singleway) {
384+
if (maxDestroyerEvents == 1) {
385+
// No need of tracking the separation between expressions if these are removed after each destroyer event.
355386
return CausalGraph::SeparationTrackingMethod::None;
356387
}
357388
for (const auto& rule : rules) {
@@ -365,12 +396,12 @@ class Set::Implementation {
365396

366397
Set::Set(const std::vector<Rule>& rules,
367398
const std::vector<AtomsVector>& initialExpressions,
368-
const SystemType& eventSelectionFunction,
399+
uint64_t maxDestroyerEvents,
369400
const Matcher::OrderingSpec& orderingSpec,
370401
const Matcher::EventDeduplication& eventDeduplication,
371402
unsigned int randomSeed)
372403
: implementation_(std::make_shared<Implementation>(
373-
rules, initialExpressions, eventSelectionFunction, orderingSpec, eventDeduplication, randomSeed)) {}
404+
rules, initialExpressions, maxDestroyerEvents, orderingSpec, eventDeduplication, randomSeed)) {}
374405

375406
int64_t Set::replaceOnce(const std::function<bool()>& shouldAbort) {
376407
return implementation_->replaceOnce(shouldAbort, true);

0 commit comments

Comments
 (0)