kcat sensitivity tuning (Python)#
geckopy objects in geckopy.kcat_tuning.
Functions#
Function |
Summary |
|---|---|
Names, EC codes and sources for |
|
Give every tied position its representative's value. |
|
The reaction an |
|
The fewest-changes point whose RMSE is within |
|
Per-row prior std dev in log-space, from source-group membership. |
|
Map one |
|
Vectorised |
|
Tune kcats against experimental data with CMA-ES. |
|
Per-reaction carbon-weight lookup for RMSE weighting. |
|
One condition's simulated outcome. |
|
One kcat the tuning changed, with its provenance. |
|
The changed kcats, most consequential first. |
|
The corrections as a TSV document, header included. |
|
RMSE (mean, per-condition array) for one tuning dataset. |
|
Combine both datasets' RMSE into one score, per |
|
Experimental data used by evotune kcat tuning. |
|
Outcome of a |
|
Find the maximum kinetic value across one or more EC numbers. |
|
Sweep sigma and pick the one matching |
|
Per-kcat fold change from the prior, always >= 1. |
|
One (parsimony, fit) trade-off, from |
|
Which kcats have earned the right to be tuned. |
|
Fraction of the total screened impact carried by changed kcats. |
|
Index of the parameter each position follows. |
|
Biologically plausible bounds for proposed kcats, in 1/s. |
|
Load the three evotune-kcat-tuning experimental data files. |
|
Tuned fit at one candidate |
|
Per-kcat displacement from the prior, in prior standard deviations. |
|
How many kcats differ from their prior by more than |
|
How many parameters remain free once ties are applied. |
|
Score |
|
Pick the smallest share past which tuning stops paying for itself. |
|
Put every kcat that moved less than |
|
Report which kcats the data can actually speak to. |
|
Build a |
|
Iteratively bump the most-limiting kcat by |
|
Outcome of a sigma scan. |
|
Simulate every condition in one tuning dataset. |
|
Median fold change and near-prior fraction, per source group. |
|
Tune at each of |
|
Round values in |
|
Sweep |
|
Outcome of a sensitivity-tuning run. |
Reference#
annotate_from_model#
annotate_from_model(model, rxn_ids: Sequence[str])
Names, EC codes and sources for rxn_ids, read off an ecModel.
An isozyme copy carries a _EXP_n suffix that the underlying
reaction does not, so the name is looked up under the stripped id.
Anything absent is reported as "".
apply_ties#
apply_ties(particles: np.ndarray, tie_map: np.ndarray)
Give every tied position its representative's value.
particles is (n_params,) or (n_params, n_particles);
the array is modified in place and returned, so this can sit
directly in a sampling loop.
base_reaction#
base_reaction(rxn_id: str)
The reaction an ec.rxns entry belongs to, suffix removed.
best_parsimonious#
best_parsimonious(points: Sequence[FrontierPoint], tolerance: float=0.02)
The fewest-changes point whose RMSE is within tolerance of the
best on the frontier.
Defaults to 2%, which is the scale of the alternate-optimum noise measured on this model -- below that, two vectors are not meaningfully different in fit, so the sparser one wins.
build_sigma0_log#
build_sigma0_log(groups: np.ndarray, params: EvotuneParams)
Per-row prior std dev in log-space, from source-group membership.
Mirrors MATLAB's sigma0log = sigma0logDefault * ones(...); sigma0log(uniqKcatParams) = sigma0logSource(kcatSourceIdx(...)):
every row starts at params.sigma0_log_default, then rows whose
group is a real (non-UNLABELLED_GROUP) entry get
overridden by that group's sigma0_log_source value.
Parameters
groupsPer-row group names from
classify_kcat_sources.paramsSupplies
sigma0_log_defaultandsigma0_log_source.
Returns
numpy.ndarray of float, same shape as ``groups``.
:
classify_kcat_source#
classify_kcat_source(source: str, params: EvotuneParams, *, okp_method: Optional[str]=None)
Map one ec.source string to a trust-tier group name.
Matching is case-insensitive, mirroring MATLAB's
strcmpi(ecModel.ec.source, kcatSources{i}), and ignores the
bracketed fuzzy-match detail geckopy appends to a database source.
ec.source reads "brenda (wc=0, origin=1)" where MATLAB's
reads "brenda", so matching the whole string would leave every
fuzzy-matched kcat unlabelled and silently on the default prior
width -- which is most of a model's kcats. A source_groups entry
listing the full string still matches it.
Parameters
sourceOne
ec.sourceentry.paramsSupplies
source_groups.okp_methodThe project's configured OpenKineticsPredictor method (
OkpParams.method), if any. A group withmatch_okp=Truealso matchessourcewhen it equals this (case-insensitive).
Returns
strA key of
params.source_groups, orUNLABELLED_GROUPif nothing matches -- callers then fall back toparams.sigma0_log_default/shrink_thr_default/force_prior_thr_default.
classify_kcat_sources#
classify_kcat_sources(ec_sources: list[str], params: EvotuneParams, *, okp_method: Optional[str]=None)
Vectorised classify_kcat_source over ec.source.
Returns
`numpy.ndarray of str, shape ``(len(ec_sources),)``` :
cmaes_kcat_tuning#
cmaes_kcat_tuning(model: 'EcModel', *, adapter: Optional['ModelAdapter']=None, params: Optional['EvotuneParams']=None, evotune_data: Optional[EvotuneData]=None, okp_method: Optional[str]=None, bio_rxn: Optional[str]=None, make_anaerobic: Optional[Callable[['EcModel'], None]]=None, change_protein_biomass: Optional[Callable[['EcModel', float], None]]=None, tunable_mask: Optional[np.ndarray]=None, screen: Optional[pd.DataFrame]=None, target_impact_share: float=0.9, popsize: Optional[int]=None, n_proc: Optional[int]=None, seed: Optional[int]=None, verbose: bool=True)
Tune kcats against experimental data with CMA-ES.
Configuration comes entirely from EvotuneParams: trust tiers,
max_growth_weight, prior_penalty_weight and tie_isozymes
control the search, and max_generations/rmse_threshold are
its stopping conditions.
Parameters
modelEcModel with a populated
ec.kcat. Mutated in place: on return, tunable rows carry the best kcat vector CMA-ES found, with kcat constraints already applied.tunable_mask, screen, target_impact_shareControl which kcats are searched over, in order of precedence. Pass
tunable_maskto fix the set yourself. Otherwise, a mask is built byselect_tunable_maskattarget_impact_share, fromscreenif given (so a previously computedscreen_kcat_leveragereport need not be recomputed) or from a fresh screen otherwise.popsizeCMA-ES population size. Defaults to
cma's own dimension-scaled default (4 + floor(3 ln n)), so it adapts to however many free parameters the mask/tying produced rather than a value calibrated on one particular model.n_procNumber of worker processes for scoring each generation's candidates. Defaults to
cobra.Configuration().processes.1runs the original serial path (noPoolat all). See the module docstring's "Parallel scoring" section.seedIf given, seeds the search, for reproducible runs.
verboseWhether per-generation progress is logged at INFO.
make_anaerobic, change_protein_biomassForwarded to
simulate.simulate_evotune_dataset-- see its docstring; geckopy has no generic organism-agnostic implementation of these yet. See the module docstring's "Parallel scoring" section for a picklability caveat whenn_proc>1.
Returns
EvotuneResultrxns/old_kcat/new_kcat/groupscover the selected tunable set, tied groups included (their members share one value innew_kcat).rmse_trace/objective_tracerecord the best-so-far plain RMSE and optimised objective per generation.
Raises
ValueErrorIf there are no tunable kcats, or fewer than two free parameters remain after masking and tying -- too little for CMA-ES to search over.
compute_excarbon#
compute_excarbon(model: 'EcModel', rxn_ids: Iterable[str], bio_rxn_id: str)
Per-reaction carbon-weight lookup for RMSE weighting.
The biomass reaction gets BIOMASS_CARBON_EQUIV. Every
other reaction gets the carbon count of its single exchanged
metabolite (via cobrapy's parsed Metabolite.elements, not a
hand-rolled formula parser). A missing/unparseable formula, or a
computed carbon count of exactly 0 (e.g. O2, water), both fall
back to 1 -- matching MATLAB's Ematrix(isnan(Ematrix)) = 1 and
excarbon(excarbon == 0) = 1 respectively.
Parameters
modelModel providing the metabolite formulas.
rxn_idsReaction IDs to compute weights for (typically every reaction referenced by a tuning dataset: its exchange columns, its zero-flux list, and the biomass reaction).
bio_rxn_idThe biomass reaction ID.
Returns
dict[str, float]
:
ConditionSimResult#
One condition's simulated outcome.
feasible=False (LP infeasible/unbounded, or no biomass flux)
means growth/exch_fluxes/block_fluxes are meaningless;
.distance applies the fixed infeasibility penalty for that
row instead of reading them.
Correction#
One kcat the tuning changed, with its provenance.
corrections#
corrections(kcat: np.ndarray, kcat0: np.ndarray, rxn_ids: Sequence[str], *, sources: Optional[Sequence[str]]=None, names: Optional[Sequence[str]]=None, ec_codes: Optional[Sequence[str]]=None, leverage: Optional[np.ndarray]=None, rel_tol: float=0.02)
The changed kcats, most consequential first.
Parameters
kcat, kcat0Tuned and prior kcat vectors, same length as
rxn_ids.sources, names, ec_codesPer-kcat provenance. Missing entries are reported as
""rather than dropped, so a row never silently disappears.leveragePer-kcat effect on the distance from a one-at-a-time screen (see
.tuning.screen_kcat_leverage). If given, rows are ordered by it and the share columns are filled; otherwise rows are ordered by fold change and the shares are 0.rel_tolA kcat within
1 + rel_tolfold of its prior is unchanged.
Returns
list of :class:Correction, ordered most consequential first.
:
corrections_tsv#
corrections_tsv(rows: Sequence[Correction])
The corrections as a TSV document, header included.
dataset_rmse#
dataset_rmse(flux_data: 'FluxData', sims: list[ConditionSimResult], *, constrain: bool, excarbon: dict[str, float], bio_rxn_id: str, penalty: float=INFEASIBLE_PENALTY)
RMSE (mean, per-condition array) for one tuning dataset.
Parameters
flux_dataThe dataset being scored (
evotune_data.flux_dataorevotune_data.max_grate).simsOne
.simulate.ConditionSimResultperflux_data.condsrow, e.g. from.simulate.simulate_evotune_dataset.constrainMust match the value passed to
.simulate.simulate_evotune_datasetfor thesesims: True includes carbon-weighted exchange-flux terms (flux data), False scores growth alone (max-growth data).excarbonCarbon-weight lookup from
compute_excarbon, covering every reaction referenced bysims'exch_fluxes/block_fluxesplusbio_rxn_id. A reaction missing from the lookup falls back to a weight of 1.bio_rxn_idThe biomass reaction ID (for reading
flux_data.gr_rate's carbon-equivalent counterpart).penaltyRMSE substituted for an infeasible condition.
Returns
rmse: floatWeighted (by
flux_data.evotune_rmse_weight, if present) mean of the per-condition RMSEs.rmse_list: numpy.ndarrayThe per-condition RMSEs (post-penalty, pre-weighting), shape
(len(flux_data.conds),).
evotune_distance#
evotune_distance(evotune_data: 'EvotuneData', *, flux_sims: list[ConditionSimResult] | None, max_grate_sims: list[ConditionSimResult] | None, excarbon: dict[str, float], bio_rxn_id: str, penalty: float=INFEASIBLE_PENALTY, max_growth_weight: float=1.0)
Combine both datasets' RMSE into one score, per abc_max.m.
flux_sims must be given (non-None) iff evotune_data.flux_data
is not None, and likewise for max_grate_sims /
evotune_data.max_grate. A dataset absent from evotune_data doesn't
contribute to the combined RMSE (MATLAB: a missing dataset's
rmse_* stays [] and drops out of validIdx).
Parameters
max_growth_weightRelative weight of the max-growth dataset against the flux dataset, giving
(rmse_flux + w * rmse_max_grate) / (w + 1). At 2 the eight max-growth conditions carry twice the weight of the 33 flux conditions, which is the intended emphasis here: the max-growth set is where the carbon-source diversity lives, while 30 of the 33 flux conditions are glucose.This is deliberately not MATLAB's convention.
abc_max.mappliesweights = [maxGrowthWeight, 1]to[rmse_flux, rmse_maxGrate], so there the same field scales the flux term. A run reproducing MATLAB must pass 0.5 here to match itsmaxGrowthWeight = 2.
Returns
rmse: floatWeighted mean of whichever dataset RMSEs are present.
NaNif neither dataset is present.detail: dict[str, numpy.ndarray]Per-condition RMSE arrays, keyed
"flux_data"/"max_grate"for whichever datasets were scored.
EvotuneData#
Experimental data used by evotune kcat tuning.
Attributes
flux_dataPer-condition growth/exchange-flux measurements, from
evotuneFluxData.tsv.Noneif that file isn't present.max_gratePer-condition maximum-growth-rate measurements (one active carbon source per condition, at
-1000i.e. unconstrained), fromevotuneMaxGrowth.tsv.Noneif that file isn't present.zero_fluxReaction IDs assumed to carry zero flux, from
evotuneZeroExch.tsv. Empty if that file isn't present.
EvotuneResult#
Outcome of a cmaes_kcat_tuning run.
The model is mutated in place (the best kcat vector found is
applied to model.ec.kcat), and this result records the run's
history for inspection.
Attributes
rxnsTunable
ec.rxnsIDs, in the column order used throughout (rxns[i]corresponds toold_kcat[i]/new_kcat[i]/groups[i]). Tied isozyme copies are all listed, sharing one value innew_kcat.old_kcat, new_kcatkcat values before/after tuning, parallel to
rxns.groupsSource-group name per tunable row (from
priors.classify_kcat_sources).rmse_traceBest-so-far plain RMSE, per generation. Always the plain RMSE, so runs stay comparable across penalties and against MATLAB even when the search used a penalised objective.
objective_traceBest-so-far value of the quantity the search actually minimised, per generation. Identical to
rmse_tracewhenparams.prior_penalty_weightis 0.n_generationsNumber of generations actually run.
convergedTrue if
rmse_trace[-1] <= params.rmse_threshold; False ifmax_generationswas reached first.
find_max_value#
find_max_value(ec_string: str, brenda: 'BrendaData')
Find the maximum kinetic value across one or more EC numbers.
Searches both brenda.kcat_max and brenda.sa_max for each EC token
in ec_string and returns the overall maximum together with
its organism and parameter type. Wildcards in EC tokens (-)
trigger prefix matching: "1.1.1.-" matches any code starting
with "1.1.1.".
Ported from GECKO MATLAB: src/geckomat/kcat_sensitivity_analysis/findMaxValue.m.
Parameters
ec_stringSpace-separated EC tokens. Each token may optionally start with
"EC"(matching the MATLAB convention) and may contain-wildcards.brendaPre-loaded BRENDA tables.
Returns
valueThe overall maximum kinetic value (1/s).
organismThe organism contributing the maximum.
parameterEither
"K_cat"(came from the kcat table) or"SA*Mw"(came from the SA-derived kcat table).
If no EC matches anything in either table, returns
:
```(0.0, "", "")``.` :
fit_sigma#
fit_sigma(model: 'EcModel', *, growth_rate: Optional[float]=None, p_tot: Optional[float]=None, f: Optional[float]=None, n_sigma_steps: int=100, method: Literal['grid', 'bisect']='bisect')
Sweep sigma and pick the one matching growth_rate best.
Growth is monotone non-decreasing in sigma, so method="bisect"
(default) finds the best sigma in about log2(n) solves; the
returned grids then contain only the sigmas actually evaluated.
method="grid" keeps the legacy [1/n, 2/n, ..., 1.0] sweep
for diagnostic plotting (every grid point evaluated).
The best sigma is re-applied to model before return.
Ported from GECKO MATLAB: src/geckomat/kcat_sensitivity_analysis/sigmaFitter.m.
Parameters
modelEcModel with the protein pool machinery installed and the objective set (typically the biomass reaction). Mutated in place: the optimal sigma is applied via
set_prot_pool_sizebefore return.growth_rateExperimental growth rate to match. Defaults to
model.adapter.params.gr_exp.p_totTotal protein content (g/gDCW). Defaults to
model.adapter.params.p_tot.fMass fraction of model enzymes. Defaults to
model.adapter.params.f.n_sigma_stepsFor
method="grid", the number of sigma values tried in(0, 1](i / nfori = 1..n). Formethod="bisect", it caps the number of bisection iterations (the default 100 is far more than the ~20 needed to converge). Default 100.method"bisect"(default) finds the best sigma in ~log2(n)solves using the monotonicity of growth in sigma;"grid"sweeps the full grid.
Returns
SigmaFitterResult
:
Raises
ValueErrorIf
model.adapteris None and any default is needed; ifn_sigma_stepsis < 1; or ifmethodis unknown.
fold_change#
fold_change(kcat: np.ndarray, kcat0: np.ndarray)
Per-kcat fold change from the prior, always >= 1.
max(k/k0, k0/k). Reported in preference to
movement_in_sigma because sigma0_log differs by source
(0.10 for custom up to 0.30 for unlabelled), so dividing by it
hides the per-source behaviour: a vector that moves every source by
the same number of sigmas has moved the trusted ones three times
less in fold terms. Only the ranking of the sigmas across
sources is meaningful, not their absolute values.
FrontierPoint#
One (parsimony, fit) trade-off, from parsimony_frontier.
identifiability_mask#
identifiability_mask(drmse: np.ndarray, sigma0_log: np.ndarray, threshold: float)
Which kcats have earned the right to be tuned.
Keeps a kcat when |dRMSE| * sigma0_log > threshold, where
dRMSE is its one-at-a-time effect on the distance. Multiplying
by sigma0_log makes a trusted source clear a proportionally
higher bar before it may move: with custom at 0.10 and
unlabelled at 0.30, a custom kcat needs three times the
measured effect of an unlabelled one to qualify.
This uses only the ranking of the sigmas across sources, which is
the part that carries meaning -- their absolute values are a
convention. One global threshold sets the overall strictness;
the per-source differentiation is derived, not tuned.
isozyme_tie_map#
isozyme_tie_map(rxn_ids: Sequence[str], kcat0: np.ndarray, sources: Optional[Sequence[str]]=None, *, rel_tol: float=1e-09)
Index of the parameter each position follows.
Positions that are their own representative are free; the rest
follow one. Copies group together when they share a base reaction,
a prior value (to rel_tol) and -- when sources is given --
a source group.
Returns
`numpy.ndarray of int, one entry per position. ``tie_map[i] == i``` :
for a free parameter, otherwise the index it follows. The
:
representative of a group is its lowest index, so the map is
:
idempotent: ``tie_map[tie_map] == tie_map``.
:
kcat_bounds#
kcat_bounds(kcat0: np.ndarray)
Biologically plausible bounds for proposed kcats, in 1/s.
Proposals outside these bounds are not hypotheses worth spending an FBA solve on. The window is 1e-2 to 1e4 for an ordinary kcat, and always widened far enough to contain the prior with a hundred-fold margin either side, so an unusually slow or fast enzyme keeps a window around itself rather than being clipped towards the generic range.
Parameters
kcat0Prior kcat per tunable row, 1/s.
Returns
tuple of numpy.ndarrayLower and upper bound per row.
load_evotune_data#
load_evotune_data(adapter: 'ModelAdapter')
Load the three evotune-kcat-tuning experimental data files.
Each file is optional and resolved as
<adapter.params.path>/data/<filename>; a missing file yields
None (flux_data/max_grate) or an empty list
(zero_flux) rather than raising.
Parameters
adapterA
ModelAdapterwhoseparams.pathlocates thedata/folder.
Returns
EvotuneData
:
MaskSizePoint#
Tuned fit at one candidate target_impact_share.
Attributes
target_impact_shareThe share this point was tuned at.
n_selectedFree parameters
.tuning.select_tunable_maskchose at this share (tied isozyme copies count once).rmseThe lowest value reached in each seed's
EvotuneResult.rmse_trace-- notrmse_trace[-1], becausermse_traceonly records plain RMSE at a step that improved the (possibly prior-penalised) objective, percmaes_kcat_tuning's own docstring; with the default nonzeroprior_penalty_weighta later generation can improve the objective while its plain RMSE is slightly worse than an earlier generation's, making the final entry not necessarily the best one recorded.min(rmse_trace)is the best that was recorded either way, though a candidate that improved plain RMSE without improving the objective is invisible to both and tormse_traceitself -- setparams.prior_penalty_weight=0before a sweep meant to measure fit alone, if that residual gap matters. Kept per-seed rather than pre-averaged so a caller can look at the spread directly.seedsThe seeds each entry of
rmsecame from.
movement_in_sigma#
movement_in_sigma(kcat: np.ndarray, kcat0: np.ndarray, sigma0_log: np.ndarray)
Per-kcat displacement from the prior, in prior standard deviations.
|log(kcat / kcat0)| / sigma0_log, matching MATLAB's
devFromPrior = abs(muLog - log(kcat0)) ./ sigma0log.
n_changed#
n_changed(kcat: np.ndarray, kcat0: np.ndarray, rel_tol: float=0.02)
How many kcats differ from their prior by more than rel_tol.
The 2% default matches the threshold MATLAB reports its per-source "unchanged" percentages at.
n_free#
n_free(tie_map: np.ndarray)
How many parameters remain free once ties are applied.
parsimony_frontier#
parsimony_frontier(score: Callable[[np.ndarray], float], kcat: np.ndarray, kcat0: np.ndarray, sigma0_log: np.ndarray, thresholds: Sequence[float]=(0.0, 1.0, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 15.0))
Score kcat with progressively more of it reverted to prior.
Parameters
scoreScores one kcat vector; one call per threshold.
kcat, kcat0, sigma0_logTuned vector, prior vector, and per-kcat prior sigma.
thresholdsSigma cutoffs to sweep, ascending.
0.0reverts nothing and so reproduceskcat's own score.
Returns
list of FrontierPoint, in the order given.
:
revert_below#
revert_below(kcat: np.ndarray, kcat0: np.ndarray, sigma0_log: np.ndarray, threshold: float)
Put every kcat that moved less than threshold sigma back on
its prior, leaving the rest at their tuned values.
screen_kcat_leverage#
screen_kcat_leverage(model: 'EcModel', *, adapter: Optional['ModelAdapter']=None, params: Optional['EvotuneParams']=None, evotune_data: Optional[EvotuneData]=None, okp_method: Optional[str]=None, bio_rxn: Optional[str]=None, make_anaerobic: Optional[Callable[['EcModel'], None]]=None, change_protein_biomass: Optional[Callable[['EcModel', float], None]]=None, tunable_mask: Optional[np.ndarray]=None, fold: float=2.0, n_proc: Optional[int]=None)
Report which kcats the data can actually speak to.
No optimisation happens here -- this only says which parameters
are worth curating or tuning, and can be read on its own well
before any tuning run. Each tie-group (isozyme copies sharing a
prior and a source move together when params.tie_isozymes) is
perturbed up and down by fold, and its leverage is the largest
resulting change in plain RMSE -- always the plain fit, never the
prior_penalty_weight-penalised objective, since leverage asks
what the data supports, independent of how much a move is charged
for. Groups are ranked by that leverage weighted by
sigma0_log -- .parsimony.identifiability_mask's
convention, where a trusted source needs proportionally more
measured effect to rank alongside an untrusted one. This
trust-weighting means the reported cum_leverage_share is a
different quantity from .parsimony.impact_share, which is
unweighted.
Costs one simulation per condition per tie-group probed (two probes each, up and down) plus one baseline -- comparable in scale to a full tuning run's evaluation budget, not a cheap pre-check. FVA-unreachable kcats are not filtered out beforehand: a kcat that cannot carry flux under any condition shows zero leverage here by construction and simply sorts to the bottom, which is a more direct test than a separately computed static reachability mask.
Returns
pandas.DataFrameOne row per tie-group, sorted by
rankdescending:rxn_id(the group's representative),n_isozymes,source_group,kcat0,leverage,sigma0_log,rank(leverage * sigma0_log), andcum_leverage_share(the running total ofrank, as a fraction of its sum over every row). An internal_positionscolumn (a tuple of indices intomodel.ec.rxns) is whatselect_tunable_maskneeds to expand a row back into a mask; drop it before showing the table to a person.
select_tunable_mask#
select_tunable_mask(model: 'EcModel', screen: pd.DataFrame, *, target_impact_share: float=0.9)
Build a tunable_mask from a screen_kcat_leverage report.
Keeps the fewest highest-ranked groups whose combined leverage
reaches target_impact_share of the total -- a relative cutoff,
so the same target_impact_share selects a comparable quality of
parameter set on any model, rather than an absolute leverage value
or a fixed count calibrated on a different model's scale.
target_impact_share=0.9 is a starting point, not a validated
universal constant; check the resulting mask's size against
screen_kcat_leverage's own curve before trusting it on a
new model.
Pure and cheap -- no simulation, only screen is read.
sensitivity_tuning#
sensitivity_tuning(model: 'EcModel', *, desired_growth_rate: Optional[float]=None, fold_change: float=10.0, prot_to_ignore: Optional[list[str]]=None, max_iterations: int=1000, bio_rxn: Optional[str]=None, verbose: bool=True)
Iteratively bump the most-limiting kcat by fold_change until
the model can reach desired_growth_rate.
Each iteration:
Solves the LP at the current kcat values.
If growth didn't increase since the last iteration, warns and breaks (likely a substrate / pool constraint, not a kcat one).
If growth reached the target, returns.
Otherwise picks the
usage_prot_*reaction with the highest flux (= most-produced enzyme), excluding any whose UniProt ID is inprot_to_ignore.Finds the catalysed reaction consuming the most of that enzyme's pseudometabolite and multiplies that reaction's
ec.kcatbyfold_change.Annotates
ec.noteswith the original kcat and source (unless the source is already"sensitivityTuning").Re-applies kcat constraints for that single reaction.
Ported from GECKO MATLAB: src/geckomat/kcat_sensitivity_analysis/sensitivityTuning.m.
Parameters
modelEcModel with the protein pool / usage rxn machinery installed and a populated
ec.kcat. Mutated in place.desired_growth_rateTarget growth rate. Defaults to
model.adapter.params.gr_exp.fold_changePer-iteration kcat multiplier.
prot_to_ignoreUniProt IDs whose usage rxns are excluded from being picked as the limiting enzyme.
max_iterationsHard cap on tuning iterations. A backstop against a target growth rate that is unreachable by raising kcats alone.
bio_rxnBiomass reaction id to set as the objective. Defaults to
params.bio_rxnfrom the adapter when one is attached; if neither is available the model's current objective is used.verboseWhether per-iteration progress is logged at INFO.
Returns
TunedKcatsResult
:
Raises
ValueErrorIf the initial LP is infeasible, or if
desired_growth_rateis needed but no adapter / value is available.NotImplementedErrorIf
model.ec.gecko_lightis True.
SigmaFitterResult#
Outcome of a sigma scan.
Attributes
sigmaThe sigma value that minimised the absolute relative error between predicted and experimental growth.
sigma_gridSigma values that were tried.
growth_gridLP-optimal growth at each sigma.
error_gridAbsolute relative error at each sigma, in percent.
simulate_evotune_dataset#
simulate_evotune_dataset(model: 'EcModel', flux_data: 'FluxData', *, constrain: bool, zero_flux_rxns: list[str], bio_rxn_id: str, make_anaerobic: Optional[Callable[['EcModel'], None]]=None, change_protein_biomass: Optional[Callable[['EcModel', float], None]]=None)
Simulate every condition in one tuning dataset.
Mirrors abc_max.m's rmsecal FBA half:
Every exchange reaction that appears as a condition name anywhere in
flux_data.conds(matched by name againstflux_data.exch_mets) is blocked (lower_bound = 0) up front, across the board -- not just the reaction for the row being solved -- so one row's carbon source can't leak flux into another row's simulation.For row
i, that row's own carbon-source reaction is unblocked: fixed at the measured uptake rate ifconstrainis True (evotune_data.flux_data), or fully opened to-1000ifconstrainis False (evotune_data.max_grate-- "what's the best possible growth on this carbon source", not "growth at this measured rate").Optional per-condition adjustments run if the corresponding callable is supplied: anaerobic switch when the oxygen exchange column reads exactly 0, protein-content rescaling when
Ptotis given and nonzero.Solve; record growth, plus (
constraindatasets only, sincemax_gratehas no exchange measurements to compare against) every non-NaN measured exchange's simulated flux and everyzero_flux_rxnsentry's simulated flux.
All mutation happens inside with model:, which cobra reverts
on exit -- only bounds are touched here, so this is safe to call
repeatedly against one persistent (per-worker) model instance; a
per-particle EcModel.copy() is not used (prohibitively
expensive on a real-scale model).
Parameters
modelEcModel with the current particle's kcats already applied (via
apply_kcat_constraints).flux_dataThe dataset to simulate.
constrainTrue for flux data (fix uptake at the measured rate), False for max-growth data (open uptake fully).
zero_flux_rxnsReaction IDs assumed zero-flux in every condition (
evotune_data.zero_flux); the row's own carbon source is skipped even if it's also listed here.bio_rxn_idThe biomass reaction ID (
adapter.params.bio_rxn).make_anaerobic, change_protein_biomassOptional adapter-specific hooks (MATLAB:
modelAdapter.makeModelAnaerobic/.changeProteinBiomass). geckopy has no generic, organism-agnostic port of these yet -- in MATLAB they're written per model-adapter subclass (e.g.YeastGEMAdapter.m). Left as an injection point rather than a hard requirement: data with no anaerobic/Ptotrows never needs them, and passingNonefor either just skips that adjustment instead of raising.
Returns
list[ConditionSimResult]One entry per row of
flux_data.conds, in order.
Raises
ValueErrorIf any
flux_data.condsentry can't be matched by name to an exchange metabolite influx_data.exch_mets.
source_movement#
source_movement(kcat: np.ndarray, kcat0: np.ndarray, groups: np.ndarray, order: Sequence[str], near_tol: float=0.1)
Median fold change and near-prior fraction, per source group.
Two medians are reported per source, separating how many kcats a
result moves from how far it moves them. median_fold is taken
over every kcat of the source; where most of a source sits at its
prior -- which a restrictive tunable_mask guarantees -- it
saturates at 1.0 and carries no information about the tier.
median_fold_moved conditions on the kcats that did move, and so
stays informative at any sparsity.
Parameters
orderSource names from most to least trusted. Used only to report whether movement respects that ranking.
near_tolA kcat within
1 + near_tolfold of its prior counts as untouched, and is excluded frommedian_fold_moved.
Returns
dict keyed by source name, plus ``"_ordered"``: whether
:
```median_fold_movedis non-decreasing alongorder``, i.e.`
:
whether the result moved trusted kcats less far than untrusted
:
`ones. A source with nothing moved reports ``median_fold_moved``` :
1.0 and takes no part in that check.
:
sweep_tunable_mask_size#
sweep_tunable_mask_size(model: 'EcModel', *, adapter: Optional['ModelAdapter']=None, params: Optional['EvotuneParams']=None, evotune_data: Optional['EvotuneData']=None, okp_method: Optional[str]=None, bio_rxn: Optional[str]=None, make_anaerobic: Optional[Callable[['EcModel'], None]]=None, change_protein_biomass: Optional[Callable[['EcModel', float], None]]=None, screen: Optional[pd.DataFrame]=None, target_impact_shares: Sequence[float]=(0.3, 0.5, 0.7, 0.8, 0.9, 0.95), seeds: Sequence[int]=(0, 1), n_proc: Optional[int]=None, verbose: bool=True)
Tune at each of target_impact_shares and report the fit.
The screen is computed once (or reused, via screen) and shared
across every share and seed; only .tuning.cmaes_kcat_tuning
reruns per point. Each run starts from model's own current
ec.kcat -- model is not mutated (a fresh copy is tuned each
time), unlike .tuning.cmaes_kcat_tuning itself.
Cost is one screen (comparable to a full tuning run) plus
len(target_impact_shares) * len(seeds) tuning runs, each sized
by however many parameters that share selects -- the later points
in an increasing sequence of shares cost more, both because CMA-ES
scales its population with dimension and because a wider search
space usually takes longer to explore.
The defaults are a coarse scout, not a guarantee of finding the
true optimum: five or six points cannot resolve a curve whose real
knee sits inside one of the gaps between them, and how wide that
gap needs to be to matter is exactly what a first sweep is for
finding out. Look at n_selected after a first sweep -- if
consecutive shares jump by a large multiple in parameter count
(target_impact_share is closer to the leverage tail than to its
bulk there, e.g. the jump from 0.9 to 0.95 was 5x on one real
model), or if the recommendation lands next to the widest such gap,
that gap is where the true knee could be hiding; rerun with a few
shares added inside it. The same applies at the low end: if the
smallest share measured isn't clearly worse than the best one, it
has not necessarily bracketed the true minimum, and a smaller share
is worth adding to confirm it.
seeds defaults to two, not one, doubling the cost of the
default call: recommend_target_impact_share's noise-floor
check needs at least two seeds at a point to have anything to
check against, and with only one it silently falls back to a
cruder rule everywhere. Pass a single seed for the very first,
cheapest possible scout of the shape; widen to more once a
promising region is known, since that is where seed noise is most
likely to matter (fit values close enough together for it to
decide the comparison).
Parameters
modelEcModel with a populated
ec.kcat, used as the tuning starting point at every share/seed. Not mutated.target_impact_sharesCandidate shares to measure, any order (sorted on return).
seedsSeeds tuned at every share.
screenA precomputed
.tuning.screen_kcat_leveragereport. Computed once, fresh, if not given.
adapter, params, evotune_data, okp_method, bio_rxn, make_anaerobic,
:
change_protein_biomass, n_proc, verboseForwarded to
.tuning.screen_kcat_leverageand.tuning.cmaes_kcat_tuning; see their docstrings.
Returns
list of :class:MaskSizePoint, one per share, sorted by
:
```target_impact_share`` ascending.` :
truncate_values#
truncate_values(arr: np.ndarray, columns: Optional[list[int]]=None)
Round values in arr to ~6 significant figures.
For each value v:
order_magn = max(ceil(log10(|v|)), 0)
result = round(v, 6 - order_magn)
Zero, NaN, and infinite values are returned unchanged.
For 2D input, only the columns selected by columns are
processed (default: all). Other columns are copied through
unchanged. The input is never mutated; a new array is returned.
Ported from GECKO MATLAB: src/geckomat/kcat_sensitivity_analysis/truncateValues.m.
Parameters
arr1-D or 2-D numeric array.
columnsColumn indices to process for 2-D input. Default: every column. Ignored for 1-D input.
Returns
numpy.ndarrayNew array with the truncated values.
tune_prior_penalty_weight#
tune_prior_penalty_weight(model: 'EcModel', *, adapter: Optional['ModelAdapter']=None, params: Optional['EvotuneParams']=None, evotune_data: Optional[EvotuneData]=None, okp_method: Optional[str]=None, bio_rxn: Optional[str]=None, make_anaerobic: Optional[Callable[['EcModel'], None]]=None, change_protein_biomass: Optional[Callable[['EcModel', float], None]]=None, tunable_mask: Optional[np.ndarray]=None, screen: Optional[pd.DataFrame]=None, target_impact_share: float=0.9, candidates: Sequence[float]=(0.0, 0.01, 0.03, 0.1), seeds: Sequence[int]=(0, 1), fold_threshold: float=2.0, popsize: Optional[int]=None, n_proc: Optional[int]=None, verbose: bool=True)
Sweep prior_penalty_weight and report the fit/reproducibility
trade-off, so the value doesn't have to be picked blind.
prior_penalty_weight charges for moving a kcat away from its
prior, and mainly exists to keep large corrections reproducible
across seeds rather than landing on an arbitrary point along a flat
direction. How strong a charge is enough is model- and
data-specific: too little and the search's largest corrections
disagree across seeds; too much and the search stops finding real
corrections at all, giving up fit for nothing. This runs
cmaes_kcat_tuning at every value in candidates, each at
every seed in seeds, all against the same tunable set (built
once, the way cmaes_kcat_tuning would on its own if
tunable_mask/screen aren't given), and reports both fit and
cross-seed reproducibility per value.
This is exactly as expensive as it sounds: len(candidates) * len(seeds) full cmaes_kcat_tuning runs, none of them
reusable across candidates. Reuse a screen you already have to
at least avoid recomputing that part.
model is restored to its original kcats before every run and
left that way when this returns -- unlike cmaes_kcat_tuning,
which mutates model with its result, this function only
reports, since there is no single "best" candidate for it to leave
the model in. Fewer than two seeds makes every reproducibility
column NaN, since there is nothing to compare.
Parameters
candidatesprior_penalty_weightvalues to try.seedsSeeds to run each candidate at. Reproducibility columns pool every pair of seeds, so more than two sharpens them but costs proportionally more.
fold_thresholdA kcat counts as "moved" past this fold change from its prior. Matches
.parsimony.fold_change's convention.
tunable_mask, screen, target_impact_share, popsize, n_proc,
:
make_anaerobic, change_protein_biomass, okp_method, bio_rxnAs in
cmaes_kcat_tuning.
Returns
pandas.DataFrameOne row per candidate:
prior_penalty_weight,n_seeds,distance_meananddistance_spread(max minus min across seeds),fit_cost(distance_meanrelative to the best candidate's, as a fraction),n_changed_mean,n_movers(kcats pastfold_thresholdin either seed of a pair, summed over every pair),n_both_moved(past it in both -- the denominator for direction agreement, since a kcat only one seed moved has no direction to agree about),pct_direction_agree,median_fold_spreadandmax_fold_spread(among movers, the largest fold-change between what the two seeds landed on).
TunedKcatsResult#
Outcome of a sensitivity-tuning run.
Attributes
rxnsIDs of the reactions whose kcat was bumped, in tuning order.
rxn_namesReaction names parallel to
rxns(falls back to the reaction ID when it has no name).enzymesFor each tuned reaction, the catalysing enzyme ID(s),
;-joined when more than one enzyme is involved.old_kcatkcat value before tuning, parallel to
rxns.new_kcatkcat value after tuning, parallel to
rxns.sourceOriginal kcat provenance (
model.ec.source) before tuning, parallel torxns.