# Gas mixtures A **mixture** ({class}`~solprop.physdata.mixtures.Mixture`) is an ordered set of labelled populations. Each population ({class}`~solprop.physdata.mixtures.Component`) supplies a species and a number-density path along z in m⁻³; labels distinguish populations, including two populations of the same species. One mixture object supplies both the linear operator (the summed susceptibility) and the density weights of every nonlinear response, so the dispersion and the nonlinearity of a fill cannot disagree about its composition. This page is for anyone filling a guide or a free-space medium with more than one gas, with air, or with a gas whose optical data they supply themselves. It covers the species and their data, the two ways of building a mixture (a shared fill and independent profiles), how the linear operator and each nonlinear response combine the components, the per-species statistics and energy accounting, how to build and update a run from a mixture, and what the output file records. The single-gas data behind the species are on the [materials page](materials.md); pressure and temperature paths are on the [profiles page](../waveguides/profiles.md). ## Species and optical tables `solprop.physdata.species("Ar")` builds a built-in gas as a `Species`: its susceptibility, real-gas equation of state, transport properties and Kerr model are optmat's ([materials from optmat](material-selection.md#materials-from-optmat)), and its `IonisationData` is optmat's first ionisation energy. Its rate and Raman response are optmat's too, resolved for the run ([SEP 3](../../dev/seps/archive/sep-0003-optmat-materials.md)); a built-in species carries no Raman constants of its own. A custom species carries its own data, Raman constants included ({class}`~solprop.physdata.raman_types.MolecularRaman`). Missing response data is explicit; a response requiring unavailable data reports the species name. ### Air There are two different things called air, and only one of them is nonlinear. - **`Gas.Air`, the built-in entry**, is a *dispersion-only* species: optmat's refractive index of the composition `"dry air (N2, O2, Ar)"` as a whole ([materials](materials.md)). It has no Kerr coefficient, no ionisation data and no Raman data. Used as a species in a fill built by hand, such as `Mixture.fill({"Air": 1.0}, 1.0)`, it gives the right linear index but carries no Raman response under the default selection, refuses a Kerr response (`Air (Air) has no Kerr data`) and refuses plasma, and an explicit Raman selection of its label raises. On the keyword interfaces `kerr=False, raman=True` is refused for every gas (`raman=True needs kerr=True`), and with `kerr=False` and `raman` unset the fill runs with no nonlinear response at all and no warning, so a hand-built `Air` fill is a linear medium; use `Mixture.air` for a nonlinear one. - **`Mixture.air(pressure, ...)`** builds separate nitrogen, oxygen and argon populations (optmat's CIPM-2007 composition `"dry air (N2, O2, Ar)"`, renormalised over the three), each with its own dispersion, Kerr coefficient, ionisation channel and — for N₂ and O₂ — Raman response. Carbon dioxide and the other trace constituents are omitted; `fractions=` replaces the composition. `Mixture.air(..., argon=False)` takes optmat's `"dry air (N2, O2)"`, nitrogen and oxygen renormalised to each other, argon folded away with the trace gases. **`gas="air"` on the drivers means the two-component mixture.** `prop_capillary`, `prop_waveguide` and their `Simulation` constructors take `gas="air"` (any case) or `gas=Gas.Air` as `Mixture.air(pressure, temperature=..., argon=False)`, never as the dispersion-only entry, so the run has N₂ and O₂ Kerr, plasma and Raman channels; with `raman` unset both molecules' Raman responses are on ([Raman](raman.md#gas-fills-in-a-capillary-or-waveguide)). Omitting argon is a deliberate approximation: argon is 0.93 % of dry air's molecules, so leaving it out costs about 1 % of the dispersion and Kerr response, and its ionisation contribution must be assessed at the actual field strength. Its 15.76 eV ionisation potential does not exclude ionisation in every air calculation. Pass `gas=Mixture.air(...)` to keep it; argon then contributes dispersion and Kerr, and its missing Raman response is logged at INFO. These rounded major constituents follow the dry-air composition described by [NOAA](https://www.noaa.gov/jetstream/atmosphere). The fractions define the model; they are not a claim about every ambient atmosphere. ### Tabulated optical data `TabulatedSusceptibility` accepts increasing angular frequencies in rad/s, real refractivity per density `(n - 1)/rho` in m³, intensity absorption cross-sections in m², a declared wavelength range in metres, and a citation. It forms ```{math} \gamma(\omega)=2\frac{n(\omega)-1}{\rho} +i\frac{c\sigma(\omega)}{\omega}. ``` This is a conversion of dilute optical data. The medium subsequently forms the exact square root of the summed susceptibility. A cubic spline represents the real part; shape-preserving interpolation of the absorption cross-section prevents negative absorption between non-negative samples. Frequency derivatives differentiate these interpolants, including `1/omega` in the imaginary part. Evaluation outside the declared support raises `MaterialError`. Arrays are copied and made read-only. ```{code-block} python import numpy as np from solprop.constants import C from solprop.physdata import Species, TabulatedSusceptibility omega = np.linspace(2e15, 4e15, 101) optics = TabulatedSusceptibility( omega=omega, refractivity=np.full(omega.shape, 1e-30), # m³; synthetic example absorption_cross_section=np.full(omega.shape, 2e-24), # m² wavelength_range=(2 * np.pi * C / omega[-1], 2 * np.pi * C / omega[0]), citation="Synthetic constant optical data for this example", ) absorber = Species("example absorber", optics) ``` Custom species default to `IdealGas()`. Their nonlinear data (third-order hyperpolarisability, ionisation data, Raman model) must be supplied explicitly; selecting a response does not invent missing constants. ## Shared fills and independent profiles `Mixture.fill` takes species-to-fraction mappings summing to one and a total pressure in bar. Pressure may be a scalar, a `ZProfile`, endpoint pressures, or a pair of position and pressure sequences. Endpoint pairs require `length=` in metres. The standard pressure-gradient floor applies unless `floor=` is supplied. A zero fraction is an exact zero-density population and retains its label. ```{code-block} python from solprop.physdata import Mixture fill = Mixture.fill({"He": 0.8, "Ar": 0.2}, (3.0, 0.2), length=1.5) air = Mixture.air(6.0, temperature=300.0) changed = fill.with_fill({"He": 0.7, "Ar": 0.3}, pressure=2.0) ``` Each component uses its own equation of state at its partial pressure: `rho_s(z) = eos_s(x_s * p(z), T)`. These real-gas partials under Dalton's rule approximate a mixture EOS; cross-species virial terms are absent. No mixture EOS is used in propagation. The size of this approximation is quantified [below](#linear-propagation-and-absorption) at ten bar; that comparison is not an accuracy guarantee near condensation or at higher pressure. `IdealGas` uses `rho = p/(k_B T)`, converting bar at the interface. `Mixture.fill`, `Mixture.partial_pressures` and `Mixture.air` take `selection=`, a {class}`~solprop.physdata.selection.MaterialSelection`, and without one take `MaterialSelection()`: each named species is resolved from optmat, its linear law, equation of state and transport properties, with the same partial-pressure rule over optmat's equations of state, and `Mixture.air` takes optmat's named composition. Supplied species are kept as they are. See [materials from optmat](material-selection.md#materials-from-optmat). **A shared fill is one flow.** Given the core radius of the guide it flows through, `Mixture.fill(..., radius=)` (and `Mixture.air`, `with_fill`) builds an endpoint pair or `(z, p)` table as the steady flow of the whole mixture ({func}`solprop.gasflow.steady_fill`, [profiles](../waveguides/profiles.md#steady-flow-fills)): it follows the bore, chokes a vacuum end (`vacuum_end="choked"`, the default with a radius; `"floor"` keeps the declared ends and still follows the bore) and checks the flow regime, using the mixture's viscosity from Wilke's rule and its sound speed from the mole-weighted molar mass and the mixed ideal-gas heat capacities ({mod}`solprop.physdata.transport`). The species are assumed not to separate along the fill; separation is not modelled. Without a radius the uniform law and the numerical floor remain, since the choke needs the bore; the simulation constructors supply the guide's. A `vacuum_end=` beside a constant pressure or a profile, which have no end to choke, is refused by name, and so is a varying `radius=` beside a varying pressure profile built without the bore (it would be used as given; build it with `PressureGradient.from_stages(..., radius=)`). A choked end depends on the gas, so `with_fill(fractions)` that keeps a fill with a choked end is refused and asks for the pressure again, which builds the flow for the new composition. **A temperature along z.** `temperature=` of `Mixture.fill`, `Mixture.air`, `partial_pressures` and `of` takes a number or a `ZProfile` in K (a constant path is stored as its number, bit for bit). Every density path then follows it, $\rho_s(z) = \mathrm{eos}_s\big(x_s\,p(z), T(z)\big)$ ({class}`~solprop.profiles.DensityPath`), and a gradient built from end pressures follows the flow weight $\eta(T)\,T$ of the whole mixture, with or without a radius ([profiles](../waveguides/profiles.md#a-temperature-that-varies-along-the-fibre)). A `PressureGradient` supplied beside a varying temperature — to `fill`, `air`, `with_fill` or `partial_pressures` — must carry the weight of this gas and this temperature path (`solprop.gasflow.steady_fill(..., temperature=)`); a missing or foreign weight is refused by name. A declared pressure path that is not a steady-flow fill is taken as given. `with_fill(fractions)` refuses to keep a weighted fill, whose weight is the old composition's. A Mixture owns its temperature: a gas-filled mode or a simulation constructor given a Mixture refuses a `temperature=` that is not the mixture's own (omit it). A shared fill stays one flow, but one total pressure does not fix the densities once the temperature varies: `Mixture.at_parameters({"pressure": ...})` is refused by name, and a gas-filled mode declares the populations as its parameters, `density: