Skip to content

Zalmoxis.mixing

mixing

Multi-material volume-additive mixing for layered planet models.

Provides: - LayerMixture: container for a layer's EOS components and mass fractions. - parse_layer_components(): parse config strings into LayerMixture. - parse_all_layer_mixtures(): parse all layers. - calculate_mixed_density(): harmonic-mean density from multiple EOS. - get_mixed_nabla_ad(): mass-weighted adiabatic gradient from multiple EOS. - any_component_is_tdep(): check if any component uses T-dependent EOS.

Imports
  • eos_functions: calculate_density, _get_paleos_unified_nabla_ad, _get_paleos_nabla_ad, _compute_paleos_dtdp
  • binodal: rogers2025_suppression_weight, gupta2025_suppression_weight
  • constants: TDEP_EOS_NAMES

LayerMixture(components=list(), fractions=list()) dataclass

A layer composed of one or more EOS materials with mass fractions.

Parameters:

Name Type Description Default
components list of str

EOS identifier strings, e.g. ["PALEOS:MgSiO3", "PALEOS:H2O"].

list()
fractions list of float

Mass fractions corresponding to each component. Must sum to 1.0.

list()

has_tdep()

True if any component uses a T-dependent EOS.

Source code in src/zalmoxis/mixing.py
156
157
158
def has_tdep(self) -> bool:
    """True if any component uses a T-dependent EOS."""
    return any(c in TDEP_EOS_NAMES for c in self.components)

is_single()

True if this mixture contains exactly one component.

Source code in src/zalmoxis/mixing.py
122
123
124
def is_single(self) -> bool:
    """True if this mixture contains exactly one component."""
    return len(self.components) == 1

primary()

Return the dominant (highest fraction) EOS identifier.

Source code in src/zalmoxis/mixing.py
126
127
128
129
def primary(self) -> str:
    """Return the dominant (highest fraction) EOS identifier."""
    idx = int(np.argmax(self.fractions))
    return self.components[idx]

update_fractions(new_fractions)

Update mass fractions at runtime (called by PROTEUS/CALLIOPE).

Parameters:

Name Type Description Default
new_fractions list of float

New mass fractions, must have same length as components and sum to 1.0 (within tolerance 1e-6).

required

Raises:

Type Description
ValueError

If length mismatch or fractions don't sum to 1.0.

Source code in src/zalmoxis/mixing.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def update_fractions(self, new_fractions: list[float]) -> None:
    """Update mass fractions at runtime (called by PROTEUS/CALLIOPE).

    Parameters
    ----------
    new_fractions : list of float
        New mass fractions, must have same length as components
        and sum to 1.0 (within tolerance 1e-6).

    Raises
    ------
    ValueError
        If length mismatch or fractions don't sum to 1.0.
    """
    if len(new_fractions) != len(self.components):
        raise ValueError(
            f'Expected {len(self.components)} fractions, got {len(new_fractions)}.'
        )
    if any(f < 0 for f in new_fractions):
        raise ValueError(f'Mass fractions must be non-negative, got {new_fractions}.')
    total = sum(new_fractions)
    if abs(total - 1.0) > 1e-6:
        raise ValueError(f'Fractions must sum to 1.0, got {total:.8f}.')
    self.fractions = list(new_fractions)

parse_layer_components(config_value)

Parse a layer EOS config string into a LayerMixture.

Formats

Single material (backward compat)::

"PALEOS:iron"  ->  LayerMixture(["PALEOS:iron"], [1.0])

Multi-material with mass fractions::

"PALEOS:MgSiO3:0.85+PALEOS:H2O:0.15"
->  LayerMixture(["PALEOS:MgSiO3", "PALEOS:H2O"], [0.85, 0.15])

Parameters:

Name Type Description Default
config_value str

Layer EOS config string from TOML.

required

Returns:

Type Description
LayerMixture

Raises:

