Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected behavior of transformList #267

Open
timmocking opened this issue Feb 5, 2024 · 4 comments
Open

Unexpected behavior of transformList #267

timmocking opened this issue Feb 5, 2024 · 4 comments

Comments

@timmocking
Copy link

I want to create a transformList object by supplying a custom "w" parameter for every channel in a loop. However, when I create a list of transformations, the w-parameter is retro-actively modified for earlier items in the list.

transforms <- list()
for (i in 1:8){
  transforms[[LETTERS[i]]] <- flowCore::logicleTransform(w = i)
}
# Print the width of "A" (should be 1)
print(as.list(environment(transforms[1]$A@.Data))$w) # output: [1] 8

For some reason, this can be prevented by calling summary() on the list if flowCore is loaded. However, the list becomes a closure type if flowCore is not loaded...

library(flowCore)
transforms <- list()
for (i in 1:8){
  transforms[[LETTERS[i]]] <- flowCore::logicleTransform(w = i)
  summary(transforms[[LETTERS[i]]])
}
# Print the width of "A" (should be 1)
print(as.list(environment(transforms[1]$A@.Data))$w) # output: [1] 1

Is this the expected behavior?

R version 4.3.1 (2023-06-16)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 20.04.6 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0 
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C               LC_TIME=en_GB.UTF-8        LC_COLLATE=en_US.UTF-8     LC_MONETARY=en_GB.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_GB.UTF-8       LC_NAME=C                  LC_ADDRESS=C               LC_TELEPHONE=C             LC_MEASUREMENT=en_GB.UTF-8 LC_IDENTIFICATION=C       

time zone: Europe/Amsterdam
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] compiler_4.3.1      RProtoBufLib_2.12.1 cytolib_2.12.1      tools_4.3.1         rstudioapi_0.15.0   Biobase_2.60.0      S4Vectors_0.38.2    flowCore_2.12.2     BiocGenerics_0.46.0
[10] matrixStats_1.2.0   stats4_4.3.1      
@SamGG
Copy link
Contributor

SamGG commented Feb 5, 2024

There is maybe a better solution... @mikejiang ?

