initial commit
This commit is contained in:
commit
4b5c0e3528
3806
datas/Bidon_07-07_16-22.csv
Normal file
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
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
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
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
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
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
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
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
3776
datas/Rauzan_08-07_15-40.csv
Normal file
File diff suppressed because it is too large
Load diff
3777
datas/Rauzan_08-07_15-40.csv.old
Normal file
3777
datas/Rauzan_08-07_15-40.csv.old
Normal file
File diff suppressed because it is too large
Load diff
3718
datas/Rauzan_08-07_15-48.csv
Normal file
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
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
3805
datas/VffF1_27-06_15-51.csv
Normal file
File diff suppressed because it is too large
Load diff
3865
datas/Vrr2_down_19-06_14-52.csv
Normal file
3865
datas/Vrr2_down_19-06_14-52.csv
Normal file
File diff suppressed because it is too large
Load diff
3874
datas/Vrr2_down_19-06_14-57.csv
Normal file
3874
datas/Vrr2_down_19-06_14-57.csv
Normal file
File diff suppressed because it is too large
Load diff
3869
datas/Vrr2_down_19-06_15-02.csv
Normal file
3869
datas/Vrr2_down_19-06_15-02.csv
Normal file
File diff suppressed because it is too large
Load diff
3817
datas/Vrr2_down_19-06_15-06_temp.csv
Normal file
3817
datas/Vrr2_down_19-06_15-06_temp.csv
Normal file
File diff suppressed because it is too large
Load diff
3844
datas/Vrr2_up_18-06_14-58.csv
Normal file
3844
datas/Vrr2_up_18-06_14-58.csv
Normal file
File diff suppressed because it is too large
Load diff
3882
datas/Vrr2_up_18-06_15-03.csv
Normal file
3882
datas/Vrr2_up_18-06_15-03.csv
Normal file
File diff suppressed because it is too large
Load diff
3868
datas/Vrr2_up_18-06_15-08.csv
Normal file
3868
datas/Vrr2_up_18-06_15-08.csv
Normal file
File diff suppressed because it is too large
Load diff
3850
datas/Vrr2_up_18-06_15-15.csv
Normal file
3850
datas/Vrr2_up_18-06_15-15.csv
Normal file
File diff suppressed because it is too large
Load diff
3262
datas/Vrr2_up_19-06_13-57_temp.csv
Normal file
3262
datas/Vrr2_up_19-06_13-57_temp.csv
Normal file
File diff suppressed because it is too large
Load diff
3863
datas/Vrr2_up_19-06_14-05.csv
Normal file
3863
datas/Vrr2_up_19-06_14-05.csv
Normal file
File diff suppressed because it is too large
Load diff
3890
datas/Vrr2_up_19-06_14-11.csv
Normal file
3890
datas/Vrr2_up_19-06_14-11.csv
Normal file
File diff suppressed because it is too large
Load diff
3854
datas/Vrr2_up_19-06_14-16.csv
Normal file
3854
datas/Vrr2_up_19-06_14-16.csv
Normal file
File diff suppressed because it is too large
Load diff
3876
datas/Vrr2_up_19-06_14-22.csv
Normal file
3876
datas/Vrr2_up_19-06_14-22.csv
Normal file
File diff suppressed because it is too large
Load diff
3809
datas/Vrr2_up_19-06_14-28.csv
Normal file
3809
datas/Vrr2_up_19-06_14-28.csv
Normal file
File diff suppressed because it is too large
Load diff
3856
datas/Vrr2_up_19-06_14-34.csv
Normal file
3856
datas/Vrr2_up_19-06_14-34.csv
Normal file
File diff suppressed because it is too large
Load diff
3836
datas/Vrr2_up_19-06_14-41.csv
Normal file
3836
datas/Vrr2_up_19-06_14-41.csv
Normal file
File diff suppressed because it is too large
Load diff
3826
datas/Vrr3_16-06_14-04.csv
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
1228
datas/Vrr3_16-06_16-14.csv
Normal file
File diff suppressed because it is too large
Load diff
7
datas/Vrr3_16-06_16-23.csv
Normal file
7
datas/Vrr3_16-06_16-23.csv
Normal 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
|
||||||
|
3826
datas/Vrr3_16-06_16-33.csv
Normal file
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
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
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
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
10
deno.json
Normal 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
98
deno.lock
Normal 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
103
main.ts
Normal 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
150
measure_clean_compa.ts
Normal 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
216
measure_clean_compa_2.ts
Normal 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
226
measure_clean_compa_2_b.ts
Normal 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
201
measure_clean_compa_3.ts
Normal 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
163
measure_curve_fit.ts
Normal 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
33
notes_essais.md
Normal 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
8
plot/deno.json
Normal 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
217
plot/mod.ts
Normal 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
plot_filtrage_compa_haut_bas.png
Normal file
BIN
plot_filtrage_compa_haut_bas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
BIN
plot_filtrage_multiple_clean.png
Normal file
BIN
plot_filtrage_multiple_clean.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
plot_filtrage_multiple_raw.png
Normal file
BIN
plot_filtrage_multiple_raw.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
101
rapport_essais_filtrabilite.md
Normal file
101
rapport_essais_filtrabilite.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Rapport d’essais – Filtrabilité
|
||||||
|
|
||||||
|
## 1. Contexte et objectifs
|
||||||
|
|
||||||
|
L’objectif principal de cette campagne d’essais est de valider la robustesse du
|
||||||
|
système de mesure de l’IPAM (Indice de Performance d’Absorption 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 l’IPAM (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 : ?`.
|
||||||
|
|
||||||
|
\
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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**.\
|
||||||
|

|
||||||
|
|
||||||
|
- L’**IPAM dérive notablement** dès qu’il 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
|
||||||
|
l’IPAM 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 d’urgence.
|
||||||
|
- 16h20 : Bruit de type "sploosh" détecté (cavitation ?).
|
||||||
|
- 16h30 : Arrêt pompe – tension mesurée à 0 V.
|
||||||
|
- 16h39 : Re-remplissage.
|
||||||
|
- 16h40 : Nouvel arrêt d’urgence.
|
||||||
|
- 16h43 : Remplissage avec nouvelle version VRR3.
|
||||||
|
- 16h49 : Arrêt d’urgence répété.
|
||||||
|
- 16h50 : Pompe à l’arrê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 l’application
|
||||||
|
|
||||||
|
- **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 l’homogé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 l’application lors de l’annulation 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 d’urgence et
|
||||||
|
emballements nécessite une inspection approfondie.
|
||||||
|
|
||||||
|
---
|
||||||
163
simu/calcul_coord.ts
Normal file
163
simu/calcul_coord.ts
Normal 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
137
simu/erreur_mc.ts
Normal 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
122
utils/fit.ts
Normal 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
78
utils/list.ts
Normal 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
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue