diff --git a/src/index.ts b/src/index.ts index 3e74ce3..baeca14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from "./lib/types"; export * from "./lib/fastDownward/fdTypes"; export * from "./lib/pathPlanner"; -export * from "./lib/fastDownward/missingSkillDetection"; \ No newline at end of file +export * from "./lib/fastDownward/missingSkillDetection"; +export * from "./lib/compositeLearningUnit"; \ No newline at end of file diff --git a/src/lib/compositeLearningUnit.spec.ts b/src/lib/compositeLearningUnit.spec.ts new file mode 100644 index 0000000..90ea1be --- /dev/null +++ b/src/lib/compositeLearningUnit.spec.ts @@ -0,0 +1,400 @@ +import { LearningUnit, Skill } from "./types"; +import { computeSkills, getTeachingGoals, getRequiredSkills, getSuggestedSkills} from "./compositeLearningUnit" +import { getPath, getSkillAnalysis } from "./pathPlanner"; + +describe("Composite Unit", () => { + + describe("testing composite unit", () => { + + const skillsMap: Skill[] = [ + { id: "sk:1", repositoryId: "3", nestedSkills: [] }, + { id: "sk:2", repositoryId: "3", nestedSkills: [] }, + { id: "sk:3", repositoryId: "3", nestedSkills: [] }, + { id: "sk:4", repositoryId: "3", nestedSkills: [] }, + { id: "sk:5", repositoryId: "3", nestedSkills: [] }, + { id: "sk:6", repositoryId: "3", nestedSkills: [] }, + { id: "sk:A", repositoryId: "3", nestedSkills: [] }, + { id: "sk:B", repositoryId: "3", nestedSkills: [] } + ].sort((a, b) => a.id.localeCompare(b.id)); + + const compositeLus: LearningUnit[] = [ + newLearningUnit(skillsMap, "lu:10", [], ["sk:1"]), + newLearningUnit(skillsMap, "lu:20", [], ["sk:2"]), + newLearningUnit(skillsMap, "lu:50", [], ["sk:5"]), + newLearningUnit(skillsMap, "lu:A10", [], ["sk:A"]), + newLearningUnit(skillsMap, "lu:1", ["sk:1", "sk:2"], ["sk:3"]), + newLearningUnit(skillsMap, "lu:2", ["sk:3", "sk:4"], ["sk:6"]), + newLearningUnit(skillsMap, "lu:3", ["sk:5"], ["sk:4"]), + newCompositeLearningUnit(skillsMap, "cu:A", ["sk:A"], ["sk:B"], [] + , ["sk:5"] + , []), + ]; + + compositeLus.find(lu => lu.id == "cu:A").children!.push( + compositeLus.find(lu => lu.id == "lu:1")!, + compositeLus.find(lu => lu.id == "lu:2")!, + compositeLus.find(lu => lu.id == "lu:3")! + ) + + computeSkills(compositeLus.filter(lu => lu.children.length > 0)); + + it("check composite unit", () => { + + const compositeLus: LearningUnit[] = [ + newLearningUnit(skillsMap, "lu:10", [], ["sk:1"]), + newLearningUnit(skillsMap, "lu:20", [], ["sk:2"]), + newLearningUnit(skillsMap, "lu:50", [], ["sk:5"]), + newLearningUnit(skillsMap, "lu:A10", [], ["sk:A"]), + newLearningUnit(skillsMap, "lu:1", ["sk:1", "sk:2"], ["sk:3"], + [{ weight: 0.4, skill: "sk:4" }]), + newLearningUnit(skillsMap, "lu:2", ["sk:3", "sk:4"], ["sk:6"], + [{ weight: 0.3, skill: "sk:3" }]), + newLearningUnit(skillsMap, "lu:3", ["sk:5"], ["sk:4"]), + newCompositeLearningUnit(skillsMap, "cu:A", ["sk:A"], ["sk:B"], [{ weight: 0.1, skill: "sk:1" }] + , ["sk:5"] + , [{ weight: 0.4, skill: "sk:4" }]), + ]; + + compositeLus.find(lu => lu.id == "cu:A").children!.push( + compositeLus.find(lu => lu.id == "lu:1")!, + compositeLus.find(lu => lu.id == "lu:2")!, + compositeLus.find(lu => lu.id == "lu:3")! + ) + + computeSkills(compositeLus.filter(lu => lu.children.length > 0)); + + const externalRequiredSkills: Skill[] = [ + { id: "sk:A", repositoryId: "3", nestedSkills: [] } + ]; + + const exportedRequiredSkills: Skill[] = [ + { id: "sk:5", repositoryId: "3", nestedSkills: [] } + ]; + + const unusedRequiredSkills: Skill[] = [ + { id: "sk:1", repositoryId: "3", nestedSkills: [] }, + { id: "sk:2", repositoryId: "3", nestedSkills: [] }, + { id: "sk:3", repositoryId: "3", nestedSkills: [] }, + { id: "sk:4", repositoryId: "3", nestedSkills: [] } + ]; + + const taughtSkills: Skill[] = [ + { id: "sk:B", repositoryId: "3", nestedSkills: [] }, + { id: "sk:3", repositoryId: "3", nestedSkills: [] }, + { id: "sk:6", repositoryId: "3", nestedSkills: [] }, + { id: "sk:4", repositoryId: "3", nestedSkills: [] }, + ]; + + const externalRecommendedSkills = [{ weight: 0.1, skill: skillsMap.find(skill => skill.id == "sk:1") }]; + + const exportedRecommendedSkills = [{ weight: 0.4, skill: skillsMap.find(skill => skill.id == "sk:4") }]; + + const unusedRecommendedSkills = [{ weight: 0.3, skill: skillsMap.find(skill => skill.id == "sk:3") }]; + + const cuA = compositeLus.find(lu => lu.id == "cu:A")!; + expect(cuA.externalRequiredSkills).toEqual(externalRequiredSkills); + expect(cuA.exportedRequiredSkills).toEqual(exportedRequiredSkills); + expect(cuA.unusedRequiredSkills).toEqual(unusedRequiredSkills); + + expect(cuA.taughtSkills).toEqual(taughtSkills); + + expect(cuA.externalRecommendedSkills).toEqual(externalRecommendedSkills); + expect(cuA.exportedRecommendedSkills).toEqual(exportedRecommendedSkills); + expect(cuA.unusedRecommendedSkills).toEqual(unusedRecommendedSkills); + + }); + + /*it("check validity of composite unit", () => { + + const skillMap: Skill[] = [ + { id: "sk:1", repositoryId: "3", nestedSkills: [] }, + { id: "sk:2", repositoryId: "3", nestedSkills: [] }, + { id: "sk:3", repositoryId: "3", nestedSkills: [] }, + { id: "sk:4", repositoryId: "3", nestedSkills: [] } + ].sort((a, b) => a.id.localeCompare(b.id)); + + const compositeLus: LearningUnit[] = [ + newLearningUnit(skillMap, "lu:1", [], ["sk:1"]), + newLearningUnit(skillMap, "lu:2", [], ["sk:2"]), + newLearningUnit(skillMap, "lu:3", [], ["sk:3"]) + ]; + + compositeLus.find(lu => lu.id == "lu:1").children!.push( + compositeLus.find(lu => lu.id == "lu:2")!, + compositeLus.find(lu => lu.id == "lu:3")! + ) + + const lu1 = compositeLus.find(lu => lu.id == "lu:1")!; + const lu1Validity = checkCompositeUnitValidity(lu1); + + expect(lu1Validity).toBeFalsy(); + });*/ + + it("check path for skill 3", () => { + + const goals = [ + ...skillsMap.filter(skill => skill.id === "sk:3") + ]; + + const path = getPath({ + skills: skillsMap, + learningUnits: compositeLus, + goal: goals, + optimalSolution: true, + contextSwitchPenalty: 1.2 + }); + + const expectedPath = [ + ...compositeLus.filter(skill => skill.id === "lu:10"), + ...compositeLus.filter(skill => skill.id === "lu:20"), + ...compositeLus.filter(skill => skill.id === "lu:1"), + ]; + + expect(path.cost).toBe(3.4); + expect(path.path.length).toBe(3); + expect(path.path).toEqual(expectedPath); + }); + + it("check path for skill 4", () => { + + const goals = [ + ...skillsMap.filter(skill => skill.id === "sk:4") + ]; + + const path = getPath({ + skills: skillsMap, + learningUnits: compositeLus, + goal: goals, + optimalSolution: true, + contextSwitchPenalty: 1.2 + }); + + const expectedPath = [ + ...compositeLus.filter(skill => skill.id === "lu:50"), + ...compositeLus.filter(skill => skill.id === "lu:3"), + ]; + + expect(path.cost).toBe(2.2); + expect(path.path.length).toBe(2); + expect(path.path).toEqual(expectedPath); + }); + + it("check path for skill 6", () => { + + const goals = [ + ...skillsMap.filter(skill => skill.id === "sk:6") + ]; + + const path = getPath({ + skills: skillsMap, + learningUnits: compositeLus, + goal: goals, + optimalSolution: true, + contextSwitchPenalty: 1.2 + }); + + const expectedPath = [ + ...compositeLus.filter(skill => skill.id === "lu:A10"), + ...compositeLus.filter(skill => skill.id === "lu:50"), + ...compositeLus.filter(skill => skill.id === "cu:A"), + ]; + + expect(path.cost).toBe(5.62); + expect(path.path.length).toBe(3); + expect(path.path).toEqual(expectedPath); + }); + + it("check path for skill B", () => { + + const goals = [ + ...skillsMap.filter(skill => skill.id === "sk:B") + ]; + + const path = getPath({ + skills: skillsMap, + learningUnits: compositeLus, + goal: goals, + optimalSolution: true, + contextSwitchPenalty: 1.2 + }); + + const expectedPath = [ + ...compositeLus.filter(skill => skill.id === "lu:50"), + ...compositeLus.filter(skill => skill.id === "lu:A10"), + ...compositeLus.filter(skill => skill.id === "cu:A"), + ]; + + expect(path.cost).toBe(5.62); + expect(path.path.length).toBe(3); + expect(path.path).toEqual(expectedPath); + }); + + it("check path for Composite unit against multi Learning units", () => { + + const compositeLus: LearningUnit[] = [ + newLearningUnit(skillsMap, "lu:A", [], ["sk:1", "sk:2"]), + newLearningUnit(skillsMap, "lu:1", ["sk:1", "sk:2"], ["sk:3"]), + newLearningUnit(skillsMap, "lu:2", ["sk:3", "sk:4"], ["sk:6"]), + newLearningUnit(skillsMap, "lu:3", [], ["sk:4"]), + newCompositeLearningUnit(skillsMap, "cu:A", [], [], [] + , [] + , []), + ]; + + compositeLus.find(lu => lu.id == "cu:A").children!.push( + compositeLus.find(lu => lu.id == "lu:1")!, + compositeLus.find(lu => lu.id == "lu:2")!, + compositeLus.find(lu => lu.id == "lu:3")!, + compositeLus.find(lu => lu.id == "lu:A")! + ) + + computeSkills(compositeLus.filter(lu => lu.children.length > 0)); + + const goals = [ + ...skillsMap.filter(skill => skill.id === "sk:1"), + ...skillsMap.filter(skill => skill.id === "sk:2"), + ...skillsMap.filter(skill => skill.id === "sk:3"), + ...skillsMap.filter(skill => skill.id === "sk:4"), + ...skillsMap.filter(skill => skill.id === "sk:6"), + ]; + + const path = getPath({ + skills: skillsMap, + learningUnits: compositeLus, + goal: goals, + optimalSolution: true, + contextSwitchPenalty: 1.2 + }); + + const expectedPath = [ + ...compositeLus.filter(skill => skill.id === "cu:A"), + ]; + + expect(path.cost).toBe(3.8); + expect(path.path.length).toBe(1); + expect(path.path).toEqual(expectedPath); + }); + + it("check path for two Composite units", () => { + + const compositeLus: LearningUnit[] = [ + newLearningUnit(skillsMap, "lu:A", [], ["sk:1", "sk:2"]), + newLearningUnit(skillsMap, "lu:1", ["sk:1", "sk:2"], ["sk:3"]), + newLearningUnit(skillsMap, "lu:2", ["sk:4"], ["sk:6"]), + newLearningUnit(skillsMap, "lu:3", [], ["sk:4"]), + newCompositeLearningUnit(skillsMap, "cu:A", [], [], [] + , [] + , []), + newCompositeLearningUnit(skillsMap, "cu:B", [], [], [] + , [] + , []), + ]; + + compositeLus.find(lu => lu.id == "cu:A").children!.push( + compositeLus.find(lu => lu.id == "lu:1")!, + compositeLus.find(lu => lu.id == "lu:A")! + ) + + compositeLus.find(lu => lu.id == "cu:B").children!.push( + compositeLus.find(lu => lu.id == "lu:2")!, + compositeLus.find(lu => lu.id == "lu:3")! + ) + + computeSkills(compositeLus.filter(lu => lu.children.length > 0)); + + const goals = [ + ...skillsMap.filter(skill => skill.id === "sk:3"), + ...skillsMap.filter(skill => skill.id === "sk:6"), + ]; + + const path = getPath({ + skills: skillsMap, + learningUnits: compositeLus, + goal: goals, + optimalSolution: true, + contextSwitchPenalty: 1.2 + }); + + const expectedPath = [ + ...compositeLus.filter(skill => skill.id === "cu:B"), + ...compositeLus.filter(skill => skill.id === "cu:A"), + ]; + + expect(path.cost).toBe(4.18); + expect(path.path.length).toBe(2); + expect(path.path).toEqual(expectedPath); + }); + }); +}); + +function newLearningUnit( + map: Skill[], + id: string, + requiredSkills: string[], + teachingGoals: string[], + suggestedSkills: { weight: number; skill: string }[] = [] +): LearningUnit { + const suggestions: { weight: number; skill: Skill }[] = []; + if (suggestedSkills.length > 0) { + for (const suggestion of suggestedSkills) { + const skill = map.find(skill => suggestion.skill.includes(skill.id)); + if (skill) { + suggestions.push({ weight: suggestion.weight, skill: skill }); + } + } + } + + return { + id: id, + children: [], + requiredSkills: map.filter(skill => requiredSkills.includes(skill.id)), + teachingGoals: map.filter(skill => teachingGoals.includes(skill.id)), + suggestedSkills: suggestions, + getTeachingGoals: getTeachingGoals, + getRequiredSkills: getRequiredSkills, + getSuggestedSkills: getSuggestedSkills, + }; +} + +function newCompositeLearningUnit( + map: Skill[], + id: string, + requiredSkills: string[], + teachingGoals: string[], + suggestedSkills: { weight: number; skill: string }[] = [], + exportedRequiredSkills: string[], + exportedRecommendedSkills: { weight: number; skill: string }[] = [], +): LearningUnit { + const suggestions: { weight: number; skill: Skill }[] = []; + if (suggestedSkills.length > 0) { + for (const suggestion of suggestedSkills) { + const skill = map.find(skill => suggestion.skill.includes(skill.id)); + if (skill) { + suggestions.push({ weight: suggestion.weight, skill: skill }); + } + } + } + + const exportedSuggestions: { weight: number; skill: Skill }[] = []; + if (exportedRecommendedSkills.length > 0) { + for (const suggestion of exportedRecommendedSkills) { + const skill = map.find(skill => suggestion.skill.includes(skill.id)); + if (skill) { + exportedSuggestions.push({ weight: suggestion.weight, skill: skill }); + } + } + } + + return { + id: id, + children: [], + requiredSkills: map.filter(skill => requiredSkills.includes(skill.id)), + teachingGoals: map.filter(skill => teachingGoals.includes(skill.id)), + suggestedSkills: suggestions, + exportedRequiredSkills: map.filter(skill => exportedRequiredSkills.includes(skill.id)), + exportedRecommendedSkills: exportedSuggestions, + getTeachingGoals: getTeachingGoals, + getRequiredSkills: getRequiredSkills, + getSuggestedSkills: getSuggestedSkills, + }; +} \ No newline at end of file diff --git a/src/lib/compositeLearningUnit.ts b/src/lib/compositeLearningUnit.ts new file mode 100644 index 0000000..e905466 --- /dev/null +++ b/src/lib/compositeLearningUnit.ts @@ -0,0 +1,113 @@ +import { LearningUnit, Skill } from "./types"; + +export function computeSkills( + LearningUnits: LearningUnit[] +) { + LearningUnits.forEach(lu => { + lu.externalRequiredSkills = lu.requiredSkills.map(skill => skill); + computeUnusedRequiredSkills(lu); + + lu.externalRecommendedSkills = lu.suggestedSkills.map(skill => skill); + computeUnusedRecommendedSkills(lu); + + computeTaughtSkills(lu); + }); +} + +function computeTaughtSkills( + LU: LearningUnit +) { + let skills: Skill[] = LU.teachingGoals.map(skill => skill); + let skillsString: string[] = LU.teachingGoals.map(skill => skill.id); + + if (LU.children != undefined) { + LU.children.forEach(child => { + const teachingGoalsSkills = child.teachingGoals.filter(skill => !skillsString.includes(skill.id)); + skills = skills.concat(teachingGoalsSkills); + skillsString = skillsString.concat(teachingGoalsSkills.map(skill => skill.id)); + }); + } + + LU.taughtSkills = skills; +} + +export function computeUnusedRecommendedSkills( + LU: LearningUnit +) { + + let suggestions: { weight: number; skill: Skill }[] = []; + if (LU.children != undefined) { + LU.children.forEach(child => { + child.suggestedSkills.forEach(sug => { + const oneSuggestion = {weight: sug.weight, skill: sug.skill} + const suggestionExist = suggestions.filter(skill => skill.skill == oneSuggestion.skill + && skill.weight == oneSuggestion.weight); + if (suggestionExist.length == 0) { + suggestions = suggestions.concat(oneSuggestion); + } + }); + }); + } + + suggestions = suggestions.filter(skill => !(LU.exportedRecommendedSkills!.map(sk => sk.skill.id).includes(skill.skill.id) + && LU.exportedRecommendedSkills!.map(sk => sk.weight).includes(skill.weight))); + + LU.unusedRecommendedSkills = suggestions; +} + +function computeUnusedRequiredSkills( + LU: LearningUnit +) { + let skillsString: string[] = []; + + let skills: Skill[] = [];// = LU.requiredSkills.map(skill => skill); + //let skillsString: string[] = LU.teachingGoals.map(skill => skill.id); + + if (LU.children != undefined) { + LU.children.forEach(child => { + const requiredSkills = child.requiredSkills.filter(skill => !skillsString.includes(skill.id)); + skills = skills.concat(requiredSkills); + skillsString = skillsString.concat(requiredSkills.map(skill => skill.id)); + }); + } + + skills = skills.filter(skill => !LU.exportedRequiredSkills!.map(sk => sk.id).includes(skill.id)); + LU.unusedRequiredSkills = skills; +} + +export function getTeachingGoals(): Skill[] { + const LU: LearningUnit = this; + let skills: Skill[] = []; + if (LU.children.length > 0) { + skills = LU.taughtSkills!; + } else { + skills = LU.teachingGoals; + } + + return skills; +} + +export function getRequiredSkills(): Skill[] { + const LU: LearningUnit = this; + let skills: Skill[] = []; + if (LU.children.length > 0) { + skills = LU.externalRequiredSkills!.concat(LU.exportedRequiredSkills!); + } else { + skills = LU.requiredSkills; + } + + return skills; +} + +export function getSuggestedSkills(): { weight: number; skill: Skill }[] { + const LU: LearningUnit = this; + let skills: { weight: number; skill: Skill }[] = []; + + if (LU.children.length > 0) { + skills = LU.externalRecommendedSkills!.concat(LU.exportedRecommendedSkills!); + } else { + skills = LU.suggestedSkills; + } + + return skills; +} \ No newline at end of file diff --git a/src/lib/fastDownward/debugger.spec.ts b/src/lib/fastDownward/debugger.spec.ts index 35832cb..05c794d 100644 --- a/src/lib/fastDownward/debugger.spec.ts +++ b/src/lib/fastDownward/debugger.spec.ts @@ -8,6 +8,7 @@ import { DistanceMap } from "./distanceMap"; import { GlobalKnowledge } from "./global-knowledge"; import { SearchNode } from "./searchNode"; import { State } from "./state"; +import { getTeachingGoals, getRequiredSkills, getSuggestedSkills} from "../compositeLearningUnit" describe("toString() methods", () => { // Test data to be used across al tests @@ -98,8 +99,12 @@ function newLearningUnit( ) { return { id: id, + children: [], requiredSkills: map.filter(skill => requiredSkills.includes(skill.id)), teachingGoals: map.filter(skill => teachingGoals.includes(skill.id)), - suggestedSkills: [] + suggestedSkills: [], + getTeachingGoals: getTeachingGoals, + getRequiredSkills: getRequiredSkills, + getSuggestedSkills: getSuggestedSkills, }; } diff --git a/src/lib/fastDownward/distanceMap.spec.ts b/src/lib/fastDownward/distanceMap.spec.ts index 6dbf3be..9364ac2 100644 --- a/src/lib/fastDownward/distanceMap.spec.ts +++ b/src/lib/fastDownward/distanceMap.spec.ts @@ -1,5 +1,6 @@ import { LearningUnit, Skill } from "../types"; import { DistanceMap } from "./distanceMap"; +import { getTeachingGoals, getRequiredSkills, getSuggestedSkills} from "../compositeLearningUnit" describe("getDistance", () => { it("2 connected Units; No nesting", () => { @@ -80,8 +81,12 @@ function newLearningUnit( ) { return { id: id, + children: [], requiredSkills: map.filter(skill => requiredSkills.includes(skill.id)), teachingGoals: map.filter(skill => teachingGoals.includes(skill.id)), - suggestedSkills: [] + suggestedSkills: [], + getTeachingGoals: getTeachingGoals, + getRequiredSkills: getRequiredSkills, + getSuggestedSkills: getSuggestedSkills, }; } diff --git a/src/lib/fastDownward/distanceMap.ts b/src/lib/fastDownward/distanceMap.ts index eb3dc1e..7bbabf3 100644 --- a/src/lib/fastDownward/distanceMap.ts +++ b/src/lib/fastDownward/distanceMap.ts @@ -28,13 +28,14 @@ export class DistanceMap { graph.setEdge(childName, "sk" + skill.id); }); }); + learningUnits.forEach(lu => { graph.setNode("lu" + lu.id, lu); - lu.requiredSkills.forEach(req => { + lu.getRequiredSkills().forEach(req => { graph.setEdge("sk" + req.id, "lu" + lu.id); }); - lu.teachingGoals.forEach(goal => { + lu.getTeachingGoals().forEach(goal => { graph.setEdge("lu" + lu.id, "sk" + goal.id); }); }); diff --git a/src/lib/fastDownward/fastDownward.ts b/src/lib/fastDownward/fastDownward.ts index f327dae..2272a5b 100644 --- a/src/lib/fastDownward/fastDownward.ts +++ b/src/lib/fastDownward/fastDownward.ts @@ -3,6 +3,7 @@ import { SearchNode } from "./searchNode"; import { State } from "./state"; import { HeuristicFunction, CostFunction } from "./fdTypes"; import { GlobalKnowledge } from "./global-knowledge"; +import { getTeachingGoals, getRequiredSkills, getSuggestedSkills} from "../compositeLearningUnit" /** * Compute which LearningUnits are reachable based on the given state. @@ -16,11 +17,11 @@ function availableActions( // However, we can also check that we always learn at least one new skill const usefulLus = learningUnits .filter(unit => - unit.requiredSkills.every(skill => currentState.learnedSkills.includes(skill.id)) + unit.getRequiredSkills().every(skill => currentState.learnedSkills.includes(skill.id)) ) .filter(lu => // Do not suggest learning units that do not teach any unknown skills - lu.teachingGoals.some(skill => !currentState.learnedSkills.includes(skill.id)) + lu.getTeachingGoals().some(skill => !currentState.learnedSkills.includes(skill.id)) ); // // Do not suggest learning units that do not teach any unknown skills @@ -40,22 +41,24 @@ export function computeCost( const sameContext = contextSwitchPenalty !== 1 ? // Check if the current LU requires any skills that are provided by the LU of the currentNode, only if a penalty is defined - lu.teachingGoals.some( - skill => currentNode.action?.requiredSkills.includes(skill) ?? true + lu.getTeachingGoals().some( + skill => currentNode.action?.getRequiredSkills().includes(skill) ?? true ) : true; + const luCost = lu.children.length > 0 ? lu.children.length * 0.95 : 1; + const suggestionPenalty = suggestionViolationPenalty ? // Identify all missing suggested skills in the current state - 1 + - lu.suggestedSkills + luCost + + lu.getSuggestedSkills() .filter( suggestion => !currentNode.state.learnedSkills.includes(suggestion.skill.id) ) .map(suggestion => suggestion.weight) .reduce((a, b) => a + b, 0) : // No penalty for not following suggestions - 1; + luCost; const cost = sameContext ? // Same context or no penalty defined @@ -245,9 +248,13 @@ export function search( skillsNotFound.forEach(sk => remainSkills.push({ id: sk, repositoryId: "0", nestedSkills: [] })); const tempLU = { id: "-1", + children: [], requiredSkills: remainSkills, teachingGoals: [], suggestedSkills:[], + getTeachingGoals: getTeachingGoals, + getRequiredSkills: getRequiredSkills, + getSuggestedSkills: getSuggestedSkills, } noPath.path.push(tempLU); pathList.push(noPath); @@ -275,9 +282,13 @@ export function search( skillsNotFound.forEach(sk => remainSkills.push({ id: sk, repositoryId: "0", nestedSkills: [] })); const tempLU = { id: "-1", - requiredSkills: remainSkills, + children: [], + requiredSkills: remainSkills, teachingGoals: [], suggestedSkills:[], + getTeachingGoals: getTeachingGoals, + getRequiredSkills: getRequiredSkills, + getSuggestedSkills: getSuggestedSkills, } noPath.path.push(tempLU); pathList.push(noPath); diff --git a/src/lib/fastDownward/missingSkillDetection.ts b/src/lib/fastDownward/missingSkillDetection.ts index 099151a..c7ca15d 100644 --- a/src/lib/fastDownward/missingSkillDetection.ts +++ b/src/lib/fastDownward/missingSkillDetection.ts @@ -13,12 +13,12 @@ function availableActions( globalKnowledge: GlobalKnowledge ) { // Find the LearningUnits for each goal in the state using teachingGoals. - let usefulLus = learningUnits.filter(lu => lu.teachingGoals.some(skill => currentState.learnedSkills.includes(skill.id))); + let usefulLus = learningUnits.filter(lu => lu.getTeachingGoals().some(skill => currentState.learnedSkills.includes(skill.id))); // Find the LearningUnits for each goal in the state using globalKnowledge (Skill groups). globalKnowledge.getAllParents().forEach(parent => { if (currentState.getHashCode().includes(parent.id) ) { - const lu = learningUnits.filter(lu => lu.teachingGoals.some(skill => parent.nestedSkills.includes(skill.id))); + const lu = learningUnits.filter(lu => lu.getTeachingGoals().some(skill => parent.nestedSkills.includes(skill.id))); usefulLus = usefulLus.concat(lu); } }); @@ -60,12 +60,11 @@ export function skillAnalysis( const openList: SearchNode[] = [new SearchNode(initialState, null, null, 0, 0)]; - // goalString is a string with all the skills for the request goal (or skill) to analyze. + // goalArray is a array of string with all the skills for the request goal (or skill) to analyze. // Case 1: If a skill does not have requirements, then we can't find it through skill requirements tracing. // Case 2: If a skill depends on skill groups (parent/child), then we can't find it through skill requirements tracing. - // We use goalString to find the missing skills that do not have requirements and the skill that uses skill groups. - // Note: I used a string to avoid performance issues in finding and deleting (I will do more research to compare it with other alternatives) - let goalString = `,`.concat(initialState.learnedSkills.join(",").concat(`,`)); + // We use goalArray to find the missing skills that do not have requirements and the skill that uses skill groups. + const goalsArray = initialState.learnedSkills.slice(0); while (openList.length > 0) { currentNode = openList.shift()!; @@ -73,33 +72,35 @@ export function skillAnalysis( // Stop searching in a sub-path for a skill if we reached a skill without requirement. // Reaching a skill without requirement in a sub-path means that there is a path for learning this skill const unit = currentNode!.action!; - if (unit && unit.requiredSkills.length == 0) { + if (unit && unit.getRequiredSkills().length == 0) { continue; } for (const lu of availableActions(currentNode.state, learningUnits, globalKnowledge)) { - // Removing found (reachable) skills from the goalString - lu.teachingGoals.forEach(skill => { - goalString = goalString.replace(`,${skill.id},`,`,`); + // Removing found (reachable) skills from the goalsArray + lu.getTeachingGoals().forEach(skill => { + if (goalsArray.indexOf(skill.id) >= 0) { + goalsArray.splice(goalsArray.indexOf(skill.id), 1); - // Removing nested skills from the goalString if the parent skill is found (reachable) - skill.nestedSkills.forEach(nestedSkill => - goalString = goalString.replace(`,${nestedSkill},`,`,`) - ); + // Removing nested skills from the goalsArray if the parent skill is found (reachable) + skill.nestedSkills.forEach(nestedSkill => + goalsArray.splice(goalsArray.indexOf(nestedSkill), 1) + ); - // Removing parent skill from the goalString if the it has one child - // And that child skill is found (reachable) - const allParents = globalKnowledge.getAllParents(); - const parent = allParents.filter(parent => parent.nestedSkills.every(child => skill.id.includes(child))); - parent.forEach(sk => goalString = goalString.replace(`,${sk.id},`,`,`)); + // Removing parent skill from the goalsArray if the it has one child + // And that child skill is found (reachable) + const allParents = globalKnowledge.getAllParents(); + const parent = allParents.filter(parent => parent.nestedSkills.every(child => skill.id == child)); + parent.forEach(sk => goalsArray.splice(goalsArray.indexOf(sk.id), 1)); + } }); // Find in reverse order LearningUnits for the required skills - lu.requiredSkills.forEach(skill => { + lu.getRequiredSkills().forEach(skill => { // Find LearningUnits for the required skills - let requiredLus = learningUnits.filter(unit => unit.teachingGoals.map(sk => sk.id).includes(skill.id)); + let requiredLus = learningUnits.filter(unit => unit.getTeachingGoals().map(sk => sk.id).includes(skill.id)); // Check the nested skills (Skill groups) in globalKnowledge for the required skill if (requiredLus.length == 0) { @@ -127,25 +128,25 @@ export function skillAnalysis( } } - // If goalString still has some goals, then we check the skill groups in globalKnowledge + // If goalsArray still has some goals, then we check the skill groups in globalKnowledge // To remove parent skill if all his children is found (Reachable) - // We do loop over the goal skills, to void the order of the skills until no more changes on the goalString - if (goalString.length > 0) { + // We do loop over the goal skills, to void the order of the skills until no more changes on the goalsArray + if (goalsArray.length > 0) { while (true) { - const goalSkills = goalString.split(",").filter(skill => skill.length > 0); + const goalSkills = goalsArray.slice(0); let goalSkillsCount = goalSkills.length; goalSkills.forEach(sk => { const parentSkill = globalKnowledge.getAllParents().find(parentSkill => parentSkill.id == sk); if (parentSkill) { let parentSkillCount = parentSkill.nestedSkills.length; parentSkill.nestedSkills.forEach(skill => { - if (goalString.includes(`,`.concat(skill).concat(`,`))) { + if (goalsArray.includes(skill)) { parentSkillCount--; } } ); if (parentSkillCount == parentSkill.nestedSkills.length) { - goalString = goalString.replace(`,${sk},`,`,`); + goalsArray.splice(goalsArray.indexOf(sk), 1) goalSkillsCount--; } } else { @@ -157,7 +158,7 @@ export function skillAnalysis( break; } } - goalString.split(",").filter(skill => skill.length > 0).forEach(sk => { + goalsArray.filter(skill => skill.length > 0).forEach(sk => { if (!openListMap.has(sk)) { openListMap.set(sk, -1); } diff --git a/src/lib/fastDownward/state.ts b/src/lib/fastDownward/state.ts index ae02d68..cac0855 100644 --- a/src/lib/fastDownward/state.ts +++ b/src/lib/fastDownward/state.ts @@ -87,7 +87,7 @@ export class State { mergedSkills.push(skill); }); - operator.teachingGoals.forEach(goal => { + operator.getTeachingGoals().forEach(goal => { if (!mergedSkills.includes(goal.id)) { mergedSkills.push(goal.id); } diff --git a/src/lib/pathPlanner.spec.ts b/src/lib/pathPlanner.spec.ts index 83245e1..6941950 100644 --- a/src/lib/pathPlanner.spec.ts +++ b/src/lib/pathPlanner.spec.ts @@ -10,6 +10,7 @@ import { getSkillAnalysis } from "./pathPlanner"; import { CostFunction } from "./fastDownward/fdTypes"; +import { getTeachingGoals, getRequiredSkills, getSuggestedSkills} from "./compositeLearningUnit" describe("Path Planer", () => { // Re-usable test data (must be passed to dataHandler.init() before each test) @@ -1608,8 +1609,12 @@ function newLearningUnit( return { id: id, + children: [], requiredSkills: map.filter(skill => requiredSkills.includes(skill.id)), teachingGoals: map.filter(skill => teachingGoals.includes(skill.id)), - suggestedSkills: suggestions + suggestedSkills: suggestions, + getTeachingGoals: getTeachingGoals, + getRequiredSkills: getRequiredSkills, + getSuggestedSkills: getSuggestedSkills, }; } diff --git a/src/lib/pathPlanner.ts b/src/lib/pathPlanner.ts index 12ccb32..beb3a96 100644 --- a/src/lib/pathPlanner.ts +++ b/src/lib/pathPlanner.ts @@ -226,16 +226,16 @@ function populateGraph({ learningUnits.forEach(lu => { const luName = "lu" + lu.id; graph.setNode("lu" + lu.id, lu); - lu.requiredSkills.forEach(req => { + lu.getRequiredSkills().forEach(req => { graph.setEdge("sk" + req.id, luName); }); - lu.teachingGoals.forEach(goal => { + lu.getTeachingGoals().forEach(goal => { graph.setEdge(luName, "sk" + goal.id); }); if (suggestions) { - lu.suggestedSkills.forEach(suggestion => { + lu.getSuggestedSkills().forEach(suggestion => { // Analogous to requirements: Skill -> LearningUnit graph.setEdge("sk" + suggestion.skill.id, luName); }); @@ -278,12 +278,12 @@ export async function computeSuggestedSkills( for (let i = 1; i < learningUnits.length; i++) { const previousUnit = learningUnits[i - 1]; const currentUnit = learningUnits[i]; - const missingSkills = previousUnit.teachingGoals + const missingSkills = previousUnit.getTeachingGoals() .map(goal => goal.id) // Do not copy hard constraints also to soft constraints - .filter(goalId => !currentUnit.requiredSkills.map(skill => skill.id).includes(goalId)) + .filter(goalId => !currentUnit.getRequiredSkills().map(skill => skill.id).includes(goalId)) // Do not copy currently taught skills to avoid cycles - .filter(goalId => !currentUnit.teachingGoals.map(skill => skill.id).includes(goalId)); + .filter(goalId => !currentUnit.getTeachingGoals().map(skill => skill.id).includes(goalId)); await fnUpdate(currentUnit, missingSkills); } diff --git a/src/lib/types.ts b/src/lib/types.ts index fe8219b..7609906 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -20,11 +20,22 @@ export type Edge = { export type LearningUnit = { id: string; + children: LearningUnit[]; mediaTime?: number; words?: number; requiredSkills: Skill[]; teachingGoals: Skill[]; suggestedSkills: { weight: number; skill: Skill }[]; + externalRequiredSkills?: Skill[]; + exportedRequiredSkills?: Skill[]; + unusedRequiredSkills?: Skill[]; + taughtSkills?: Skill[]; + externalRecommendedSkills?: { weight: number; skill: Skill }[]; + exportedRecommendedSkills?: { weight: number; skill: Skill }[]; + unusedRecommendedSkills?: { weight: number; skill: Skill }[]; + getTeachingGoals(): Skill[]; + getRequiredSkills(): Skill[]; + getSuggestedSkills(): { weight: number; skill: Skill }[]; }; export const isLearningUnit = (element: Skill | LearningUnit): element is LearningUnit => {