adesomics.population.population_model

  1import numpy as np
  2import pandas as pd
  3
  4from adesomics.io.metadata import Metadata
  5from adesomics.plotting.style import annotate_study, group_colors, short_labels
  6import matplotlib.pyplot as plt
  7from adesomics.plotting.style import group_colors, annotate_study, GREY, INK
  8
  9from pathlib import Path
 10
 11def _save(fig, filename: str, outdir: str = "results") -> Path:
 12    out = Path(outdir)
 13    out.mkdir(parents=True, exist_ok=True)      
 14    path = (out / filename).resolve()           
 15    fig.savefig(path, dpi=200, bbox_inches="tight")
 16    print(f"Saved plot to {path}")              
 17    return path
 18
 19class Population:
 20    """Exponential growth of one sample, driven by its FBA growth rate.
 21
 22    Parameters
 23    ----------
 24    growth_rate
 25        Specific growth rate mu (1/h). If omitted, it is taken from the
 26        model's FBA objective on first access.
 27    """
 28
 29    def __init__(self, model=None, n0: float = 1.0,
 30                 growth_rate: float | None = None, label: str | None = None):
 31        if model is None and growth_rate is None:
 32            raise ValueError("Need either a model or an explicit growth_rate")
 33        self.model = model
 34        self.n0 = n0
 35        self._growth_rate = growth_rate
 36        self.label = label
 37
 38    @property
 39    def growth_rate(self) -> float:
 40        if self._growth_rate is None:
 41            self._growth_rate = self.model.optimize().objective_value
 42        return self._growth_rate
 43
 44    @property
 45    def doubling_time(self) -> float:
 46        """Doubling time in hours; inf for a non-growing population."""
 47        mu = self.growth_rate
 48        return np.log(2) / mu if mu > 0 else np.inf
 49
 50    def simulate(self, t_max: float, n_points: int = 100):
 51        t = np.linspace(0, t_max, n_points)
 52        return t, self.n0 * np.exp(self.growth_rate * t)
 53
 54    def plot_growth_curve(self, t_max: float, n_points: int = 100,
 55                          ax=None, **kwargs):
 56        import matplotlib.pyplot as plt
 57
 58        t, n = self.simulate(t_max, n_points)
 59        ax = ax or plt.subplots(figsize=(7, 5))[1]
 60        kwargs.setdefault("label", self.label or f"mu = {self.growth_rate:.3f} 1/h")
 61        ax.plot(t, n, **kwargs)
 62        ax.set(xlabel="Time (h)", ylabel="Population size")
 63        return ax
 64
 65    def __repr__(self):
 66        return f"<Population {self.label!r}: mu={self.growth_rate:.3f} 1/h>"
 67
 68
 69class PopulationSet:
 70    """One Population per sample, tied to the experiment's Metadata.
 71
 72    Colours and labels come from adesomics.plotting.style, so growth curves
 73    match the DE plots of the same experiment.
 74    """
 75
 76    def __init__(self, populations: dict[str, Population], metadata: Metadata):
 77        if metadata is not None:
 78            missing = set(populations) - set(metadata.samples)
 79            if missing:
 80                raise ValueError(f"Not in metadata: {sorted(missing)}")
 81
 82            self.metadata = metadata.subset_samples(list(populations))
 83            self.populations = {s: populations[s] for s in self.metadata.samples}
 84        else:
 85            print("Populationmodel currently only works with linked metadata as input.")
 86            raise SystemExit(1) 
 87            
 88    # --- construction ----------------------------------------------------
 89
 90    @classmethod
 91    def from_connector(cls, connector, metadata: Metadata,
 92                       samples: list[str] | None = None, n0: float = 1.0):
 93        """Build one Population per sample from E-Flux-constrained solutions."""
 94        samples = samples if samples is not None else list(connector.tpm.columns)
 95        solutions = connector.compare(samples)
 96
 97        pops = {
 98            s: Population(n0=n0,
 99                          growth_rate=solutions[s].objective_value,
100                          label=s)
101            for s in samples
102        }
103        return cls(pops, metadata)
104
105    # --- access ----------------------------------------------------------
106
107    @property
108    def growth_rates(self) -> pd.Series:
109        return pd.Series({s: p.growth_rate for s, p in self.populations.items()},
110                         name="mu")
111
112    def summary(self) -> pd.DataFrame:
113        """Growth rate and doubling time per sample, with its condition."""
114        df = pd.DataFrame({
115            "condition": self.metadata.groups,
116            "mu": self.growth_rates,
117            "doubling_time_h": {s: p.doubling_time
118                                for s, p in self.populations.items()},
119        })
120        return df.loc[self.metadata.samples]
121
122    def by_group(self) -> pd.DataFrame:
123        """Mean and spread of mu per condition (replicates collapsed)."""
124        return (self.summary()
125                .groupby("condition")["mu"]
126                .agg(["mean", "std", "count"]))
127
128    # --- plotting --------------------------------------------------------
129
130    def plot_growth_curves(self, t_max: float, n_points: int = 100,
131                           ax=None, by_group: bool = True):
132        ax = ax or plt.subplots(figsize=(7, 5))[1]
133        lut = group_colors(self.metadata)
134        seen = set()
135
136        for sample in self.metadata.samples:
137            group = self.metadata.groups[sample]
138            if by_group:
139                label = group if group not in seen else None
140                seen.add(group)
141            else:
142                label = sample
143
144            self.populations[sample].plot_growth_curve(
145                t_max, n_points, ax=ax, color=lut[group], label=label,
146            )
147
148        ax.set(title="FBA-predicted exponential growth")
149        ax.legend(frameon=False, fontsize=7)
150        annotate_study(ax, self.metadata)
151        _save(ax.figure, "growth_curves.png", "results")
152        return ax
153
154    def plot_growth_rates(self, ax=None, save=None):
155        summ = self.summary()
156        by = self.by_group()
157        lut = group_colors(self.metadata)
158        order = list(by.index)
159
160        ax = ax or plt.subplots(figsize=(8, 5))[1]
161        label_x = summ["mu"].max() * 1.05
162        for i, group in enumerate(order):
163            members = summ.index[summ["condition"] == group]
164            ax.barh(i, by.loc[group, "mean"], color=lut[group], alpha=0.85,
165                    height=0.62, zorder=2)
166            ax.scatter(summ.loc[members, "mu"], [i] * len(members),
167                    color=INK, s=30, zorder=3)
168            td = np.log(2) / by.loc[group, "mean"]
169            ax.text(label_x, i, f"t$_d$ = {td:.1f} h",
170                va="center", fontsize=10, color=GREY)
171
172        ax.set_yticks(range(len(order)))
173        ax.set_yticklabels([g.replace("_", " ") for g in order])
174        ax.set(xlabel="specific growth rate  $\\mu$  (1/h)",
175            title="Growth rate by condition")
176        ax.margins(x=0.22)
177        ax.grid(axis="y", visible=False)
178        annotate_study(ax, self.metadata)
179        _save(ax.figure, "Bar_GrowthRates.png", "results")
180        return ax
181
182    def __len__(self):
183        return len(self.populations)
184
185    def __repr__(self):
186        return (f"<PopulationSet: {len(self)} samples, "
187                f"{self.metadata.groups.nunique()} conditions>")
class Population:
20class Population:
21    """Exponential growth of one sample, driven by its FBA growth rate.
22
23    Parameters
24    ----------
25    growth_rate
26        Specific growth rate mu (1/h). If omitted, it is taken from the
27        model's FBA objective on first access.
28    """
29
30    def __init__(self, model=None, n0: float = 1.0,
31                 growth_rate: float | None = None, label: str | None = None):
32        if model is None and growth_rate is None:
33            raise ValueError("Need either a model or an explicit growth_rate")
34        self.model = model
35        self.n0 = n0
36        self._growth_rate = growth_rate
37        self.label = label
38
39    @property
40    def growth_rate(self) -> float:
41        if self._growth_rate is None:
42            self._growth_rate = self.model.optimize().objective_value
43        return self._growth_rate
44
45    @property
46    def doubling_time(self) -> float:
47        """Doubling time in hours; inf for a non-growing population."""
48        mu = self.growth_rate
49        return np.log(2) / mu if mu > 0 else np.inf
50
51    def simulate(self, t_max: float, n_points: int = 100):
52        t = np.linspace(0, t_max, n_points)
53        return t, self.n0 * np.exp(self.growth_rate * t)
54
55    def plot_growth_curve(self, t_max: float, n_points: int = 100,
56                          ax=None, **kwargs):
57        import matplotlib.pyplot as plt
58
59        t, n = self.simulate(t_max, n_points)
60        ax = ax or plt.subplots(figsize=(7, 5))[1]
61        kwargs.setdefault("label", self.label or f"mu = {self.growth_rate:.3f} 1/h")
62        ax.plot(t, n, **kwargs)
63        ax.set(xlabel="Time (h)", ylabel="Population size")
64        return ax
65
66    def __repr__(self):
67        return f"<Population {self.label!r}: mu={self.growth_rate:.3f} 1/h>"

