Skip to content

Commit

Permalink
Merge pull request #211 from GannettDigital/issue-210
Browse files Browse the repository at this point in the history
issue-210: replanning updates for actionTree
  • Loading branch information
scottgunther authored Nov 8, 2019
2 parents b236715 + 1d71de6 commit 0ecfbaa
Show file tree
Hide file tree
Showing 10 changed files with 4,146 additions and 3,334 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Pending Version

## 0.9.0

* Zach Knox
* Update offlineReplanning to support actionTree algorithm
* This uses some randomness to help stop the algorithm from going down the same path all the time. This randomness can be seeded from the config with `replanningSeed`.
* If you plan to use replanning with your existing actionTree models, you will likely need to update your models to better reflect how your website works.
* Updated reduce-to-minimum-set-of-plans to compare hashes of plans. In the past, it was comparing sets in a way which would never detect duplicates.

## 0.8.7

* Mike Millgate
Expand Down
8 changes: 7 additions & 1 deletion docs/pages/documentation/configuration-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ This section documents utilization of the configuration file in place of CLI opt

### plannerAlgorithm
* The algorithm to use when the planner generates tests
* Supported algorithms: `forwardStateSpaceSearchHeuristic`
* Supported algorithms: `actionTree`, `forwardStateSpaceSearchHeuristic`
* Example: `algorithm: forwardStateSpaceSearchHeuristic`

### plannerTestLength
Expand All @@ -199,6 +199,12 @@ This section documents utilization of the configuration file in place of CLI opt
* The replanning step will most likely greatly reduce the number of generated tests
* Example: `plannerTestLength: 75`

### replanningSeed
* Seed provided to the replanning algorithm's random number generator
* Using seeded random numbers allows for consisten plans between generation runs
* If replanning is not enabled with `plannerTestLength`, this value has no effect
* Example: `replanningSeed: 867.5309`

### debug
* Adds node debugging flag to spawned child processes
* Default is `false`
Expand Down
49 changes: 16 additions & 33 deletions lib/planner/reduce-to-minimum-set-of-plans.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,27 @@
'use strict';

const setOperations = require('../util/set-operations.js');
const crypto = require('crypto');

