Skip to content

Commit

Permalink
refactor: use shallow copied engine to compute alternatives
Browse files Browse the repository at this point in the history
Instead of generating all publicodes rules from the combination from
(size, motorisation and fuel) and using `contexte` (which is very
costly in term of computation) do the combination at runtime and using
`setSituation` on a shallow copied local engine.
  • Loading branch information
EmileRolley committed Feb 26, 2025
1 parent ce385ef commit 03ddd45
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 75 deletions.
28 changes: 14 additions & 14 deletions scripts/precompile.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { writeFileSync } from "fs"
import { join } from "path"
import { stringify } from "yaml"
// import { stringify } from "yaml"
import { getModelFromSource } from "@publicodes/tools/compilation"
import Engine from "publicodes"

import getPersonas from "./compile-personas.js"
import generateAlternatives from "./generate-alternatives.js"
// import generateAlternatives from "./generate-alternatives.js"

const ROOT_PATH = new URL(".", import.meta.url).pathname
const SRC_FILES = join(ROOT_PATH, "../src/rules/")
const ALTERNATIVES_DEST_PATH = join(
ROOT_PATH,
"../src/rules/alternatives.publicodes",
)
// const ALTERNATIVES_DEST_PATH = join(
// ROOT_PATH,
// "../src/rules/alternatives.publicodes",
// )
const PERSONAS_DEST_PATH = join(ROOT_PATH, "../src/personas/personas.json")

const model = getModelFromSource(SRC_FILES)
Expand All @@ -25,14 +25,14 @@ const resolvedRules = Object.fromEntries(
}),
)

const alternatives = generateAlternatives(resolvedRules)
console.log(`✅ './src/rules/alternatives.publicodes' generated`)
writeFileSync(
ALTERNATIVES_DEST_PATH,
`# GENERATED FILE - DO NOT EDIT\n\n${stringify(alternatives, {
aliasDuplicateObjects: false,
})}`,
)
// const alternatives = generateAlternatives(resolvedRules)
// console.log(`✅ './src/rules/alternatives.publicodes' generated`)
// writeFileSync(
// ALTERNATIVES_DEST_PATH,
// `# GENERATED FILE - DO NOT EDIT\n\n${stringify(alternatives, {
// aliasDuplicateObjects: false,
// })}`,
// )

const personas = getPersonas(resolvedRules)
writeFileSync(PERSONAS_DEST_PATH, JSON.stringify(personas))
Expand Down
148 changes: 87 additions & 61 deletions src/CarSimulator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Engine, {
Possibility,
Situation as PublicodesSituation,
serializeUnit,
} from "publicodes"
Expand Down Expand Up @@ -82,15 +83,6 @@ const engineLogger = {
error: (message: string) => console.error(message),
}

export const RULE_NAMES = Object.keys(rules) as RuleName[]
export const ALTERNATIVES_VOITURE_NAMESPACE: RuleName = "alternatives . voiture"
export const ALTERNATIVES_RULES = RULE_NAMES.filter(
(rule) =>
rule.startsWith(ALTERNATIVES_VOITURE_NAMESPACE) &&
`${rule} . coûts` in rules &&
`${rule} . empreinte` in rules,
)

