Skip to content

Commit 05bed07

Browse files
Merge pull request #15 from OSIPI/OGC_AmsterdamUMC_standardized
Ogc amsterdam umc standardized
2 parents 442c514 + 0515c18 commit 05bed07

File tree

7 files changed

+295
-44
lines changed

7 files changed

+295
-44
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
.idea*
22
.github*
33
__pycache__/
4-
5-
4+
*.nii.gz
5+
bvals.txt
6+
*json
67
# Unit test / coverage reports
78
.tox/
89
.coverage

phantoms/MR_XCAT_qMRI/sim_ivim_sig.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# code adapted from MAtlab code by Eric Schrauben: https://github.com/schrau24/XCAT-ERIC
99
# This code generates a 4D IVIM phantom as nifti file
1010

11-
def phantom(bvalue, noise, TR=3000, TE=40, motion=False, rician=False, interleaved=False):
11+
def phantom(bvalue, noise, TR=8000, TE=80, motion=False, rician=False, interleaved=False):
1212
np.random.seed(42)
1313
if motion:
1414
states = range(1,21)
@@ -367,7 +367,8 @@ def XCAT_to_MR_DCE(XCAT, TR, TE, bvalue, D, f, Ds, b0=3, ivim_cont = True):
367367
bvalue = np.array([0., 1, 2, 5, 10, 20, 30, 50, 75, 100, 150, 250, 350, 400, 550, 700, 850, 1000])
368368
noise = 0.0005
369369
motion = False
370-
sig, XCAT, Dim, fim, Dpim, legend = phantom(bvalue, noise, motion=motion, interleaved=False)
370+
interleaved = False
371+
sig, XCAT, Dim, fim, Dpim, legend = phantom(bvalue, noise, motion=motion, interleaved=interleaved)
371372
# sig = np.flip(sig,axis=0)
372373
# sig = np.flip(sig,axis=1)
373374
res=np.eye(4)
@@ -402,12 +403,16 @@ def XCAT_to_MR_DCE(XCAT, TR, TE, bvalue, D, f, Ds, b0=3, ivim_cont = True):
402403
nifti_img = nib.Nifti1Image(sig, affine=res) # Replace affine if necessary
403404
# Save the NIfTI image to a file
404405
nifti_img.header.set_data_dtype(np.float64)
405-
if motion:
406+
if not motion:
407+
output_file = 'output.nii.gz' # Replace with your desired output file name
408+
elif interleaved:
406409
output_file = 'output_resp_int.nii.gz' # Replace with your desired output file name
407410
else:
408-
output_file = 'output.nii.gz' # Replace with your desired output file name
411+
output_file = 'output_resp.nii.gz' # Replace with your desired output file name
412+
409413
nib.save(nifti_img, output_file)
410414

415+
411416
nifti_img = nib.Nifti1Image(XCAT, affine=res) # Replace affine if necessary
412417
# Save the NIfTI image to a file
413418
output_file = 'output_xcat.nii.gz' # Replace with your desired output file name

src/original/OGC_AmsterdamUMC/LSQ_fitting.py

Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def order(Dt, Fp, Dp, S0=None):
6666
return Dt, Fp, Dp, S0
6767

6868