module.exports = function reduceToMinimumSetOfPlans(plans, algorithm, callback) {
let finalPlans = [];
let hashes = new Set();

plans.forEach(function(plan, index) {
let planPath;
switch (algorithm) {
case 'actionTree':
planPath = new Set(plan);
break;
default:
planPath = new Set(plan.path);
}
let hasSuperset = false;
if (algorithm.toLowerCase() === 'actiontree') {
plans.forEach(function(plan, index) {
const hash = crypto.createHash('sha256');
let planString = JSON.stringify(plan);
const planHashDigest = hash.update(planString).digest('base64');

let plansWithCurrentPlanRemoved = plans.filter(function(plan, filterIndex) {
return index !== filterIndex;
});
let hasExistingPlan = hashes.has(planHashDigest);

for (let myPlan of plansWithCurrentPlanRemoved) {
let myPlanPath;
switch (algorithm) {
case 'actionTree':
myPlanPath = new Set(myPlan);
break;
default:
myPlanPath = new Set(myPlan.path);
}
if (setOperations.isSuperset(myPlanPath, planPath)) {
hasSuperset = true;
break;
if (!hasExistingPlan) {
hashes.add(planHashDigest);
finalPlans.push(plan);
}
}

if (!hasSuperset) {
finalPlans.push(plan);
}
});
});

callback(null, finalPlans);
callback(null, finalPlans);
} else {
callback(null, plans);
};
};
109 changes: 95 additions & 14 deletions lib/planner/search-algorithms/offline-replanning.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const crypto = require('crypto');
const Emitter = require('../../util/emitter.js');
const setOperations = require('../../util/set-operations.js');
const _ = require('lodash');
Expand All @@ -9,19 +10,25 @@ const configHandler = require('../../util/config/config-handler.js');
let offlineReplanning;
module.exports = offlineReplanning = {
_plans: [],
_planHashes: new Map(),
_satisfiedActions: new Set(),
_actionOccurrences: null,
_savedPlans: null,
_existingPlans: null,
_discoveredActions: null,
_algorithm: null,
_randomSeed: 25,
* replan(existingPlans, discoveredActions, algorithm, options) {
let next = yield;

if (configHandler.get('debug')) {
console.log('\nBeginning offline replanning\n');
};

if (configHandler.get('replanningSeed')) {
offlineReplanning._randomSeed = configHandler.get('replanningSeed');
}

offlineReplanning._algorithm = algorithm;
offlineReplanning._existingPlans = existingPlans;
offlineReplanning._discoveredActions = discoveredActions;
Expand All @@ -38,10 +45,13 @@ module.exports = offlineReplanning = {
offlineReplanning._savedPlans = [];
let startNodes = yield offlineReplanning.emitAsync('startNodes.get', next);
let plan = startNodes[0];
let newlySatisfiedActions = new Set();
let looped = false;
while (
plan.path.length < options.testLength &&
offlineReplanning._satisfiedActions.size < offlineReplanning._actionOccurrences.size
) {
looped = true;
let action = offlineReplanning._getAction(plan);
if (action === null) {
break;
Expand All @@ -55,15 +65,55 @@ module.exports = offlineReplanning = {
if (backtrackPlan !== null) {
plan = backtrackPlan;
} else if (!offlineReplanning._satisfiedActions.has(action)) {
newlySatisfiedActions.add(action);
offlineReplanning._satisfiedActions.add(action);
yield offlineReplanning.emitAsync('offlineReplanning.updateExistingPlansAndActionOccurrences', next);
} else {
let duplicate = offlineReplanning._comparePlan(plan);
while (duplicate !== undefined || offlineReplanning._isDuplicate(plan)) {
let {backtrackPlan, newSavedPlans, removed} =
yield offlineReplanning.emitAsync('offlineReplanning.backtrack',
offlineReplanning._savedPlans.length - 2, // last good plan
next);
if (backtrackPlan !== null) {
offlineReplanning._savedPlans = newSavedPlans;
if (configHandler.get('debug')) {
console.log(`Backtracked to: ${backtrackPlan.lastAction}`);
}
plan = backtrackPlan;

for (let removedAction of removed) {
if (newlySatisfiedActions.has(removedAction)) {
newlySatisfiedActions.delete(removedAction);
offlineReplanning._satisfiedActions.delete(removedAction);
}
}
} else {
throw new SimulatoError.PLANNER.FAILED_TO_BACKTRACK(
'Unable to backtrack. No suitable backtrack point found'
);
}
duplicate = offlineReplanning._comparePlan(backtrackPlan);
}
}

if (backtrackPlan === null && configHandler.get('debug')) {
console.log(`Added the following action to the plan: ${action}`);
}
}
offlineReplanning._savePlan(plan);

if (plan.path.length === 0) {
break;
} else if (looped && newlySatisfiedActions.size === 0) {
if (configHandler.get('debug')) {
console.log('Retrying replanning for plan ' + offlineReplanning._plans.length);
}
} else {
if (configHandler.get('debug')) {
console.log('Newly Satisfied: ' + newlySatisfiedActions.size);
}
offlineReplanning._savePlan(plan);
}
}

if (offlineReplanning._algorithm.toLowerCase() === 'actiontree') {
Expand Down Expand Up @@ -139,6 +189,9 @@ module.exports = offlineReplanning = {
);
}
offlineReplanning._plans.push(plan);
const hash = crypto.createHash('sha256');
offlineReplanning._planHashes.set(hash.update(
JSON.stringify(plan.path)).digest('base64'), offlineReplanning._plans.length - 1);
if (configHandler.get('debug')) {
console.log(
`Actions covered: ${offlineReplanning._satisfiedActions.size} / ` +
Expand All @@ -162,7 +215,13 @@ module.exports = offlineReplanning = {
if (plan.path.length > 0) {
return null;
} else {
throw new SimulatoError.PLANNER.NO_STARTING_ACTIONS('No possible actions in starting state');
// This will trigger if there are actions not covered, which, while a real problem
// shouldn't stop plans from being created. The command line output will still catch
// these issues and show the actions not covered.
const err = new SimulatoError.PLANNER.NO_STARTING_ACTIONS('No possible actions in starting state');
console.log(err);

return null;
}
}
return action;
Expand All @@ -173,7 +232,12 @@ module.exports = offlineReplanning = {
let superset;
if (offlineReplanning._algorithm.toLowerCase() === 'actiontree') {
// actionTree plans _are_ the path
superset = setOperations.isSuperset(offlineReplanning._satisfiedActions, new Set(plan));
let fixedPlan = plan.map((action) => {
return action.name;
});
let planset = new Set(fixedPlan);
planset.delete(fixedPlan[0]);
superset = setOperations.isSuperset(offlineReplanning._satisfiedActions, planset);
} else { // forwardStateSpaceSearchHeuristic
// forwardStateSpaceSearchHeuristic plans contain an object with the plan path
superset = setOperations.isSuperset(offlineReplanning._satisfiedActions, new Set(plan.path));
Expand All @@ -188,7 +252,7 @@ module.exports = offlineReplanning = {
},
_isDuplicate(plan) {
for (let aPlan of offlineReplanning._plans) {
if (setOperations.isEqual(aPlan.path, new Set(plan.path))) {
if (setOperations.isEqual(new Set(aPlan.path), new Set(plan.path))) {
return true;
}
}
Expand All @@ -198,20 +262,20 @@ module.exports = offlineReplanning = {
let nonZeroActionCountActions = offlineReplanning._filterZeroActionCountActions(plan.actions);

let unsatisfiedActions = setOperations.difference(nonZeroActionCountActions, offlineReplanning._satisfiedActions);

let actionWithSameComponent = offlineReplanning._getActionWithSameComponent(plan, unsatisfiedActions);
if (actionWithSameComponent) {
return actionWithSameComponent;
} else if (unsatisfiedActions.size > 0) {
return [...unsatisfiedActions][0];
const actionIndex = Math.floor(offlineReplanning._random(0, unsatisfiedActions.size - 1));
return [...unsatisfiedActions][actionIndex];
}

let unusedActions = setOperations.difference(nonZeroActionCountActions, new Set(plan.path));
let mostOccurringUnusedAction = offlineReplanning._getMostOccurringAction(unusedActions);

if (mostOccurringUnusedAction) {
return mostOccurringUnusedAction;
} else if (unusedActions.size > 0) {
return [...unusedActions][0];
if (unusedActions.size > 0) {
const actionIndex = Math.floor(offlineReplanning._random(0, unusedActions.size - 1));
return [...unusedActions][actionIndex];
}

let leastOccurringActionInPath = offlineReplanning._getLeastOccurringActionInPath(plan, nonZeroActionCountActions);
Expand Down Expand Up @@ -307,25 +371,42 @@ module.exports = offlineReplanning = {
let next = yield;
let backtrackPlan = null;
let newSavedPlans = null;

let removed = [];
while (index >= 0) {
backtrackPlan = yield offlineReplanning.emitAsync('searchNode.clone', offlineReplanning._savedPlans[index], next);
backtrackPlan = yield offlineReplanning.emitAsync('searchNode.clone',
offlineReplanning._savedPlans[index], next); // TODO: Potentially no need to clone—future improvement?
let actions = new Set(backtrackPlan.actions);

actions.delete(offlineReplanning._savedPlans[index + 1].lastAction);
removed.push(offlineReplanning._savedPlans[index + 1].lastAction);
actions.delete(removed[removed.length - 1]);

if (actions.size > 0) {
newSavedPlans = offlineReplanning._savedPlans.slice(0, index + 1);
backtrackPlan.actions = actions;
offlineReplanning._savedPlans[index].actions = actions;
return callback(null, {backtrackPlan, newSavedPlans});
return callback(null, {backtrackPlan, newSavedPlans, removed});
}

index--;
}

return callback(null, {backtrackPlan: null, newSavedPlans: null});
},
_comparePlan(plan) {
const hash = crypto.createHash('sha256');
let planString = JSON.stringify(plan.path);
const planHashDigest = hash.update(planString).digest('base64');

return offlineReplanning._planHashes.get(planHashDigest);
},
_random(min, max) {
max = max ? max : 1;
min = min ? min : 0;
offlineReplanning._randomSeed = (offlineReplanning._randomSeed * 9301 + 49297) % 233280;
const rand = offlineReplanning._randomSeed / 233280;

return min + rand * (max - min);
},
};

Emitter.mixIn(offlineReplanning, plannerEventDispatch);
Expand Down
7 changes: 5 additions & 2 deletions lib/planner/write-plans-to-disk.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ const crypto = require('crypto');

module.exports = function writePlansToDisk(plans) {
const outputPath = configHandler.get('outputPath');
let index = 0;
for (let plan of plans) {
const sha256 = (x) => crypto.createHash('sha256').update(x, 'utf8').digest('hex');
let testName = `simulato`+`--${sha256(JSON.stringify(plan))}.json`;
let planString = JSON.stringify(Array.from(plan));
let testName = `simulato`+`--${index}--${sha256(planString)}.json`;
let filePath = path.resolve(outputPath, testName);
fs.writeFileSync(filePath, JSON.stringify(plan));
fs.writeFileSync(filePath, planString);
index += 1;
}
console.log(`Generated and wrote ${plans.length} test(s) to disk`);
};
Loading

0 comments on commit 0ecfbaa

Please sign in to comment.