/**
* A wrapper around the {@link Engine} class to compute the available aids for the
* given inputs (which are a subset of the Publicodes situation corresponding to
Expand Down Expand Up @@ -148,9 +140,7 @@ export class CarSimulator {
...inputs,
}
}
this.engine.setSituation(
getSituation(this.inputs) as PublicodesSituation<RuleName>,
)
this.engine.setSituation(getSituation(this.inputs))
return this
}

Expand Down Expand Up @@ -193,55 +183,49 @@ export class CarSimulator {
* @note This method is an expensive operation.
*/
public evaluateAlternatives(): Alternative[] {
const infos = ALTERNATIVES_RULES.map((rule: RuleName) => {
const splittedRule = rule.split(" . ").slice(2)
const sizeOption = splittedRule[0] as Questions["voiture . gabarit"]
const motorisationOption =
splittedRule[1] as Questions["voiture . motorisation"]
const fuelOption = (
motorisationOption !== "électrique" ? splittedRule[2] : undefined
) as Questions["voiture . thermique . carburant"]
const localEngine = this.getEngine().shallowCopy()
const localSituation = localEngine.getSituation()
const carSizes = this.getEngine().getPossibilitiesFor("voiture . gabarit")!
const carMotorisations = this.getEngine().getPossibilitiesFor(
"voiture . motorisation",
)!
const carFuels = this.getEngine().getPossibilitiesFor(
"voiture . thermique . carburant",
)!

if (!sizeOption || !motorisationOption) {
throw new Error(
`Invalid alternative rule ${rule}. It should have a size and a motorisation option.`,
)
}
const res = []

return {
kind: "car",
title: this.engine.getRule(rule).title,
cost: this.evaluateRule(ruleName(rule, "coûts")),
emissions: this.evaluateRule(ruleName(rule, "empreinte")),
size: {
value: sizeOption,
title: this.engine.getRule(ruleName("voiture . gabarit", sizeOption))
.title,
isEnumValue: true,
isApplicable: true,
},
motorisation: {
value: motorisationOption,
title: this.engine.getRule(
ruleName("voiture . motorisation", motorisationOption),
).title,
isEnumValue: true,
isApplicable: true,
},
fuel: fuelOption
? {
value: fuelOption,
title: this.engine.getRule(
ruleName("voiture . thermique . carburant", fuelOption),
).title,
isEnumValue: true,
isApplicable: true,
}
: undefined,
} as Alternative
})
// NOTE: we want to use default values for the alternatives as they are
// specific for each alternative.
delete localSituation["voiture . prix d'achat"]
delete localSituation["voiture . électrique . consommation électricité"]
delete localSituation["voiture . thermique . consommation carburant"]
for (const size of carSizes) {
localSituation["voiture . gabarit"] =
size.publicodesValue as Situation["voiture . gabarit"]
for (const motorisation of carMotorisations) {
localSituation["voiture . motorisation"] =
motorisation.publicodesValue as Situation["voiture . motorisation"]
if (motorisation.nodeValue === "électrique") {
localEngine.setSituation(
localSituation as PublicodesSituation<RuleName>,
)

res.push(getAlternative(localEngine, size, motorisation, undefined))
} else {
for (const fuel of carFuels) {
localSituation["voiture . thermique . carburant"] =
fuel.publicodesValue

return infos
localEngine.setSituation(localSituation)

res.push(getAlternative(localEngine, size, motorisation, fuel))
}
}
}
}

return res
}

