v2.0.0
Loading...
Searching...
No Matches
rt_averaging.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "rt_averaging.h"
18
20
21#include <utils/ioutils.h>
22#include "rt_detect_trigger.h"
23#include <math/numerics.h>
24
25//=============================================================================================================
26// QT INCLUDES
27//=============================================================================================================
28
29#include <QDebug>
30
31//=============================================================================================================
32// EIGEN INCLUDES
33//=============================================================================================================
34
35//=============================================================================================================
36// USED NAMESPACES
37//=============================================================================================================
38
39using namespace RTPROCESSINGLIB;
40using namespace FIFFLIB;
41using namespace UTILSLIB;
42using namespace Eigen;
43using namespace MNELIB;
44
45//=============================================================================================================
46// DEFINE MEMBER METHODS RtAveragingWorker
47//=============================================================================================================
48
50 quint32 iPreStimSamples,
51 quint32 iPostStimSamples,
52 qint32 iBaselineFromMSecs,
53 qint32 iBaselineToMSecs,
54 quint32 iTriggerIndex,
55 FiffInfo::SPtr pFiffInfo)
56: QObject()
57, m_iNumAverages(numAverages)
58, m_iPreStimSamples(iPreStimSamples)
59, m_iPostStimSamples(iPostStimSamples)
61, m_iNewTriggerIndex(iTriggerIndex)
65, m_pairBaselineSec(qMakePair(float(iBaselineFromMSecs) / 1000.0f, float(iBaselineToMSecs) / 1000.0f))
66, m_pFiffInfo(pFiffInfo)
67{
68 m_mapThresholds["eog"] = 300e-6;
69
70 m_stimEvokedSet.info = *m_pFiffInfo.data();
71
74
75 if (m_iNumAverages <= 0) {
76 qDebug() << "RtAveragingWorker::RtAveragingWorker - Number of averages <= 0. Setting to 1 as default.";
78 }
79}
80
81//=============================================================================================================
82
83void RtAveragingWorker::doWork(const MatrixXd& rawSegment)
84{
85 if (this->thread()->isInterruptionRequested()) {
86 return;
87 }
88
90 reset();
91 }
92
93 doAveraging(rawSegment);
94}
95
96//=============================================================================================================
97
99{
100 if (numAve <= 0) {
101 qDebug() << "[RtAveragingWorker::setAverageNumber] Number of averages <= 0 are not allowed. Returning.";
102 return;
103 }
104
105 if (numAve < m_iNumAverages) {
106 int iDiff = m_iNumAverages - numAve;
107
108 //Do averaging for each trigger type
109 QMutableMapIterator<double, QList<Eigen::MatrixXd>> idx(m_mapStimAve);
110
111 while (idx.hasNext()) {
112 idx.next();
113
114 if (idx.value().size() > iDiff) {
115 //Pop data from buffer
116 for (int i = 0; i < iDiff; ++i) {
117 idx.value().pop_front();
118 }
119 }
120 }
121 }
122
123 m_iNumAverages = numAve;
124}
125
126//=============================================================================================================
127
128void RtAveragingWorker::setPreStim(qint32 samples, qint32 secs)
129{
130 Q_UNUSED(secs);
131
132 m_iNewPreStimSamples = samples;
133}
134
135//=============================================================================================================
136
137void RtAveragingWorker::setPostStim(qint32 samples, qint32 secs)
138{
139 Q_UNUSED(secs);
140
141 m_iNewPostStimSamples = samples;
142}
143
144//=============================================================================================================
145
147{
148 m_iNewTriggerIndex = idx;
149}
150
151//=============================================================================================================
152
153void RtAveragingWorker::setArtifactReduction(const QMap<QString, double>& mapThresholds)
154{
155 if (mapThresholds["Active"] == 0.0) {
156 m_bActivateThreshold = false;
157 } else {
159 }
160
161 m_mapThresholds = mapThresholds;
162}
163
164//=============================================================================================================
165
167{
168 m_bDoBaselineCorrection = activate;
169
171 for (int i = 0; i < m_stimEvokedSet.evoked.size(); ++i) {
172 m_stimEvokedSet.evoked[i].baseline = qMakePair(-1.0f, -1.0f);
173 }
174 } else {
175 for (int i = 0; i < m_stimEvokedSet.evoked.size(); ++i) {
176 m_stimEvokedSet.evoked[i].baseline = m_pairBaselineSec;
177 }
178 }
179}
180
181//=============================================================================================================
182
184 int fromMSec)
185{
186 m_pairBaselineSec.first = float(fromMSec) / 1000.0f;
187 m_pairBaselineSamp.first = float(fromSamp);
188
189 for (int i = 0; i < m_stimEvokedSet.evoked.size(); ++i) {
190 m_stimEvokedSet.evoked[i].baseline.first = float(fromMSec) / 1000.0f;
191 }
192}
193
194//=============================================================================================================
195
197 int toMSec)
198{
199 m_pairBaselineSec.second = float(toMSec) / 1000.0f;
200 m_pairBaselineSamp.second = float(toSamp);
201
202 for (int i = 0; i < m_stimEvokedSet.evoked.size(); ++i) {
203 m_stimEvokedSet.evoked[i].baseline.second = float(toMSec) / 1000.0f;
204 }
205}
206
207//=============================================================================================================
208
209void RtAveragingWorker::doAveraging(const MatrixXd& rawSegment)
210{
211 //Detect trigger
212 QList<QPair<int, double>> lDetectedTriggers = RTPROCESSINGLIB::detectTriggerFlanksMax(rawSegment, m_iTriggerChIndex, 0, m_fTriggerThreshold, true);
213
214 //TODO: This does not permit the same trigger type twice in one data block
215 for (int i = 0; i < lDetectedTriggers.size(); ++i) {
216 if (!m_mapFillingBackBuffer.contains(lDetectedTriggers.at(i).second)) {
217 double dTriggerType = lDetectedTriggers.at(i).second;
218
219 //qDebug()<<"Adding dTriggerType"<<dTriggerType;
220
221 m_mapFillingBackBuffer[dTriggerType] = false;
222 m_mapMatDataPostIdx[dTriggerType] = 0;
223 m_mapDataPost[dTriggerType].resize(m_pFiffInfo->chs.size(), m_iPostStimSamples);
224 m_mapDataPost[dTriggerType].setZero();
225 }
226 }
227
228 // Pre-stimulus samples of a trigger may reach back into the previous blocks
229 MatrixXd matHistoryAndBlock(rawSegment.rows(), m_matPreHistory.cols() + rawSegment.cols());
230 matHistoryAndBlock << m_matPreHistory, rawSegment;
231
232 //Do averaging for each trigger type
233 QMutableMapIterator<double, bool> idx(m_mapFillingBackBuffer);
234 QStringList lResponsibleTriggerTypes;
235
236 while (idx.hasNext()) {
237 idx.next();
238
239 double dTriggerType = idx.key();
240
241 //Fill back buffer and decide when to do the data packing of the different buffers
242 if (m_mapFillingBackBuffer[dTriggerType]) {
243 if (m_mapMatDataPostIdx[dTriggerType] != m_iPostStimSamples) {
244 fillBackBuffer(rawSegment, dTriggerType);
245 }
246
247 if (m_mapMatDataPostIdx[dTriggerType] == m_iPostStimSamples) {
248 m_mapMatDataPostIdx[dTriggerType] = 0;
249 m_mapFillingBackBuffer[dTriggerType] = false;
250 emitEvoked(dTriggerType, lResponsibleTriggerTypes);
251 }
252 } else {
253 for (int i = 0; i < lDetectedTriggers.size(); ++i) {
254 if (dTriggerType == lDetectedTriggers.at(i).second) {
255 int iTriggerPos = lDetectedTriggers.at(i).first;
256
257 m_mapDataPre[dTriggerType] = matHistoryAndBlock.middleCols(iTriggerPos, m_iPreStimSamples);
258
259 //Do back buffer stuff
260 if (rawSegment.cols() - iTriggerPos >= m_mapDataPost[dTriggerType].cols()) {
261 m_mapDataPost[dTriggerType] = rawSegment.block(0,
262 iTriggerPos,
263 m_mapDataPost[dTriggerType].rows(),
264 m_mapDataPost[dTriggerType].cols());
265 emitEvoked(dTriggerType, lResponsibleTriggerTypes);
266 m_mapMatDataPostIdx[dTriggerType] = 0;
267 m_mapFillingBackBuffer[dTriggerType] = false;
268 } else {
269 m_mapDataPost[dTriggerType].block(0,
270 0,
271 m_mapDataPost[dTriggerType].rows(),
272 rawSegment.cols() - iTriggerPos) = rawSegment.block(0,
273 iTriggerPos,
274 rawSegment.rows(),
275 rawSegment.cols() - iTriggerPos);
276 m_mapMatDataPostIdx[dTriggerType] = rawSegment.cols() - iTriggerPos;
277 m_mapFillingBackBuffer[dTriggerType] = true;
278 }
279 }
280 }
281 }
282 }
283
284 m_matPreHistory = matHistoryAndBlock.rightCols(m_iPreStimSamples);
285}
286
287//=============================================================================================================
288
289void RtAveragingWorker::emitEvoked(double dTriggerType, QStringList& lResponsibleTriggerTypes)
290{
291 //Merge the different buffers
292 mergeData(dTriggerType);
293
294 //Calculate the final average/evoked data
295 generateEvoked(dTriggerType);
296
297 //List of all trigger types which lead to the recent emit of a new evoked set. */
298 if (!lResponsibleTriggerTypes.contains(QString::number(dTriggerType))) {
299 lResponsibleTriggerTypes << QString::number(dTriggerType);
300 }
301
302 if (m_stimEvokedSet.evoked.size() > 0) {
303 emit resultReady(m_stimEvokedSet, lResponsibleTriggerTypes);
304 }
305
306 // qDebug()<<"RtAveragingWorker::emitEvoked() - dTriggerType:" << dTriggerType;
307 // qDebug()<<"RtAveragingWorker::emitEvoked() - m_mapStimAve[dTriggerType].size():" << m_mapStimAve[dTriggerType].size();
308}
309
310//=============================================================================================================
311
312void RtAveragingWorker::fillBackBuffer(const MatrixXd& data, double dTriggerType)
313{
314 int iResidualCols = data.cols();
315 if (m_mapMatDataPostIdx[dTriggerType] + data.cols() > m_iPostStimSamples) {
316 iResidualCols = m_iPostStimSamples - m_mapMatDataPostIdx[dTriggerType];
317 m_mapDataPost[dTriggerType].block(0, m_mapMatDataPostIdx[dTriggerType], m_mapDataPost[dTriggerType].rows(), iResidualCols) = data.block(0, 0, data.rows(), iResidualCols);
318 } else {
319 m_mapDataPost[dTriggerType].block(0, m_mapMatDataPostIdx[dTriggerType], m_mapDataPost[dTriggerType].rows(), iResidualCols) = data;
320 }
321
322 m_mapMatDataPostIdx[dTriggerType] += iResidualCols;
323}
324
325//=============================================================================================================
326
327void RtAveragingWorker::mergeData(double dTriggerType)
328{
329 if (m_mapDataPre[dTriggerType].rows() != m_mapDataPost[dTriggerType].rows()) {
330 qDebug() << "[RtAveragingWorker::mergeData] Rows of m_mapDataPre (" << m_mapDataPre[dTriggerType].rows() << ") and m_mapDataPost (" << m_mapDataPost[dTriggerType].rows() << ") are not the same. Returning.";
331 return;
332 }
333
334 MatrixXd mergedData(m_mapDataPre[dTriggerType].rows(), m_mapDataPre[dTriggerType].cols() + m_mapDataPost[dTriggerType].cols());
335
336 mergedData << m_mapDataPre[dTriggerType], m_mapDataPost[dTriggerType];
337
338 //Perform artifact threshold
339 bool bArtifactDetected = false;
340
342 qDebug() << "[RtAveragingWorker::mergeData] Doing artifact reduction for" << m_mapThresholds;
343
344 bArtifactDetected = MNEEpochDataList::checkForArtifact(mergedData,
347 }
348
349 if (!bArtifactDetected) {
350 //Add cut data to average buffer
351 m_mapStimAve[dTriggerType].append(mergedData);
352
353 //Pop data from buffer
354 int iDiff = m_mapStimAve[dTriggerType].size() - m_iNumAverages;
355 if (iDiff > 0) {
356 for (int i = 0; i < iDiff; ++i) {
357 m_mapStimAve[dTriggerType].pop_front();
358 }
359 }
360 }
361}
362
363//=============================================================================================================
364
365void RtAveragingWorker::generateEvoked(double dTriggerType)
366{
367 if (m_mapStimAve[dTriggerType].isEmpty()) {
368 qDebug() << "[RtAveragingWorker::generateEvoked] m_mapStimAve is empty for type" << dTriggerType << "Returning.";
369 return;
370 }
371
372 //Init evoked
373 m_stimEvokedSet.info = *m_pFiffInfo.data();
374 FiffEvoked evoked;
375 evoked.setInfo(*m_pFiffInfo.data());
376 int iEvokedIdx = -1;
377
378 for (int i = 0; i < m_stimEvokedSet.evoked.size(); ++i) {
379 if (m_stimEvokedSet.evoked.at(i).comment == QString::number(dTriggerType)) {
380 evoked = m_stimEvokedSet.evoked.at(i);
381 iEvokedIdx = i;
382 break;
383 }
384 }
385
386 //If the evoked is not yet present add it here
387 if (iEvokedIdx == -1) {
389 evoked.times = RowVectorXf::LinSpaced(m_iPreStimSamples + m_iPostStimSamples,
391 m_iPostStimSamples - 1) /
392 m_pFiffInfo->sfreq;
393 evoked.first = -m_iPreStimSamples;
394 evoked.last = m_iPostStimSamples - 1;
395 evoked.comment = QString::number(dTriggerType);
396 }
397
398 // Generate final evoked
399 MatrixXd finalAverage = MatrixXd::Zero(m_mapStimAve[dTriggerType].first().rows(), m_iPreStimSamples + m_iPostStimSamples);
400
401 for (int i = 0; i < m_mapStimAve[dTriggerType].size(); ++i) {
402 finalAverage += m_mapStimAve[dTriggerType].at(i);
403 }
404
405 if (!m_mapStimAve[dTriggerType].isEmpty()) {
406 finalAverage = finalAverage / m_mapStimAve[dTriggerType].size();
407 }
408
410 finalAverage = Numerics::rescale(finalAverage, evoked.times, m_pairBaselineSec, QString("mean"));
411 }
412
413 evoked.data = finalAverage;
414
415 evoked.nave = m_mapStimAve[dTriggerType].size();
416
417 //Add new data to evoked data set
418 if (iEvokedIdx != -1) {
419 //Evoked data is already present
420 m_stimEvokedSet.evoked[iEvokedIdx] = evoked;
421 } else {
422 //Evoked data is not present yet
423 m_stimEvokedSet.evoked.append(evoked);
424 }
425}
426
427//=============================================================================================================
428
430{
431 //Reset
435
436 //Clear all evoked data information
437 m_stimEvokedSet.evoked.clear();
438
439 //Clear all maps
440 m_mapStimAve.clear();
441 m_mapDataPre.clear();
442 m_matPreHistory = MatrixXd::Zero(m_pFiffInfo->chs.size(), m_iPreStimSamples);
443 m_mapDataPost.clear();
444 m_mapMatDataPostIdx.clear();
446}
447
448//=============================================================================================================
449// DEFINE MEMBER METHODS RtAveraging
450//=============================================================================================================
451
452RtAveraging::RtAveraging(quint32 numAverages,
453 quint32 iPreStimSamples,
454 quint32 iPostStimSamples,
455 qint32 iBaselineFromMSecs,
456 qint32 iBaselineToMSecs,
457 quint32 iTriggerIndex,
458 FiffInfo::SPtr pFiffInfo,
459 QObject* parent)
460: QObject(parent)
461{
462 qRegisterMetaType<Eigen::MatrixXd>("Eigen::MatrixXd");
463
464 RtAveragingWorker* worker = new RtAveragingWorker(numAverages,
465 iPreStimSamples,
466 iPostStimSamples,
467 iBaselineFromMSecs,
468 iBaselineToMSecs,
469 iTriggerIndex,
470 pFiffInfo);
471 worker->moveToThread(&m_workerThread);
472
473 connect(&m_workerThread, &QThread::finished,
474 worker, &QObject::deleteLater);
475
476 connect(this, &RtAveraging::operate,
478
479 connect(worker, &RtAveragingWorker::resultReady,
480 this, &RtAveraging::handleResults, Qt::DirectConnection);
481
499 worker, &RtAveragingWorker::reset);
500
501 m_workerThread.start();
502}
503
504//=============================================================================================================
505
510
511//=============================================================================================================
512
513void RtAveraging::append(const MatrixXd& data)
514{
515 emit operate(data);
516}
517
518//=============================================================================================================
519
521 const QStringList& lResponsibleTriggerTypes)
522{
523 emit evokedStim(evokedStimSet,
524 lResponsibleTriggerTypes);
525}
526
527//=============================================================================================================
528
529void RtAveraging::restart(quint32 numAverages,
530 quint32 iPreStimSamples,
531 quint32 iPostStimSamples,
532 qint32 iBaselineFromMSecs,
533 qint32 iBaselineToMSecs,
534 quint32 iTriggerIndex,
535 FiffInfo::SPtr pFiffInfo)
536{
537 stop();
538
539 RtAveragingWorker* worker = new RtAveragingWorker(numAverages,
540 iPreStimSamples,
541 iPostStimSamples,
542 iBaselineFromMSecs,
543 iBaselineToMSecs,
544 iTriggerIndex,
545 pFiffInfo);
546 worker->moveToThread(&m_workerThread);
547
548 connect(&m_workerThread, &QThread::finished,
549 worker, &QObject::deleteLater);
550
551 connect(this, &RtAveraging::operate,
553
554 connect(worker, &RtAveragingWorker::resultReady,
555 this, &RtAveraging::handleResults, Qt::DirectConnection);
556
574 worker, &RtAveragingWorker::reset);
575
576 m_workerThread.start();
577}
578
579//=============================================================================================================
580
582{
583 m_workerThread.requestInterruption();
584 m_workerThread.quit();
585 m_workerThread.wait();
586}
587
588//=============================================================================================================
589
591{
592 emit averageNumberChanged(numAve);
593}
594
595//=============================================================================================================
596
597void RtAveraging::setPreStim(qint32 samples,
598 qint32 secs)
599{
600 Q_UNUSED(secs);
601
602 emit averagePreStimChanged(samples,
603 secs);
604}
605
606//=============================================================================================================
607
608void RtAveraging::setPostStim(qint32 samples,
609 qint32 secs)
610{
611 Q_UNUSED(secs);
612
613 emit averagePostStimChanged(samples,
614 secs);
615}
616
617//=============================================================================================================
618
620{
622}
623
624//=============================================================================================================
625
626void RtAveraging::setArtifactReduction(const QMap<QString, double>& mapThresholds)
627{
628 emit averageArtifactReductionChanged(mapThresholds);
629}
630
631//=============================================================================================================
632
634{
635 emit averageBaselineActiveChanged(activate);
636}
637
638//=============================================================================================================
639
641 int fromMSec)
642{
643 emit averageBaselineFromChanged(fromSamp,
644 fromMSec);
645}
646
647//=============================================================================================================
648
650 int toMSec)
651{
652 emit averageBaselineToChanged(toSamp,
653 toMSec);
654}
655
656//=============================================================================================================
657
659{
661}
Header-only Eigen matrix text I/O — round-trips dense matrices to whitespace-separated ASCII for cros...
Threshold and edge-based trigger detection on streaming stim channels.
Real-time stimulus-locked averaging worker producing running evoked responses.
General numerical helpers: GCD, log2, histogram binning, baseline rescaling, sparsity tests.
Ordered list of MNELIB::MNEEpochData objects sharing a common FIFFLIB::FiffInfo.
Core MNE data structures (source spaces, source estimates, hemispheres).
FIFF file I/O, in-memory data structures and high-level readers/writers.
DSPSHARED_EXPORT QMap< int, QList< QPair< int, double > > > detectTriggerFlanksMax(const Eigen::MatrixXd &data, const QList< int > &lTriggerChannels, int iOffsetIndex, double dThreshold, bool bRemoveOffset, int iBurstLengthSamp=100)
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Background worker thread that accumulates and averages epochs in real time.
void emitEvoked(double dTriggerType, QStringList &lResponsibleTriggerTypes)
void setArtifactReduction(const QMap< QString, double > &mapThresholds)
FIFFLIB::FiffInfo::SPtr m_pFiffInfo
QPair< float, float > m_pairBaselineSamp
RtAveragingWorker(quint32 numAverages, quint32 iPreStimSamples, quint32 iPostStimSamples, qint32 iBaselineFromMSecs, qint32 iBaselineToMSecs, quint32 iTriggerIndex, FIFFLIB::FiffInfo::SPtr pFiffInfo)
QMap< double, qint32 > m_mapMatDataPostIdx
QMap< double, Eigen::MatrixXd > m_mapDataPost
void setBaselineTo(int toSamp, int toMSec)
QMap< double, Eigen::MatrixXd > m_mapDataPre
QMap< QString, double > m_mapThresholds
FIFFLIB::FiffEvokedSet m_stimEvokedSet
QPair< float, float > m_pairBaselineSec
void doAveraging(const Eigen::MatrixXd &rawSegment)
QMap< double, bool > m_mapFillingBackBuffer
void doWork(const Eigen::MatrixXd &matData)
void setPostStim(qint32 samples, qint32 secs)
QMap< double, QList< Eigen::MatrixXd > > m_mapStimAve
void resultReady(const FIFFLIB::FiffEvokedSet &evokedStimSet, const QStringList &lResponsibleTriggerTypes)
void fillBackBuffer(const Eigen::MatrixXd &data, double dTriggerType)
void setBaselineFrom(int fromSamp, int fromMSec)
void setPreStim(qint32 samples, qint32 secs)
void mergeData(double dTriggerType)
void generateEvoked(double dTriggerType)
void averageBaselineToChanged(int toSamp, int toMSec)
void setBaselineFrom(int fromSamp, int fromMSec)
void restart(quint32 numAverages, quint32 iPreStimSamples, quint32 iPostStimSamples, qint32 iBaselineFromMSecs, qint32 iBaselineToMSecs, quint32 iTriggerIndex, FIFFLIB::FiffInfo::SPtr pFiffInfo)
void averageBaselineActiveChanged(bool activate)
void setPreStim(qint32 samples, qint32 secs)
void averagePostStimChanged(qint32 samples, qint32 secs)
void setBaselineTo(int toSamp, int toMSec)
void averageNumberChanged(qint32 numAve)
void averagePreStimChanged(qint32 samples, qint32 secs)
void setAverageNumber(qint32 numAve)
void append(const Eigen::MatrixXd &data)
void averageArtifactReductionChanged(const QMap< QString, double > &mapThresholds)
void handleResults(const FIFFLIB::FiffEvokedSet &evokedStimSet, const QStringList &lResponsibleTriggerTypes)
void setPostStim(qint32 samples, qint32 secs)
void setTriggerChIndx(qint32 idx)
void setBaselineActive(bool activate)
void setArtifactReduction(const QMap< QString, double > &mapThresholds)
void evokedStim(const FIFFLIB::FiffEvokedSet &evokedStimSet, const QStringList &lResponsibleTriggerTypes)
void averageBaselineFromChanged(int fromSamp, int fromMSec)
void averageTriggerChIdxChanged(qint32 idx)
RtAveraging(quint32 numAverages, quint32 iPreStimSamples, quint32 iPostStimSamples, qint32 iBaselineFromMSecs, qint32 iBaselineToMSecs, quint32 iTriggerIndex, FIFFLIB::FiffInfo::SPtr pFiffInfo, QObject *parent=0)
void operate(const Eigen::MatrixXd &matData)
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:77
Eigen::RowVectorXf times
Eigen::MatrixXd data
QPair< float, float > baseline
void setInfo(const FiffInfo &p_info, bool applyProj=true)
Set of FiffEvoked instances sharing one FiffInfo, plus channel-picking and compensation helpers.
QSharedPointer< FiffInfo > SPtr
Definition fiff_info.h:92
static Eigen::MatrixXd rescale(const Eigen::MatrixXd &data, const Eigen::RowVectorXf &times, const QPair< float, float > &baseline, QString mode)
static bool checkForArtifact(const Eigen::MatrixXd &data, const FIFFLIB::FiffInfo &pFiffInfo, const QMap< QString, double > &mapReject, const QStringList &lExcludeChs=QStringList())