Type Description
ValueError

If no components found or fractions are invalid.

Source code in src/zalmoxis/mixing.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def parse_layer_components(config_value: str) -> LayerMixture:
    """Parse a layer EOS config string into a LayerMixture.

    Formats
    -------
    Single material (backward compat)::

        "PALEOS:iron"  ->  LayerMixture(["PALEOS:iron"], [1.0])

    Multi-material with mass fractions::

        "PALEOS:MgSiO3:0.85+PALEOS:H2O:0.15"
        ->  LayerMixture(["PALEOS:MgSiO3", "PALEOS:H2O"], [0.85, 0.15])

    Parameters
    ----------
    config_value : str
        Layer EOS config string from TOML.

    Returns
    -------
    LayerMixture

    Raises
    ------
    ValueError
        If no components found or fractions are invalid.
    """
    if not config_value or not config_value.strip():
        raise ValueError('Empty EOS config string.')

    segments = config_value.split('+')
    components = []
    fractions = []

    for seg in segments:
        eos_name, frac = _parse_single_component(seg)
        if not eos_name:
            raise ValueError(f'Empty EOS name in component: {seg!r}')
        components.append(eos_name)
        fractions.append(frac)

    if not components:
        raise ValueError(f'No components parsed from: {config_value!r}')

    # Normalize fractions if needed
    total = sum(fractions)
    if len(components) > 1 and abs(total - 1.0) > 1e-6:
        logger.warning(
            f'Mass fractions sum to {total:.6f}, normalizing to 1.0. '
            f'Components: {components}, fractions: {fractions}'
        )
        fractions = [f / total for f in fractions]
    elif len(components) == 1:
        fractions = [1.0]

    if any(f < 0 for f in fractions):
        raise ValueError(f'Negative mass fraction in {config_value!r}: {fractions}')

    return LayerMixture(components=components, fractions=fractions)

parse_all_layer_mixtures(layer_eos_config)

Parse all layers in layer_eos_config into LayerMixture objects.

Parameters:

Name Type Description Default
layer_eos_config dict

Per-layer EOS config, e.g. {"core": "PALEOS:iron", "mantle": "PALEOS:MgSiO3:0.85+PALEOS:H2O:0.15"}.

required

Returns:

Type Description
dict

Per-layer LayerMixture objects, keyed by layer name.

Source code in src/zalmoxis/mixing.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def parse_all_layer_mixtures(
    layer_eos_config: dict[str, str],
) -> dict[str, LayerMixture]:
    """Parse all layers in layer_eos_config into LayerMixture objects.

    Parameters
    ----------
    layer_eos_config : dict
        Per-layer EOS config, e.g.
        ``{"core": "PALEOS:iron", "mantle": "PALEOS:MgSiO3:0.85+PALEOS:H2O:0.15"}``.

    Returns
    -------
    dict
        Per-layer LayerMixture objects, keyed by layer name.
    """
    return {
        layer: parse_layer_components(eos_str)
        for layer, eos_str in layer_eos_config.items()
        if eos_str
    }

calculate_mixed_density(pressure, temperature, mixture, material_dictionaries, solidus_func, liquidus_func, interpolation_functions, mushy_zone_factors=None, condensed_rho_min=CONDENSED_RHO_MIN_DEFAULT, condensed_rho_scale=CONDENSED_RHO_SCALE_DEFAULT, binodal_T_scale=BINODAL_T_SCALE_DEFAULT)

Compute volume-additive mixed density for a layer mixture.

For a single component, delegates directly to calculate_density(). For multiple components, evaluates each independently at (P, T) and returns a suppressed harmonic mean where each component is weighted by the product of:

  1. A density-based sigmoid (_condensed_weight): suppresses gas-like components by density.
  2. A binodal sigmoid (_binodal_factor): suppresses H2 when it is thermodynamically immiscible with its partner (below the binodal temperature). Only applies to H2 components in mixtures with silicate or water.
Notes

The suppressed harmonic mean does not conserve mass: when a component is suppressed (sigma near 0), its mass is excluded from the structural calculation. This is physically equivalent to treating vapor-phase volatiles as having negligible structural contribution. The approximation is valid when suppressed components are at low density (vapor/gas) and do not contribute meaningfully to hydrostatic support.

Parameters:

Name Type Description Default
pressure float

Pressure in Pa.

required
temperature float

Temperature in K.

required
mixture LayerMixture

Layer mixture specification.

required
material_dictionaries dict

EOS registry.

required
solidus_func callable or None

Solidus melting curve function.

required
liquidus_func callable or None

Liquidus melting curve function.

required
interpolation_functions dict

Interpolation cache.

required
mushy_zone_factors dict or float or None

Per-EOS mushy zone factors. Dict keyed by EOS name, a single float (applied to all), or None (default 1.0 for all).

None
condensed_rho_min float

Sigmoid center for phase-aware suppression (kg/m^3).

CONDENSED_RHO_MIN_DEFAULT
condensed_rho_scale float

Sigmoid width for phase-aware suppression (kg/m^3).

CONDENSED_RHO_SCALE_DEFAULT
binodal_T_scale float

Binodal sigmoid width in K. Controls the smooth transition at the miscibility boundary. Default 50 K.

BINODAL_T_SCALE_DEFAULT

Returns:

Type Description
float or None

Mixed density in kg/m^3, or None if all components are gas-like.

Source code in src/zalmoxis/mixing.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
def calculate_mixed_density(
    pressure,
    temperature,
    mixture,
    material_dictionaries,
    solidus_func,
    liquidus_func,
    interpolation_functions,
    mushy_zone_factors=None,
    condensed_rho_min=CONDENSED_RHO_MIN_DEFAULT,
    condensed_rho_scale=CONDENSED_RHO_SCALE_DEFAULT,
    binodal_T_scale=BINODAL_T_SCALE_DEFAULT,
):
    """Compute volume-additive mixed density for a layer mixture.

    For a single component, delegates directly to ``calculate_density()``.
    For multiple components, evaluates each independently at (P, T) and
    returns a suppressed harmonic mean where each component is weighted
    by the product of:

    1. A density-based sigmoid (``_condensed_weight``): suppresses
       gas-like components by density.
    2. A binodal sigmoid (``_binodal_factor``): suppresses H2 when it
       is thermodynamically immiscible with its partner (below the
       binodal temperature). Only applies to H2 components in mixtures
       with silicate or water.

    Notes
    -----
    The suppressed harmonic mean does not conserve mass: when a component
    is suppressed (sigma near 0), its mass is excluded from the structural
    calculation. This is physically equivalent to treating vapor-phase
    volatiles as having negligible structural contribution. The
    approximation is valid when suppressed components are at low density
    (vapor/gas) and do not contribute meaningfully to hydrostatic support.

    Parameters
    ----------
    pressure : float
        Pressure in Pa.
    temperature : float
        Temperature in K.
    mixture : LayerMixture
        Layer mixture specification.
    material_dictionaries : dict
        EOS registry.
    solidus_func : callable or None
        Solidus melting curve function.
    liquidus_func : callable or None
        Liquidus melting curve function.
    interpolation_functions : dict
        Interpolation cache.
    mushy_zone_factors : dict or float or None
        Per-EOS mushy zone factors. Dict keyed by EOS name, a single
        float (applied to all), or None (default 1.0 for all).
    condensed_rho_min : float
        Sigmoid center for phase-aware suppression (kg/m^3).
    condensed_rho_scale : float
        Sigmoid width for phase-aware suppression (kg/m^3).
    binodal_T_scale : float
        Binodal sigmoid width in K. Controls the smooth transition at
        the miscibility boundary. Default 50 K.

    Returns
    -------
    float or None
        Mixed density in kg/m^3, or None if all components are gas-like.
    """
    from .eos_functions import calculate_density

    # Fast path: single component (no suppression)
    if mixture.is_single():
        mzf = _get_mushy_zone_factor(mixture.components[0], mushy_zone_factors)
        return calculate_density(
            pressure,
            material_dictionaries,
            mixture.components[0],
            temperature,
            solidus_func,
            liquidus_func,
            interpolation_functions,
            mzf,
        )

    # Multi-component suppressed harmonic mean
    w_eff_sum = 0.0
    inv_rho_sum = 0.0
    for eos_name, w_i in zip(mixture.components, mixture.fractions):
        if w_i <= 0:
            continue
        mzf = _get_mushy_zone_factor(eos_name, mushy_zone_factors)
        rho_i = calculate_density(
            pressure,
            material_dictionaries,
            eos_name,
            temperature,
            solidus_func,
            liquidus_func,
            interpolation_functions,
            mzf,
        )
        if rho_i is None or not np.isfinite(rho_i) or rho_i <= 0:
            # Abort the entire mixture rather than skipping this component.
            # A None from calculate_density indicates an EOS table coverage
            # gap, not a vapor-phase state (vapor has low but finite density).
            # Treating it as suppressed (continue) would silently hide table
            # errors. The Picard loop falls back to old_density for this shell.
            return None
        # Per-component sigmoid center and width: each volatile has its own
        # critical density. Fall back to the global config value if not listed.
        rho_min_i = _COMPONENT_RHO_MIN.get(eos_name, condensed_rho_min)
        rho_scale_i = _COMPONENT_RHO_SCALE.get(eos_name, condensed_rho_scale)
        sigma_i = _condensed_weight(rho_i, rho_min_i, rho_scale_i)
        sigma_i *= _binodal_factor(
            eos_name, w_i, mixture, pressure, temperature, binodal_T_scale
        )
        w_eff = w_i * sigma_i
        if w_eff <= 0:
            continue
        w_eff_sum += w_eff
        inv_rho_sum += w_eff / rho_i

    if w_eff_sum <= 0 or inv_rho_sum <= 0:
        return None
    return w_eff_sum / inv_rho_sum

