From f6160003dbd4bda597ff473541ce111a5d2dbb2d Mon Sep 17 00:00:00 2001 From: Murat Bilgel Date: Fri, 27 Sep 2024 17:58:42 -0400 Subject: [PATCH 1/5] fix: align parameter names with Innis et al. 2007 consensus paper --- src/dynamicpet/kineticmodel/kineticmodel.py | 8 +-- src/dynamicpet/kineticmodel/logan.py | 20 ++++--- src/dynamicpet/kineticmodel/srtm.py | 52 ++++++++--------- src/dynamicpet/kineticmodel/suvr.py | 6 +- tests/test_kineticmodel.py | 12 ++-- tests/test_kineticmodel_simulated.py | 64 ++++++++++----------- 6 files changed, 82 insertions(+), 80 deletions(-) diff --git a/src/dynamicpet/kineticmodel/kineticmodel.py b/src/dynamicpet/kineticmodel/kineticmodel.py index 0fe6cb3..87c390c 100644 --- a/src/dynamicpet/kineticmodel/kineticmodel.py +++ b/src/dynamicpet/kineticmodel/kineticmodel.py @@ -110,11 +110,11 @@ def get_parameter(self, param_name: str) -> SpatialImage | NumpyNumberArray: else: param_vector: NumpyNumberArray = self.parameters[param_name] return param_vector - elif param_name == "bp" and "dvr" in self.parameters: - self.parameters[param_name] = self.parameters["dvr"] - 1 + elif param_name == "BP_ND" and "DVR" in self.parameters: + self.parameters[param_name] = self.parameters["DVR"] - 1 return self.get_parameter(param_name) - elif param_name == "dvr" and "bp" in self.parameters: - self.parameters[param_name] = self.parameters["bp"] + 1 + elif param_name == "DVR" and "BP_ND" in self.parameters: + self.parameters[param_name] = self.parameters["BP_ND"] + 1 return self.get_parameter(param_name) else: raise AttributeError( diff --git a/src/dynamicpet/kineticmodel/logan.py b/src/dynamicpet/kineticmodel/logan.py index a631c92..78877a2 100644 --- a/src/dynamicpet/kineticmodel/logan.py +++ b/src/dynamicpet/kineticmodel/logan.py @@ -30,7 +30,7 @@ class LRTM(KineticModel): @classmethod def get_param_names(cls) -> list[str]: """Get names of kinetic model parameters.""" - return ["dvr"] + return ["DVR"] def fit( # noqa: max-complexity: 12 self, @@ -38,7 +38,7 @@ def fit( # noqa: max-complexity: 12 integration_type: INTEGRATION_TYPE_OPTS = "trapz", weight_by: WEIGHT_OPTS | NumpyNumberArray | None = "frame_duration", tstar: float = 0, - k2: float | None = None, + k2prime: float | None = None, ) -> None: """Estimate model parameters. @@ -56,7 +56,8 @@ def fit( # noqa: max-complexity: 12 to fit the kinetic model. Elements outside the mask will be set to to 0 in parametric estimate outputs. tstar: time beyond which to assume linearity - k2: (avg.) effective tissue-to-plasma efflux constant, in unit of 1/min + k2prime: (avg.) effective tissue-to-plasma efflux constant in the + reference region, in unit of 1/min """ # get reference TAC as a 1-D vector reftac: NumpyNumberArray = self.reftac.dataobj.flatten()[:, np.newaxis] @@ -82,7 +83,7 @@ def fit( # noqa: max-complexity: 12 dvr = np.zeros((num_elements, 1)) - if not k2: + if not k2prime: # TODO # Check Eq. 7 assumption (i.e., that tac / reftac is reasonably # constant) by calculating R2 etc. for each tac. @@ -103,13 +104,14 @@ def fit( # noqa: max-complexity: 12 # ----- Get DVR ----- # Set up the weighted linear regression model based on Logan et al.: - # - use Eq. 6 if k2 is provided - # - use Eq. 7 if k2 is not provided + # - use Eq. 6 if k2prime is provided + # - use Eq. 7 if k2prime is not provided x = np.column_stack( ( np.ones_like(tac_tstar), - (int_reftac_tstar + (reftac_tstar / k2 if k2 else 0)) / tac_tstar, + (int_reftac_tstar + (reftac_tstar / k2prime if k2prime else 0)) + / tac_tstar, ) ) y = int_tac_tstar / tac_tstar @@ -123,8 +125,8 @@ def fit( # noqa: max-complexity: 12 # distribution volume ratio dvr[k] = b[1] - self.set_parameter("dvr", dvr, mask) - # should tstar (and k2?) also be stored? + self.set_parameter("DVR", dvr, mask) + # should tstar (and k2prime?) also be stored? def fitted_tacs(self) -> TemporalMatrix | TemporalImage: """Get fitted TACs based on estimated model parameters.""" diff --git a/src/dynamicpet/kineticmodel/srtm.py b/src/dynamicpet/kineticmodel/srtm.py index 269cef4..511229b 100644 --- a/src/dynamicpet/kineticmodel/srtm.py +++ b/src/dynamicpet/kineticmodel/srtm.py @@ -30,7 +30,7 @@ class SRTMLammertsma1996(KineticModel): @classmethod def get_param_names(cls) -> list[str]: """Get names of kinetic model parameters.""" - return ["bp", "r1", "k2"] + return ["BP_ND", "R1", "k2"] def fit( self, @@ -56,7 +56,7 @@ def fit( weights = tacs.get_weights(weight_by) - bp = np.zeros((num_elements, 1)) + bp_nd = np.zeros((num_elements, 1)) r1 = np.zeros((num_elements, 1)) k2 = np.zeros((num_elements, 1)) for k in trange(num_elements): @@ -69,10 +69,10 @@ def fit( sigma=weights, bounds=([0, 0, 0], [15, 10, 1]), ) - bp[k], r1[k], k2[k] = popt + bp_nd[k], r1[k], k2[k] = popt - self.set_parameter("bp", bp, mask) - self.set_parameter("r1", r1, mask) + self.set_parameter("BP_ND", bp_nd, mask) + self.set_parameter("R1", r1, mask) self.set_parameter("k2", k2, mask) def fitted_tacs(self) -> TemporalMatrix | TemporalImage: @@ -82,11 +82,11 @@ def fitted_tacs(self) -> TemporalMatrix | TemporalImage: for i in trange(num_elements): idx = np.unravel_index(i, self.tacs.shape[:-1]) - bp = self.parameters["bp"][*idx] - r1 = self.parameters["r1"][*idx] + bp_nd = self.parameters["BP_ND"][*idx] + r1 = self.parameters["R1"][*idx] k2 = self.parameters["k2"][*idx] - if bp or r1 or k2: - fitted_tacs_dataobj[*idx, :] = srtm_model(self.reftac, bp, r1, k2) + if bp_nd or r1 or k2: + fitted_tacs_dataobj[*idx, :] = srtm_model(self.reftac, bp_nd, r1, k2) if isinstance(self.tacs, TemporalImage): img = image_maker(fitted_tacs_dataobj, self.tacs.img) @@ -100,13 +100,13 @@ def fitted_tacs(self) -> TemporalMatrix | TemporalImage: def srtm_model( - reftac: TemporalMatrix, bp: float, r1: float, k2: float + reftac: TemporalMatrix, bp_nd: float, r1: float, k2: float ) -> NumpyNumberArray: """SRTM model to generate a target TAC. Args: reftac: reference TAC - bp: binding potential + bp_nd: binding potential r1: relative radiotracer delivery parameter, R1 k2: k2 @@ -130,7 +130,7 @@ def srtm_model( t_upsampled, t, reftac.dataobj.astype("float").flatten() ) - k2a = k2 / (1 + bp) + k2a = k2 / (1 + bp_nd) conv_res_upsampled = ( convolve(reftac_upsampled, np.exp(-k2a * t_upsampled), mode="full")[ : len(t_upsampled) @@ -157,11 +157,11 @@ class SRTMZhou2003(KineticModel): def get_param_names(cls) -> list[str]: """Get names of kinetic model parameters.""" return [ - "dvr", - "r1", + "DVR", + "R1", "k2", "k2a", - "r1_lrsc", + "R1_lrsc", "k2_lrsc", "k2a_lrsc", "noise_var_eq_dvr", @@ -277,8 +277,8 @@ def fit( # noqa: max-complexity: 12 r1[k], k2[k], k2a[k] = b - self.set_parameter("dvr", dvr, mask) - self.set_parameter("r1", r1, mask) + self.set_parameter("DVR", dvr, mask) + self.set_parameter("R1", r1, mask) self.set_parameter("k2", k2, mask) self.set_parameter("k2a", k2a, mask) self.set_parameter("noise_var_eq_dvr", noise_var_eq_dvr, mask) @@ -315,7 +315,7 @@ def fit( # noqa: max-complexity: 12 r1_lrsc[k], k2_lrsc[k], k2a_lrsc[k] = b - self.set_parameter("r1_lrsc", r1_lrsc, mask) + self.set_parameter("R1_lrsc", r1_lrsc, mask) self.set_parameter("k2_lrsc", k2_lrsc, mask) self.set_parameter("k2a_lrsc", k2a_lrsc, mask) @@ -342,7 +342,7 @@ def prep_refine_r1( m = 3 smooth_r1_img: SpatialImage = smooth_image( - self.get_parameter("r1"), fwhm # type: ignore + self.get_parameter("R1"), fwhm # type: ignore ) smooth_k2_img: SpatialImage = smooth_image( self.get_parameter("k2"), fwhm # type: ignore @@ -352,7 +352,7 @@ def prep_refine_r1( ) noise_var_eq_r1_img: SpatialImage = self.get_parameter("noise_var_eq_r1") # type: ignore - r1_img: SpatialImage = self.get_parameter("r1") # type: ignore + r1_img: SpatialImage = self.get_parameter("R1") # type: ignore k2_img: SpatialImage = self.get_parameter("k2") # type: ignore k2a_img: SpatialImage = self.get_parameter("k2a") # type: ignore noise_var_eq_r1_data = noise_var_eq_r1_img.get_fdata() @@ -409,19 +409,19 @@ def fitted_tacs(self) -> TemporalMatrix | TemporalImage: num_elements = self.tacs.num_elements fitted_tacs_dataobj = np.empty_like(self.tacs.dataobj) - use_lrsc = "r1_lrsc" in self.parameters + use_lrsc = "R1_lrsc" in self.parameters for i in trange(num_elements): idx = np.unravel_index(i, self.tacs.shape[:-1]) - dvr = self.parameters["dvr"][*idx] - r1 = self.parameters["r1"][*idx] + dvr = self.parameters["DVR"][*idx] + r1 = self.parameters["R1"][*idx] k2 = self.parameters["k2"][*idx] if dvr or r1 or k2: - bp = dvr - 1 + bp_nd = dvr - 1 if use_lrsc: - r1 = self.parameters["r1_lrsc"][*idx] + r1 = self.parameters["R1_lrsc"][*idx] k2 = self.parameters["k2_lrsc"][*idx] - fitted_tacs_dataobj[*idx, :] = srtm_model(self.reftac, bp, r1, k2) + fitted_tacs_dataobj[*idx, :] = srtm_model(self.reftac, bp_nd, r1, k2) if isinstance(self.tacs, TemporalImage): img = image_maker(fitted_tacs_dataobj, self.tacs.img) diff --git a/src/dynamicpet/kineticmodel/suvr.py b/src/dynamicpet/kineticmodel/suvr.py index d3452c7..ea9fbcf 100644 --- a/src/dynamicpet/kineticmodel/suvr.py +++ b/src/dynamicpet/kineticmodel/suvr.py @@ -20,7 +20,7 @@ class SUVR(KineticModel): @classmethod def get_param_names(cls) -> list[str]: """Get names of kinetic model parameters.""" - return ["suvr"] + return ["SUVR"] def fit(self, mask: NumpyNumberArray | None = None) -> None: """Calculate SUVR. @@ -45,7 +45,7 @@ def fit(self, mask: NumpyNumberArray | None = None) -> None: frame_duration=frame_duration) >>> km = SUVR(reftac, tacs) >>> km.fit() - >>> km.get_parameter('suvr') + >>> km.get_parameter('SUVR') array([1.5, 3. ]) """ tacs: TemporalMatrix = self.tacs.timeseries_in_mask(mask) @@ -58,7 +58,7 @@ def fit(self, mask: NumpyNumberArray | None = None) -> None: ) suvr = numerator / denominator - self.set_parameter("suvr", suvr, mask) + self.set_parameter("SUVR", suvr, mask) def fitted_tacs(self) -> TemporalMatrix | TemporalImage: """Get fitted TACs based on estimated model parameters.""" diff --git a/tests/test_kineticmodel.py b/tests/test_kineticmodel.py index 1221f8d..f83e94d 100644 --- a/tests/test_kineticmodel.py +++ b/tests/test_kineticmodel.py @@ -75,7 +75,7 @@ def test_suvr_tm(reftac: TemporalMatrix, tacs: TemporalMatrix) -> None: """Test SUVR calculation using TemporalMatrix.""" km = SUVR(reftac, tacs) km.fit() - suvr: NumpyRealNumberArray = km.get_parameter("suvr") # type: ignore + suvr: NumpyRealNumberArray = km.get_parameter("SUVR") # type: ignore assert suvr.shape == (2,) assert np.all(suvr == [2, 3]) @@ -87,7 +87,7 @@ def test_suvr_ti(reftac: TemporalMatrix, tacs_img: TemporalImage) -> None: fitted_tacs = km.fitted_tacs() - suvr_img: SpatialImage = km.get_parameter("suvr") # type: ignore + suvr_img: SpatialImage = km.get_parameter("SUVR") # type: ignore assert suvr_img.shape == (1, 1, 2) assert np.all(suvr_img.get_fdata() == np.array([[[2, 3]]])) assert np.all(fitted_tacs.dataobj == tacs_img.dataobj) @@ -97,13 +97,13 @@ def test_srtm_zhou2003_ti(reftac: TemporalMatrix, tacs_img: TemporalImage) -> No """Test SRTM Zhou 2003 using TemporalImage.""" km = SRTMZhou2003(reftac, tacs_img) km.fit(integration_type="trapz") - dvr_img: SpatialImage = km.get_parameter("dvr") # type: ignore + dvr_img: SpatialImage = km.get_parameter("DVR") # type: ignore assert dvr_img.shape == (1, 1, 2) - bp_img: SpatialImage = km.get_parameter("bp") # type: ignore + bp_nd_img: SpatialImage = km.get_parameter("BP_ND") # type: ignore - assert np.allclose(dvr_img.get_fdata(), bp_img.get_fdata() + 1) + assert np.allclose(dvr_img.get_fdata(), bp_nd_img.get_fdata() + 1) def test_logan_tm(reftac: TemporalMatrix, tacs_img: TemporalImage) -> None: @@ -111,5 +111,5 @@ def test_logan_tm(reftac: TemporalMatrix, tacs_img: TemporalImage) -> None: km = LRTM(reftac, tacs_img) km.fit(integration_type="trapz") - dvr_img: SpatialImage = km.get_parameter("dvr") # type: ignore + dvr_img: SpatialImage = km.get_parameter("DVR") # type: ignore assert dvr_img.shape == (1, 1, 2) diff --git a/tests/test_kineticmodel_simulated.py b/tests/test_kineticmodel_simulated.py index 75cb1ef..75070b8 100644 --- a/tests/test_kineticmodel_simulated.py +++ b/tests/test_kineticmodel_simulated.py @@ -108,36 +108,36 @@ def frame_duration() -> NDArray[np.double]: def srtm_gen_ode_model( y: NumpyRealNumberArray, t: NumpyRealNumberArray, - bp_val: NumpyRealNumberArray, + bp_nd_val: NumpyRealNumberArray, r1_val: NumpyRealNumberArray, plasma_rate: float = -0.03, - kref1: float = 1.0, - kref2: float = 0.3, + k1prime: float = 1.0, + k2prime: float = 0.3, ) -> list[Any]: """Generative ODE model describing SRTM. Args: y: concentrations t: time - bp_val: binding potential + bp_nd_val: binding potential r1_val: relative radiotracer delivery plasma_rate: plasma rate - kref1: k1 for reference region - kref2: k2 for reference region + k1prime: k1 for reference region + k2prime: k2 for reference region Returns: time derivatives of concentrations """ - k1 = r1_val * kref1 + k1 = r1_val * k1prime - dvr = 1 + bp_val - k2a = kref2 * r1_val / dvr + dvr = 1 + bp_nd_val + k2a = k2prime * r1_val / dvr cp, ct, cr = y dcpdt = plasma_rate * cp dctdt = k1 * cp - k2a * ct - dcrdt = kref1 * cp - kref2 * cr + dcrdt = k1prime * cp - k2prime * cr dydt = [dcpdt, dctdt, dcrdt] @@ -147,7 +147,7 @@ def srtm_gen_ode_model( def get_tacs_and_reftac_dataobj( frame_start: NDArray[np.double], frame_duration: NDArray[np.double], - bp: float, + bp_nd: float, r1: float, ) -> tuple[NDArray[np.double], NDArray[np.double]]: """Get digitized TACs.""" @@ -160,7 +160,7 @@ def get_tacs_and_reftac_dataobj( num_ode_pts = 500 t_ode = np.linspace(frame_start[0], frame_end[-1], num_ode_pts) - y = odeint(srtm_gen_ode_model, y0, t_ode, args=(bp, r1)) + y = odeint(srtm_gen_ode_model, y0, t_ode, args=(bp_nd, r1)) # Digitize this curve cref = np.zeros(len(frame_start)) @@ -178,10 +178,10 @@ def test_srtmzhou2003_tm( frame_duration: NDArray[np.double], ) -> None: """Test SRTM Zhou 2003 calculation using TemporalMatrix.""" - bp_true = 1.5 + bp_nd_true = 1.5 r1_true = 1.2 ct, cref = get_tacs_and_reftac_dataobj( - frame_start, frame_duration, bp_true, r1_true + frame_start, frame_duration, bp_nd_true, r1_true ) tac = TemporalMatrix(ct, frame_start, frame_duration) @@ -194,11 +194,11 @@ def test_srtmzhou2003_tm( fitted_tacs = km.fitted_tacs() - bp: NumpyRealNumberArray = km.get_parameter("bp") # type: ignore - r1: NumpyRealNumberArray = km.get_parameter("r1") # type: ignore + bp_nd: NumpyRealNumberArray = km.get_parameter("BP_ND") # type: ignore + r1: NumpyRealNumberArray = km.get_parameter("R1") # type: ignore relative_tol = 0.007 # .007 means that 0.7% error is tolerated - assert np.allclose(bp, bp_true, rtol=relative_tol) + assert np.allclose(bp_nd, bp_nd_true, rtol=relative_tol) assert np.allclose(r1, r1_true, rtol=relative_tol) assert np.allclose(fitted_tacs.dataobj, tac.dataobj, rtol=0.01) @@ -208,10 +208,10 @@ def test_srtmzhou2003_ti( frame_duration: NDArray[np.double], ) -> None: """Test SRTM Zhou 2003 calculation using TemporalImage.""" - bp_true = 1.5 + bp_nd_true = 1.5 r1_true = 1.2 ct, cref = get_tacs_and_reftac_dataobj( - frame_start, frame_duration, bp_true, r1_true + frame_start, frame_duration, bp_nd_true, r1_true ) dims = (10, 11, 12, len(frame_start)) @@ -232,12 +232,12 @@ def test_srtmzhou2003_ti( fitted_tacs = km.fitted_tacs() - bp: SpatialImage = km.get_parameter("bp") # type: ignore - r1: SpatialImage = km.get_parameter("r1") # type: ignore - r1_lrsc: SpatialImage = km.get_parameter("r1_lrsc") # type: ignore + bp_nd: SpatialImage = km.get_parameter("BP_ND") # type: ignore + r1: SpatialImage = km.get_parameter("R1") # type: ignore + r1_lrsc: SpatialImage = km.get_parameter("R1_lrsc") # type: ignore relative_tol = 0.007 # .007 means that 0.7% error is tolerated - assert np.allclose(bp.get_fdata(), bp_true, rtol=relative_tol) + assert np.allclose(bp_nd.get_fdata(), bp_nd_true, rtol=relative_tol) assert np.allclose(r1.get_fdata(), r1_true, rtol=relative_tol) assert np.allclose(r1_lrsc.get_fdata(), r1_true, rtol=relative_tol) assert np.allclose(fitted_tacs.dataobj, tac.dataobj, rtol=0.01) @@ -248,10 +248,10 @@ def test_srtmlammertsma1996_tm( frame_duration: NDArray[np.double], ) -> None: """Test SRTM Lammertsma 1996 calculation using TemporalMatrix.""" - bp_true = 1.5 + bp_nd_true = 1.5 r1_true = 1.2 ct, cref = get_tacs_and_reftac_dataobj( - frame_start, frame_duration, bp_true, r1_true + frame_start, frame_duration, bp_nd_true, r1_true ) tac = TemporalMatrix(ct, frame_start, frame_duration) @@ -262,11 +262,11 @@ def test_srtmlammertsma1996_tm( fitted_tacs = km.fitted_tacs() - bp: NumpyRealNumberArray = km.get_parameter("bp") # type: ignore - r1: NumpyRealNumberArray = km.get_parameter("r1") # type: ignore + bp_nd: NumpyRealNumberArray = km.get_parameter("BP_ND") # type: ignore + r1: NumpyRealNumberArray = km.get_parameter("R1") # type: ignore relative_tol = 0.02 # .02 means that 2% error is tolerated - assert np.allclose(bp, bp_true, rtol=relative_tol) + assert np.allclose(bp_nd, bp_nd_true, rtol=relative_tol) assert np.allclose(r1, r1_true, rtol=relative_tol) assert np.allclose(fitted_tacs.dataobj, tac.dataobj, rtol=relative_tol) @@ -276,10 +276,10 @@ def test_lrtm_tm( frame_duration: NDArray[np.double], ) -> None: """Test Logan 1996 calculation using TemporalMatrix.""" - bp_true = 1.5 + bp_nd_true = 1.5 r1_true = 1.2 ct, cref = get_tacs_and_reftac_dataobj( - frame_start, frame_duration, bp_true, r1_true + frame_start, frame_duration, bp_nd_true, r1_true ) tac = TemporalMatrix(ct, frame_start, frame_duration) @@ -290,7 +290,7 @@ def test_lrtm_tm( # to fit to obtain the best possible recovery of true values km.fit(integration_type="trapz") - bp: NumpyRealNumberArray = km.get_parameter("bp") # type: ignore + bp_nd: NumpyRealNumberArray = km.get_parameter("BP_ND") # type: ignore relative_tol = 0.02 # .02 means that 2% error is tolerated - assert np.allclose(bp, bp_true, rtol=relative_tol) + assert np.allclose(bp_nd, bp_nd_true, rtol=relative_tol) From d3d58c1da7caca3450e6b6391e835d2f6ce8a774 Mon Sep 17 00:00:00 2001 From: Murat Bilgel Date: Fri, 27 Sep 2024 19:24:00 -0400 Subject: [PATCH 2/5] update README: add NESMA --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c58a9db..73eab01 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Dynamic PET +# Dynamic PET Dynamic PET logo