11"""Issue #1016: heritability of palbociclib escape in Spencer-lab MCF10A lineages.
22
33Run as ``OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 python -m lineage.palbociclib_analysis
4- [n_boot]``. The fits run in parallel worker processes, so BLAS's own threads should be
4+ [n_boot | sensitivity ]``. The fits run in parallel worker processes, so BLAS's own threads should be
55pinned to one, or the workers oversubscribe the machine. Results are written to
66``output/palbociclib_analysis.json`` and drawn by :mod:`lineage.figures.figure22`.
77
@@ -86,6 +86,10 @@ def percentile_ci(samples: np.ndarray) -> list:
8686 return [np .asarray (lo ).tolist (), np .asarray (hi ).tolist ()]
8787
8888
89+ def log (msg : str , t0 : float ):
90+ print (f"[{ time .time () - t0 :7.0f} s] { msg } " , file = sys .stderr , flush = True )
91+
92+
8993def run (n_boot : int = 200 , n_workers : int = 16 , seed : int = 0 ) -> dict :
9094 rng = np .random .default_rng (seed )
9195 pops = [load_lineages (c ) for c in CONDITIONS ]
@@ -102,8 +106,10 @@ def run(n_boot: int = 200, n_workers: int = 16, seed: int = 0) -> dict:
102106
103107 t0 = time .time ()
104108 res ["state_selection" ] = select_states (pops , n_jobs = n_workers , rng = rng )
109+ log ("state selection done" , t0 )
105110
106111 sweep = dose_sweep (pops , list (DOSES_NM ), num_states = 2 , n_starts = 12 , n_jobs = n_workers , rng = rng )
112+ log ("dose sweep done" , t0 )
107113 res ["LL" ] = {k : float (v ) for k , v in sweep .LL .items ()}
108114 res ["lrt" ] = sweep .lrt
109115 res ["emissions" ] = [e .params .tolist () for e in sweep .per_dose [0 ].estimate .E ]
@@ -121,6 +127,7 @@ def run(n_boot: int = 200, n_workers: int = 16, seed: int = 0) -> dict:
121127 with ProcessPoolExecutor (n_workers ) as exe :
122128 futs = [exe .submit (bootstrap_transitions , sweep , pops , len (c ), s ) for c , s in zip (chunks , seeds , strict = True )]
123129 boots = np .concatenate ([f .result () for f in futs if f .result ().size ])
130+ log ("bootstrap done" , t0 )
124131 summaries = [summarize_T (b ) for b in boots ]
125132 boot : dict = {
126133 key : percentile_ci (np .array ([s [key ] for s in summaries ], dtype = float ))
@@ -142,6 +149,7 @@ def run(n_boot: int = 200, n_workers: int = 16, seed: int = 0) -> dict:
142149 res ["n_boot" ] = int (boots .shape [0 ])
143150
144151 cv = cross_validated_scores (sweep , pops , horizon = HORIZON_H , n_folds = 5 , escape_col = ESCAPE_COL , rng = rng )
152+ log ("cross-validated forecasts done" , t0 )
145153 res ["auc" ] = auc_table (cv , n_boot = 1000 , rng = rng )
146154 pairs = cv ["pairs" ]
147155 res ["pairs" ] = {
@@ -159,10 +167,63 @@ def run(n_boot: int = 200, n_workers: int = 16, seed: int = 0) -> dict:
159167 return res
160168
161169
170+ SENSITIVITY_OUTPUT = os .path .join ("output" , "palbociclib_sensitivity.json" )
171+
172+
173+ def sensitivity (n_workers : int = 16 , seed : int = 1 ) -> dict :
174+ """Refit the dose sweep without the roots' lifetimes, which are selected on dividing.
175+
176+ Also reports how well each fit reproduces division among cells born into drug, the
177+ cells not selected on their outcome: the observed fraction divided by 20 h (Kaplan-Meier)
178+ against the fraction the fitted states predict.
179+ """
180+ from statsmodels .duration .survfunc import SurvfuncRight
181+
182+ from .BaumWelch import do_E_step
183+ from .tHMM import tHMM
184+
185+ rng = np .random .default_rng (seed )
186+ out : dict = {}
187+ for label , keep in (("with_root_lifetimes" , True ), ("without_root_lifetimes" , False )):
188+ pops = [load_lineages (c , root_lifetimes = keep ) for c in CONDITIONS ]
189+ sweep = dose_sweep (pops , list (DOSES_NM ), num_states = 2 , n_starts = 12 , n_jobs = n_workers , rng = rng )
190+ E = sweep .per_dose [0 ].estimate .E
191+ entry = {
192+ "LL" : {k : float (v ) for k , v in sweep .LL .items ()},
193+ "lrt" : sweep .lrt ,
194+ "emissions" : [e .params .tolist () for e in E ],
195+ "pi" : [tO .estimate .pi .tolist () for tO in sweep .per_dose ],
196+ "point" : summarize_T (sweep .T ),
197+ "divided_by_20h" : [],
198+ }
199+ # Always score division on the full data, whichever lineages were fit.
200+ full = [load_lineages (c ) for c in CONDITIONS ]
201+ for pop , tO in zip (full , sweep .per_dose , strict = True ):
202+ fixed = tHMM (pop , 2 , fpi = tO .estimate .pi , fT = tO .estimate .T , fE = E )
203+ gam = np .vstack (do_E_step (fixed )[3 ])
204+ obs = np .vstack ([lin .obs for lin in pop ])
205+ m = np .isfinite (obs [:, 1 ]) & np .concatenate ([np .arange (len (lin )) > 0 for lin in pop ])
206+ km = SurvfuncRight (obs [m , 1 ], obs [m , 2 ])
207+ w = gam [m ].mean (axis = 0 )
208+ S20 = sum (w [k ] * np .exp (- ((20.0 / e .params [3 ]) ** e .params [2 ])) for k , e in enumerate (E ))
209+ entry ["divided_by_20h" ].append (
210+ {"observed" : float (1.0 - np .interp (20.0 , km .surv_times , km .surv_prob )), "model" : float (1.0 - S20 )}
211+ )
212+ out [label ] = entry
213+ return out
214+
215+
162216if __name__ == "__main__" :
163- n_boot = int (sys .argv [1 ]) if len (sys .argv ) > 1 else 200
164- out = run (n_boot = n_boot )
165- os .makedirs (os .path .dirname (OUTPUT ), exist_ok = True )
166- with open (OUTPUT , "w" ) as f :
167- json .dump (out , f , indent = 1 , default = float )
168- print (json .dumps ({k : v for k , v in out .items () if not k .startswith ("roc_" )}, indent = 1 , default = float ))
217+ if len (sys .argv ) > 1 and sys .argv [1 ] == "sensitivity" :
218+ out = sensitivity ()
219+ os .makedirs (os .path .dirname (SENSITIVITY_OUTPUT ), exist_ok = True )
220+ with open (SENSITIVITY_OUTPUT , "w" ) as f :
221+ json .dump (out , f , indent = 1 , default = float )
222+ print (json .dumps (out , indent = 1 , default = float ))
223+ else :
224+ n_boot = int (sys .argv [1 ]) if len (sys .argv ) > 1 else 200
225+ out = run (n_boot = n_boot )
226+ os .makedirs (os .path .dirname (OUTPUT ), exist_ok = True )
227+ with open (OUTPUT , "w" ) as f :
228+ json .dump (out , f , indent = 1 , default = float )
229+ print (json .dumps ({k : v for k , v in out .items () if not k .startswith ("roc_" )}, indent = 1 , default = float ))
0 commit comments