calculate_mixed_density_batch(pressures, temperatures, mixture, material_dictionaries, solidus_func, liquidus_func, interpolation_functions, mushy_zone_factors=None, condensed_rho_min=CONDENSED_RHO_MIN_DEFAULT, condensed_rho_scale=CONDENSED_RHO_SCALE_DEFAULT, binodal_T_scale=BINODAL_T_SCALE_DEFAULT)

Vectorized mixed density for a batch of (P, T) points in one layer.

Parameters:

Name Type Description Default
pressures ndarray

1D array of pressures in Pa.

required
temperatures ndarray

1D array of temperatures in K.

required
mixture LayerMixture

Layer mixture specification (same for all points in the batch).

required
material_dictionaries dict

EOS registry.

required
solidus_func callable or None

Solidus melting curve function.

required
liquidus_func callable or None

Liquidus melting curve function.

required
interpolation_functions dict

Interpolation cache.

required
mushy_zone_factors dict or float or None

Per-EOS mushy zone factors.

None
condensed_rho_min float

Global sigmoid center fallback (kg/m^3).

CONDENSED_RHO_MIN_DEFAULT
condensed_rho_scale float

Global sigmoid width fallback (kg/m^3).

CONDENSED_RHO_SCALE_DEFAULT
binodal_T_scale float

Binodal sigmoid width in K.

BINODAL_T_SCALE_DEFAULT

Returns:

Type Description
ndarray

1D array of densities in kg/m^3. NaN where lookup fails or all components are suppressed.

