TPCCLIB
Loading...
Searching...
No Matches
imginput.c File Reference

Procedures for handling model input data. More...

#include "libtpcmodext.h"

Go to the source code of this file.

Functions

int imgReadModelingData (char *petfile, char *siffile, char *inputfile1, char *inputfile2, char *inputfile3, double *fitdur, int *fitframeNr, IMG *img, DFT *inp, DFT *iinp, int verifypeak, FILE *loginfo, int verbose, char *status)
 

Detailed Description

Procedures for handling model input data.

Author
Vesa Oikonen

Definition in file imginput.c.

Function Documentation

◆ imgReadModelingData()

int imgReadModelingData ( char * petfile,
char * siffile,
char * inputfile1,
char * inputfile2,
char * inputfile3,
double * fitdur,
int * fitframeNr,
IMG * img,
DFT * inp,
DFT * iinp,
int verifypeak,
FILE * loginfo,
int verbose,
char * status )

Read PET image and input TAC for modelling.

Input calibration units are converted to units of tissue data. Input data is optionally interpolated to tissue times. If input data extends much further than fit duration, the last part is removed to save computation time in simulations. Input data is optionally verified not to end too early, and not to start too late.

Returns
Returns 0 when successful, otherwise a non-zero value.
See also
imgRead, dftRead, dftReadinput, dftReadModelingData, dftEndtime, imgEndtime
Parameters
petfilePET dynamic image filename; one time frame is sufficient here.
siffileOptional SIF. SIF is automatically read with NIfTI and Analyze formats, if found in the same folder and files are consistently named. SIF filename needs to be specified here only if that is not the case, or if other image formats do not contain information on frame times or tracer isotope, or if weights from SIF are needed. Enter NULL if not needed.
inputfile11st input data filename.
inputfile22nd input data filename (or NULL if only not needed).
inputfile33rd input data filename (or NULL if only not needed).
fitdurFit duration (in minutes); shortened if longer than PET data; enter <=0 to use all data in image; input is verified to not be much shorter than fitdur.
fitframeNrNr of time frames (samples) in PET data that are inside fitdur will be written here; pointer MUST be provided.
imgPointer to initiated IMG structure into which PET data will be written.
inpPointer to initiated DFT into which input data (plasma, blood, and/or reference tissue TAC) will be written without interpolation; enter NULL if not needed. Times will be in seconds, like in image.
iinpPointer to initiated DFT into which input data (plasma, blood, and/or reference tissue TAC) will be written, interpolated to PET frames; enter NULL if not needed. Times will be in seconds, like in image.
verifypeakSet to <>0 to verify that input TAC does not start too late and has decent peak to provide reliable AUC0-T.
loginfoGive file pointer (for example stdout) where log information is printed; NULL if not needed; warnings will be printed in stderr anyway.
verboseVerbose level; if zero, then only warnings are printed into stderr.
statusPointer to a string (allocated for at least 256 chars) where error message or other execution status will be written; enter NULL, if not needed.

Definition at line 24 of file imginput.c.

