aeromaps.core.gemseo¶
A discipline interfacing a Python function.
Tuning an MDAChain (three traps, all silent)
-
tolerance,max_mda_iterandlog_convergenceare tuned on the chain::process.mda_chain.settings.tolerance = 1e-8
Never on an inner MDA. MDAChain_Settings cascades those three down to the inner
MDAs from a pydantic validator that re-runs on any assignment to the chain's
settings -- including the initialize_defaults = False that MDAChain.execute
performs on its first run. A value written on inner_mdas[i].settings is therefore
reverted to the chain's the moment the chain executes.
-
over_relaxation_factorandacceleration_methodare the opposite case. They do not belong toBaseMDASettings, so the chain neither forwards nor cascades them: passed asMDAChainkwargs they configure the outer chain alone and are ignored by the solver that actually iterates. At construction they go throughinner_mda_settings; afterwards, through the inner solver's properties::process.mda_chain.inner_mdas[0].over_relaxation_factor = 0.7
BaseMDASolver.__init__ builds its RelaxationAcceleration from the settings
once and never re-reads them, so assigning to inner_mdas[i].settings.* does
nothing at all.
- A chain that does not converge still returns a full set of outputs, and GEMSEO only
logs a warning. :func:
check_mda_convergenceturns that into an error -- including the case where the residual does reach the tolerance because the coupling variables have gone NaN and a dead component differences against itself to exactly zero.
Damping and acceleration are worth reaching for: on
tests/tested_configs/config_elasticity_demand.yaml the same solution is reached in 109
iterations by default, 65 at over_relaxation_factor=0.7 and 46 with
acceleration_method="Alternate2Delta".
A model holds a coupling inside a physical domain by declaring _coupling_bounds, which
:func:apply_coupling_bounds hands to BaseMDASolver.set_bounds(). That governs the
iterate carried between iterations, not a value a producer passes straight to a consumer
within one Gauss-Seidel sweep -- see RPKElasticity.AIRFARE_BOUNDS_RELATIVE. It works
only while missing values travel outside the vector, as
:func:freeze_nan_masks_after_first_sweep arranges; :class:CustomDataConverter describes
the in-band sentinel used everywhere else.
CustomDataConverter ¶
Bases: SimpleGrammarDataConverter
Converts pd.Series couplings to/from the flat arrays GEMSEO iterates on.
Missing values travel in a frozen mask, not in the vector
While a mask is in force for the running solve (see
:func:freeze_nan_masks_after_first_sweep) an undefined position is written as
DEAD_FILL and restored to NaN on the way out. Both sides of the residual carry the
same constant, so the position differences to exactly zero and contributes nothing,
with no 1e6-magnitude number in a vector the solver may blend, project or normalise. A
NaN where the mask says a value is defined is recorded by
:func:record_nan_intrusion and reported by :func:check_mda_convergence.
Before the first sweep, outside a solve, or for a coupling whose length changed, the sentinel below is used instead.
The -999999 sentinel
GEMSEO has no notion of a missing value: there is no NaN handling anywhere in
gemseo/mda, gemseo/core/grammars or gemseo/core/data_converters, and the
converter API is a pure value/array shim with no place to declare one. A NaN reaching
the residual is therefore fatal to the whole convergence machinery, not merely to its
reporting::
norm([1e-12, nan, 3e-12]) -> nan
nan <= tolerance -> False # convergence can never be declared
nan >= previous_residual -> False # nor can the divergence guard ever fire
Hence the sentinel. NaN is mapped to -999999 on the way in and back to NaN on the
way out, so the mask travels inside the data and survives the slicing, concatenation
and reassembly GEMSEO performs on the coupling vector. Two NaN then difference to
exactly zero and the variable contributes nothing to the residual, which is the
intended behaviour: AeroMAPS series are legitimately NaN over the historical years and
for pathways a scenario does not use.
What it costs, measured on config_elasticity_demand.yaml (186 couplings, 13166
residual components): 2994 components (22.7%) are NaN on both sides and therefore
identically zero at every iteration. A further 7343 (55.8%) are zero because the
coupling genuinely does not move -- the ASK of an aircraft type absent from the
scenario, or the years before the price-elasticity loop starts. Only 21.5% of the
residual vector describes a system that is actually coupled.
That dilution is harmless with the default INITIAL_RESIDUAL_NORM scaling, which is
a ratio: a dead component drops out of the numerator and the denominator alike. It is
not harmless under N_COUPLING_VARIABLES, which divides by sqrt(n) over the
full vector and would therefore loosen the criterion by a factor of about 2.2 here.
Two traps
Both follow from the flag riding in the numeric channel, and both are why the mask exists. They apply wherever no mask is in force.
-
BaseMDASolver.set_bounds()-- GEMSEO's native way to keep an iterate inside a physical domain -- is incompatible with any coupling that carries a NaN. Bounds are enforced by projecting the whole iterate (SequenceTransformer._project), so-999999is clipped up to the lower bound, stops matching== -999999, and is fed to the disciplines as a real value -- while the output DataFrames look untouched, because each discipline recomputes its NaN output every iteration. Bounding a NaN-carrying airfare on tutorial 08 turns a 9-iteration solve at 1.27e-11 into 20 iterations at 6.88e-07, on a scenario with no excursion to bound. -
Blending is safe only while the NaN pattern is stable. Damping and acceleration recombine successive iterates, and mixing a sentinel with a real value yields an arbitrary number of order 1e5-1e6 that no longer maps back to NaN. In the nominal regime the pattern does not move,
old == new == -999999, and the blend is the identity -- damping and acceleration are then not merely safe but markedly faster (109 iterations, versus 65 atover_relaxation_factor=0.7and 46 withacceleration_method="Alternate2Delta", for identical results). Under the mask there is nothing of that magnitude to blend.
Known limitations
_series_names, _series_indexes and _list_names are class attributes
mutated through self, so they are shared by every converter instance in the
interpreter and keyed by bare variable name. Two processes with different horizons in
one session overwrite each other's index; if the lengths match, the years are silently
mislabelled rather than raising. The masks are not shared this way -- see
:func:freeze_nan_masks_after_first_sweep.
Where no mask is in force, a genuine value of exactly -999999.0 is read back as
NaN.
NAN_SENTINEL
class-attribute
instance-attribute
¶
NAN_SENTINEL = -999999.0
The in-band sentinel, used only where no frozen mask is available.
DEAD_FILL
class-attribute
instance-attribute
¶
DEAD_FILL = 0.0
What a masked-out position carries in the vector.
Any constant works: both sides of the residual carry the same one, so a masked position differences to exactly zero.
get_value_size
classmethod
¶
get_value_size(name, value)
Return the size of a data value.
The size is typically what is returned by ndarray.size or len(list).
The size of a number is 1.
Args: name: The data name. value: The data value to get the size from.
Returns: The size.
Source code in aeromaps/core/gemseo.py
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 | |
AeroMAPSAutoModelWrapper ¶
AeroMAPSAutoModelWrapper(model)
Bases: _CouplingSeedMixin, AutoPyDiscipline
Wraps the AeroMAPSModel class into a discipline. Inputs and outputs are automatically declared from the model's compute() function signature.
Source code in aeromaps/core/gemseo.py
545 546 547 548 549 550 551 552 553 554 555 556 557 | |
AeroMAPSCustomModelWrapper ¶
AeroMAPSCustomModelWrapper(model)
Bases: _CouplingSeedMixin, Discipline
Wraps the AeroMAPSModel class into a discipline. Inputs and outputs are declared through the attributes 'input_names' and 'output_names' of the model.
Source code in aeromaps/core/gemseo.py
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 | |
MDAConvergenceError ¶
Bases: RuntimeError
Raised when an MDA stopped before reaching its convergence tolerance.
The results of such a run are not a solution of the coupled system: the coupling variables are still moving when the solver gives up. Reporting this as an error rather than a warning is deliberate -- a silently unconverged run is indistinguishable from a converged one in the output DataFrames.
disable_gemseo_execution_statistics ¶
disable_gemseo_execution_statistics()
Disable GEMSEO's execution statistics shared memory.
GEMSEO's ExecutionStatistics creates semaphores for each discipline via multiprocessing.Value(). With many disciplines (20+ regions × 50+ models), this exhausts macOS semaphore limits (kern.sysv.shmmni=32).
This function patches ExecutionStatistics to use regular Python attributes instead of shared memory, avoiding semaphore creation.
Safe to call multiple times (only patches once).
TODO: Investigate possibilities with gemseo.configure ?¶
Source code in aeromaps/core/gemseo.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
nan_mask ¶
nan_mask(name, shape)
The mask in force for name, or None to fall back to the sentinel.
A shape mismatch also returns None: a coupling whose length changed mid-solve is a
different bug (see tests/core/test_mda_input_mutation.py) and must not be
silently re-masked here.
Source code in aeromaps/core/gemseo.py
191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
record_nan_intrusion ¶
record_nan_intrusion(name, count)
Note a NaN at a position the frozen mask says carries a value.
That is the "converged on NaN" condition: a coupling that held a value after the first
sweep no longer does, so the state the solver measures is not a solution. Recorded
rather than raised, so :func:check_mda_convergence reports it under the process'
on_mda_failure policy like every other convergence failure here.
Source code in aeromaps/core/gemseo.py
207 208 209 210 211 212 213 214 215 216 217 | |
nan_intrusions ¶
nan_intrusions(mda)
The NaN intrusions recorded during mda's last solve.
Source code in aeromaps/core/gemseo.py
220 221 222 | |
freeze_nan_masks_after_first_sweep ¶
freeze_nan_masks_after_first_sweep(mda_chain)
Make every solver in mda_chain carry a frozen missing-value mask.
AeroMAPS series are legitimately undefined over the historical years, and a coupling
belonging to a pathway a scenario does not use is undefined throughout. GEMSEO has no
notion of a missing value; the alternative to a mask is the in-band -999999
sentinel of :class:CustomDataConverter, a flag in the numeric channel that the
solver may blend, scale, project or normalise away. The mask holds the same
information beside the vector, so the vector carries only real numbers.
It is taken after the solver's first complete sweep and held fixed for the rest of
that solve. MDAGaussSeidel._pre_solve runs every discipline once, before _solve
and therefore before any residual: the first moment at which every coupling has been
written by its real producer. Nothing earlier serves -- the first value a coupling is
converted from is usually the length-1 grammar placeholder, and the first full-length
one may be a _coupling_defaults seed that is NaN-free where the real series is not.
Nor does the process horizon: the undefined prefix runs 19 to 21 years depending on the
variable, so the window is per variable, not per process.
Frozen per solve, not per session, since a later compute() may be a different
scenario. A solver whose _pre_solve does not sweep simply keeps the sentinel.
Source code in aeromaps/core/gemseo.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | |
apply_coupling_bounds ¶
apply_coupling_bounds(mda_chain, disciplines, namespace='')
Hand the solver the physical domain of every coupling that declares one.
A model declares its domain as self._coupling_bounds = {name: (low, high)} in
_initialize_df, next to _coupling_defaults, and nothing else in the model
refers to it again: enforcement belongs to the algorithm. BaseMDASolver.set_bounds
projects the iterate itself, so the value a discipline receives is the value the
residual is formed on -- which a model clipping its own input cannot guarantee (see
RPKElasticity.AIRFARE_BOUNDS_RELATIVE).
Bounds are silently ignored for any name the solver is not actually resolving
(BaseMDASolver.set_bounds filters on _resolved_variable_names), so a scenario
whose chain does not contain the coupling is unaffected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mda_chain
|
The chain to configure. |
required | |
disciplines
|
The wrapped disciplines to collect declarations from. |
required | |
namespace
|
str
|
Prefix applied to the variable names, for multi-regional chains where the same
model appears once per region as |
''
|
Returns:
| Type | Description |
|---|---|
bounds
|
What was handed to the chain, for logging and testing. |
Source code in aeromaps/core/gemseo.py
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 | |
apply_namespace_to_discipline ¶
apply_namespace_to_discipline(discipline, namespace)
Apply a namespace prefix to all inputs and outputs of a GEMSEO discipline.
This function creates a deep copy of the discipline and applies the namespace to isolate regional I/O variables. This is essential for multi-regional scenarios where multiple instances of the same discipline must coexist without variable conflicts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
discipline
|
Discipline
|
The GEMSEO discipline to namespace. Will be deep-copied to avoid modifying the original. |
required |
namespace
|
str
|
The namespace prefix to apply (e.g., "FR", "DE"). Variables will be renamed from "var_name" to "{namespace}:var_name". |
required |
Returns:
| Type | Description |
|---|---|
Discipline
|
A new discipline instance with namespaced inputs and outputs. |
Notes
The deep copy is necessary because add_namespace_to_input/output() modifies
the discipline in place. Without it, we would modify the original disciplines
stored in the process objects.
Examples:
>>> from aeromaps.core.gemseo import apply_namespace_to_discipline
>>> namespaced_disc = apply_namespace_to_discipline(discipline, "FR")
>>> # Now inputs/outputs are prefixed: "co2_emissions" -> "FR:co2_emissions"
Source code in aeromaps/core/gemseo.py
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 | |
apply_namespace_to_disciplines ¶
apply_namespace_to_disciplines(disciplines, namespace)
Apply a namespace to a list of disciplines.
Convenience function to namespace multiple disciplines at once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
disciplines
|
list[Discipline]
|
List of GEMSEO disciplines to namespace. |
required |
namespace
|
str
|
The namespace prefix to apply. |
required |
Returns:
| Type | Description |
|---|---|
list[Discipline]
|
List of new discipline instances with namespaced I/O. |
Source code in aeromaps/core/gemseo.py
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 | |
build_namespaced_inputs ¶
build_namespaced_inputs(parameters, namespace)
Build a namespaced input dictionary from an AeroMAPS parameters object.
This function converts all parameters into a dictionary with namespaced keys, suitable for execution of a multi-regional MDAChain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameters
|
AeroMAPS Parameters object containing model inputs. |
required | |
namespace
|
str
|
The namespace prefix to apply to all parameter names. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Dictionary with namespaced keys mapping to parameter values.
E.g., {"FR:rpk_init": |
Source code in aeromaps/core/gemseo.py
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 | |
check_mda_convergence ¶
check_mda_convergence(
mda_chain, context="", on_failure="raise"
)
Check that every solver inside mda_chain reached its tolerance.
A chain with no strongly coupled disciplines has no solver to check and always passes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mda_chain
|
The executed |
required | |
context
|
str
|
Prefix identifying the run in the message, e.g. a region name. |
''
|
on_failure
|
str
|
|
'raise'
|
Returns:
| Type | Description |
|---|---|
failures
|
One message per solver that did not converge; empty if all did. |
Source code in aeromaps/core/gemseo.py
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 | |