gpuwm-global 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arwen_global/__init__.py +61 -0
- arwen_global/__main__.py +4 -0
- arwen_global/_version.py +35 -0
- arwen_global/abi_fast_model.py +456 -0
- arwen_global/abi_operator.py +1214 -0
- arwen_global/abi_radiance_operator.py +714 -0
- arwen_global/abi_reference.py +722 -0
- arwen_global/analysis_fetch.py +188 -0
- arwen_global/analysis_initial.py +1127 -0
- arwen_global/assimilate.py +2107 -0
- arwen_global/bands.py +1035 -0
- arwen_global/cards.py +1606 -0
- arwen_global/checkpoint.py +651 -0
- arwen_global/cli.py +2930 -0
- arwen_global/column_sounding.py +488 -0
- arwen_global/config.py +1535 -0
- arwen_global/configs/arwen_global_gdas_t255_imex_dry.toml +61 -0
- arwen_global/configs/arwen_global_gdas_t255_imex_dry_dt40.toml +61 -0
- arwen_global/configs/arwen_global_gdas_t255_jet48_24h.toml +133 -0
- arwen_global/configs/arwen_global_gdas_t255_native_24h.toml +135 -0
- arwen_global/configs/arwen_global_gdas_t255_native_24h_bare.toml +89 -0
- arwen_global/configs/arwen_global_gdas_t255_native_closure_24h.toml +144 -0
- arwen_global/configs/arwen_global_gdas_t255_native_imex_24h.toml +129 -0
- arwen_global/configs/arwen_global_gdas_t255_native_imex_24h_dt120.toml +119 -0
- arwen_global/configs/arwen_global_gdas_t255_native_imex_24h_dt135.toml +120 -0
- arwen_global/configs/arwen_global_gdas_t255_native_imex_wall.toml +113 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h.toml +138 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h_alpha050.toml +133 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h_alpha053.toml +133 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h_alpha060.toml +133 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h_dt100.toml +130 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h_dt200.toml +130 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h_dt450.toml +130 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h_dt600.toml +130 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_24h_fluxform.toml +147 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_6h.toml +135 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_6h_unclipped.toml +140 -0
- arwen_global/configs/arwen_global_gdas_t255_native_sl_si_wall.toml +122 -0
- arwen_global/configs/arwen_global_gdas_t255_sl_si_dry.toml +66 -0
- arwen_global/configs/arwen_global_gdas_t383_native_24h.toml +90 -0
- arwen_global/configs/arwen_global_gdas_t383_native_imex_24h.toml +99 -0
- arwen_global/configs/arwen_global_gdas_t383_native_imex_24h_dt80.toml +114 -0
- arwen_global/configs/arwen_global_gdas_t383_native_sl_si_24h.toml +114 -0
- arwen_global/configs/arwen_global_gdas_t383_native_sl_si_wall.toml +122 -0
- arwen_global/configs/arwen_global_gdas_t533_24h.toml +61 -0
- arwen_global/configs/arwen_global_gdas_t533_native_24h.toml +90 -0
- arwen_global/configs/arwen_global_gdas_t533_native_imex_24h.toml +131 -0
- arwen_global/configs/arwen_global_gdas_t533_native_sl_si_24h.toml +114 -0
- arwen_global/configs/arwen_global_gdas_t533_native_sl_si_wall.toml +122 -0
- arwen_global/configs/arwen_global_gdas_t63_48h.toml +60 -0
- arwen_global/configs/arwen_global_gdas_t799_native_sl_si_wall.toml +122 -0
- arwen_global/configs/arwen_global_held_suarez_t42_imex.toml +75 -0
- arwen_global/configs/arwen_global_held_suarez_t42_sl_si.toml +79 -0
- arwen_global/configs/arwen_global_hybrid_6h_reference.toml +69 -0
- arwen_global/configs/arwen_global_jw06_t255_imex.toml +71 -0
- arwen_global/configs/arwen_global_jw06_t255_imex_steady.toml +71 -0
- arwen_global/configs/arwen_global_jw06_t255_sl_si.toml +76 -0
- arwen_global/configs/arwen_global_jw06_t255_sl_si_steady.toml +76 -0
- arwen_global/configs/arwen_global_jw06_t85_imex.toml +63 -0
- arwen_global/configs/arwen_global_jw06_t85_imex_steady.toml +63 -0
- arwen_global/configs/arwen_global_jw06_t85_sl_si.toml +68 -0
- arwen_global/configs/arwen_global_jw06_t85_sl_si_steady.toml +68 -0
- arwen_global/configs/arwen_global_level5_native_smoke.toml +73 -0
- arwen_global/configs/arwen_global_moist_smoke.toml +84 -0
- arwen_global/configs/arwen_global_native_physics_bridge_example.toml +63 -0
- arwen_global/configs/arwen_global_t21_baroclinic_ten_day.toml +68 -0
- arwen_global/configs/arwen_global_t255_quickstart.toml +137 -0
- arwen_global/configs/arwen_global_t85_vertical_modes_ab.toml +87 -0
- arwen_global/configs/global_spectral_held_suarez_smoke.toml +42 -0
- arwen_global/configs/global_spectral_primitive_smoke.toml +42 -0
- arwen_global/configs/global_spectral_williamson2.toml +36 -0
- arwen_global/configs_dir.py +110 -0
- arwen_global/constants.py +99 -0
- arwen_global/core/__init__.py +46 -0
- arwen_global/core/gf.py +527 -0
- arwen_global/core/kernels/__init__.py +170 -0
- arwen_global/core/kernels/common.cuh +7 -0
- arwen_global/core/kernels/gf.cu +4325 -0
- arwen_global/core/kernels/glibc_flt32.cuh +649 -0
- arwen_global/core/kernels/morrison.cu +1207 -0
- arwen_global/core/kernels/noah.cu +1526 -0
- arwen_global/core/kernels/ntiedtke.cu +3840 -0
- arwen_global/core/kernels/rrtmgp_cloud.cu +131 -0
- arwen_global/core/kernels/rrtmgp_gas.cu +411 -0
- arwen_global/core/kernels/rrtmgp_mcica.cu +159 -0
- arwen_global/core/kernels/rrtmgp_planck_common.cuh +94 -0
- arwen_global/core/kernels/rrtmgp_rte.cu +574 -0
- arwen_global/core/kernels/rrtmgp_validation.cu +119 -0
- arwen_global/core/kernels/sfclay.cu +554 -0
- arwen_global/core/kernels/ysu.cu +728 -0
- arwen_global/core/kernels/ysu_validation.cu +34 -0
- arwen_global/core/landuse.py +468 -0
- arwen_global/core/morrison.py +244 -0
- arwen_global/core/noah.py +513 -0
- arwen_global/core/npref.py +11296 -0
- arwen_global/core/ntiedtke.py +1590 -0
- arwen_global/core/physics.py +5486 -0
- arwen_global/core/physics_inventory.py +370 -0
- arwen_global/core/rrtmgp.py +5125 -0
- arwen_global/core/sfclay.py +181 -0
- arwen_global/core/ysu.py +264 -0
- arwen_global/core/ysu_contract.py +57 -0
- arwen_global/cycle.py +1233 -0
- arwen_global/da/__init__.py +106 -0
- arwen_global/da/analysis.py +1993 -0
- arwen_global/da/ensemble.py +767 -0
- arwen_global/da/letkf_point.py +1310 -0
- arwen_global/da/localisation.py +518 -0
- arwen_global/da/measure.py +166 -0
- arwen_global/da/observation_errors.py +131 -0
- arwen_global/da/observations.py +326 -0
- arwen_global/da/operators.py +685 -0
- arwen_global/da/options.py +819 -0
- arwen_global/da/osse.py +1189 -0
- arwen_global/da/perturbations.py +661 -0
- arwen_global/da/static_covariance.py +724 -0
- arwen_global/da/window.py +234 -0
- arwen_global/da_anchor.py +312 -0
- arwen_global/da_control.py +243 -0
- arwen_global/da_door.py +1105 -0
- arwen_global/da_filter.py +1597 -0
- arwen_global/da_scorecard.py +967 -0
- arwen_global/da_static.py +247 -0
- arwen_global/da_streams.py +697 -0
- arwen_global/da_twin.py +533 -0
- arwen_global/da_window.py +75 -0
- arwen_global/data/authorities/rw-wps-ecmwf-open-data-global-forecast-grib2.mapping.json +174 -0
- arwen_global/data/authorities/rw-wps-gdas-global-analysis-grib2.mapping.json +932 -0
- arwen_global/data/authorities/rw-wps-gdas-pgrb2-0p25-microwave-columns.mapping.json +1026 -0
- arwen_global/data/authorities/rw-wps-gfs-pgrb2-0p25-cloud-cover.mapping.json +1084 -0
- arwen_global/data/authorities/rw-wps-gfs-surface-flux-grib2.mapping.json +1090 -0
- arwen_global/data/authorities/rw-wps-gfs-surface-state-grib2.mapping.json +1046 -0
- arwen_global/data/door-pins.json +125 -0
- arwen_global/data/engine-seam.json +285 -0
- arwen_global/data/noah_tables/GENPARM.TBL +36 -0
- arwen_global/data/noah_tables/LANDUSE.TBL +586 -0
- arwen_global/data/noah_tables/PROVENANCE.md +28 -0
- arwen_global/data/noah_tables/SOILPARM.TBL +45 -0
- arwen_global/data/noah_tables/VEGPARM.TBL +291 -0
- arwen_global/data/render-catalog-global.json +3247 -0
- arwen_global/data/static-covariance-v1-receipt.json +503 -0
- arwen_global/data/static-covariance-v1.npz +0 -0
- arwen_global/device_memory.py +463 -0
- arwen_global/diurnal_phase.py +1099 -0
- arwen_global/doctor.py +686 -0
- arwen_global/doors.py +793 -0
- arwen_global/dynamics.py +3646 -0
- arwen_global/engine_compat.py +331 -0
- arwen_global/engine_seam.py +149 -0
- arwen_global/export.py +317 -0
- arwen_global/fetch_doors.py +281 -0
- arwen_global/go_door.py +192 -0
- arwen_global/imex.py +410 -0
- arwen_global/initial_conditions.py +161 -0
- arwen_global/insitu/__init__.py +39 -0
- arwen_global/insitu/budgets.py +285 -0
- arwen_global/insitu/capture.py +221 -0
- arwen_global/insitu/energy.py +425 -0
- arwen_global/insitu/ledger.py +567 -0
- arwen_global/insitu/options.py +91 -0
- arwen_global/insitu/spectra.py +95 -0
- arwen_global/insitu/tripwires.py +465 -0
- arwen_global/mapped_source_compat.py +503 -0
- arwen_global/massflux_diagnostic.py +731 -0
- arwen_global/microwave/__init__.py +88 -0
- arwen_global/microwave/__main__.py +266 -0
- arwen_global/microwave/absorption.py +211 -0
- arwen_global/microwave/atms_bridge.py +253 -0
- arwen_global/microwave/atms_fetch.py +568 -0
- arwen_global/microwave/calibrate.py +350 -0
- arwen_global/microwave/channels.py +101 -0
- arwen_global/microwave/columns.py +282 -0
- arwen_global/microwave/emissivity.py +105 -0
- arwen_global/microwave/entry.py +763 -0
- arwen_global/microwave/rte.py +447 -0
- arwen_global/microwave/score.py +568 -0
- arwen_global/migration.py +407 -0
- arwen_global/native_qualification.py +376 -0
- arwen_global/obs_doors.py +201 -0
- arwen_global/obs_operators.py +618 -0
- arwen_global/obs_pack.py +152 -0
- arwen_global/obs_scorecard.py +1637 -0
- arwen_global/obs_streams.py +1360 -0
- arwen_global/obs_table.py +854 -0
- arwen_global/pbl_free_atmosphere.py +1001 -0
- arwen_global/physics/__init__.py +18 -0
- arwen_global/physics/arwen_bridge.py +196 -0
- arwen_global/physics/arwen_massflux.py +844 -0
- arwen_global/physics/banding.py +201 -0
- arwen_global/physics/builtin_adapters.py +186 -0
- arwen_global/physics/exchange.py +203 -0
- arwen_global/physics/frozen_surface.py +464 -0
- arwen_global/physics/native_batch.py +384 -0
- arwen_global/physics/native_options.py +410 -0
- arwen_global/physics/native_runtime.py +1788 -0
- arwen_global/physics/native_state.py +297 -0
- arwen_global/physics/native_suite.py +634 -0
- arwen_global/physics/provenance.py +126 -0
- arwen_global/physics/reference.py +1596 -0
- arwen_global/physics/registry.py +224 -0
- arwen_global/physics/surface_diagnostics.py +217 -0
- arwen_global/pins.py +425 -0
- arwen_global/profile.py +552 -0
- arwen_global/radiance_streams.py +1013 -0
- arwen_global/radiance_tables/abi-fast-model.json +5321 -0
- arwen_global/radiance_tables/abi-operator-entries.json +351 -0
- arwen_global/radiance_tables/atms-noaa-20-2026-09-01.entry.json +325 -0
- arwen_global/radiance_tables/atms-noaa-21-2026-09-01.entry.json +325 -0
- arwen_global/radiation_scorecard.py +1392 -0
- arwen_global/receipt.py +174 -0
- arwen_global/regional/__init__.py +30 -0
- arwen_global/regional/artifact.py +379 -0
- arwen_global/regional/interpolation.py +234 -0
- arwen_global/regional/runtime.py +405 -0
- arwen_global/regional/translate.py +301 -0
- arwen_global/render_door.py +285 -0
- arwen_global/runner.py +1673 -0
- arwen_global/runplan.py +3005 -0
- arwen_global/semi_implicit.py +986 -0
- arwen_global/semilag/__init__.py +115 -0
- arwen_global/semilag/_cuda.py +60 -0
- arwen_global/semilag/cases.py +700 -0
- arwen_global/semilag/interpolate.py +479 -0
- arwen_global/semilag/kernels.cu +596 -0
- arwen_global/semilag/options.py +229 -0
- arwen_global/semilag/pins.py +64 -0
- arwen_global/semilag/rhs.py +220 -0
- arwen_global/semilag/state.py +102 -0
- arwen_global/semilag/step.py +592 -0
- arwen_global/semilag/tables.py +270 -0
- arwen_global/semilag/tracers.py +303 -0
- arwen_global/semilag/trajectory.py +963 -0
- arwen_global/semilag/vectors.py +147 -0
- arwen_global/sizing.py +3879 -0
- arwen_global/sources.py +565 -0
- arwen_global/spectral/__init__.py +55 -0
- arwen_global/spectral/__main__.py +4 -0
- arwen_global/spectral/backend.py +100 -0
- arwen_global/spectral/checkpoint.py +237 -0
- arwen_global/spectral/cli.py +420 -0
- arwen_global/spectral/compression.py +612 -0
- arwen_global/spectral/config.py +507 -0
- arwen_global/spectral/constants.py +16 -0
- arwen_global/spectral/diffusion.py +40 -0
- arwen_global/spectral/eddy_viscosity.py +193 -0
- arwen_global/spectral/export.py +211 -0
- arwen_global/spectral/fused.py +216 -0
- arwen_global/spectral/grid.py +231 -0
- arwen_global/spectral/initial_conditions.py +97 -0
- arwen_global/spectral/legendre.py +190 -0
- arwen_global/spectral/pins.py +72 -0
- arwen_global/spectral/primitive.py +437 -0
- arwen_global/spectral/receipt.py +47 -0
- arwen_global/spectral/runner.py +614 -0
- arwen_global/spectral/sampling.py +571 -0
- arwen_global/spectral/shallow_water.py +147 -0
- arwen_global/spectral/state.py +74 -0
- arwen_global/spectral/timestep.py +59 -0
- arwen_global/spectral/transform.py +1694 -0
- arwen_global/spectral/vector.py +137 -0
- arwen_global/spill.py +781 -0
- arwen_global/state.py +357 -0
- arwen_global/statics.py +972 -0
- arwen_global/statics_rows.py +172 -0
- arwen_global/status.py +162 -0
- arwen_global/storm_reader.py +845 -0
- arwen_global/surface_energy.py +1122 -0
- arwen_global/surface_seeding.py +694 -0
- arwen_global/testcases.py +373 -0
- arwen_global/transport.py +1191 -0
- arwen_global/trigger_diagnostic.py +717 -0
- arwen_global/tui_worker.py +153 -0
- arwen_global/upper_air_scorecard.py +2048 -0
- arwen_global/vertical.py +624 -0
- arwen_global/water.py +221 -0
- arwen_global/water_budget.py +1393 -0
- arwen_global/wrfout_export.py +515 -0
- gpuwm_global-0.1.0.dist-info/METADATA +476 -0
- gpuwm_global-0.1.0.dist-info/RECORD +284 -0
- gpuwm_global-0.1.0.dist-info/WHEEL +5 -0
- gpuwm_global-0.1.0.dist-info/entry_points.txt +2 -0
- gpuwm_global-0.1.0.dist-info/licenses/LICENSE +202 -0
- gpuwm_global-0.1.0.dist-info/licenses/NOTICE +98 -0
- gpuwm_global-0.1.0.dist-info/top_level.txt +1 -0
arwen_global/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Arwen Global: moist spectral model, native physics, and parent bridge."""
|
|
2
|
+
from ._version import (
|
|
3
|
+
CONSOLE_SCRIPT,
|
|
4
|
+
DISTRIBUTION_NAME,
|
|
5
|
+
IMPORT_NAME,
|
|
6
|
+
__version__,
|
|
7
|
+
)
|
|
8
|
+
from .config import ArwenGlobalConfig, load_config
|
|
9
|
+
from .dynamics import MoistHybridModel
|
|
10
|
+
from .export import export_parent, read_parent_export
|
|
11
|
+
from .initial_conditions import analytic_initial_state
|
|
12
|
+
from .migration import migrate_level4_checkpoint
|
|
13
|
+
from .native_qualification import qualify_native_adapter
|
|
14
|
+
from .pins import PINS_HASH, pins_hash, pins_receipt
|
|
15
|
+
from .regional import (
|
|
16
|
+
attach_parent_series,
|
|
17
|
+
install_initial_and_attach_parent,
|
|
18
|
+
translate_parent_to_regional_frame,
|
|
19
|
+
write_regional_target_from_state,
|
|
20
|
+
)
|
|
21
|
+
from .runner import build_model_and_cold_state, build_transform, run
|
|
22
|
+
from .semi_implicit import BarotropicSemiImplicit, VerticalModeSemiImplicit
|
|
23
|
+
from .state import (
|
|
24
|
+
ArwenGlobalState,
|
|
25
|
+
MoistHybridState,
|
|
26
|
+
PhysicsState,
|
|
27
|
+
SurfaceState,
|
|
28
|
+
)
|
|
29
|
+
from .vertical import HybridCoordinate
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"CONSOLE_SCRIPT",
|
|
33
|
+
"DISTRIBUTION_NAME",
|
|
34
|
+
"IMPORT_NAME",
|
|
35
|
+
"__version__",
|
|
36
|
+
"ArwenGlobalConfig",
|
|
37
|
+
"ArwenGlobalState",
|
|
38
|
+
"BarotropicSemiImplicit",
|
|
39
|
+
"VerticalModeSemiImplicit",
|
|
40
|
+
"HybridCoordinate",
|
|
41
|
+
"MoistHybridModel",
|
|
42
|
+
"MoistHybridState",
|
|
43
|
+
"PINS_HASH",
|
|
44
|
+
"PhysicsState",
|
|
45
|
+
"SurfaceState",
|
|
46
|
+
"analytic_initial_state",
|
|
47
|
+
"attach_parent_series",
|
|
48
|
+
"build_model_and_cold_state",
|
|
49
|
+
"build_transform",
|
|
50
|
+
"export_parent",
|
|
51
|
+
"install_initial_and_attach_parent",
|
|
52
|
+
"load_config",
|
|
53
|
+
"migrate_level4_checkpoint",
|
|
54
|
+
"pins_hash",
|
|
55
|
+
"pins_receipt",
|
|
56
|
+
"qualify_native_adapter",
|
|
57
|
+
"read_parent_export",
|
|
58
|
+
"run",
|
|
59
|
+
"translate_parent_to_regional_frame",
|
|
60
|
+
"write_regional_target_from_state",
|
|
61
|
+
]
|
arwen_global/__main__.py
ADDED
arwen_global/_version.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""The distribution's identity, read back rather than restated.
|
|
2
|
+
|
|
3
|
+
Kept in its own module so that asking what version is installed does not
|
|
4
|
+
import the model. ``arwen_global/__init__.py`` pulls in the dynamics, the
|
|
5
|
+
regional bridge and the semi-implicit solvers at module scope, all of which
|
|
6
|
+
need the engine; a packaging gate that only wants the number must not need a
|
|
7
|
+
GPU stack to get it.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import PackageNotFoundError
|
|
12
|
+
from importlib.metadata import version as _distribution_version
|
|
13
|
+
|
|
14
|
+
__all__ = ["DISTRIBUTION_NAME", "IMPORT_NAME", "CONSOLE_SCRIPT", "__version__"]
|
|
15
|
+
|
|
16
|
+
#: The name this package is published under.
|
|
17
|
+
DISTRIBUTION_NAME = "gpuwm-global"
|
|
18
|
+
|
|
19
|
+
#: The name a user types in an import line. It is the model's own name and
|
|
20
|
+
#: not the engine's: putting the engine's name in every user import line
|
|
21
|
+
#: would name the wrong owner for code the engine does not carry.
|
|
22
|
+
IMPORT_NAME = "arwen_global"
|
|
23
|
+
|
|
24
|
+
#: The one console script, and the prog name every parser and every refusal
|
|
25
|
+
#: sentence in this package uses.
|
|
26
|
+
CONSOLE_SCRIPT = "gpuwm-global"
|
|
27
|
+
|
|
28
|
+
# Read out of installed metadata. pyproject's [project].version is the single
|
|
29
|
+
# place the number is written; a second literal is a promise to update two
|
|
30
|
+
# files at every cut, and the engine already broke that promise once.
|
|
31
|
+
try:
|
|
32
|
+
__version__ = _distribution_version(DISTRIBUTION_NAME)
|
|
33
|
+
except PackageNotFoundError: # pragma: no cover - an uninstalled source tree
|
|
34
|
+
# Says so rather than inventing a number.
|
|
35
|
+
__version__ = "0+unknown"
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""The ABI clear-sky fast forward model: its coefficient table and its trainer.
|
|
2
|
+
|
|
3
|
+
The operator the ensemble filter runs is Rust (``rw_goes forward``, in
|
|
4
|
+
``tools/rustwx/crates/rw-goes/src/forward.rs``): it takes the model
|
|
5
|
+
columns of a ``gpuwm-da.abi-columns.v2`` stream, evaluates per-layer
|
|
6
|
+
optical depths from a coefficient table, marches the emission radiative
|
|
7
|
+
transfer, converts through the instrument's own band-corrected Planck
|
|
8
|
+
relation and writes brightness temperatures with finite-difference
|
|
9
|
+
Jacobians. This module is the table's trainer and its Python oracle:
|
|
10
|
+
|
|
11
|
+
* the **trainer** fits, per model layer and per band, the layer nadir
|
|
12
|
+
optical depth CRTM (the numerical reference, ``abi_reference``) computed
|
|
13
|
+
on the analysed columns of the case, as a function of named per-layer
|
|
14
|
+
features (the layer's vapor path, its temperature and pressure, the
|
|
15
|
+
slant vapor path above it, its neighbours' vapor) in one of two forms:
|
|
16
|
+
``linear`` (the window band: dry and continuum terms add) or
|
|
17
|
+
``two_term`` (the water-vapor band: ``exp(wet polynomial) + dry``,
|
|
18
|
+
fitted by damped Gauss-Newton on the log residual so the emitting layers
|
|
19
|
+
are fitted to relative accuracy);
|
|
20
|
+
* the **oracle** evaluates a table in numpy exactly as the Rust operator
|
|
21
|
+
does, so a test can pin the Rust to it bit for bit on a small column
|
|
22
|
+
set, and the validation numbers a table carries come from it.
|
|
23
|
+
|
|
24
|
+
The table is tied to the vertical coordinate it was trained on (per-layer
|
|
25
|
+
coefficients); it carries the coordinate and the Rust refuses a column set
|
|
26
|
+
on another. The Planck constants are the instrument's own, read from the
|
|
27
|
+
Level 1b granules (``fk1, fk2, bc1, bc2``); the transmittance model is
|
|
28
|
+
trained on CRTM with the GOES-16 ABI coefficients (the closest public
|
|
29
|
+
set), and the difference to the GOES-18 set is recorded as the sensor term
|
|
30
|
+
(0.04 K in band 13, 0.2 K in band 8 on this case).
|
|
31
|
+
|
|
32
|
+
Nothing here is a data path of the shipped system: the trainer runs once
|
|
33
|
+
per coordinate, the oracle runs in tests.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import datetime as dt
|
|
38
|
+
import hashlib
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any
|
|
43
|
+
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
FAST_MODEL_SCHEMA = "gpuwm-da.abi-fast-model.v1"
|
|
47
|
+
|
|
48
|
+
#: The feature vocabulary, by name. The Rust operator computes exactly
|
|
49
|
+
#: these (``forward.rs::features``); a table may only name terms built
|
|
50
|
+
#: from them.
|
|
51
|
+
FEATURE_NAMES: tuple[str, ...] = (
|
|
52
|
+
"one", "dp", "u", "e", "lnp", "Tn", "lu", "lUs", "Twn", "lP", "lsec", "lu_up", "lu_dn", "Tn_up", "Tn_dn",
|
|
53
|
+
)
|
|
54
|
+
FEATURE_DEFINITIONS: dict[str, str] = {
|
|
55
|
+
"one": "1",
|
|
56
|
+
"dp": "layer thickness, hPa (p_half below minus p_half above)",
|
|
57
|
+
"u": "layer vapor path, q[g/kg] * dp[hPa]",
|
|
58
|
+
"e": "vapor pressure proxy, q[g/kg] * p_full[hPa] * 1.608e-3 (hPa)",
|
|
59
|
+
"lnp": "ln(p_full[hPa])",
|
|
60
|
+
"Tn": "(T[K] - 250) / 50",
|
|
61
|
+
"lu": "ln(max(u, u_floor))",
|
|
62
|
+
"lUs": "ln(max(U_mid, u_floor) * sec(zenith)), U_mid the vapor path from the top of the atmosphere to the layer middle",
|
|
63
|
+
"Twn": "(Tw - 250) / 50, Tw the vapor-path-weighted temperature from the top to the layer middle",
|
|
64
|
+
"lP": "ln(max(Pw, 1e-3)), Pw the vapor-path-weighted pressure from the top to the layer middle (hPa)",
|
|
65
|
+
"lsec": "ln(sec(zenith))",
|
|
66
|
+
"lu_up": "lu of the layer above (the top layer repeats its own)",
|
|
67
|
+
"lu_dn": "lu of the layer below (the bottom layer repeats its own)",
|
|
68
|
+
"Tn_up": "Tn of the layer above",
|
|
69
|
+
"Tn_dn": "Tn of the layer below",
|
|
70
|
+
}
|
|
71
|
+
FEATURE_CONSTANTS = {"t_ref_k": 250.0, "t_scale_k": 50.0, "u_floor": 1.0e-8, "e_factor": 1.608e-3, "pw_floor_hpa": 1.0e-3}
|
|
72
|
+
|
|
73
|
+
#: The window band (13): dry (pressure, temperature) and vapor (continuum,
|
|
74
|
+
#: self and foreign) terms add; linear least squares per layer.
|
|
75
|
+
LINEAR_TERMS_WINDOW: tuple[tuple[str, ...], ...] = (
|
|
76
|
+
("dp",), ("dp", "Tn"), ("dp", "Tn", "Tn"), ("u",), ("u", "e"), ("u", "e", "Tn"), ("u", "lnp"), ("u", "Tn"),
|
|
77
|
+
("u", "Tn", "Tn"), ("u", "u"), ("u", "e", "e"),
|
|
78
|
+
)
|
|
79
|
+
#: The water-vapor band (8): ``exp(wet) + dry``.
|
|
80
|
+
WET_TERMS: tuple[tuple[str, ...], ...] = (
|
|
81
|
+
("one",), ("lu",), ("lu", "lu"), ("lu", "lu", "lu"), ("lnp",), ("Tn",), ("lu", "Tn"), ("lu", "lnp"),
|
|
82
|
+
("Tn", "Tn"), ("lUs",), ("lUs", "lUs"), ("lu", "lUs"), ("Twn",), ("Twn", "lu"), ("lP",), ("lUs", "Tn"),
|
|
83
|
+
("lsec",), ("lsec", "lu"), ("lsec", "lsec"), ("lsec", "lUs"), ("lUs", "lUs", "lUs"), ("lsec", "Tn"),
|
|
84
|
+
("lnp", "lnp"), ("Tn", "Tn", "Tn"), ("lu", "Tn", "Tn"),
|
|
85
|
+
("lu_up",), ("lu_dn",), ("lu_up", "lu"), ("lu_dn", "lu"), ("lu_up", "lu_up"), ("lu_dn", "lu_dn"), ("Tn_up",), ("Tn_dn",),
|
|
86
|
+
("lu_up", "lsec"), ("lu_dn", "lsec"),
|
|
87
|
+
)
|
|
88
|
+
DRY_TERMS: tuple[tuple[str, ...], ...] = (("dp",), ("dp", "Tn"), ("dp", "Tn", "Tn"), ("dp", "lnp"))
|
|
89
|
+
|
|
90
|
+
#: Finite-difference steps the Rust Jacobians use (and the oracle).
|
|
91
|
+
JACOBIAN_STEPS = {"temperature_k": 0.1, "vapor_relative": 0.01, "skin_k": 0.1}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FastModelError(ValueError):
|
|
95
|
+
"""The table cannot be built or applied as asked. Names the breakage."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# features and terms
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def features(temperature_k: np.ndarray, q_gkg: np.ndarray, p_half_hpa: np.ndarray, p_full_hpa: np.ndarray,
|
|
103
|
+
zenith_deg: np.ndarray) -> dict[str, np.ndarray]:
|
|
104
|
+
"""Every named feature, ``(n, nlay)``, top of atmosphere first."""
|
|
105
|
+
c = FEATURE_CONSTANTS
|
|
106
|
+
T = np.asarray(temperature_k, dtype=np.float64)
|
|
107
|
+
q = np.asarray(q_gkg, dtype=np.float64)
|
|
108
|
+
p_half = np.asarray(p_half_hpa, dtype=np.float64)
|
|
109
|
+
p_full = np.asarray(p_full_hpa, dtype=np.float64)
|
|
110
|
+
sec = 1.0 / np.cos(np.radians(np.asarray(zenith_deg, dtype=np.float64)))
|
|
111
|
+
dp = p_half[:, 1:] - p_half[:, :-1]
|
|
112
|
+
u = q * dp
|
|
113
|
+
lnp = np.log(p_full)
|
|
114
|
+
Tn = (T - c["t_ref_k"]) / c["t_scale_k"]
|
|
115
|
+
lu = np.log(np.maximum(u, c["u_floor"]))
|
|
116
|
+
cum = np.cumsum(u, axis=1)
|
|
117
|
+
U_mid = np.maximum(cum - 0.5 * u, c["u_floor"])
|
|
118
|
+
lUs = np.log(U_mid * sec[:, None])
|
|
119
|
+
Tw = (np.cumsum(u * T, axis=1) - 0.5 * u * T) / U_mid
|
|
120
|
+
Twn = (Tw - c["t_ref_k"]) / c["t_scale_k"]
|
|
121
|
+
Pw = (np.cumsum(u * p_full, axis=1) - 0.5 * u * p_full) / U_mid
|
|
122
|
+
lP = np.log(np.maximum(Pw, c["pw_floor_hpa"]))
|
|
123
|
+
lsec = np.log(sec)[:, None] * np.ones_like(u)
|
|
124
|
+
e = q * p_full * c["e_factor"]
|
|
125
|
+
lu_up = np.concatenate([lu[:, :1], lu[:, :-1]], axis=1)
|
|
126
|
+
lu_dn = np.concatenate([lu[:, 1:], lu[:, -1:]], axis=1)
|
|
127
|
+
Tn_up = np.concatenate([Tn[:, :1], Tn[:, :-1]], axis=1)
|
|
128
|
+
Tn_dn = np.concatenate([Tn[:, 1:], Tn[:, -1:]], axis=1)
|
|
129
|
+
return {"one": np.ones_like(u), "dp": dp, "u": u, "e": e, "lnp": lnp, "Tn": Tn, "lu": lu, "lUs": lUs, "Twn": Twn,
|
|
130
|
+
"lP": lP, "lsec": lsec, "lu_up": lu_up, "lu_dn": lu_dn, "Tn_up": Tn_up, "Tn_dn": Tn_dn}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def clip_features(feat: dict[str, np.ndarray], clip: dict[str, dict[str, list[float]]]) -> dict[str, np.ndarray]:
|
|
134
|
+
"""Hold every clipped feature inside the per-layer range the table
|
|
135
|
+
recorded from its training set (the Rust does the same), so a column
|
|
136
|
+
outside the training envelope extrapolates no further than its edge."""
|
|
137
|
+
out = dict(feat)
|
|
138
|
+
for name, bounds in clip.items():
|
|
139
|
+
lo = np.asarray(bounds["min"], dtype=np.float64)[None, :]
|
|
140
|
+
hi = np.asarray(bounds["max"], dtype=np.float64)[None, :]
|
|
141
|
+
out[name] = np.minimum(np.maximum(feat[name], lo), hi)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def design(feat: dict[str, np.ndarray], terms) -> np.ndarray:
|
|
146
|
+
"""``(n, nlay, n_terms)`` products of named features."""
|
|
147
|
+
n, nlay = feat["one"].shape
|
|
148
|
+
X = np.ones((n, nlay, len(terms)))
|
|
149
|
+
for j, term in enumerate(terms):
|
|
150
|
+
for name in term:
|
|
151
|
+
if name not in feat:
|
|
152
|
+
raise FastModelError(f"term {term} names an unknown feature {name!r}; the vocabulary is {FEATURE_NAMES}")
|
|
153
|
+
X[:, :, j] *= feat[name]
|
|
154
|
+
return X
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Planck and the emission march (the arithmetic the Rust carries)
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def planck_radiance(planck: dict, temperature_k: np.ndarray) -> np.ndarray:
|
|
162
|
+
"""Band-corrected Planck radiance, the L1b convention: ``L = fk1 / (exp(fk2 / (bc1 + bc2 T)) - 1)``."""
|
|
163
|
+
teff = planck["bc1"] + planck["bc2"] * np.asarray(temperature_k, dtype=np.float64)
|
|
164
|
+
return planck["fk1"] / (np.exp(planck["fk2"] / teff) - 1.0)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def planck_temperature(planck: dict, radiance: np.ndarray) -> np.ndarray:
|
|
168
|
+
"""The inverse: ``T = (fk2 / ln(fk1 / L + 1) - bc1) / bc2``."""
|
|
169
|
+
return (planck["fk2"] / np.log(planck["fk1"] / np.asarray(radiance, dtype=np.float64) + 1.0) - planck["bc1"]) / planck["bc2"]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def emission_radiance(planck: dict, od_slant: np.ndarray, temperature_k: np.ndarray, skin_k: np.ndarray,
|
|
173
|
+
emissivity: np.ndarray) -> np.ndarray:
|
|
174
|
+
"""The clear-sky emission march CRTM's solver takes (specular
|
|
175
|
+
downwelling along the same slant path): upwelling layer emission plus
|
|
176
|
+
the surface's emission and its reflection of the downwelling."""
|
|
177
|
+
n = od_slant.shape[0]
|
|
178
|
+
cum = np.cumsum(od_slant, axis=1)
|
|
179
|
+
t_top = np.concatenate([np.ones((n, 1)), np.exp(-cum)], axis=1) # top of layer k to space
|
|
180
|
+
B = planck_radiance(planck, temperature_k)
|
|
181
|
+
up = np.sum(B * (t_top[:, :-1] - t_top[:, 1:]), axis=1)
|
|
182
|
+
t_sfc = t_top[:, -1]
|
|
183
|
+
rev = np.cumsum(od_slant[:, ::-1], axis=1)[:, ::-1] # layer k to the surface
|
|
184
|
+
t_bot_sfc = np.concatenate([np.exp(-rev[:, 1:]), np.ones((n, 1))], axis=1)
|
|
185
|
+
down = np.sum(B * (t_bot_sfc - np.exp(-rev)), axis=1)
|
|
186
|
+
return up + emissivity * planck_radiance(planck, skin_k) * t_sfc + (1.0 - emissivity) * down * t_sfc
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# the table: evaluation (the oracle)
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def layer_optical_depth(band_table: dict, feat: dict[str, np.ndarray]) -> np.ndarray:
|
|
194
|
+
"""Nadir layer optical depth ``(n, nlay)`` from a band's table."""
|
|
195
|
+
feat = clip_features(feat, band_table.get("clip", {}))
|
|
196
|
+
layers = band_table["layers"]
|
|
197
|
+
nlay = len(layers)
|
|
198
|
+
if feat["one"].shape[1] != nlay:
|
|
199
|
+
raise FastModelError(f"the columns carry {feat['one'].shape[1]} layers, the table {nlay}")
|
|
200
|
+
if band_table["form"] == "linear":
|
|
201
|
+
X = design(feat, [tuple(t) for t in band_table["terms_linear"]])
|
|
202
|
+
coef = np.asarray([row["linear"] for row in layers]) # (nlay, nt)
|
|
203
|
+
return np.maximum(np.einsum("nkt,kt->nk", X, coef), 0.0)
|
|
204
|
+
if band_table["form"] == "two_term":
|
|
205
|
+
Xw = design(feat, [tuple(t) for t in band_table["terms_wet"]])
|
|
206
|
+
Xd = design(feat, [tuple(t) for t in band_table["terms_dry"]])
|
|
207
|
+
w = np.asarray([row["wet"] for row in layers])
|
|
208
|
+
d = np.asarray([row["dry"] for row in layers])
|
|
209
|
+
cap = np.asarray([row["ln_od_max"] for row in layers])[None, :]
|
|
210
|
+
wet = np.exp(np.minimum(np.einsum("nkt,kt->nk", Xw, w), cap))
|
|
211
|
+
dry = np.maximum(np.einsum("nkt,kt->nk", Xd, d), 0.0)
|
|
212
|
+
return wet + dry
|
|
213
|
+
raise FastModelError(f"unknown table form {band_table['form']!r}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def evaluate(table: dict, band: int, columns: dict, *, emissivity: np.ndarray | None = None) -> dict:
|
|
217
|
+
"""Brightness temperature and the pieces, exactly as ``rw_goes forward``
|
|
218
|
+
computes them. ``columns`` carries ``temperature_k, q_gkg, p_half_hpa,
|
|
219
|
+
p_full_hpa, zenith, skin_k`` and, for the table's own emissivity,
|
|
220
|
+
``land_fraction, landuse_category, sea_ice_fraction, snowh_m``."""
|
|
221
|
+
bt_table = table["bands"][str(int(band))]
|
|
222
|
+
feat = features(columns["temperature_k"], columns["q_gkg"], columns["p_half_hpa"], columns["p_full_hpa"], columns["zenith"])
|
|
223
|
+
od = layer_optical_depth(bt_table, feat)
|
|
224
|
+
sec = 1.0 / np.cos(np.radians(np.asarray(columns["zenith"], dtype=np.float64)))
|
|
225
|
+
if emissivity is None:
|
|
226
|
+
emissivity = table_emissivity(bt_table["emissivity"], columns)
|
|
227
|
+
rad = emission_radiance(bt_table["planck"], od * sec[:, None], columns["temperature_k"], columns["skin_k"], emissivity)
|
|
228
|
+
return {"bt": planck_temperature(bt_table["planck"], rad), "radiance": rad, "od_nadir": od, "emissivity": emissivity}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def table_emissivity(emis_table: dict, columns: dict) -> np.ndarray:
|
|
232
|
+
"""The table's surface emissivity per column: water by the reference's
|
|
233
|
+
sea-water mean, land by IGBP class, snow-covered land and sea ice by
|
|
234
|
+
their own values; fractions composed linearly."""
|
|
235
|
+
land = np.clip(np.asarray(columns["land_fraction"], dtype=np.float64), 0.0, 1.0)
|
|
236
|
+
water = 1.0 - land
|
|
237
|
+
ice = np.clip(np.asarray(columns.get("sea_ice_fraction", np.zeros_like(land)), dtype=np.float64), 0.0, 1.0) * water
|
|
238
|
+
water = water - ice
|
|
239
|
+
cls = np.asarray(columns["landuse_category"], dtype=int)
|
|
240
|
+
by_class = np.asarray(emis_table["land_by_igbp_class"], dtype=np.float64)
|
|
241
|
+
land_e = by_class[np.clip(cls, 1, by_class.size) - 1]
|
|
242
|
+
snow = np.asarray(columns.get("snowh_m", np.zeros_like(land)), dtype=np.float64) > emis_table.get("snow_depth_threshold_m", 0.01)
|
|
243
|
+
snow = snow | (cls == 15)
|
|
244
|
+
land_e = np.where(snow, emis_table["snow"], land_e)
|
|
245
|
+
return water * emis_table["water"] + ice * emis_table["ice"] + land * land_e
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def jacobians(table: dict, band: int, columns: dict, *, emissivity: np.ndarray | None = None) -> dict:
|
|
249
|
+
"""Central finite differences in layer temperature, layer vapor
|
|
250
|
+
(relative) and skin, the steps of :data:`JACOBIAN_STEPS`; per g/kg for
|
|
251
|
+
the vapor row. The oracle for the Rust ``forward`` Jacobians."""
|
|
252
|
+
base = evaluate(table, band, columns, emissivity=emissivity)
|
|
253
|
+
emis = base["emissivity"]
|
|
254
|
+
n, nlay = columns["temperature_k"].shape
|
|
255
|
+
jac_t = np.zeros((n, nlay))
|
|
256
|
+
jac_q = np.zeros((n, nlay))
|
|
257
|
+
hT = JACOBIAN_STEPS["temperature_k"]
|
|
258
|
+
rq = JACOBIAN_STEPS["vapor_relative"]
|
|
259
|
+
for k in range(nlay):
|
|
260
|
+
for sign in (+1, -1):
|
|
261
|
+
c = dict(columns)
|
|
262
|
+
T = np.array(columns["temperature_k"], dtype=np.float64, copy=True)
|
|
263
|
+
T[:, k] += sign * hT
|
|
264
|
+
c["temperature_k"] = T
|
|
265
|
+
jac_t[:, k] += sign * evaluate(table, band, c, emissivity=emis)["bt"] / (2 * hT)
|
|
266
|
+
c = dict(columns)
|
|
267
|
+
q = np.array(columns["q_gkg"], dtype=np.float64, copy=True)
|
|
268
|
+
dq = np.maximum(q[:, k] * rq, 1e-9)
|
|
269
|
+
q[:, k] += sign * dq
|
|
270
|
+
c["q_gkg"] = q
|
|
271
|
+
jac_q[:, k] += sign * evaluate(table, band, c, emissivity=emis)["bt"] / (2 * dq)
|
|
272
|
+
hS = JACOBIAN_STEPS["skin_k"]
|
|
273
|
+
plus = dict(columns); plus["skin_k"] = np.asarray(columns["skin_k"]) + hS
|
|
274
|
+
minus = dict(columns); minus["skin_k"] = np.asarray(columns["skin_k"]) - hS
|
|
275
|
+
jac_skin = (evaluate(table, band, plus, emissivity=emis)["bt"] - evaluate(table, band, minus, emissivity=emis)["bt"]) / (2 * hS)
|
|
276
|
+
return {"bt": base["bt"], "jac_t": jac_t, "jac_q": jac_q, "jac_tskin": jac_skin, "emissivity": emis}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# the trainer
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def _fit_linear(X: np.ndarray, y: np.ndarray, ridge: float) -> np.ndarray:
|
|
284
|
+
scale = np.maximum(np.abs(X).max(axis=0), 1e-12)
|
|
285
|
+
A = X / scale
|
|
286
|
+
return np.linalg.solve(A.T @ A + ridge * np.eye(A.shape[1]) * A.shape[0], A.T @ y) / scale
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _fit_two_term(y: np.ndarray, Xw: np.ndarray, Xd: np.ndarray, *, iterations: int, ridge: float,
|
|
290
|
+
damping: float = 1e-3) -> tuple[np.ndarray, np.ndarray, float]:
|
|
291
|
+
"""``OD = max(Xd d, 0) + exp(Xw w)`` by damped Gauss-Newton on ``ln OD``."""
|
|
292
|
+
ly = np.log(np.maximum(y, 1e-12))
|
|
293
|
+
sw = np.maximum(np.abs(Xw).max(axis=0), 1e-12)
|
|
294
|
+
sd = np.maximum(np.abs(Xd).max(axis=0), 1e-12)
|
|
295
|
+
A = Xw / sw
|
|
296
|
+
w = np.linalg.solve(A.T @ A + ridge * np.eye(A.shape[1]) * A.shape[0], A.T @ ly) / sw
|
|
297
|
+
d = np.zeros(Xd.shape[1])
|
|
298
|
+
best: tuple[float, np.ndarray, np.ndarray] | None = None
|
|
299
|
+
for _ in range(iterations):
|
|
300
|
+
wet = np.exp(np.minimum(Xw @ w, 50.0))
|
|
301
|
+
dry = np.maximum(Xd @ d, 0.0)
|
|
302
|
+
m = wet + dry
|
|
303
|
+
r = ly - np.log(np.maximum(m, 1e-12))
|
|
304
|
+
cost = float(np.mean(r * r))
|
|
305
|
+
if best is None or cost < best[0]:
|
|
306
|
+
best = (cost, w.copy(), d.copy())
|
|
307
|
+
J = np.concatenate([(wet / m)[:, None] * Xw / sw, (1.0 / m)[:, None] * Xd / sd], axis=1)
|
|
308
|
+
H = J.T @ J
|
|
309
|
+
step = np.linalg.solve(H + damping * np.diag(np.diag(H)) + 1e-12 * np.eye(H.shape[0]), J.T @ r)
|
|
310
|
+
w = w + step[:Xw.shape[1]] / sw
|
|
311
|
+
d = d + step[Xw.shape[1]:] / sd
|
|
312
|
+
assert best is not None
|
|
313
|
+
return best[1], best[2], best[0]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _clip_ranges(feat: dict[str, np.ndarray], train: np.ndarray, margin: float = 0.05) -> dict:
|
|
317
|
+
"""Per-layer min and max of every non-trivial feature over the
|
|
318
|
+
training columns, widened by ``margin`` of the range."""
|
|
319
|
+
clip: dict[str, dict[str, list[float]]] = {}
|
|
320
|
+
for name in FEATURE_NAMES:
|
|
321
|
+
if name in ("one", "dp", "u", "e", "lsec"):
|
|
322
|
+
continue
|
|
323
|
+
lo = feat[name][train].min(axis=0)
|
|
324
|
+
hi = feat[name][train].max(axis=0)
|
|
325
|
+
pad = margin * np.maximum(hi - lo, 1e-6)
|
|
326
|
+
clip[name] = {"min": (lo - pad).tolist(), "max": (hi + pad).tolist()}
|
|
327
|
+
return clip
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def train_band(band: int, columns: dict, od_nadir: np.ndarray, *, train: np.ndarray, form: str,
|
|
331
|
+
planck: dict, ridge: float = 1e-12, iterations: int = 12) -> dict:
|
|
332
|
+
"""One band's table: per-layer coefficients in ``form`` (``linear`` for
|
|
333
|
+
the window band, ``two_term`` for the vapor band), the clip ranges and
|
|
334
|
+
the validation on the held-out columns (BT of the oracle against the
|
|
335
|
+
reference's BT recomputed through the same march, so the number is
|
|
336
|
+
the transmittance model's alone)."""
|
|
337
|
+
feat = features(columns["temperature_k"], columns["q_gkg"], columns["p_half_hpa"], columns["p_full_hpa"], columns["zenith"])
|
|
338
|
+
clip = _clip_ranges(feat, train)
|
|
339
|
+
feat_c = clip_features(feat, clip)
|
|
340
|
+
n, nlay = od_nadir.shape
|
|
341
|
+
layers: list[dict] = []
|
|
342
|
+
if form == "linear":
|
|
343
|
+
X = design(feat_c, LINEAR_TERMS_WINDOW)
|
|
344
|
+
for k in range(nlay):
|
|
345
|
+
c = _fit_linear(X[train, k, :], od_nadir[train, k], ridge)
|
|
346
|
+
layers.append({"linear": c.tolist()})
|
|
347
|
+
table = {"form": form, "terms_linear": [list(t) for t in LINEAR_TERMS_WINDOW]}
|
|
348
|
+
elif form == "two_term":
|
|
349
|
+
Xw = design(feat_c, WET_TERMS)
|
|
350
|
+
Xd = design(feat_c, DRY_TERMS)
|
|
351
|
+
for k in range(nlay):
|
|
352
|
+
w, d, cost = _fit_two_term(od_nadir[train, k], Xw[train, k, :], Xd[train, k, :], iterations=iterations, ridge=ridge)
|
|
353
|
+
layers.append({"wet": w.tolist(), "dry": d.tolist(), "ln_od_max": float(np.log(od_nadir[train, k].max() * 3.0 + 1e-12)),
|
|
354
|
+
"fit_log_rms": float(np.sqrt(cost))})
|
|
355
|
+
table = {"form": form, "terms_wet": [list(t) for t in WET_TERMS], "terms_dry": [list(t) for t in DRY_TERMS]}
|
|
356
|
+
else:
|
|
357
|
+
raise FastModelError(f"form must be linear or two_term, got {form!r}")
|
|
358
|
+
table.update({"band": int(band), "planck": dict(planck), "layers": layers, "clip": clip})
|
|
359
|
+
return table
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def validate_band(band_table: dict, columns: dict, od_nadir_reference: np.ndarray, *, test: np.ndarray,
|
|
363
|
+
emissivity: np.ndarray) -> dict:
|
|
364
|
+
"""The oracle's BT against the BT of the reference's own optical depths
|
|
365
|
+
through the same march (isolating the transmittance model), on the
|
|
366
|
+
held-out columns; plus the layer relative error where the band emits."""
|
|
367
|
+
band = int(band_table["band"])
|
|
368
|
+
table = {"bands": {str(band): band_table}}
|
|
369
|
+
ours = evaluate(table, band, columns, emissivity=emissivity)
|
|
370
|
+
sec = 1.0 / np.cos(np.radians(columns["zenith"]))
|
|
371
|
+
ref_rad = emission_radiance(band_table["planck"], od_nadir_reference * sec[:, None], columns["temperature_k"],
|
|
372
|
+
columns["skin_k"], emissivity)
|
|
373
|
+
ref_bt = planck_temperature(band_table["planck"], ref_rad)
|
|
374
|
+
d = ours["bt"] - ref_bt
|
|
375
|
+
rel = np.abs(ours["od_nadir"] - od_nadir_reference) / np.maximum(od_nadir_reference, 1e-9)
|
|
376
|
+
land = np.asarray(columns["land_fraction"]) >= 0.5
|
|
377
|
+
zen = np.asarray(columns["zenith"])
|
|
378
|
+
by_zenith = {}
|
|
379
|
+
for lo, hi in ((0, 20), (20, 40), (40, 50), (50, 60), (60, 70)):
|
|
380
|
+
m = test & (zen >= lo) & (zen < hi)
|
|
381
|
+
if m.any():
|
|
382
|
+
by_zenith[f"{lo}_{hi}"] = {"n": int(m.sum()), "rms_k": float(np.sqrt(np.mean(d[m] ** 2))), "bias_k": float(d[m].mean())}
|
|
383
|
+
return {
|
|
384
|
+
"against": "the reference's own layer optical depths through the same emission march (the transmittance model alone)",
|
|
385
|
+
"test_columns": int(test.sum()), "train_columns": int((~test).sum()),
|
|
386
|
+
"test_rms_k": float(np.sqrt(np.mean(d[test] ** 2))), "test_bias_k": float(d[test].mean()),
|
|
387
|
+
"test_p99_abs_k": float(np.percentile(np.abs(d[test]), 99)), "test_max_abs_k": float(np.abs(d[test]).max()),
|
|
388
|
+
"train_rms_k": float(np.sqrt(np.mean(d[~test] ** 2))),
|
|
389
|
+
"test_water_rms_k": float(np.sqrt(np.mean(d[test & ~land] ** 2))) if np.any(test & ~land) else None,
|
|
390
|
+
"test_land_rms_k": float(np.sqrt(np.mean(d[test & land] ** 2))) if np.any(test & land) else None,
|
|
391
|
+
"by_zenith": by_zenith,
|
|
392
|
+
"median_relative_od_error_per_layer_test": np.median(rel[test], axis=0).round(4).tolist(),
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def emissivity_table_from_reference(reference_run: dict, band: int, columns: dict) -> dict:
|
|
397
|
+
"""The per-class emissivity the reference used on the case: sea water
|
|
398
|
+
(its Nalli model's mean at the case's winds and angles), land by IGBP
|
|
399
|
+
class (the class means; a class absent from the case takes the land
|
|
400
|
+
mean), snow and sea ice from CRTM's NPOESS tables' means where the
|
|
401
|
+
case had them, else stated defaults."""
|
|
402
|
+
e = np.asarray(reference_run["emissivity"][int(band)], dtype=np.float64)
|
|
403
|
+
land = np.asarray(columns["land_fraction"]) >= 0.5
|
|
404
|
+
cls = np.asarray(columns["landuse_category"], dtype=int)
|
|
405
|
+
snow = np.asarray(columns.get("snowh_m", np.zeros(cls.shape))) > 0.01
|
|
406
|
+
ice = (np.asarray(columns.get("sea_ice_fraction", np.zeros(cls.shape))) >= 0.5) & ~land
|
|
407
|
+
water = ~land & ~ice
|
|
408
|
+
land_mean = float(e[land & ~snow].mean()) if np.any(land & ~snow) else 0.96
|
|
409
|
+
by_class = []
|
|
410
|
+
counts = []
|
|
411
|
+
for c in range(1, 21):
|
|
412
|
+
m = land & ~snow & (cls == c)
|
|
413
|
+
counts.append(int(m.sum()))
|
|
414
|
+
by_class.append(float(e[m].mean()) if m.sum() >= 5 else land_mean)
|
|
415
|
+
return {
|
|
416
|
+
"water": float(e[water].mean()) if water.any() else 0.98,
|
|
417
|
+
"water_std": float(e[water].std()) if water.any() else None,
|
|
418
|
+
"land_by_igbp_class": by_class,
|
|
419
|
+
"land_class_counts": counts,
|
|
420
|
+
"snow": float(e[land & snow].mean()) if np.any(land & snow) else 0.985,
|
|
421
|
+
"ice": float(e[ice].mean()) if ice.any() else 0.98,
|
|
422
|
+
"snow_depth_threshold_m": 0.01,
|
|
423
|
+
"source": "the reference run's own surface emissivity on the case's columns (sea water: Nalli; land: IGBP table)",
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def coordinate_identity(a_half_pa, b_half) -> dict:
|
|
428
|
+
a = np.asarray(a_half_pa, dtype=np.float64)
|
|
429
|
+
b = np.asarray(b_half, dtype=np.float64)
|
|
430
|
+
digest = hashlib.sha256(a.tobytes() + b.tobytes()).hexdigest()
|
|
431
|
+
return {"nlev": int(a.size - 1), "a_half_pa": a.tolist(), "b_half": b.tolist(), "sha256": digest}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def write_table(path: str | os.PathLike, table: dict) -> Path:
|
|
435
|
+
path = Path(path)
|
|
436
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
437
|
+
table = dict(table)
|
|
438
|
+
table.setdefault("schema", FAST_MODEL_SCHEMA)
|
|
439
|
+
table.setdefault("written_utc", dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"))
|
|
440
|
+
path.write_text(json.dumps(table, indent=1, sort_keys=True), encoding="utf-8")
|
|
441
|
+
return path
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def read_table(path: str | os.PathLike) -> dict:
|
|
445
|
+
table = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
446
|
+
if table.get("schema") != FAST_MODEL_SCHEMA:
|
|
447
|
+
raise FastModelError(f"{path} declares schema {table.get('schema')!r}, not {FAST_MODEL_SCHEMA}")
|
|
448
|
+
return table
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
__all__ = [
|
|
452
|
+
"DRY_TERMS", "FAST_MODEL_SCHEMA", "FEATURE_CONSTANTS", "FEATURE_DEFINITIONS", "FEATURE_NAMES", "JACOBIAN_STEPS",
|
|
453
|
+
"LINEAR_TERMS_WINDOW", "WET_TERMS", "FastModelError", "clip_features", "coordinate_identity", "design",
|
|
454
|
+
"emission_radiance", "emissivity_table_from_reference", "evaluate", "features", "jacobians", "layer_optical_depth",
|
|
455
|
+
"planck_radiance", "planck_temperature", "read_table", "table_emissivity", "train_band", "validate_band", "write_table",
|
|
456
|
+
]
|