library(flowCore)
#> Warning: package 'flowCore' was built under R version 4.3.2
transforms <- list()
for (i in 1:8){
  transforms[[LETTERS[i]]] <- logicleTransform(w = i)
}
# Print the width of "A" (should be 1)
environment(transforms$A@.Data)$w # output: [1] 8
#> [1] 8
environment(transforms$H@.Data)$w # output: [1] 8
#> [1] 8
# each transform/function has its own environment
sapply(transforms, environment)
#> $A
#> <environment: 0x000001f0342bb308>
#> 
#> $B
#> <environment: 0x000001f0341d58f8>
#> 
#> $C
#> <environment: 0x000001f0341cac30>
#> 
#> $D
#> <environment: 0x000001f0340f9820>
#> 
#> $E
#> <environment: 0x000001f033db1740>
#> 
#> $F
#> <environment: 0x000001f033c91230>
#> 
#> $G
#> <environment: 0x000001f033c62d48>
#> 
#> $H
#> <environment: 0x000001f0336e89a0>
# 
# so we can assign a value in each environment and check
for (i in 1:8) {
  assign("w", i, envir = environment(transforms[[i]]))
}
for (i in 1:8) {
  print(get("w", envir = environment(transforms[[i]])))
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
#> [1] 6
#> [1] 7
#> [1] 8
environment(transforms$A@.Data)$w
#> [1] 1
environment(transforms$H@.Data)$w
#> [1] 8

Created on 2024-02-05 with reprex v2.1.0

timmocking added a commit to AUMC-HEMA/CytoTools that referenced this issue Feb 7, 2024
@SamGG
Copy link
Contributor

SamGG commented Jan 25, 2025

Hi guys,

I worked on this. Here are two functions that allows getting and setting parameters of the function performing the intensity transformation. This should work for any transformation as parameter names are not specified.

Let me know if the code is OK and if you will incorporate it flowCore.

#' Get the parameters of the function called by the transformation
#'
#' @param trans a transformation list (such as the value returned by
#'   transformList or estimateLogicle functions) or a transform map (such as the
#'   value returned by arcsinhTransform or logicleTransform functions).
#' @returns a list of parameters  of the function called by each  transformation
#'   of trans.
#' @export
transform_getParams <- function(trans) {
  getParams <- function(transMap) {
    func <- transMap@f
    env_func <- environment(func)
    vars <- ls(env_func)
    vals <- sapply(vars, function(v) get(v, envir = env_func))
    return(vals)
  }
  if (inherits(trans, "transformMap"))
    return(getParams(trans))
  if (inherits(trans, "transformList"))
    return(lapply(trans@transforms, getParams))
  stop("Not implemented for an object of class ", class(trans))
}

#' Set the parameters of the function called by the transformation
#'
#' @param trans a transformation list (such as the value returned by
#'   transformList or estimateLogicle functions) or a transform map (such as the
#'   value returned by arcsinhTransform or logicleTransform functions).
#' @param params a list of parameters as returned by transfom_getParams
#'   function.
#' @returns a transformation list or a single transformation map according to
#'   trans.
#' @export
transform_setParams <- function(trans, params) {
  setParams <- function(transMap, params_map) {
    func <- transMap@f
    env_func <- environment(func)
    vars <- ls(env_func)
    varp <- names(params_map)
    comm <- intersect(vars, varp)
    if (length(comm)) {
      # make a copy of the environment to avoid overwriting
      env_copy <- as.environment(as.list(env_func, all.names=TRUE))
      # update the copy
      for (v in comm) {
        assign(v, unname(params_map[v]), envir = env_copy)
      }
      # assign the copy to the function
      environment(transMap@f) <- env_copy
    }
    return(transMap)
  }
  if (inherits(trans, "transformMap")) {
    # TODO: check params match trans
    return(setParams(trans, params))
  }
  if (inherits(trans, "transformList")) {
    # TODO: check params match trans
    trans_maps <- trans@transforms
    trans_mrks <- names(trans_maps)
    params_mrks <- names(params)
    comm_mrks <- intersect(trans_mrks, params_mrks)
    if (length(comm_mrks)) {
      for (mrk in comm_mrks) {
        trans_maps[[mrk]] <- setParams(trans_maps[[mrk]], params[[mrk]])
      }
    }
    trans@transforms <- trans_maps
    return(trans)
  }
  stop("Not implemented for an object of class ", class(trans))
}

The following tests checks that the original environments are not changed when modifying parameters.

library(flowCore)

data(GvHD)
samp <- GvHD[[1]]

## User defined logicle function
lgcl <- logicleTransform( w = 0.5, t= 10000, m =4.5)
trans <- transformList(c("FL1-H", "FL2-H"), lgcl)

## Automatically estimate the logicle transformation based on the data
lgcl <- estimateLogicle(samp, channels = c("FL1-H", "FL2-H", "FL3-H", "FL2-A", "FL4-H"))
(res = transform_getParams(lgcl))
#> $`FL1-H`
#> $`FL1-H`$a
#> [1] 0
#> 
#> $`FL1-H`$k
#> transform object 'FL1-H_logicleTransform'
#> 
#> $`FL1-H`$m
#> [1] 4.5
#> 
#> $`FL1-H`$t
#> [1] 10000
#> 
#> $`FL1-H`$transformationId
#> [1] "FL1-H_logicleTransform"
#> 
#> $`FL1-H`$w
#> [1] 0
#> 
#> 
#> $`FL2-H`
#> $`FL2-H`$a
#> [1] 0
#> 
#> $`FL2-H`$k
#> transform object 'FL2-H_logicleTransform'
#> 
#> $`FL2-H`$m
#> [1] 4.5
#> 
#> $`FL2-H`$t
#> [1] 10000
#> 
#> $`FL2-H`$transformationId
#> [1] "FL2-H_logicleTransform"
#> 
#> $`FL2-H`$w
#> [1] 0
#> 
#> 
#> $`FL3-H`
#> $`FL3-H`$a
#> [1] 0
#> 
#> $`FL3-H`$k
#> transform object 'FL3-H_logicleTransform'
#> 
#> $`FL3-H`$m
#> [1] 4.5
#> 
#> $`FL3-H`$t
#> [1] 10000
#> 
#> $`FL3-H`$transformationId
#> [1] "FL3-H_logicleTransform"
#> 
#> $`FL3-H`$w
#> [1] 0
#> 
#> 
#> $`FL2-A`
#> $`FL2-A`$a
#> [1] 0
#> 
#> $`FL2-A`$k
#> transform object 'FL2-A_logicleTransform'
#> 
#> $`FL2-A`$m
#> [1] 4.5
#> 
#> $`FL2-A`$t
#> [1] 1023
#> 
#> $`FL2-A`$transformationId
#> [1] "FL2-A_logicleTransform"
#> 
#> $`FL2-A`$w
#> [1] 0
#> 
#> 
#> $`FL4-H`
#> $`FL4-H`$a
#> [1] 0
#> 
#> $`FL4-H`$k
#> transform object 'FL4-H_logicleTransform'
#> 
#> $`FL4-H`$m
#> [1] 4.5
#> 
#> $`FL4-H`$t
#> [1] 10000
#> 
#> $`FL4-H`$transformationId
#> [1] "FL4-H_logicleTransform"
#> 
#> $`FL4-H`$w
#> [1] 0

# set params of a transformation Map (ie one marker)
class(res)
#> [1] "list"
res[[1]]$m
#> [1] 4.5
res[[1]]$m = 6.5  # update the list
transform_getParams(lgcl)[[1]]$m  # transform is unchanged
#> [1] 4.5
res2 = transform_setParams(lgcl@transforms[[1]], res[[1]])
transform_getParams(res2)$m
#> [1] 6.5
transform_getParams(lgcl)[[1]]$m
#> [1] 4.5

# set params of a transformation List
res[[1]]$m = 7.5  # update into the list
res3 = transform_setParams(lgcl, res)
transform_getParams(res3)[[1]]$m
#> [1] 7.5
transform_getParams(res2)$m
#> [1] 6.5
transform_getParams(lgcl)[[1]]$m
#> [1] 4.5

Created on 2025-01-25 with reprex v2.1.1

@djhammill
Copy link

djhammill commented Jan 26, 2025

This is just a scoping issue - switch to using lapply and everything will work as expected.

trans <- lapply(
  1:8,
  function(z) {
    flowCore::logicleTransform(w = z)
  }
)
print(as.list(environment(trans[[1]]@.Data))$w) 
[1]  1

@SamGG
Copy link
Contributor

SamGG commented Jan 26, 2025

Hi Dillon.

Good point. lapply() is achieving the trick and answers Tim's question. The major point in the lapply() code is that the transformation is created within an anonymous function of z, whose environment is reduced to z only. That's the trick.

Probably, I was not clear about the goal of my proposal. I want to retrieve the parameters of any transformList in order to print them and to change them. The test shows that the parameters are changed in the transfromList returned by transform_setParams(), without modifying the values of the given/input transformList. Because parameters are in the environment of the transformation function, changing their values does not create a copy but directly change the values in place, which is not the behaviur that I want.

Best.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants