torchcell.datamodels.schema.Media

class Media(*, provenance_gaps: list[~torchcell.verification.sourced.ProvenanceGap] = <factory>, name: str | None, state: str | None, is_synthetic: bool | None, base_medium: str | None = None, components: list[~torchcell.datamodels.schema.MediaComponent] = <factory>, dropouts: list[~torchcell.datamodels.schema.Compound] = <factory>, dropout_roles: list[~torchcell.datamodels.schema.MediaComponentRole] = <factory>, provenance: list[~torchcell.verification.sourced.SourcedValue] = <factory>)[source]

Bases: ProvenanceGapMixin

Growth medium resolved to typed COMPONENTS (provenance-first).

name + state remain the human label; is_synthetic (REQUIRED) records defined-by-construction vs natural/complex; components is the compositional breakdown; dropouts are auxotrophic components deliberately OMITTED (recorded so ‘omitted’ is distinguishable from ‘not yet listed’). is_fully_characterized / open_gaps make under-definition queryable.

A medium the source never states is still a Media (#901, #902, #903): Environment.media stays required, and each of name, state and is_synthetic is stated or None beside a ProvenanceGap naming that field, its reason and where we looked (_stated_or_gapped). So SynLethDB’s aggregated pairs, whose release carries no condition field, and an SGD phenotype annotation with no condition property store a medium whose gaps say exactly that, never a guessed YPD and never a free-text stand-in. A partly stated medium gaps only what is missing: SGD’s “YPD with 2% glucose” states a name, not a physical state. media_identity keys a gapped medium by its gaps, so an absence never merges with a stated medium or with another source’s absence.

dropout_roles (lane X, 2026-10-15) is the ROLE-level counterpart of dropouts: a release that states a nutrient CLASS is absent without naming the compound removed (YeastPhenome’s SC (-C), SD + His + Leu + Lys + Ura, -N). A fully listed composition states such an absence by having no component in that role (M9_NOCARBON_WETMORE2015), but an unlisted one cannot: components=[] already means “composition not yet entered”, so a carbon-free medium and a medium whose recipe was never entered are the same bytes without this field. It is the same “omitted is distinguishable from not yet listed” rule one level up.

classmethod validate_state(v: str | None) → str | None[source]

Validate that a stated state is one of solid, liquid, or gas.

classmethod validate_dropout_roles_unique(v: list[MediaComponentRole]) → list[MediaComponentRole][source]

A role is omitted once; a repeat is a parse bug, not a stronger statement.

property stated_name: str

The name the source states; raises for a medium whose name is a typed gap.

For code that builds or reads a medium it knows to be named (a loader’s own library medium, an FBA expansion of one): a gapped medium has no label to stand in, so it is refused by name rather than spelled "None".

property is_fully_characterized: bool

True iff every listed component has a defined identity AND a concentration.

property open_gaps: list[str]

Component names still needing a fuller definition or a concentration.

model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].