Skip to content

Commit

Permalink
Merge pull request #13 from betagouv/fix-improve-perfs
Browse files Browse the repository at this point in the history
  • Loading branch information
EmileRolley authored Feb 26, 2025
2 parents 9634141 + 03ddd45 commit 210cecf
Show file tree
Hide file tree
Showing 13 changed files with 287 additions and 168 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/compile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ jobs:
name: Compile and test
run: yarn test

- id: compile
name: Compile
run: yarn compile

- name: Run pkg.pr.new
run: npx pkg-pr-new publish
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,30 @@ yarn test

#### Lancer la documentation

> [!TIP]
> Pour facilement parcourir la documentation et tester différentes situations pendant
> le développement, il est conseillé d'utiliser la commande `yarn dev` à la place.
> La `online-doc` sera bientôt dépréciée.
> Le code de la documentation est disponible dans le dossier
> [`online-doc/`](https://github.com/betagouv/publicodes-voiture/tree/main/online-doc).
Pour lancer l'app React en local permettant de parcourir la documentation du
modèle, il suffit d'exécuter la commande suivante :

```
yarn install --cwd doc
yarn doc
```

## Publier une nouvelle version

Afin de publier une nouvelle version il suffit d'exécuter la commande `yarn
version`.

```
```
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@
"compile:rules": "publicodes compile src/rules",
"compile": "yarn compile:rules && tsup",
"test": "yarn compile:rules && vitest run",
"dev": "publicodes dev",
"doc": "yarn run compile && cd online-doc && yarn run dev",
"doc:build": "yarn run compile && cd online-doc && yarn run build"
},
"devDependencies": {
"@incubateur-ademe/nosgestesclimat": "^3.5.4",
"@incubateur-ademe/publicodes-commun": "^1.1.5",
"@publicodes/tools": "^1.5.3",
"@publicodes/tools": "https://pkg.pr.new/publicodes/publicodes/@publicodes/tools@1858c5e",
"csv-parser": "^3.0.0",
"terser": "^5.36.0",
"tsup": "^8.3.5",
Expand All @@ -62,7 +63,7 @@
"access": "public"
},
"dependencies": {
"publicodes": "^1.7.2"
"publicodes": "https://pkg.pr.new/publicodes/publicodes@56e0eea"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
8 changes: 5 additions & 3 deletions scripts/compile-personas.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { readFileSync } from "fs"
* Parses the personas.yaml file
*/
export default function getPersonas(rules) {
const personas = parse(readFileSync("personas.yaml", "utf-8"))
const personas = parse(
readFileSync("situations/personas.publicodes", "utf-8"),
)
let error = false

Object.entries(personas).forEach(([personaName, persona]) => {
Expand All @@ -15,10 +17,10 @@ export default function getPersonas(rules) {
if (!persona.description) {
console.warn(`[getPersonas] '${personaName}' has no description`)
}
if (!persona.situation) {
if (!persona.contexte) {
console.warn(`[getPersonas] '${personaName}' has no situation`)
} else {
Object.entries(persona.situation).forEach(([name, value]) => {
Object.entries(persona.contexte).forEach(([name, value]) => {
if (!(name in rules)) {
console.error(
`[getPersonas] '${personaName}' has an unknown rule '${name}'`,
Expand Down
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
14 changes: 7 additions & 7 deletions personas.yaml → situations/personas.publicodes
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
valeur par défaut:
titre: Valeur par défaut
situation: {}
empreinte: 3022.851504292707
coûts: 6370.257297587041
contexte: {}

aller-retours travail électrique ville:
titre: Aller-retours travail en ville (électrique)
description: |
Personne qui va travailler avec une petite voiture électrique en ville
situation:
contexte:
voiture . motorisation: "'électrique'"
voiture . gabarit: "'petite'"
voiture . prix d'achat: 5400
Expand All @@ -29,7 +29,7 @@ aller-retours travail électrique ville occasion:
titre: Aller-retours travail en ville (électrique) - Occasion
description: |
Personne qui va travailler avec une petite voiture électrique en ville
situation:
contexte:
voiture . motorisation: "'électrique'"
voiture . gabarit: "'petite'"
voiture . prix d'achat: 5400
Expand All @@ -45,7 +45,7 @@ grandes vacances familiales:
description: |
Personne qui utilise une voiture essence pour partir en vacances en
famille.
situation:
contexte:
voiture . motorisation: "'thermique'"
voiture . thermique . carburant: "'essence E5 ou E10'"
voiture . gabarit: "'berline'"
Expand All @@ -63,7 +63,7 @@ famille usage régulier diesel:
Elle a besoin d'une voiture spacieuse pour transporter ses enfants à l'école
et aller au travail.
situation:
contexte:
voiture . motorisation: "'thermique'"
voiture . thermique . carburant: "'gazole B7 ou B10'"
voiture . gabarit: "'berline'"
Expand All @@ -81,7 +81,7 @@ famille usage régulier biocarburant:
Elle a besoin d'une voiture spacieuse pour transporter ses enfants à l'école
et aller au travail.
situation:
contexte:
voiture . motorisation: "'thermique'"
voiture . thermique . carburant: "'essence E85'"
voiture . gabarit: "'berline'"
Expand All @@ -99,7 +99,7 @@ famille usage régulier moyenne biocarburant:
Elle a besoin d'une voiture spacieuse pour transporter ses enfants à l'école
et aller au travail.
situation:
contexte:
voiture . motorisation: "'thermique'"
voiture . thermique . carburant: "'essence E85'"
voiture . gabarit: "'moyenne'"
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
}
2 changes: 1 addition & 1 deletion src/personas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import _rawPersonas from "./personas.json"
export type Persona = {
titre: string
description?: string
situation: Situation
contexte: Situation
empreinte: number
coûts: number
}
Expand Down
Loading

0 comments on commit 210cecf

Please sign in to comment.