69-
def fit_segmented_array(bvalues, dw_data, njobs=4, bounds=([0, 0, 0.005],[0.005, 0.7, 0.2]), cutoff=75):
69+
def fit_segmented_array(bvalues, dw_data, njobs=4, bounds=([0, 0, 0.005],[0.005, 0.7, 0.2]), cutoff=75,p0=[0.001, 0.1, 0.01,1]):
7070
"""
7171
This is an implementation of the segmented fit, in which we first estimate D using a curve fit to b-values>cutoff;
7272
then estimate f from the fitted S0 and the measured S0 and finally estimate D* while fixing D and f. This fit
@@ -90,7 +90,7 @@ def fit_segmented_array(bvalues, dw_data, njobs=4, bounds=([0, 0, 0.005],[0.005,
9090
try:
9191
# define the parallel function
9292
def parfun(i):
93-
return fit_segmented(bvalues, dw_data[i, :], bounds=bounds, cutoff=cutoff)
93+
return fit_segmented(bvalues, dw_data[i, :], bounds=bounds, cutoff=cutoff,p0=p0)
9494

9595
output = Parallel(n_jobs=njobs)(delayed(parfun)(i) for i in tqdm(range(len(dw_data)), position=0, leave=True))
9696
Dt, Fp, Dp = np.transpose(output)
@@ -107,11 +107,11 @@ def parfun(i):
107107
Fp = np.zeros(len(dw_data))
108108
for i in tqdm(range(len(dw_data)), position=0, leave=True):
109109
# fill arrays with fit results on a per voxel base:
110-
Dt[i], Fp[i], Dp[i] = fit_segmented(bvalues, dw_data[i, :], bounds=bounds, cutoff=cutoff)
110+
Dt[i], Fp[i], Dp[i] = fit_segmented(bvalues, dw_data[i, :], bounds=bounds, cutoff=cutoff,p0=p0)
111111
return [Dt, Fp, Dp, S0]
112112

113113

114-
def fit_segmented(bvalues, dw_data, bounds=([0, 0, 0.005],[0.005, 0.7, 0.2]), cutoff=75):
114+
def fit_segmented(bvalues, dw_data, bounds=([0, 0, 0.005],[0.005, 0.7, 0.2]), cutoff=75,p0=[0.001, 0.1, 0.01,1]):
115115
"""
116116
This is an implementation of the segmented fit, in which we first estimate D using a curve fit to b-values>cutoff;
117117
then estimate f from the fitted S0 and the measured S0 and finally estimate D* while fixing D and f.
@@ -124,23 +124,24 @@ def fit_segmented(bvalues, dw_data, bounds=([0, 0, 0.005],[0.005, 0.7, 0.2]), cu
124124
:return Dp: Fitted Dp
125125
:return S0: Fitted S0
126126
"""
127+
p0 = [p0[0] * 1000, p0[1] * 10, p0[2] * 10, p0[3]]
127128
try:
128129
# determine high b-values and data for D
129130
high_b = bvalues[bvalues >= cutoff]
130131
high_dw_data = dw_data[bvalues >= cutoff]
131132
# correct the bounds. Note that S0 bounds determine the max and min of f
132-
bounds1 = ([bounds[0][0] * 1000., 1 - bounds[1][1]], [bounds[1][0] * 1000., 1. - bounds[0][
133+
bounds1 = ([bounds[0][0] * 1000., 0.7 - bounds[1][1]], [bounds[1][0] * 1000., 1.3 - bounds[0][
133134
1]]) # By bounding S0 like this, we effectively insert the boundaries of f
134135
# fit for S0' and D
135136
params, _ = curve_fit(lambda b, Dt, int: int * np.exp(-b * Dt / 1000), high_b, high_dw_data,
136-
p0=(1, 1),
137+
p0=(p0[0], p0[3]-p0[1]/10),
137138
bounds=bounds1)
138139
Dt, Fp = params[0] / 1000, 1 - params[1]
139140
# remove the diffusion part to only keep the pseudo-diffusion
140141
dw_data_remaining = dw_data - (1 - Fp) * np.exp(-bvalues * Dt)
141-
bounds2 = (bounds[0][2], bounds[1][2])
142+
bounds2 = (bounds[0][2]*10, bounds[1][2]*10)
142143
# fit for D*
143-
params, _ = curve_fit(lambda b, Dp: Fp * np.exp(-b * Dp), bvalues, dw_data_remaining, p0=(0.1), bounds=bounds2)
144+
params, _ = curve_fit(lambda b, Dp: Fp * np.exp(-b * Dp), bvalues, dw_data_remaining, p0=(p0[2]), bounds=bounds2)
144145
Dp = params[0]
145146
return Dt, Fp, Dp
146147
except:
@@ -150,7 +151,7 @@ def fit_segmented(bvalues, dw_data, bounds=([0, 0, 0.005],[0.005, 0.7, 0.2]), cu
150151

151152

152153
def fit_least_squares_array(bvalues, dw_data, S0_output=True, fitS0=True, njobs=4,
153-
bounds=([0, 0, 0.005, 0.7],[0.005, 0.7, 0.2, 1.3])):
154+
bounds=([0, 0, 0.005, 0.7],[0.005, 0.7, 0.2, 1.3]),p0=[0.001, 0.1, 0.01, 1]):
154155
"""
155156
This is an implementation of the conventional IVIM fit. It is fitted in array form.
156157
:param bvalues: 1D Array with the b-values
@@ -175,7 +176,7 @@ def fit_least_squares_array(bvalues, dw_data, S0_output=True, fitS0=True, njobs=
175176
try:
176177
# defining parallel function
177178
def parfun(i):
178-
return fit_least_squares(bvalues, dw_data[i, :], S0_output=S0_output, fitS0=fitS0, bounds=bounds)
179+
return fit_least_squares(bvalues, dw_data[i, :], S0_output=S0_output, fitS0=fitS0, bounds=bounds,p0=p0)
179180

180181
output = Parallel(n_jobs=njobs)(delayed(parfun)(i) for i in tqdm(range(len(dw_data)), position=0, leave=True))
181182
Dt, Fp, Dp, S0 = np.transpose(output)
@@ -192,14 +193,14 @@ def parfun(i):
192193
# running in a single loop and filling arrays
193194
for i in tqdm(range(len(dw_data)), position=0, leave=True):
194195
Dt[i], Fp[i], Dp[i], S0[i] = fit_least_squares(bvalues, dw_data[i, :], S0_output=S0_output, fitS0=fitS0,
195-
bounds=bounds)
196+
bounds=bounds,p0=p0)
196197
return [Dt, Fp, Dp, S0]
197198
else:
198199
# if S0 is not exported
199200
if njobs > 1:
200201
try:
201202
def parfun(i):
202-
return fit_least_squares(bvalues, dw_data[i, :], fitS0=fitS0, bounds=bounds)
203+
return fit_least_squares(bvalues, dw_data[i, :], fitS0=fitS0, bounds=bounds,p0=p0)
203204

204205
output = Parallel(n_jobs=njobs)(delayed(parfun)(i) for i in tqdm(range(len(dw_data)), position=0, leave=True))
205206
Dt, Fp, Dp = np.transpose(output)
@@ -213,12 +214,12 @@ def parfun(i):
213214
Fp = np.zeros(len(dw_data))
214215
for i in range(len(dw_data)):
215216
Dt[i], Fp[i], Dp[i] = fit_least_squares(bvalues, dw_data[i, :], S0_output=S0_output, fitS0=fitS0,
216-
bounds=bounds)
217+
bounds=bounds,p0=p0)
217218
return [Dt, Fp, Dp]
218219

219220

220221
def fit_least_squares(bvalues, dw_data, S0_output=False, fitS0=True,
221-
bounds=([0, 0, 0.005, 0.7],[0.005, 0.7, 0.2, 1.3])):
222+
bounds=([0, 0, 0.005, 0.7],[0.005, 0.7, 0.2, 1.3]), p0=[0.001, 0.1, 0.01, 1]):
222223
"""
223224
This is an implementation of the conventional IVIM fit. It fits a single curve
224225
:param bvalues: Array with the b-values
@@ -236,13 +237,15 @@ def fit_least_squares(bvalues, dw_data, S0_output=False, fitS0=True,
236237
# bounds are rescaled such that each parameter changes at roughly the same rate to help fitting.
237238
bounds = ([bounds[0][0] * 1000, bounds[0][1] * 10, bounds[0][2] * 10],
238239
[bounds[1][0] * 1000, bounds[1][1] * 10, bounds[1][2] * 10])
239-
params, _ = curve_fit(ivimN_noS0, bvalues, dw_data, p0=[1, 1, 0.1], bounds=bounds)
240+
p0=[p0[0]*1000,p0[1]*10,p0[2]*10]
241+
params, _ = curve_fit(ivimN_noS0, bvalues, dw_data, p0=p0, bounds=bounds)
240242
S0 = 1
241243
else:
242244
# bounds are rescaled such that each parameter changes at roughly the same rate to help fitting.
243245
bounds = ([bounds[0][0] * 1000, bounds[0][1] * 10, bounds[0][2] * 10, bounds[0][3]],
244246
[bounds[1][0] * 1000, bounds[1][1] * 10, bounds[1][2] * 10, bounds[1][3]])
245-
params, _ = curve_fit(ivimN, bvalues, dw_data, p0=[1, 1, 0.1, 1], bounds=bounds)
247+
p0=[p0[0]*1000,p0[1]*10,p0[2]*10,p0[3]]
248+
params, _ = curve_fit(ivimN, bvalues, dw_data, p0=p0, bounds=bounds)
246249
S0 = params[3]
247250
# correct for the rescaling of parameters
248251
Dt, Fp, Dp = params[0] / 1000, params[1] / 10, params[2] / 10
@@ -463,6 +466,20 @@ def fit_segmented_tri_exp(bvalues, dw_data, bounds=([0, 0, 0, 0.005, 0, 0.06], [
463466
return 0., 0., 0., 0., 0., 0.
464467

465468

469+
def neg_log_likelihood(p, bvalues, dw_data):
470+
"""
471+
This function determines the negative of the log of the likelihood of parameters p, given the data dw_data for the Bayesian fit
472+
:param p: 1D Array with the estimates of D, f, D* and (optionally) S0
473+
:param bvalues: 1D array with b-values
474+
:param dw_data: 1D Array diffusion-weighted data
475+
:returns: the log-likelihood of the parameters given the data
476+
"""
477+
if len(p) == 4:
478+
return 0.5 * (len(bvalues) + 1) * np.log(
479+
np.sum((ivim(bvalues, p[0], p[1], p[2], p[3]) - dw_data) ** 2)) # 0.5*sum simplified
480+
else:
481+
return 0.5 * (len(bvalues) + 1) * np.log(
482+
np.sum((ivim(bvalues, p[0], p[1], p[2], 1) - dw_data) ** 2)) # 0.5*sum simplified
466483
def empirical_neg_log_prior(Dt0, Fp0, Dp0, S00=None):
467484
"""
468485
This function determines the negative of the log of the empirical prior probability of the IVIM parameters
@@ -516,23 +533,6 @@ def neg_log_prior(p):
516533

517534
return neg_log_prior
518535

519-
520-
def neg_log_likelihood(p, bvalues, dw_data):
521-
"""
522-
This function determines the negative of the log of the likelihood of parameters p, given the data dw_data for the Bayesian fit
523-
:param p: 1D Array with the estimates of D, f, D* and (optionally) S0
524-
:param bvalues: 1D array with b-values
525-
:param dw_data: 1D Array diffusion-weighted data
526-
:returns: the log-likelihood of the parameters given the data
527-
"""
528-
if len(p) == 4:
529-
return 0.5 * (len(bvalues) + 1) * np.log(
530-
np.sum((ivim(bvalues, p[0], p[1], p[2], p[3]) - dw_data) ** 2)) # 0.5*sum simplified
531-
else:
532-
return 0.5 * (len(bvalues) + 1) * np.log(
533-
np.sum((ivim(bvalues, p[0], p[1], p[2], 1) - dw_data) ** 2)) # 0.5*sum simplified
534-
535-
536536
def neg_log_posterior(p, bvalues, dw_data, neg_log_prior):
537537
"""
538538
This function determines the negative of the log of the likelihood of parameters p, given the prior likelihood and the data
@@ -545,6 +545,39 @@ def neg_log_posterior(p, bvalues, dw_data, neg_log_prior):
545545
return neg_log_likelihood(p, bvalues, dw_data) + neg_log_prior(p)
546546

547547

548+
def flat_neg_log_prior(Dt_range, Fp_range, Dp_range, S0_range=None):
549+
"""
550+
This function determines the negative of the log of the empirical prior probability of the IVIM parameters
551+
:param Dt0: 1D Array with the initial D estimates
552+
:param Dt0: 1D Array with the initial f estimates
553+
:param Dt0: 1D Array with the initial D* estimates
554+
:param Dt0: 1D Array with the initial S0 estimates (optional)
555+
"""
556+
def neg_log_prior(p):
557+
# depends on whether S0 is fitted or not
558+
if len(p) == 4:
559+
Dt, Fp, Dp, S0 = p[0], p[1], p[2], p[3]
560+
else:
561+
Dt, Fp, Dp = p[0], p[1], p[2]
562+
# make D*<D very unlikely
563+
if (Dp < Dt):
564+
return 1e3
565+
else:
566+
# determine and return the prior for D, f and D* (and S0)
567+
if len(p) == 4:
568+
if Dt_range[0] < Dt < Dt_range[1] and Fp_range[0] < Fp < Fp_range[1] and Dp_range[0] < Dp < Dp_range[1]: # and S0_range[0] < S0 < S0_range[1]: << not sure whether this helps. Technically it should be here
569+
return 0
570+
else:
571+
return 1e3
572+
else:
573+
if Dt_range[0] < Dt < Dt_range[1] and Fp_range[0] < Fp < Fp_range[1] and Dp_range[0] < Dp < Dp_range[1]:
574+
return 0
575+
else:
576+
return 1e3
577+
578+
return neg_log_prior
579+
580+
548581
def fit_bayesian_array(bvalues, dw_data, paramslsq, arg):
549582
"""
550583
This is an implementation of the Bayesian IVIM fit for arrays. The fit is taken from Barbieri et al. which was
@@ -563,7 +596,6 @@ def fit_bayesian_array(bvalues, dw_data, paramslsq, arg):
563596
:return Dp: Array with Dp in each voxel
564597
:return S0: Array with S0 in each voxel
565598
"""
566-
arg = checkarg_lsq(arg)
567599
# fill out missing args
568600
Dt0, Fp0, Dp0, S00 = paramslsq
569601
# determine prior
@@ -606,7 +638,7 @@ def parfun(i):
606638
return Dt_pred, Fp_pred, Dp_pred, S0_pred
607639

608640

609-
def fit_bayesian(bvalues, dw_data, neg_log_prior, x0=[0.001, 0.2, 0.05], fitS0=True):
641+
def fit_bayesian(bvalues, dw_data, neg_log_prior, x0=[0.001, 0.2, 0.05, 1], fitS0=True):
610642
'''
611643
This is an implementation of the Bayesian IVIM fit. It returns the Maximum a posterior probability.
612644
The fit is taken from Barbieri et al. which was initially introduced in http://arxiv.org/10.1002/mrm.25765 and
@@ -623,7 +655,7 @@ def fit_bayesian(bvalues, dw_data, neg_log_prior, x0=[0.001, 0.2, 0.05], fitS0=T
623655
'''
624656
try:
625657
# define fit bounds
626-
bounds = [(0, 0.005), (0, 0.7), (0.005, 0.2), (0, 2.5)]
658+
bounds = [(0, 0.005), (0, 1.5), (0, 2), (0, 2.5)]
627659
# Find the Maximum a posterior probability (MAP) by minimising the negative log of the posterior
628660
if fitS0:
629661
params = minimize(neg_log_posterior, x0=x0, args=(bvalues, dw_data, neg_log_prior), bounds=bounds)
@@ -704,7 +736,7 @@ def goodness_of_fit(bvalues, Dt, Fp, Dp, S0, dw_data, Fp2=None, Dp2=None):
704736
# plt.ion()
705737
# plt.show()
706738
# print(R2[vox])
707-
return R2, adjust
739+
return R2, adjusted_R2
708740
# ed_R2
709741

710742
def MSE(bvalues, Dt, Fp, Dp, S0, dw_data):

src/original/PV_MUMC/two_step_IVIM_fit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def fit_least_squares_array(bvalues, dw_data, fitS0=True, bounds=([0.9, 0.0001,
5050
Fmv = np.zeros(len(dw_data))
5151
for i in tqdm.tqdm(range(len(dw_data)), position=0, leave=True):
5252
# fill arrays with fit results on a per voxel base:
53-
Dpar[i], Fmv[i], Dmv[i], S0[i] = fit_least_squares(bvalues, dw_data[i, :], S0_output=True, fitS0=fitS0, bounds=bounds)
53+
Dpar[i], Fmv[i], Dmv[i], S0[i] = fit_least_squares(bvalues, dw_data[i, :], S0_output=False, fitS0=fitS0, bounds=bounds)
5454
return [Dpar, Fmv, Dmv, S0]
5555

5656

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from src.wrappers.OsipiBase import OsipiBase
2+
from src.original.OGC_AmsterdamUMC.LSQ_fitting import flat_neg_log_prior, fit_bayesian, empirical_neg_log_prior, fit_segmented
3+
import numpy as np
4+
5+
class OGC_AmsterdamUMC_Bayesian_biexp(OsipiBase):
6+
"""
7+
Bayesian Bi-exponential fitting algorithm by Oliver Gurney-Champion, Amsterdam UMC
8+
"""
9+
10+
# I'm thinking that we define default attributes for each submission like this
11+
# And in __init__, we can call the OsipiBase control functions to check whether
12+
# the user inputs fulfil the requirements
13+
14+
# Some basic stuff that identifies the algorithm
15+
id_author = "Oliver Gurney Champion, Amsterdam UMC"
16+
id_algorithm_type = "Bi-exponential fit"
17+
id_return_parameters = "f, D*, D, S0"
18+
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"
19+
20+
# Algorithm requirements
21+
required_bvalues = 4
22+
required_thresholds = [0,
23+
0] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
24+
required_bounds = False
25+
required_bounds_optional = True # Bounds may not be required but are optional
26+
required_initial_guess = False
27+
required_initial_guess_optional = True
28+
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?
29+
30+
def __init__(self, bvalues=None, bounds=([0, 0, 0.005, 0.7],[0.005, 0.7, 0.2, 1.3]), initial_guess=None, fitS0=True, thresholds=None, prior_in=None):
31+
"""
32+
Everything this algorithm requires should be implemented here.
33+
Number of segmentation thresholds, bounds, etc.
34+
35+
Our OsipiBase object could contain functions that compare the inputs with
36+
the requirements.
37+
38+
Args:
39+
datain is a 2D array with values of D, f, D* (and S0) that will form the prior.
40+
"""
41+
super(OGC_AmsterdamUMC_Bayesian_biexp, self).__init__(bvalues, bounds,initial_guess,fitS0,prior_in)
42+
if bounds is None:
43+
self.bounds=([0, 0, 0.005, 0.7],[0.005, 1, 0.2, 1.3])
44+
else:
45+
self.bounds=bounds
46+
if prior_in is None:
47+
self.neg_log_prior=flat_neg_log_prior([self.bounds[0][0],self.bounds[1][0]],[self.bounds[0][1],self.bounds[1][1]],[self.bounds[0][2],self.bounds[1][2]],[self.bounds[0][3],self.bounds[1][3]])
48+
else:
49+
if len(prior_in) is 4:
50+
self.neg_log_prior = empirical_neg_log_prior(prior_in[0], prior_in[1], prior_in[2],prior_in[3])
51+
else:
52+
self.neg_log_prior = empirical_neg_log_prior(prior_in[0], prior_in[1], prior_in[2])
53+
self.OGC_algorithm = fit_bayesian
54+
if initial_guess is None:
55+
self.initial_guess = [0.001, 0.5, 0.1, 1]
56+
else:
57+
self.initial_guess = initial_guess
58+
self.fitS0=fitS0
59+
60+
def ivim_fit(self, signals, bvalues=None):
61+
"""Perform the IVIM fit
62+
63+
Args:
64+
signals (array-like)
65+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
66+
67+
Returns:
68+
_type_: _description_
69+
"""
70+
bvalues=np.array(bvalues)
71+
fit_results = fit_segmented(bvalues, signals,bounds=self.bounds,cutoff=150,p0=self.initial_guess)
72+
fit_results=fit_results+(1,)
73+
fit_results = self.OGC_algorithm(bvalues, signals, self.neg_log_prior, x0=fit_results, fitS0=self.fitS0)
74+
75+
D = fit_results[0]
76+
f = fit_results[1]
77+
Dstar = fit_results[2]
78+
79+
return f, Dstar, D

0 commit comments

Comments
 (0)