Exponential growth of one sample, driven by its FBA growth rate.

Parameters

growth_rate Specific growth rate mu (1/h). If omitted, it is taken from the model's FBA objective on first access.

Population( model=None, n0: float = 1.0, growth_rate: float | None = None, label: str | None = None)
30    def __init__(self, model=None, n0: float = 1.0,
31                 growth_rate: float | None = None, label: str | None = None):
32        if model is None and growth_rate is None:
33            raise ValueError("Need either a model or an explicit growth_rate")
34        self.model = model
35        self.n0 = n0
36        self._growth_rate = growth_rate
37        self.label = label
model
n0
label
growth_rate: float
39    @property
40    def growth_rate(self) -> float:
41        if self._growth_rate is None:
42            self._growth_rate = self.model.optimize().objective_value
43        return self._growth_rate
doubling_time: float
45    @property
46    def doubling_time(self) -> float:
47        """Doubling time in hours; inf for a non-growing population."""
48        mu = self.growth_rate
49        return np.log(2) / mu if mu > 0 else np.inf

Doubling time in hours; inf for a non-growing population.

def simulate(self, t_max: float, n_points: int = 100):
51    def simulate(self, t_max: float, n_points: int = 100):
52        t = np.linspace(0, t_max, n_points)
53        return t, self.n0 * np.exp(self.growth_rate * t)
def plot_growth_curve(self, t_max: float, n_points: int = 100, ax=None, **kwargs):
55    def plot_growth_curve(self, t_max: float, n_points: int = 100,
56                          ax=None, **kwargs):
57        import matplotlib.pyplot as plt
58
59        t, n = self.simulate(t_max, n_points)
60        ax = ax or plt.subplots(figsize=(7, 5))[1]
61        kwargs.setdefault("label", self.label or f"mu = {self.growth_rate:.3f} 1/h")
62        ax.plot(t, n, **kwargs)
63        ax.set(xlabel="Time (h)", ylabel="Population size")
64        return ax
class PopulationSet:
 70class PopulationSet:
 71    """One Population per sample, tied to the experiment's Metadata.
 72
 73    Colours and labels come from adesomics.plotting.style, so growth curves
 74    match the DE plots of the same experiment.
 75    """
 76
 77    def __init__(self, populations: dict[str, Population], metadata: Metadata):
 78        if metadata is not None:
 79            missing = set(populations) - set(metadata.samples)
 80            if missing:
 81                raise ValueError(f"Not in metadata: {sorted(missing)}")
 82
 83            self.metadata = metadata.subset_samples(list(populations))
 84            self.populations = {s: populations[s] for s in self.metadata.samples}
 85        else:
 86            print("Populationmodel currently only works with linked metadata as input.")
 87            raise SystemExit(1) 
 88            
 89    # --- construction ----------------------------------------------------
 90
 91    @classmethod
 92    def from_connector(cls, connector, metadata: Metadata,
 93                       samples: list[str] | None = None, n0: float = 1.0):
 94        """Build one Population per sample from E-Flux-constrained solutions."""
 95        samples = samples if samples is not None else list(connector.tpm.columns)
 96        solutions = connector.compare(samples)
 97
 98        pops = {
 99            s: Population(n0=n0,
100                          growth_rate=solutions[s].objective_value,
101                          label=s)
102            for s in samples
103        }
104        return cls(pops, metadata)
105
106    # --- access ----------------------------------------------------------
107
108    @property
109    def growth_rates(self) -> pd.Series:
110        return pd.Series({s: p.growth_rate for s, p in self.populations.items()},
111                         name="mu")
112
113    def summary(self) -> pd.DataFrame:
114        """Growth rate and doubling time per sample, with its condition."""
115        df = pd.DataFrame({
116            "condition": self.metadata.groups,
117            "mu": self.growth_rates,
118            "doubling_time_h": {s: p.doubling_time
119                                for s, p in self.populations.items()},
120        })
121        return df.loc[self.metadata.samples]
122
123    def by_group(self) -> pd.DataFrame:
124        """Mean and spread of mu per condition (replicates collapsed)."""
125        return (self.summary()
126                .groupby("condition")["mu"]
127                .agg(["mean", "std", "count"]))
128
129    # --- plotting --------------------------------------------------------
130
131    def plot_growth_curves(self, t_max: float, n_points: int = 100,
132                           ax=None, by_group: bool = True):
133        ax = ax or plt.subplots(figsize=(7, 5))[1]
134        lut = group_colors(self.metadata)
135        seen = set()
136
137        for sample in self.metadata.samples:
138            group = self.metadata.groups[sample]
139            if by_group:
140                label = group if group not in seen else None
141                seen.add(group)
142            else:
143                label = sample
144
145            self.populations[sample].plot_growth_curve(
146                t_max, n_points, ax=ax, color=lut[group], label=label,
147            )
148
149        ax.set(title="FBA-predicted exponential growth")
150        ax.legend(frameon=False, fontsize=7)
151        annotate_study(ax, self.metadata)
152        _save(ax.figure, "growth_curves.png", "results")
153        return ax
154
155    def plot_growth_rates(self, ax=None, save=None):
156        summ = self.summary()
157        by = self.by_group()
158        lut = group_colors(self.metadata)
159        order = list(by.index)
160
161        ax = ax or plt.subplots(figsize=(8, 5))[1]
162        label_x = summ["mu"].max() * 1.05
163        for i, group in enumerate(order):
164            members = summ.index[summ["condition"] == group]
165            ax.barh(i, by.loc[group, "mean"], color=lut[group], alpha=0.85,
166                    height=0.62, zorder=2)
167            ax.scatter(summ.loc[members, "mu"], [i] * len(members),
168                    color=INK, s=30, zorder=3)
169            td = np.log(2) / by.loc[group, "mean"]
170            ax.text(label_x, i, f"t$_d$ = {td:.1f} h",
171                va="center", fontsize=10, color=GREY)
172
173        ax.set_yticks(range(len(order)))
174        ax.set_yticklabels([g.replace("_", " ") for g in order])
175        ax.set(xlabel="specific growth rate  $\\mu$  (1/h)",
176            title="Growth rate by condition")
177        ax.margins(x=0.22)
178        ax.grid(axis="y", visible=False)
179        annotate_study(ax, self.metadata)
180        _save(ax.figure, "Bar_GrowthRates.png", "results")
181        return ax
182
183    def __len__(self):
184        return len(self.populations)
185
186    def __repr__(self):
187        return (f"<PopulationSet: {len(self)} samples, "
188                f"{self.metadata.groups.nunique()} conditions>")