67 {
68 int ret, fi, n, first, last, ii, input_nr=0;
69 DFT tmpdft, dft;
70 SIF sif;
71 double f, starttime, endtime;
72 FILE *wfp;
73 char *fname, tmp[512];
74
75
76 if(loginfo!=NULL && verbose>0) {
77 fprintf(loginfo, "imgReadModelingData(\n");
78 fprintf(loginfo, " %s,\n", petfile);
79 fprintf(loginfo, " %s,\n", inputfile1);
80 fprintf(loginfo, " %s,\n", inputfile2);
81 fprintf(loginfo, " %s,\n", inputfile3);
82 fprintf(loginfo, " %g,\n", *fitdur);
83 fprintf(loginfo, " *fitframeNr, *tis, *inp, *iinp *loginfo, %d, *status\n", verbose);
84 fprintf(loginfo, ")\n");
85 }
86 /* Check the function input */
87 if(status!=NULL) sprintf(status, "program error");
88 if(img==NULL || (inp==NULL && iinp==NULL)) return -1;
89 if(petfile==NULL || strlen(petfile)<1) return -2;
90 ret=0;
91 if(inputfile1==NULL || strlen(inputfile1)<1) return -3; else input_nr++;
92 if(inputfile2!=NULL && strlen(inputfile2)>0) input_nr++;
93 if(inputfile3!=NULL && strlen(inputfile3)>0) {
94 if(input_nr<2) return -4;
95 input_nr++;
96 }
97 if(fitframeNr==NULL || fitdur==NULL) return -5;
98 if(status!=NULL) sprintf(status, "arguments validated");
99 /* Warnings will be printed into stderr */
100 wfp=stderr;
101
102 /* Check that input file(s) exist, because image is read first, and it might take a while */
103 if(loginfo!=NULL && verbose>1) fprintf(loginfo, "checking access to input files\n");
104 if(access(inputfile1, 0) == -1) {
105 sprintf(tmp, "cannot read '%s'", inputfile1);
106 if(status!=NULL) strcpy(status, tmp);
107 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
108 else fprintf(wfp, "Error: %s\n", tmp);
109 return(1);
110 }
111 for(ii=2; ii<=input_nr; ii++) {
112 if(ii==2) fname=inputfile2; else fname=inputfile3;
113 if(access(fname, 0) == -1) {
114 sprintf(tmp, "cannot read '%s'", fname);
115 if(status!=NULL) strcpy(status, tmp);
116 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
117 else fprintf(wfp, "Error: %s\n", tmp);
118 return(1);
119 }
120 }
121
122 /* Delete any previous data and initiate temp data */
123 dftEmpty(inp); dftEmpty(iinp); imgEmpty(img);
124
125
126 /*
127 * Read PET image
128 */
129 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "reading image %s\n", petfile);
130 imgInit(img);
131 ret=imgRead(petfile, img); fflush(stderr); fflush(stdout);
132 if(ret) {
133 sprintf(tmp, "cannot read '%s': %s", petfile, img->statmsg);
134 if(status!=NULL) strcpy(status, tmp);
135 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
136 else fprintf(wfp, "Error: %s\n", tmp);
137 if(loginfo!=NULL) fprintf(loginfo, "imgRead(%s, img) := %d\n", petfile, ret);
138 return(2);
139 }
140 /* Check if PET data is raw or image */
141 if(img->type!=IMG_TYPE_IMAGE) {
142 sprintf(tmp, "%s is not an image.\n", petfile);
143 if(status!=NULL) strcpy(status, tmp);
144 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
145 else fprintf(wfp, "Error: %s\n", tmp);
146 imgEmpty(img); return(3);
147 }
148 if(loginfo!=NULL && verbose>2)
149 fprintf(loginfo, "image contains %d frames and %d planes.\n", img->dimt, img->dimz);
150
151 /* Read SIF, if specified */
152 if(siffile!=NULL && siffile[0]) {
153 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "reading SIF %s\n", siffile);
154 sifInit(&sif);
155 ret=sifRead(siffile, &sif);
156 if(ret) {
157 sprintf(tmp, "cannot read '%s': %s", siffile, siferrmsg);
158 if(status!=NULL) strcpy(status, tmp);
159 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
160 else fprintf(wfp, "Error: %s\n", tmp);
161 imgEmpty(img); return(4);
162 }
163 /* Get isotope from SIF, if available */
164 {
165 double hl;
167 if(hl>0.0) {
168 img->isotopeHalflife=60.0*hl;
169 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "isotope code read from %s\n", siffile);
170 }
171 }
172 /* Get frame times from SIF if not available in image */
173 if(!imgExistentTimes(img) && img->dimt<=sif.frameNr) {
174 for(fi=0; fi<img->dimt; fi++) {
175 img->start[fi]=sif.x1[fi]; img->end[fi]=sif.x2[fi];
176 img->mid[fi]=0.5*(sif.x1[fi]+sif.x2[fi]);
177 }
178 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "image frame times read from %s\n", siffile);
179 }
180 sifEmpty(&sif);
181 }
182
183 /* Check that image frame times are available */
184 if(loginfo!=NULL && verbose>1) fprintf(loginfo, "checking image contents\n");
185 if(!imgExistentTimes(img)) {
186 strcpy(tmp, "image frame times not available");
187 if(status!=NULL) strcpy(status, tmp);
188 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
189 else fprintf(wfp, "Error: %s\n", tmp);
190 imgEmpty(img); return(5);
191 }
192 /* Make sure that there is no overlap in image frames */
193 if(loginfo!=NULL && verbose>1) fprintf(loginfo, "checking frame overlap in %s\n", petfile);
194 ret=imgDeleteFrameOverlap(img);
195 if(ret) {
196 strcpy(tmp, "image has overlapping frame times");
197 if(status!=NULL) strcpy(status, tmp);
198 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
199 else fprintf(wfp, "Error: %s\n", tmp);
200 imgEmpty(img); return(5);
201 }
202 /* Check fit end time with the PET image */
203 if(!(*fitdur>0.0)) *fitdur=1.0E+99;
204 else if(*fitdur<1.0E+10) *fitdur*=60.0; // fit time must be in sec
205 *fitframeNr=fittime_from_img(img, fitdur, verbose-2);
206 if(*fitframeNr<1 || *fitdur<=3.5) {
207 strcpy(tmp, "image has no data in fit time range");
208 if(status!=NULL) strcpy(status, tmp);
209 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
210 else fprintf(wfp, "Error: %s\n", tmp);
211 imgEmpty(img); return(5);
212 }
213 *fitdur/=60.0; // back to minutes
214 if(loginfo!=NULL && verbose>3) {
215 fprintf(loginfo, "fittimeFinal := %g min\n", *fitdur);
216 fprintf(loginfo, "fitframeNr := %d\n", *fitframeNr);
217 }
218
219
220 /*
221 * Read first input data
222 */
223 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "reading input data in %s\n", inputfile1);
224 dftInit(&dft);
225 if(dftRead(inputfile1, &dft)) {
226 sprintf(tmp, "cannot read '%s': %s", inputfile1, dfterrmsg);
227 if(status!=NULL) strcpy(status, tmp);
228 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
229 else fprintf(wfp, "Error: %s\n", tmp);
230 imgEmpty(img); return(11);
231 }
232 /* Check TAC nr */
233 if(dft.voiNr>1) {
234 if(verbose>0) {
235 strcpy(tmp, "only first TAC is used as input");
236 if(loginfo!=NULL) fprintf(loginfo, "Warning: %s.\n", tmp);
237 else fprintf(wfp, "Warning: %s.\n", tmp);
238 }
239 dft.voiNr=1;
240 }
241 /* check if file contains NAs (missing values) */
242 if(dft_nr_of_NA(&dft)>0) {
243 strcpy(tmp, "missing values in input file");
244 if(status!=NULL) strcpy(status, tmp);
245 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s.\n", tmp);
246 else fprintf(wfp, "Error: %s.\n", tmp);
247 imgEmpty(img); dftEmpty(&dft); return(12);
248 }
249 /* Check the concentration units */
250 ret=cunit_check_dft_vs_img(&dft, img, tmp, verbose-2);
251 if(ret==0) {
252 if(loginfo!=NULL && verbose>3) fprintf(loginfo, "%s\n", tmp);
253 } else if(ret<0) {
254 fprintf(wfp, "Warning: %s\n", tmp);
255 } else {
256 if(status!=NULL) strcpy(status, tmp);
257 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s.\n", tmp);
258 else fprintf(wfp, "Error: %s.\n", tmp);
259 imgEmpty(img); dftEmpty(&dft); return(13);
260 }
261 if(verbose>3)
262 printf("input time range := %g - %g %s\n",
263 dft.x[0], dft.x[dft.frameNr-1], petTunit(dft.timeunit));
264 /* Set time unit to sec like in IMG */
265 if(dft.timeunit==TUNIT_UNKNOWN) {
266 if(dftEndtime(&dft)<0.2*imgEndtime(img)) dft.timeunit=TUNIT_MIN;
267 else dft.timeunit=TUNIT_SEC;
268 fprintf(wfp, "Warning: assuming that times are in %s in %s\n",
269 petTunit(dft.timeunit), inputfile1);
270 }
271 /* Set time unit to sec like in IMG */
272 ret=dftTimeunitConversion(&dft, TUNIT_SEC);
273 if(verbose>1)
274 printf("input time range := %g - %g %s\n",
275 dft.x[0], dft.x[dft.frameNr-1], petTunit(dft.timeunit));
276
277 /* Verify the peak if requested; only for the first input TAC, because
278 for example metabolite TAC may not show bolus shape */
279 if(verifypeak!=0) {
280 if(loginfo!=NULL && verbose>1) fprintf(loginfo, "verifying input peak\n");
281 ret=dftVerifyPeak(&dft, 0, verbose-5, tmp);
282 if(ret>0) {
283 if(status!=NULL) strcpy(status, tmp);
284 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s.\n", tmp);
285 else fprintf(wfp, "Error: %s.\n", tmp);
286 imgEmpty(img); dftEmpty(&dft); return(14);
287 }
288 }
289 if(verbose>5)
290 printf("input time range := %g - %g %s\n",
291 dft.x[0], dft.x[dft.frameNr-1], petTunit(dft.timeunit));
292
293
294 /*
295 * Read following input files, if required
296 */
297 dftInit(&tmpdft);
298 for(ii=2; ii<=input_nr; ii++) {
299 if(ii==2) fname=inputfile2; else fname=inputfile3;
300
301 /* Make room for one more curve in input data */
302 ret=dftAddmem(&dft, 1);
303 if(ret) {
304 strcpy(tmp, "cannot allocate more memory");
305 if(status!=NULL) strcpy(status, tmp);
306 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
307 else fprintf(wfp, "Error: %s\n", tmp);
308 imgEmpty(img); dftEmpty(&dft); return(15);
309 }
310 /* Read input TAC */
311 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "reading input data in %s\n", fname);
312 if(dftRead(fname, &tmpdft)) {
313 sprintf(tmp, "cannot read '%s': %s", fname, dfterrmsg);
314 if(status!=NULL) strcpy(status, tmp);
315 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
316 else fprintf(wfp, "Error: %s\n", tmp);
317 imgEmpty(img); dftEmpty(&dft); return(16);
318 }
319 /* Check TAC nr */
320 if(tmpdft.voiNr>1) {
321 if(verbose>0) {
322 strcpy(tmp, "only first TAC is used as input");
323 if(loginfo!=NULL) fprintf(loginfo, "Warning: %s.\n", tmp);
324 else fprintf(wfp, "Warning: %s.\n", tmp);
325 }
326 dft.voiNr=1;
327 }
328 /* check if file contains NAs (missing values) */
329 if(dft_nr_of_NA(&tmpdft)>0) {
330 strcpy(tmp, "missing values in input file");
331 if(status!=NULL) strcpy(status, tmp);
332 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
333 else fprintf(wfp, "Error: %s\n", tmp);
334 imgEmpty(img); dftEmpty(&dft); dftEmpty(&tmpdft); return(17);
335 }
336 /* Check and correct the sample time unit */
337 if(tmpdft.timeunit==TUNIT_UNKNOWN) {
338 if(dftEndtime(&tmpdft)<0.2*imgEndtime(img)) tmpdft.timeunit=TUNIT_MIN;
339 else tmpdft.timeunit=TUNIT_SEC;
340 fprintf(wfp, "Warning: assuming that times are in %s in %s\n",
341 petTunit(tmpdft.timeunit), fname);
342 }
343 dftTimeunitConversion(&tmpdft, dft.timeunit);
344 /* Check the concentration units */
345 ret=cunit_check_dft_vs_img(&tmpdft, img, tmp, verbose-2);
346 if(ret==0) {
347 if(loginfo!=NULL && verbose>3) fprintf(loginfo, "%s\n", tmp);
348 } else if(ret<0) {
349 fprintf(wfp, "Warning: %s\n", tmp);
350 } else {
351 if(status!=NULL) strcpy(status, tmp);
352 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
353 else fprintf(wfp, "Error: %s\n", tmp);
354 imgEmpty(img); dftEmpty(&dft); dftEmpty(&tmpdft); return(18);
355 }
356 /* Copy to input data */
357 if(loginfo!=NULL && verbose>1)
358 fprintf(loginfo, "interpolating %d samples into %d samples.\n", tmpdft.frameNr, dft.frameNr);
359 ret=dftInterpolateInto(&tmpdft, &dft, tmp, verbose);
360 if(ret) {
361 if(loginfo!=NULL) fprintf(loginfo, "dftInterpolateInto() := %d\n", ret);
362 if(status!=NULL) strcpy(status, tmp);
363 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
364 else fprintf(wfp, "Error: %s\n", tmp);
365 imgEmpty(img); dftEmpty(&dft); dftEmpty(&tmpdft); return(19);
366 }
367 /* Remove the originally read input data */
368 dftEmpty(&tmpdft);
369
370 } // next input file
371 if(verbose>10)
372 printf("input time range := %g - %g %s\n",
373 dft.x[0], dft.x[dft.frameNr-1], petTunit(dft.timeunit));
374
375
376 /*
377 * Check and set input data range vs fit time length
378 */
379 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "checking and setting input sample time range\n");
380 /* Check that input data does not end _much_ before fitdur;
381 dftInterpolateForIMG() below tests this again. */
382 starttime=0; endtime=*fitdur; // start and end times always in min
383 n=fittime_from_dft(&dft, &starttime, &endtime, &first, &last, verbose-1);
384 if(loginfo!=NULL && verbose>2) {
385 fprintf(loginfo, "starttime := %g min\n", starttime);
386 fprintf(loginfo, "endtime := %g min\n", endtime);
387 fprintf(loginfo, "sample_nr := %d\n", n);
388 }
389 if(*fitdur>1.5*endtime && (*fitdur-endtime)>0.15) {
390 strcpy(tmp, "input TAC is too short");
391 if(status!=NULL) strcpy(status, tmp);
392 else if(verbose>0) fprintf(wfp, "Error: %s\n", tmp);
393 imgEmpty(img); dftEmpty(&dft); return(21);
394 }
395#if(0)
396 /* Do NOT test this here! It would prevent using this function in
397 reading steady-state studies etc */
398 if(n<4) {
399 strcpy(tmp, "too few input samples in specified fit duration");
400 if(status!=NULL) strcpy(status, tmp);
401 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s\n", tmp);
402 else fprintf(wfp, "Error: %s\n", tmp);
403 imgEmpty(img); dftEmpty(&dft); return(22);
404 }
405#endif
406 /* Cut off too many input samples to make calculation faster */
407 if(verbose>10) {
408 printf("input time range := %g - %g %s\n",
409 dft.x[0], dft.x[dft.frameNr-1], petTunit(dft.timeunit));
410 printf("fitdur := %g min\n", *fitdur);
411 }
412 f=*fitdur*60.0+60.0; // add 60 s to allow time delay correction
413 if(dftEndtime(&dft)>f) {
414 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "Input TAC cutoff at %g sec\n", f);
415 for(fi=0; fi<dft.frameNr; fi++) if(dft.x[fi]>f) break;
416 if(fi<dft.frameNr) fi++;
417 dft.frameNr=fi;
418 }
419 if(verbose>10) printf("input time range := %g - %g %s\n",
420 dft.x[0], dft.x[dft.frameNr-1], petTunit(dft.timeunit));
421
422 /* Copy input data to given pointer, if required */
423 if(inp!=NULL) {
424 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "copying input TAC\n");
425 ret=dftdup(&dft, inp);
426 if(ret!=0) {
427 strcpy(tmp, "cannot copy TAC contents");
428 if(status!=NULL) strcpy(status, tmp);
429 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s.\n", tmp);
430 else fprintf(wfp, "Error: %s\n", tmp);
431 imgEmpty(img); dftEmpty(&dft); return(31);
432 }
433 }
434
435 /* Interpolate input data to given pointer, if required */
436 if(iinp!=NULL) {
437 if(loginfo!=NULL && verbose>0) fprintf(loginfo, "interpolating input TAC\n");
438 ret=dftInterpolateForIMG(&dft, img, *fitframeNr, iinp, NULL, NULL, verbose, tmp);
439 if(ret!=0) {
440 if(status!=NULL) strcpy(status, tmp);
441 else if(loginfo!=NULL) fprintf(loginfo, "Error: %s.\n", tmp);
442 else fprintf(wfp, "Error: %s\n", tmp);
443 imgEmpty(img); dftEmpty(&dft); return(33);
444 }
445 }
446
447 dftEmpty(&dft);
448 if(status!=NULL) sprintf(status, "ok");
449 return(0);
450}
void dftInit(DFT *data)
Definition dft.c:38
int dftdup(DFT *dft1, DFT *dft2)
Definition dft.c:655
char dfterrmsg[64]
Definition dft.c:6
int dftAddmem(DFT *dft, int voiNr)
Definition dft.c:107
void dftEmpty(DFT *data)
Definition dft.c:20
int dft_nr_of_NA(DFT *dft)
Definition dft.c:905
int dftVerifyPeak(DFT *dft, int index, int verbose, char *status)
Definition dftinput.c:747
int dftInterpolateInto(DFT *inp, DFT *tis, char *status, int verbose)
Definition dftint.c:281
int dftRead(char *filename, DFT *data)
Definition dftio.c:22
int dftTimeunitConversion(DFT *dft, int tunit)
Definition dftunit.c:119
int fittime_from_dft(DFT *dft, double *startTime, double *endTime, int *first, int *last, int verbose)
Definition fittime.c:16
double imgEndtime(IMG *img)
Definition fittime.c:339
int fittime_from_img(IMG *img, double *fittime, int verbose)
Definition fittime.c:74
double dftEndtime(DFT *dft)
Definition fittime.c:315
double hlFromIsotope(char *isocode)
Definition halflife.c:55
int imgExistentTimes(IMG *img)
Definition img.c:613
void imgEmpty(IMG *image)
Definition img.c:121
void imgInit(IMG *image)
Definition img.c:60
int imgRead(const char *fname, IMG *img)
Definition imgfile.c:26
int imgDeleteFrameOverlap(IMG *img)
Definition imgframe.c:77
void sifInit(SIF *data)
Definition sif.c:17
void sifEmpty(SIF *data)
Definition sif.c:33
int sifRead(char *filename, SIF *data)
Definition sifio.c:21
#define IMG_TYPE_IMAGE
char siferrmsg[128]
Definition sif.c:7
char * petTunit(int tunit)
Definition petunits.c:226
int dftInterpolateForIMG(DFT *input, IMG *img, int frame_nr, DFT *output, double *ti1, double *ti2, int verbose, char *status)
Definition misc_model.c:17
int cunit_check_dft_vs_img(DFT *dft, IMG *img, char *errmsg, int verbose)
Definition units_check.c:18
int timeunit
int voiNr
int frameNr
double * x
char type
unsigned short int dimt
float * start
unsigned short int dimz
float * end
const char * statmsg
float isotopeHalflife
float * mid
double * x1
int frameNr
double * x2
char isotope_name[8]