Source code in src/zalmoxis/mixing.py
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
def calculate_mixed_density_batch(
    pressures,
    temperatures,
    mixture,
    material_dictionaries,
    solidus_func,
    liquidus_func,
    interpolation_functions,
    mushy_zone_factors=None,
    condensed_rho_min=CONDENSED_RHO_MIN_DEFAULT,
    condensed_rho_scale=CONDENSED_RHO_SCALE_DEFAULT,
    binodal_T_scale=BINODAL_T_SCALE_DEFAULT,
):
    """Vectorized mixed density for a batch of (P, T) points in one layer.

    Parameters
    ----------
    pressures : numpy.ndarray
        1D array of pressures in Pa.
    temperatures : numpy.ndarray
        1D array of temperatures in K.
    mixture : LayerMixture
        Layer mixture specification (same for all points in the batch).
    material_dictionaries : dict
        EOS registry.
    solidus_func : callable or None
        Solidus melting curve function.
    liquidus_func : callable or None
        Liquidus melting curve function.
    interpolation_functions : dict
        Interpolation cache.
    mushy_zone_factors : dict or float or None
        Per-EOS mushy zone factors.
    condensed_rho_min : float
        Global sigmoid center fallback (kg/m^3).
    condensed_rho_scale : float
        Global sigmoid width fallback (kg/m^3).
    binodal_T_scale : float
        Binodal sigmoid width in K.

    Returns
    -------
    numpy.ndarray
        1D array of densities in kg/m^3. NaN where lookup fails or all
        components are suppressed.
    """
    from .eos_functions import calculate_density_batch

    n = len(pressures)

    # Fast path: single component (no suppression)
    if mixture.is_single():
        mzf = _get_mushy_zone_factor(mixture.components[0], mushy_zone_factors)
        return calculate_density_batch(
            pressures,
            temperatures,
            material_dictionaries,
            mixture.components[0],
            solidus_func,
            liquidus_func,
            interpolation_functions,
            mzf,
        )

    # Multi-component: evaluate each component, then suppressed harmonic mean
    w_eff_sum = np.zeros(n)
    inv_rho_sum = np.zeros(n)
    any_invalid = np.zeros(n, dtype=bool)

    for eos_name, w_i in zip(mixture.components, mixture.fractions):
        if w_i <= 0:
            continue
        mzf = _get_mushy_zone_factor(eos_name, mushy_zone_factors)
        rho_i = calculate_density_batch(
            pressures,
            temperatures,
            material_dictionaries,
            eos_name,
            solidus_func,
            liquidus_func,
            interpolation_functions,
            mzf,
        )
        # Mark shells where any component has invalid density
        bad = ~np.isfinite(rho_i) | (rho_i <= 0)
        any_invalid |= bad

        # Per-component sigmoid
        rho_min_i = _COMPONENT_RHO_MIN.get(eos_name, condensed_rho_min)
        rho_scale_i = _COMPONENT_RHO_SCALE.get(eos_name, condensed_rho_scale)
        sigma_i = _condensed_weight_batch(np.where(bad, 1.0, rho_i), rho_min_i, rho_scale_i)

        # Binodal suppression (scalar per shell, only for H2 components)
        if eos_name in _H2_EOS_NAMES:
            for j in range(n):
                if not bad[j]:
                    sigma_i[j] *= _binodal_factor(
                        eos_name,
                        w_i,
                        mixture,
                        pressures[j],
                        temperatures[j],
                        binodal_T_scale,
                    )

        w_eff = w_i * sigma_i
        w_eff = np.where(bad, 0.0, w_eff)

        ok = w_eff > 0
        w_eff_sum += np.where(ok, w_eff, 0.0)
        inv_rho_sum += np.where(ok, w_eff / np.where(ok, rho_i, 1.0), 0.0)

    result = np.where(
        (w_eff_sum > 0) & (inv_rho_sum > 0) & ~any_invalid,
        w_eff_sum / np.where(inv_rho_sum > 0, inv_rho_sum, 1.0),
        np.nan,
    )
    return result

get_mixed_nabla_ad(pressure, temperature, mixture, material_dictionaries, interpolation_functions, solidus_func=None, liquidus_func=None, mushy_zone_factors=None, condensed_rho_min=CONDENSED_RHO_MIN_DEFAULT, condensed_rho_scale=CONDENSED_RHO_SCALE_DEFAULT, binodal_T_scale=BINODAL_T_SCALE_DEFAULT, precomputed_densities=None)

Compute mass-fraction-weighted nabla_ad for a mixture.

For a single component, returns that component's nabla_ad directly. For multiple components, computes a weighted average where each component's contribution is scaled by the same sigmoid suppression used for density. This prevents vapor-phase components from contributing their (typically large) nabla_ad to the mixture.

Components that do not support nabla_ad (e.g., Seager2007) are skipped and their fraction is redistributed among T-dependent components.

Parameters:

Name Type Description Default
pressure float

Pressure in Pa.

required
temperature float

Temperature in K.

required
mixture LayerMixture

Layer mixture specification.

required
material_dictionaries dict

EOS registry.

required
interpolation_functions dict

Interpolation cache.

required
solidus_func callable or None

Solidus function (for PALEOS-2phase).

None
liquidus_func callable or None

Liquidus function (for PALEOS-2phase).

None
mushy_zone_factors dict or float or None

Per-EOS mushy zone factors. Dict keyed by EOS name, a single float (applied to all), or None (default 1.0 for all).

None
condensed_rho_min float

Sigmoid center for phase-aware suppression (kg/m^3).

CONDENSED_RHO_MIN_DEFAULT
condensed_rho_scale float

Sigmoid width for phase-aware suppression (kg/m^3).

CONDENSED_RHO_SCALE_DEFAULT
binodal_T_scale float

Binodal sigmoid width in K for H2 miscibility suppression. Default 50 K.

BINODAL_T_SCALE_DEFAULT
precomputed_densities dict or None

Optional pre-computed per-component densities, keyed by EOS name. When provided, skips the calculate_density() call for the sigmoid weight computation, avoiding redundant EOS table lookups when density was already computed in calculate_mixed_density().

None

Returns:

Type Description
float or None

Dimensionless adiabatic gradient, or None if no component provides it.

Notes

Suppressing a vapor component's nabla_ad means the temperature profile ignores the volatile. This is consistent with the density suppression (both treat the vapor as structurally absent) but differs from reality where vapor affects heat transport. This is a known limitation.

Source code in src/zalmoxis/mixing.py
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
def get_mixed_nabla_ad(
    pressure,
    temperature,
    mixture,
    material_dictionaries,
    interpolation_functions,
    solidus_func=None,
    liquidus_func=None,
    mushy_zone_factors=None,
    condensed_rho_min=CONDENSED_RHO_MIN_DEFAULT,
    condensed_rho_scale=CONDENSED_RHO_SCALE_DEFAULT,
    binodal_T_scale=BINODAL_T_SCALE_DEFAULT,
    precomputed_densities=None,
):
    """Compute mass-fraction-weighted nabla_ad for a mixture.

    For a single component, returns that component's nabla_ad directly.
    For multiple components, computes a weighted average where each
    component's contribution is scaled by the same sigmoid suppression
    used for density. This prevents vapor-phase components from
    contributing their (typically large) nabla_ad to the mixture.

    Components that do not support nabla_ad (e.g., Seager2007) are skipped
    and their fraction is redistributed among T-dependent components.

    Parameters
    ----------
    pressure : float
        Pressure in Pa.
    temperature : float
        Temperature in K.
    mixture : LayerMixture
        Layer mixture specification.
    material_dictionaries : dict
        EOS registry.
    interpolation_functions : dict
        Interpolation cache.
    solidus_func : callable or None
        Solidus function (for PALEOS-2phase).
    liquidus_func : callable or None
        Liquidus function (for PALEOS-2phase).
    mushy_zone_factors : dict or float or None
        Per-EOS mushy zone factors. Dict keyed by EOS name, a single
        float (applied to all), or None (default 1.0 for all).
    condensed_rho_min : float
        Sigmoid center for phase-aware suppression (kg/m^3).
    condensed_rho_scale : float
        Sigmoid width for phase-aware suppression (kg/m^3).
    binodal_T_scale : float
        Binodal sigmoid width in K for H2 miscibility suppression.
        Default 50 K.
    precomputed_densities : dict or None
        Optional pre-computed per-component densities, keyed by EOS name.
        When provided, skips the ``calculate_density()`` call for the
        sigmoid weight computation, avoiding redundant EOS table lookups
        when density was already computed in ``calculate_mixed_density()``.

    Returns
    -------
    float or None
        Dimensionless adiabatic gradient, or None if no component provides it.

    Notes
    -----
    Suppressing a vapor component's nabla_ad means the temperature profile
    ignores the volatile. This is consistent with the density suppression
    (both treat the vapor as structurally absent) but differs from reality
    where vapor affects heat transport. This is a known limitation.
    """
    from .eos_functions import calculate_density

    if mixture.is_single():
        return _nabla_ad_for_component(
            mixture.components[0],
            pressure,
            temperature,
            material_dictionaries,
            interpolation_functions,
            solidus_func,
            liquidus_func,
        )

    # Multi-component weighted average with condensed-phase suppression
    weighted_sum = 0.0
    weight_total = 0.0

    for eos_name, w_i in zip(mixture.components, mixture.fractions):
        if w_i <= 0:
            continue
        # Use pre-computed density if available, otherwise compute it
        if precomputed_densities is not None and eos_name in precomputed_densities:
            rho_i = precomputed_densities[eos_name]
        else:
            mzf = _get_mushy_zone_factor(eos_name, mushy_zone_factors)
            rho_i = calculate_density(
                pressure,
                material_dictionaries,
                eos_name,
                temperature,
                solidus_func,
                liquidus_func,
                interpolation_functions,
                mzf,
            )
        if rho_i is not None and np.isfinite(rho_i) and rho_i > 0:
            rho_min_i = _COMPONENT_RHO_MIN.get(eos_name, condensed_rho_min)
            rho_scale_i = _COMPONENT_RHO_SCALE.get(eos_name, condensed_rho_scale)
            sigma_i = _condensed_weight(rho_i, rho_min_i, rho_scale_i)
            sigma_i *= _binodal_factor(
                eos_name, w_i, mixture, pressure, temperature, binodal_T_scale
            )
        else:
            sigma_i = 0.0
        w_eff = w_i * sigma_i
        if w_eff <= 0:
            continue
        nabla = _nabla_ad_for_component(
            eos_name,
            pressure,
            temperature,
            material_dictionaries,
            interpolation_functions,
            solidus_func,
            liquidus_func,
        )
        if nabla is not None and np.isfinite(nabla) and nabla >= 0:
            weighted_sum += w_eff * nabla
            weight_total += w_eff

    if weight_total <= 0:
        return None

    # Renormalize by actual weight of contributing components
    return weighted_sum / weight_total

any_component_is_tdep(layer_mixtures)

Check if any component in any layer uses a T-dependent EOS.

Parameters:

Name Type Description Default
layer_mixtures dict

Per-layer LayerMixture objects.

required

Returns:

Type Description
bool
Source code in src/zalmoxis/mixing.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def any_component_is_tdep(
    layer_mixtures: dict[str, LayerMixture],
) -> bool:
    """Check if any component in any layer uses a T-dependent EOS.

    Parameters
    ----------
    layer_mixtures : dict
        Per-layer LayerMixture objects.

    Returns
    -------
    bool
    """
    return any(mix.has_tdep() for mix in layer_mixtures.values())