One Population per sample, tied to the experiment's Metadata.

Colours and labels come from adesomics.plotting.style, so growth curves match the DE plots of the same experiment.

PopulationSet( populations: dict[str, Population], metadata: adesomics.io.metadata.Metadata)
77    def __init__(self, populations: dict[str, Population], metadata: Metadata):
78        if metadata is not None:
79            missing = set(populations) - set(metadata.samples)
80            if missing:
81                raise ValueError(f"Not in metadata: {sorted(missing)}")
82
83            self.metadata = metadata.subset_samples(list(populations))
84            self.populations = {s: populations[s] for s in self.metadata.samples}
85        else:
86            print("Populationmodel currently only works with linked metadata as input.")
87            raise SystemExit(1) 
@classmethod
def from_connector( cls, connector, metadata: adesomics.io.metadata.Metadata, samples: list[str] | None = None, n0: float = 1.0):
 91    @classmethod
 92    def from_connector(cls, connector, metadata: Metadata,
 93                       samples: list[str] | None = None, n0: float = 1.0):
 94        """Build one Population per sample from E-Flux-constrained solutions."""
 95        samples = samples if samples is not None else list(connector.tpm.columns)
 96        solutions = connector.compare(samples)
 97
 98        pops = {
 99            s: Population(n0=n0,
100                          growth_rate=solutions[s].objective_value,
101                          label=s)
102            for s in samples
103        }
104        return cls(pops, metadata)