/**
Expand Down Expand Up @@ -333,7 +317,7 @@ export class CarSimulator {
}
}

function getSituation(inputs: Questions): Situation {
function getSituation(inputs: Questions): PublicodesSituation<RuleName> {
return Object.fromEntries(
Object.entries(inputs)
.filter(([, value]) => value !== undefined)
Expand All @@ -350,6 +334,48 @@ function getSituation(inputs: Questions): Situation {
)
}

function ruleName(namespace: RuleName, rule: string): RuleName {
return (namespace + " . " + rule) as RuleName
function getAlternative(
engine: Engine,
size: Possibility,
motorisation: Possibility,
fuel?: Possibility,
): Alternative {
return {
kind: "car",
title: `${size.title} ${motorisation.title}${fuel ? ` (${fuel.title})` : ""}`,
cost: {
title: "Coûts annuels",
unit: "€/an",
isEnumValue: false,
isApplicable: true,
value: engine.evaluate("coûts").nodeValue,
},
emissions: {
title: "Empreinte CO2e",
unit: "kgCO2e/an",
isEnumValue: false,
isApplicable: true,
value: engine.evaluate("empreinte").nodeValue,
},
size: {
value: size.nodeValue,
title: size.title,
isEnumValue: true,
isApplicable: true,
},
motorisation: {
value: motorisation.nodeValue,
title: motorisation.title,
isEnumValue: true,
isApplicable: true,
},
fuel: fuel
? {
value: fuel.nodeValue,
title: fuel.title,
isEnumValue: true,
isApplicable: true,
}
: undefined,
} as Alternative
}
92 changes: 92 additions & 0 deletions test/CarSimulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,11 @@ describe("CarSimulator", () => {
describe("evaluateAlternatives()", () => {
test("should return all possible alternatives with default values", () => {
const engine = globalTestEngine.shallowCopy()
engine.setInputs({ "usage . km annuels . renseignés": 1000000 })
console.time("evaluateAlternatives")
const alternatives = engine.evaluateAlternatives()
console.timeEnd("evaluateAlternatives")

// TODO: use engine.getOptions
const nbMotorisations = 3
const nbFuels = 4
Expand Down Expand Up @@ -305,6 +307,96 @@ describe("CarSimulator", () => {
}
})
})

test("increasing the annual distance should increase the cost and emissions", () => {
const engine = globalTestEngine.shallowCopy()
engine.setInputs({ "usage . km annuels . renseignés": 10 })
const alternatives = engine.evaluateAlternatives()

engine.setInputs({ "usage . km annuels . renseignés": 10000 })
const newAlternatives = engine.evaluateAlternatives()

expect(alternatives).toHaveLength(newAlternatives.length)
alternatives.forEach((alternative, i) => {
expect(alternative.cost.value).toBeLessThan(
newAlternatives[i].cost.value!,
)
expect(alternative.emissions.value).toBeLessThan(
newAlternatives[i].emissions.value!,
)
})
})

test("set km to 0 should return 0 for emissions", () => {
const engine = globalTestEngine.shallowCopy()
engine.setInputs({
"usage . km annuels . renseignés": 0,
"usage . km annuels . connus": true,
})
const alternatives = engine.evaluateAlternatives()

alternatives.forEach((alternative) => {
expect(alternative.emissions.value).toEqual(0)
})
})

test("modify the consumption shouldn't modify the cost and emissions", () => {
const engine = globalTestEngine.shallowCopy()
const alternatives = engine.evaluateAlternatives()

engine.setInputs({
"voiture . électrique . consommation électricité": 4,
"voiture . thermique . consommation carburant": 10,
})
const newAlternatives = engine.evaluateAlternatives()

expect(alternatives).toHaveLength(newAlternatives.length)
alternatives.forEach((alternative, i) => {
expect(alternative.cost.value).toEqual(newAlternatives[i].cost.value)
expect(alternative.emissions.value).toEqual(
newAlternatives[i].emissions.value,
)
})
})

test("modify the fuel price should modify the cost", () => {
const engine = globalTestEngine.shallowCopy()
const alternatives = engine.evaluateAlternatives()

engine.setInputs({
"voiture . thermique . prix carburant": 20,
"voiture . électrique . prix kWh": 20,
})
const newAlternatives = engine.evaluateAlternatives()

expect(alternatives).toHaveLength(newAlternatives.length)
alternatives.forEach((alternative, i) => {
expect(alternative.cost.value).toBeLessThan(
newAlternatives[i].cost.value!,
)
expect(alternative.emissions.value).toEqual(
newAlternatives[i].emissions.value,
)
})
})

test("modify the car price shouldn't modify the cost", () => {
const engine = globalTestEngine.shallowCopy()
const alternatives = engine.evaluateAlternatives()

engine.setInputs({
"voiture . prix d'achat": 20000,
})
const newAlternatives = engine.evaluateAlternatives()

expect(alternatives).toHaveLength(newAlternatives.length)
alternatives.forEach((alternative, i) => {
expect(alternative.cost.value).toEqual(newAlternatives[i].cost.value)
expect(alternative.emissions.value).toEqual(
newAlternatives[i].emissions.value,
)
})
})
})

describe("evaluateTargetCar()", () => {
Expand Down

0 comments on commit 03ddd45

Please sign in to comment.