initial commit

This commit is contained in:
Julien Oculi 2025-07-21 10:56:11 +02:00
commit 4b5c0e3528
68 changed files with 183191 additions and 0 deletions

3806
datas/Bidon_07-07_16-22.csv Normal file

File diff suppressed because it is too large Load diff

3830
datas/Rauzan_07-07_16-37.csv Normal file

File diff suppressed because it is too large Load diff

3839
datas/Rauzan_07-07_16-50.csv Normal file

File diff suppressed because it is too large Load diff

3783
datas/Rauzan_07-07_16-56.csv Normal file

File diff suppressed because it is too large Load diff

3841
datas/Rauzan_07-07_17-04.csv Normal file

File diff suppressed because it is too large Load diff

3808
datas/Rauzan_08-07_14-52.csv Normal file

File diff suppressed because it is too large Load diff

3836
datas/Rauzan_08-07_14-58.csv Normal file

File diff suppressed because it is too large Load diff

3830
datas/Rauzan_08-07_15-04.csv Normal file

File diff suppressed because it is too large Load diff

3776
datas/Rauzan_08-07_15-40.csv Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

3718
datas/Rauzan_08-07_15-48.csv Normal file

File diff suppressed because it is too large Load diff

3840
datas/Rauzan_08-07_16-06.csv Normal file

File diff suppressed because it is too large Load diff

3805
datas/VffF1_27-06_15-51.csv Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

3826
datas/Vrr3_16-06_14-04.csv Normal file

File diff suppressed because it is too large Load diff

3898
datas/Vrr3_16-06_14-11.csv Normal file

File diff suppressed because it is too large Load diff

3879
datas/Vrr3_16-06_14-20.csv Normal file

File diff suppressed because it is too large Load diff

3818
datas/Vrr3_16-06_14-31.csv Normal file

File diff suppressed because it is too large Load diff

3885
datas/Vrr3_16-06_14-39.csv Normal file

File diff suppressed because it is too large Load diff

3869
datas/Vrr3_16-06_14-46.csv Normal file

File diff suppressed because it is too large Load diff

3841
datas/Vrr3_16-06_14-55.csv Normal file

File diff suppressed because it is too large Load diff

3844
datas/Vrr3_16-06_15-02.csv Normal file

File diff suppressed because it is too large Load diff

3856
datas/Vrr3_16-06_15-09.csv Normal file

File diff suppressed because it is too large Load diff

3887
datas/Vrr3_16-06_15-17.csv Normal file

File diff suppressed because it is too large Load diff

3851
datas/Vrr3_16-06_15-24.csv Normal file

File diff suppressed because it is too large Load diff

3874
datas/Vrr3_16-06_15-43.csv Normal file

File diff suppressed because it is too large Load diff

3859
datas/Vrr3_16-06_15-49.csv Normal file

File diff suppressed because it is too large Load diff

3871
datas/Vrr3_16-06_15-56.csv Normal file

File diff suppressed because it is too large Load diff

1228
datas/Vrr3_16-06_16-14.csv Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
horodatage
16 juin 2025 16:20:49
volume de vin filtré en mL;134.01
resultat IPAM;nan
num point ;Pression;Poids;Temps
1 horodatage
2 16 juin 2025 16:20:49
3 volume de vin filtré en mL;134.01
4 resultat IPAM;nan
5 num point ;Pression;Poids;Temps

3826
datas/Vrr3_16-06_16-33.csv Normal file

File diff suppressed because it is too large Load diff

3887
datas/Vrr3_16-06_16-44.csv Normal file

File diff suppressed because it is too large Load diff

3833
datas/Vrr3_16-06_16-54.csv Normal file

File diff suppressed because it is too large Load diff

3722
datas/Vrr3_16-06_17-00.csv Normal file

File diff suppressed because it is too large Load diff

10
deno.json Normal file
View file

@ -0,0 +1,10 @@
{
"fmt": {
"semiColons": false,
"useTabs": true,
"singleQuote": true
},
"imports": {
"@std/csv": "jsr:@std/csv@^1.0.6"
}
}

98
deno.lock Normal file
View file

@ -0,0 +1,98 @@
{
"version": "5",
"specifiers": {
"jsr:@opensrc/deno-open@*": "1.0.0",
"jsr:@std/assert@~0.218.2": "0.218.2",
"jsr:@std/csv@*": "1.0.6",
"jsr:@std/csv@^1.0.6": "1.0.6",
"jsr:@std/path@~0.218.2": "0.218.2",
"jsr:@std/streams@^1.0.9": "1.0.9",
"npm:mathjs@*": "14.4.0",
"npm:webview@*": "1.1.0"
},
"jsr": {
"@opensrc/deno-open@1.0.0": {
"integrity": "590decada8cf2d50f4b35c7d3a8d642e18e45d6e47078ea02a4dbf0096bbf9fd",
"dependencies": [
"jsr:@std/path"
]
},
"@std/assert@0.218.2": {
"integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf"
},
"@std/csv@1.0.6": {
"integrity": "52ef0e62799a0028d278fa04762f17f9bd263fad9a8e7f98c14fbd371d62d9fd",
"dependencies": [
"jsr:@std/streams"
]
},
"@std/path@0.218.2": {
"integrity": "b568fd923d9e53ad76d17c513e7310bda8e755a3e825e6289a0ce536404e2662",
"dependencies": [
"jsr:@std/assert"
]
},
"@std/streams@1.0.9": {
"integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035"
}
},
"npm": {
"@babel/runtime@7.27.1": {
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog=="
},
"complex.js@2.4.2": {
"integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g=="
},
"decimal.js@10.5.0": {
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="
},
"escape-latex@1.2.0": {
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
},
"fraction.js@5.2.2": {
"integrity": "sha512-uXBDv5knpYmv/2gLzWQ5mBHGBRk9wcKTeWu6GLTUEQfjCxO09uM/mHDrojlL+Q1mVGIIFo149Gba7od1XPgSzQ=="
},
"javascript-natural-sort@0.7.1": {
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
},
"mathjs@14.4.0": {
"integrity": "sha512-CpoYDhNENefjIG9wU9epr+0pBHzlaySfpWcblZdAf5qXik/j/U8eSmx/oNbmXO0F5PyfwPGVD/wK4VWsTho1SA==",
"dependencies": [
"@babel/runtime",
"complex.js",
"decimal.js",
"escape-latex",
"fraction.js",
"javascript-natural-sort",
"seedrandom",
"tiny-emitter",
"typed-function"
],
"bin": true
},
"seedrandom@3.0.5": {
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"tiny-emitter@2.1.0": {
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"typed-function@4.2.1": {
"integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA=="
},
"webview@1.1.0": {
"integrity": "sha512-9wYyizAuFoJjHqSnUR7rxND/WIbG1RBHMhYU5c8nWnQQUs1U8J5jpHR4lGn/qgQ1jt/XXQGXnm9U53WJcNMEng==",
"dependencies": [
"yargs-parser"
],
"bin": true
},
"yargs-parser@20.2.9": {
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="
}
},
"workspace": {
"dependencies": [
"jsr:@std/csv@^1.0.6"
]
}
}

103
main.ts Normal file
View file

@ -0,0 +1,103 @@
import { Data as PlotlyData, Plot } from './plot/mod.ts'
type Data = {
timestamp: string //ISO string
volume: number //mL
ipam: number //ipam
record: {
pression: number //bar
mass: number //g
timestamp: number //s
}[]
}
function parseDatas(file: string): Data {
const lines = file.split('\n')
lines.shift() //remove title
const [day, month, year, time] = lines.shift()!.split(' ')
const monthList = [
'_',
'janvier',
'fevrier',
'mars',
'avril',
'mai',
'juin',
'juillet',
'aout',
'septembre',
'decembre',
]
const timestamp = `${year}-${
monthList.indexOf(month).toString().padStart(2, '0')
}-${day.padStart(2, '0')}T${time}`
lines.shift() //remove empty line
const volume = lines.shift()!.split(';').slice(1).map(Number).at(0)!
const ipam = lines.shift()!.split(';').slice(1).map(Number).at(0)!
lines.shift() //remove empty line
lines.shift() //remove title
const record = lines
.map((line) => line.split(';').slice(1).map(Number))
.map(([pression, mass, timestamp]) => ({ pression, mass, timestamp }))
return {
timestamp,
volume,
ipam,
record,
}
}
function getDatasPlot(datas: Data): Partial<PlotlyData>[] {
const relativeTimestamp = datas.record.map((data) =>
data.timestamp - datas.record[0].timestamp
)
const meta = `${datas.volume}mL ${datas.ipam}IPAM ${
datas.timestamp.split('T')[1]
}`
const { ipam } = datas
return [
{
x: relativeTimestamp, //.map(Math.log10),
y: datas.record.map((data) => data.mass),
name: `mass (${meta})`,
line: {
color: `rgb(${50 * Math.log(ipam)}, 0, ${255 - 50 * Math.log(ipam)})`,
},
},
// {
// x: relativeTimestamp,
// y: datas.record.map((data) => data.pression),
// name: `pression (${meta})`,
// yaxis: 'y2',
// line: {
// color: `rgb(${50 * Math.log(ipam)}, 0, ${255 - 50 * Math.log(ipam)})`,
// },
// },
]
}
const plot = new Plot()
const plots: Partial<PlotlyData>[] = []
for await (const file of Deno.readDir('./datas')) {
if (!file.isFile) continue
if (!file.name.match(/Vrr3_\d{2}-\d{2}_\d{2}-\d{2}\.csv/)) continue
const path = `./datas/${file.name}`
const csv = await Deno.readTextFile(path)
const datas = parseDatas(csv)
plots.push(...getDatasPlot(datas))
}
plot.plot(plots, {
xaxis: { title: { text: 'timestamp' } },
yaxis: { title: { text: 'mass [g]' } },
yaxis2: { title: { text: 'pression [bar]' }, side: 'right' },
})

150
measure_clean_compa.ts Normal file
View file

@ -0,0 +1,150 @@
import { Data as PlotlyData, Plot } from './plot/mod.ts'
type Data = {
timestamp: string //ISO string
volume: number //mL
ipam: number //ipam
record: {
pression: number //bar
mass: number //g
timestamp: number //s
}[]
}
function parseDatas(file: string): Data {
const lines = file.split('\n')
lines.shift() //remove title
const [day, month, year, time] = lines.shift()!.split(' ')
const monthList = [
'_',
'janvier',
'fevrier',
'mars',
'avril',
'mai',
'juin',
'juillet',
'aout',
'septembre',
'decembre',
]
const timestamp = `${year}-${
monthList.indexOf(month).toString().padStart(2, '0')
}-${day.padStart(2, '0')}T${time}`
lines.shift() //remove empty line
const volume = lines.shift()!.split(';').slice(1).map(Number).at(0)!
const ipam = lines.shift()!.split(';').slice(1).map(Number).at(0)!
lines.shift() //remove empty line
lines.shift() //remove title
const record = lines
.map((line) => line.split(';').slice(1).map(Number))
.map(([pression, mass, timestamp]) => ({ pression, mass, timestamp }))
return {
timestamp,
volume,
ipam,
record,
}
}
function filterRecord(record: Data['record'], ceil = 0.4): Data['record'] {
const filteredRecord: Data['record'] = []
record.forEach((value, index) => {
if (index === 0) filteredRecord.push(value)
if (value.mass < 10) filteredRecord.push(value)
const previousMass = filteredRecord.at(-1)?.mass ?? value.mass
const delta = Math.abs(value.mass - previousMass) / value.mass
if (delta >= ceil) filteredRecord.push({ ...value, mass: previousMass })
else filteredRecord.push(value)
})
return filteredRecord
}
function cutRecord(record: Data['record']): Data['record'] {
let minMass = record[0].mass
let minMassIndex = 0
for (const value of record) {
minMassIndex++
if (value.mass < minMass) minMass = value.mass
if (value.mass > minMass) break
}
return record.slice(minMassIndex)
}
function getDatasPlot(datas: Data): Partial<PlotlyData>[] {
const record = cutRecord(filterRecord(datas.record))
// const minMassIndex = record.findIndex((value) => )
// const record2 = record.filter(({mass}) => mass)
const x = record.map((
{ timestamp },
) => (timestamp - datas.record[0].timestamp))
const y = record.map((data) => data.mass).map((_, i, a) =>
(a[i + 1] - a[i]) / (x[i + 1] - x[i])
)
const meta = `${datas.volume}mL ${datas.ipam}IPAM ${
datas.timestamp.split('T')[1]
}`
const { ipam } = datas
const mean = y.filter((v) => !Number.isNaN(v)).reduce(
(p, c) => p + c / y.length,
0,
) //p + c / y.length, 0)
console.log({ mean, ipam, v: mean / 200 })
return [
{
x,
y,
name: `mass (${meta})`,
line: {
color: `rgb(${50 * Math.log(ipam)}, 0, ${255 - 50 * Math.log(ipam)})`,
},
},
]
}
const xTestFit = Array.from({ length: 190 }).map((_, i) => i * 1e3 + 3e3)
const yTestFit = xTestFit
.map((x) => 200 * (1 - Math.exp(-21e-6 * x) + 2e-7 * x))
.map((_, i, a) => (a[i + 1] - a[i]) / (xTestFit[i + 1] - xTestFit[i]))
// 200 * (1 - Math.exp(-19e-6 * x) + 3e-7 * x)
const testFit = {
x: xTestFit,
y: yTestFit,
name: 'test fit',
}
const plot = new Plot()
const plots: Partial<PlotlyData>[] = []
plots.push(testFit)
for await (const file of Deno.readDir('./datas')) {
if (!file.isFile) continue
if (!file.name.match(/Vrr3_\d{2}-\d{2}_\d{2}-\d{2}\.csv/)) continue
const path = `./datas/${file.name}`
const csv = await Deno.readTextFile(path)
const datas = parseDatas(csv)
// if (datas.ipam !== 4) continue
plots.push(...getDatasPlot(datas))
}
plot.plot(plots, {
xaxis: { title: { text: 'timestamp' } },
yaxis: { title: { text: 'mass [g]' } },
yaxis2: { title: { text: 'pression [bar]' }, side: 'right' },
})

216
measure_clean_compa_2.ts Normal file
View file

@ -0,0 +1,216 @@
import { Data as PlotlyData, Plot } from './plot/mod.ts'
type Data = {
timestamp: string //ISO string
volume: number //mL
ipam: number //ipam
record: {
pression: number //bar
mass: number //g
timestamp: number //s
}[]
}
function parseDatas(file: string): Data {
const lines = file.split('\n')
lines.shift() //remove title
const [day, month, year, time] = lines.shift()!.split(' ')
const monthList = [
'_',
'janvier',
'fevrier',
'mars',
'avril',
'mai',
'juin',
'juillet',
'aout',
'septembre',
'decembre',
]
const timestamp = `${year}-${
monthList.indexOf(month).toString().padStart(2, '0')
}-${day.padStart(2, '0')}T${time}`
lines.shift() //remove empty line
const volume = lines.shift()!.split(';').slice(1).map(Number).at(0)!
const ipam = lines.shift()!.split(';').slice(1).map(Number).at(0)!
lines.shift() //remove empty line
lines.shift() //remove title
const record = lines
.map((line) => line.split(';').slice(1).map(Number))
.map(([pression, mass, timestamp]) => ({ pression, mass, timestamp }))
return {
timestamp,
volume,
ipam,
record,
}
}
function filterRecord(record: Data['record'], ceil = 0.4): Data['record'] {
const filteredRecord: Data['record'] = []
record.forEach((value, index) => {
if (index === 0) filteredRecord.push(value)
if (value.mass < 10) filteredRecord.push(value)
const previousMass = filteredRecord.at(-1)?.mass ?? value.mass
const delta = Math.abs(value.mass - previousMass) / value.mass
if (delta >= ceil) filteredRecord.push({ ...value, mass: previousMass })
else filteredRecord.push(value)
})
return filteredRecord
}
function cutRecord(record: Data['record']): Data['record'] {
let minMass = record[0].mass
let minMassIndex = 0
for (const value of record) {
minMassIndex++
if (value.mass < minMass) minMass = value.mass
if (value.mass > minMass) break
}
return record.slice(minMassIndex)
}
function lowpass(array: number[], window: number): number[] {
return array.slice(0, -window)
.map(
(_, index) =>
array.slice(index, index + window).reduce(
(prev, curr) => prev + curr / window,
0,
),
)
}
function derivative(x: number[], y: number[]): number[] {
return y.slice(0, -1).map((_, index) =>
(y[index + 1] - y[index]) / (x[index + 1] - x[index])
)
}
function getDatasPlot(datas: Data) {
const record = cutRecord(filterRecord(datas.record))
// const minMassIndex = record.findIndex((value) => )
// const record2 = record.filter(({mass}) => mass)
const x = record.map((
{ timestamp },
) => (timestamp - datas.record[0].timestamp)).map((x) => x / 6e3)
const y = record.map((data) => data.mass)
.map((y) => y / record.slice(-2, -1)[0].mass)
.map((y) => 1 - y)
.map(Math.log)
const meta = `${datas.volume}mL ${datas.ipam}IPAM ${
datas.timestamp.split('T')[1]
}`
const { ipam } = datas
// const dY = lowpass(derivative(x, lowpass(y, 100)), 100)
const dY = lowpass(derivative(x, y), 100)
const xMinIndex = x.findIndex((xi) => xi > 5)
const xMaxIndex = x.findIndex((xi) => xi > 10)
const expSlope = dY.slice(
xMinIndex,
xMaxIndex,
).reduce((prev, curr, _, array) => prev + curr / array.length, 0)
const yRaw = record.map((data) => data.mass)
const yMax = yRaw.reduce((max, curr) => curr > max ? curr : max)
const linearY = x.map((_, index) =>
yRaw[index] - (yMax * (1 - Math.exp(expSlope * x[index] * 6)))
)
const fitY = x.map((_, index) =>
yMax * (1 - Math.exp(expSlope * x[index] * 6))
)
const yLinDeriv = derivative(x, linearY)
const ipamSlope = yLinDeriv.slice(xMinIndex, xMaxIndex).reduce(
(p, c, _, a) => p + c / a.length,
0,
) * 6
console.log({
ipam,
ipamSlope: ipamSlope.toFixed(2),
expSlope: expSlope.toFixed(4),
})
console.log(x.at(-2))
return (
{
x: x,
// y: dY,
// y: linearY,
// y: lowpass(linearY, 200),
// y: lowpass(yLinDeriv, 200).map((y) => Math.abs(y)),
// y: fitY,
y: yRaw,
name: `mass (${meta})`,
line: {
color: `rgb(${50 * Math.log(ipam)}, 0, ${255 - 50 * Math.log(ipam)})`,
},
} satisfies Partial<PlotlyData>
)
}
const plot = new Plot()
const plots: Partial<PlotlyData>[] = []
for await (const file of Deno.readDir('./datas')) {
if (!file.isFile) continue
if (!file.name.match(/Vrr3_\d{2}-\d{2}_\d{2}-\d{2}\.csv/)) continue
const path = `./datas/${file.name}`
const csv = await Deno.readTextFile(path)
const datas = parseDatas(csv)
// if (datas.ipam !== 4) continue
if (Number.isNaN(datas.ipam)) continue
const plotDatas = getDatasPlot(datas)
// while (true) {
// const slope =
// }
if (datas.ipam === 4) {
const xFit: number[] = plotDatas.x
const yFit: number[] = xFit.map((x) => 200 * (1 - Math.exp(-0.021 * x * 6)))
// .map((x) => x / 200)
// .map((x) => 1 - x)
// .map(Math.log)
// const dYFit = lowpass(derivative(xFit, lowpass(yFit, 100)), 100)
// const dYFit = lowpass(derivative(xFit, yFit), 100)
const testFit = {
x: xFit,
// y: dYFit,
y: yFit,
name: 'test fit',
}
// plots.push(testFit)
}
plots.push(plotDatas)
}
plot.plot(plots, {
xaxis: { title: { text: 'timestamp [min/10]' } },
yaxis: { title: { text: 'mass [g]' } },
})
//RMD 4005 x2

226
measure_clean_compa_2_b.ts Normal file
View file

@ -0,0 +1,226 @@
import { Data as PlotlyData, Plot } from './plot/mod.ts'
type Data = {
timestamp: string //ISO string
volume: number //mL
ipam: number //ipam
record: {
pression: number //bar
mass: number //g
timestamp: number //s
}[]
}
function parseDatas(file: string): Data {
const lines = file.split('\n')
lines.shift() //remove title
const [day, month, year, time] = lines.shift()!.split(' ')
const monthList = [
'_',
'janvier',
'fevrier',
'mars',
'avril',
'mai',
'juin',
'juillet',
'aout',
'septembre',
'decembre',
]
const timestamp = `${year}-${
monthList.indexOf(month).toString().padStart(2, '0')
}-${day.padStart(2, '0')}T${time}`
lines.shift() //remove empty line
const volume = lines.shift()!.split(';').slice(1).map(Number).at(0)!
const ipam = lines.shift()!.split(';').slice(1).map(Number).at(0)!
lines.shift() //remove empty line
lines.shift() //remove title
const record = lines
.map((line) => line.split(';').slice(1).map(Number))
.map(([pression, mass, timestamp]) => ({ pression, mass, timestamp }))
return {
timestamp,
volume,
ipam,
record,
}
}
function filterRecord(record: Data['record'], ceil = 0.4): Data['record'] {
const filteredRecord: Data['record'] = []
record.forEach((value, index) => {
if (index === 0) filteredRecord.push(value)
if (value.mass < 10) filteredRecord.push(value)
const previousMass = filteredRecord.at(-1)?.mass ?? value.mass
const delta = Math.abs(value.mass - previousMass) / value.mass
if (delta >= ceil) filteredRecord.push({ ...value, mass: previousMass })
else filteredRecord.push(value)
})
return filteredRecord
}
function cutRecord(record: Data['record']): Data['record'] {
let minMass = record[0].mass
let minMassIndex = 0
for (const value of record) {
minMassIndex++
if (value.mass < minMass) minMass = value.mass
if (value.mass > minMass) break
}
return record.slice(minMassIndex)
}
function lowpass(array: number[], window: number): number[] {
return array.slice(0, -window)
.map(
(_, index) =>
array.slice(index, index + window).reduce(
(prev, curr) => prev + curr / window,
0,
),
)
}
function derivative(x: number[], y: number[]): number[] {
return y.slice(0, -1).map((_, index) =>
(y[index + 1] - y[index]) / (x[index + 1] - x[index])
)
}
function getDatasPlot(datas: Data) {
const record = cutRecord(filterRecord(datas.record))
// const minMassIndex = record.findIndex((value) => )
// const record2 = record.filter(({mass}) => mass)
const x = record.map((
{ timestamp },
) => (timestamp - datas.record[0].timestamp)).map((x) => x / 6e3)
const y = record.map((data) => data.mass)
.map((y) => y / record.slice(-2, -1)[0].mass)
.map((y) => 1 - y)
.map(Math.log)
const meta = `${datas.volume}mL ${datas.ipam}IPAM ${
datas.timestamp.split('T')[1]
}`
const { ipam } = datas
// const dY = lowpass(derivative(x, lowpass(y, 100)), 100)
const dY = lowpass(derivative(x, y), 100)
const xMinIndex = x.findIndex((xi) => xi > 5)
const xMaxIndex = x.findIndex((xi) => xi > 10)
const expSlope = dY.slice(
xMinIndex,
xMaxIndex,
).reduce((prev, curr, _, array) => prev + curr / array.length, 0)
const yRaw = record.map((data) => data.mass)
const yMax = yRaw.reduce((max, curr) => curr > max ? curr : max)
const linearY = x.map((_, index) =>
yRaw[index] - (yMax * (1 - Math.exp(expSlope * x[index] * 6)))
)
const fitY = x.map((_, index) =>
yMax * (1 - Math.exp(expSlope * x[index] * 6))
)
const yLinDeriv = derivative(x, linearY)
const ipamSlope = yLinDeriv.slice(xMinIndex, xMaxIndex).reduce(
(p, c, _, a) => p + c / a.length,
0,
) * 6
console.log({
ipam,
ipamSlope: ipamSlope.toFixed(2),
expSlope: expSlope.toFixed(4),
})
console.log(x.at(-2))
return ([
{
x: x,
// y: dY,
// y: linearY,
// y: lowpass(linearY, 200),
// y: lowpass(yLinDeriv, 200).map((y) => Math.abs(y)),
// y: fitY,
y: yRaw,
name: `mass (${meta})`,
line: {
color: `rgb(${50 * Math.log(ipam)}, 0, ${255 - 50 * Math.log(ipam)})`,
},
},
{
x: x,
y: dY,
yaxis: 'y2',
name: `fit (${meta})`,
line: {
color: `rgb(${50 * Math.log(ipam)}, 127, ${255 - 50 * Math.log(ipam)})`,
},
},
] satisfies Partial<PlotlyData>[])
}
const plot = new Plot()
const plots: Partial<PlotlyData>[] = []
for await (const file of Deno.readDir('./datas')) {
if (!file.isFile) continue
if (!file.name.match(/Vrr3_\d{2}-\d{2}_\d{2}-\d{2}\.csv/)) continue
const path = `./datas/${file.name}`
const csv = await Deno.readTextFile(path)
const datas = parseDatas(csv)
// if (datas.ipam !== 4) continue
if (Number.isNaN(datas.ipam)) continue
const plotDatas = getDatasPlot(datas)
// while (true) {
// const slope =
// }
// if (datas.ipam === 4) {
// const xFit: number[] = plotDatas.x
// const yFit: number[] = xFit.map((x) => 200 * (1 - Math.exp(-0.021 * x * 6)))
// .map((x) => x / 200)
// .map((x) => 1 - x)
// .map(Math.log)
// const dYFit = lowpass(derivative(xFit, lowpass(yFit, 100)), 100)
// const dYFit = lowpass(derivative(xFit, yFit), 100)
// const testFit = {
// x: xFit,
// // y: dYFit,
// y: yFit,
// name: 'test fit',
// }
// plots.push(testFit)
// }
plots.push(...plotDatas)
}
plot.plot(plots, {
xaxis: { title: { text: 'timestamp' } },
yaxis: { title: { text: 'mass [g]' } },
yaxis2: { title: { text: 'fit' }, side: 'right' },
})
//RMD 4005 x2

201
measure_clean_compa_3.ts Normal file
View file

@ -0,0 +1,201 @@
import { Data as PlotlyData, Plot } from './plot/mod.ts'
type Data = {
timestamp: string //ISO string
volume: number //mL
ipam: number //ipam
record: {
pression: number //bar
mass: number //g
timestamp: number //s
}[]
}
function parseDatas(file: string): Data {
const lines = file.split('\n')
lines.shift() //remove title
const [day, month, year, time] = lines.shift()!.split(' ')
const monthList = [
'_',
'janv.',
'fev.',
'mars',
'avril',
'mai',
'juin',
'juil.',
'aout',
'sept.',
'dec.',
]
const timestamp = `${year}-${
monthList.indexOf(month).toString().padStart(2, '0')
}-${day.padStart(2, '0')}T${time}`
lines.shift() //remove empty line
const volume = lines.shift()!.split(';').slice(1).map(Number).at(0)!
const ipam = lines.shift()!.split(';').slice(1).map(Number).at(0)!
lines.shift() //remove empty line
lines.shift() //remove title
const record = lines
.map((line) => line.split(';').slice(1).map(Number))
.map(([pression, mass, timestamp]) => ({ pression, mass, timestamp }))
return {
timestamp,
volume,
ipam,
record,
}
}
function filterRecord(record: Data['record'], ceil = 0.4): Data['record'] {
const filteredRecord: Data['record'] = []
record.forEach((value, index) => {
if (index === 0) filteredRecord.push(value)
if (value.mass < 10) filteredRecord.push(value)
const previousMass = filteredRecord.at(-1)?.mass ?? value.mass
const delta = Math.abs(value.mass - previousMass) / value.mass
if (delta >= ceil) filteredRecord.push({ ...value, mass: previousMass })
else filteredRecord.push(value)
})
return filteredRecord
}
function cutRecord(record: Data['record']): Data['record'] {
let minMass = record[0].mass
let minMassIndex = 0
for (const value of record) {
minMassIndex++
if (value.mass < minMass) minMass = value.mass
if (value.mass > minMass) break
}
return record.slice(minMassIndex)
}
function lowpass(array: number[], window: number): number[] {
return array.slice(0, -window)
.map(
(_, index) =>
array.slice(index, index + window).reduce(
(prev, curr) => prev + curr / window,
0,
),
)
}
function derivative(x: number[], y: number[]): number[] {
return y.slice(0, -1).map((_, index) =>
(y[index + 1] - y[index]) / (x[index + 1] - x[index])
)
}
function getDatasPlot(datas: Data) {
const record = cutRecord(filterRecord(datas.record))
// const minMassIndex = record.findIndex((value) => )
// const record2 = record.filter(({mass}) => mass)
const x = record.map((
{ timestamp },
) => (timestamp - datas.record[0].timestamp)).map((x) => x / 6e3)
const y = record.map((data) => data.mass)
.map((y) => y / record.slice(-2, -1)[0].mass)
.map((y) => 1 - y)
.map(Math.log)
const meta = `${datas.volume}mL ${datas.ipam}IPAM ${
datas.timestamp.split('T')[1]
}`
const { ipam } = datas
// const dY = lowpass(derivative(x, lowpass(y, 100)), 100)
const dY = lowpass(derivative(x, y), 100)
const xMinIndex = x.findIndex((xi) => xi > 5)
const xMaxIndex = x.findIndex((xi) => xi > 10)
const expSlope = dY.slice(
xMinIndex,
xMaxIndex,
).reduce((prev, curr, _, array) => prev + curr / array.length, 0)
const yRaw = record.map((data) => data.mass)
const yMax = yRaw.reduce((max, curr) => curr > max ? curr : max)
const linearY = x.map((_, index) =>
yRaw[index] - (yMax * (1 - Math.exp(expSlope * x[index] * 6)))
)
const fitY = x.map((_, index) =>
yMax * (1 - Math.exp(expSlope * x[index] * 6))
)
const yLinDeriv = derivative(x, linearY)
const ipamSlope = yLinDeriv.slice(xMinIndex, xMaxIndex).reduce(
(p, c, _, a) => p + c / a.length,
0,
) * 6
console.log({
ipam,
ipamSlope: ipamSlope.toFixed(2),
expSlope: expSlope.toFixed(4),
})
console.log(x.at(-2))
const isHaut = datas.timestamp.includes('07-08')
const color = `rgb(${isHaut ? 255 : 0}, ${255 ** (ipam / 47)}, ${
255 - 255 ** (ipam / 47)
})`
console.log(color)
return ([
{
x: x,
// y: dY,
// y: linearY,
// y: lowpass(linearY, 200),
// y: lowpass(yLinDeriv, 200).map((y) => Math.abs(y)),
// y: fitY,
y: yRaw,
name: `mass (${meta}) ${isHaut ? 'réservoir haut' : 'réservoir bas'}`,
line: { color },
},
// {
// x: x,
// y: dY,
// yaxis: 'y2',
// name: `fit (${meta})`,
// line: {
// color: `rgb(${50 * Math.log(ipam)}, 127, ${255 - 50 * Math.log(ipam)})`,
// },
// },
] satisfies Partial<PlotlyData>[])
}
const plot = new Plot()
const plots: Partial<PlotlyData>[] = []
for await (const file of Deno.readDir('./datas')) {
if (!file.isFile) continue
if (!file.name.match(/^Rauzan_\d{2}-\d{2}_\d{2}-\d{2}\.csv$/)) continue
const path = `./datas/${file.name}`
const csv = await Deno.readTextFile(path)
const datas = parseDatas(csv)
if (Number.isNaN(datas.ipam)) continue
const plotDatas = getDatasPlot(datas)
plots.push(...plotDatas)
}
plot.plot(plots, {
xaxis: { title: { text: 'timestamp [min/10]' } },
yaxis: { title: { text: 'mass [g]' } },
// yaxis2: { title: { text: 'fit' }, side: 'right' },
})

163
measure_curve_fit.ts Normal file
View file

@ -0,0 +1,163 @@
import { Data as PlotlyData, Plot } from './plot/mod.ts'
import { curveFit } from './utils/fit.ts'
type Data = {
timestamp: string //ISO string
volume: number //mL
ipam: number //ipam
record: {
pression: number //bar
mass: number //g
timestamp: number //s
}[]
}
function parseDatas(file: string): Data {
const lines = file.split('\n')
lines.shift() //remove title
const [day, month, year, time] = lines.shift()!.split(' ')
const monthList = [
'_',
'janvier',
'fevrier',
'mars',
'avril',
'mai',
'juin',
'juillet',
'aout',
'septembre',
'decembre',
]
const timestamp = `${year}-${
monthList.indexOf(month).toString().padStart(2, '0')
}-${day.padStart(2, '0')}T${time}`
lines.shift() //remove empty line
const volume = lines.shift()!.split(';').slice(1).map(Number).at(0)!
const ipam = lines.shift()!.split(';').slice(1).map(Number).at(0)!
lines.shift() //remove empty line
lines.shift() //remove title
const record = lines
.map((line) => line.split(';').slice(1).map(Number))
.map(([pression, mass, timestamp]) => ({ pression, mass, timestamp }))
return {
timestamp,
volume,
ipam,
record,
}
}
function filterRecord(record: Data['record'], ceil = 0.4): Data['record'] {
const filteredRecord: Data['record'] = []
record.forEach((value, index) => {
if (index === 0) filteredRecord.push(value)
if (value.mass < 10) filteredRecord.push(value)
const previousMass = filteredRecord.at(-1)?.mass ?? value.mass
const delta = Math.abs(value.mass - previousMass) / value.mass
if (delta >= ceil) filteredRecord.push({ ...value, mass: previousMass })
else filteredRecord.push(value)
})
return filteredRecord
}
function cutRecord(record: Data['record']): Data['record'] {
let minMass = record[0].mass
let minMassIndex = 0
for (const value of record) {
minMassIndex++
if (value.mass < minMass) minMass = value.mass
if (value.mass > minMass) break
}
return record.slice(minMassIndex)
}
function pickLess(array: number[], step: number): number[] {
const result: number[] = []
for (let i = 0; i < array.length; i += step) {
result.push(array[i])
}
return result
}
function getDatasPlot(datas: Data) {
const record = cutRecord(filterRecord(datas.record))
const x = record.map((
{ timestamp },
) => (timestamp - datas.record[0].timestamp)).map((x) => x / 6e3)
const y = record.map((data) => data.mass)
const meta = `${datas.volume}mL ${datas.ipam}IPAM ${
datas.timestamp.split('T')[1]
}`
const yLess = pickLess(y, 2)
const xLess = pickLess(x, 2)
const { ipam } = datas
const yMax = yLess.reduce((max, curr) => curr > max ? curr : max)
const fitFn = (x: number, [a, b]: [number, number]) =>
yMax * (1 - Math.exp(-a * x) + b * x)
const fit = curveFit(xLess, yLess, fitFn, [0.2, 0.005])
console.log(fit.params)
console.log({
ipam,
})
return ([
{
x: xLess,
y: yLess,
name: `mass (${meta})`,
line: {
color: `rgb(${50 * Math.log(ipam)}, 0, ${255 - 50 * Math.log(ipam)})`,
},
},
{
x: xLess,
y: xLess.map((x) => fitFn(x, fit.params)),
// y: xLess.map((x) => fitFn(x, [0.5, 0.01])),
yaxis: 'y2',
name: `fit (${meta})`,
line: {
color: `rgb(${50 * Math.log(ipam)}, 127, ${255 - 50 * Math.log(ipam)})`,
},
},
] satisfies Partial<PlotlyData>[])
}
const plot = new Plot()
const plots: Partial<PlotlyData>[] = []
for await (const file of Deno.readDir('./datas')) {
if (!file.isFile) continue
if (!file.name.match(/Vrr3_\d{2}-\d{2}_\d{2}-\d{2}\.csv/)) continue
const path = `./datas/${file.name}`
const csv = await Deno.readTextFile(path)
const datas = parseDatas(csv)
// if (datas.ipam !== 4) continue
if (Number.isNaN(datas.ipam)) continue
const plotDatas = getDatasPlot(datas)
plots.push(...plotDatas)
}
plot.plot(plots, {
xaxis: { title: { text: 'timestamp' } },
yaxis: { title: { text: 'mass [g]' } },
yaxis2: { title: { text: 'fit' }, side: 'right' },
})
//RMD 4005 x2

33
notes_essais.md Normal file
View file

@ -0,0 +1,33 @@
# 16/06
- **14h31** Erreur arret vide puis relancé
Suivante en cycle fermé
- **15h01** Refill ?
- **15h09** Pliure sur le filtre
- **15h24** Partial refill
- **15h42** Full purge
- **15h56** Refill
- **16h10** Arret urgence
- **16h20** Sploosh ?
- **16h30** Arret pompe. Tension 0v
- **16h39** Refill
- **16h40** Arret urgence
- **16h43** Refill new vrr3
- **16h49** Arret urgence
- **16h50** Arret pompe. Tension 0v
# 18/06
- **15h55** Pompe qui "pompe"
# 26/07
- **15h46** Arret moteur
- **15h47** Emballement moteur
# 27/07
- **16h41** Bruit pompe (? Bulle)
- **16h56** Arret moteur

8
plot/deno.json Normal file
View file

@ -0,0 +1,8 @@
{
"name": "@julien/plot",
"exports": "./mod.ts",
"imports": {
"@opensrc/deno-open": "jsr:@opensrc/deno-open@^1.0.0",
"webview": "npm:webview@^1.1.0"
}
}

217
plot/mod.ts Normal file
View file

@ -0,0 +1,217 @@
/// <reference lib="dom" />
import type {
Config,
Data,
Layout,
// deno-lint-ignore no-unused-vars
newPlot,
// deno-lint-ignore no-unused-vars
Plots,
} from "npm:@types/plotly.js-dist-min";
export { Config, Data, Layout };
import webview from "npm:webview";
import { open } from "jsr:@opensrc/deno-open";
export type PlotOptions = {
width: number;
height: number;
title: string;
engine: "webview" | "browser";
};
export class Plot {
static #port: number;
static #server = Deno.serve({
port: 0,
onListen: ({ port }) => this.#port = port,
}, (req: Request) => {
const { pathname, searchParams } = new URL(req.url);
if (pathname === "/") {
return new Response(html(), {
headers: {
"Content-Type": "text/html; charset=utf-8",
},
});
}
if (pathname === "/favicon.ico") {
return new Response(null);
}
if (pathname === "/ws") {
const { socket, response } = Deno.upgradeWebSocket(req);
socket.addEventListener(
"open",
() => {
const uuid = searchParams.get("uuid") ?? "";
this.#wsList.set(uuid, socket);
this.#connectionPool.get(uuid)?.();
this.#connectionPool.delete(uuid);
},
);
return response;
}
return new Response(null, { status: 400 });
});
static #wsList = new Map<string, WebSocket>();
static #windows: Deno.ChildProcess[] = [];
static #connectionPool = new Map<string, (value?: undefined) => void>();
#window: Deno.ChildProcess | undefined;
#size: [number, number];
#title: string | undefined;
#uuid = crypto.randomUUID();
#ws = () => Plot.#wsList.get(this.#uuid);
#unbined = false;
#engine: "webview" | "browser";
constructor(options: Partial<PlotOptions> = {}) {
this.#title = options.title;
this.#size = [options.width ?? 400, options.height ?? 400];
this.#engine = options.engine ?? "webview";
}
update(
data: Partial<Data>,
layout: Partial<Layout> | null = null,
) {
this.#ws()?.send(
JSON.stringify({ kind: "plot.update", value: { data, layout } }),
);
}
async plot(
data: Partial<Data[]>,
layout: Partial<Layout> = {},
config: Partial<Config> = {},
) {
const cmd = new Deno.Command(webview.binaryPath, {
args: [
`--url=http://localhost:${Plot.#port}?uuid=${this.#uuid}`,
`--title="${this.#title ?? `Plot ${Plot.#port}`}"`,
`--width=${this.#size[0]}`,
`--height=${this.#size[1]}`,
],
});
if (this.#engine === "browser") {
open(`http://localhost:${Plot.#port}?uuid=${this.#uuid}`);
} else {
this.#window = cmd.spawn();
}
Plot.#windows.push(this.#window);
const { promise, resolve } = Promise.withResolvers();
Plot.#connectionPool.set(this.#uuid, resolve);
await promise;
this.#ws()?.send(
JSON.stringify({ kind: "plot.plot", value: { data, layout, config } }),
);
}
#moveWindow(x = 0, y = 0) {
Plot.#wsList.get(this.#uuid)?.send(
JSON.stringify({ kind: "window.move", value: { x, y } }),
);
}
#resizeWindow(width: number, height: number) {
this.#ws()?.send(
JSON.stringify({ kind: "window.resize", value: { width, height } }),
);
}
get window(): {
move: (x: number, y: number) => void;
resize: (width: number, height: number) => void;
} {
return { move: this.#moveWindow, resize: this.#resizeWindow };
}
unbind() {
this.#unbined = true;
}
[Symbol.dispose]() {
if (this.#unbined) return;
this.#ws()?.close(0, "instance dispose");
if (this.#engine === "webview") {
this.#window?.kill();
}
}
static [Symbol.dispose]() {
this.#wsList.forEach((socket) => socket.close(0, "instance dispose"));
this.#wsList.clear();
this.#server.shutdown();
}
}
function html() {
return `
<html>
<head>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📊</text></svg>">
<script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
<style>
*,::before,::after { box-sizing: border-box; }
body { display: grid; place-items: center; height: 100dvh; width: 100dvw; margin: 0; padding: 0; }
#root { width: 95%; height: 95%; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module">(${client.toString()})()</script>
</body>
</html>`;
}
function client() {
if ("Deno" in globalThis) throw new Error("client only function");
const url = new URL(location.href);
const uuid = url.searchParams.get("uuid") ?? crypto.randomUUID();
const ws = new WebSocket(`${location.origin}/ws?uuid=${uuid}`);
const root = document.querySelector<HTMLElement>("#root")!;
// ws.addEventListener("open", () => {});
ws.addEventListener("close", () => {
close();
});
ws.addEventListener("message", ({ data }) => {
const { kind, value } = JSON.parse(data);
if (kind === "plot.plot") {
// use webgl as default type
(value.data as Record<string, unknown>[]).forEach((data) =>
data.type = data?.type ?? "scattergl"
);
//@ts-expect-error client side load from script tag
Plotly.newPlot(
root,
value.data,
value.layout,
{ responsive: true, ...(value.config ?? {}) },
);
} else if (kind === "plot.update") {
//@ts-expect-error client side load from script tag
Plotly.react(root, value.data, value.layout);
} else if (kind === "window.move") {
globalThis.moveTo(value.x, value.y);
} else if (kind === "window.resize") {
globalThis.resizeTo(value.width, value.height);
}
});
addEventListener("beforeunload", () => {
ws.close(1000, "window closed");
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View file

@ -0,0 +1,101 @@
# Rapport dessais Filtrabilité
## 1. Contexte et objectifs
Lobjectif principal de cette campagne dessais est de valider la robustesse du
système de mesure de lIPAM (Indice de Performance dAbsorption des Matières)
dans différents scénarios expérimentaux, notamment en présence de décantation,
de variations de réservoir, et lors de remplissages/purges. Un second objectif
est de caractériser le comportement de la pompe et du moteur au cours des
essais.
---
## 2. Traitement des données
- Plusieurs méthodes de traitement du signal ont été testées pour fiabiliser la
détection de lIPAM (réduction du bruit, élimination des valeurs aberrantes).
- Le lissage par filtre passe-bas ou moyenne glissante permet de supprimer
efficacement les IPAM erronés `IPAM : ?`.
![Mesures brutes](./plot_filtrage_multiple_raw.png)\
![Mesures lissées](./plot_filtrage_multiple_clean.png)
---
## 3. Synthèse des essais
### 3.1 Comportement IPAM
- Aucun écart significatif observé entre les mesures réalisées avec le
**réservoir haut** et le **réservoir bas**.\
![Comparaison haut/bas](./plot_filtrage_compa_haut_bas.png)
- L**IPAM dérive notablement** dès quil dépasse la valeur de 20 :
- Décantation très rapide (~1 minute).
- Reprise de mesure après remplissage post-décantation = **valeurs faussées**.
- Une mesure juste après la décantation (sans remplissage) est **plus
représentative**.
- La première mesure post-purge/décantation est valide mais très bruitée.
- Il est recommandé d**homogénéiser le fluide** pour assurer la stabilité de
lIPAM lorsque ce dernier dépasse 20.
---
### 3.2 Événements enregistrés durant les essais
#### 16/06
- 14h31 : Arrêt sur réservoir vide, relance immédiate.
- 15h01 : Suspicion de remplissage.
- 15h09 : Filtre plié constaté.
- 15h24 : Re-remplissage partiel.
- 15h42 : Purge complète.
- 15h56 : Nouveau remplissage.
- 16h10 : Arrêt durgence.
- 16h20 : Bruit de type "sploosh" détecté (cavitation ?).
- 16h30 : Arrêt pompe tension mesurée à 0 V.
- 16h39 : Re-remplissage.
- 16h40 : Nouvel arrêt durgence.
- 16h43 : Remplissage avec nouvelle version VRR3.
- 16h49 : Arrêt durgence répété.
- 16h50 : Pompe à larrêt tension toujours à 0 V.
#### 18/06
- 15h55 : Pompe fonctionnant de manière anormale (“pompe qui pompe” en continu).
#### 26/07
- 15h46 : Arrêt moteur.
- 15h47 : Emballement moteur (surrégime probable).
#### 27/07
- 16h41 : Bruit suspect détecté au niveau de la pompe (bulle ?).
- 16h56 : Nouvel arrêt moteur.
---
## 4. Retours sur lapplication
- **Problème de nommage de fichier :** Lorsque l'utilisateur clique sur
`annuler` à l'étape de nommage du fichier IPAM, une mesure est tout de même
déclenchée avec un nom incorrect.
- **Encodage des fichiers CSV :** Les fichiers sont encodés en **LATIN-1**, mais
interprétés comme **UTF-8**, entraînant des erreurs de décodage.
---
## 5. Recommandations
- Forcer lhomogénéisation du vin si IPAM > 20 pour éviter les dérives.
- Éviter de laisser le vin décanter (~ 1min) dans le réservoir si l'IPAM > 20.
- Corriger le comportement de lapplication lors de lannulation du nommage.
- Mettre en place un encodage explicite (UTF-8 recommandé) pour les fichiers
CSV.
- Surveiller les événements moteurs/pompes : répétition des arrêts durgence et
emballements nécessite une inspection approfondie.
---

163
simu/calcul_coord.ts Normal file
View file

@ -0,0 +1,163 @@
import { Plot } from '../plot/mod.ts'
import { range } from '../utils/list.ts'
const distance = 200 //cm
const size = 264 //cm
const gap = 2.6 //cm
const radius = 1 //cm
const markerSize = 2 * radius * 4 //4: px aspect factor
function getPoints() {
const plot = {
x: [] as number[],
y: [] as number[],
mode: 'markers',
name: 'references',
marker: { size: markerSize },
}
const step = 20 * gap
for (const x of range(0, size + 1, step)) {
for (const y of range(0, size + 1, step)) {
plot.x.push(x)
plot.y.push(y)
}
}
return plot
}
function computePointsActual() {
const plot = {
x: [] as number[],
y: [] as number[],
mode: 'markers',
name: 'actual',
marker: { size: markerSize },
opacity: 0.5,
}
const step = 20 * gap
for (const y of range(0, size + 1, step)) {
for (const z of range(0, 2 * size + 1, step)) {
//Spheric coordinates
const x = distance
const rho = Math.hypot(x, y, z)
const phi = Math.atan(y / x)
const theta = Math.asin(z / rho)
//Cartesian coordinates
const x1 = Math.tan(phi) * distance
const y1 = Math.tan(theta) * distance
plot.x.push(x1)
plot.y.push(y1)
}
}
return plot
}
function computePointsActualFixedFormula() {
const plot = {
x: [] as number[],
y: [] as number[],
mode: 'markers',
name: 'actual fixed formula',
marker: { size: markerSize },
opacity: 0.5,
}
const step = 20 * gap
for (const y of range(0, size + 1, step)) {
for (const z of range(0, 2 * size + 1, step)) {
//Spheric coordinates
const x = distance
const rho = Math.hypot(x, y, z)
const phi = Math.atan(y / x)
const theta = Math.acos(z / rho)
//Cartesian coordinates
const x1 = Math.tan(phi) * distance
const y1 = Math.tan(theta) * distance
plot.x.push(x1)
plot.y.push(y1)
}
}
return plot
}
function computePointsFixed() {
const plot = {
x: [] as number[],
y: [] as number[],
mode: 'markers',
name: 'fixed',
marker: { size: markerSize },
opacity: 0.5,
}
const step = 20 * gap
for (const x of range(0, size + 1, step)) {
for (const y of range(0, size + 1, step)) {
//Projected coordinates
const phi = Math.atan(x / distance)
const theta = Math.atan(y / distance)
//Cartesian coordinates
const x1 = Math.tan(phi) * distance
const y1 = Math.tan(theta) * distance
plot.x.push(x1)
plot.y.push(y1)
}
}
return plot
}
function getHoles() {
const plot = {
x: [] as number[],
y: [] as number[],
mode: 'markers',
marker: {
size: markerSize,
color: 'rgba(0, 0, 0, 0)',
line: {
color: 'rgba(0, 0, 0, 1)',
width: 1,
},
},
opacity: 0.3,
name: 'holes',
}
let shift = false
for (const x of range(0, size, gap)) {
for (const y of range(0, size, gap)) {
plot.x.push(shift ? x + gap / 2 : x)
plot.y.push(y)
shift = !shift
}
}
return plot
}
const plot = new Plot()
plot.plot([
getPoints(),
getHoles(),
computePointsActual(),
computePointsActualFixedFormula(),
computePointsFixed(),
], {
xaxis: { range: [-10, size + 10] },
yaxis: { range: [-10, size + 10] },
})

137
simu/erreur_mc.ts Normal file
View file

@ -0,0 +1,137 @@
import { Plot } from '../plot/mod.ts'
import { range } from '../utils/list.ts'
const distance = 200 //cm
const size = 300 //cm
const gap = 2.6 //cm
const radius = 1 //cm
const markerSize = 2 * radius * 4 //4: px aspect factor
function getPoints() {
const plot = {
x: [] as number[],
y: [] as number[],
mode: 'markers',
name: 'references',
marker: { size: markerSize },
}
const step = 20 * gap
for (const x of range(0, size + 1, step)) {
for (const y of range(0, size + 1, step)) {
plot.x.push(x)
plot.y.push(y)
}
}
return plot
}
function computePoints(
samples: number,
options = { static: true, dynamic: true, rounding: true },
) {
const label = [
options.static ? 'static' : undefined,
options.dynamic ? 'dyn.' : undefined,
options.rounding ? 'round.' : undefined,
].filter((v) => v).join('+')
const plot = {
x: [] as number[],
y: [] as number[],
mode: 'markers',
name: `simulation (${label})`,
marker: { size: markerSize },
opacity: 0.3,
}
const step = 20 * gap
for (const x of range(0, size + 1, step)) {
for (const y of range(0, size + 1, step)) {
//Projected coordinates
const phi = Math.atan(x / distance)
const theta = Math.atan(y / distance)
for (const _ of range(0, samples)) {
// Static errors
const laserAxisError = () => Math.random() * 3 - 3 / 2
const staticErrors = () => laserAxisError()
// Dynamic errors
const headMotorsError = () => Math.random() * 0.02 - 0.01
const headOrbitError = () => Math.random() * 0.2 - 0.1
const dynamicErrors = () => headMotorsError() + headOrbitError()
// Code rounding
const codeThetaRound = () => Math.random() * 4.39 - 4.39 / 2
const codePhiRound = () => Math.random() * 6.43 - 6.43 / 2
const thetaErr = (options.static && staticErrors()) +
(options.dynamic && dynamicErrors()) +
(options.rounding && codeThetaRound())
const phiErr = (options.static && staticErrors()) +
(options.dynamic && dynamicErrors()) +
(options.rounding && codePhiRound())
//Cartesian coordinates
const x1 = Math.tan(phi + toRad(phiErr)) * distance
const y1 = Math.tan(theta + toRad(thetaErr)) * distance
plot.x.push(x1)
plot.y.push(y1)
}
}
}
return plot
}
function getHoles() {
const plot = {
x: [] as number[],
y: [] as number[],
mode: 'markers',
marker: {
size: markerSize,
color: 'rgba(0, 0, 0, 0)',
line: {
color: 'rgba(0, 0, 0, 1)',
width: 1,
},
},
opacity: 0.3,
name: 'holes',
}
let shift = false
for (const x of range(0, size, gap)) {
for (const y of range(0, size, gap)) {
plot.x.push(shift ? x + gap / 2 : x)
plot.y.push(y)
shift = !shift
}
}
return plot
}
const plot = new Plot()
plot.plot([
computePoints(1e3, { static: true, dynamic: true, rounding: true }),
computePoints(1e3, { static: true, dynamic: true, rounding: false }),
computePoints(1e3, { static: false, dynamic: true, rounding: false }),
getPoints(),
getHoles(),
], {
xaxis: { range: [-10, size + 10] },
yaxis: { range: [-10, size + 10] },
})
function toRad(degree: number): number {
return degree * Math.PI / 180
}

122
utils/fit.ts Normal file
View file

@ -0,0 +1,122 @@
import * as math from 'npm:mathjs'
/**
* Performs non-linear least squares regression of data to an arbitrary function using math.js.
*
* @param {number[]} xData - Array of x-coordinates.
* @param {number[]} yData - Array of y-coordinates.
* @param {(x: number, params: number[]) => number} func - The function to fit to the data. Takes x and an array of parameters as input.
* @param {number[]} initialParams - Initial guesses for the parameters.
* @param {number} maxIterations - Maximum number of iterations.
* @param {number} tolerance - Tolerance for convergence.
* @returns {{ params: number[], residuals: number[], rSquared: number, iterations: number, converged: boolean }} - An object containing the fitted parameters and other information.
*/
export function curveFit<T extends number[]>(
xData: number[],
yData: number[],
func: (x: number, params: T) => number,
initialParams: T,
maxIterations: number = 1000,
tolerance: number = 1e-8,
): {
params: T
residuals: number[]
rSquared: number
iterations: number
converged: boolean
} {
const numPoints: number = xData.length
const numParams: number = initialParams.length
if (numPoints !== yData.length) {
throw new Error('xData and yData must have the same length.')
}
const params: number[] = [...initialParams]
let iterations: number = 0
let converged: boolean = false
while (iterations < maxIterations) {
const jacobian: number[][] = []
const residuals: number[] = []
for (let i: number = 0; i < numPoints; i++) {
const x: number = xData[i]
const y: number = yData[i]
const predicted: number = func(x, params as T)
const residual: number = y - predicted
residuals.push(residual)
const row: number[] = []
for (let j: number = 0; j < numParams; j++) {
const delta: number = 1e-6
const paramsPlusDelta = [...params] as T
paramsPlusDelta[j] += delta
const predictedPlusDelta: number = func(x, paramsPlusDelta)
const derivative: number = (predictedPlusDelta - predicted) / delta
row.push(derivative)
}
jacobian.push(row)
}
const jacobianTranspose: number[][] = math.transpose(jacobian)
const hessian: math.Matrix = math.matrix(
math.multiply(jacobianTranspose, jacobian),
)
const gradient: math.Matrix = math.matrix(
math.multiply(jacobianTranspose, residuals),
)
try {
const deltaParams: math.Matrix = math.usolve(
hessian,
gradient,
) as math.Matrix
let maxDelta: number = 0
for (let i: number = 0; i < numParams; i++) {
params[i] += deltaParams.get([i, 0])
maxDelta = Math.max(maxDelta, Math.abs(deltaParams.get([i, 0])))
}
if (maxDelta < tolerance) {
converged = true
break
}
} catch (_error) {
// console.error('Matrix solve failed:', error)
return {
params: params as T,
residuals: residuals,
rSquared: NaN,
iterations: iterations,
converged: false,
}
}
iterations++
}
const calculatedResiduals: number[] = xData.map((x: number, i: number) =>
yData[i] - func(x, params as T)
)
const ssRes: number = calculatedResiduals.reduce(
(sum: number, r: number) => sum + r * r,
0,
)
const yMean: number = yData.reduce((sum: number, y: number) => sum + y, 0) /
numPoints
const ssTot: number = yData.reduce(
(sum: number, y: number) => sum + (y - yMean) * (y - yMean),
0,
)
const rSquared: number = 1 - ssRes / ssTot
return {
params: params as T,
residuals: calculatedResiduals,
rSquared: rSquared,
iterations: iterations,
converged: converged,
}
}

78
utils/list.ts Normal file
View file

@ -0,0 +1,78 @@
export function* range(
start: number,
end: number,
step = 1,
): Generator<number> {
for (let i = start; i < end; i += step) yield i
}
export type List<T> = IteratorObject<T> | T[]
export function count(): <T>(previous: T, current: T) => number {
let count = 1
let first = false
return () => {
if (first) {
count++
first = false
}
return count++
}
}
export function sum(previous: number, current: number) {
return previous + current
}
export function mean(): (mean: number, value: number) => number {
let count = 0
let total = 0
let first = true
return (mean: number, value: number) => {
if (first) {
total = mean
first = false
}
count++
total += value
return total / count
}
}
export function stdDeviation(list: List<number>): number {
let count = 0
const meanValue = list.reduce(mean())
const deviation = list.reduce((dev, value) => {
count++
return dev + (value - meanValue) ** 2
}, 0)
return Math.sqrt(deviation / (count - 1))
}
export function min(min: number, value: number): number {
return value < min ? value : min
}
export function max(max: number, value: number): number {
return value > max ? value : max
}
export function makeBins(count: number, array: number[]): number[] {
const bins: number[] = new Array(count + 1).fill(0)
const minValue = array.reduce(min)
const maxValue = array.reduce(max)
const size = (maxValue - minValue) / count
array.forEach((value) => {
for (const step of range(0, count + 1)) {
if (
value >= minValue + step * size && value < minValue + (step + 1) * size
) {
bins[step]++
break
}
}
})
return bins
}