Build one Population per sample from E-Flux-constrained solutions.

growth_rates: pandas.core.series.Series
108    @property
109    def growth_rates(self) -> pd.Series:
110        return pd.Series({s: p.growth_rate for s, p in self.populations.items()},
111                         name="mu")
def summary(self) -> pandas.core.frame.DataFrame:
113    def summary(self) -> pd.DataFrame:
114        """Growth rate and doubling time per sample, with its condition."""
115        df = pd.DataFrame({
116            "condition": self.metadata.groups,
117            "mu": self.growth_rates,
118            "doubling_time_h": {s: p.doubling_time
119                                for s, p in self.populations.items()},
120        })
121        return df.loc[self.metadata.samples]

Growth rate and doubling time per sample, with its condition.

def by_group(self) -> pandas.core.frame.DataFrame:
123    def by_group(self) -> pd.DataFrame:
124        """Mean and spread of mu per condition (replicates collapsed)."""
125        return (self.summary()
126                .groupby("condition")["mu"]
127                .agg(["mean", "std", "count"]))

Mean and spread of mu per condition (replicates collapsed).

def plot_growth_curves( self, t_max: float, n_points: int = 100, ax=None, by_group: bool = True):
131    def plot_growth_curves(self, t_max: float, n_points: int = 100,
132                           ax=None, by_group: bool = True):
133        ax = ax or plt.subplots(figsize=(7, 5))[1]
134        lut = group_colors(self.metadata)
135        seen = set()
136
137        for sample in self.metadata.samples:
138            group = self.metadata.groups[sample]
139            if by_group:
140                label = group if group not in seen else None
141                seen.add(group)
142            else:
143                label = sample
144
145            self.populations[sample].plot_growth_curve(
146                t_max, n_points, ax=ax, color=lut[group], label=label,
147            )
148
149        ax.set(title="FBA-predicted exponential growth")
150        ax.legend(frameon=False, fontsize=7)
151        annotate_study(ax, self.metadata)
152        _save(ax.figure, "growth_curves.png", "results")
153        return ax
def plot_growth_rates(self, ax=None, save=None):
155    def plot_growth_rates(self, ax=None, save=None):
156        summ = self.summary()
157        by = self.by_group()
158        lut = group_colors(self.metadata)
159        order = list(by.index)
160
161        ax = ax or plt.subplots(figsize=(8, 5))[1]
162        label_x = summ["mu"].max() * 1.05
163        for i, group in enumerate(order):
164            members = summ.index[summ["condition"] == group]
165            ax.barh(i, by.loc[group, "mean"], color=lut[group], alpha=0.85,
166                    height=0.62, zorder=2)
167            ax.scatter(summ.loc[members, "mu"], [i] * len(members),
168                    color=INK, s=30, zorder=3)
169            td = np.log(2) / by.loc[group, "mean"]
170            ax.text(label_x, i, f"t$_d$ = {td:.1f} h",
171                va="center", fontsize=10, color=GREY)
172
173        ax.set_yticks(range(len(order)))
174        ax.set_yticklabels([g.replace("_", " ") for g in order])
175        ax.set(xlabel="specific growth rate  $\\mu$  (1/h)",
176            title="Growth rate by condition")
177        ax.margins(x=0.22)
178        ax.grid(axis="y", visible=False)
179        annotate_study(ax, self.metadata)
180        _save(ax.figure, "Bar_GrowthRates.png", "results")
181        return ax