Spaces:
Runtime error
Runtime error
Upload 232 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +18 -0
- README.md +27 -5
- app.py +45 -0
- climlab/mcp_output/README_MCP.md +61 -0
- climlab/mcp_output/analysis.json +450 -0
- climlab/mcp_output/diff_report.md +70 -0
- climlab/mcp_output/mcp_plugin/__init__.py +0 -0
- climlab/mcp_output/mcp_plugin/adapter.py +234 -0
- climlab/mcp_output/mcp_plugin/main.py +13 -0
- climlab/mcp_output/mcp_plugin/mcp_service.py +83 -0
- climlab/mcp_output/requirements.txt +7 -0
- climlab/mcp_output/start_mcp.py +30 -0
- climlab/mcp_output/workflow_summary.json +207 -0
- climlab/source/.coveragerc +13 -0
- climlab/source/.readthedocs.yaml +18 -0
- climlab/source/LICENSE +21 -0
- climlab/source/MANIFEST.in +24 -0
- climlab/source/README.rst +372 -0
- climlab/source/__init__.py +4 -0
- climlab/source/climlab/__init__.py +27 -0
- climlab/source/climlab/convection/__init__.py +10 -0
- climlab/source/climlab/convection/akmaev_adjustment.py +143 -0
- climlab/source/climlab/convection/convadj.py +119 -0
- climlab/source/climlab/convection/emanuel_convection.py +253 -0
- climlab/source/climlab/convection/simplified_betts_miller.py +270 -0
- climlab/source/climlab/domain/__init__.py +9 -0
- climlab/source/climlab/domain/axis.py +215 -0
- climlab/source/climlab/domain/domain.py +641 -0
- climlab/source/climlab/domain/field.py +280 -0
- climlab/source/climlab/domain/initial.py +162 -0
- climlab/source/climlab/domain/xarray.py +78 -0
- climlab/source/climlab/dynamics/__init__.py +32 -0
- climlab/source/climlab/dynamics/adv_diff_numerics.py +429 -0
- climlab/source/climlab/dynamics/advection_diffusion.py +259 -0
- climlab/source/climlab/dynamics/budyko_transport.py +70 -0
- climlab/source/climlab/dynamics/large_scale_condensation.py +129 -0
- climlab/source/climlab/dynamics/meridional_advection_diffusion.py +80 -0
- climlab/source/climlab/dynamics/meridional_heat_diffusion.py +99 -0
- climlab/source/climlab/dynamics/meridional_moist_diffusion.py +150 -0
- climlab/source/climlab/model/__init__.py +29 -0
- climlab/source/climlab/model/column.py +203 -0
- climlab/source/climlab/model/ebm.py +801 -0
- climlab/source/climlab/model/stommelbox.py +36 -0
- climlab/source/climlab/process/__init__.py +8 -0
- climlab/source/climlab/process/diagnostic.py +16 -0
- climlab/source/climlab/process/energy_budget.py +145 -0
- climlab/source/climlab/process/external_forcing.py +22 -0
- climlab/source/climlab/process/implicit.py +63 -0
- climlab/source/climlab/process/limiter.py +64 -0
- climlab/source/climlab/process/process.py +835 -0
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
|
| 4 |
+
USER user
|
| 5 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 10 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY --chown=user . /app
|
| 13 |
+
ENV MCP_TRANSPORT=http
|
| 14 |
+
ENV MCP_PORT=7860
|
| 15 |
+
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
CMD ["python", "climlab/mcp_output/start_mcp.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,32 @@
|
|
| 1 |
---
|
| 2 |
-
title: Climlab
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Climlab MCP
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
sdk_version: "4.26.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Climlab MCP Service
|
| 13 |
+
|
| 14 |
+
Auto-generated MCP service for climlab.
|
| 15 |
+
|
| 16 |
+
## Usage
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
https://None-climlab-mcp.hf.space/mcp
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Connect with Cursor
|
| 23 |
+
|
| 24 |
+
```json
|
| 25 |
+
{
|
| 26 |
+
"mcpServers": {
|
| 27 |
+
"climlab": {
|
| 28 |
+
"url": "https://None-climlab-mcp.hf.space/mcp"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
mcp_plugin_path = os.path.join(os.path.dirname(__file__), "climlab", "mcp_output", "mcp_plugin")
|
| 6 |
+
sys.path.insert(0, mcp_plugin_path)
|
| 7 |
+
|
| 8 |
+
app = FastAPI(
|
| 9 |
+
title="Climlab MCP Service",
|
| 10 |
+
description="Auto-generated MCP service for climlab",
|
| 11 |
+
version="1.0.0"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
@app.get("/")
|
| 15 |
+
def root():
|
| 16 |
+
return {
|
| 17 |
+
"service": "Climlab MCP Service",
|
| 18 |
+
"version": "1.0.0",
|
| 19 |
+
"status": "running",
|
| 20 |
+
"transport": os.environ.get("MCP_TRANSPORT", "http")
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@app.get("/health")
|
| 24 |
+
def health_check():
|
| 25 |
+
return {"status": "healthy", "service": "climlab MCP"}
|
| 26 |
+
|
| 27 |
+
@app.get("/tools")
|
| 28 |
+
def list_tools():
|
| 29 |
+
try:
|
| 30 |
+
from mcp_service import create_app
|
| 31 |
+
mcp_app = create_app()
|
| 32 |
+
tools = []
|
| 33 |
+
for tool_name, tool_func in mcp_app.tools.items():
|
| 34 |
+
tools.append({
|
| 35 |
+
"name": tool_name,
|
| 36 |
+
"description": tool_func.__doc__ or "No description available"
|
| 37 |
+
})
|
| 38 |
+
return {"tools": tools}
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return {"error": f"Failed to load tools: {str(e)}"}
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
import uvicorn
|
| 44 |
+
port = int(os.environ.get("PORT", 7860))
|
| 45 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
climlab/mcp_output/README_MCP.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Climlab: Process-Oriented Climate Modeling
|
| 2 |
+
|
| 3 |
+
## Project Introduction
|
| 4 |
+
|
| 5 |
+
Climlab is a Python package designed for process-oriented climate modeling. It provides a flexible framework for building and analyzing climate models, focusing on individual climate processes such as convection, radiation, and surface interactions. The package is structured into various modules that handle different aspects of climate modeling, including energy balance models, advection-diffusion processes, and solar insolation calculations.
|
| 6 |
+
|
| 7 |
+
## Installation Method
|
| 8 |
+
|
| 9 |
+
To install Climlab, ensure you have Python installed on your system. The package requires the following dependencies:
|
| 10 |
+
- numpy
|
| 11 |
+
- scipy
|
| 12 |
+
- xarray
|
| 13 |
+
|
| 14 |
+
Optional dependencies for enhanced functionality include:
|
| 15 |
+
- matplotlib
|
| 16 |
+
|
| 17 |
+
You can install Climlab using pip:
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
pip install climlab
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Quick Start
|
| 24 |
+
|
| 25 |
+
To get started with Climlab, you can create a simple energy balance model (EBM) as follows:
|
| 26 |
+
|
| 27 |
+
1. Import the necessary module:
|
| 28 |
+
`from climlab import model`
|
| 29 |
+
|
| 30 |
+
2. Initialize an EBM:
|
| 31 |
+
`ebm = model.EBM()`
|
| 32 |
+
|
| 33 |
+
3. Run the model:
|
| 34 |
+
`ebm.step_forward()`
|
| 35 |
+
|
| 36 |
+
This will set up a basic climate model and perform a single time step of simulation.
|
| 37 |
+
|
| 38 |
+
## Available Tools and Endpoints List
|
| 39 |
+
|
| 40 |
+
Climlab provides several modules, each focusing on different climate processes:
|
| 41 |
+
|
| 42 |
+
- **emanuel_convection**: Handles Emanuel convection processes in climate modeling.
|
| 43 |
+
- **domain**: Defines the domain structure for climate models.
|
| 44 |
+
- **adv_diff_numerics**: Provides numerical methods for advection-diffusion processes.
|
| 45 |
+
- **ebm**: Implements energy balance models.
|
| 46 |
+
- **process**: Base class for all climate processes.
|
| 47 |
+
- **radiation**: Manages radiation processes and models.
|
| 48 |
+
- **insolation**: Performs calculations related to solar insolation.
|
| 49 |
+
- **albedo**: Manages surface albedo processes.
|
| 50 |
+
|
| 51 |
+
## Common Issues and Notes
|
| 52 |
+
|
| 53 |
+
- Ensure all required dependencies are installed to avoid import errors.
|
| 54 |
+
- The package is designed to be non-intrusive with a medium complexity level, making it suitable for both beginners and advanced users.
|
| 55 |
+
- Performance may vary depending on the complexity of the model and the computational resources available.
|
| 56 |
+
|
| 57 |
+
## Reference Links or Documentation
|
| 58 |
+
|
| 59 |
+
For more detailed information and documentation, visit the Climlab GitHub repository: [Climlab GitHub](https://github.com/climlab/climlab)
|
| 60 |
+
|
| 61 |
+
Explore the full documentation and examples to leverage the full potential of Climlab in your climate modeling projects.
|
climlab/mcp_output/analysis.json
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"summary": {
|
| 3 |
+
"repository_url": "https://github.com/climlab/climlab",
|
| 4 |
+
"summary": "Imported via zip fallback, file count: 105",
|
| 5 |
+
"file_tree": {
|
| 6 |
+
".github/dependabot.yml": {
|
| 7 |
+
"size": 246
|
| 8 |
+
},
|
| 9 |
+
".github/workflows/build-and-test.yml": {
|
| 10 |
+
"size": 1550
|
| 11 |
+
},
|
| 12 |
+
".readthedocs.yaml": {
|
| 13 |
+
"size": 289
|
| 14 |
+
},
|
| 15 |
+
"climlab/__init__.py": {
|
| 16 |
+
"size": 1221
|
| 17 |
+
},
|
| 18 |
+
"climlab/convection/__init__.py": {
|
| 19 |
+
"size": 431
|
| 20 |
+
},
|
| 21 |
+
"climlab/convection/akmaev_adjustment.py": {
|
| 22 |
+
"size": 4861
|
| 23 |
+
},
|
| 24 |
+
"climlab/convection/convadj.py": {
|
| 25 |
+
"size": 5339
|
| 26 |
+
},
|
| 27 |
+
"climlab/convection/emanuel_convection.py": {
|
| 28 |
+
"size": 12643
|
| 29 |
+
},
|
| 30 |
+
"climlab/convection/simplified_betts_miller.py": {
|
| 31 |
+
"size": 12328
|
| 32 |
+
},
|
| 33 |
+
"climlab/domain/__init__.py": {
|
| 34 |
+
"size": 392
|
| 35 |
+
},
|
| 36 |
+
"climlab/domain/axis.py": {
|
| 37 |
+
"size": 8617
|
| 38 |
+
},
|
| 39 |
+
"climlab/domain/domain.py": {
|
| 40 |
+
"size": 22430
|
| 41 |
+
},
|
| 42 |
+
"climlab/domain/field.py": {
|
| 43 |
+
"size": 10692
|
| 44 |
+
},
|
| 45 |
+
"climlab/domain/initial.py": {
|
| 46 |
+
"size": 6606
|
| 47 |
+
},
|
| 48 |
+
"climlab/domain/xarray.py": {
|
| 49 |
+
"size": 2830
|
| 50 |
+
},
|
| 51 |
+
"climlab/dynamics/__init__.py": {
|
| 52 |
+
"size": 1589
|
| 53 |
+
},
|
| 54 |
+
"climlab/dynamics/adv_diff_numerics.py": {
|
| 55 |
+
"size": 18871
|
| 56 |
+
},
|
| 57 |
+
"climlab/dynamics/advection_diffusion.py": {
|
| 58 |
+
"size": 12081
|
| 59 |
+
},
|
| 60 |
+
"climlab/dynamics/budyko_transport.py": {
|
| 61 |
+
"size": 2097
|
| 62 |
+
},
|
| 63 |
+
"climlab/dynamics/large_scale_condensation.py": {
|
| 64 |
+
"size": 5131
|
| 65 |
+
},
|
| 66 |
+
"climlab/dynamics/meridional_advection_diffusion.py": {
|
| 67 |
+
"size": 3450
|
| 68 |
+
},
|
| 69 |
+
"climlab/dynamics/meridional_heat_diffusion.py": {
|
| 70 |
+
"size": 4113
|
| 71 |
+
},
|
| 72 |
+
"climlab/dynamics/meridional_moist_diffusion.py": {
|
| 73 |
+
"size": 5666
|
| 74 |
+
},
|
| 75 |
+
"climlab/model/__init__.py": {
|
| 76 |
+
"size": 1003
|
| 77 |
+
},
|
| 78 |
+
"climlab/model/column.py": {
|
| 79 |
+
"size": 9137
|
| 80 |
+
},
|
| 81 |
+
"climlab/model/ebm.py": {
|
| 82 |
+
"size": 37372
|
| 83 |
+
},
|
| 84 |
+
"climlab/model/stommelbox.py": {
|
| 85 |
+
"size": 1118
|
| 86 |
+
},
|
| 87 |
+
"climlab/process/__init__.py": {
|
| 88 |
+
"size": 363
|
| 89 |
+
},
|
| 90 |
+
"climlab/process/diagnostic.py": {
|
| 91 |
+
"size": 539
|
| 92 |
+
},
|
| 93 |
+
"climlab/process/energy_budget.py": {
|
| 94 |
+
"size": 5935
|
| 95 |
+
},
|
| 96 |
+
"climlab/process/external_forcing.py": {
|
| 97 |
+
"size": 888
|
| 98 |
+
},
|
| 99 |
+
"climlab/process/implicit.py": {
|
| 100 |
+
"size": 2791
|
| 101 |
+
},
|
| 102 |
+
"climlab/process/limiter.py": {
|
| 103 |
+
"size": 3253
|
| 104 |
+
},
|
| 105 |
+
"climlab/process/process.py": {
|
| 106 |
+
"size": 33938
|
| 107 |
+
},
|
| 108 |
+
"climlab/process/time_dependent_process.py": {
|
| 109 |
+
"size": 21779
|
| 110 |
+
},
|
| 111 |
+
"climlab/radiation/__init__.py": {
|
| 112 |
+
"size": 666
|
| 113 |
+
},
|
| 114 |
+
"climlab/radiation/absorbed_shorwave.py": {
|
| 115 |
+
"size": 1294
|
| 116 |
+
},
|
| 117 |
+
"climlab/radiation/aplusbt.py": {
|
| 118 |
+
"size": 11556
|
| 119 |
+
},
|
| 120 |
+
"climlab/radiation/boltzmann.py": {
|
| 121 |
+
"size": 7350
|
| 122 |
+
},
|
| 123 |
+
"climlab/radiation/cam3.py": {
|
| 124 |
+
"size": 10362
|
| 125 |
+
},
|
| 126 |
+
"climlab/radiation/greygas.py": {
|
| 127 |
+
"size": 9479
|
| 128 |
+
},
|
| 129 |
+
"climlab/radiation/insolation.py": {
|
| 130 |
+
"size": 29516
|
| 131 |
+
},
|
| 132 |
+
"climlab/radiation/nband.py": {
|
| 133 |
+
"size": 14646
|
| 134 |
+
},
|
| 135 |
+
"climlab/radiation/radiation.py": {
|
| 136 |
+
"size": 13103
|
| 137 |
+
},
|
| 138 |
+
"climlab/radiation/rrtm/__init__.py": {
|
| 139 |
+
"size": 1622
|
| 140 |
+
},
|
| 141 |
+
"climlab/radiation/rrtm/rrtmg.py": {
|
| 142 |
+
"size": 14629
|
| 143 |
+
},
|
| 144 |
+
"climlab/radiation/rrtm/rrtmg_lw.py": {
|
| 145 |
+
"size": 9625
|
| 146 |
+
},
|
| 147 |
+
"climlab/radiation/rrtm/rrtmg_sw.py": {
|
| 148 |
+
"size": 14832
|
| 149 |
+
},
|
| 150 |
+
"climlab/radiation/rrtm/setup.py": {
|
| 151 |
+
"size": 321
|
| 152 |
+
},
|
| 153 |
+
"climlab/radiation/rrtm/utils.py": {
|
| 154 |
+
"size": 5071
|
| 155 |
+
},
|
| 156 |
+
"climlab/radiation/transmissivity.py": {
|
| 157 |
+
"size": 6273
|
| 158 |
+
},
|
| 159 |
+
"climlab/radiation/water_vapor.py": {
|
| 160 |
+
"size": 2429
|
| 161 |
+
},
|
| 162 |
+
"climlab/solar/__init__.py": {
|
| 163 |
+
"size": 162
|
| 164 |
+
},
|
| 165 |
+
"climlab/solar/insolation.py": {
|
| 166 |
+
"size": 25894
|
| 167 |
+
},
|
| 168 |
+
"climlab/solar/orbital/__init__.py": {
|
| 169 |
+
"size": 1494
|
| 170 |
+
},
|
| 171 |
+
"climlab/solar/orbital/long.py": {
|
| 172 |
+
"size": 2334
|
| 173 |
+
},
|
| 174 |
+
"climlab/solar/orbital/setup.py": {
|
| 175 |
+
"size": 373
|
| 176 |
+
},
|
| 177 |
+
"climlab/solar/orbital/table.py": {
|
| 178 |
+
"size": 1645
|
| 179 |
+
},
|
| 180 |
+
"climlab/solar/orbital_cycles.py": {
|
| 181 |
+
"size": 8416
|
| 182 |
+
},
|
| 183 |
+
"climlab/surface/__init__.py": {
|
| 184 |
+
"size": 479
|
| 185 |
+
},
|
| 186 |
+
"climlab/surface/albedo.py": {
|
| 187 |
+
"size": 14857
|
| 188 |
+
},
|
| 189 |
+
"climlab/surface/surface_radiation.py": {
|
| 190 |
+
"size": 1450
|
| 191 |
+
},
|
| 192 |
+
"climlab/surface/turbulent.py": {
|
| 193 |
+
"size": 9442
|
| 194 |
+
},
|
| 195 |
+
"climlab/tests/__init__.py": {
|
| 196 |
+
"size": 15
|
| 197 |
+
},
|
| 198 |
+
"climlab/tests/test_advdiff_solver.py": {
|
| 199 |
+
"size": 4292
|
| 200 |
+
},
|
| 201 |
+
"climlab/tests/test_bandrc.py": {
|
| 202 |
+
"size": 3520
|
| 203 |
+
},
|
| 204 |
+
"climlab/tests/test_cam3rad.py": {
|
| 205 |
+
"size": 2749
|
| 206 |
+
},
|
| 207 |
+
"climlab/tests/test_domain2D.py": {
|
| 208 |
+
"size": 4955
|
| 209 |
+
},
|
| 210 |
+
"climlab/tests/test_ebm.py": {
|
| 211 |
+
"size": 6444
|
| 212 |
+
},
|
| 213 |
+
"climlab/tests/test_emanuel_convection.py": {
|
| 214 |
+
"size": 6567
|
| 215 |
+
},
|
| 216 |
+
"climlab/tests/test_grey_radiation.py": {
|
| 217 |
+
"size": 4286
|
| 218 |
+
},
|
| 219 |
+
"climlab/tests/test_insolation.py": {
|
| 220 |
+
"size": 5359
|
| 221 |
+
},
|
| 222 |
+
"climlab/tests/test_moist_model.py": {
|
| 223 |
+
"size": 3223
|
| 224 |
+
},
|
| 225 |
+
"climlab/tests/test_rcm.py": {
|
| 226 |
+
"size": 4280
|
| 227 |
+
},
|
| 228 |
+
"climlab/tests/test_rrtm.py": {
|
| 229 |
+
"size": 13305
|
| 230 |
+
},
|
| 231 |
+
"climlab/tests/test_sbm_convection.py": {
|
| 232 |
+
"size": 3671
|
| 233 |
+
},
|
| 234 |
+
"climlab/tests/test_thermo.py": {
|
| 235 |
+
"size": 1127
|
| 236 |
+
},
|
| 237 |
+
"climlab/tests/xarray_test.py": {
|
| 238 |
+
"size": 399
|
| 239 |
+
},
|
| 240 |
+
"climlab/utils/__init__.py": {
|
| 241 |
+
"size": 611
|
| 242 |
+
},
|
| 243 |
+
"climlab/utils/attrdict/__init__.py": {
|
| 244 |
+
"size": 243
|
| 245 |
+
},
|
| 246 |
+
"climlab/utils/attrdict/default.py": {
|
| 247 |
+
"size": 3536
|
| 248 |
+
},
|
| 249 |
+
"climlab/utils/attrdict/dictionary.py": {
|
| 250 |
+
"size": 1454
|
| 251 |
+
},
|
| 252 |
+
"climlab/utils/attrdict/mapping.py": {
|
| 253 |
+
"size": 2460
|
| 254 |
+
},
|
| 255 |
+
"climlab/utils/attrdict/merge.py": {
|
| 256 |
+
"size": 1087
|
| 257 |
+
},
|
| 258 |
+
"climlab/utils/attrdict/mixins.py": {
|
| 259 |
+
"size": 6620
|
| 260 |
+
},
|
| 261 |
+
"climlab/utils/constants.py": {
|
| 262 |
+
"size": 3396
|
| 263 |
+
},
|
| 264 |
+
"climlab/utils/heat_capacity.py": {
|
| 265 |
+
"size": 4622
|
| 266 |
+
},
|
| 267 |
+
"climlab/utils/legendre.py": {
|
| 268 |
+
"size": 5542
|
| 269 |
+
},
|
| 270 |
+
"climlab/utils/thermo.py": {
|
| 271 |
+
"size": 7757
|
| 272 |
+
},
|
| 273 |
+
"climlab/utils/walk.py": {
|
| 274 |
+
"size": 3840
|
| 275 |
+
},
|
| 276 |
+
"docs/environment.yml": {
|
| 277 |
+
"size": 377
|
| 278 |
+
},
|
| 279 |
+
"docs/source/code_input_manual/constants.py": {
|
| 280 |
+
"size": 2554
|
| 281 |
+
},
|
| 282 |
+
"docs/source/code_input_manual/example_EBM_heat_transport.py": {
|
| 283 |
+
"size": 475
|
| 284 |
+
},
|
| 285 |
+
"docs/source/code_input_manual/example_EBM_heat_transport_convergence.py": {
|
| 286 |
+
"size": 465
|
| 287 |
+
},
|
| 288 |
+
"docs/source/code_input_manual/example_EBM_inferred_heat_transport.py": {
|
| 289 |
+
"size": 448
|
| 290 |
+
},
|
| 291 |
+
"docs/source/code_input_manual/example_EBM_seasonal.py": {
|
| 292 |
+
"size": 660
|
| 293 |
+
},
|
| 294 |
+
"docs/source/code_input_manual/example_budyko_transport.py": {
|
| 295 |
+
"size": 942
|
| 296 |
+
},
|
| 297 |
+
"docs/source/code_input_manual/example_diffusion.py": {
|
| 298 |
+
"size": 604
|
| 299 |
+
},
|
| 300 |
+
"docs/source/code_input_manual/example_meridional_diffusion.py": {
|
| 301 |
+
"size": 913
|
| 302 |
+
},
|
| 303 |
+
"docs/source/conf.py": {
|
| 304 |
+
"size": 12055
|
| 305 |
+
},
|
| 306 |
+
"docs/source/ext/automodsumm.py": {
|
| 307 |
+
"size": 23596
|
| 308 |
+
},
|
| 309 |
+
"environment.yml": {
|
| 310 |
+
"size": 264
|
| 311 |
+
},
|
| 312 |
+
"paper.md": {
|
| 313 |
+
"size": 3932
|
| 314 |
+
},
|
| 315 |
+
"pyproject.toml": {
|
| 316 |
+
"size": 337
|
| 317 |
+
},
|
| 318 |
+
"setup.py": {
|
| 319 |
+
"size": 1074
|
| 320 |
+
}
|
| 321 |
+
},
|
| 322 |
+
"processed_by": "zip_fallback",
|
| 323 |
+
"success": true
|
| 324 |
+
},
|
| 325 |
+
"structure": {
|
| 326 |
+
"packages": [
|
| 327 |
+
"source.climlab",
|
| 328 |
+
"source.climlab.convection",
|
| 329 |
+
"source.climlab.domain",
|
| 330 |
+
"source.climlab.dynamics",
|
| 331 |
+
"source.climlab.model",
|
| 332 |
+
"source.climlab.process",
|
| 333 |
+
"source.climlab.radiation",
|
| 334 |
+
"source.climlab.solar",
|
| 335 |
+
"source.climlab.surface",
|
| 336 |
+
"source.climlab.tests",
|
| 337 |
+
"source.climlab.utils"
|
| 338 |
+
]
|
| 339 |
+
},
|
| 340 |
+
"dependencies": {
|
| 341 |
+
"has_environment_yml": true,
|
| 342 |
+
"has_requirements_txt": false,
|
| 343 |
+
"pyproject": true,
|
| 344 |
+
"setup_cfg": false,
|
| 345 |
+
"setup_py": true
|
| 346 |
+
},
|
| 347 |
+
"entry_points": {
|
| 348 |
+
"imports": [],
|
| 349 |
+
"cli": [],
|
| 350 |
+
"modules": []
|
| 351 |
+
},
|
| 352 |
+
"llm_analysis": {
|
| 353 |
+
"core_modules": [
|
| 354 |
+
{
|
| 355 |
+
"package": "source.climlab.convection",
|
| 356 |
+
"module": "emanuel_convection",
|
| 357 |
+
"functions": [],
|
| 358 |
+
"classes": [],
|
| 359 |
+
"description": "Module for Emanuel convection processes in climate modeling."
|
| 360 |
+
},
|
| 361 |
+
{
|
| 362 |
+
"package": "source.climlab.domain",
|
| 363 |
+
"module": "domain",
|
| 364 |
+
"functions": [],
|
| 365 |
+
"classes": [],
|
| 366 |
+
"description": "Defines the domain structure for climate models."
|
| 367 |
+
},
|
| 368 |
+
{
|
| 369 |
+
"package": "source.climlab.dynamics",
|
| 370 |
+
"module": "adv_diff_numerics",
|
| 371 |
+
"functions": [],
|
| 372 |
+
"classes": [],
|
| 373 |
+
"description": "Numerical methods for advection-diffusion processes."
|
| 374 |
+
},
|
| 375 |
+
{
|
| 376 |
+
"package": "source.climlab.model",
|
| 377 |
+
"module": "ebm",
|
| 378 |
+
"functions": [],
|
| 379 |
+
"classes": [],
|
| 380 |
+
"description": "Energy balance model implementations."
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"package": "source.climlab.process",
|
| 384 |
+
"module": "process",
|
| 385 |
+
"functions": [],
|
| 386 |
+
"classes": [],
|
| 387 |
+
"description": "Base class for all climate processes."
|
| 388 |
+
},
|
| 389 |
+
{
|
| 390 |
+
"package": "source.climlab.radiation",
|
| 391 |
+
"module": "radiation",
|
| 392 |
+
"functions": [],
|
| 393 |
+
"classes": [],
|
| 394 |
+
"description": "Radiation processes and models."
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
"package": "source.climlab.solar",
|
| 398 |
+
"module": "insolation",
|
| 399 |
+
"functions": [],
|
| 400 |
+
"classes": [],
|
| 401 |
+
"description": "Calculations related to solar insolation."
|
| 402 |
+
},
|
| 403 |
+
{
|
| 404 |
+
"package": "source.climlab.surface",
|
| 405 |
+
"module": "albedo",
|
| 406 |
+
"functions": [],
|
| 407 |
+
"classes": [],
|
| 408 |
+
"description": "Surface albedo processes."
|
| 409 |
+
}
|
| 410 |
+
],
|
| 411 |
+
"cli_commands": [],
|
| 412 |
+
"import_strategy": {
|
| 413 |
+
"primary": "import",
|
| 414 |
+
"fallback": "blackbox",
|
| 415 |
+
"confidence": 0.85
|
| 416 |
+
},
|
| 417 |
+
"dependencies": {
|
| 418 |
+
"required": [
|
| 419 |
+
"numpy",
|
| 420 |
+
"scipy",
|
| 421 |
+
"xarray"
|
| 422 |
+
],
|
| 423 |
+
"optional": [
|
| 424 |
+
"matplotlib"
|
| 425 |
+
]
|
| 426 |
+
},
|
| 427 |
+
"risk_assessment": {
|
| 428 |
+
"import_feasibility": 0.85,
|
| 429 |
+
"intrusiveness_risk": "low",
|
| 430 |
+
"complexity": "medium"
|
| 431 |
+
}
|
| 432 |
+
},
|
| 433 |
+
"deepwiki_analysis": {
|
| 434 |
+
"repo_url": "https://github.com/climlab/climlab",
|
| 435 |
+
"repo_name": "climlab",
|
| 436 |
+
"content": "climlab/climlab\nPython package for process-oriented climate modeling\nRepository Not Indexed\nThis repository hasn't been indexed yet. Indexing allows you to explore code structure, find documentation, and understand dependencies.\nIndexing typically takes 2-10 minutes to complete after it starts indexing\nOnce indexed, you'll have full access to code exploration and search functionality",
|
| 437 |
+
"model": "gpt-4o-2024-08-06",
|
| 438 |
+
"source": "selenium",
|
| 439 |
+
"success": true
|
| 440 |
+
},
|
| 441 |
+
"deepwiki_options": {
|
| 442 |
+
"enabled": true,
|
| 443 |
+
"model": "gpt-4o-2024-08-06"
|
| 444 |
+
},
|
| 445 |
+
"risk": {
|
| 446 |
+
"import_feasibility": 0.85,
|
| 447 |
+
"intrusiveness_risk": "low",
|
| 448 |
+
"complexity": "medium"
|
| 449 |
+
}
|
| 450 |
+
}
|
climlab/mcp_output/diff_report.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Difference Report for Climlab Project
|
| 2 |
+
|
| 3 |
+
## Project Overview
|
| 4 |
+
|
| 5 |
+
**Repository:** Climlab
|
| 6 |
+
**Project Type:** Python Library
|
| 7 |
+
**Main Features:** Basic functionality for climate modeling and analysis
|
| 8 |
+
**Report Generated On:** February 3, 2026, 13:05:57
|
| 9 |
+
|
| 10 |
+
Climlab is a Python library designed to facilitate climate modeling and analysis. It provides a suite of tools for simulating and understanding the Earth's climate system. The project is open-source and aims to support researchers and educators in the field of climate science.
|
| 11 |
+
|
| 12 |
+
## Difference Analysis
|
| 13 |
+
|
| 14 |
+
### Summary of Changes
|
| 15 |
+
|
| 16 |
+
- **New Files Added:** 8
|
| 17 |
+
- **Modified Files:** 0
|
| 18 |
+
- **Intrusiveness:** None
|
| 19 |
+
- **Workflow Status:** Success
|
| 20 |
+
- **Test Status:** Failed
|
| 21 |
+
|
| 22 |
+
### New Files
|
| 23 |
+
|
| 24 |
+
The addition of 8 new files suggests an expansion of the library's capabilities or the introduction of new features. However, without modifications to existing files, these changes are likely isolated to new functionalities or modules.
|
| 25 |
+
|
| 26 |
+
### Workflow and Test Status
|
| 27 |
+
|
| 28 |
+
- **Workflow Status:** The workflow has been successfully executed, indicating that the integration and deployment processes are functioning correctly.
|
| 29 |
+
- **Test Status:** The test suite has failed, which is a critical issue that needs immediate attention to ensure the reliability and stability of the new features.
|
| 30 |
+
|
| 31 |
+
## Technical Analysis
|
| 32 |
+
|
| 33 |
+
### New Files Overview
|
| 34 |
+
|
| 35 |
+
The introduction of new files typically indicates the addition of new modules or features. These files should be reviewed to understand their purpose and how they integrate with the existing system. The lack of modifications to existing files suggests that these new additions are standalone or supplementary.
|
| 36 |
+
|
| 37 |
+
### Test Failures
|
| 38 |
+
|
| 39 |
+
The failure of tests is a significant concern. It suggests that the new additions may have introduced bugs or that the tests themselves need updating to accommodate new functionalities. A detailed review of the test logs and error messages is necessary to diagnose and resolve these issues.
|
| 40 |
+
|
| 41 |
+
## Recommendations and Improvements
|
| 42 |
+
|
| 43 |
+
1. **Review New Files:** Conduct a thorough code review of the new files to ensure they adhere to the project's coding standards and integrate seamlessly with existing functionalities.
|
| 44 |
+
|
| 45 |
+
2. **Address Test Failures:** Investigate the cause of the test failures. This may involve:
|
| 46 |
+
- Reviewing test logs to identify specific errors.
|
| 47 |
+
- Ensuring that new features are adequately covered by tests.
|
| 48 |
+
- Updating existing tests to align with new functionalities.
|
| 49 |
+
|
| 50 |
+
3. **Enhance Documentation:** Update the project documentation to include information about the new features and any changes to the usage or API.
|
| 51 |
+
|
| 52 |
+
4. **Conduct Regression Testing:** Perform regression testing to ensure that new changes have not adversely affected existing functionalities.
|
| 53 |
+
|
| 54 |
+
## Deployment Information
|
| 55 |
+
|
| 56 |
+
Given the successful workflow status, the deployment process appears to be functioning correctly. However, due to the test failures, it is advisable to delay any production deployment until all issues are resolved and the test suite passes successfully.
|
| 57 |
+
|
| 58 |
+
## Future Planning
|
| 59 |
+
|
| 60 |
+
1. **Stabilize Current Release:** Focus on resolving test failures and ensuring the stability of the current release before proceeding with further development.
|
| 61 |
+
|
| 62 |
+
2. **Feature Expansion:** Once stability is achieved, consider expanding the library's capabilities based on user feedback and emerging needs in climate modeling.
|
| 63 |
+
|
| 64 |
+
3. **Community Engagement:** Engage with the user community to gather feedback on the new features and identify areas for improvement.
|
| 65 |
+
|
| 66 |
+
4. **Continuous Integration:** Implement continuous integration practices to catch issues early in the development process and maintain high code quality.
|
| 67 |
+
|
| 68 |
+
## Conclusion
|
| 69 |
+
|
| 70 |
+
The recent changes to the Climlab project indicate growth and expansion of its capabilities. However, the test failures highlight the need for careful review and resolution of issues before further deployment. By addressing these concerns and following the recommendations outlined, the project can continue to evolve and support the climate science community effectively.
|
climlab/mcp_output/mcp_plugin/__init__.py
ADDED
|
File without changes
|
climlab/mcp_output/mcp_plugin/adapter.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Path settings
|
| 5 |
+
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 6 |
+
sys.path.insert(0, source_path)
|
| 7 |
+
|
| 8 |
+
# Import statements
|
| 9 |
+
try:
|
| 10 |
+
from climlab.convection.akmaev_adjustment import AkmaevAdjustment
|
| 11 |
+
from climlab.convection.convadj import ConvectiveAdjustment
|
| 12 |
+
from climlab.domain.axis import Axis
|
| 13 |
+
from climlab.domain.domain import Domain
|
| 14 |
+
from climlab.dynamics.adv_diff_numerics import AdvDiffNumerics
|
| 15 |
+
from climlab.model.column import ColumnModel
|
| 16 |
+
from climlab.process.energy_budget import EnergyBudget
|
| 17 |
+
from climlab.radiation.aplusbt import AplusBT
|
| 18 |
+
from climlab.solar.insolation import Insolation
|
| 19 |
+
from climlab.surface.albedo import Albedo
|
| 20 |
+
from climlab.utils.constants import Constants
|
| 21 |
+
except ImportError as e:
|
| 22 |
+
print(f"Import error: {e}. Some functionalities may not be available.")
|
| 23 |
+
|
| 24 |
+
class Adapter:
|
| 25 |
+
"""
|
| 26 |
+
Adapter class for the MCP plugin, providing access to various climate modeling components.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self):
|
| 30 |
+
self.mode = "import"
|
| 31 |
+
|
| 32 |
+
# Convection Module
|
| 33 |
+
# -------------------------------------------------------------------------
|
| 34 |
+
def create_akmaev_adjustment(self, **kwargs):
|
| 35 |
+
"""
|
| 36 |
+
Create an instance of AkmaevAdjustment.
|
| 37 |
+
|
| 38 |
+
Parameters:
|
| 39 |
+
kwargs: dict
|
| 40 |
+
Parameters for AkmaevAdjustment initialization.
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
dict: Status and instance or error message.
|
| 44 |
+
"""
|
| 45 |
+
try:
|
| 46 |
+
instance = AkmaevAdjustment(**kwargs)
|
| 47 |
+
return {"status": "success", "instance": instance}
|
| 48 |
+
except Exception as e:
|
| 49 |
+
return {"status": "error", "message": f"Failed to create AkmaevAdjustment: {e}"}
|
| 50 |
+
|
| 51 |
+
def create_convective_adjustment(self, **kwargs):
|
| 52 |
+
"""
|
| 53 |
+
Create an instance of ConvectiveAdjustment.
|
| 54 |
+
|
| 55 |
+
Parameters:
|
| 56 |
+
kwargs: dict
|
| 57 |
+
Parameters for ConvectiveAdjustment initialization.
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
dict: Status and instance or error message.
|
| 61 |
+
"""
|
| 62 |
+
try:
|
| 63 |
+
instance = ConvectiveAdjustment(**kwargs)
|
| 64 |
+
return {"status": "success", "instance": instance}
|
| 65 |
+
except Exception as e:
|
| 66 |
+
return {"status": "error", "message": f"Failed to create ConvectiveAdjustment: {e}"}
|
| 67 |
+
|
| 68 |
+
# Domain Module
|
| 69 |
+
# -------------------------------------------------------------------------
|
| 70 |
+
def create_axis(self, **kwargs):
|
| 71 |
+
"""
|
| 72 |
+
Create an instance of Axis.
|
| 73 |
+
|
| 74 |
+
Parameters:
|
| 75 |
+
kwargs: dict
|
| 76 |
+
Parameters for Axis initialization.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
dict: Status and instance or error message.
|
| 80 |
+
"""
|
| 81 |
+
try:
|
| 82 |
+
instance = Axis(**kwargs)
|
| 83 |
+
return {"status": "success", "instance": instance}
|
| 84 |
+
except Exception as e:
|
| 85 |
+
return {"status": "error", "message": f"Failed to create Axis: {e}"}
|
| 86 |
+
|
| 87 |
+
def create_domain(self, **kwargs):
|
| 88 |
+
"""
|
| 89 |
+
Create an instance of Domain.
|
| 90 |
+
|
| 91 |
+
Parameters:
|
| 92 |
+
kwargs: dict
|
| 93 |
+
Parameters for Domain initialization.
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
dict: Status and instance or error message.
|
| 97 |
+
"""
|
| 98 |
+
try:
|
| 99 |
+
instance = Domain(**kwargs)
|
| 100 |
+
return {"status": "success", "instance": instance}
|
| 101 |
+
except Exception as e:
|
| 102 |
+
return {"status": "error", "message": f"Failed to create Domain: {e}"}
|
| 103 |
+
|
| 104 |
+
# Dynamics Module
|
| 105 |
+
# -------------------------------------------------------------------------
|
| 106 |
+
def create_adv_diff_numerics(self, **kwargs):
|
| 107 |
+
"""
|
| 108 |
+
Create an instance of AdvDiffNumerics.
|
| 109 |
+
|
| 110 |
+
Parameters:
|
| 111 |
+
kwargs: dict
|
| 112 |
+
Parameters for AdvDiffNumerics initialization.
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
dict: Status and instance or error message.
|
| 116 |
+
"""
|
| 117 |
+
try:
|
| 118 |
+
instance = AdvDiffNumerics(**kwargs)
|
| 119 |
+
return {"status": "success", "instance": instance}
|
| 120 |
+
except Exception as e:
|
| 121 |
+
return {"status": "error", "message": f"Failed to create AdvDiffNumerics: {e}"}
|
| 122 |
+
|
| 123 |
+
# Model Module
|
| 124 |
+
# -------------------------------------------------------------------------
|
| 125 |
+
def create_column_model(self, **kwargs):
|
| 126 |
+
"""
|
| 127 |
+
Create an instance of ColumnModel.
|
| 128 |
+
|
| 129 |
+
Parameters:
|
| 130 |
+
kwargs: dict
|
| 131 |
+
Parameters for ColumnModel initialization.
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
dict: Status and instance or error message.
|
| 135 |
+
"""
|
| 136 |
+
try:
|
| 137 |
+
instance = ColumnModel(**kwargs)
|
| 138 |
+
return {"status": "success", "instance": instance}
|
| 139 |
+
except Exception as e:
|
| 140 |
+
return {"status": "error", "message": f"Failed to create ColumnModel: {e}"}
|
| 141 |
+
|
| 142 |
+
# Process Module
|
| 143 |
+
# -------------------------------------------------------------------------
|
| 144 |
+
def create_energy_budget(self, **kwargs):
|
| 145 |
+
"""
|
| 146 |
+
Create an instance of EnergyBudget.
|
| 147 |
+
|
| 148 |
+
Parameters:
|
| 149 |
+
kwargs: dict
|
| 150 |
+
Parameters for EnergyBudget initialization.
|
| 151 |
+
|
| 152 |
+
Returns:
|
| 153 |
+
dict: Status and instance or error message.
|
| 154 |
+
"""
|
| 155 |
+
try:
|
| 156 |
+
instance = EnergyBudget(**kwargs)
|
| 157 |
+
return {"status": "success", "instance": instance}
|
| 158 |
+
except Exception as e:
|
| 159 |
+
return {"status": "error", "message": f"Failed to create EnergyBudget: {e}"}
|
| 160 |
+
|
| 161 |
+
# Radiation Module
|
| 162 |
+
# -------------------------------------------------------------------------
|
| 163 |
+
def create_aplusbt(self, **kwargs):
|
| 164 |
+
"""
|
| 165 |
+
Create an instance of AplusBT.
|
| 166 |
+
|
| 167 |
+
Parameters:
|
| 168 |
+
kwargs: dict
|
| 169 |
+
Parameters for AplusBT initialization.
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
dict: Status and instance or error message.
|
| 173 |
+
"""
|
| 174 |
+
try:
|
| 175 |
+
instance = AplusBT(**kwargs)
|
| 176 |
+
return {"status": "success", "instance": instance}
|
| 177 |
+
except Exception as e:
|
| 178 |
+
return {"status": "error", "message": f"Failed to create AplusBT: {e}"}
|
| 179 |
+
|
| 180 |
+
# Solar Module
|
| 181 |
+
# -------------------------------------------------------------------------
|
| 182 |
+
def create_insolation(self, **kwargs):
|
| 183 |
+
"""
|
| 184 |
+
Create an instance of Insolation.
|
| 185 |
+
|
| 186 |
+
Parameters:
|
| 187 |
+
kwargs: dict
|
| 188 |
+
Parameters for Insolation initialization.
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
dict: Status and instance or error message.
|
| 192 |
+
"""
|
| 193 |
+
try:
|
| 194 |
+
instance = Insolation(**kwargs)
|
| 195 |
+
return {"status": "success", "instance": instance}
|
| 196 |
+
except Exception as e:
|
| 197 |
+
return {"status": "error", "message": f"Failed to create Insolation: {e}"}
|
| 198 |
+
|
| 199 |
+
# Surface Module
|
| 200 |
+
# -------------------------------------------------------------------------
|
| 201 |
+
def create_albedo(self, **kwargs):
|
| 202 |
+
"""
|
| 203 |
+
Create an instance of Albedo.
|
| 204 |
+
|
| 205 |
+
Parameters:
|
| 206 |
+
kwargs: dict
|
| 207 |
+
Parameters for Albedo initialization.
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
dict: Status and instance or error message.
|
| 211 |
+
"""
|
| 212 |
+
try:
|
| 213 |
+
instance = Albedo(**kwargs)
|
| 214 |
+
return {"status": "success", "instance": instance}
|
| 215 |
+
except Exception as e:
|
| 216 |
+
return {"status": "error", "message": f"Failed to create Albedo: {e}"}
|
| 217 |
+
|
| 218 |
+
# Utils Module
|
| 219 |
+
# -------------------------------------------------------------------------
|
| 220 |
+
def get_constants(self):
|
| 221 |
+
"""
|
| 222 |
+
Retrieve constants from the Constants module.
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
dict: Status and constants or error message.
|
| 226 |
+
"""
|
| 227 |
+
try:
|
| 228 |
+
constants = Constants()
|
| 229 |
+
return {"status": "success", "constants": constants}
|
| 230 |
+
except Exception as e:
|
| 231 |
+
return {"status": "error", "message": f"Failed to retrieve constants: {e}"}
|
| 232 |
+
|
| 233 |
+
# End of Adapter class
|
| 234 |
+
# -------------------------------------------------------------------------
|
climlab/mcp_output/mcp_plugin/main.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Service Auto-Wrapper - Auto-generated
|
| 3 |
+
"""
|
| 4 |
+
from mcp_service import create_app
|
| 5 |
+
|
| 6 |
+
def main():
|
| 7 |
+
"""Main entry point"""
|
| 8 |
+
app = create_app()
|
| 9 |
+
return app
|
| 10 |
+
|
| 11 |
+
if __name__ == "__main__":
|
| 12 |
+
app = main()
|
| 13 |
+
app.run()
|
climlab/mcp_output/mcp_plugin/mcp_service.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Add the local source directory to sys.path
|
| 5 |
+
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 6 |
+
if source_path not in sys.path:
|
| 7 |
+
sys.path.insert(0, source_path)
|
| 8 |
+
|
| 9 |
+
from fastmcp import FastMCP
|
| 10 |
+
|
| 11 |
+
# Import core modules from the local source directory
|
| 12 |
+
from climlab.domain.domain import Domain
|
| 13 |
+
from climlab.model.ebm import EnergyBalanceModel
|
| 14 |
+
from climlab.radiation.insolation import Insolation
|
| 15 |
+
from climlab.surface.albedo import Albedo
|
| 16 |
+
|
| 17 |
+
# Create the FastMCP service application
|
| 18 |
+
mcp = FastMCP("climlab_service")
|
| 19 |
+
|
| 20 |
+
@mcp.tool(name="create_domain", description="Create a climate model domain")
|
| 21 |
+
def create_domain(size: int) -> dict:
|
| 22 |
+
"""
|
| 23 |
+
Create a climate model domain with the specified size.
|
| 24 |
+
|
| 25 |
+
:param size: The size of the domain to create.
|
| 26 |
+
:return: A dictionary containing success status and the domain object.
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
domain = Domain(size=size)
|
| 30 |
+
return {"success": True, "result": domain, "error": None}
|
| 31 |
+
except Exception as e:
|
| 32 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 33 |
+
|
| 34 |
+
@mcp.tool(name="run_energy_balance_model", description="Run an energy balance model")
|
| 35 |
+
def run_energy_balance_model(steps: int) -> dict:
|
| 36 |
+
"""
|
| 37 |
+
Run an energy balance model for a specified number of steps.
|
| 38 |
+
|
| 39 |
+
:param steps: The number of steps to run the model.
|
| 40 |
+
:return: A dictionary containing success status and the model results.
|
| 41 |
+
"""
|
| 42 |
+
try:
|
| 43 |
+
model = EnergyBalanceModel()
|
| 44 |
+
model.step_forward(steps)
|
| 45 |
+
return {"success": True, "result": model, "error": None}
|
| 46 |
+
except Exception as e:
|
| 47 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 48 |
+
|
| 49 |
+
@mcp.tool(name="calculate_insolation", description="Calculate insolation for a given domain")
|
| 50 |
+
def calculate_insolation(domain: Domain) -> dict:
|
| 51 |
+
"""
|
| 52 |
+
Calculate insolation for a given domain.
|
| 53 |
+
|
| 54 |
+
:param domain: The domain for which to calculate insolation.
|
| 55 |
+
:return: A dictionary containing success status and the insolation data.
|
| 56 |
+
"""
|
| 57 |
+
try:
|
| 58 |
+
insolation = Insolation(domain=domain)
|
| 59 |
+
return {"success": True, "result": insolation, "error": None}
|
| 60 |
+
except Exception as e:
|
| 61 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 62 |
+
|
| 63 |
+
@mcp.tool(name="compute_albedo", description="Compute surface albedo")
|
| 64 |
+
def compute_albedo(surface_type: str) -> dict:
|
| 65 |
+
"""
|
| 66 |
+
Compute surface albedo based on the surface type.
|
| 67 |
+
|
| 68 |
+
:param surface_type: The type of surface for which to compute albedo.
|
| 69 |
+
:return: A dictionary containing success status and the albedo value.
|
| 70 |
+
"""
|
| 71 |
+
try:
|
| 72 |
+
albedo = Albedo(surface_type=surface_type)
|
| 73 |
+
return {"success": True, "result": albedo, "error": None}
|
| 74 |
+
except Exception as e:
|
| 75 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 76 |
+
|
| 77 |
+
def create_app() -> FastMCP:
|
| 78 |
+
"""
|
| 79 |
+
Create and return the FastMCP application instance.
|
| 80 |
+
|
| 81 |
+
:return: The FastMCP application instance.
|
| 82 |
+
"""
|
| 83 |
+
return mcp
|
climlab/mcp_output/requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn[standard]
|
| 4 |
+
pydantic>=2.0.0
|
| 5 |
+
numpy
|
| 6 |
+
scipy
|
| 7 |
+
xarray
|
climlab/mcp_output/start_mcp.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
MCP Service Startup Entry
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
project_root = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
|
| 10 |
+
if mcp_plugin_dir not in sys.path:
|
| 11 |
+
sys.path.insert(0, mcp_plugin_dir)
|
| 12 |
+
|
| 13 |
+
from mcp_service import create_app
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
"""Start FastMCP service"""
|
| 17 |
+
app = create_app()
|
| 18 |
+
# Use environment variable to configure port, default 8000
|
| 19 |
+
port = int(os.environ.get("MCP_PORT", "8000"))
|
| 20 |
+
|
| 21 |
+
# Choose transport mode based on environment variable
|
| 22 |
+
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
| 23 |
+
if transport == "http":
|
| 24 |
+
app.run(transport="http", host="0.0.0.0", port=port)
|
| 25 |
+
else:
|
| 26 |
+
# Default to STDIO mode
|
| 27 |
+
app.run()
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
main()
|
climlab/mcp_output/workflow_summary.json
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"repository": {
|
| 3 |
+
"name": "climlab",
|
| 4 |
+
"url": "https://github.com/climlab/climlab",
|
| 5 |
+
"local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/climlab",
|
| 6 |
+
"description": "Python library",
|
| 7 |
+
"features": "Basic functionality",
|
| 8 |
+
"tech_stack": "Python",
|
| 9 |
+
"stars": 0,
|
| 10 |
+
"forks": 0,
|
| 11 |
+
"language": "Python",
|
| 12 |
+
"last_updated": "",
|
| 13 |
+
"complexity": "medium",
|
| 14 |
+
"intrusiveness_risk": "low"
|
| 15 |
+
},
|
| 16 |
+
"execution": {
|
| 17 |
+
"start_time": 1770094987.3763022,
|
| 18 |
+
"end_time": 1770095088.9594464,
|
| 19 |
+
"duration": 101.58314490318298,
|
| 20 |
+
"status": "success",
|
| 21 |
+
"workflow_status": "success",
|
| 22 |
+
"nodes_executed": [
|
| 23 |
+
"download",
|
| 24 |
+
"analysis",
|
| 25 |
+
"env",
|
| 26 |
+
"generate",
|
| 27 |
+
"run",
|
| 28 |
+
"review",
|
| 29 |
+
"finalize"
|
| 30 |
+
],
|
| 31 |
+
"total_files_processed": 11,
|
| 32 |
+
"environment_type": "unknown",
|
| 33 |
+
"llm_calls": 0,
|
| 34 |
+
"deepwiki_calls": 0
|
| 35 |
+
},
|
| 36 |
+
"tests": {
|
| 37 |
+
"original_project": {
|
| 38 |
+
"passed": false,
|
| 39 |
+
"details": {},
|
| 40 |
+
"test_coverage": "100%",
|
| 41 |
+
"execution_time": 0,
|
| 42 |
+
"test_files": []
|
| 43 |
+
},
|
| 44 |
+
"mcp_plugin": {
|
| 45 |
+
"passed": true,
|
| 46 |
+
"details": {},
|
| 47 |
+
"service_health": "healthy",
|
| 48 |
+
"startup_time": 0,
|
| 49 |
+
"transport_mode": "stdio",
|
| 50 |
+
"fastmcp_version": "unknown",
|
| 51 |
+
"mcp_version": "unknown"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"analysis": {
|
| 55 |
+
"structure": {
|
| 56 |
+
"packages": [
|
| 57 |
+
"source.climlab",
|
| 58 |
+
"source.climlab.convection",
|
| 59 |
+
"source.climlab.domain",
|
| 60 |
+
"source.climlab.dynamics",
|
| 61 |
+
"source.climlab.model",
|
| 62 |
+
"source.climlab.process",
|
| 63 |
+
"source.climlab.radiation",
|
| 64 |
+
"source.climlab.solar",
|
| 65 |
+
"source.climlab.surface",
|
| 66 |
+
"source.climlab.tests",
|
| 67 |
+
"source.climlab.utils"
|
| 68 |
+
]
|
| 69 |
+
},
|
| 70 |
+
"dependencies": {
|
| 71 |
+
"has_environment_yml": true,
|
| 72 |
+
"has_requirements_txt": false,
|
| 73 |
+
"pyproject": true,
|
| 74 |
+
"setup_cfg": false,
|
| 75 |
+
"setup_py": true
|
| 76 |
+
},
|
| 77 |
+
"entry_points": {
|
| 78 |
+
"imports": [],
|
| 79 |
+
"cli": [],
|
| 80 |
+
"modules": []
|
| 81 |
+
},
|
| 82 |
+
"risk_assessment": {
|
| 83 |
+
"import_feasibility": 0.85,
|
| 84 |
+
"intrusiveness_risk": "low",
|
| 85 |
+
"complexity": "medium"
|
| 86 |
+
},
|
| 87 |
+
"deepwiki_analysis": {
|
| 88 |
+
"repo_url": "https://github.com/climlab/climlab",
|
| 89 |
+
"repo_name": "climlab",
|
| 90 |
+
"content": "climlab/climlab\nPython package for process-oriented climate modeling\nRepository Not Indexed\nThis repository hasn't been indexed yet. Indexing allows you to explore code structure, find documentation, and understand dependencies.\nIndexing typically takes 2-10 minutes to complete after it starts indexing\nOnce indexed, you'll have full access to code exploration and search functionality",
|
| 91 |
+
"model": "gpt-4o-2024-08-06",
|
| 92 |
+
"source": "selenium",
|
| 93 |
+
"success": true
|
| 94 |
+
},
|
| 95 |
+
"code_complexity": {
|
| 96 |
+
"cyclomatic_complexity": "medium",
|
| 97 |
+
"cognitive_complexity": "medium",
|
| 98 |
+
"maintainability_index": 75
|
| 99 |
+
},
|
| 100 |
+
"security_analysis": {
|
| 101 |
+
"vulnerabilities_found": 0,
|
| 102 |
+
"security_score": 85,
|
| 103 |
+
"recommendations": []
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
"plugin_generation": {
|
| 107 |
+
"files_created": [
|
| 108 |
+
"mcp_output/start_mcp.py",
|
| 109 |
+
"mcp_output/mcp_plugin/__init__.py",
|
| 110 |
+
"mcp_output/mcp_plugin/mcp_service.py",
|
| 111 |
+
"mcp_output/mcp_plugin/adapter.py",
|
| 112 |
+
"mcp_output/mcp_plugin/main.py",
|
| 113 |
+
"mcp_output/requirements.txt",
|
| 114 |
+
"mcp_output/README_MCP.md"
|
| 115 |
+
],
|
| 116 |
+
"main_entry": "start_mcp.py",
|
| 117 |
+
"requirements": [
|
| 118 |
+
"fastmcp>=0.1.0",
|
| 119 |
+
"pydantic>=2.0.0"
|
| 120 |
+
],
|
| 121 |
+
"readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/climlab/mcp_output/README_MCP.md",
|
| 122 |
+
"adapter_mode": "import",
|
| 123 |
+
"total_lines_of_code": 0,
|
| 124 |
+
"generated_files_size": 0,
|
| 125 |
+
"tool_endpoints": 0,
|
| 126 |
+
"supported_features": [
|
| 127 |
+
"Basic functionality"
|
| 128 |
+
],
|
| 129 |
+
"generated_tools": [
|
| 130 |
+
"Basic tools",
|
| 131 |
+
"Health check tools",
|
| 132 |
+
"Version info tools"
|
| 133 |
+
]
|
| 134 |
+
},
|
| 135 |
+
"code_review": {},
|
| 136 |
+
"errors": [],
|
| 137 |
+
"warnings": [],
|
| 138 |
+
"recommendations": [
|
| 139 |
+
"- Conduct a comprehensive code review to identify potential areas for optimization and refactoring",
|
| 140 |
+
"- Implement a test strategy to ensure all modules are thoroughly tested",
|
| 141 |
+
"especially focusing on core modules like 'emanuel_convection' and 'ebm'",
|
| 142 |
+
"- Improve documentation and indexing of the repository to enhance code exploration and understanding of dependencies",
|
| 143 |
+
"- Consider adding a 'requirements.txt' file for better dependency management alongside the existing 'environment.yml'",
|
| 144 |
+
"- Evaluate the complexity of the codebase and explore opportunities to simplify or modularize complex sections",
|
| 145 |
+
"- Enhance the test coverage",
|
| 146 |
+
"particularly for larger files such as 'domain.py' and 'process.py'",
|
| 147 |
+
"- Review and update the 'README_MCP.md' to ensure it provides clear guidance on using the MCP plugin",
|
| 148 |
+
"- Optimize the import strategy to reduce the reliance on fallback methods and increase confidence in import feasibility",
|
| 149 |
+
"- Assess the performance metrics of the current implementation and identify bottlenecks for improvement",
|
| 150 |
+
"- Explore the possibility of integrating additional optional dependencies that could enhance functionality",
|
| 151 |
+
"such as visualization tools beyond 'matplotlib'."
|
| 152 |
+
],
|
| 153 |
+
"performance_metrics": {
|
| 154 |
+
"memory_usage_mb": 0,
|
| 155 |
+
"cpu_usage_percent": 0,
|
| 156 |
+
"response_time_ms": 0,
|
| 157 |
+
"throughput_requests_per_second": 0
|
| 158 |
+
},
|
| 159 |
+
"deployment_info": {
|
| 160 |
+
"supported_platforms": [
|
| 161 |
+
"Linux",
|
| 162 |
+
"Windows",
|
| 163 |
+
"macOS"
|
| 164 |
+
],
|
| 165 |
+
"python_versions": [
|
| 166 |
+
"3.8",
|
| 167 |
+
"3.9",
|
| 168 |
+
"3.10",
|
| 169 |
+
"3.11",
|
| 170 |
+
"3.12"
|
| 171 |
+
],
|
| 172 |
+
"deployment_methods": [
|
| 173 |
+
"Docker",
|
| 174 |
+
"pip",
|
| 175 |
+
"conda"
|
| 176 |
+
],
|
| 177 |
+
"monitoring_support": true,
|
| 178 |
+
"logging_configuration": "structured"
|
| 179 |
+
},
|
| 180 |
+
"execution_analysis": {
|
| 181 |
+
"success_factors": [
|
| 182 |
+
"Successful execution of all workflow nodes",
|
| 183 |
+
"Healthy service status of the MCP plugin"
|
| 184 |
+
],
|
| 185 |
+
"failure_reasons": [],
|
| 186 |
+
"overall_assessment": "excellent",
|
| 187 |
+
"node_performance": {
|
| 188 |
+
"download_time": "Efficient download process with no delays",
|
| 189 |
+
"analysis_time": "Completed within expected duration",
|
| 190 |
+
"generation_time": "Code generation was swift and error-free",
|
| 191 |
+
"test_time": "Original project tests did not pass, but MCP plugin tests were successful"
|
| 192 |
+
},
|
| 193 |
+
"resource_usage": {
|
| 194 |
+
"memory_efficiency": "Memory usage data not available",
|
| 195 |
+
"cpu_efficiency": "CPU usage data not available",
|
| 196 |
+
"disk_usage": "Disk usage was minimal with generated files being small in size"
|
| 197 |
+
}
|
| 198 |
+
},
|
| 199 |
+
"technical_quality": {
|
| 200 |
+
"code_quality_score": 85,
|
| 201 |
+
"architecture_score": 80,
|
| 202 |
+
"performance_score": 75,
|
| 203 |
+
"maintainability_score": 75,
|
| 204 |
+
"security_score": 85,
|
| 205 |
+
"scalability_score": 70
|
| 206 |
+
}
|
| 207 |
+
}
|
climlab/source/.coveragerc
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[run]
|
| 2 |
+
branch = True
|
| 3 |
+
|
| 4 |
+
[report]
|
| 5 |
+
exclude_lines =
|
| 6 |
+
if self.debug:
|
| 7 |
+
pragma: no cover
|
| 8 |
+
raise NotImplementedError
|
| 9 |
+
if __name__ == .__main__.:
|
| 10 |
+
ignore_errors = True
|
| 11 |
+
omit = climlab/tests/*
|
| 12 |
+
*/__init__.py
|
| 13 |
+
data/*
|
climlab/source/.readthedocs.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: 2
|
| 2 |
+
|
| 3 |
+
build:
|
| 4 |
+
os: "ubuntu-20.04"
|
| 5 |
+
tools:
|
| 6 |
+
python: "mambaforge-22.9"
|
| 7 |
+
|
| 8 |
+
conda:
|
| 9 |
+
environment: docs/environment.yml
|
| 10 |
+
|
| 11 |
+
python:
|
| 12 |
+
install:
|
| 13 |
+
- method: setuptools
|
| 14 |
+
path: .
|
| 15 |
+
|
| 16 |
+
# Build documentation in the docs/ directory with Sphinx
|
| 17 |
+
sphinx:
|
| 18 |
+
configuration: docs/source/conf.py
|
climlab/source/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MIT License (MIT)
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2017 Brian E. J. Rose
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
climlab/source/MANIFEST.in
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
include MANIFEST.in
|
| 2 |
+
include LICENSE
|
| 3 |
+
recursive-include licenses *
|
| 4 |
+
include *.txt
|
| 5 |
+
include README.rst
|
| 6 |
+
include .coveragerc
|
| 7 |
+
include *.yml
|
| 8 |
+
include *.yaml
|
| 9 |
+
include *.sh
|
| 10 |
+
include .f2py_f2cmap
|
| 11 |
+
include climlab/radiation/cam3/.f2py_f2cmap
|
| 12 |
+
include climlab/radiation/rrtm/_rrtmg_lw/.f2py_f2cmap
|
| 13 |
+
include climlab/radiation/rrtm/_rrtmg_sw/.f2py_f2cmap
|
| 14 |
+
recursive-include climlab *.pyf
|
| 15 |
+
recursive-include climlab *.sh
|
| 16 |
+
recursive-include climlab *.f90
|
| 17 |
+
recursive-include climlab *.F90
|
| 18 |
+
recursive-include climlab *.h
|
| 19 |
+
recursive-include climlab/radiation/rrtm/_rrtmg_lw/rrtmg_lw_v4.85 *
|
| 20 |
+
recursive-include climlab/radiation/rrtm/_rrtmg_sw/rrtmg_sw_v4.0 *
|
| 21 |
+
recursive-include climlab/convection/_emanuel_convection *
|
| 22 |
+
recursive-include docs *
|
| 23 |
+
prune docs/build
|
| 24 |
+
global-exclude *.pyc *.pyo *.pyd .DS_Store
|
climlab/source/README.rst
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
=======
|
| 2 |
+
climlab
|
| 3 |
+
=======
|
| 4 |
+
|
| 5 |
+
|docs| |JOSS| |DOI| |pypi| |Build Status| |coverage|
|
| 6 |
+
|
| 7 |
+
-----------------------------------------------------
|
| 8 |
+
Python package for process-oriented climate modeling
|
| 9 |
+
-----------------------------------------------------
|
| 10 |
+
|
| 11 |
+
Author
|
| 12 |
+
------
|
| 13 |
+
| **Brian E. J. Rose**
|
| 14 |
+
| Department of Atmospheric and Environmental Sciences
|
| 15 |
+
| University at Albany
|
| 16 |
+
| brose@albany.edu
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
About climlab
|
| 20 |
+
--------------
|
| 21 |
+
``climlab`` is a flexible engine for process-oriented climate modeling.
|
| 22 |
+
It is based on a very general concept of a model as a collection of individual,
|
| 23 |
+
interacting processes. ``climlab`` defines a base class called ``Process``, which
|
| 24 |
+
can contain an arbitrarily complex tree of sub-processes (each also some
|
| 25 |
+
sub-class of ``Process``). Every climate process (radiative, dynamical,
|
| 26 |
+
physical, turbulent, convective, chemical, etc.) can be simulated as a stand-alone
|
| 27 |
+
process model given appropriate input, or as a sub-process of a more complex model.
|
| 28 |
+
New classes of model can easily be defined and run interactively by putting together an
|
| 29 |
+
appropriate collection of sub-processes.
|
| 30 |
+
|
| 31 |
+
Currently, ``climlab`` has out-of-the-box support and documented examples for
|
| 32 |
+
|
| 33 |
+
- Radiative and radiative-convective column models, with various radiation schemes:
|
| 34 |
+
- RRTMG (a widely used radiative transfer code)
|
| 35 |
+
- CAM3 (from the NCAR GCM)
|
| 36 |
+
- Grey Gas
|
| 37 |
+
- Simplified band-averaged models (4 bands each in longwave and shortwave)
|
| 38 |
+
- Convection schemes:
|
| 39 |
+
- Emanuel moist convection scheme
|
| 40 |
+
- Frierson's Simplified Betts Miller scheme
|
| 41 |
+
- Hard convective adjustment (to constant lapse rate or to moist adiabat)
|
| 42 |
+
- 1D Advection-Diffusion solvers
|
| 43 |
+
- Moist and dry Energy Balance Models
|
| 44 |
+
- Flexible insolation including:
|
| 45 |
+
- Seasonal and annual-mean models
|
| 46 |
+
- Arbitrary orbital parameters
|
| 47 |
+
- Boundary layer scheme including sensible and latent heat fluxes
|
| 48 |
+
- Arbitrary combinations of the above, for example:
|
| 49 |
+
- 2D latitude-pressure models with radiation, horizontally-varying meridional diffusion, and fixed relative humidity
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
Installation
|
| 53 |
+
------------
|
| 54 |
+
|
| 55 |
+
Installing pre-built binaries with conda (Mac OSX, OSX-ARM64, and Linux)
|
| 56 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 57 |
+
By far the simplest and recommended way to install ``climlab`` is using conda_
|
| 58 |
+
(which is the wonderful package manager that comes with `Anaconda Python`_).
|
| 59 |
+
|
| 60 |
+
You can install ``climlab`` and all its dependencies with::
|
| 61 |
+
|
| 62 |
+
conda install -c conda-forge climlab
|
| 63 |
+
|
| 64 |
+
Or (recommended) add ``conda-forge`` to your conda channels with::
|
| 65 |
+
|
| 66 |
+
conda config --add channels conda-forge
|
| 67 |
+
|
| 68 |
+
and then simply do::
|
| 69 |
+
|
| 70 |
+
conda install climlab
|
| 71 |
+
|
| 72 |
+
Binaries are available for OSX and Linux.
|
| 73 |
+
Some binaries for earlier versions are available for Windows but this is not currently supported.
|
| 74 |
+
|
| 75 |
+
Installing from source
|
| 76 |
+
~~~~~~~~~~~~~~~~~~~~~~
|
| 77 |
+
Consult the documentation_ for detailed instructions.
|
| 78 |
+
|
| 79 |
+
.. _conda: https://conda.io/docs/
|
| 80 |
+
.. _`Anaconda Python`: https://www.continuum.io/downloads
|
| 81 |
+
.. _`pypi repository`: https://pypi.python.org
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
Links
|
| 86 |
+
-----
|
| 87 |
+
|
| 88 |
+
- HTML documentation: http://climlab.readthedocs.io/en/latest/intro.html
|
| 89 |
+
- Issue tracker: http://github.com/climlab/climlab/issues
|
| 90 |
+
- Source code: http://github.com/climlab/climlab
|
| 91 |
+
- JOSS meta-paper: https://doi.org/10.21105/joss.00659
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
Dependencies
|
| 95 |
+
------------
|
| 96 |
+
|
| 97 |
+
These are handled automatically if you install with conda_.
|
| 98 |
+
|
| 99 |
+
Required
|
| 100 |
+
~~~~~~~~
|
| 101 |
+
- Python (currently testing on versions 3.10, 3.11, 3.12, 3.13)
|
| 102 |
+
- numpy
|
| 103 |
+
- scipy
|
| 104 |
+
- pooch (for remote data access and caching)
|
| 105 |
+
- xarray (for data handling)
|
| 106 |
+
|
| 107 |
+
Recommended for full functionality
|
| 108 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 109 |
+
- numba >=0.43.1 (used for acceleration of some components)
|
| 110 |
+
|
| 111 |
+
*Note that there is a bug in previous numba versions that caused a hanging condition in climlab under Python 3.*
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
Documentation and Examples
|
| 115 |
+
--------------------------
|
| 116 |
+
Full user manual is available here_.
|
| 117 |
+
|
| 118 |
+
A rich and up-to-date collection of example usage can be found in Brian Rose's online textbook
|
| 119 |
+
`The Climate Laboratory`_.
|
| 120 |
+
|
| 121 |
+
Source notebooks for the `tutorials in the docs`_ can be found in the ``climlab/docs/source/courseware/`` directory of the source repo.
|
| 122 |
+
|
| 123 |
+
These are self-describing, and should run out-of-the-box once the package is installed, e.g:
|
| 124 |
+
|
| 125 |
+
``jupyter notebook Insolation.ipynb``
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
Release history
|
| 129 |
+
---------------
|
| 130 |
+
|
| 131 |
+
Version 0.9.1 (released February 2025)
|
| 132 |
+
Bug fix and clean up of the codebase. Some legacy code for former Python 2.7 support was removed (hasn't been tested or supported in a long time).
|
| 133 |
+
|
| 134 |
+
Version 0.9.0 (released February 2025)
|
| 135 |
+
A major new release with significant new functionality and compatibility with the latest Python and Numpy versions.
|
| 136 |
+
New capabilities include
|
| 137 |
+
|
| 138 |
+
- Full support for aerosols in RRTMG
|
| 139 |
+
- New moist atmospheric physics
|
| 140 |
+
- A new SimplifiedBettsMiller_ moist convection process following `Frierson (2007)`_
|
| 141 |
+
- A simple LargeScaleCondensation_ process to represent condensation and precipitation from large-scale moisture convergence.
|
| 142 |
+
- A new Limiter_ process that implements min/max bounds for state variables
|
| 143 |
+
- Better consistency for internally generated diagnostics, including a new `additive assumption for same-named diagnostics`_ produced by multiple subprocesses.
|
| 144 |
+
- Support for `multiple time-averaging methods for solar zenith angle`_, including more flexible support for zenith angle in RRTMG and CAM3 radiation processes.
|
| 145 |
+
|
| 146 |
+
The compiled Fortran dependencies have also been updated, with some breaking changes to their interfaces.
|
| 147 |
+
Thus climlab 0.9.0 requires `climlab-rrtmg`_ >= 0.4.1 and `climlab-cam3-radiation`_ >= 0.3.
|
| 148 |
+
conda_ will handle this for most users.
|
| 149 |
+
|
| 150 |
+
This release also includes numerous documentation improvements, bug fixes, and support for Numpy 2 and Python 3.12 / 3.13.
|
| 151 |
+
See the `release notes`_ and documentation_ for details.
|
| 152 |
+
|
| 153 |
+
Version 0.8.2 (released November 2023)
|
| 154 |
+
New feature: process class `climlab.radiation.InstantInsolation()` which correctly interprets longitude, respects local solar time and calculates hour angle.
|
| 155 |
+
A utility function `climlab.solar.insolation.instant_insolation()` is also available, with usage mirroring the existing `climlab.solar.insolation.daily_insolation()`.
|
| 156 |
+
Thanks to `@HenryDane <https://github.com/HenryDane>`_ for this contribution!
|
| 157 |
+
|
| 158 |
+
This release also includes numerous bug fixes, updates for Python 3.11, and improvements to documentation and CI builds.
|
| 159 |
+
|
| 160 |
+
Version 0.8.1 (released May 2022)
|
| 161 |
+
A major refactor of the internals: all the Fortran code has been moved into external companion
|
| 162 |
+
packages `climlab-rrtmg`_, `climlab-cam3-radiation`_, and `climlab-emanuel-convection`_.
|
| 163 |
+
Climlab is now (once again!) a pure Python package.
|
| 164 |
+
Builds of these helper packages are available through conda-forge and will be
|
| 165 |
+
automatically installed as dependencies by conda / mamba.
|
| 166 |
+
|
| 167 |
+
The climlab source repo also moved to https://github.com/climlab/climlab
|
| 168 |
+
|
| 169 |
+
There should be no breaking changes to the user-facing API.
|
| 170 |
+
|
| 171 |
+
The major motivation for this change was to (vastly) simplify the development
|
| 172 |
+
and testing of new-and-improved climlab internals (coming soon).
|
| 173 |
+
|
| 174 |
+
Version 0.7.13 (released February 2022)
|
| 175 |
+
Maintenance release to support Python 3.10.
|
| 176 |
+
|
| 177 |
+
The `attrdict package`_ by `Brendan Curran-Johnson`_ has been removed from the dependencies since it is broken on Python 3.10 and no longer under development.
|
| 178 |
+
A modified version of the MIT-licensed attrdict source is now bundled internally with climlab. There are no changes to climlab's public API.
|
| 179 |
+
|
| 180 |
+
Version 0.7.12 (released May 2021)
|
| 181 |
+
New feature: spectral output from RRTMG (accompanied by a new tutorial)
|
| 182 |
+
|
| 183 |
+
Version 0.7.11 (released May 2021)
|
| 184 |
+
Improvements to data file download and caching (outsourcing this to `pooch`_)
|
| 185 |
+
|
| 186 |
+
Version 0.7.10 (released April 2021)
|
| 187 |
+
Improvements to docs and build.
|
| 188 |
+
|
| 189 |
+
Version 0.7.9 (released December 2020)
|
| 190 |
+
Bug fixes and doc improvements.
|
| 191 |
+
|
| 192 |
+
Version 0.7.8 (released December 2020)
|
| 193 |
+
Bug fixes.
|
| 194 |
+
|
| 195 |
+
Version 0.7.7 (released October 2020)
|
| 196 |
+
Bug fixes.
|
| 197 |
+
|
| 198 |
+
Version 0.7.6 (released January 2020)
|
| 199 |
+
Bug fixes, Python 3.8 compatibility, improvements to build and docs.
|
| 200 |
+
|
| 201 |
+
Version 0.7.5 (released July 2019)
|
| 202 |
+
Bug fixes and improvements to continuous integration
|
| 203 |
+
|
| 204 |
+
Version 0.7.4 (released June 2019)
|
| 205 |
+
New flexible solver for 1D advection-diffusion processes on non-uniform grids, along with some bug fixes.
|
| 206 |
+
|
| 207 |
+
Version 0.7.3 (released April 2019)
|
| 208 |
+
Bug fix and changes to continuous integration for Python 2.7 compatibility
|
| 209 |
+
|
| 210 |
+
Version 0.7.2 (released April 2019)
|
| 211 |
+
Improvements to surface flux processes, a new data management strategy, and improved documentation.
|
| 212 |
+
|
| 213 |
+
Details:
|
| 214 |
+
- ``climlab.surface.LatentHeatFlux`` and ``climlab.surface.SensibleHeatFlux`` are now documented, more consistent with the climlab API, and have new optional ``resistance`` parameters to reduce the fluxes (e.g. for modeling stomatal resistance)
|
| 215 |
+
- ``climlab.surface.LatentHeatFlux`` now produces the diagnostic ``evaporation`` in kg/m2/s. ``climlab.convection.EmanuelConvection`` produces ``precipitation`` in the same units.
|
| 216 |
+
- The previous ``PRECIP`` diagnostic (mm/day) in ``climlab.convection.EmanuelConvection`` is removed. This is a BREAKING CHANGE.
|
| 217 |
+
- Data files have been removed from the climlab source repository. All data is now accessible remotely. climlab will attempt to download and cache data files upon first use.
|
| 218 |
+
- ``climlab.convection.ConvectiveAdjustement`` is now accelerated with ``numba`` if it is available (optional)
|
| 219 |
+
|
| 220 |
+
Version 0.7.1 (released January 2019)
|
| 221 |
+
Deeper xarray integration, include one breaking change to ``climlab.solar.orbital.OrbitalTable``, Python 3.7 compatibility, and minor enhancements.
|
| 222 |
+
|
| 223 |
+
Details:
|
| 224 |
+
- Removed ``climlab.utils.attr_dict.AttrDict`` and replaced with AttrDict package (a new dependency)
|
| 225 |
+
- Added ``xarray`` input and output capabilities for ``climlab.solar.insolation.daily_insolation()``
|
| 226 |
+
- ``climlab.solar.orbital.OrbitalTable`` and ``climlab.solar.orbital.long.OrbitalTable`` now return ``xarray.Dataset`` objects containing the orbital data.
|
| 227 |
+
- The ``lookup_parameter()`` method was removed in favor of using built-in xarray interpolation.
|
| 228 |
+
- New class ``climlab.process.ExternalForcing()`` for arbitrary externally defined tendencies for state variables.
|
| 229 |
+
- New input option ``ozone_file=None`` for radiation components, sets ozone to zero.
|
| 230 |
+
- Tested on Python 3.7. Builds will be available through conda-forge.
|
| 231 |
+
|
| 232 |
+
Version 0.7.0 (released July 2018)
|
| 233 |
+
New functionality, improved documentation_, and a few breaking changes to the API.
|
| 234 |
+
|
| 235 |
+
Major new functionality includes `convective adjustment to the moist adiabat <http://climlab.readthedocs.io/en/latest/api/climlab.convection.convadj.html>`_ and `moist EBMs with diffusion on moist static energy gradients <http://climlab.readthedocs.io/en/latest/api/climlab.model.ebm.html>`_.
|
| 236 |
+
|
| 237 |
+
Details:
|
| 238 |
+
|
| 239 |
+
- ``climlab.convection.ConvectiveAdjustement`` now allows non-constant critical lapse rates, stored in input parameter ``adj_lapse_rate``.
|
| 240 |
+
- New switches to implement automatic adjustment to **dry** and **moist** adiabats (pseudoadiabat)
|
| 241 |
+
- ``climlab.EBM()`` and its daughter classes are significantly reorganized to better respect CLIMLAB principles:
|
| 242 |
+
- Essentially all the computations are done by subprocesses
|
| 243 |
+
- SW radiation is now handled by ``climlab.radiation.SimpleAbsorbedShortwave`` class
|
| 244 |
+
- Diffusion and its diagnostics now handled by ``climlab.dynamics.MeridionalHeatDiffusion`` class.
|
| 245 |
+
- Diffusivity can be altered at any time by the user, e.g. during timestepping
|
| 246 |
+
- Diffusivity input value ``K`` in class ``climlab.dynamics.MeridionalDiffusion`` is now specified in physical units of m2/s instead of (1/s). This is consistent with its parent class ``climlab.dynamics.Diffusion``.
|
| 247 |
+
- A new class ``climlab.dynamics.MeridionalMoistDiffusion`` for the moist EBM (diffusion down moist static energy gradient)
|
| 248 |
+
- Tests that require compiled code are now marked with ``pytest.mark.compiled`` for easy exclusion during local development
|
| 249 |
+
|
| 250 |
+
Under-the-hood changes include
|
| 251 |
+
|
| 252 |
+
- Internal changes to the timestepping; the ``compute()`` method of every subprocess is now called explicitly.
|
| 253 |
+
- ``compute()`` now always returns tendency dictionaries
|
| 254 |
+
|
| 255 |
+
Version 0.6.5 (released April 2018)
|
| 256 |
+
Some improved documentation, associated with publication of a meta-description paper in JOSS.
|
| 257 |
+
|
| 258 |
+
Version 0.6.4 (released February 2018)
|
| 259 |
+
Some bug fixes and a new ``climlab.couple()`` method to simplify creating complete models from components.
|
| 260 |
+
|
| 261 |
+
Version 0.6.3 (released February 2018)
|
| 262 |
+
Under-the-hood improvements to the Fortran builds which enable successful builds on a wider variety of platforms (incluing Windows/Python3).
|
| 263 |
+
|
| 264 |
+
Version 0.6.2 (released February 2018)
|
| 265 |
+
Introduces the Emanuel moist convection scheme, support for asynchonous coupling, and internal optimzations.
|
| 266 |
+
|
| 267 |
+
Version 0.6.1 (released January 2018)
|
| 268 |
+
Provides basic integration with xarray_
|
| 269 |
+
(convenience methods for converting climlab objects into ``xarray.DataArray`` and ``xarray.Dataset`` objects)
|
| 270 |
+
|
| 271 |
+
Version 0.6.0 (released December 2017)
|
| 272 |
+
Provides full Python 3 compatibility, updated documentation, and minor enhancements and bug fixes.
|
| 273 |
+
|
| 274 |
+
Version 0.5.5 (released early April 2017)
|
| 275 |
+
Finally provides easy binary distribution with conda_
|
| 276 |
+
|
| 277 |
+
Version 0.5.2 (released late March 2017)
|
| 278 |
+
Many under-the-hood improvements to the build procedure,
|
| 279 |
+
which should make it much easier to get `climlab` installed on user machines.
|
| 280 |
+
Binary distribution with conda_ is coming soon!
|
| 281 |
+
|
| 282 |
+
Version 0.5 (released March 2017)
|
| 283 |
+
Bug fixes and full functionality for the RRTMG radiation module,
|
| 284 |
+
an improved common API for all radiation modules, and better documentation.
|
| 285 |
+
|
| 286 |
+
Version 0.4.2 (released January 2017)
|
| 287 |
+
Introduces the RRTMG radiation scheme,
|
| 288 |
+
a much-improved build process for the Fortran extension,
|
| 289 |
+
and numerous enhancements and simplifications to the API.
|
| 290 |
+
|
| 291 |
+
Version 0.4 (released October 2016)
|
| 292 |
+
Includes comprehensive documentation, an automated test suite,
|
| 293 |
+
support for latitude-longitude grids, and numerous small enhancements and bug fixes.
|
| 294 |
+
|
| 295 |
+
Version 0.3 (released February 2016)
|
| 296 |
+
Includes many internal changes and some backwards-incompatible changes
|
| 297 |
+
(hopefully simplifications) to the public API.
|
| 298 |
+
It also includes the CAM3 radiation module.
|
| 299 |
+
|
| 300 |
+
Version 0.2 (released January 2015)
|
| 301 |
+
The package and its API was completely redesigned around a truly object-oriented
|
| 302 |
+
modeling framework in January 2015.
|
| 303 |
+
|
| 304 |
+
It was used extensively for a graduate-level climate modeling course in Spring 2015:
|
| 305 |
+
http://www.atmos.albany.edu/facstaff/brose/classes/ATM623_Spring2015/
|
| 306 |
+
|
| 307 |
+
Many more examples are found in the online lecture notes for that course:
|
| 308 |
+
http://nbviewer.jupyter.org/github/brian-rose/ClimateModeling_courseware/blob/master/index.ipynb
|
| 309 |
+
|
| 310 |
+
Version 0.1
|
| 311 |
+
The first versions of the code and notebooks were originally developed in winter / spring 2014
|
| 312 |
+
in support of an undergraduate course at the University at Albany.
|
| 313 |
+
|
| 314 |
+
See the original course webpage at
|
| 315 |
+
http://www.atmos.albany.edu/facstaff/brose/classes/ENV480_Spring2014/
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
The documentation_ was first created by Moritz Kreuzer
|
| 319 |
+
(Potsdam Institut for Climate Impact Research) as part of a thesis project in Spring 2016.
|
| 320 |
+
|
| 321 |
+
.. _documentation: http://climlab.readthedocs.io
|
| 322 |
+
.. _xarray: http://xarray.pydata.org/en/stable/
|
| 323 |
+
.. _pooch: https://www.fatiando.org/pooch/latest/index.html
|
| 324 |
+
.. _`tutorials in the docs`: https://climlab.readthedocs.io/en/latest/tutorial.html
|
| 325 |
+
.. _here: http://climlab.readthedocs.io
|
| 326 |
+
.. _`The Climate Laboratory`: https://brian-rose.github.io/ClimateLaboratoryBook/
|
| 327 |
+
.. _`attrdict package`: https://github.com/bcj/AttrDict
|
| 328 |
+
.. _`Brendan Curran-Johnson`: https://github.com/bcj
|
| 329 |
+
.. _`release notes`: https://github.com/climlab/climlab/releases
|
| 330 |
+
|
| 331 |
+
Contact and Bug Reports
|
| 332 |
+
-----------------------
|
| 333 |
+
Users are strongly encouraged to submit bug reports and feature requests on
|
| 334 |
+
github at https://github.com/climlab/climlab
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
License
|
| 338 |
+
-------
|
| 339 |
+
This code is freely available under the MIT license.
|
| 340 |
+
See the accompanying LICENSE file.
|
| 341 |
+
|
| 342 |
+
.. |JOSS| image:: http://joss.theoj.org/papers/10.21105/joss.00659/status.svg
|
| 343 |
+
:target: https://doi.org/10.21105/joss.00659
|
| 344 |
+
.. |pypi| image:: https://badge.fury.io/py/climlab.svg
|
| 345 |
+
:target: https://badge.fury.io/py/climlab
|
| 346 |
+
.. |Build Status| image:: https://github.com/climlab/climlab/actions/workflows/build-and-test.yml/badge.svg
|
| 347 |
+
:target: https://github.com/climlab/climlab/actions/workflows/build-and-test.yml
|
| 348 |
+
.. |coverage| image:: https://codecov.io/github/climlab/climlab/coverage.svg?branch=main
|
| 349 |
+
:target: https://codecov.io/github/climlab/climlab?branch=main
|
| 350 |
+
.. |DOI| image:: https://zenodo.org/badge/24968065.svg
|
| 351 |
+
:target: https://zenodo.org/badge/latestdoi/24968065
|
| 352 |
+
.. |docs| image:: http://readthedocs.org/projects/climlab/badge/?version=latest
|
| 353 |
+
:target: http://climlab.readthedocs.io/en/latest/intro.html
|
| 354 |
+
:alt: Documentation Status
|
| 355 |
+
.. _`climlab-rrtmg`: https://github.com/climlab/climlab-rrtmg
|
| 356 |
+
.. _`climlab-cam3-radiation`: https://github.com/climlab/climlab-cam3-radiation
|
| 357 |
+
.. _`climlab-emanuel-convection`: https://github.com/climlab/climlab-emanuel-convection
|
| 358 |
+
.. _`multiple time-averaging methods for solar zenith angle`: https://climlab.readthedocs.io/en/latest/api/climlab.solar.insolation.html#climlab.solar.insolation.daily_insolation_factors
|
| 359 |
+
.. _`Frierson (2007)`: https://doi.org/10.1175/JAS3935.1
|
| 360 |
+
.. _Limiter: https://climlab.readthedocs.io/en/latest/api/climlab.process.limiter.html
|
| 361 |
+
.. _SimplifiedBettsMiller: https://climlab.readthedocs.io/en/latest/api/climlab.convection.SimplifiedBettsMiller.html
|
| 362 |
+
.. _LargeScaleCondensation: https://climlab.readthedocs.io/en/latest/api/climlab.dynamics.LargeScaleCondensation.html
|
| 363 |
+
.. _`additive assumption for same-named diagnostics`: https://climlab.readthedocs.io/en/latest/architecture.html#additive-diagnostics-for-subprocesses
|
| 364 |
+
|
| 365 |
+
=======
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
Support
|
| 369 |
+
-------
|
| 370 |
+
Development of ``climlab`` is partially supported by the National Science Foundation under award AGS-1455071 to Brian Rose.
|
| 371 |
+
|
| 372 |
+
Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.
|
climlab/source/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
climlab Project Package Initialization File
|
| 4 |
+
"""
|
climlab/source/climlab/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
This chapter documents the source code of the ``climlab`` package.
|
| 3 |
+
The focus is on the methods and functions that the user invokes
|
| 4 |
+
while using the package.
|
| 5 |
+
|
| 6 |
+
Nevertheless also the underlying code of the ``climlab`` architecture
|
| 7 |
+
has been documented for a comprehensive understanding and traceability.
|
| 8 |
+
'''
|
| 9 |
+
# Version number is declared in setup.py
|
| 10 |
+
try:
|
| 11 |
+
from importlib import metadata
|
| 12 |
+
__version__ = metadata.version(__name__)
|
| 13 |
+
except ImportError: # for Python < 3.8, importlib.metadata will not work
|
| 14 |
+
from pkg_resources import get_distribution
|
| 15 |
+
__version__ = get_distribution(__name__).version
|
| 16 |
+
|
| 17 |
+
# this should ensure that we can still import constants.py as climlab.constants
|
| 18 |
+
from .utils import constants, thermo, legendre
|
| 19 |
+
# some more useful shorcuts
|
| 20 |
+
from .model.column import GreyRadiationModel, RadiativeConvectiveModel, BandRCModel
|
| 21 |
+
from .model.ebm import EBM, EBM_annual, EBM_seasonal
|
| 22 |
+
from .domain.field import Field, global_mean
|
| 23 |
+
from .domain.axis import Axis
|
| 24 |
+
from .domain.initial import column_state, surface_state
|
| 25 |
+
from .process import Process, TimeDependentProcess, ImplicitProcess, DiagnosticProcess, EnergyBudget
|
| 26 |
+
from .process import process_like, get_axes, couple
|
| 27 |
+
from .domain.xarray import to_xarray
|
climlab/source/climlab/convection/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
Modules for atmospheric convection.
|
| 3 |
+
|
| 4 |
+
For simple adjustment of temperature to a prescribed lapse rate, use :class:`~climlab.convection.ConvectiveAdjustment`
|
| 5 |
+
|
| 6 |
+
For a full convection scheme including interactive water vapor, use :class:`~climlab.convection.EmanuelConvection`
|
| 7 |
+
'''
|
| 8 |
+
from .convadj import ConvectiveAdjustment
|
| 9 |
+
from .emanuel_convection import EmanuelConvection
|
| 10 |
+
from .simplified_betts_miller import SimplifiedBettsMiller
|
climlab/source/climlab/convection/akmaev_adjustment.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from climlab import constants as const
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def convective_adjustment_direct(p, T, c, lapserate=6.5):
|
| 7 |
+
"""Convective Adjustment to a specified lapse rate.
|
| 8 |
+
|
| 9 |
+
Input argument lapserate gives the lapse rate expressed in degrees K per km
|
| 10 |
+
(positive means temperature increasing downward).
|
| 11 |
+
|
| 12 |
+
Default lapse rate is 6.5 K / km.
|
| 13 |
+
|
| 14 |
+
Returns the adjusted Column temperature.
|
| 15 |
+
inputs:
|
| 16 |
+
p is pressure in hPa
|
| 17 |
+
T is temperature in K
|
| 18 |
+
c is heat capacity in in J / m**2 / K
|
| 19 |
+
|
| 20 |
+
Implements the conservative adjustment algorithm from Akmaev (1991) MWR
|
| 21 |
+
"""
|
| 22 |
+
# make sure lapserate has same dimensionality as T
|
| 23 |
+
lapserate = lapserate * np.ones_like(T)
|
| 24 |
+
# largely follows notation and algorithm in Akmaev (1991) MWR
|
| 25 |
+
alpha = const.Rd / const.g * lapserate / 1.E3 # same dimensions as lapserate
|
| 26 |
+
L = p.size
|
| 27 |
+
### now handles variable lapse rate in multiple dimensions
|
| 28 |
+
# prepend const.ps = 1000 hPa as ref pressure to compute potential temperature
|
| 29 |
+
pextended = np.insert(p,0,const.ps)
|
| 30 |
+
# For now, let's assume that the vertical axis is the last axis
|
| 31 |
+
Pi = np.cumprod((p / pextended[:-1])**alpha, axis=-1) # Akmaev's equation 14 recurrence formula
|
| 32 |
+
beta = 1./Pi
|
| 33 |
+
theta = T * beta
|
| 34 |
+
q = Pi * c
|
| 35 |
+
n_k = np.zeros(L, dtype=int)
|
| 36 |
+
theta_k = np.zeros_like(p)
|
| 37 |
+
s_k = np.zeros_like(p)
|
| 38 |
+
t_k = np.zeros_like(p)
|
| 39 |
+
thetaadj = Akmaev_adjustment_multidim(theta, q, beta, n_k,
|
| 40 |
+
theta_k, s_k, t_k)
|
| 41 |
+
T = thetaadj * Pi
|
| 42 |
+
return T
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def Akmaev_adjustment_multidim(theta, q, beta, n_k, theta_k, s_k, t_k):
|
| 46 |
+
num_lev = theta.shape[-1] # number of vertical levels
|
| 47 |
+
otherdims = theta.shape[:-1] # everything except last dimension, which we assume is vertical
|
| 48 |
+
if otherdims != ():
|
| 49 |
+
othersize = np.prod(otherdims)
|
| 50 |
+
theta_reshape = theta.reshape((othersize, num_lev))
|
| 51 |
+
q_reshape = q.reshape((othersize, num_lev))
|
| 52 |
+
beta_reshape = beta.reshape((othersize, num_lev))
|
| 53 |
+
for n in range(othersize):
|
| 54 |
+
theta_reshape[n,:] = Akmaev_adjustment(theta_reshape[n,:],
|
| 55 |
+
q_reshape[n,:], beta_reshape[n,:], n_k, theta_k, s_k, t_k)
|
| 56 |
+
theta = theta_reshape.reshape(theta.shape)
|
| 57 |
+
else:
|
| 58 |
+
theta = Akmaev_adjustment(theta, q, beta, n_k, theta_k, s_k, t_k)
|
| 59 |
+
return theta
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def Akmaev_adjustment(theta, q, beta, n_k, theta_k, s_k, t_k):
|
| 63 |
+
'''Single column only.'''
|
| 64 |
+
L = q.size # number of vertical levels
|
| 65 |
+
# Akmaev step 1
|
| 66 |
+
k = 1
|
| 67 |
+
n_k[k-1] = 1
|
| 68 |
+
theta_k[k-1] = theta[k-1]
|
| 69 |
+
l = 2
|
| 70 |
+
while True:
|
| 71 |
+
# Akmaev step 2
|
| 72 |
+
n = 1
|
| 73 |
+
thistheta = theta[l-1]
|
| 74 |
+
while True:
|
| 75 |
+
# Akmaev step 3
|
| 76 |
+
if theta_k[k-1] <= thistheta:
|
| 77 |
+
# Akmaev step 6
|
| 78 |
+
k += 1
|
| 79 |
+
break # to step 7
|
| 80 |
+
else:
|
| 81 |
+
if n <= 1:
|
| 82 |
+
s = q[l-1]
|
| 83 |
+
t = s*thistheta
|
| 84 |
+
# Akmaev step 4
|
| 85 |
+
if n_k[k-1] <= 1:
|
| 86 |
+
# lower adjacent level is not an earlier-formed neutral layer
|
| 87 |
+
s_k[k-1] = q[l-n-1]
|
| 88 |
+
t_k[k-1] = s_k[k-1] * theta_k[k-1]
|
| 89 |
+
# Akmaev step 5
|
| 90 |
+
# join current and underlying layers
|
| 91 |
+
n += n_k[k-1]
|
| 92 |
+
s += s_k[k-1]
|
| 93 |
+
t += t_k[k-1]
|
| 94 |
+
s_k[k-1] = s
|
| 95 |
+
t_k[k-1] = t
|
| 96 |
+
thistheta = t/s
|
| 97 |
+
if k==1:
|
| 98 |
+
# joint neutral layer is the first one
|
| 99 |
+
break # to step 7
|
| 100 |
+
k -= 1
|
| 101 |
+
# back to step 3
|
| 102 |
+
# Akmaev step 7
|
| 103 |
+
if l == L: # the scan is over
|
| 104 |
+
break # to step 8
|
| 105 |
+
l += 1
|
| 106 |
+
n_k[k-1] = n
|
| 107 |
+
theta_k[k-1] = thistheta
|
| 108 |
+
# back to step 2
|
| 109 |
+
|
| 110 |
+
# update the potential temperatures
|
| 111 |
+
while True:
|
| 112 |
+
while True:
|
| 113 |
+
# Akmaev step 8
|
| 114 |
+
if n==1: # current model level was not included in any neutral layer
|
| 115 |
+
break # to step 11
|
| 116 |
+
while True:
|
| 117 |
+
# Akmaev step 9
|
| 118 |
+
theta[l-1] = thistheta
|
| 119 |
+
if n==1:
|
| 120 |
+
break
|
| 121 |
+
# Akmaev step 10
|
| 122 |
+
l -= 1
|
| 123 |
+
n -= 1
|
| 124 |
+
# back to step 9
|
| 125 |
+
# Akmaev step 11
|
| 126 |
+
if k==1:
|
| 127 |
+
break
|
| 128 |
+
k -= 1
|
| 129 |
+
l -= 1
|
| 130 |
+
n = n_k[k-1]
|
| 131 |
+
thistheta = theta_k[k-1]
|
| 132 |
+
# back to step 8
|
| 133 |
+
return theta
|
| 134 |
+
|
| 135 |
+
# Attempt to use numba to compile the Akmaev_adjustment function
|
| 136 |
+
# which gives at least 10x speedup
|
| 137 |
+
# If numba is not available or compilation fails, the code will be executed
|
| 138 |
+
# in pure Python. Results should be identical
|
| 139 |
+
try:
|
| 140 |
+
from numba import jit
|
| 141 |
+
Akmaev_adjustment = jit(signature_or_function=Akmaev_adjustment, nopython=True)
|
| 142 |
+
except ImportError:
|
| 143 |
+
pass
|
climlab/source/climlab/convection/convadj.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from builtins import range
|
| 2 |
+
import numpy as np
|
| 3 |
+
from climlab import constants as const
|
| 4 |
+
from climlab.utils.thermo import rho_moist, pseudoadiabat
|
| 5 |
+
from climlab.process.time_dependent_process import TimeDependentProcess
|
| 6 |
+
from climlab.domain.field import Field
|
| 7 |
+
from .akmaev_adjustment import convective_adjustment_direct
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ConvectiveAdjustment(TimeDependentProcess):
|
| 11 |
+
'''Hard Convective Adjustment to a prescribed lapse rate.
|
| 12 |
+
|
| 13 |
+
This process computes the instantaneous adjustment to conservatively
|
| 14 |
+
remove any instabilities in each column.
|
| 15 |
+
|
| 16 |
+
Instability is defined as a temperature decrease with height that exceeds
|
| 17 |
+
the prescribed critical lapse rate. This critical rate is set by input argument
|
| 18 |
+
``adj_lapse_rate``, which can be either a numerical or string value.
|
| 19 |
+
|
| 20 |
+
Numerical values for ``adj_lapse_rate`` are given in units of K / km. Both
|
| 21 |
+
array and scalar values are valid. For scalar values, the assumption is that
|
| 22 |
+
the critical lapse rate is the same at every level.
|
| 23 |
+
|
| 24 |
+
If an array is given, it is assumed to represent the in-situ critical lapse
|
| 25 |
+
rate (in K/km) at every grid point.
|
| 26 |
+
|
| 27 |
+
Alternatively, string arguments can be given as follows:
|
| 28 |
+
|
| 29 |
+
- ``'DALR'`` or ``'dry adiabat'``: critical lapse rate is set to g/cp = 9.8 K / km
|
| 30 |
+
- ``'MALR'`` or ``'moist adiabat'`` or ``'pseudoadiabat'``: critical lapse rate follows the in-situ moist pseudoadiabat at every level
|
| 31 |
+
|
| 32 |
+
Adjustment includes the surface if ``'Ts'`` is included in the ``state``
|
| 33 |
+
dictionary. This implicitly accounts for turbulent surface fluxes.
|
| 34 |
+
Otherwise only the atmospheric temperature is adjusted.
|
| 35 |
+
|
| 36 |
+
If ``adj_lapse_rate`` is an array, its size must match the number of vertical
|
| 37 |
+
levels of the adjustment. This is number of pressure levels if the surface is
|
| 38 |
+
not adjusted, or number of pressure levels + 1 if the surface is adjusted.
|
| 39 |
+
|
| 40 |
+
This process implements the conservative adjustment algorithm described in
|
| 41 |
+
Akmaev (1991) Monthly Weather Review.
|
| 42 |
+
'''
|
| 43 |
+
def __init__(self, adj_lapse_rate=None, **kwargs):
|
| 44 |
+
super(ConvectiveAdjustment, self).__init__(**kwargs)
|
| 45 |
+
# lapse rate for convective adjustment, in K / km
|
| 46 |
+
self.adj_lapse_rate = adj_lapse_rate
|
| 47 |
+
self.param['adj_lapse_rate'] = adj_lapse_rate
|
| 48 |
+
self.time_type = 'adjustment'
|
| 49 |
+
self.adjustment = {}
|
| 50 |
+
@property
|
| 51 |
+
def pcol(self):
|
| 52 |
+
patm = self.lev
|
| 53 |
+
if 'Ts' in self.state:
|
| 54 |
+
# surface pressure should correspond to model domain!
|
| 55 |
+
ps = self.lev_bounds[-1]
|
| 56 |
+
return np.append(patm, ps)
|
| 57 |
+
else:
|
| 58 |
+
return patm
|
| 59 |
+
@property
|
| 60 |
+
def ccol(self):
|
| 61 |
+
c_atm = self.Tatm.domain.heat_capacity
|
| 62 |
+
if 'Ts' in self.state:
|
| 63 |
+
c_sfc = self.Ts.domain.heat_capacity
|
| 64 |
+
return np.append(c_atm, c_sfc)
|
| 65 |
+
else:
|
| 66 |
+
return c_atm
|
| 67 |
+
@property
|
| 68 |
+
def Tcol(self):
|
| 69 |
+
# For now, let's assume that the vertical axis is the last axis
|
| 70 |
+
Tatm = self.Tatm
|
| 71 |
+
if 'Ts' in self.state:
|
| 72 |
+
Ts = np.atleast_1d(self.Ts)
|
| 73 |
+
return np.concatenate((Tatm, Ts),axis=-1)
|
| 74 |
+
else:
|
| 75 |
+
return Tatm
|
| 76 |
+
@property
|
| 77 |
+
def adj_lapse_rate(self):
|
| 78 |
+
lapserate = self._adj_lapse_rate
|
| 79 |
+
if type(lapserate) is str:
|
| 80 |
+
if lapserate in ['DALR', 'dry adiabat']:
|
| 81 |
+
return const.g / const.cp * 1.E3
|
| 82 |
+
elif lapserate in ['MALR', 'moist adiabat', 'pseudoadiabat']:
|
| 83 |
+
# critical lapse rate at each level is set by pseudoadiabat
|
| 84 |
+
dTdp = pseudoadiabat(self.Tcol,self.pcol) / 100. # K / Pa
|
| 85 |
+
# Could include water vapor effect on density here ...
|
| 86 |
+
# Replace Tcol with virtual temperature
|
| 87 |
+
rho = self.pcol*100./const.Rd/self.Tcol # in kg/m**3
|
| 88 |
+
return dTdp * const.g * rho * 1000. # K / km
|
| 89 |
+
else:
|
| 90 |
+
raise ValueError('adj_lapse_rate must be either numeric or any of \'DALR\', \'dry adiabat\', \'MALR\', \'moist adiabat\', \'pseudoadiabat\'.')
|
| 91 |
+
else:
|
| 92 |
+
return lapserate
|
| 93 |
+
@adj_lapse_rate.setter
|
| 94 |
+
def adj_lapse_rate(self, lapserate):
|
| 95 |
+
self._adj_lapse_rate = lapserate
|
| 96 |
+
self.param['adj_lapse_rate'] = lapserate
|
| 97 |
+
|
| 98 |
+
def _compute(self):
|
| 99 |
+
if self.adj_lapse_rate is None:
|
| 100 |
+
self.adjustment['Ts'] = self.Ts * 0.
|
| 101 |
+
self.adjustment['Tatm'] = self.Tatm * 0.
|
| 102 |
+
else:
|
| 103 |
+
# convective adjustment routine expect reversered vertical axis
|
| 104 |
+
pflip = self.pcol[..., ::-1]
|
| 105 |
+
Tflip = self.Tcol[..., ::-1]
|
| 106 |
+
cflip = self.ccol[..., ::-1]
|
| 107 |
+
lapseflip = np.atleast_1d(self.adj_lapse_rate)[..., ::-1]
|
| 108 |
+
Tadj_flip = convective_adjustment_direct(pflip, Tflip, cflip, lapserate=lapseflip)
|
| 109 |
+
Tadj = Tadj_flip[..., ::-1]
|
| 110 |
+
if 'Ts' in self.state:
|
| 111 |
+
Ts = Field(Tadj[...,-1], domain=self.Ts.domain)
|
| 112 |
+
Tatm = Field(Tadj[...,:-1], domain=self.Tatm.domain)
|
| 113 |
+
self.adjustment['Ts'] = Ts - self.Ts
|
| 114 |
+
else:
|
| 115 |
+
Tatm = Field(Tadj, domain=self.Tatm.domain)
|
| 116 |
+
self.adjustment['Tatm'] = Tatm - self.Tatm
|
| 117 |
+
# return the adjustment, independent of timestep
|
| 118 |
+
# because the parent process might have set a different timestep!
|
| 119 |
+
return self.adjustment
|
climlab/source/climlab/convection/emanuel_convection.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
A climlab process for the Emanuel convection scheme
|
| 3 |
+
'''
|
| 4 |
+
import numpy as np
|
| 5 |
+
import warnings
|
| 6 |
+
from climlab.process import TimeDependentProcess
|
| 7 |
+
from climlab.utils.thermo import qsat
|
| 8 |
+
from climlab import constants as const
|
| 9 |
+
try:
|
| 10 |
+
from climlab_emanuel_convection import emanuel_convection as convect
|
| 11 |
+
except:
|
| 12 |
+
warnings.warn('Cannot import EmanuelConvection fortran extension, this module will not be functional.')
|
| 13 |
+
# The array conversion routines we are borrowing from the RRTMG wrapper
|
| 14 |
+
from climlab.radiation.rrtm.utils import _climlab_to_rrtm as _climlab_to_convect
|
| 15 |
+
from climlab.radiation.rrtm.utils import _rrtm_to_climlab as _convect_to_climlab
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# Thermodynamic constants
|
| 19 |
+
CPD = const.cp
|
| 20 |
+
CPV = const.cpv
|
| 21 |
+
RV = const.Rv
|
| 22 |
+
RD = const.Rd
|
| 23 |
+
LV0 = const.Lhvap
|
| 24 |
+
G = const.g
|
| 25 |
+
ROWL = const.rho_w
|
| 26 |
+
# specific heat of liquid water -- artifically small!
|
| 27 |
+
# Kerry Emanuel's notes say this is intentional, do not change this.
|
| 28 |
+
CL=2500.0
|
| 29 |
+
#CPV = CPD # try neglecting effect of water vapor on heat capacity
|
| 30 |
+
|
| 31 |
+
class EmanuelConvection(TimeDependentProcess):
|
| 32 |
+
'''
|
| 33 |
+
The climlab wrapper for Kerry Emanuel's moist convection scheme <https://emanuel.mit.edu/FORTRAN-subroutine-convect>
|
| 34 |
+
|
| 35 |
+
From the documentation distributed with the Fortran 77 code CONVECT:
|
| 36 |
+
|
| 37 |
+
The subroutine is designed to be used in time-marching models of mesoscale to global-scale dimensions.
|
| 38 |
+
It is meant to represent the effects of all moist convection, including shallow, non-precipitating cumulus.
|
| 39 |
+
It also contains a dry adiabatic adjustment scheme.
|
| 40 |
+
|
| 41 |
+
Since the method of calculating the convective fluxes involves a relaxation toward quasi-equilibrium,
|
| 42 |
+
subroutine CONVECT must be run for at least several time steps to give meaningful results.
|
| 43 |
+
At the first time step, the tendencies and convective precipitation will be zero.
|
| 44 |
+
If the initial sounding is unstable, these will rapidly increase over successive time steps,
|
| 45 |
+
depending on the values of the constants ALPHA and DAMP.
|
| 46 |
+
Thus the user interested in convective fluxes and precipitation
|
| 47 |
+
associated with a single initial sounding (i.e., without large-scale forcing)
|
| 48 |
+
should still march CONVECT forward enough time steps that the fluxes have
|
| 49 |
+
returned back to zero;
|
| 50 |
+
the net tendencies and precipitation integrated over this time interval are then the desired results.
|
| 51 |
+
But it should be cautioned that these quantities will not necessarily be
|
| 52 |
+
independent of other model parameters such as the time step.
|
| 53 |
+
CONVECT is very much built on the philosophy that convection,
|
| 54 |
+
to the extent it can be represented in terms of large-scale variables,
|
| 55 |
+
is never very far away from statistical equilibrium with the large-scale flow.
|
| 56 |
+
To achieve a smooth evolution of the convective forcing,
|
| 57 |
+
CONVECT should be called at least every 20 minutes during the time integration.
|
| 58 |
+
CONVECT will work at longer time intervals, but the convective tendencies may become noisy.
|
| 59 |
+
|
| 60 |
+
Basic characteristics:
|
| 61 |
+
|
| 62 |
+
State:
|
| 63 |
+
|
| 64 |
+
- ``Ts``: surface radiative temperature -- optional, and ignored
|
| 65 |
+
- ``Tatm``: air temperature in K
|
| 66 |
+
- ``q``: specific humidity in kg kg\ :sup:`-1`
|
| 67 |
+
- ``U``: zonal velocity in m s\ :sup:`-1` (optional)
|
| 68 |
+
- ``V``: meridional velocity in m s\ :sup:`-1` (optional)
|
| 69 |
+
|
| 70 |
+
Input arguments and default values (taken from convect43.f fortran source):
|
| 71 |
+
|
| 72 |
+
- ``MINORIG = 0``, index of lowest level from which convection may originate (zero means lowest)
|
| 73 |
+
- ``ELCRIT = 0.0011``, autoconversion threshold water content (g/g)
|
| 74 |
+
- ``TLCRIT = -55.0``, critical temperature below which the auto-conversion threshold is assumed to be zero (the autoconversion threshold varies linearly between 0 C and TLCRIT)
|
| 75 |
+
- ``ENTP = 1.5``, coefficient of mixing in the entrainment formulation
|
| 76 |
+
- ``SIGD = 0.05``, fractional area covered by unsaturated downdraft
|
| 77 |
+
- ``SIGS = 0.12``, fraction of precipitation falling outside of cloud
|
| 78 |
+
- ``OMTRAIN = 50.0``, assumed fall speed (Pa/s) of rain
|
| 79 |
+
- ``OMTSNOW = 5.5``, assumed fall speed (Pa/s) of snow
|
| 80 |
+
- ``COEFFR = 1.0``, coefficient governing the rate of evaporation of rain
|
| 81 |
+
- ``COEFFS = 0.8``, coefficient governing the rate of evaporation of snow
|
| 82 |
+
- ``CU = 0.7``, coefficient governing convective momentum transport
|
| 83 |
+
- ``BETA = 10.0``, coefficient used in downdraft velocity scale calculation
|
| 84 |
+
- ``DTMAX = 0.9``, maximum negative temperature perturbation a lifted parcel is allowed to have below its LFC
|
| 85 |
+
- ``ALPHA = 0.2``, first parameter that controls the rate of approach to quasi-equilibrium
|
| 86 |
+
- ``DAMP = 0.1``, second parameter that controls the rate of approach to quasi-equilibrium (DAMP must be less than 1)
|
| 87 |
+
- ``IPBL = 0``, switch to bypass the dry convective adjustment (bypass if IPBL==0)
|
| 88 |
+
|
| 89 |
+
Tendencies computed:
|
| 90 |
+
|
| 91 |
+
- air temperature (K s\ :sup:`-1`)
|
| 92 |
+
- specific humidity (kg kg\ :sup:`-1` s\ :sup:`-1`)
|
| 93 |
+
- optional:
|
| 94 |
+
- U and V wind components (m s\ :sup:`-1` s\ :sup:`-1`), if ``U`` and ``V`` are included in state dictionary
|
| 95 |
+
|
| 96 |
+
Diagnostics computed:
|
| 97 |
+
|
| 98 |
+
- ``CBMF`` (cloud base mass flux in kg m\ :sup:`-2` s\ :sup:`-1`) -- this is actually stored internally and used as input for subsequent timesteps
|
| 99 |
+
- ``precipitation`` (convective precipitation rate in kg m\ :sup:`-2` s\ :sup:`-1` or mm s\ :sup:`-1`)
|
| 100 |
+
- ``relative_humidity`` (dimensionless)
|
| 101 |
+
|
| 102 |
+
:Example:
|
| 103 |
+
|
| 104 |
+
Here is an example of setting up a single-column
|
| 105 |
+
Radiative-Convective model with interactive water vapor.
|
| 106 |
+
|
| 107 |
+
This example also demonstrates *asynchronous coupling*:
|
| 108 |
+
the radiation uses a longer timestep than the other model components::
|
| 109 |
+
|
| 110 |
+
import numpy as np
|
| 111 |
+
import climlab
|
| 112 |
+
from climlab import constants as const
|
| 113 |
+
# Temperatures in a single column
|
| 114 |
+
full_state = climlab.column_state(num_lev=30, water_depth=2.5)
|
| 115 |
+
temperature_state = {'Tatm':full_state.Tatm,'Ts':full_state.Ts}
|
| 116 |
+
# Initialize a nearly dry column (small background stratospheric humidity)
|
| 117 |
+
q = np.ones_like(full_state.Tatm) * 5.E-6
|
| 118 |
+
# Add specific_humidity to the state dictionary
|
| 119 |
+
full_state['q'] = q
|
| 120 |
+
# ASYNCHRONOUS COUPLING -- the radiation uses a much longer timestep
|
| 121 |
+
# The top-level model
|
| 122 |
+
model = climlab.TimeDependentProcess(state=full_state,
|
| 123 |
+
timestep=const.seconds_per_hour)
|
| 124 |
+
# Radiation coupled to water vapor
|
| 125 |
+
rad = climlab.radiation.RRTMG(state=temperature_state,
|
| 126 |
+
specific_humidity=full_state.q,
|
| 127 |
+
albedo=0.3,
|
| 128 |
+
timestep=const.seconds_per_day
|
| 129 |
+
)
|
| 130 |
+
# Convection scheme -- water vapor is a state variable
|
| 131 |
+
conv = climlab.convection.EmanuelConvection(state=full_state,
|
| 132 |
+
timestep=const.seconds_per_hour)
|
| 133 |
+
# Surface heat flux processes
|
| 134 |
+
shf = climlab.surface.SensibleHeatFlux(state=temperature_state, Cd=0.5E-3,
|
| 135 |
+
timestep=const.seconds_per_hour)
|
| 136 |
+
lhf = climlab.surface.LatentHeatFlux(state=full_state, Cd=0.5E-3,
|
| 137 |
+
timestep=const.seconds_per_hour)
|
| 138 |
+
# Couple all the submodels together
|
| 139 |
+
model.add_subprocess('Radiation', rad)
|
| 140 |
+
model.add_subprocess('Convection', conv)
|
| 141 |
+
model.add_subprocess('SHF', shf)
|
| 142 |
+
model.add_subprocess('LHF', lhf)
|
| 143 |
+
print(model)
|
| 144 |
+
|
| 145 |
+
# Run the model
|
| 146 |
+
model.integrate_years(1)
|
| 147 |
+
# Check for energy balance
|
| 148 |
+
print(model.ASR - model.OLR)
|
| 149 |
+
'''
|
| 150 |
+
def __init__(self,
|
| 151 |
+
MINORIG = 0, # index of lowest level from which convection may originate (zero means lowest)
|
| 152 |
+
# Default parameter values taken from convect43c.f fortran source
|
| 153 |
+
ELCRIT=.0011,
|
| 154 |
+
TLCRIT=-55.0,
|
| 155 |
+
ENTP=1.5,
|
| 156 |
+
SIGD=0.05,
|
| 157 |
+
SIGS=0.12,
|
| 158 |
+
OMTRAIN=50.0,
|
| 159 |
+
OMTSNOW=5.5,
|
| 160 |
+
COEFFR=1.0,
|
| 161 |
+
COEFFS=0.8,
|
| 162 |
+
CU=0.7,
|
| 163 |
+
BETA=10.0,
|
| 164 |
+
DTMAX=0.9,
|
| 165 |
+
ALPHA=0.2,
|
| 166 |
+
DAMP=0.1,
|
| 167 |
+
IPBL=0,
|
| 168 |
+
**kwargs):
|
| 169 |
+
super(EmanuelConvection, self).__init__(**kwargs)
|
| 170 |
+
self.time_type = 'explicit'
|
| 171 |
+
# Define inputs and diagnostics
|
| 172 |
+
surface_shape = self.state['Tatm'][...,0].shape
|
| 173 |
+
# Hack to handle single column and multicolumn
|
| 174 |
+
if surface_shape == ():
|
| 175 |
+
init = np.atleast_1d(np.zeros(surface_shape))
|
| 176 |
+
self.multidim=False
|
| 177 |
+
else:
|
| 178 |
+
init = np.zeros(surface_shape)[...,np.newaxis]
|
| 179 |
+
self.multidim=True
|
| 180 |
+
self.add_diagnostic('CBMF', init*0.) # cloud base mass flux
|
| 181 |
+
self.add_diagnostic('precipitation', init*0.) # Precip rate (kg/m2/s)
|
| 182 |
+
self.add_diagnostic('relative_humidity', 0*self.Tatm)
|
| 183 |
+
self.add_input('MINORIG', MINORIG)
|
| 184 |
+
self.add_input('ELCRIT', ELCRIT)
|
| 185 |
+
self.add_input('TLCRIT', TLCRIT)
|
| 186 |
+
self.add_input('ENTP', ENTP)
|
| 187 |
+
self.add_input('SIGD', SIGD)
|
| 188 |
+
self.add_input('SIGS', SIGS)
|
| 189 |
+
self.add_input('OMTRAIN', OMTRAIN)
|
| 190 |
+
self.add_input('OMTSNOW', OMTSNOW)
|
| 191 |
+
self.add_input('COEFFR', COEFFR)
|
| 192 |
+
self.add_input('COEFFS', COEFFS)
|
| 193 |
+
self.add_input('CU', CU)
|
| 194 |
+
self.add_input('BETA', BETA)
|
| 195 |
+
self.add_input('DTMAX', DTMAX)
|
| 196 |
+
self.add_input('ALPHA', ALPHA)
|
| 197 |
+
self.add_input('DAMP', DAMP)
|
| 198 |
+
self.add_input('IPBL', IPBL)
|
| 199 |
+
|
| 200 |
+
def _compute(self):
|
| 201 |
+
# Invert arrays so the first element is the bottom of column
|
| 202 |
+
T = _climlab_to_convect(self.state['Tatm'])
|
| 203 |
+
dom = self.state['Tatm'].domain
|
| 204 |
+
P = _climlab_to_convect(dom.lev.points)
|
| 205 |
+
PH = _climlab_to_convect(dom.lev.bounds)
|
| 206 |
+
Q = _climlab_to_convect(self.state['q'])
|
| 207 |
+
QS = qsat(T,P)
|
| 208 |
+
ND = np.size(T, axis=1)
|
| 209 |
+
NCOL = np.size(T, axis=0)
|
| 210 |
+
NL = ND-1
|
| 211 |
+
try:
|
| 212 |
+
U = _climlab_to_convect(self.state['U'])
|
| 213 |
+
except:
|
| 214 |
+
U = np.zeros_like(T)
|
| 215 |
+
try:
|
| 216 |
+
V = _climlab_to_convect(self.state['V'])
|
| 217 |
+
except:
|
| 218 |
+
V = np.zeros_like(T)
|
| 219 |
+
NTRA = 1
|
| 220 |
+
TRA = np.zeros((NCOL,ND,NTRA), order='F') # tracers ignored
|
| 221 |
+
DELT = self.timestep_in_seconds
|
| 222 |
+
CBMF = self.CBMF
|
| 223 |
+
(IFLAG, FT, FQ, FU, FV, FTRA, PRECIP, WD, TPRIME, QPRIME, CBMFnew,
|
| 224 |
+
Tout, Qout, QSout, Uout, Vout, TRAout) = \
|
| 225 |
+
convect(T, Q, QS, U, V, TRA, P, PH, NCOL, ND, NL, NTRA, DELT, self.IPBL, CBMF,
|
| 226 |
+
CPD, CPV, CL, RV, RD, LV0, G, ROWL, self.MINORIG,
|
| 227 |
+
self.ELCRIT, self.TLCRIT, self.ENTP, self.SIGD, self.SIGS,
|
| 228 |
+
self.OMTRAIN, self.OMTSNOW, self.COEFFR, self.COEFFS,
|
| 229 |
+
self.CU, self.BETA, self.DTMAX, self.ALPHA, self.DAMP
|
| 230 |
+
)
|
| 231 |
+
# If dry adjustment is being used then the tendencies need to be adjusted
|
| 232 |
+
if self.IPBL != 0:
|
| 233 |
+
FT += (Tout - T) / DELT
|
| 234 |
+
FQ += (Qout - Q) / DELT
|
| 235 |
+
tendencies = {'Tatm': _convect_to_climlab(FT)*np.ones_like(self.state['Tatm']),
|
| 236 |
+
'q': _convect_to_climlab(FQ)*np.ones_like(self.state['q'])}
|
| 237 |
+
if 'Ts' in self.state:
|
| 238 |
+
# for some strange reason self.Ts is breaking tests under Python 3.5 in some configurations
|
| 239 |
+
tendencies['Ts'] = 0. * self.state['Ts']
|
| 240 |
+
if 'U' in self.state:
|
| 241 |
+
tendencies['U'] = _convect_to_climlab(FU) * np.ones_like(self.state['U'])
|
| 242 |
+
if 'V' in self.state:
|
| 243 |
+
tendencies['V'] = _convect_to_climlab(FV) * np.ones_like(self.state['V'])
|
| 244 |
+
self.CBMF = CBMFnew
|
| 245 |
+
# Need to convert from mm/day to mm/s or kg/m2/s
|
| 246 |
+
# Hack to handle single column and multicolumn
|
| 247 |
+
if self.multidim:
|
| 248 |
+
self.precipitation[:,0] = _convect_to_climlab(PRECIP)/const.seconds_per_day
|
| 249 |
+
else:
|
| 250 |
+
self.precipitation[:] = _convect_to_climlab(PRECIP)/const.seconds_per_day
|
| 251 |
+
self.IFLAG = IFLAG
|
| 252 |
+
self.relative_humidity[:] = self.q / qsat(self.Tatm,self.lev)
|
| 253 |
+
return tendencies
|
climlab/source/climlab/convection/simplified_betts_miller.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
A climlab process for the Frierson Simplified Betts Miller convection scheme
|
| 3 |
+
|
| 4 |
+
:Example:
|
| 5 |
+
|
| 6 |
+
Here is an example of setting up a complete single-column
|
| 7 |
+
Radiative-Convective model with interactive water vapor.
|
| 8 |
+
The model includes the following processes:
|
| 9 |
+
|
| 10 |
+
- Constant insolation
|
| 11 |
+
- Longwave and Shortwave radiation
|
| 12 |
+
- Surface turbulent fluxes of sensible and latent heat
|
| 13 |
+
- Moist convection using the Simplified Betts Miller scheme
|
| 14 |
+
|
| 15 |
+
The state variables for this model will be surface temperature,
|
| 16 |
+
air temperature, and specific humidity.
|
| 17 |
+
This model has a simple but self-contained hydrological cycle:
|
| 18 |
+
water is evaporated from the surface and transported aloft by
|
| 19 |
+
the moist convection scheme.
|
| 20 |
+
|
| 21 |
+
The vertical distribution of temperature and humidity at
|
| 22 |
+
equilibrium will be determined by the interactions between
|
| 23 |
+
moist convection, radiation, and surface fluxes::
|
| 24 |
+
|
| 25 |
+
import numpy as np
|
| 26 |
+
import climlab
|
| 27 |
+
from climlab.utils import constants as const
|
| 28 |
+
|
| 29 |
+
num_lev = 30
|
| 30 |
+
water_depth = 10.
|
| 31 |
+
short_timestep = const.seconds_per_hour * 3
|
| 32 |
+
long_timestep = short_timestep*3
|
| 33 |
+
insolation = 342.
|
| 34 |
+
albedo = 0.18
|
| 35 |
+
|
| 36 |
+
# set initial conditions -- 24C at the surface, -60C at 200 hPa, isothermal stratosphere
|
| 37 |
+
strat_idx = 6
|
| 38 |
+
Tinitial = np.zeros(num_lev)
|
| 39 |
+
Tinitial[:strat_idx] = -60. + const.tempCtoK
|
| 40 |
+
Tinitial[strat_idx:] = np.linspace(-60, 22, num_lev-strat_idx) + const.tempCtoK
|
| 41 |
+
Tsinitial = 24. + const.tempCtoK
|
| 42 |
+
|
| 43 |
+
full_state = climlab.column_state(water_depth=water_depth, num_lev=num_lev)
|
| 44 |
+
full_state['Tatm'][:] = Tinitial
|
| 45 |
+
full_state['Ts'][:] = Tsinitial
|
| 46 |
+
|
| 47 |
+
# Initialize the model with a nearly dry atmosphere
|
| 48 |
+
qStrat = 5.E-6 # a very small background specific humidity value
|
| 49 |
+
full_state['q'] = 0.*full_state.Tatm + qStrat
|
| 50 |
+
|
| 51 |
+
temperature_state = {'Tatm':full_state.Tatm,'Ts':full_state.Ts}
|
| 52 |
+
# Surface model
|
| 53 |
+
shf = climlab.surface.SensibleHeatFlux(name='Sensible Heat Flux',
|
| 54 |
+
state=temperature_state, Cd=3E-3,
|
| 55 |
+
timestep=short_timestep)
|
| 56 |
+
lhf = climlab.surface.LatentHeatFlux(name='Latent Heat Flux',
|
| 57 |
+
state=full_state, Cd=3E-3,
|
| 58 |
+
timestep=short_timestep)
|
| 59 |
+
surface = climlab.couple([shf,lhf], name="Slab")
|
| 60 |
+
# Convection scheme -- water vapor is a state variable
|
| 61 |
+
conv = climlab.convection.SimplifiedBettsMiller(name='Convection',
|
| 62 |
+
state=full_state,
|
| 63 |
+
timestep=short_timestep,
|
| 64 |
+
)
|
| 65 |
+
rad = climlab.radiation.RRTMG(name='Radiation',
|
| 66 |
+
state=temperature_state,
|
| 67 |
+
specific_humidity=full_state.q, # water vapor is an input here, not a state variable
|
| 68 |
+
albedo=albedo,
|
| 69 |
+
insolation=insolation,
|
| 70 |
+
timestep=long_timestep,
|
| 71 |
+
icld=0, # no clouds
|
| 72 |
+
)
|
| 73 |
+
atm = climlab.couple([rad, conv], name='Atmosphere')
|
| 74 |
+
moistmodel = climlab.couple([atm,surface], name='Moist column model')
|
| 75 |
+
|
| 76 |
+
print(moistmodel)
|
| 77 |
+
|
| 78 |
+
Try running this model and verifying that the atmosphere moistens
|
| 79 |
+
itself via convection, e.g::
|
| 80 |
+
|
| 81 |
+
moistmodel.integrate_years(1)
|
| 82 |
+
moistmodel.q
|
| 83 |
+
|
| 84 |
+
which should produce something like::
|
| 85 |
+
|
| 86 |
+
Field([5.00000000e-06, 5.00000000e-06, 5.00000000e-06, 5.00000000e-06,
|
| 87 |
+
5.00000000e-06, 5.00000000e-06, 8.55725020e-05, 2.02525334e-04,
|
| 88 |
+
4.03568410e-04, 6.98905819e-04, 1.08494727e-03, 1.54761989e-03,
|
| 89 |
+
2.06592591e-03, 2.62545894e-03, 3.22046387e-03, 3.84210271e-03,
|
| 90 |
+
4.48057560e-03, 5.12535633e-03, 5.76585382e-03, 6.39443880e-03,
|
| 91 |
+
7.00456365e-03, 7.47003956e-03, 8.02017591e-03, 8.57294739e-03,
|
| 92 |
+
9.10816435e-03, 9.63014344e-03, 1.01386863e-02, 1.06365703e-02,
|
| 93 |
+
1.11337461e-02, 1.51187832e-02])
|
| 94 |
+
|
| 95 |
+
showing that humidity is now penetrating up to tropopause.
|
| 96 |
+
'''
|
| 97 |
+
import numpy as np
|
| 98 |
+
import warnings
|
| 99 |
+
from climlab.process import TimeDependentProcess
|
| 100 |
+
from climlab.utils.thermo import qsat
|
| 101 |
+
from climlab import constants as const
|
| 102 |
+
from climlab.domain.field import Field
|
| 103 |
+
from climlab.domain import zonal_mean_column
|
| 104 |
+
# The array conversion routines
|
| 105 |
+
#from climlab.radiation.rrtm.utils import _climlab_to_rrtm as _climlab_to_convect
|
| 106 |
+
#from climlab.radiation.rrtm.utils import _rrtm_to_climlab as _convect_to_climlab
|
| 107 |
+
try:
|
| 108 |
+
from climlab_sbm_convection import betts_miller
|
| 109 |
+
except:
|
| 110 |
+
warnings.warn('Cannot import SimplifiedBettsMiller fortran extension, this module will not be functional.')
|
| 111 |
+
|
| 112 |
+
HLv = const.Lhvap
|
| 113 |
+
Cp_air = const.cp
|
| 114 |
+
Grav = const.g
|
| 115 |
+
rdgas = const.Rd
|
| 116 |
+
rvgas = const.Rv
|
| 117 |
+
kappa = const.kappa
|
| 118 |
+
es0 = 1.0
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class SimplifiedBettsMiller(TimeDependentProcess):
|
| 122 |
+
'''
|
| 123 |
+
The climlab wrapper for Dargan Frierson's Simplified Betts Miller moist
|
| 124 |
+
convection scheme (Frierson 2007, J. Atmos. Sci. 64, doi:10.1175/JAS3935.1)
|
| 125 |
+
|
| 126 |
+
Basic characteristics:
|
| 127 |
+
|
| 128 |
+
State:
|
| 129 |
+
|
| 130 |
+
- ``Tatm``: air temperature in K
|
| 131 |
+
- ``q``: specific humidity in kg kg\ :sup:`-1`
|
| 132 |
+
|
| 133 |
+
Input arguments and default values:
|
| 134 |
+
|
| 135 |
+
- ``tau_bm = 7200.``: Betts-Miller relaxation timescale (seconds)
|
| 136 |
+
- ``rhbm = 0.8``: relative humidity profile to which the scheme is relaxing (dimensionless)
|
| 137 |
+
- ``do_simp = False``: do the simple method where you adjust timescales to make precip continuous always.
|
| 138 |
+
- ``do_shallower = True``: do the shallow convection scheme where it chooses a smaller depth such that precipitation is zero.
|
| 139 |
+
- ``do_changeqref = True``: do the shallow convection scheme where it changes the profile of both q and T in order make precip zero.
|
| 140 |
+
- ``do_envsat = True``: reference profile is rhbm times saturated wrt environment (if false, it's rhbm times parcel).
|
| 141 |
+
- ``do_taucape = False``: scheme where taubm is proportional to CAPE\ :sup:`-1/2`
|
| 142 |
+
- ``capetaubm = 900.``: for the above scheme, the value of CAPE (J/kg) for which tau = tau_bm. Ignored unless ``do_taucape == True``.
|
| 143 |
+
- ``tau_min = 2400.``: for the above scheme, the minimum relaxation time allowed (seconds). Ignored unless ``do_taucape == True``.
|
| 144 |
+
|
| 145 |
+
Diagnostics:
|
| 146 |
+
|
| 147 |
+
- ``precipitation``: Precipitation rate (column total) in units of kg m\ :sup:`-2` s\ :sup:`-1` or mm s\ :sup:`-1`
|
| 148 |
+
- ``cape``: Convective Available Potential Energy (CAPE) in units of J kg\ :sup:`-1`
|
| 149 |
+
- ``cin``: Convective Inhibition (CIN) in units of J kg\ :sup:`-1`
|
| 150 |
+
|
| 151 |
+
See Frierson (2007) for more details.
|
| 152 |
+
'''
|
| 153 |
+
def __init__(self,
|
| 154 |
+
tau_bm=7200.,
|
| 155 |
+
rhbm=0.8,
|
| 156 |
+
do_simp=False,
|
| 157 |
+
do_shallower=True,
|
| 158 |
+
do_changeqref=True,
|
| 159 |
+
do_envsat=True,
|
| 160 |
+
do_taucape=False,
|
| 161 |
+
capetaubm=900., # only used if do_taucape == True
|
| 162 |
+
tau_min=2400., # only used if do_taucape == True
|
| 163 |
+
**kwargs):
|
| 164 |
+
super(SimplifiedBettsMiller, self).__init__(**kwargs)
|
| 165 |
+
self.time_type = 'explicit'
|
| 166 |
+
# Define inputs and diagnostics
|
| 167 |
+
surface_shape = self.state['Tatm'][...,0].shape
|
| 168 |
+
# Hack to handle single column and multicolumn
|
| 169 |
+
if surface_shape == ():
|
| 170 |
+
init = np.atleast_1d(np.zeros(surface_shape))
|
| 171 |
+
self.multidim=False
|
| 172 |
+
else:
|
| 173 |
+
init = np.zeros(surface_shape)[...,np.newaxis]
|
| 174 |
+
self.multidim=True
|
| 175 |
+
init = Field(init, domain=self.state.Ts.domain)
|
| 176 |
+
self.add_diagnostic('precipitation', init*0.) # Precip rate (kg/m2/s)
|
| 177 |
+
self.add_diagnostic('cape', init*0.)
|
| 178 |
+
self.add_diagnostic('cin', init*0.)
|
| 179 |
+
self.add_input('tau_bm', tau_bm)
|
| 180 |
+
self.add_input('rhbm', rhbm)
|
| 181 |
+
self.add_input('capetaubm', capetaubm)
|
| 182 |
+
self.add_input('tau_min', tau_min)
|
| 183 |
+
self.add_input('do_simp', do_simp)
|
| 184 |
+
self.add_input('do_shallower', do_shallower)
|
| 185 |
+
self.add_input('do_changeqref', do_changeqref)
|
| 186 |
+
self.add_input('do_envsat', do_envsat)
|
| 187 |
+
self.add_input('do_taucape', do_taucape)
|
| 188 |
+
if hasattr(rhbm, 'shape'):
|
| 189 |
+
assert np.all(rhbm.shape == self.Tatm.shape), f'rhbm {rhbm.shape} has to have same shape as Tatm {self.Tatm.shape}'
|
| 190 |
+
self.rhbm = rhbm
|
| 191 |
+
else:
|
| 192 |
+
self.rhbm = rhbm * np.ones_like(self.Tatm)
|
| 193 |
+
|
| 194 |
+
self._KX = self.lev.size
|
| 195 |
+
try:
|
| 196 |
+
self._JX = self.lat.size
|
| 197 |
+
except:
|
| 198 |
+
self._JX = 1
|
| 199 |
+
try:
|
| 200 |
+
self._IX = self.lon.size
|
| 201 |
+
except:
|
| 202 |
+
self._IX = 1
|
| 203 |
+
|
| 204 |
+
def _climlab_to_sbm(self, field):
|
| 205 |
+
'''Prepare field with proper dimension order.
|
| 206 |
+
Betts-Miller code expects 3D arrays with (IX, JX, KX)
|
| 207 |
+
and 2D arrays with (IX, JX).
|
| 208 |
+
climlab grid dimensions are any of:
|
| 209 |
+
- (KX,)
|
| 210 |
+
- (JX, KX)
|
| 211 |
+
- (JX, IX, KX)
|
| 212 |
+
'''
|
| 213 |
+
if np.isscalar(field):
|
| 214 |
+
return field
|
| 215 |
+
else:
|
| 216 |
+
num_dims = len(field.shape)
|
| 217 |
+
if num_dims==1: # (num_lev only)
|
| 218 |
+
return np.tile(field, [self._IX, self._JX, 1])
|
| 219 |
+
elif num_dims==2: # (num_lat, num_lev)
|
| 220 |
+
return np.tile(field, [self._IX, 1, 1])
|
| 221 |
+
else: # assume we have (num_lon, num_lat, num_lev)
|
| 222 |
+
return field
|
| 223 |
+
|
| 224 |
+
def _sbm_to_climlab(self, field):
|
| 225 |
+
''' Output is either (IX, JX, KX) or (IX, JX).
|
| 226 |
+
Transform this to...
|
| 227 |
+
- (KX,) or (1,) if IX==1 and JX==1
|
| 228 |
+
- (IX,KX) or (IX, 1) if IX>1 and JX==1
|
| 229 |
+
- no change if IX>1, JX>1
|
| 230 |
+
'''
|
| 231 |
+
return np.squeeze(field)
|
| 232 |
+
|
| 233 |
+
def _compute(self):
|
| 234 |
+
# Convection code expects that first element on pressure axis is TOA
|
| 235 |
+
# which is the same as climlab convention.
|
| 236 |
+
# All we have to do is ensure the input fields are (num_lat, num_lon, num_lev)
|
| 237 |
+
T = self._climlab_to_sbm(self.state['Tatm'])
|
| 238 |
+
RHBM = self._climlab_to_sbm(self.rhbm)
|
| 239 |
+
dom = self.state['Tatm'].domain
|
| 240 |
+
P = self._climlab_to_sbm(dom.lev.points) * 100. # convert to Pascals
|
| 241 |
+
PH = self._climlab_to_sbm(dom.lev.bounds) * 100.
|
| 242 |
+
Q = self._climlab_to_sbm(self.state['q'])
|
| 243 |
+
dt = self.timestep_in_seconds
|
| 244 |
+
|
| 245 |
+
(rain, tdel, qdel, q_ref, bmflag, klzbs, cape, cin, t_ref, \
|
| 246 |
+
invtau_bm_t, invtau_bm_q, capeflag) = \
|
| 247 |
+
betts_miller(dt, T, Q, RHBM, P, PH,
|
| 248 |
+
HLv,Cp_air,Grav,rdgas,rvgas,kappa, es0,
|
| 249 |
+
self.tau_bm, self.do_simp, self.do_shallower,
|
| 250 |
+
self.do_changeqref, self.do_envsat, self.do_taucape,
|
| 251 |
+
self.capetaubm, self.tau_min,self._IX, self._JX, self._KX, )
|
| 252 |
+
|
| 253 |
+
# Routine returns adjustments rather than tendencies
|
| 254 |
+
dTdt = tdel / dt
|
| 255 |
+
dQdt = qdel / dt
|
| 256 |
+
tendencies = {'Tatm': self._sbm_to_climlab(dTdt)*np.ones_like(self.state['Tatm']),
|
| 257 |
+
'q': self._sbm_to_climlab(dQdt)*np.ones_like(self.state['q'])}
|
| 258 |
+
if 'Ts' in self.state:
|
| 259 |
+
tendencies['Ts'] = 0. * self.state['Ts']
|
| 260 |
+
# Need to convert from kg/m2 (mm) to kg/m2/s (mm/s)
|
| 261 |
+
# Hack to handle single column and multicolumn
|
| 262 |
+
if self.multidim:
|
| 263 |
+
self.precipitation[:,0] = self._sbm_to_climlab(rain)/dt
|
| 264 |
+
self.cape[:,0] = self._sbm_to_climlab(cape)
|
| 265 |
+
self.cin[:,0] = self._sbm_to_climlab(cin)
|
| 266 |
+
else:
|
| 267 |
+
self.precipitation[:] = self._sbm_to_climlab(rain)/dt
|
| 268 |
+
self.cape[:] = self._sbm_to_climlab(cape)
|
| 269 |
+
self.cin[:] = self._sbm_to_climlab(cin)
|
| 270 |
+
return tendencies
|
climlab/source/climlab/domain/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
Modules for self-describing gridded fields in climlab.
|
| 3 |
+
'''
|
| 4 |
+
__all__ = ['axis', 'domain', 'field', 'initial', 'xarray']
|
| 5 |
+
|
| 6 |
+
from climlab.domain.domain import single_column, zonal_mean_surface, surface_2D, zonal_mean_column, box_model_domain
|
| 7 |
+
from climlab.domain.initial import column_state, surface_state
|
| 8 |
+
from climlab.domain.field import Field, global_mean
|
| 9 |
+
from climlab.domain.axis import Axis
|
climlab/source/climlab/domain/axis.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from builtins import str, object
|
| 2 |
+
import numpy as np
|
| 3 |
+
from climlab import constants as const
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
axis_types = ['lev', 'lat', 'lon', 'depth', 'abstract']
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# will need to implement a simple cartesian distance axis type
|
| 10 |
+
# and probaly also an abstract dimensionless axis type (for box models)
|
| 11 |
+
|
| 12 |
+
class Axis(object):
|
| 13 |
+
"""Creates a new climlab Axis object.
|
| 14 |
+
|
| 15 |
+
An :class:`~climlab.domain.axis.Axis` is an object where information of a
|
| 16 |
+
spacial dimension of a :class:`~climlab.domain.domain._Domain` are specified.
|
| 17 |
+
|
| 18 |
+
These include the `type` of the axis, the `number of points`, location of
|
| 19 |
+
`points` and `bounds` on the spatial dimension, magnitude of bounds
|
| 20 |
+
differences `delta` as well as their `unit`.
|
| 21 |
+
|
| 22 |
+
The `axes` of a :class:`~climlab.domain.domain._Domain` are stored in the
|
| 23 |
+
dictionary axes, so they can be accessed through ``dom.axes`` if ``dom``
|
| 24 |
+
is an instance of :class:`~climlab.domain.domain._Domain`.
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
**Initialization parameters** \n
|
| 28 |
+
|
| 29 |
+
An instance of ``Axis`` is initialized with the following
|
| 30 |
+
arguments *(for detailed information see Object attributes below)*:
|
| 31 |
+
|
| 32 |
+
:param str axis_type: information about the type of axis
|
| 33 |
+
[default: 'abstract']
|
| 34 |
+
:param int num_points: number of points on axis
|
| 35 |
+
[default: 10]
|
| 36 |
+
:param array points: array with specific points (optional)
|
| 37 |
+
:param array bounds: array with specific bounds between points (optional)
|
| 38 |
+
:raises: :exc:`ValueError`
|
| 39 |
+
if ``axis_type`` is not one of the valid types or
|
| 40 |
+
their euqivalents (see below).
|
| 41 |
+
:raises: :exc:`ValueError`
|
| 42 |
+
if ``points`` are given and not array-like.
|
| 43 |
+
:raises: :exc:`ValueError`
|
| 44 |
+
if ``bounds`` are given and not array-like.
|
| 45 |
+
|
| 46 |
+
**Object attributes** \n
|
| 47 |
+
|
| 48 |
+
Following object attributes are generated during initialization:
|
| 49 |
+
|
| 50 |
+
:ivar str axis_type: Information about the type of axis. Valid axis types are:
|
| 51 |
+
|
| 52 |
+
* ``'lev'``
|
| 53 |
+
* ``'lat'``
|
| 54 |
+
* ``'lon'``
|
| 55 |
+
* ``'depth'``
|
| 56 |
+
* ``'abstract'`` (default)
|
| 57 |
+
|
| 58 |
+
:ivar int num_points: number of points on axis
|
| 59 |
+
:ivar str units: Unit of the axis. During intialization the unit is
|
| 60 |
+
chosen from the ``defaultUnits`` dictionary (see below).
|
| 61 |
+
:ivar array points: array with all points of the axis (grid)
|
| 62 |
+
:ivar array bounds: array with all bounds between points (staggered grid)
|
| 63 |
+
:ivar array delta: array with spatial differences between bounds
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
**Axis Types** \n
|
| 67 |
+
|
| 68 |
+
A couple of differing axis type strings are rendered to valid axis types.
|
| 69 |
+
Alternate forms are listed here:
|
| 70 |
+
|
| 71 |
+
* ``'lev'``
|
| 72 |
+
* ``'p'``
|
| 73 |
+
* ``'press'``
|
| 74 |
+
* ``'pressure'``
|
| 75 |
+
* ``'P'``
|
| 76 |
+
* ``'Pressure'``
|
| 77 |
+
* ``'Press'``
|
| 78 |
+
* ``'lat'``
|
| 79 |
+
* ``'Latitude'``
|
| 80 |
+
* ``'latitude'``
|
| 81 |
+
* ``'lon'``
|
| 82 |
+
* ``'Longitude'``
|
| 83 |
+
* ``'longitude'``
|
| 84 |
+
* ``'depth'``
|
| 85 |
+
* ``'Depth'``
|
| 86 |
+
* ``'waterDepth'``
|
| 87 |
+
* ``'water_depth'``
|
| 88 |
+
* ``'slab'``
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
The **default units** are::
|
| 92 |
+
|
| 93 |
+
defaultUnits = {'lev': 'mb',
|
| 94 |
+
'lat': 'degrees',
|
| 95 |
+
'lon': 'degrees',
|
| 96 |
+
'depth': 'meters',
|
| 97 |
+
'abstract': 'none'}
|
| 98 |
+
|
| 99 |
+
If bounds are not given during initialization, **default end points**
|
| 100 |
+
are used::
|
| 101 |
+
|
| 102 |
+
defaultEndPoints = {'lev': (0., climlab.constants.ps),
|
| 103 |
+
'lat': (-90., 90.),
|
| 104 |
+
'lon': (0., 360.),
|
| 105 |
+
'depth': (0., 10.),
|
| 106 |
+
'abstract': (0, num_points)}
|
| 107 |
+
|
| 108 |
+
:Example:
|
| 109 |
+
|
| 110 |
+
Creation of a standalone Axis::
|
| 111 |
+
|
| 112 |
+
>>> import climlab
|
| 113 |
+
>>> ax = climlab.domain.Axis(axis_type='Latitude', num_points=36)
|
| 114 |
+
|
| 115 |
+
>>> print ax
|
| 116 |
+
Axis of type lat with 36 points.
|
| 117 |
+
|
| 118 |
+
>>> ax.points
|
| 119 |
+
array([-87.5, -82.5, -77.5, -72.5, -67.5, -62.5, -57.5, -52.5, -47.5,
|
| 120 |
+
-42.5, -37.5, -32.5, -27.5, -22.5, -17.5, -12.5, -7.5, -2.5,
|
| 121 |
+
2.5, 7.5, 12.5, 17.5, 22.5, 27.5, 32.5, 37.5, 42.5,
|
| 122 |
+
47.5, 52.5, 57.5, 62.5, 67.5, 72.5, 77.5, 82.5, 87.5])
|
| 123 |
+
|
| 124 |
+
>>> ax.bounds
|
| 125 |
+
array([-90., -85., -80., -75., -70., -65., -60., -55., -50., -45., -40.,
|
| 126 |
+
-35., -30., -25., -20., -15., -10., -5., 0., 5., 10., 15.,
|
| 127 |
+
20., 25., 30., 35., 40., 45., 50., 55., 60., 65., 70.,
|
| 128 |
+
75., 80., 85., 90.])
|
| 129 |
+
|
| 130 |
+
>>> ax.delta
|
| 131 |
+
array([ 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5.,
|
| 132 |
+
5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5.,
|
| 133 |
+
5., 5., 5., 5., 5., 5., 5., 5., 5., 5.])
|
| 134 |
+
|
| 135 |
+
"""
|
| 136 |
+
def __str__(self):
|
| 137 |
+
return ("Axis of type " + self.axis_type + " with " +
|
| 138 |
+
str(self.num_points) + " points.")
|
| 139 |
+
|
| 140 |
+
def __init__(self, axis_type='abstract', num_points=10, points=None, bounds=None):
|
| 141 |
+
if axis_type in axis_types:
|
| 142 |
+
pass
|
| 143 |
+
elif axis_type in ['p', 'press', 'pressure', 'P', 'Pressure', 'Press']:
|
| 144 |
+
axis_type = 'lev'
|
| 145 |
+
elif axis_type in ['Latitude', 'latitude']:
|
| 146 |
+
axis_type = 'lat'
|
| 147 |
+
elif axis_type in ['Longitude', 'longitude']:
|
| 148 |
+
axis_type = 'lon'
|
| 149 |
+
elif axis_type in ['depth', 'Depth', 'waterDepth', 'water_depth', 'slab']:
|
| 150 |
+
axis_type = 'depth'
|
| 151 |
+
else:
|
| 152 |
+
raise ValueError('axis_type %s not recognized' % axis_type)
|
| 153 |
+
self.axis_type = axis_type
|
| 154 |
+
|
| 155 |
+
defaultEndPoints = {'lev': (0., const.ps),
|
| 156 |
+
'lat': (-90., 90.),
|
| 157 |
+
'lon': (0., 360.),
|
| 158 |
+
'depth': (0., 10.),
|
| 159 |
+
'abstract': (0, num_points)}
|
| 160 |
+
defaultUnits = {'lev': 'mb',
|
| 161 |
+
'lat': 'degrees',
|
| 162 |
+
'lon': 'degrees',
|
| 163 |
+
'depth': 'meters',
|
| 164 |
+
'abstract': 'none'}
|
| 165 |
+
# if points and/or bounds are supplied, make sure they are increasing
|
| 166 |
+
if points is not None:
|
| 167 |
+
try:
|
| 168 |
+
# using np.atleast_1d() ensures that we can use a single point
|
| 169 |
+
points = np.sort(np.atleast_1d(np.array(points, dtype=float)))
|
| 170 |
+
except:
|
| 171 |
+
raise ValueError('points must be array_like.')
|
| 172 |
+
if bounds is not None:
|
| 173 |
+
try:
|
| 174 |
+
bounds = np.sort(np.atleast_1d(np.array(bounds, dtype=float)))
|
| 175 |
+
except:
|
| 176 |
+
raise ValueError('bounds must be array_like.')
|
| 177 |
+
|
| 178 |
+
if bounds is None:
|
| 179 |
+
# assume default end points
|
| 180 |
+
end0 = defaultEndPoints[axis_type][0]
|
| 181 |
+
end1 = defaultEndPoints[axis_type][1]
|
| 182 |
+
if points is not None:
|
| 183 |
+
# only points are given
|
| 184 |
+
num_points = points.size
|
| 185 |
+
bounds = points[:-1] + np.diff(points)/2.
|
| 186 |
+
temp = np.append(np.flipud(bounds), end0)
|
| 187 |
+
bounds = np.append(np.flipud(temp), end1)
|
| 188 |
+
else:
|
| 189 |
+
# no points or bounds
|
| 190 |
+
# create an evenly spaced axis
|
| 191 |
+
delta = (end1 - end0) / num_points
|
| 192 |
+
bounds = np.linspace(end0, end1, num_points+1)
|
| 193 |
+
points = np.linspace(end0 + delta/2., end1-delta/2., num_points)
|
| 194 |
+
else: # bounds are given
|
| 195 |
+
end0 = bounds[0]
|
| 196 |
+
end1 = bounds[1]
|
| 197 |
+
num_points = bounds.size - 1
|
| 198 |
+
if points is None:
|
| 199 |
+
# only bounds given. Assume points are halfway between bounds
|
| 200 |
+
points = bounds[:-1] + np.diff(bounds)/2.
|
| 201 |
+
else:
|
| 202 |
+
# points and bounds both given, check that they are compatible
|
| 203 |
+
if points.size != num_points:
|
| 204 |
+
raise ValueError('points and bounds have incompatible sizes')
|
| 205 |
+
self.num_points = num_points
|
| 206 |
+
self.units = defaultUnits[axis_type]
|
| 207 |
+
# pressure axis should decrease from surface to TOA
|
| 208 |
+
# NO! Now define the lowest (near-to-surface) element as lev[-1]
|
| 209 |
+
# and the nearest to space as lev[0]
|
| 210 |
+
#if axis_type is 'lev':
|
| 211 |
+
# points = np.flipud(points)
|
| 212 |
+
# bounds = np.flipud(bounds)
|
| 213 |
+
self.points = points
|
| 214 |
+
self.bounds = bounds
|
| 215 |
+
self.delta = np.abs(np.diff(self.bounds))
|
climlab/source/climlab/domain/domain.py
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from builtins import str, object
|
| 2 |
+
from climlab.domain.axis import Axis
|
| 3 |
+
from climlab.utils import heat_capacity
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class _Domain(object):
|
| 7 |
+
"""Private parent class for `Domains`.
|
| 8 |
+
|
| 9 |
+
A `Domain` defines an area or spatial base for a climlab
|
| 10 |
+
:class:`~climlab.process.process.Process` object. It consists of axes which
|
| 11 |
+
are :class:`~climlab.domain.axis.Axis` objects that define the dimensions
|
| 12 |
+
of the `Domain`.
|
| 13 |
+
|
| 14 |
+
In a `Domain` the heat capacity of grid points, bounds or cells/boxes is
|
| 15 |
+
specified.
|
| 16 |
+
|
| 17 |
+
There are daughter classes :class:`~climlab.domain.domain.Atmosphere` and
|
| 18 |
+
:class:`~climlab.domain.domain.Ocean` of the private
|
| 19 |
+
:class:`~climlab.domain.domain._Domain` class implemented which themselves
|
| 20 |
+
have daughter classes :class:`~climlab.domain.domain.SlabAtmosphere` and
|
| 21 |
+
:class:`~climlab.domain.domain.SlabOcean`.
|
| 22 |
+
|
| 23 |
+
Several methods are implemented that create `Domains` with special
|
| 24 |
+
specifications. These are
|
| 25 |
+
|
| 26 |
+
- :func:`~climlab.domain.domain.single_column`
|
| 27 |
+
|
| 28 |
+
- :func:`~climlab.domain.domain.zonal_mean_column`
|
| 29 |
+
|
| 30 |
+
- :func:`~climlab.domain.domain.box_model_domain`
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
**Initialization parameters** \n
|
| 34 |
+
|
| 35 |
+
An instance of ``_Domain`` is initialized with the following
|
| 36 |
+
arguments:
|
| 37 |
+
|
| 38 |
+
:param axes: Axis object or dictionary of Axis object where domain will
|
| 39 |
+
be defined on.
|
| 40 |
+
:type axes: dict or :class:`~climlab.domain.axis.Axis`
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
**Object attributes** \n
|
| 44 |
+
|
| 45 |
+
Following object attributes are generated during initialization:
|
| 46 |
+
|
| 47 |
+
:ivar str domain_type: Set to ``'undefined'``.
|
| 48 |
+
:ivar dict axes: A dictionary of the domains axes. Created by
|
| 49 |
+
:func:`_make_axes_dict` called with input
|
| 50 |
+
argument ``axes``
|
| 51 |
+
:ivar int numdims: Number of :class:`~climlab.domain.axis.Axis` objects
|
| 52 |
+
in ``self.axes`` dictionary.
|
| 53 |
+
:ivar dict ax_index: A dictionary of domain axes and their corresponding index
|
| 54 |
+
in an ordered list of the axes with: \n
|
| 55 |
+
- ``'lev'`` or ``'depth'`` is last
|
| 56 |
+
- ``'lat'`` is second last
|
| 57 |
+
:ivar tuple shape: Number of points of all domain axes. Order in
|
| 58 |
+
tuple given by ``self.ax_index``.
|
| 59 |
+
:ivar array heat_capacity: the domain's heat capacity over axis specified
|
| 60 |
+
in function call of :func:`set_heat_capacity`
|
| 61 |
+
|
| 62 |
+
"""
|
| 63 |
+
def __str__(self):
|
| 64 |
+
return ("climlab Domain object with domain_type=" + self.domain_type + " and shape=" +
|
| 65 |
+
str(self.shape))
|
| 66 |
+
def __init__(self, axes=None, **kwargs):
|
| 67 |
+
self.domain_type = 'undefined'
|
| 68 |
+
# self.axes should be a dictionary of axes
|
| 69 |
+
# make it possible to give just a single axis:
|
| 70 |
+
self.axes = self._make_axes_dict(axes)
|
| 71 |
+
self.numdims = len(list(self.axes.keys()))
|
| 72 |
+
shape = []
|
| 73 |
+
axcount = 0
|
| 74 |
+
axindex = {}
|
| 75 |
+
# ordered list of axes
|
| 76 |
+
# lev OR depth is last
|
| 77 |
+
# lat is second-last
|
| 78 |
+
add_lev = False
|
| 79 |
+
add_depth = False
|
| 80 |
+
add_lon = False
|
| 81 |
+
add_lat = False
|
| 82 |
+
axlist = list(self.axes.keys())
|
| 83 |
+
if 'lev' in axlist:
|
| 84 |
+
axlist.remove('lev')
|
| 85 |
+
add_lev = True
|
| 86 |
+
elif 'depth' in axlist:
|
| 87 |
+
axlist.remove('depth')
|
| 88 |
+
add_depth = True
|
| 89 |
+
if 'lon' in axlist:
|
| 90 |
+
axlist.remove('lon')
|
| 91 |
+
add_lon = True
|
| 92 |
+
if 'lat' in axlist:
|
| 93 |
+
axlist.remove('lat')
|
| 94 |
+
add_lat = True
|
| 95 |
+
axlist2 = axlist[:]
|
| 96 |
+
if add_lat:
|
| 97 |
+
axlist2.append('lat')
|
| 98 |
+
if add_lon:
|
| 99 |
+
axlist2.append('lon')
|
| 100 |
+
if add_depth:
|
| 101 |
+
axlist2.append('depth')
|
| 102 |
+
if add_lev:
|
| 103 |
+
axlist2.append('lev')
|
| 104 |
+
#for axType, ax in self.axes.iteritems():
|
| 105 |
+
for axType in axlist2:
|
| 106 |
+
ax = self.axes[axType]
|
| 107 |
+
shape.append(ax.num_points)
|
| 108 |
+
# can access axes as object attributes
|
| 109 |
+
setattr(self, axType, ax)
|
| 110 |
+
#
|
| 111 |
+
axindex[axType] = axcount
|
| 112 |
+
axcount += 1
|
| 113 |
+
self.axis_index = axindex
|
| 114 |
+
self.shape = tuple(shape)
|
| 115 |
+
|
| 116 |
+
self.set_heat_capacity()
|
| 117 |
+
|
| 118 |
+
def set_heat_capacity(self):
|
| 119 |
+
"""A dummy function to set the heat capacity of a domain.
|
| 120 |
+
|
| 121 |
+
*Should be overridden by daugter classes.*
|
| 122 |
+
|
| 123 |
+
"""
|
| 124 |
+
self.heat_capacity = None
|
| 125 |
+
# implemented by daughter classes
|
| 126 |
+
|
| 127 |
+
def _make_axes_dict(self, axes):
|
| 128 |
+
"""Makes an axes dictionary.
|
| 129 |
+
|
| 130 |
+
.. note::
|
| 131 |
+
|
| 132 |
+
In case the input is ``None``, the dictionary :code:`{'empty': None}`
|
| 133 |
+
is returned.
|
| 134 |
+
|
| 135 |
+
**Function-call argument** \n
|
| 136 |
+
|
| 137 |
+
:param axes: axes input
|
| 138 |
+
:type axes: dict or single instance of
|
| 139 |
+
:class:`~climlab.domain.axis.Axis` object or ``None``
|
| 140 |
+
:raises: :exc:`ValueError` if input is not an instance of Axis class
|
| 141 |
+
or a dictionary of Axis objetcs
|
| 142 |
+
:returns: dictionary of input axes
|
| 143 |
+
:rtype: dict
|
| 144 |
+
|
| 145 |
+
"""
|
| 146 |
+
if type(axes) is dict:
|
| 147 |
+
axdict = axes
|
| 148 |
+
elif type(axes) is Axis:
|
| 149 |
+
ax = axes
|
| 150 |
+
axdict = {ax.axis_type: ax}
|
| 151 |
+
elif axes is None:
|
| 152 |
+
axdict = {'empty': None}
|
| 153 |
+
else:
|
| 154 |
+
raise ValueError('axes needs to be Axis object or dictionary of Axis object')
|
| 155 |
+
return axdict
|
| 156 |
+
|
| 157 |
+
def __getitem__(self, indx):
|
| 158 |
+
# Make domains sliceable
|
| 159 |
+
# First create a bare domain object (without calling the __init__ method)
|
| 160 |
+
dout = type(self).__new__(type(self))
|
| 161 |
+
# inherit *most* of the attributes of self
|
| 162 |
+
# For now we are just slicing the heat capacity
|
| 163 |
+
# But would be great to have some logic for slicing axes
|
| 164 |
+
# I am not 100% percent clear on how all this works
|
| 165 |
+
# But for now we're just going to "try" to slice to avoid
|
| 166 |
+
# some failures
|
| 167 |
+
for key, value in self.__dict__.items():
|
| 168 |
+
if key == 'heat_capacity':
|
| 169 |
+
try:
|
| 170 |
+
dout.heat_capacity = self.heat_capacity[indx]
|
| 171 |
+
except:
|
| 172 |
+
dout.heat_capacity = self.heat_capacity
|
| 173 |
+
elif key == 'shape':
|
| 174 |
+
try:
|
| 175 |
+
dout.shape = self.heat_capacity[indx].shape
|
| 176 |
+
except:
|
| 177 |
+
dout.shape = self.shape
|
| 178 |
+
else:
|
| 179 |
+
setattr(dout, key, value)
|
| 180 |
+
return dout
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
class Atmosphere(_Domain):
|
| 184 |
+
"""Class for the implementation of an Atmosphere Domain.
|
| 185 |
+
|
| 186 |
+
**Object attributes** \n
|
| 187 |
+
|
| 188 |
+
Additional to the parent class :class:`~climlab.domain.domain._Domain`
|
| 189 |
+
the following object attribute is modified during initialization:
|
| 190 |
+
|
| 191 |
+
:ivar str domain_type: is set to ``'atm'``
|
| 192 |
+
|
| 193 |
+
:Example:
|
| 194 |
+
|
| 195 |
+
Setting up an Atmosphere Domain::
|
| 196 |
+
|
| 197 |
+
>>> import climlab
|
| 198 |
+
>>> atm_ax = climlab.domain.Axis(axis_type='pressure', num_points=10)
|
| 199 |
+
>>> atm_domain = climlab.domain.Atmosphere(axes=atm_ax)
|
| 200 |
+
|
| 201 |
+
>>> print atm_domain
|
| 202 |
+
climlab Domain object with domain_type=atm and shape=(10,)
|
| 203 |
+
|
| 204 |
+
>>> atm_domain.axes
|
| 205 |
+
{'lev': <climlab.domain.axis.Axis object at 0x7fe5b8ef8e10>}
|
| 206 |
+
|
| 207 |
+
>>> atm_domain.heat_capacity
|
| 208 |
+
array([ 1024489.79591837, 1024489.79591837, 1024489.79591837,
|
| 209 |
+
1024489.79591837, 1024489.79591837, 1024489.79591837,
|
| 210 |
+
1024489.79591837, 1024489.79591837, 1024489.79591837,
|
| 211 |
+
1024489.79591837])
|
| 212 |
+
|
| 213 |
+
"""
|
| 214 |
+
def __init__(self, **kwargs):
|
| 215 |
+
super(Atmosphere, self).__init__(**kwargs)
|
| 216 |
+
self.domain_type = 'atm'
|
| 217 |
+
|
| 218 |
+
def set_heat_capacity(self):
|
| 219 |
+
"""Sets the heat capacity of the Atmosphere Domain.
|
| 220 |
+
|
| 221 |
+
Calls the utils heat capacity function
|
| 222 |
+
:func:`~climlab.utils.heat_capacity.atmosphere` and gives the delta
|
| 223 |
+
array of grid points of it's level axis
|
| 224 |
+
``self.axes['lev'].delta`` as input.
|
| 225 |
+
|
| 226 |
+
**Object attributes** \n
|
| 227 |
+
|
| 228 |
+
During method execution following object attribute is modified:
|
| 229 |
+
|
| 230 |
+
:ivar array heat_capacity: the ocean domain's heat capacity over
|
| 231 |
+
the ``'lev'`` Axis.
|
| 232 |
+
|
| 233 |
+
"""
|
| 234 |
+
self.heat_capacity = heat_capacity.atmosphere(self.axes['lev'].delta)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
class Ocean(_Domain):
|
| 238 |
+
"""Class for the implementation of an Ocean Domain.
|
| 239 |
+
|
| 240 |
+
**Object attributes** \n
|
| 241 |
+
|
| 242 |
+
Additional to the parent class :class:`~climlab.domain.domain._Domain`
|
| 243 |
+
the following object attribute is modified during initialization:
|
| 244 |
+
|
| 245 |
+
:ivar str domain_type: is set to ``'ocean'``
|
| 246 |
+
|
| 247 |
+
:Example:
|
| 248 |
+
|
| 249 |
+
Setting up an Ocean Domain::
|
| 250 |
+
|
| 251 |
+
>>> import climlab
|
| 252 |
+
>>> ocean_ax = climlab.domain.Axis(axis_type='depth', num_points=5)
|
| 253 |
+
>>> ocean_domain = climlab.domain.Ocean(axes=ocean_ax)
|
| 254 |
+
|
| 255 |
+
>>> print ocean_domain
|
| 256 |
+
climlab Domain object with domain_type=ocean and shape=(5,)
|
| 257 |
+
|
| 258 |
+
>>> ocean_domain.axes
|
| 259 |
+
{'depth': <climlab.domain.axis.Axis object at 0x7fe5b8f102d0>}
|
| 260 |
+
|
| 261 |
+
>>> ocean_domain.heat_capacity
|
| 262 |
+
array([ 8362600., 8362600., 8362600., 8362600., 8362600.])
|
| 263 |
+
|
| 264 |
+
"""
|
| 265 |
+
def __init__(self, **kwargs):
|
| 266 |
+
super(Ocean, self).__init__(**kwargs)
|
| 267 |
+
self.domain_type = 'ocean'
|
| 268 |
+
|
| 269 |
+
def set_heat_capacity(self):
|
| 270 |
+
"""Sets the heat capacity of the Ocean Domain.
|
| 271 |
+
|
| 272 |
+
Calls the utils heat capacity function
|
| 273 |
+
:func:`~climlab.utils.heat_capacity.ocean` and gives the delta
|
| 274 |
+
array of grid points of it's depth axis
|
| 275 |
+
``self.axes['depth'].delta`` as input.
|
| 276 |
+
|
| 277 |
+
**Object attributes** \n
|
| 278 |
+
|
| 279 |
+
During method execution following object attribute is modified:
|
| 280 |
+
|
| 281 |
+
:ivar array heat_capacity: the ocean domain's heat capacity over
|
| 282 |
+
the ``'depth'`` Axis.
|
| 283 |
+
|
| 284 |
+
"""
|
| 285 |
+
self.heat_capacity = heat_capacity.ocean(self.axes['depth'].delta)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def make_slabocean_axis(num_points=1):
|
| 289 |
+
"""Convenience method to create a simple axis for a slab ocean.
|
| 290 |
+
|
| 291 |
+
**Function-call argument** \n
|
| 292 |
+
|
| 293 |
+
:param int num_points: number of points for the slabocean Axis [default: 1]
|
| 294 |
+
:returns: an Axis with ``axis_type='depth'`` and ``num_points=num_points``
|
| 295 |
+
:rtype: :class:`~climlab.domain.axis.Axis`
|
| 296 |
+
|
| 297 |
+
:Example:
|
| 298 |
+
|
| 299 |
+
::
|
| 300 |
+
|
| 301 |
+
>>> import climlab
|
| 302 |
+
>>> slab_ocean_axis = climlab.domain.make_slabocean_axis()
|
| 303 |
+
|
| 304 |
+
>>> print slab_ocean_axis
|
| 305 |
+
Axis of type depth with 1 points.
|
| 306 |
+
|
| 307 |
+
>>> slab_ocean_axis.axis_type
|
| 308 |
+
'depth'
|
| 309 |
+
|
| 310 |
+
>>> slab_ocean_axis.bounds
|
| 311 |
+
array([ 0., 10.])
|
| 312 |
+
|
| 313 |
+
>>> slab_ocean_axis.units
|
| 314 |
+
'meters'
|
| 315 |
+
|
| 316 |
+
"""
|
| 317 |
+
depthax = Axis(axis_type='depth', num_points=num_points)
|
| 318 |
+
return depthax
|
| 319 |
+
|
| 320 |
+
def make_slabatm_axis(num_points=1):
|
| 321 |
+
"""Convenience method to create a simple axis for a slab atmosphere.
|
| 322 |
+
|
| 323 |
+
**Function-call argument** \n
|
| 324 |
+
|
| 325 |
+
:param int num_points: number of points for the slabatmosphere Axis [default: 1]
|
| 326 |
+
:returns: an Axis with ``axis_type='lev'`` and ``num_points=num_points``
|
| 327 |
+
:rtype: :class:`~climlab.domain.axis.Axis`
|
| 328 |
+
|
| 329 |
+
:Example:
|
| 330 |
+
|
| 331 |
+
::
|
| 332 |
+
|
| 333 |
+
>>> import climlab
|
| 334 |
+
>>> slab_atm_axis = climlab.domain.make_slabatm_axis()
|
| 335 |
+
|
| 336 |
+
>>> print slab_atm_axis
|
| 337 |
+
Axis of type lev with 1 points.
|
| 338 |
+
|
| 339 |
+
>>> slab_atm_axis.axis_type
|
| 340 |
+
'lev'
|
| 341 |
+
|
| 342 |
+
>>> slab_atm_axis.bounds
|
| 343 |
+
array([ 0., 1000.])
|
| 344 |
+
|
| 345 |
+
>>> slab_atm_axis.units
|
| 346 |
+
'mb'
|
| 347 |
+
|
| 348 |
+
"""
|
| 349 |
+
depthax = Axis(axis_type='lev', num_points=num_points)
|
| 350 |
+
return depthax
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class SlabOcean(Ocean):
|
| 355 |
+
"""A class to create a SlabOcean Domain by default.
|
| 356 |
+
|
| 357 |
+
Initializes the parent :class:`Ocean` class with a simple axis for a
|
| 358 |
+
Slab Ocean created by :func:`make_slabocean_axis` which has just 1 cell
|
| 359 |
+
in depth by default.
|
| 360 |
+
|
| 361 |
+
:Example:
|
| 362 |
+
|
| 363 |
+
Creating a SlabOcean Domain::
|
| 364 |
+
|
| 365 |
+
>>> import climlab
|
| 366 |
+
>>> slab_ocean_domain = climlab.domain.SlabOcean()
|
| 367 |
+
|
| 368 |
+
>>> print slab_ocean_domain
|
| 369 |
+
climlab Domain object with domain_type=ocean and shape=(1,)
|
| 370 |
+
|
| 371 |
+
>>> slab_ocean_domain.axes
|
| 372 |
+
{'depth': <climlab.domain.axis.Axis object at 0x7fe5c42814d0>}
|
| 373 |
+
|
| 374 |
+
>>> slab_ocean_domain.heat_capacity
|
| 375 |
+
array([ 41813000.])
|
| 376 |
+
|
| 377 |
+
"""
|
| 378 |
+
def __init__(self, axes=make_slabocean_axis(), **kwargs):
|
| 379 |
+
super(SlabOcean, self).__init__(axes=axes, **kwargs)
|
| 380 |
+
|
| 381 |
+
class SlabAtmosphere(Atmosphere):
|
| 382 |
+
"""A class to create a SlabAtmosphere Domain by default.
|
| 383 |
+
|
| 384 |
+
Initializes the parent :class:`Atmosphere` class with a simple axis for a
|
| 385 |
+
Slab Atmopshere created by :func:`make_slabatm_axis` which has just 1 cell
|
| 386 |
+
in height by default.
|
| 387 |
+
|
| 388 |
+
:Example:
|
| 389 |
+
|
| 390 |
+
Creating a SlabAtmosphere Domain::
|
| 391 |
+
|
| 392 |
+
>>> import climlab
|
| 393 |
+
>>> slab_atm_domain = climlab.domain.SlabAtmosphere()
|
| 394 |
+
|
| 395 |
+
>>> print slab_atm_domain
|
| 396 |
+
climlab Domain object with domain_type=atm and shape=(1,)
|
| 397 |
+
|
| 398 |
+
>>> slab_atm_domain.axes
|
| 399 |
+
{'lev': <climlab.domain.axis.Axis object at 0x7fe5c4281610>}
|
| 400 |
+
|
| 401 |
+
>>> slab_atm_domain.heat_capacity
|
| 402 |
+
array([ 10244897.95918367])
|
| 403 |
+
|
| 404 |
+
"""
|
| 405 |
+
def __init__(self, axes=make_slabatm_axis(), **kwargs):
|
| 406 |
+
super(SlabAtmosphere, self).__init__(axes=axes, **kwargs)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def single_column(num_lev=30, water_depth=1., lev=None, **kwargs):
|
| 410 |
+
"""Creates domains for a single column of atmosphere overlying a slab of water.
|
| 411 |
+
|
| 412 |
+
Can also pass a pressure array or pressure level axis object specified in ``lev``.
|
| 413 |
+
|
| 414 |
+
If argument ``lev`` is not ``None`` then function tries to build a level axis
|
| 415 |
+
and ``num_lev`` is ignored.
|
| 416 |
+
|
| 417 |
+
**Function-call argument** \n
|
| 418 |
+
|
| 419 |
+
:param int num_lev: number of pressure levels
|
| 420 |
+
(evenly spaced from surface to TOA) [default: 30]
|
| 421 |
+
:param float water_depth: depth of the ocean slab [default: 1.]
|
| 422 |
+
:param lev: specification for height axis (optional)
|
| 423 |
+
:type lev: :class:`~climlab.domain.axis.Axis` or pressure array
|
| 424 |
+
:raises: :exc:`ValueError` if `lev` is given but neither Axis
|
| 425 |
+
nor pressure array.
|
| 426 |
+
:returns: a list of 2 Domain objects (slab ocean, atmosphere)
|
| 427 |
+
:rtype: :py:class:`list` of :class:`SlabOcean`, :class:`SlabAtmosphere`
|
| 428 |
+
|
| 429 |
+
:Example:
|
| 430 |
+
|
| 431 |
+
::
|
| 432 |
+
|
| 433 |
+
>>> from climlab import domain
|
| 434 |
+
|
| 435 |
+
>>> sfc, atm = domain.single_column(num_lev=2, water_depth=10.)
|
| 436 |
+
|
| 437 |
+
>>> print sfc
|
| 438 |
+
climlab Domain object with domain_type=ocean and shape=(1,)
|
| 439 |
+
|
| 440 |
+
>>> print atm
|
| 441 |
+
climlab Domain object with domain_type=atm and shape=(2,)
|
| 442 |
+
|
| 443 |
+
"""
|
| 444 |
+
if lev is None:
|
| 445 |
+
levax = Axis(axis_type='lev', num_points=num_lev)
|
| 446 |
+
elif isinstance(lev, Axis):
|
| 447 |
+
levax = lev
|
| 448 |
+
else:
|
| 449 |
+
try:
|
| 450 |
+
levax = Axis(axis_type='lev', points=lev)
|
| 451 |
+
except:
|
| 452 |
+
raise ValueError('lev must be Axis object or pressure array')
|
| 453 |
+
depthax = Axis(axis_type='depth', bounds=[water_depth, 0.])
|
| 454 |
+
slab = SlabOcean(axes=depthax, **kwargs)
|
| 455 |
+
atm = Atmosphere(axes=levax, **kwargs)
|
| 456 |
+
return slab, atm
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def zonal_mean_surface(num_lat=90, water_depth=10., lat=None, **kwargs):
|
| 460 |
+
"""Creates a 1D slab ocean Domain in latitude with uniform water depth.
|
| 461 |
+
|
| 462 |
+
Domain has a single heat capacity according to the specified water depth.
|
| 463 |
+
|
| 464 |
+
**Function-call argument** \n
|
| 465 |
+
|
| 466 |
+
:param int num_lat: number of latitude points [default: 90]
|
| 467 |
+
:param float water_depth: depth of the slab ocean in meters [default: 10.]
|
| 468 |
+
:param lat: specification for latitude axis (optional)
|
| 469 |
+
:type lat: :class:`~climlab.domain.axis.Axis` or latitude array
|
| 470 |
+
:raises: :exc:`ValueError` if `lat` is given but neither Axis nor latitude array.
|
| 471 |
+
:returns: surface domain
|
| 472 |
+
:rtype: :class:`SlabOcean`
|
| 473 |
+
|
| 474 |
+
:Example:
|
| 475 |
+
|
| 476 |
+
::
|
| 477 |
+
|
| 478 |
+
>>> from climlab import domain
|
| 479 |
+
>>> sfc = domain.zonal_mean_surface(num_lat=36)
|
| 480 |
+
|
| 481 |
+
>>> print sfc
|
| 482 |
+
climlab Domain object with domain_type=ocean and shape=(36, 1)
|
| 483 |
+
|
| 484 |
+
"""
|
| 485 |
+
if lat is None:
|
| 486 |
+
latax = Axis(axis_type='lat', num_points=num_lat)
|
| 487 |
+
elif isinstance(lat, Axis):
|
| 488 |
+
latax = lat
|
| 489 |
+
else:
|
| 490 |
+
try:
|
| 491 |
+
latax = Axis(axis_type='lat', points=lat)
|
| 492 |
+
except:
|
| 493 |
+
raise ValueError('lat must be Axis object or latitude array')
|
| 494 |
+
depthax = Axis(axis_type='depth', bounds=[water_depth, 0.])
|
| 495 |
+
axes = {'depth': depthax, 'lat': latax}
|
| 496 |
+
slab = SlabOcean(axes=axes, **kwargs)
|
| 497 |
+
return slab
|
| 498 |
+
|
| 499 |
+
def surface_2D(num_lat=90, num_lon=180, water_depth=10., lon=None,
|
| 500 |
+
lat=None, **kwargs):
|
| 501 |
+
"""Creates a 2D slab ocean Domain in latitude and longitude with uniform water depth.
|
| 502 |
+
|
| 503 |
+
Domain has a single heat capacity according to the specified water depth.
|
| 504 |
+
|
| 505 |
+
**Function-call argument** \n
|
| 506 |
+
|
| 507 |
+
:param int num_lat: number of latitude points [default: 90]
|
| 508 |
+
:param int num_lon: number of longitude points [default: 180]
|
| 509 |
+
:param float water_depth: depth of the slab ocean in meters [default: 10.]
|
| 510 |
+
:param lat: specification for latitude axis (optional)
|
| 511 |
+
:type lat: :class:`~climlab.domain.axis.Axis` or latitude array
|
| 512 |
+
:param lon: specification for longitude axis (optional)
|
| 513 |
+
:type lon: :class:`~climlab.domain.axis.Axis` or longitude array
|
| 514 |
+
:raises: :exc:`ValueError` if `lat` is given but neither Axis nor latitude array.
|
| 515 |
+
:raises: :exc:`ValueError` if `lon` is given but neither Axis nor longitude array.
|
| 516 |
+
:returns: surface domain
|
| 517 |
+
:rtype: :class:`SlabOcean`
|
| 518 |
+
|
| 519 |
+
:Example:
|
| 520 |
+
|
| 521 |
+
::
|
| 522 |
+
|
| 523 |
+
>>> from climlab import domain
|
| 524 |
+
>>> sfc = domain.surface_2D(num_lat=36, num_lat=72)
|
| 525 |
+
|
| 526 |
+
>>> print sfc
|
| 527 |
+
climlab Domain object with domain_type=ocean and shape=(36, 72, 1)
|
| 528 |
+
|
| 529 |
+
"""
|
| 530 |
+
if lat is None:
|
| 531 |
+
latax = Axis(axis_type='lat', num_points=num_lat)
|
| 532 |
+
elif isinstance(lat, Axis):
|
| 533 |
+
latax = lat
|
| 534 |
+
else:
|
| 535 |
+
try:
|
| 536 |
+
latax = Axis(axis_type='lat', points=lat)
|
| 537 |
+
except:
|
| 538 |
+
raise ValueError('lat must be Axis object or latitude array')
|
| 539 |
+
if lon is None:
|
| 540 |
+
lonax = Axis(axis_type='lon', num_points=num_lon)
|
| 541 |
+
elif isinstance(lon, Axis):
|
| 542 |
+
lonax = lon
|
| 543 |
+
else:
|
| 544 |
+
try:
|
| 545 |
+
lonax = Axis(axis_type='lon', points=lon)
|
| 546 |
+
except:
|
| 547 |
+
raise ValueError('lon must be Axis object or longitude array')
|
| 548 |
+
depthax = Axis(axis_type='depth', bounds=[water_depth, 0.])
|
| 549 |
+
axes = {'lat': latax, 'lon': lonax, 'depth': depthax}
|
| 550 |
+
slab = SlabOcean(axes=axes, **kwargs)
|
| 551 |
+
return slab
|
| 552 |
+
|
| 553 |
+
def zonal_mean_column(num_lat=90, num_lev=30, water_depth=10., lat=None,
|
| 554 |
+
lev=None, **kwargs):
|
| 555 |
+
"""Creates two Domains with one water cell, a latitude axis and
|
| 556 |
+
a level/height axis.
|
| 557 |
+
|
| 558 |
+
* SlabOcean: one water cell and a latitude axis above
|
| 559 |
+
(similar to :func:`zonal_mean_surface`)
|
| 560 |
+
* Atmosphere: a latitude axis and a level/height axis (two dimensional)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
**Function-call argument** \n
|
| 564 |
+
|
| 565 |
+
:param int num_lat: number of latitude points on the axis
|
| 566 |
+
[default: 90]
|
| 567 |
+
:param int num_lev: number of pressure levels
|
| 568 |
+
(evenly spaced from surface to TOA) [default: 30]
|
| 569 |
+
:param float water_depth: depth of the water cell (slab ocean) [default: 10.]
|
| 570 |
+
:param lat: specification for latitude axis (optional)
|
| 571 |
+
:type lat: :class:`~climlab.domain.axis.Axis` or latitude array
|
| 572 |
+
:param lev: specification for height axis (optional)
|
| 573 |
+
:type lev: :class:`~climlab.domain.axis.Axis` or pressure array
|
| 574 |
+
:raises: :exc:`ValueError` if `lat` is given but neither Axis nor latitude array.
|
| 575 |
+
:raises: :exc:`ValueError` if `lev` is given but neither Axis nor pressure array.
|
| 576 |
+
:returns: a list of 2 Domain objects (slab ocean, atmosphere)
|
| 577 |
+
:rtype: :py:class:`list` of :class:`SlabOcean`, :class:`Atmosphere`
|
| 578 |
+
|
| 579 |
+
:Example:
|
| 580 |
+
|
| 581 |
+
::
|
| 582 |
+
|
| 583 |
+
>>> from climlab import domain
|
| 584 |
+
>>> sfc, atm = domain.zonal_mean_column(num_lat=36,num_lev=10)
|
| 585 |
+
|
| 586 |
+
>>> print sfc
|
| 587 |
+
climlab Domain object with domain_type=ocean and shape=(36, 1)
|
| 588 |
+
|
| 589 |
+
>>> print atm
|
| 590 |
+
climlab Domain object with domain_type=atm and shape=(36, 10)
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
"""
|
| 594 |
+
if lat is None:
|
| 595 |
+
latax = Axis(axis_type='lat', num_points=num_lat)
|
| 596 |
+
elif isinstance(lat, Axis):
|
| 597 |
+
latax = lat
|
| 598 |
+
else:
|
| 599 |
+
try:
|
| 600 |
+
latax = Axis(axis_type='lat', points=lat)
|
| 601 |
+
except:
|
| 602 |
+
raise ValueError('lat must be Axis object or latitude array')
|
| 603 |
+
if lev is None:
|
| 604 |
+
levax = Axis(axis_type='lev', num_points=num_lev)
|
| 605 |
+
elif isinstance(lev, Axis):
|
| 606 |
+
levax = lev
|
| 607 |
+
else:
|
| 608 |
+
try:
|
| 609 |
+
levax = Axis(axis_type='lev', points=lev)
|
| 610 |
+
except:
|
| 611 |
+
raise ValueError('lev must be Axis object or pressure array')
|
| 612 |
+
|
| 613 |
+
depthax = Axis(axis_type='depth', bounds=[water_depth, 0.])
|
| 614 |
+
#axes = {'depth': depthax, 'lat': latax, 'lev': levax}
|
| 615 |
+
slab = SlabOcean(axes={'lat':latax, 'depth':depthax}, **kwargs)
|
| 616 |
+
atm = Atmosphere(axes={'lat':latax, 'lev':levax}, **kwargs)
|
| 617 |
+
return slab, atm
|
| 618 |
+
|
| 619 |
+
def box_model_domain(num_points=2, **kwargs):
|
| 620 |
+
"""Creates a box model domain (a single abstract axis).
|
| 621 |
+
|
| 622 |
+
:param int num_points: number of boxes [default: 2]
|
| 623 |
+
:returns: Domain with single axis of type ``'abstract'``
|
| 624 |
+
and ``self.domain_type = 'box'``
|
| 625 |
+
:rtype: :class:`_Domain`
|
| 626 |
+
|
| 627 |
+
:Example:
|
| 628 |
+
|
| 629 |
+
::
|
| 630 |
+
|
| 631 |
+
>>> from climlab import domain
|
| 632 |
+
>>> box = domain.box_model_domain(num_points=2)
|
| 633 |
+
|
| 634 |
+
>>> print box
|
| 635 |
+
climlab Domain object with domain_type=box and shape=(2,)
|
| 636 |
+
|
| 637 |
+
"""
|
| 638 |
+
ax = Axis(axis_type='abstract', num_points=num_points)
|
| 639 |
+
boxes = _Domain(axes=ax, **kwargs)
|
| 640 |
+
boxes.domain_type = 'box'
|
| 641 |
+
return boxes
|
climlab/source/climlab/domain/field.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Trying a new data model for state variables and domains:
|
| 2 |
+
# Create a new sub-class of numpy.ndarray
|
| 3 |
+
# that has as an attribute the domain itself
|
| 4 |
+
|
| 5 |
+
# Following a tutorial on subclassing ndarray here:
|
| 6 |
+
#
|
| 7 |
+
# http://docs.scipy.org/doc/numpy/user/basics.subclassing.html
|
| 8 |
+
import numpy as np
|
| 9 |
+
from climlab.domain.xarray import Field_to_xarray
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Field(np.ndarray):
|
| 13 |
+
"""Custom class for climlab gridded quantities, called Field.
|
| 14 |
+
|
| 15 |
+
This class behaves exactly like :py:class:`numpy.ndarray`
|
| 16 |
+
but every object has an attribute called ``self.domain``
|
| 17 |
+
which is the domain associated with that field (e.g. state variables).
|
| 18 |
+
|
| 19 |
+
**Initialization parameters** \n
|
| 20 |
+
|
| 21 |
+
An instance of ``Field`` is initialized with the following
|
| 22 |
+
arguments:
|
| 23 |
+
|
| 24 |
+
:param array input_array: the array which the Field object should be
|
| 25 |
+
initialized with
|
| 26 |
+
:param domain: the domain associated with that field
|
| 27 |
+
(e.g. state variables)
|
| 28 |
+
:type domain: :class:`~climlab.domain.domain._Domain`
|
| 29 |
+
|
| 30 |
+
**Object attributes** \n
|
| 31 |
+
|
| 32 |
+
Following object attribute is generated during initialization:
|
| 33 |
+
|
| 34 |
+
:var domain: the domain associated with that field
|
| 35 |
+
(e.g. state variables)
|
| 36 |
+
:vartype domain: :class:`~climlab.domain.domain._Domain`
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
:Example:
|
| 40 |
+
|
| 41 |
+
::
|
| 42 |
+
|
| 43 |
+
>>> import climlab
|
| 44 |
+
>>> import numpy as np
|
| 45 |
+
>>> from climlab import domain
|
| 46 |
+
>>> from climlab.domain import field
|
| 47 |
+
|
| 48 |
+
>>> # distribution of state
|
| 49 |
+
>>> distr = np.linspace(0., 10., 30)
|
| 50 |
+
>>> # domain creation
|
| 51 |
+
>>> sfc, atm = domain.single_column()
|
| 52 |
+
>>> # build state of type Field
|
| 53 |
+
>>> s = field.Field(distr, domain=atm)
|
| 54 |
+
|
| 55 |
+
>>> print s
|
| 56 |
+
[ 0. 0.34482759 0.68965517 1.03448276 1.37931034
|
| 57 |
+
1.72413793 2.06896552 2.4137931 2.75862069 3.10344828
|
| 58 |
+
3.44827586 3.79310345 4.13793103 4.48275862 4.82758621
|
| 59 |
+
5.17241379 5.51724138 5.86206897 6.20689655 6.55172414
|
| 60 |
+
6.89655172 7.24137931 7.5862069 7.93103448 8.27586207
|
| 61 |
+
8.62068966 8.96551724 9.31034483 9.65517241 10. ]
|
| 62 |
+
|
| 63 |
+
>>> print s.domain
|
| 64 |
+
climlab Domain object with domain_type=atm and shape=(30,)
|
| 65 |
+
|
| 66 |
+
>>> # can slice this and it preserves the domain
|
| 67 |
+
>>> # a more full-featured implementation would have intelligent
|
| 68 |
+
>>> # slicing like in iris
|
| 69 |
+
>>> s.shape == s.domain.shape
|
| 70 |
+
True
|
| 71 |
+
>>> s[:1].shape == s[:1].domain.shape
|
| 72 |
+
False
|
| 73 |
+
|
| 74 |
+
>>> # But some things work very well. E.g. new field creation:
|
| 75 |
+
>>> s2 = np.zeros_like(s)
|
| 76 |
+
|
| 77 |
+
>>> print s2
|
| 78 |
+
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
|
| 79 |
+
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
|
| 80 |
+
|
| 81 |
+
>>> print s2.domain
|
| 82 |
+
climlab Domain object with domain_type=atm and shape=(30,)
|
| 83 |
+
|
| 84 |
+
"""
|
| 85 |
+
def __new__(cls, input_array, domain=None, interfaces=False):
|
| 86 |
+
# Input array is an already formed ndarray instance
|
| 87 |
+
# We first cast to be our class type
|
| 88 |
+
#obj = np.asarray(input_array).view(cls)
|
| 89 |
+
# This should ensure that shape is (1,) for scalar input
|
| 90 |
+
#obj = np.atleast_1d(input_array).view(cls)
|
| 91 |
+
# add the new attribute to the created instance
|
| 92 |
+
# do some checking for correct dimensions
|
| 93 |
+
|
| 94 |
+
# input argument interfaces indicates whether input_array exists
|
| 95 |
+
# on cell interfaces for each dimensions
|
| 96 |
+
# It should be either a single Boolean
|
| 97 |
+
# or an array of Booleans compatible with number of dimensions
|
| 98 |
+
if input_array is None:
|
| 99 |
+
return None
|
| 100 |
+
else:
|
| 101 |
+
try:
|
| 102 |
+
shape = np.array(domain.shape) + np.where(interfaces,1,0)
|
| 103 |
+
except:
|
| 104 |
+
raise ValueError('domain and interfaces inconsistent.')
|
| 105 |
+
try:
|
| 106 |
+
#assert obj.shape == domain.shape
|
| 107 |
+
# This will work if input_array is any of:
|
| 108 |
+
# - scalar
|
| 109 |
+
# - same shape as domain
|
| 110 |
+
# - broadcast-compatible with domain shape
|
| 111 |
+
obj = (input_array * np.ones(shape)).view(cls)
|
| 112 |
+
assert np.all(obj.shape == shape)
|
| 113 |
+
except:
|
| 114 |
+
try:
|
| 115 |
+
# Do we get a match if we add a singleton dimension
|
| 116 |
+
# (e.g. a singleton depth axis)?
|
| 117 |
+
obj = np.expand_dims(input_array, axis=-1).view(cls)
|
| 118 |
+
assert np.all(obj.shape == shape)
|
| 119 |
+
#obj = np.transpose(np.atleast_2d(obj))
|
| 120 |
+
#if obj.shape == domain.shape:
|
| 121 |
+
# obj.domain = domain
|
| 122 |
+
except:
|
| 123 |
+
raise ValueError('Cannot reconcile shapes of input_array and domain.')
|
| 124 |
+
obj.domain = domain
|
| 125 |
+
obj.interfaces = interfaces
|
| 126 |
+
# would be nice to have some automatic domain creation here if none given
|
| 127 |
+
|
| 128 |
+
# Finally, we must return the newly created object:
|
| 129 |
+
return obj
|
| 130 |
+
|
| 131 |
+
def __array_finalize__(self, obj):
|
| 132 |
+
# ``self`` is a new object resulting from
|
| 133 |
+
# ndarray.__new__(Field, ...), therefore it only has
|
| 134 |
+
# attributes that the ndarray.__new__ constructor gave it -
|
| 135 |
+
# i.e. those of a standard ndarray.
|
| 136 |
+
#
|
| 137 |
+
# We could have got to the ndarray.__new__ call in 3 ways:
|
| 138 |
+
# From an explicit constructor - e.g. Field():
|
| 139 |
+
# obj is None
|
| 140 |
+
# (we're in the middle of the Field.__new__
|
| 141 |
+
# constructor, and self.domain will be set when we return to
|
| 142 |
+
# Field.__new__)
|
| 143 |
+
if obj is None: return
|
| 144 |
+
# From view casting - e.g arr.view(Field):
|
| 145 |
+
# obj is arr
|
| 146 |
+
# (type(obj) can be Field)
|
| 147 |
+
# From new-from-template - e.g statearr[:3]
|
| 148 |
+
# type(obj) is Field
|
| 149 |
+
#
|
| 150 |
+
# Note that it is here, rather than in the __new__ method,
|
| 151 |
+
# that we set the default value for 'domain', because this
|
| 152 |
+
# method sees all creation of default objects - with the
|
| 153 |
+
# Field.__new__ constructor, but also with
|
| 154 |
+
# arr.view(Field).
|
| 155 |
+
try:
|
| 156 |
+
self.domain = obj.domain
|
| 157 |
+
except:
|
| 158 |
+
self.domain = None
|
| 159 |
+
try:
|
| 160 |
+
self.interfaces = obj.interfaces
|
| 161 |
+
except:
|
| 162 |
+
pass
|
| 163 |
+
# We do not need to return anything
|
| 164 |
+
|
| 165 |
+
## Loosely based on the approach in numpy.ma.core.MaskedArray
|
| 166 |
+
# This determines how we slice a Field object
|
| 167 |
+
def __getitem__(self, indx):
|
| 168 |
+
"""
|
| 169 |
+
x.__getitem__(y) <==> x[y]
|
| 170 |
+
Return the item described by i, as a Field.
|
| 171 |
+
"""
|
| 172 |
+
# create a view of just the data as np.ndarray and slice it
|
| 173 |
+
dout = self.view(np.ndarray)[indx]
|
| 174 |
+
try:
|
| 175 |
+
#Force dout to type Field
|
| 176 |
+
dout = dout.view(type(self))
|
| 177 |
+
# Now slice the domain
|
| 178 |
+
dout.domain = self.domain[indx]
|
| 179 |
+
# Inherit attributes from self
|
| 180 |
+
if hasattr(self, 'interfaces'):
|
| 181 |
+
dout.interfaces = self.interfaces
|
| 182 |
+
except:
|
| 183 |
+
# The above will fail if we extract a single item
|
| 184 |
+
# in which case we should just return the item
|
| 185 |
+
pass
|
| 186 |
+
return dout
|
| 187 |
+
|
| 188 |
+
def to_xarray(self):
|
| 189 |
+
"""Convert Field object to xarray.DataArray"""
|
| 190 |
+
return Field_to_xarray(self)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def global_mean(field):
|
| 194 |
+
"""Calculates the latitude weighted global mean of a field
|
| 195 |
+
with latitude dependence.
|
| 196 |
+
|
| 197 |
+
:param Field field: input field
|
| 198 |
+
:raises: :exc:`ValueError` if input field has no latitude axis
|
| 199 |
+
:return: latitude weighted global mean of the field
|
| 200 |
+
:rtype: float
|
| 201 |
+
|
| 202 |
+
:Example:
|
| 203 |
+
|
| 204 |
+
initial global mean temperature of EBM model::
|
| 205 |
+
|
| 206 |
+
>>> import climlab
|
| 207 |
+
>>> model = climlab.EBM()
|
| 208 |
+
>>> climlab.global_mean(model.Ts)
|
| 209 |
+
Field(11.997968598413685)
|
| 210 |
+
|
| 211 |
+
"""
|
| 212 |
+
try:
|
| 213 |
+
lat = field.domain.lat.points
|
| 214 |
+
except:
|
| 215 |
+
raise ValueError('No latitude axis in input field.')
|
| 216 |
+
try:
|
| 217 |
+
# Field is 2D latitude / longitude
|
| 218 |
+
lon = field.domain.lon.points
|
| 219 |
+
return _global_mean_latlon(field.squeeze())
|
| 220 |
+
except:
|
| 221 |
+
# Field is 1D latitude only (zonal average)
|
| 222 |
+
lat_radians = np.deg2rad(lat)
|
| 223 |
+
return _global_mean(field.squeeze(), lat_radians)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _global_mean(array, lat_radians):
|
| 227 |
+
# Use np.array() here to strip the Field data and return a plain array
|
| 228 |
+
# (This will be more graceful once we are using xarray.DataArray
|
| 229 |
+
# for all internal grid info instead of the Field object)
|
| 230 |
+
return np.array(np.average(array, weights=np.cos(lat_radians)))
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _global_mean_latlon(field):
|
| 234 |
+
dom = field.domain
|
| 235 |
+
lon, lat = np.meshgrid(dom.lon.points, dom.lat.points)
|
| 236 |
+
dy = np.deg2rad(np.diff(dom.lat.bounds))
|
| 237 |
+
dx = np.deg2rad(np.diff(dom.lon.bounds))*np.cos(np.deg2rad(lat))
|
| 238 |
+
area = dx * dy[:,np.newaxis] # grid cell area in radians^2
|
| 239 |
+
return np.array(np.average(field, weights=area))
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def to_latlon(array, domain, axis = 'lon'):
|
| 243 |
+
"""Broadcasts a 1D axis dependent array across another axis.
|
| 244 |
+
|
| 245 |
+
:param array input_array: the 1D array used for broadcasting
|
| 246 |
+
:param domain: the domain associated with that
|
| 247 |
+
array
|
| 248 |
+
:param axis: the axis that the input array will
|
| 249 |
+
be broadcasted across
|
| 250 |
+
[default: 'lon']
|
| 251 |
+
:return: Field with the same shape as the
|
| 252 |
+
domain
|
| 253 |
+
:Example:
|
| 254 |
+
|
| 255 |
+
::
|
| 256 |
+
|
| 257 |
+
>>> import climlab
|
| 258 |
+
>>> from climlab.domain.field import to_latlon
|
| 259 |
+
>>> import numpy as np
|
| 260 |
+
|
| 261 |
+
>>> state = climlab.surface_state(num_lat=3, num_lon=4)
|
| 262 |
+
>>> m = climlab.EBM_annual(state=state)
|
| 263 |
+
>>> insolation = np.array([237., 417., 237.])
|
| 264 |
+
>>> insolation = to_latlon(insolation, domain = m.domains['Ts'])
|
| 265 |
+
>>> insolation.shape
|
| 266 |
+
(3, 4, 1)
|
| 267 |
+
>>> insolation
|
| 268 |
+
Field([[[ 237.], [[ 417.], [[ 237.],
|
| 269 |
+
[ 237.], [ 417.], [ 237.],
|
| 270 |
+
[ 237.], [ 417.], [ 237.],
|
| 271 |
+
[ 237.]], [ 417.]], [ 237.]]])
|
| 272 |
+
|
| 273 |
+
"""
|
| 274 |
+
# if array is latitude dependent (has the same shape as lat)
|
| 275 |
+
theaxis, array, depth = np.meshgrid(domain.axes[axis].points, array,
|
| 276 |
+
domain.axes['depth'].points)
|
| 277 |
+
if axis == 'lat':
|
| 278 |
+
# if array is longitude dependent (has the same shape as lon)
|
| 279 |
+
np.swapaxes(array,1,0)
|
| 280 |
+
return Field(array, domain=domain)
|
climlab/source/climlab/domain/initial.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Convenience routines for setting up initial conditions."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
from climlab.domain import domain
|
| 4 |
+
from climlab.domain.field import Field
|
| 5 |
+
from climlab.utils.attrdict import AttrDict
|
| 6 |
+
from climlab.utils import legendre
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def column_state(num_lev=30,
|
| 10 |
+
num_lat=1,
|
| 11 |
+
lev=None,
|
| 12 |
+
lat=None,
|
| 13 |
+
water_depth=1.0):
|
| 14 |
+
"""Sets up a state variable dictionary consisting of temperatures
|
| 15 |
+
for atmospheric column (``Tatm``) and surface mixed layer (``Ts``).
|
| 16 |
+
|
| 17 |
+
Surface temperature is always 288 K. Atmospheric temperature is initialized
|
| 18 |
+
between 278 K at lowest altitude and 200 at top of atmosphere according to
|
| 19 |
+
the number of levels given.
|
| 20 |
+
|
| 21 |
+
**Function-call arguments** \n
|
| 22 |
+
|
| 23 |
+
:param int num_lev: number of pressure levels
|
| 24 |
+
(evenly spaced from surface to top of atmosphere)
|
| 25 |
+
[default: 30]
|
| 26 |
+
:param int num_lat: number of latitude points on the axis
|
| 27 |
+
[default: 1]
|
| 28 |
+
:param lev: specification for height axis (optional)
|
| 29 |
+
:type lev: :class:`~climlab.domain.axis.Axis`
|
| 30 |
+
or pressure array
|
| 31 |
+
:param array lat: size of array determines dimension of latitude
|
| 32 |
+
(optional)
|
| 33 |
+
:param float water_depth: *irrelevant*
|
| 34 |
+
|
| 35 |
+
:returns: dictionary with two temperature
|
| 36 |
+
:class:`~climlab.domain.field.Field`
|
| 37 |
+
for atmospheric column ``Tatm`` and
|
| 38 |
+
surface mixed layer ``Ts``
|
| 39 |
+
:rtype: dict
|
| 40 |
+
|
| 41 |
+
:Example:
|
| 42 |
+
|
| 43 |
+
::
|
| 44 |
+
|
| 45 |
+
>>> from climlab.domain import initial
|
| 46 |
+
>>> T_dict = initial.column_state()
|
| 47 |
+
|
| 48 |
+
>>> print T_dict
|
| 49 |
+
{'Tatm': Field([ 200. , 202.68965517, 205.37931034, 208.06896552,
|
| 50 |
+
210.75862069, 213.44827586, 216.13793103, 218.82758621,
|
| 51 |
+
221.51724138, 224.20689655, 226.89655172, 229.5862069 ,
|
| 52 |
+
232.27586207, 234.96551724, 237.65517241, 240.34482759,
|
| 53 |
+
243.03448276, 245.72413793, 248.4137931 , 251.10344828,
|
| 54 |
+
253.79310345, 256.48275862, 259.17241379, 261.86206897,
|
| 55 |
+
264.55172414, 267.24137931, 269.93103448, 272.62068966,
|
| 56 |
+
275.31034483, 278. ]), 'Ts': Field([ 288.])}
|
| 57 |
+
|
| 58 |
+
"""
|
| 59 |
+
if lat is not None:
|
| 60 |
+
num_lat = np.array(lat).size
|
| 61 |
+
if lev is not None:
|
| 62 |
+
num_lev = np.array(lev).size
|
| 63 |
+
|
| 64 |
+
if num_lat == 1:
|
| 65 |
+
sfc, atm = domain.single_column(water_depth=water_depth,
|
| 66 |
+
num_lev=num_lev,
|
| 67 |
+
lev=lev)
|
| 68 |
+
else:
|
| 69 |
+
sfc, atm = domain.zonal_mean_column(water_depth=water_depth,
|
| 70 |
+
num_lev=num_lev,
|
| 71 |
+
lev=lev,
|
| 72 |
+
num_lat=num_lat,
|
| 73 |
+
lat=lat)
|
| 74 |
+
num_lev = atm.lev.num_points
|
| 75 |
+
Ts = Field(288.*np.ones(sfc.shape), domain=sfc)
|
| 76 |
+
Tinitial = np.tile(np.linspace(200., 288.-10., num_lev), sfc.shape)
|
| 77 |
+
Tatm = Field(Tinitial, domain=atm)
|
| 78 |
+
state = AttrDict()
|
| 79 |
+
state['Ts'] = Ts
|
| 80 |
+
state['Tatm'] = Tatm
|
| 81 |
+
return state
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def surface_state(num_lat=90,
|
| 85 |
+
num_lon=None,
|
| 86 |
+
water_depth=10.,
|
| 87 |
+
T0=12.,
|
| 88 |
+
T2=-40.):
|
| 89 |
+
"""Sets up a state variable dictionary for a surface model
|
| 90 |
+
(e.g. :class:`~climlab.model.ebm.EBM`) with a uniform slab ocean depth.
|
| 91 |
+
|
| 92 |
+
The domain is either 1D (latitude) or 2D (latitude, longitude)
|
| 93 |
+
depending on whether the input argument num_lon is supplied.
|
| 94 |
+
|
| 95 |
+
Returns a single state variable `Ts`, the temperature of the surface
|
| 96 |
+
mixed layer (slab ocean).
|
| 97 |
+
|
| 98 |
+
The temperature is initialized to a smooth equator-to-pole shape given by
|
| 99 |
+
|
| 100 |
+
.. math::
|
| 101 |
+
|
| 102 |
+
T(\phi) = T_0 + T_2 P_2(\sin\phi)
|
| 103 |
+
|
| 104 |
+
where :math:`\phi` is latitude, and :math:`P_2` is the second Legendre
|
| 105 |
+
polynomial :class:`~climlab.utils.legendre.P2`.
|
| 106 |
+
|
| 107 |
+
**Function-call arguments** \n
|
| 108 |
+
|
| 109 |
+
:param int num_lat: number of latitude points [default: 90]
|
| 110 |
+
:param int num_lat: (optional) number of longitude points [default: None]
|
| 111 |
+
:param float water_depth: depth of the slab ocean in meters [default: 10.]
|
| 112 |
+
:param float T0: global-mean initial temperature in :math:`^{\circ} \\textrm{C}` [default: 12.]
|
| 113 |
+
:param float T2: 2nd Legendre coefficient for equator-to-pole gradient in
|
| 114 |
+
initial temperature, in :math:`^{\circ} \\textrm{C}` [default: -40.]
|
| 115 |
+
|
| 116 |
+
:returns: dictionary with temperature
|
| 117 |
+
:class:`~climlab.domain.field.Field`
|
| 118 |
+
for surface mixed layer ``Ts``
|
| 119 |
+
:rtype: dict
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
:Example:
|
| 123 |
+
|
| 124 |
+
::
|
| 125 |
+
|
| 126 |
+
>>> from climlab.domain import initial
|
| 127 |
+
>>> import numpy as np
|
| 128 |
+
|
| 129 |
+
>>> T_dict = initial.surface_state(num_lat=36)
|
| 130 |
+
|
| 131 |
+
>>> print np.squeeze(T_dict['Ts'])
|
| 132 |
+
[-27.88584094 -26.97777479 -25.18923361 -22.57456133 -19.21320344
|
| 133 |
+
-15.20729309 -10.67854785 -5.76457135 -0.61467228 4.61467228
|
| 134 |
+
9.76457135 14.67854785 19.20729309 23.21320344 26.57456133
|
| 135 |
+
29.18923361 30.97777479 31.88584094 31.88584094 30.97777479
|
| 136 |
+
29.18923361 26.57456133 23.21320344 19.20729309 14.67854785
|
| 137 |
+
9.76457135 4.61467228 -0.61467228 -5.76457135 -10.67854785
|
| 138 |
+
-15.20729309 -19.21320344 -22.57456133 -25.18923361 -26.97777479
|
| 139 |
+
-27.88584094]
|
| 140 |
+
|
| 141 |
+
"""
|
| 142 |
+
if num_lon is None:
|
| 143 |
+
sfc = domain.zonal_mean_surface(num_lat=num_lat,
|
| 144 |
+
water_depth=water_depth)
|
| 145 |
+
else:
|
| 146 |
+
sfc = domain.surface_2D(num_lat=num_lat,
|
| 147 |
+
num_lon=num_lon,
|
| 148 |
+
water_depth=water_depth)
|
| 149 |
+
if 'lon' in sfc.axes:
|
| 150 |
+
lon, lat = np.meshgrid(sfc.axes['lon'].points, sfc.axes['lat'].points)
|
| 151 |
+
else:
|
| 152 |
+
lat = sfc.axes['lat'].points
|
| 153 |
+
sinphi = np.sin(np.deg2rad(lat))
|
| 154 |
+
initial = T0 + T2 * legendre.P2(sinphi)
|
| 155 |
+
Ts = Field(initial, domain=sfc)
|
| 156 |
+
#if num_lon is None:
|
| 157 |
+
# Ts = Field(initial, domain=sfc)
|
| 158 |
+
#else:
|
| 159 |
+
# Ts = Field([[initial for k in range(num_lon)]], domain=sfc)
|
| 160 |
+
state = AttrDict()
|
| 161 |
+
state['Ts'] = Ts
|
| 162 |
+
return state
|
climlab/source/climlab/domain/xarray.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from builtins import str
|
| 2 |
+
from builtins import object
|
| 3 |
+
from xarray import Dataset, DataArray
|
| 4 |
+
import warnings
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def Field_to_xarray(field):
|
| 8 |
+
'''Convert a climlab.Field object to xarray.DataArray'''
|
| 9 |
+
dom = field.domain
|
| 10 |
+
dims = []; dimlist = []; coords = {};
|
| 11 |
+
for axname in dom.axes:
|
| 12 |
+
dimlist.append(axname)
|
| 13 |
+
try:
|
| 14 |
+
assert field.interfaces[dom.axis_index[axname]]
|
| 15 |
+
bounds_name = axname + '_bounds'
|
| 16 |
+
dims.append(bounds_name)
|
| 17 |
+
coords[bounds_name] = dom.axes[axname].bounds
|
| 18 |
+
except:
|
| 19 |
+
dims.append(axname)
|
| 20 |
+
coords[axname] = dom.axes[axname].points
|
| 21 |
+
# Might need to reorder the data
|
| 22 |
+
da = DataArray(field.transpose([dom.axis_index[name] for name in dimlist]),
|
| 23 |
+
dims=dims, coords=coords)
|
| 24 |
+
for name in dims:
|
| 25 |
+
try:
|
| 26 |
+
da[name].attrs['units'] = dom.axes[name].units
|
| 27 |
+
except:
|
| 28 |
+
pass
|
| 29 |
+
return da
|
| 30 |
+
|
| 31 |
+
def state_to_xarray(state):
|
| 32 |
+
'''Convert a dictionary of climlab.Field objects to xarray.Dataset
|
| 33 |
+
|
| 34 |
+
Input: dictionary of climlab.Field objects
|
| 35 |
+
(e.g. process.state or process.diagnostics dictionary)
|
| 36 |
+
|
| 37 |
+
Output: xarray.Dataset object with all spatial axes,
|
| 38 |
+
including 'bounds' axes indicating cell boundaries in each spatial dimension.
|
| 39 |
+
|
| 40 |
+
Any items in the dictionary that are not instances of climlab.Field
|
| 41 |
+
are ignored.'''
|
| 42 |
+
from climlab.domain.field import Field
|
| 43 |
+
|
| 44 |
+
ds = Dataset()
|
| 45 |
+
for name, field in state.items():
|
| 46 |
+
if isinstance(field, Field):
|
| 47 |
+
ds[name] = Field_to_xarray(field)
|
| 48 |
+
dom = field.domain
|
| 49 |
+
for axname, ax in dom.axes.items():
|
| 50 |
+
bounds_name = axname + '_bounds'
|
| 51 |
+
ds.coords[bounds_name] = DataArray(ax.bounds, dims=[bounds_name],
|
| 52 |
+
coords={bounds_name:ax.bounds})
|
| 53 |
+
try:
|
| 54 |
+
ds[bounds_name].attrs['units'] = ax.units
|
| 55 |
+
except:
|
| 56 |
+
pass
|
| 57 |
+
else:
|
| 58 |
+
warnings.warn('{} excluded from Dataset because it is not a Field variable.'.format(name))
|
| 59 |
+
return ds
|
| 60 |
+
|
| 61 |
+
def to_xarray(input):
|
| 62 |
+
'''Convert climlab input to xarray format.
|
| 63 |
+
|
| 64 |
+
If input is a climlab.Field object, return xarray.DataArray
|
| 65 |
+
|
| 66 |
+
If input is a dictionary (e.g. process.state or process.diagnostics),
|
| 67 |
+
return xarray.Dataset object with all spatial axes,
|
| 68 |
+
including 'bounds' axes indicating cell boundaries in each spatial dimension.
|
| 69 |
+
|
| 70 |
+
Any items in the dictionary that are not instances of climlab.Field
|
| 71 |
+
are ignored.'''
|
| 72 |
+
from climlab.domain.field import Field
|
| 73 |
+
if isinstance(input, Field):
|
| 74 |
+
return Field_to_xarray(input)
|
| 75 |
+
elif isinstance(input, dict):
|
| 76 |
+
return state_to_xarray(input)
|
| 77 |
+
else:
|
| 78 |
+
raise TypeError('input must be Field object or dictionary of Field objects')
|
climlab/source/climlab/dynamics/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
Modules for simple dynamics, mostly for use in Energy Balance Models.
|
| 3 |
+
|
| 4 |
+
:class:`~climlab.dynamics.BudykoTransport` is a relaxation to global mean.
|
| 5 |
+
|
| 6 |
+
:class:`~climlab.dynamics.LargeScaleCondensation` handles condensation due to
|
| 7 |
+
convergence of water vapor associated with the dynamics.
|
| 8 |
+
|
| 9 |
+
Other modules are 1D advection-diffusion solvers (implemented using implicit timestepping).
|
| 10 |
+
|
| 11 |
+
:class:`~climlab.dynamics.AdvectionDiffusion` is a general-purpose 1D
|
| 12 |
+
advection-diffusion process. It can be used out-of-the-box for models with
|
| 13 |
+
Cartesian grid geometry, but also accepts weighting functions for the
|
| 14 |
+
divergence operator on curvilinear grids.
|
| 15 |
+
|
| 16 |
+
:class:`~climlab.dynamics.MeridionalAdvectionDiffusion` implements the
|
| 17 |
+
1D advection-diffusion process on the sphere (flux in the north-south direction).
|
| 18 |
+
|
| 19 |
+
Subclass :class:`~climlab.dynamics.MeridionalHeatDiffusion` is the appropriate class
|
| 20 |
+
for the traditional diffusive EBM, in which transport is parameterized as a
|
| 21 |
+
meridional diffusion process down the zonal-mean surface temperature gradient.
|
| 22 |
+
|
| 23 |
+
:class:`~climlab.dynamics.MeridionalMoistDiffusion` implements the moist EBM,
|
| 24 |
+
with transport down an approximate gradient in near-surface moist static energy.
|
| 25 |
+
'''
|
| 26 |
+
|
| 27 |
+
from .budyko_transport import BudykoTransport
|
| 28 |
+
from .advection_diffusion import AdvectionDiffusion, Diffusion
|
| 29 |
+
from .meridional_advection_diffusion import MeridionalAdvectionDiffusion, MeridionalDiffusion
|
| 30 |
+
from .meridional_heat_diffusion import MeridionalHeatDiffusion
|
| 31 |
+
from .meridional_moist_diffusion import MeridionalMoistDiffusion
|
| 32 |
+
from .large_scale_condensation import LargeScaleCondensation
|
climlab/source/climlab/dynamics/adv_diff_numerics.py
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r'''
|
| 2 |
+
The 1D advection-diffusion problem
|
| 3 |
+
----------------------------------
|
| 4 |
+
|
| 5 |
+
The equation to be solved is
|
| 6 |
+
|
| 7 |
+
.. math::
|
| 8 |
+
|
| 9 |
+
\frac{\partial}{\partial t} \psi(x,t) &= -\frac{1}{w(x)} \frac{\partial}{\partial x} \left[ w(x) ~ \mathcal{F}(x,t) \right] + \dot{\psi}\\
|
| 10 |
+
\mathcal{F} &= U(x) \psi(x) -K(x) ~ \frac{\partial \psi}{\partial x} + F(x)
|
| 11 |
+
|
| 12 |
+
for the following quantities:
|
| 13 |
+
|
| 14 |
+
- state variable :math:`\psi(x,t)`
|
| 15 |
+
- diffusivity :math:`K(x)` in units of :math:`x^2 ~ t^{-1}`
|
| 16 |
+
- advecting velocity :math:`U(x)` in units of :math:`x ~ t^{-1}`
|
| 17 |
+
- a prescribed flux :math:`F(x)` (including boundary conditions) in units of :math:`\psi ~ x ~ t^{-1}`
|
| 18 |
+
- a scalar source/sink :math:`\dot{\psi}(x)` in units of :math:`\psi ~ t^{-1}`
|
| 19 |
+
- weighting function :math:`w(x)` for the divergence operator on curvilinear grids.
|
| 20 |
+
|
| 21 |
+
The boundary condition is a flux condition at the end points:
|
| 22 |
+
|
| 23 |
+
.. math::
|
| 24 |
+
\begin{align} \label{eq:fluxcondition}
|
| 25 |
+
\mathcal{F}(x_0) &= F(x_0) & \mathcal{F}(x_J) &= F(x_J)
|
| 26 |
+
\end{align}
|
| 27 |
+
|
| 28 |
+
which requires that the advecting velocity :math:`u(x) = 0` at the end points :math:`x_0, x_J`
|
| 29 |
+
|
| 30 |
+
The solver is implemented on a 1D staggered grid, with J+1 flux points
|
| 31 |
+
and J scalar points located somewhere between the flux points.
|
| 32 |
+
|
| 33 |
+
The solver does **not** assume the gridpoints are evenly spaced in :math:`x`.
|
| 34 |
+
|
| 35 |
+
Routines are provided to compute the following:
|
| 36 |
+
|
| 37 |
+
- Advective, diffusive, and total fluxes (the terms of :math:`\mathcal{F}`)
|
| 38 |
+
- Tridiagonal matrix operator for the flux convergence
|
| 39 |
+
- The actual flux convergence, or instantaneous scalar tendency given a current value of :math:`\psi(x)`
|
| 40 |
+
- Future value of :math:`\psi(x)` for an implicit timestep
|
| 41 |
+
|
| 42 |
+
Some details of the solver formulas are laid out below for reference.
|
| 43 |
+
|
| 44 |
+
Spatial discretization
|
| 45 |
+
----------------------
|
| 46 |
+
|
| 47 |
+
We use a non-uniform staggered spatial grid with scalar :math:`\psi` evaluated at :math:`J` points,
|
| 48 |
+
and flux :math:`\mathcal{F}` evaluated at :math:`J+1` flux points.
|
| 49 |
+
The indexing will run from :math:`j=0` to :math:`j=J` for the flux points,
|
| 50 |
+
and :math:`i=0` to :math:`i=J-1` for the scalar points.
|
| 51 |
+
This notation is consistent with zero-indexed Python arrays.
|
| 52 |
+
|
| 53 |
+
We define the following arrays:
|
| 54 |
+
|
| 55 |
+
- :math:`\mathcal{X}_b[j]` is a length J+1 array defining the location of the flux points.
|
| 56 |
+
- :math:`\mathcal{X}[i]` is a length J array defining the location of the scalar points, where point :math:`\mathcal{X}[j]` is somewhere between :math:`\mathcal{X}_b[j]` and :math:`\mathcal{X}_b[j+1]` for all :math:`j<J`.
|
| 57 |
+
- :math:`\psi[i], \dot{\psi}[i]` are length J arrays defined on :math:`\mathcal{X}`.
|
| 58 |
+
- :math:`U[j], K[j], F[j]` are all arrays of length J+1 defined on :math:`\mathcal{X}_b`.
|
| 59 |
+
- The grid weights are similarly in arrays :math:`W_b[j], W[i]` respectively on :math:`\mathcal{X}_b`` and :math:`\mathcal{X}`.
|
| 60 |
+
|
| 61 |
+
Centered difference formulas for the flux
|
| 62 |
+
-----------------------------------------
|
| 63 |
+
|
| 64 |
+
We use centered differences in :math:`x` to discretize the spatial derivatives.
|
| 65 |
+
The diffusive component of the flux is thus
|
| 66 |
+
|
| 67 |
+
.. math::
|
| 68 |
+
|
| 69 |
+
\begin{align*}
|
| 70 |
+
\mathcal{F}_{diff}[j] &= - K[j] \frac{ \left( \psi[i] - \psi[i-1] \right) }{\left( \mathcal{X}[i] - \mathcal{X}[i-1] \right)} & j&=i=1,2,...,J-1
|
| 71 |
+
\end{align*}
|
| 72 |
+
|
| 73 |
+
The diffusive flux is assumed to be zero at the boundaries.
|
| 74 |
+
|
| 75 |
+
The advective term requires an additional approximation since the scalar :math:`\psi` is not defined at the flux points.
|
| 76 |
+
We use a linear interpolation to the flux points:
|
| 77 |
+
|
| 78 |
+
.. math::
|
| 79 |
+
|
| 80 |
+
\begin{align*}
|
| 81 |
+
\psi_b[j] &\equiv \psi[i-1] \left( \frac{\mathcal{X}[i] - \mathcal{X}_b[j]}{\mathcal{X}[i] - \mathcal{X}[i-1]} \right) + \psi[i] \left( \frac{ \mathcal{X}_b[j] - \mathcal{X}[i-1] }{\mathcal{X}[i] - \mathcal{X}[i-1]} \right) & j&=i=1,2,...,J-1
|
| 82 |
+
\end{align*}
|
| 83 |
+
|
| 84 |
+
Note that for an evenly spaced grid, this reduces to the simple average :math:`\frac{1}{2} \left( \psi[i-1] + \psi[i] \right)`.
|
| 85 |
+
|
| 86 |
+
With this interpolation, the advective flux is approximated by
|
| 87 |
+
|
| 88 |
+
.. math::
|
| 89 |
+
|
| 90 |
+
\begin{align*}
|
| 91 |
+
\mathcal{F}_{adv}[j] &= \frac{U[j] }{\mathcal{X}[i] - \mathcal{X}[i-1]} \left( \psi[i-1] (\mathcal{X}[i] - \mathcal{X}_b[j]) + \psi[i] (\mathcal{X}_b[j] - \mathcal{X}[i-1]) \right) & j&=i=1,2,...,J-1
|
| 92 |
+
\end{align*}
|
| 93 |
+
|
| 94 |
+
The total flux away from the boundaries (after some recombining terms) is thus:
|
| 95 |
+
|
| 96 |
+
.. math::
|
| 97 |
+
|
| 98 |
+
\mathcal{F}[j] = F[j] + \psi[i-1] \left( \frac{K[j] + U[j] (\mathcal{X}[i] - \mathcal{X}_b[j]) }{ \mathcal{X}[i] - \mathcal{X}[i-1] } \right) - \psi[i] \left( \frac{K[j] - U[j] (\mathcal{X}_b[j] - \mathcal{X}[i-1]) }{\mathcal{X}[i] - \mathcal{X}[i-1] } \right)
|
| 99 |
+
|
| 100 |
+
which is valid for j=i=1,2,...,J-1.
|
| 101 |
+
|
| 102 |
+
Centered difference formulas for the flux convergence
|
| 103 |
+
-----------------------------------------------------
|
| 104 |
+
|
| 105 |
+
Centered difference approximation of the flux convergence gives
|
| 106 |
+
|
| 107 |
+
.. math::
|
| 108 |
+
|
| 109 |
+
\begin{align*}
|
| 110 |
+
\frac{\partial }{\partial t} \psi[i] &= -\frac{ W_b[j+1] \mathcal{F}[j+1] - W_b[j] \mathcal{F}[j] }{W[i] ( \mathcal{X}_b[j+1] - \mathcal{X}_b[j] )} + \dot{\psi}[i] & i&=j=0,1,...,J-1
|
| 111 |
+
\end{align*}
|
| 112 |
+
|
| 113 |
+
The flux convergences are best expressed together in matrix form:
|
| 114 |
+
|
| 115 |
+
.. math::
|
| 116 |
+
|
| 117 |
+
\begin{equation}
|
| 118 |
+
\frac{\partial \boldsymbol{\psi}}{\partial t} = \boldsymbol{T} ~ \boldsymbol{\psi} + \boldsymbol{S}
|
| 119 |
+
\end{equation}
|
| 120 |
+
|
| 121 |
+
where :math:`\boldsymbol{\psi}` is the :math:`J\times1` column vector,
|
| 122 |
+
:math:`\boldsymbol{S}` is a :math:`J\times1` column vector
|
| 123 |
+
representing the prescribed flux convergence and source terms, whose elements are
|
| 124 |
+
|
| 125 |
+
.. math::
|
| 126 |
+
|
| 127 |
+
\begin{align}
|
| 128 |
+
S[i] &= \frac{-W_b[j+1] F[j+1] + W_b[j] F[j]}{W[i] ( \mathcal{X}_b[j+1] - \mathcal{X}_b[j] )} + \dot{\psi}[i] & i&=j=0,1,...,J-1
|
| 129 |
+
\end{align}
|
| 130 |
+
|
| 131 |
+
and :math:`\boldsymbol{T}` is a :math:`J\times J` tridiagonal matrix:
|
| 132 |
+
|
| 133 |
+
.. math::
|
| 134 |
+
|
| 135 |
+
\begin{equation}
|
| 136 |
+
\boldsymbol{T} ~ \boldsymbol{\psi} = \left[\begin{array}{ccccccc} T_{m0} & T_{u1} & 0 & ... & 0 & 0 & 0 \\T_{l0} & T_{m1} & T_{u2} & ... & 0 & 0 & 0 \\ 0 & T_{l1} & T_{m2} & ... & 0 & 0 & 0 \\... & ... & ... & ... & ... & ... & ... \\0 & 0 & 0 & ... & T_{m(J-3)} & T_{u(J-2)} & 0 \\0 & 0 & 0 & ... & T_{l(J-3)} & T_{m(J-2)} & T_{u(J-1)} \\0 & 0 & 0 & ... & 0 & T_{l(J-2)} & T_{m(J-1)}\end{array}\right] \left[\begin{array}{c} \psi_0 \\ \psi_1 \\ \psi_2 \\... \\ \psi_{J-3} \\ \psi_{J-2} \\ \psi_{J-1} \end{array}\right]
|
| 137 |
+
\end{equation}
|
| 138 |
+
|
| 139 |
+
with vectors :math:`T_l, T_m, T_u` representing respectively the lower, main, and upper diagonals of :math:`\boldsymbol{T}`.
|
| 140 |
+
We will treat all three vectors as length J;
|
| 141 |
+
the 0th element of :math:`T_u` is ignored while the (J-1)th element of :math:`T_l` is ignored
|
| 142 |
+
(this is consistent with the expected inputs for the Python module scipy.linalg.solve_banded).
|
| 143 |
+
|
| 144 |
+
The instantanous tendency is then easily computed by matrix multiplication.
|
| 145 |
+
|
| 146 |
+
The elements of the main diagonal of :math:`\boldsymbol{\psi}` can be computed from
|
| 147 |
+
|
| 148 |
+
.. math::
|
| 149 |
+
|
| 150 |
+
\begin{align} \label{eq:maindiag}
|
| 151 |
+
\begin{split}
|
| 152 |
+
T_m[i] &= -\left( \frac{ W_b[j+1] \big( K[j+1] + U[j+1] (\mathcal{X}[i+1] - \mathcal{X}_b[j+1]) \big) }{ W[i] ( \mathcal{X}_b[j+1] - \mathcal{X}_b[j] )(\mathcal{X}[i+1] - \mathcal{X}[i]) } \right) \\
|
| 153 |
+
& \qquad - \left( \frac{W_b[j] \big( K[j] - U[j] (\mathcal{X}_b[j] - \mathcal{X}[i-1]) \big) }{W[i] ( \mathcal{X}_b[j+1] - \mathcal{X}_b[j] )(\mathcal{X}[i] - \mathcal{X}[i-1]) } \right) \\
|
| 154 |
+
i &=j=0,2,...,J-1
|
| 155 |
+
\end{split}
|
| 156 |
+
\end{align}
|
| 157 |
+
|
| 158 |
+
which is valid at the boundaries so long as we set :math:`W_b[0] = W_b[J] = 0`.
|
| 159 |
+
|
| 160 |
+
The lower diagonal (including the right boundary condition) is computed from
|
| 161 |
+
|
| 162 |
+
.. math::
|
| 163 |
+
|
| 164 |
+
\begin{align} \label{eq:lowerdiag}
|
| 165 |
+
\begin{split}
|
| 166 |
+
T_l[i-1] &= \left( \frac{W_b[j]}{W[i] } \right) \left( \frac{ K[j] + U[j] (\mathcal{X}[i] - \mathcal{X}_b[j]) }{( \mathcal{X}_b[j+1] - \mathcal{X}_b[j] ) (\mathcal{X}[i] - \mathcal{X}[i-1] )} \right) \\
|
| 167 |
+
i &=j =1,2,...,J-2, J-1
|
| 168 |
+
\end{split}
|
| 169 |
+
\end{align}
|
| 170 |
+
|
| 171 |
+
Finally the upper diagonal (including the left boundary condition) is computed from
|
| 172 |
+
|
| 173 |
+
.. math::
|
| 174 |
+
|
| 175 |
+
\begin{align} \label{eq:upperdiag}
|
| 176 |
+
\begin{split}
|
| 177 |
+
T_u[i+1] &= \left( \frac{W_b[j+1]}{W[i]} \right) \left( \frac{K[j+1] - U[j+1] (\mathcal{X}_b[j+1] - \mathcal{X}[i]) }{( \mathcal{X}_b[j+1] - \mathcal{X}_b[j] )(\mathcal{X}[i+1] - \mathcal{X}[i] ) } \right) \\
|
| 178 |
+
i &= j=0,...,J-2
|
| 179 |
+
\end{split}
|
| 180 |
+
\end{align}
|
| 181 |
+
|
| 182 |
+
Implicit time discretization
|
| 183 |
+
----------------------------
|
| 184 |
+
|
| 185 |
+
The forward-time finite difference approximation to LHS of the flux-convergence equation is simply
|
| 186 |
+
|
| 187 |
+
.. math::
|
| 188 |
+
\begin{equation}
|
| 189 |
+
\frac{\partial \psi[i]}{\partial t} \approx \frac{\psi^{n+1}[i]- \psi^{n}[i]}{\Delta t}
|
| 190 |
+
\end{equation}
|
| 191 |
+
|
| 192 |
+
where the superscript :math:`n` indicates the time index.
|
| 193 |
+
|
| 194 |
+
We use the implicit-time method, in which the RHS is evaluated at the future time :math:`n+1`.
|
| 195 |
+
Applying this to the matrix equation above
|
| 196 |
+
and moving all the terms at time :math:`n+1` over to the LHS yields
|
| 197 |
+
|
| 198 |
+
.. math::
|
| 199 |
+
|
| 200 |
+
\begin{equation} \label{eq:implicit_tridiagonal}
|
| 201 |
+
\left( \boldsymbol{I} - \boldsymbol{T} \Delta t \right) \boldsymbol{\psi}^{n+1} = \boldsymbol{\psi}^{n} + \boldsymbol{S} \Delta t
|
| 202 |
+
\end{equation}
|
| 203 |
+
|
| 204 |
+
where :math:`\boldsymbol{I}` is the :math:`J\times J` identity matrix.
|
| 205 |
+
|
| 206 |
+
Solving for the future value :math:`\boldsymbol{\psi}^{n+1}` is then accomplished
|
| 207 |
+
by solving the :math:`J \times J` tridiagonal linear system using standard routines.
|
| 208 |
+
|
| 209 |
+
Analytical benchmark
|
| 210 |
+
--------------------
|
| 211 |
+
|
| 212 |
+
Here is an analytical case to be used for testing purposes to validate the numerical code.
|
| 213 |
+
This is implemented in the CLIMLAB test suite.
|
| 214 |
+
|
| 215 |
+
- :math:`K=K_0` is constant
|
| 216 |
+
- :math:`w(x) = 1` everywhere (Cartesian coordinates)
|
| 217 |
+
- :math:`F = 0` everywhere
|
| 218 |
+
- :math:`\psi(x,0) = \psi_0 \sin^2\left(\frac{\pi x}{L}\right)`
|
| 219 |
+
- :math:`u(x) = U_0 \sin\left(\frac{\pi x}{L}\right)`
|
| 220 |
+
for a domain with endpoints at :math:`x=0` and :math:`x=L`.
|
| 221 |
+
|
| 222 |
+
The analytical solution is
|
| 223 |
+
|
| 224 |
+
.. math::
|
| 225 |
+
|
| 226 |
+
\begin{align}
|
| 227 |
+
\mathcal{F} &= \psi_0 \sin\left(\frac{\pi x}{L}\right) \left[U_0 \sin^2\left(\frac{\pi x}{L}\right) - 2K \frac{\pi}{L} \cos\left(\frac{\pi x}{L}\right) \right] \\
|
| 228 |
+
\frac{\partial \psi}{\partial t} &= -\psi_0 \frac{\pi}{L} \left\{ 3 U_0 \sin^2\left(\frac{\pi x}{L}\right) \cos\left(\frac{\pi x}{L}\right) -2K\frac{\pi}{L} \left[\cos^2\left(\frac{\pi x}{L}\right) -\sin^2\left(\frac{\pi x}{L}\right) \right] \right\}
|
| 229 |
+
\end{align}
|
| 230 |
+
|
| 231 |
+
which satisfies the boundary condition :math:`\mathcal{F} = 0` at :math:`x=0` and :math:`x=L`.
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
Module function reference
|
| 235 |
+
-------------------------
|
| 236 |
+
|
| 237 |
+
All the functions in ``climlab.dynamics.adv_diff_numerics`` are vectorized
|
| 238 |
+
to handle multidimensional input. The key assumption is that
|
| 239 |
+
**advection-diffusion operates along the final dimension**.
|
| 240 |
+
|
| 241 |
+
Inputs should be reshaped appropriately (e.g. with ``numpy.moveaxis()``)
|
| 242 |
+
before calling these functions.
|
| 243 |
+
'''
|
| 244 |
+
from numpy import zeros, ones, zeros_like, ones_like, matmul, diag, diag_indices, diff, newaxis
|
| 245 |
+
from numpy.linalg import solve
|
| 246 |
+
from scipy.linalg import solve_banded
|
| 247 |
+
|
| 248 |
+
def diffusive_flux(X, Xb, K, field):
|
| 249 |
+
'''Return the diffusive flux on cell boundaries (length J+1)'''
|
| 250 |
+
flux = zeros_like(K)
|
| 251 |
+
flux[...,1:-1] += field[...,:-1]*K[...,1:-1]/diff(X,axis=-1)
|
| 252 |
+
flux[...,1:-1] -= field[...,1:]*K[...,1:-1]/diff(X,axis=-1)
|
| 253 |
+
return flux
|
| 254 |
+
|
| 255 |
+
def advective_flux(X, Xb, U, field):
|
| 256 |
+
'''Return the advective flux on cell boundaries (length J+1)'''
|
| 257 |
+
flux = zeros_like(U)
|
| 258 |
+
flux[...,1:-1] += field[...,:-1]*(U[...,1:-1]*(X[...,1:]-Xb[...,1:-1]))/diff(X,axis=-1)
|
| 259 |
+
flux[...,1:-1] -= field[...,1:]*(-U[...,1:-1]*(Xb[...,1:-1]-X[...,:-1]))/diff(X,axis=-1)
|
| 260 |
+
return flux
|
| 261 |
+
|
| 262 |
+
def total_flux(X, Xb, K, U, field, prescribed_flux=None):
|
| 263 |
+
'''Return the total (advective + diffusive + prescribed) flux
|
| 264 |
+
on cell boundaries (length J+1)'''
|
| 265 |
+
if prescribed_flux is None:
|
| 266 |
+
prescribed_flux = zeros_like(U)
|
| 267 |
+
return advective_flux(X, Xb, U, field) + diffusive_flux(X, Xb, K, field) + prescribed_flux
|
| 268 |
+
|
| 269 |
+
def advdiff_tridiag(X, Xb, K, U, W=None, Wb=None, use_banded_solver=False):
|
| 270 |
+
r'''Compute the tridiagonal matrix operator for the advective-diffusive
|
| 271 |
+
flux convergence.
|
| 272 |
+
|
| 273 |
+
Input arrays of length J+1:
|
| 274 |
+
Xb, Wb, K, U
|
| 275 |
+
Input arrays of length J:
|
| 276 |
+
X, W
|
| 277 |
+
|
| 278 |
+
The 0th and Jth (i.e. first and last) elements of Wb are ignored;
|
| 279 |
+
assuming boundary condition is a prescribed flux.
|
| 280 |
+
|
| 281 |
+
The return value depends on input flag ``use_banded_solver``
|
| 282 |
+
|
| 283 |
+
If ``use_banded_solver==True``, return a 3xJ array containing the elements of the tridiagonal.
|
| 284 |
+
This version is restricted to 1D input arrays,
|
| 285 |
+
but is suitable for use with the efficient banded solver.
|
| 286 |
+
|
| 287 |
+
If ``use_banded_solver=False`` (which it must be for multidimensional input),
|
| 288 |
+
return an array (...,J,J) with the full tridiagonal matrix.
|
| 289 |
+
'''
|
| 290 |
+
J = X.shape[-1]
|
| 291 |
+
if (W is None):
|
| 292 |
+
W = ones_like(X)
|
| 293 |
+
if (Wb is None):
|
| 294 |
+
Wb = ones_like(Xb)
|
| 295 |
+
# These are all length (J-1) in the last axis
|
| 296 |
+
lower_diagonal = (Wb[...,1:-1]/W[...,1:] *
|
| 297 |
+
(K[...,1:-1]+U[...,1:-1]*(X[...,1:]-Xb[...,1:-1])) /
|
| 298 |
+
((Xb[...,2:]-Xb[...,1:-1])*(X[...,1:]-X[...,:-1])))
|
| 299 |
+
upper_diagonal = (Wb[...,1:-1]/W[...,:-1] *
|
| 300 |
+
(K[...,1:-1]-U[...,1:-1]*(Xb[...,1:-1]-X[...,:-1])) /
|
| 301 |
+
((Xb[...,1:-1]-Xb[...,:-2])*(X[...,1:]-X[...,:-1])))
|
| 302 |
+
main_diagonal_term1 = (-Wb[...,1:-1]/W[...,:-1] *
|
| 303 |
+
(K[...,1:-1]+U[...,1:-1]*(X[...,1:]-Xb[...,1:-1])) /
|
| 304 |
+
((Xb[...,1:-1]-Xb[...,:-2])*(X[...,1:]-X[...,:-1])))
|
| 305 |
+
main_diagonal_term2 = (-Wb[...,1:-1]/W[...,1:] *
|
| 306 |
+
(K[...,1:-1]-U[...,1:-1]*(Xb[...,1:-1]-X[...,:-1])) /
|
| 307 |
+
((Xb[...,2:]-Xb[...,1:-1])*(X[...,1:]-X[...,:-1])))
|
| 308 |
+
if use_banded_solver:
|
| 309 |
+
# Pack the diagonals into a 3xJ array
|
| 310 |
+
tridiag_banded = zeros((3,J))
|
| 311 |
+
# Lower diagonal (last element ignored)
|
| 312 |
+
tridiag_banded[2,:-1] = lower_diagonal
|
| 313 |
+
# Upper diagonal (first element ignored)
|
| 314 |
+
tridiag_banded[0,1:] = upper_diagonal
|
| 315 |
+
# Main diagonal, term 1, length J-1
|
| 316 |
+
tridiag_banded[1,:-1] += main_diagonal_term1
|
| 317 |
+
# Main diagonal, term 2, length J-1
|
| 318 |
+
tridiag_banded[1, 1:] += main_diagonal_term2
|
| 319 |
+
return tridiag_banded
|
| 320 |
+
else:
|
| 321 |
+
# If X.size is (...,J), then the tridiagonal operator is (...,J,J)
|
| 322 |
+
sizeJJ = tuple([n for n in X.shape[:-1]] + [J,J])
|
| 323 |
+
tridiag = zeros(sizeJJ)
|
| 324 |
+
# indices for main, upper, and lower diagonals of a JxJ matrix
|
| 325 |
+
inds_main = diag_indices(J)
|
| 326 |
+
inds_upper = (inds_main[0][:-1], inds_main[1][1:])
|
| 327 |
+
inds_lower = (inds_main[0][1:], inds_main[1][:-1])
|
| 328 |
+
# Lower diagonal (length J-1)
|
| 329 |
+
tridiag[...,inds_lower[0],inds_lower[1]] = lower_diagonal
|
| 330 |
+
# Upper diagonal (length J-1)
|
| 331 |
+
tridiag[...,inds_upper[0],inds_upper[1]] = upper_diagonal
|
| 332 |
+
# Main diagonal, term 1, length J-1
|
| 333 |
+
tridiag[...,inds_main[0][:-1],inds_main[1][:-1]] += main_diagonal_term1
|
| 334 |
+
# Main diagonal, term 2, length J-1
|
| 335 |
+
tridiag[...,inds_main[0][1:],inds_main[1][1:]] += main_diagonal_term2
|
| 336 |
+
return tridiag
|
| 337 |
+
|
| 338 |
+
def make_the_actual_tridiagonal_matrix(tridiag_banded):
|
| 339 |
+
'''Convert a (3xJ) banded array into full (JxJ) tridiagonal matrix form.'''
|
| 340 |
+
return (diag(tridiag_banded[1,:], k=0) +
|
| 341 |
+
diag(tridiag_banded[0,1:], k=1) +
|
| 342 |
+
diag(tridiag_banded[2,:-1], k=-1))
|
| 343 |
+
|
| 344 |
+
def compute_source(X, Xb, prescribed_flux=None, prescribed_source=None,
|
| 345 |
+
W=None, Wb=None):
|
| 346 |
+
'''Return the source array S consisting of the convergence of the prescribed flux
|
| 347 |
+
plus the prescribed scalar source.'''
|
| 348 |
+
if (W is None):
|
| 349 |
+
W = ones_like(X)
|
| 350 |
+
if (Wb is None):
|
| 351 |
+
Wb = ones_like(Xb)
|
| 352 |
+
if prescribed_flux is None:
|
| 353 |
+
prescribed_flux = zeros_like(Xb)
|
| 354 |
+
if prescribed_source is None:
|
| 355 |
+
prescribed_source = zeros_like(X)
|
| 356 |
+
F = prescribed_flux
|
| 357 |
+
return ((-Wb[...,1:]*F[...,1:]+Wb[...,:-1]*F[...,:-1]) /
|
| 358 |
+
(W*(Xb[...,1:]-Xb[...,:-1])) + prescribed_source)
|
| 359 |
+
|
| 360 |
+
def compute_tendency(field, tridiag, source, use_banded_solver=False):
|
| 361 |
+
r'''Return the instantaneous scalar tendency.
|
| 362 |
+
|
| 363 |
+
This is the sum of the convergence of advective+diffusive flux plus any
|
| 364 |
+
prescribed convergence or scalar sources.
|
| 365 |
+
|
| 366 |
+
The convergence is computed by matrix multiplication:
|
| 367 |
+
|
| 368 |
+
.. math::
|
| 369 |
+
|
| 370 |
+
\frac{\partial \psi}{\partial t} = T \times \psi + S
|
| 371 |
+
|
| 372 |
+
where :math:`T` is the tridiagonal flux convergence matrix.
|
| 373 |
+
'''
|
| 374 |
+
if use_banded_solver:
|
| 375 |
+
tridiag = make_the_actual_tridiagonal_matrix(tridiag)
|
| 376 |
+
# np.matmul expects the final 2 dims of each array to be matrices
|
| 377 |
+
# add a singleton dimension to field so we get (J,J)x(J,1)->(J,1)
|
| 378 |
+
result = matmul(tridiag, field[...,newaxis]) + source[...,newaxis]
|
| 379 |
+
# Now strip the extra dim
|
| 380 |
+
return result[...,0]
|
| 381 |
+
|
| 382 |
+
def implicit_step_forward(initial_field, tridiag, source, timestep,
|
| 383 |
+
use_banded_solver=False):
|
| 384 |
+
r'''Return the field at future time using an implicit timestep.
|
| 385 |
+
|
| 386 |
+
The matrix problem is
|
| 387 |
+
|
| 388 |
+
.. math::
|
| 389 |
+
|
| 390 |
+
(I - T \Delta t) \psi^{n+1} = \psi^n + S \Delta t
|
| 391 |
+
|
| 392 |
+
where :math:`T` is the tridiagonal matrix for the flux convergence, :math:`psi` is the
|
| 393 |
+
state variable, the superscript :math:`n` refers to the time index, and :math:`S \Delta t`
|
| 394 |
+
is the accumulated source over the timestep :math:`\Delta t`.
|
| 395 |
+
|
| 396 |
+
Input arguments:
|
| 397 |
+
|
| 398 |
+
- ``initial_field``: the current state variable :math:`\psi^n`, dimensions (...,J)
|
| 399 |
+
- ``tridiag``: the tridiagonal matrix :math:`T`, dimensions (...,J,J) or (...,3,J) depending on the value of ``use_banded_solver``
|
| 400 |
+
- ``source``: prescribed sources/sinks of :math:`\psi`, dimensions (...,J)
|
| 401 |
+
- ``timestep``: the discrete timestep in time units
|
| 402 |
+
- ``use_banded_solver``: switch to use the optional efficient banded solver (see below)
|
| 403 |
+
|
| 404 |
+
Returns the updated value of the state variable :math:`\psi^{n+1}`, dimensions (...,J)
|
| 405 |
+
|
| 406 |
+
The expected shape of ``tridiag`` depends on the switch ``use_banded_solver``,
|
| 407 |
+
which should be consistent with that used in the call to ``advdiff_tridiag()``.
|
| 408 |
+
If ``True``, we use the efficient banded matrix solver
|
| 409 |
+
``scipy.linalg.solve_banded()``.
|
| 410 |
+
However this will probably only work for a 1D state variable.
|
| 411 |
+
|
| 412 |
+
The default is to use the general linear system solver ``numpy.linalg.solve()``.
|
| 413 |
+
'''
|
| 414 |
+
RHS = initial_field + source*timestep
|
| 415 |
+
I = 0.*tridiag
|
| 416 |
+
J = initial_field.shape[-1]
|
| 417 |
+
if use_banded_solver:
|
| 418 |
+
I[1,:] = 1. # identity matrix in banded form
|
| 419 |
+
IminusTdt = I-tridiag*timestep
|
| 420 |
+
return solve_banded((1, 1), IminusTdt, RHS)
|
| 421 |
+
else:
|
| 422 |
+
# indices for main, upper, and lower diagonals of a JxJ matrix
|
| 423 |
+
inds_main = diag_indices(J)
|
| 424 |
+
I = 0.*tridiag
|
| 425 |
+
I[...,inds_main[0],inds_main[1]] = 1. # stacked identity matrix
|
| 426 |
+
IminusTdt = I-tridiag*timestep
|
| 427 |
+
# We add a dummy extra dimension here to accommodate a change in the broadcasting rules
|
| 428 |
+
# for numpy.linalg.solve in numpy >= 2
|
| 429 |
+
return solve(IminusTdt, RHS[..., None])[..., 0]
|
climlab/source/climlab/dynamics/advection_diffusion.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""CLIMLAB Process objects for advection-diffusion processes of the form
|
| 2 |
+
|
| 3 |
+
.. math::
|
| 4 |
+
|
| 5 |
+
\frac{\partial}{\partial t} \psi(x,t) &= -\frac{1}{w(x)} \frac{\partial}{\partial x} \left[ w(x) ~ \mathcal{F}(x,t) \right] \\
|
| 6 |
+
\mathcal{F} &= U(x) \psi(x) -K(x) ~ \frac{\partial \psi}{\partial x} + F(x)
|
| 7 |
+
|
| 8 |
+
for a state variable :math:`\psi(x,t)`, diffusivity :math:`K(x)`
|
| 9 |
+
in units of :math:`x^2 ~ t^{-1}`, advecting velocity :math:`U(x)`
|
| 10 |
+
in units of :math:`x ~ t^{-1}`, and a prescribed flux F(x)
|
| 11 |
+
(including boundary conditions) in units of :math:`\psi ~ x ~ t^{-1}`.
|
| 12 |
+
|
| 13 |
+
The prescribed flux :math:`F(x)` defaults to zero everywhere. The user can
|
| 14 |
+
implement a non-zero boundary flux condition by passing a non-zero array
|
| 15 |
+
``prescribed_flux`` as input.
|
| 16 |
+
|
| 17 |
+
:math:`w(x)` is an optional weighting function
|
| 18 |
+
for the divergence operator on curvilinear grids.
|
| 19 |
+
|
| 20 |
+
The diffusivity :math:`K` and velocity :math:`U` can be scalars,
|
| 21 |
+
or optionally vectors *specified at grid cell boundaries*
|
| 22 |
+
(so their lengths must be exactly 1 greater than the length of :math:`x`).
|
| 23 |
+
|
| 24 |
+
:math:`K` and :math:`U` can be modified by the user at any time
|
| 25 |
+
(e.g., after each timestep, if they depend on other state variables).
|
| 26 |
+
|
| 27 |
+
A fully implicit timestep is used for computational efficiency. Thus the computed
|
| 28 |
+
tendency :math:`\frac{\partial \psi}{\partial t}` will depend on the timestep.
|
| 29 |
+
|
| 30 |
+
In addition to the tendency over the implicit timestep,
|
| 31 |
+
the solver also calculates several diagnostics from the updated state:
|
| 32 |
+
|
| 33 |
+
- ``diffusive_flux`` given by :math:`-K(x) ~ \frac{\partial \psi}{\partial x}` in units of :math:`[\psi]~[x]`/s
|
| 34 |
+
- ``advective_flux`` given by :math:`U(x) \psi(x)` (same units)
|
| 35 |
+
- ``total_flux``, the sum of advective, diffusive and prescribed fluxes
|
| 36 |
+
- ``flux_convergence`` given by the right hand side of the first equation above, in units of :math:`[\psi]`/s
|
| 37 |
+
|
| 38 |
+
This base class can be used without modification for diffusion in
|
| 39 |
+
Cartesian coordinates (:math:`w=1`). Non-uniformly spaced grids are supported.
|
| 40 |
+
|
| 41 |
+
The state variable :math:`\psi` may be multi-dimensional, but the diffusion
|
| 42 |
+
will operate along a single dimension only.
|
| 43 |
+
|
| 44 |
+
Other classes implement the weighting for spherical geometry.
|
| 45 |
+
"""
|
| 46 |
+
import numpy as np
|
| 47 |
+
from climlab.process.implicit import ImplicitProcess
|
| 48 |
+
from climlab.process.process import get_axes
|
| 49 |
+
from climlab.domain.field import Field
|
| 50 |
+
from . import adv_diff_numerics
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class AdvectionDiffusion(ImplicitProcess):
|
| 54 |
+
"""A parent class for one dimensional implicit advection-diffusion modules.
|
| 55 |
+
|
| 56 |
+
**Initialization parameters** \n
|
| 57 |
+
|
| 58 |
+
:param float K: the diffusivity parameter in units of
|
| 59 |
+
:math:`\\frac{[\\textrm{length}]^2}{\\textrm{time}}`
|
| 60 |
+
where length is the unit of the spatial axis
|
| 61 |
+
on which the diffusion is occuring.
|
| 62 |
+
:param float U: Advection velocity in units of
|
| 63 |
+
:math:`\\frac{[\\textrm{length}]}{\\textrm{time}}`
|
| 64 |
+
:param str diffusion_axis: dictionary key for axis on which the
|
| 65 |
+
diffusion is occuring in process's domain
|
| 66 |
+
axes dictionary
|
| 67 |
+
:param bool use_banded_solver: input flag, whether to use
|
| 68 |
+
:py:func:`scipy.linalg.solve_banded`
|
| 69 |
+
instead of :py:func:`numpy.linalg.solve`
|
| 70 |
+
[default: False]
|
| 71 |
+
|
| 72 |
+
.. note::
|
| 73 |
+
|
| 74 |
+
The banded solver :py:func:`scipy.linalg.solve_banded` is faster than
|
| 75 |
+
:py:func:`numpy.linalg.solve` but only works for one dimensional diffusion.
|
| 76 |
+
|
| 77 |
+
**Object attributes** \n
|
| 78 |
+
|
| 79 |
+
Additional to the parent class
|
| 80 |
+
:class:`~climlab.process.implicit.ImplicitProcess`
|
| 81 |
+
following object attributes are generated or modified during initialization:
|
| 82 |
+
|
| 83 |
+
:ivar dict param: parameter dictionary is extended by
|
| 84 |
+
diffusivity parameter K (unit:
|
| 85 |
+
:math:`\\frac{[\\textrm{length}]^2}{\\textrm{time}}`)
|
| 86 |
+
:ivar bool use_banded_solver: input flag specifying numerical solving
|
| 87 |
+
method (given during initialization)
|
| 88 |
+
:ivar str diffusion_axis: dictionary key for axis where diffusion
|
| 89 |
+
is occuring:
|
| 90 |
+
specified during initialization
|
| 91 |
+
or output of method
|
| 92 |
+
:func:`_guess_diffusion_axis`
|
| 93 |
+
:ivar array _advdiffTriDiag: tridiagonal diffusion matrix made by
|
| 94 |
+
:func:`_make_diffusion_matrix()` with input
|
| 95 |
+
``self._K_dimensionless``
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
:Example:
|
| 99 |
+
|
| 100 |
+
Here is an example showing implementation of a vertical diffusion.
|
| 101 |
+
It shows that a subprocess can work on just a subset of the parent process
|
| 102 |
+
state variables.
|
| 103 |
+
|
| 104 |
+
.. plot:: code_input_manual/example_diffusion.py
|
| 105 |
+
:include-source:
|
| 106 |
+
|
| 107 |
+
"""
|
| 108 |
+
def __init__(self,
|
| 109 |
+
K=0.,
|
| 110 |
+
U=0.,
|
| 111 |
+
diffusion_axis=None,
|
| 112 |
+
use_banded_solver=False,
|
| 113 |
+
prescribed_flux=0.,
|
| 114 |
+
**kwargs):
|
| 115 |
+
super(AdvectionDiffusion, self).__init__(**kwargs)
|
| 116 |
+
self.use_banded_solver = use_banded_solver
|
| 117 |
+
if diffusion_axis is None: # diffusion axis is also advection axis!
|
| 118 |
+
self.diffusion_axis = _guess_diffusion_axis(self)
|
| 119 |
+
else:
|
| 120 |
+
self.diffusion_axis = diffusion_axis
|
| 121 |
+
for dom in list(self.domains.values()):
|
| 122 |
+
points = dom.axes[self.diffusion_axis].points
|
| 123 |
+
bounds = dom.axes[self.diffusion_axis].bounds
|
| 124 |
+
self.diffusion_axis_index = dom.axis_index[self.diffusion_axis]
|
| 125 |
+
# Cell bounds and centers in length units for diffusion operator
|
| 126 |
+
# Ensure they have shame dimensions as state var
|
| 127 |
+
for varname, value in self.state.items():
|
| 128 |
+
arr = np.moveaxis(0.*value, self.diffusion_axis_index, -1)
|
| 129 |
+
J = arr.shape[-1]
|
| 130 |
+
sizeJ = tuple([n for n in arr.shape[:-1]] + [J])
|
| 131 |
+
sizeJplus1 = tuple([n for n in arr.shape[:-1]] + [J+1])
|
| 132 |
+
arr[...,:] = points
|
| 133 |
+
self._Xcenter = arr
|
| 134 |
+
self._Xbounds = np.zeros(sizeJplus1)
|
| 135 |
+
self._Xbounds[...,:] = bounds
|
| 136 |
+
self._weight_bounds = np.ones_like(self._Xbounds) # weights for curvilinear grids
|
| 137 |
+
self._weight_center = np.ones_like(self._Xcenter)
|
| 138 |
+
self.prescribed_flux = prescribed_flux # flux including boundary conditions
|
| 139 |
+
self.K = K # Diffusivity in units of [length]**2 / [time]
|
| 140 |
+
self.U = U # Advecting velocity in units of [length] / [time]
|
| 141 |
+
diff = np.moveaxis(0.*self.K*self._weight_bounds,-1,self.diffusion_axis_index)
|
| 142 |
+
# Create a Field object defined at the cell interfaces along the diffusion axis
|
| 143 |
+
interfaces = np.tile(False, dom.numdims)
|
| 144 |
+
interfaces[self.diffusion_axis_index] = True
|
| 145 |
+
diffusive_flux = Field(diff, domain=dom, interfaces=interfaces)
|
| 146 |
+
self.add_diagnostic('diffusive_flux', diffusive_flux)
|
| 147 |
+
self.add_diagnostic('advective_flux', 0.*self.diffusive_flux)
|
| 148 |
+
self.add_diagnostic('total_flux', 0.*self.diffusive_flux)
|
| 149 |
+
for varname, value in self.state.items():
|
| 150 |
+
flux_convergence = Field(np.moveaxis(0.*self._weight_center,-1,self.diffusion_axis_index), domain=dom)
|
| 151 |
+
self.add_diagnostic('flux_convergence', flux_convergence)
|
| 152 |
+
|
| 153 |
+
@property
|
| 154 |
+
def K(self):
|
| 155 |
+
return self._K
|
| 156 |
+
@K.setter # currently this assumes that Kvalue is scalar or has the right dimensions...
|
| 157 |
+
def K(self, Kvalue):
|
| 158 |
+
self._K = Kvalue
|
| 159 |
+
self._compute_advdiff_matrix()
|
| 160 |
+
|
| 161 |
+
@property
|
| 162 |
+
def U(self):
|
| 163 |
+
return self._U
|
| 164 |
+
@U.setter
|
| 165 |
+
def U(self, Uvalue):
|
| 166 |
+
self._U = Uvalue
|
| 167 |
+
self._compute_advdiff_matrix()
|
| 168 |
+
|
| 169 |
+
@property
|
| 170 |
+
def prescribed_flux(self):
|
| 171 |
+
return self._prescribed_flux
|
| 172 |
+
@prescribed_flux.setter
|
| 173 |
+
def prescribed_flux(self, fluxvalue):
|
| 174 |
+
self._prescribed_flux = fluxvalue
|
| 175 |
+
for varname, value in self.state.items():
|
| 176 |
+
field = np.moveaxis(value, self.diffusion_axis_index,-1)
|
| 177 |
+
fluxarray = np.ones_like(self._Xbounds) * self._prescribed_flux
|
| 178 |
+
self._source = adv_diff_numerics.compute_source(X=self._Xcenter,
|
| 179 |
+
Xb=self._Xbounds, prescribed_flux=fluxarray,
|
| 180 |
+
prescribed_source=0.*field,
|
| 181 |
+
W=self._weight_center, Wb=self._weight_bounds)
|
| 182 |
+
|
| 183 |
+
def _compute_advdiff_matrix(self):
|
| 184 |
+
Karray = np.ones_like(self._Xbounds) * self.K
|
| 185 |
+
try:
|
| 186 |
+
Uarray = np.ones_like(self._Xbounds) * self.U
|
| 187 |
+
except Exception:
|
| 188 |
+
Uarray = 0.*Karray
|
| 189 |
+
self._advdiffTriDiag = adv_diff_numerics.advdiff_tridiag(X=self._Xcenter,
|
| 190 |
+
Xb=self._Xbounds, K=Karray, U=Uarray, W=self._weight_center, Wb=self._weight_bounds,
|
| 191 |
+
use_banded_solver=self.use_banded_solver)
|
| 192 |
+
|
| 193 |
+
def _implicit_solver(self):
|
| 194 |
+
newstate = {}
|
| 195 |
+
for varname, value in self.state.items():
|
| 196 |
+
field = np.moveaxis(value, self.diffusion_axis_index,-1)
|
| 197 |
+
result = adv_diff_numerics.implicit_step_forward(field,
|
| 198 |
+
self._advdiffTriDiag, self._source, self.timestep_in_seconds,
|
| 199 |
+
use_banded_solver=self.use_banded_solver)
|
| 200 |
+
newstate[varname] = np.moveaxis(result,-1,self.diffusion_axis_index)
|
| 201 |
+
return newstate
|
| 202 |
+
|
| 203 |
+
def _update_diagnostics(self, newstate):
|
| 204 |
+
Karray = np.ones_like(self._Xbounds) * self.K
|
| 205 |
+
Uarray = np.ones_like(self._Xbounds) * self.U
|
| 206 |
+
for varname, value in newstate.items():
|
| 207 |
+
field = np.moveaxis(value, self.diffusion_axis_index,-1)
|
| 208 |
+
diff_flux = adv_diff_numerics.diffusive_flux(self._Xcenter,
|
| 209 |
+
self._Xbounds, Karray, field)
|
| 210 |
+
adv_flux = adv_diff_numerics.advective_flux(self._Xcenter,
|
| 211 |
+
self._Xbounds, Uarray, field)
|
| 212 |
+
self.diffusive_flux[:] = np.moveaxis(diff_flux,-1,self.diffusion_axis_index)
|
| 213 |
+
self.advective_flux[:] = np.moveaxis(adv_flux,-1,self.diffusion_axis_index)
|
| 214 |
+
source = 0.*field
|
| 215 |
+
convergence = adv_diff_numerics.compute_tendency(field,
|
| 216 |
+
self._advdiffTriDiag, source, use_banded_solver=self.use_banded_solver)
|
| 217 |
+
self.flux_convergence[:] = np.moveaxis(convergence,-1,self.diffusion_axis_index)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class Diffusion(AdvectionDiffusion):
|
| 221 |
+
'''1D diffusion only, with advection set to zero.
|
| 222 |
+
|
| 223 |
+
Otherwise identical to the parent class AdvectionDiffusion.
|
| 224 |
+
'''
|
| 225 |
+
def __init__(self,
|
| 226 |
+
K=None,
|
| 227 |
+
diffusion_axis=None,
|
| 228 |
+
use_banded_solver=False,
|
| 229 |
+
**kwargs):
|
| 230 |
+
super(Diffusion, self).__init__(K=K, U=0.,
|
| 231 |
+
diffusion_axis=diffusion_axis,
|
| 232 |
+
use_banded_solver=use_banded_solver, **kwargs)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def _guess_diffusion_axis(process_or_domain):
|
| 236 |
+
"""Scans given process, domain or dictionary of domains for a diffusion axis
|
| 237 |
+
and returns appropriate name.
|
| 238 |
+
|
| 239 |
+
In case only one axis with length > 1 in the process or set of domains
|
| 240 |
+
exists, the name of that axis is returned. Otherwise an error is raised.
|
| 241 |
+
|
| 242 |
+
:param process_or_domain: input from where diffusion axis should be guessed
|
| 243 |
+
:type process_or_domain: :class:`~climlab.process.process.Process`,
|
| 244 |
+
:class:`~climlab.domain.domain._Domain` or
|
| 245 |
+
:py:class:`dict` of domains
|
| 246 |
+
:raises: :exc:`ValueError` if more than one diffusion axis is possible.
|
| 247 |
+
:returns: name of the diffusion axis
|
| 248 |
+
:rtype: str
|
| 249 |
+
|
| 250 |
+
"""
|
| 251 |
+
axes = get_axes(process_or_domain)
|
| 252 |
+
diff_ax = {}
|
| 253 |
+
for axname, ax in axes.items():
|
| 254 |
+
if ax.num_points > 1:
|
| 255 |
+
diff_ax.update({axname: ax})
|
| 256 |
+
if len(list(diff_ax.keys())) == 1:
|
| 257 |
+
return list(diff_ax.keys())[0]
|
| 258 |
+
else:
|
| 259 |
+
raise ValueError('More than one possible diffusion axis.')
|
climlab/source/climlab/dynamics/budyko_transport.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from climlab.process.energy_budget import EnergyBudget
|
| 2 |
+
from climlab.domain.field import global_mean
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class BudykoTransport(EnergyBudget):
|
| 6 |
+
r"""calculates the 1 dimensional heat transport as the difference
|
| 7 |
+
between the local temperature and the global mean temperature.
|
| 8 |
+
|
| 9 |
+
:param float b: budyko transport parameter \n
|
| 10 |
+
- unit: :math:`\\textrm{W} / \\left( \\textrm{m}^2 \\ ^{\circ} \\textrm{C} \\right)` \n
|
| 11 |
+
- default value: ``3.81``
|
| 12 |
+
|
| 13 |
+
As BudykoTransport is a :class:`~climlab.process.process.Process` it needs
|
| 14 |
+
a state do be defined on. See example for details.
|
| 15 |
+
|
| 16 |
+
**Computation Details:** \n
|
| 17 |
+
|
| 18 |
+
In a global Energy Balance Model
|
| 19 |
+
|
| 20 |
+
.. math::
|
| 21 |
+
|
| 22 |
+
C \\frac{dT}{dt} = R\downarrow - R\uparrow - H
|
| 23 |
+
|
| 24 |
+
with model state :math:`T`, the energy transport term :math:`H`
|
| 25 |
+
can be described as
|
| 26 |
+
|
| 27 |
+
.. math::
|
| 28 |
+
|
| 29 |
+
H = b [T - \\bar{T}]
|
| 30 |
+
|
| 31 |
+
where :math:`T` is a vector of the model temperature and :math:`\\bar{T}`
|
| 32 |
+
describes the mean value of :math:`T`.
|
| 33 |
+
|
| 34 |
+
For further information see :cite:`Budyko_1969`.
|
| 35 |
+
|
| 36 |
+
:Example:
|
| 37 |
+
|
| 38 |
+
Budyko Transport as a standalone process:
|
| 39 |
+
|
| 40 |
+
.. plot:: code_input_manual/example_budyko_transport.py
|
| 41 |
+
:include-source:
|
| 42 |
+
|
| 43 |
+
"""
|
| 44 |
+
# implemented by m-kreuzer
|
| 45 |
+
def __init__(self, b=3.81, **kwargs):
|
| 46 |
+
super(BudykoTransport, self).__init__(**kwargs)
|
| 47 |
+
self.b = b
|
| 48 |
+
|
| 49 |
+
@property
|
| 50 |
+
def b(self):
|
| 51 |
+
r"""the budyko transport parameter in unit
|
| 52 |
+
:math:`\\frac{\\textrm{W}}{\\textrm{m}^2 \\textrm{K}}`
|
| 53 |
+
|
| 54 |
+
:getter: returns the budyko transport parameter
|
| 55 |
+
:setter: sets the budyko transport parameter
|
| 56 |
+
:type: float
|
| 57 |
+
|
| 58 |
+
"""
|
| 59 |
+
return self._b
|
| 60 |
+
@b.setter
|
| 61 |
+
def b(self, value):
|
| 62 |
+
self._b = value
|
| 63 |
+
self.param['b'] = value
|
| 64 |
+
|
| 65 |
+
def _compute_heating_rates(self):
|
| 66 |
+
"""Computes energy flux convergences to get heating rates in :math:`W/m^2`.
|
| 67 |
+
|
| 68 |
+
"""
|
| 69 |
+
for varname, value in self.state.items():
|
| 70 |
+
self.heating_rate[varname] = - self.b * (value - global_mean(value))
|
climlab/source/climlab/dynamics/large_scale_condensation.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""
|
| 2 |
+
climlab process for large-scale condensation
|
| 3 |
+
|
| 4 |
+
The process object ``climlab.dynamics.LargeScaleCondensation`` does the following at each timestep:
|
| 5 |
+
|
| 6 |
+
- Calculate saturation specific humidity given air temperatures at every grid point
|
| 7 |
+
- Calculate supersaturation by comparing actual specific humidity to saturation specific humidity
|
| 8 |
+
- Compute a specific humidity tendency based on a relaxation toward saturation (if supersaturated)
|
| 9 |
+
- Compute a heating rate and temperature tendency due to the latent heating of condensation
|
| 10 |
+
- Compute precipitation rate at the surface, assuming all condensate in each column is instantly precipitated
|
| 11 |
+
|
| 12 |
+
State variables:
|
| 13 |
+
|
| 14 |
+
- ``Tatm``: air temperature in K
|
| 15 |
+
- ``q``: specific humidity in kg kg\ :sup:`-1`
|
| 16 |
+
|
| 17 |
+
Input parameters and default values:
|
| 18 |
+
|
| 19 |
+
- ``condensation_time``: condensation time constant in units of seconds (default: 4 hours)
|
| 20 |
+
- ``RH_ref``: reference relative humidity value, dimensionless (default value 0.9)
|
| 21 |
+
|
| 22 |
+
Diagnostics:
|
| 23 |
+
|
| 24 |
+
- ``latent_heating``: latent heating rate (every grid cell) in units of W m\ :sup:`-2`
|
| 25 |
+
- ``precipitation``: precipitation rate (column total) in units of kg m\ :sup:`-2` s\ :sup:`-1` or mm s\ :sup:`-1`
|
| 26 |
+
|
| 27 |
+
The condensation rule follows the SPEEDY model (Molteni 2003 doi:10.1007/s00382-002-0268-2).
|
| 28 |
+
Condensation is modeled as a relaxation of relative humidity toward a
|
| 29 |
+
specified profile wherever the tropospheric relative humidity exceeds the target.
|
| 30 |
+
|
| 31 |
+
Given specific humidity :math:`q` and saturation specific humidity :math:`q_{sat}(T,p)`,
|
| 32 |
+
relative humidity is calculated from
|
| 33 |
+
|
| 34 |
+
.. math::
|
| 35 |
+
|
| 36 |
+
r = \frac{q}{q_{sat}}
|
| 37 |
+
|
| 38 |
+
which is compared against a specified reference profile :math:`r_{lsc}` which may vary spatially.
|
| 39 |
+
|
| 40 |
+
At grid cells where :math:`r > r_{lsc}`, the specific humidity tendency is calculated from
|
| 41 |
+
|
| 42 |
+
.. math::
|
| 43 |
+
|
| 44 |
+
\left(\frac{\partial q}{\partial t}\right)_{lsc} = -\frac{(q - r_{lsc} q_{sat})}{\tau_{lsc}}
|
| 45 |
+
|
| 46 |
+
and is zero otherwise.
|
| 47 |
+
|
| 48 |
+
The two parameters of the scheme are the relaxation time constant :math:`\tau_{lsc}` and the reference RH profile :math:`r_{lsc}`.
|
| 49 |
+
|
| 50 |
+
We follow SPEEDY and set an "aggressive" default time constant :math:`\tau_{lsc} = 4` hours.
|
| 51 |
+
|
| 52 |
+
For the reference profile, SPEEDY sets a smoothly decreasing vertical profile with :math:`r_{lsc} = 0.9` at the surface
|
| 53 |
+
and :math:`r_{lsc} \approx 0.8` at the tropopause.
|
| 54 |
+
For simplicity, we will default to a uniform default value of :math:`r_{lsc} = 0.9`.
|
| 55 |
+
|
| 56 |
+
The temperature tendency due to latent heating (in units of K s\ :sup:`-1`) is calculated from
|
| 57 |
+
|
| 58 |
+
.. math::
|
| 59 |
+
|
| 60 |
+
\left(\frac{\partial T}{\partial t}\right)_{lsc} = -\frac{L}{c_p} \left(\frac{\partial q}{\partial t}\right)_{lsc}
|
| 61 |
+
|
| 62 |
+
with the associated heating rate diagnostic (in units of W m\ :sup:`-2`) computed from
|
| 63 |
+
|
| 64 |
+
.. math::
|
| 65 |
+
|
| 66 |
+
h_{lsc} = C \left(\frac{\partial T}{\partial t}\right)_{lsc}
|
| 67 |
+
|
| 68 |
+
where :math:`C = \frac{c_p dp}{g}` is the heat capacity per unit area in J K\ :sup:`-1` m\ :sup:`-2`,
|
| 69 |
+
and the precipitation rate is calculated from the vertical integral:
|
| 70 |
+
|
| 71 |
+
.. math::
|
| 72 |
+
|
| 73 |
+
P = -\frac{1}{g} \int_0^{p_0} \left(\frac{\partial q}{\partial t}\right)_{lsc} dp
|
| 74 |
+
|
| 75 |
+
or equivalently
|
| 76 |
+
|
| 77 |
+
.. math::
|
| 78 |
+
|
| 79 |
+
P = + \int_0^{p_0} \frac{h_{lsc}}{L}
|
| 80 |
+
|
| 81 |
+
where the integral implies a sum over all grid cells in each atmospheric column.
|
| 82 |
+
"""
|
| 83 |
+
import numpy as np
|
| 84 |
+
from climlab.process import TimeDependentProcess
|
| 85 |
+
from climlab.utils import constants as const
|
| 86 |
+
from climlab.utils.thermo import qsat
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class LargeScaleCondensation(TimeDependentProcess):
|
| 90 |
+
'''Climlab process class for LargeScaleCondensation.
|
| 91 |
+
Condensation is modeled as a relaxation of relative humidity toward a
|
| 92 |
+
specified reference value wherever the tropospheric relative humidity
|
| 93 |
+
exceeds the target.
|
| 94 |
+
|
| 95 |
+
State variables:
|
| 96 |
+
|
| 97 |
+
- ``Tatm``: air temperature in K
|
| 98 |
+
- ``q``: specific humidity in kg kg\ :sup:`-1`
|
| 99 |
+
|
| 100 |
+
Input parameters and default values:
|
| 101 |
+
|
| 102 |
+
- ``condensation_time``: condensation time constant in units of seconds (default: 4 hours)
|
| 103 |
+
- ``RH_ref``: reference relative humidity value, dimensionless (default value 0.9)
|
| 104 |
+
|
| 105 |
+
Diagnostics:
|
| 106 |
+
|
| 107 |
+
- ``latent_heating``: latent heating rate (every grid cell) in units of W m\ :sup:`-2`
|
| 108 |
+
- ``precipitation``: precipitation rate (column total) in units of kg m\ :sup:`-2` s\ :sup:`-1` or mm s\ :sup:`-1`
|
| 109 |
+
'''
|
| 110 |
+
def __init__(self,
|
| 111 |
+
condensation_time = 4. * const.seconds_per_hour,
|
| 112 |
+
RH_ref = 0.9,
|
| 113 |
+
**kwargs):
|
| 114 |
+
super(LargeScaleCondensation, self).__init__(**kwargs)
|
| 115 |
+
self.condensation_time = condensation_time
|
| 116 |
+
self.RH_ref = RH_ref
|
| 117 |
+
self.add_diagnostic('latent_heating', 0.*self.Tatm)
|
| 118 |
+
self.add_diagnostic('precipitation', 0.*self.Ts)
|
| 119 |
+
|
| 120 |
+
def _compute(self):
|
| 121 |
+
qsaturation = qsat(self.Tatm, self.lev)
|
| 122 |
+
qtendency = -(self.q - self.RH_ref*qsaturation) / self.condensation_time
|
| 123 |
+
|
| 124 |
+
tendencies = {}
|
| 125 |
+
tendencies['q'] = np.minimum(qtendency, 0.)
|
| 126 |
+
tendencies['Tatm'] = -const.Lhvap/const.cp * tendencies['q']
|
| 127 |
+
self.latent_heating[:] = tendencies['Tatm'] * self.Tatm.domain.heat_capacity
|
| 128 |
+
self.precipitation[:,0] = np.sum(self.latent_heating, axis=-1)/const.Lhvap
|
| 129 |
+
return tendencies
|
climlab/source/climlab/dynamics/meridional_advection_diffusion.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""General solver of the 1D meridional advection-diffusion equation on the sphere:
|
| 2 |
+
|
| 3 |
+
.. math::
|
| 4 |
+
|
| 5 |
+
\frac{\partial}{\partial t} \psi(\phi,t) &= -\frac{1}{a \cos\phi} \frac{\partial}{\partial \phi} \left[ \cos\phi ~ F(\phi,t) \right] \\
|
| 6 |
+
F &= U(\phi) \psi(\phi) -\frac{K(\phi)}{a} ~ \frac{\partial \psi}{\partial \phi}
|
| 7 |
+
|
| 8 |
+
for a state variable :math:`\psi(\phi,t)`, arbitrary diffusivity :math:`K(\phi)`
|
| 9 |
+
in units of :math:`x^2 ~ t^{-1}`, and advecting velocity :math:`U(\phi)`.
|
| 10 |
+
:math:`\phi` is latitude and :math:`a` is the Earth's radius (in meters).
|
| 11 |
+
|
| 12 |
+
:math:`K` and :math:`U` can be scalars,
|
| 13 |
+
or optionally vector *specified at grid cell boundaries*
|
| 14 |
+
(so their lengths must be exactly 1 greater than the length of :math:`\phi`).
|
| 15 |
+
|
| 16 |
+
:math:`K` and :math:`U` can be modified by the user at any time
|
| 17 |
+
(e.g., after each timestep, if they depend on other state variables).
|
| 18 |
+
|
| 19 |
+
A fully implicit timestep is used for computational efficiency. Thus the computed
|
| 20 |
+
tendency :math:`\frac{\partial \psi}{\partial t}` will depend on the timestep.
|
| 21 |
+
|
| 22 |
+
In addition to the tendency over the implicit timestep,
|
| 23 |
+
the solver also calculates several diagnostics from the updated state:
|
| 24 |
+
|
| 25 |
+
- ``diffusive_flux`` given by :math:`-\frac{K(\phi)}{a} ~ \frac{\partial \psi}{\partial \phi}` in units of :math:`[\psi]~[x]`/s
|
| 26 |
+
- ``advective_flux`` given by :math:`U(\phi) \psi(\phi)` (same units)
|
| 27 |
+
- ``total_flux``, the sum of advective, diffusive and prescribed fluxes
|
| 28 |
+
- ``flux_convergence`` (or instantanous scalar tendency) given by the right hand side of the first equation above, in units of :math:`[\psi]`/s
|
| 29 |
+
|
| 30 |
+
Non-uniform grid spacing is supported.
|
| 31 |
+
|
| 32 |
+
The state variable :math:`\psi` may be multi-dimensional, but the diffusion
|
| 33 |
+
will operate along the latitude dimension only.
|
| 34 |
+
"""
|
| 35 |
+
import numpy as np
|
| 36 |
+
from .advection_diffusion import AdvectionDiffusion, Diffusion
|
| 37 |
+
from climlab import constants as const
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class MeridionalAdvectionDiffusion(AdvectionDiffusion):
|
| 41 |
+
"""A parent class for meridional advection-diffusion processes.
|
| 42 |
+
"""
|
| 43 |
+
def __init__(self,
|
| 44 |
+
K=0.,
|
| 45 |
+
U=0.,
|
| 46 |
+
use_banded_solver=False,
|
| 47 |
+
prescribed_flux=0.,
|
| 48 |
+
**kwargs):
|
| 49 |
+
super(MeridionalAdvectionDiffusion, self).__init__(K=K, U=U,
|
| 50 |
+
diffusion_axis='lat', use_banded_solver=use_banded_solver, **kwargs)
|
| 51 |
+
# Conversion of delta from degrees (grid units) to physical length units
|
| 52 |
+
phi_stag = np.deg2rad(self.lat_bounds)
|
| 53 |
+
phi = np.deg2rad(self.lat)
|
| 54 |
+
self._Xcenter[...,:] = phi*const.a
|
| 55 |
+
self._Xbounds[...,:] = phi_stag*const.a
|
| 56 |
+
self._weight_bounds[...,:] = np.cos(phi_stag)
|
| 57 |
+
self._weight_center[...,:] = np.cos(phi)
|
| 58 |
+
# Now properly compute the weighted advection-diffusion matrix
|
| 59 |
+
self.prescribed_flux = prescribed_flux
|
| 60 |
+
self.K = K
|
| 61 |
+
self.U = U
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class MeridionalDiffusion(MeridionalAdvectionDiffusion):
|
| 65 |
+
"""A parent class for meridional diffusion-only processes,
|
| 66 |
+
with advection set to zero.
|
| 67 |
+
|
| 68 |
+
Otherwise identical to the parent class.
|
| 69 |
+
"""
|
| 70 |
+
def __init__(self,
|
| 71 |
+
K=0.,
|
| 72 |
+
use_banded_solver=False,
|
| 73 |
+
prescribed_flux=0.,
|
| 74 |
+
**kwargs):
|
| 75 |
+
# Just initialize the AdvectionDiffusion class with U=0
|
| 76 |
+
super(MeridionalDiffusion, self).__init__(
|
| 77 |
+
U=0.,
|
| 78 |
+
K=K,
|
| 79 |
+
prescribed_flux=prescribed_flux,
|
| 80 |
+
use_banded_solver=use_banded_solver, **kwargs)
|
climlab/source/climlab/dynamics/meridional_heat_diffusion.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""Solver for the 1D meridional heat diffusion equation on the sphere:
|
| 2 |
+
|
| 3 |
+
.. math::
|
| 4 |
+
|
| 5 |
+
C\frac{\partial}{\partial t} T(\phi,t) = \frac{1}{\cos\phi} \frac{\partial}{\partial \phi} \left[ \cos\phi ~ D ~ \frac{\partial T}{\partial \phi} \right]
|
| 6 |
+
|
| 7 |
+
for a temperature state variable :math:`T(\phi,t)`,
|
| 8 |
+
a vertically-integrated heat capacity :math:`C`,
|
| 9 |
+
and arbitrary thermal diffusivity :math:`D(\phi,t)`
|
| 10 |
+
in units of W/m2/K.
|
| 11 |
+
|
| 12 |
+
The diffusivity :math:`D` can be a single scalar,
|
| 13 |
+
or optionally a vector *specified at grid cell boundaries*
|
| 14 |
+
(so its length must be exactly 1 greater than the length of :math:`\phi`).
|
| 15 |
+
|
| 16 |
+
:math:`D` can be modified by the user at any time
|
| 17 |
+
(e.g., after each timestep, if it depends on other state variables).
|
| 18 |
+
|
| 19 |
+
The heat capacity :math:`C` is normally handled automatically by CLIMLAB
|
| 20 |
+
as part of the grid specification.
|
| 21 |
+
|
| 22 |
+
A fully implicit timestep is used for computational efficiency. Thus the computed
|
| 23 |
+
tendency :math:`\frac{\partial T}{\partial t}` will depend on the timestep.
|
| 24 |
+
|
| 25 |
+
The diagnostics ``diffusive_flux`` and ``flux_convergence`` are computed
|
| 26 |
+
as described in the parent class ``MeridionalDiffusion``.
|
| 27 |
+
Two additional diagnostics are computed here,
|
| 28 |
+
which are meaningful if :math:`T` represents a *zonally averaged temperature*:
|
| 29 |
+
|
| 30 |
+
- ``heat_transport`` given by :math:`\mathcal{H}(\phi) = -2 \pi ~ a^2 ~ \cos\phi ~ D ~ \frac{\partial T}{\partial \phi}` in units of PW (petawatts).
|
| 31 |
+
- ``heat_transport_convergence`` given by :math:`-\frac{1}{2 \pi ~a^2 \cos\phi} \frac{\partial \mathcal{H}}{\partial \phi}` in units of W/m2
|
| 32 |
+
|
| 33 |
+
Non-uniform grid spacing is supported.
|
| 34 |
+
|
| 35 |
+
The state variable :math:`T` may be multi-dimensional, but the diffusion
|
| 36 |
+
will operate along the latitude dimension only.
|
| 37 |
+
"""
|
| 38 |
+
import numpy as np
|
| 39 |
+
from .meridional_advection_diffusion import MeridionalDiffusion
|
| 40 |
+
from climlab import constants as const
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class MeridionalHeatDiffusion(MeridionalDiffusion):
|
| 44 |
+
'''A 1D diffusion solver for Energy Balance Models.
|
| 45 |
+
|
| 46 |
+
Solves the meridional heat diffusion equation
|
| 47 |
+
|
| 48 |
+
.. math::
|
| 49 |
+
|
| 50 |
+
C \frac{\partial T}{\partial t} = -\frac{1}{\cos\phi} \frac{\partial}{\partial \phi} \left[ -D \cos\phi \frac{\partial T}{\partial \phi} \right]
|
| 51 |
+
|
| 52 |
+
on an evenly-spaced latitude grid, with a state variable :math:`T`,
|
| 53 |
+
a heat capacity :math:`C` and diffusivity :math:`D`.
|
| 54 |
+
|
| 55 |
+
Assuming :math:`T` is a temperature in K or degC, then the units are:
|
| 56 |
+
|
| 57 |
+
- :math:`D` in W m-2 K-1
|
| 58 |
+
- :math:`C` in J m-2 K-1
|
| 59 |
+
|
| 60 |
+
:math:`D` is provided as input, and can be either scalar
|
| 61 |
+
or vector defined at latitude boundaries.
|
| 62 |
+
|
| 63 |
+
:math:`C` is normally handled automatically for temperature state variables in CLIMLAB.
|
| 64 |
+
'''
|
| 65 |
+
def __init__(self,
|
| 66 |
+
D=0.555, # in W / m^2 / degC
|
| 67 |
+
use_banded_solver=False,
|
| 68 |
+
**kwargs):
|
| 69 |
+
# First just use a dummy value for K
|
| 70 |
+
super(MeridionalHeatDiffusion, self).__init__(K=1.,
|
| 71 |
+
use_banded_solver=use_banded_solver, **kwargs)
|
| 72 |
+
# Now initialize properly
|
| 73 |
+
self.D = D
|
| 74 |
+
self.add_diagnostic('heat_transport', 0.*self.diffusive_flux)
|
| 75 |
+
self.add_diagnostic('heat_transport_convergence', 0.*self.flux_convergence)
|
| 76 |
+
|
| 77 |
+
@property
|
| 78 |
+
def D(self):
|
| 79 |
+
return self._D
|
| 80 |
+
@D.setter
|
| 81 |
+
def D(self, Dvalue):
|
| 82 |
+
self._D = Dvalue
|
| 83 |
+
self._update_diffusivity()
|
| 84 |
+
|
| 85 |
+
def _update_diffusivity(self):
|
| 86 |
+
for varname, value in self.state.items():
|
| 87 |
+
heat_capacity = value.domain.heat_capacity
|
| 88 |
+
# diffusivity in units of m**2/s
|
| 89 |
+
self.K = self.D / heat_capacity * const.a**2
|
| 90 |
+
|
| 91 |
+
def _update_diagnostics(self, newstate):
|
| 92 |
+
super(MeridionalHeatDiffusion, self)._update_diagnostics(newstate)
|
| 93 |
+
for varname, value in self.state.items():
|
| 94 |
+
heat_capacity = value.domain.heat_capacity
|
| 95 |
+
coslat_bounds = np.moveaxis(self._weight_bounds,-1,self.diffusion_axis_index)
|
| 96 |
+
self.heat_transport[:] = (self.diffusive_flux * heat_capacity *
|
| 97 |
+
2 * np.pi * const.a * coslat_bounds * 1E-15) # in PW
|
| 98 |
+
self.heat_transport_convergence[:] = (self.flux_convergence *
|
| 99 |
+
heat_capacity) # in W/m**2
|
climlab/source/climlab/dynamics/meridional_moist_diffusion.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""Solver for the 1D meridional moist static energy diffusion equation on the sphere:
|
| 2 |
+
|
| 3 |
+
.. math::
|
| 4 |
+
|
| 5 |
+
C\frac{\partial}{\partial t} T(\phi,t) = \frac{1}{\cos\phi} \frac{\partial}{\partial \phi} \left[ \cos\phi ~ D ~(1+f(T))~ \frac{\partial T}{\partial \phi} \right]
|
| 6 |
+
|
| 7 |
+
where :math:`f(T)` is a temperature-dependent moisture amplification factor given by
|
| 8 |
+
|
| 9 |
+
.. math::
|
| 10 |
+
|
| 11 |
+
f(T) = \frac{L^2 r q^*(T)}{c_p R_v T^2}
|
| 12 |
+
|
| 13 |
+
which expresses the effect of latent heat on the near-surface moist static energy,
|
| 14 |
+
where :math:`q^*(T)` is the saturation specific humidity at temperature :math:`T`
|
| 15 |
+
and :math:`r` is a relative humidity.
|
| 16 |
+
|
| 17 |
+
This class operates identically to ``MeridionalHeatDiffusion``
|
| 18 |
+
but calculates :math:`f`
|
| 19 |
+
automatically at each timestep and applies it to the diffusivity.
|
| 20 |
+
|
| 21 |
+
The magnitude of the moisture amplification is controlled by the input parameter
|
| 22 |
+
`relative_humidity` (i.e. :math:`r` in the equation above).
|
| 23 |
+
|
| 24 |
+
It can be used to implement a modified Energy Balance Model accounting for the
|
| 25 |
+
effects of moisture on the heat transport efficiency.
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
Derivation of the moist diffusion equation
|
| 29 |
+
------------------------------------------
|
| 30 |
+
|
| 31 |
+
Assume that heat transport is down the gradient of **moist static energy**
|
| 32 |
+
:math:`m = c_p T + L q + g Z`
|
| 33 |
+
|
| 34 |
+
For an EBM we want to parameterize everything in terms of a surface temperature :math:`T_s`.
|
| 35 |
+
So we write :math:`m_s = c_p T_s + L r q^*(T_s)`,
|
| 36 |
+
where :math:`m_s` is the moist static energy of near-surface air parcels,
|
| 37 |
+
:math:`r` is a near-surface relative humidity,
|
| 38 |
+
and :math:`q^*` is the **saturation specific humidity** at a reference surface pressure.
|
| 39 |
+
|
| 40 |
+
Now express this quantity in temperature units by defining a *moist temperature*
|
| 41 |
+
|
| 42 |
+
.. math::
|
| 43 |
+
|
| 44 |
+
T_m = \frac{m_s}{c_p} = T_s + \frac{L r}{c_p} q^*(T_s)
|
| 45 |
+
|
| 46 |
+
:math:`T_m` is the temperature a dry air parcel would have
|
| 47 |
+
that has the same total enthalpy as a moist air parcel at temperature :math:`T_s`
|
| 48 |
+
|
| 49 |
+
The down-gradient heat transport parameterization can then be written
|
| 50 |
+
|
| 51 |
+
.. math::
|
| 52 |
+
\mathcal{H} = -2 \pi a^2 D_m \frac{\partial T_m}{\partial \phi}
|
| 53 |
+
|
| 54 |
+
where :math:`D_m` is the thermal diffusion coefficient for this moist model, in units of W/m2/K.
|
| 55 |
+
|
| 56 |
+
The equation we are trying to solve is thus
|
| 57 |
+
|
| 58 |
+
.. math::
|
| 59 |
+
|
| 60 |
+
C \frac{\partial T_s}{\partial t} = \frac{1}{\cos\phi} \frac{\partial}{\partial \phi} \left( \cos\phi D_m \frac{\partial T_m}{\partial \phi} \right)
|
| 61 |
+
|
| 62 |
+
which we can write in terms of :math:`T_s` only by substituting in for :math:`T_m`:
|
| 63 |
+
|
| 64 |
+
.. math::
|
| 65 |
+
|
| 66 |
+
C \frac{\partial T_s}{\partial t} = \frac{1}{\cos\phi} \frac{\partial}{\partial \phi} \left( \cos\phi D_m \left(\frac{\partial T_s}{\partial \phi} + \frac{\partial}{\partial \phi} \left(\frac{L r}{c_p} q^*(T_s)\right)\right)\right)
|
| 67 |
+
|
| 68 |
+
If we make the simplifying assumption that the **relative humidity :math:`r` is constant**
|
| 69 |
+
(not a function of latitude), then
|
| 70 |
+
|
| 71 |
+
.. math::
|
| 72 |
+
|
| 73 |
+
C \frac{\partial T_s}{\partial t} = \frac{1}{\cos\phi} \frac{\partial}{\partial \phi} \left( \cos\phi D_m \left(\frac{\partial T_s}{\partial \phi} + \frac{L r}{c_p} \frac{\partial q^*}{\partial \phi} \right)\right)
|
| 74 |
+
|
| 75 |
+
To a good approximation (see Hartmann's book and others),
|
| 76 |
+
the Clausius-Clapeyron relation for saturation specific humidity gives
|
| 77 |
+
|
| 78 |
+
.. math::
|
| 79 |
+
|
| 80 |
+
\frac{\partial q^*}{dT} = \frac{L}{R_v T^2} q^*(T)
|
| 81 |
+
|
| 82 |
+
Then using a chain rule we have
|
| 83 |
+
|
| 84 |
+
.. math::
|
| 85 |
+
|
| 86 |
+
\frac{\partial q^*}{\partial \phi} = \frac{\partial q^*}{\partial T_s} \frac{\partial T_s}{\partial \phi} = \frac{L q^*(T_s)}{R_v T_s^2} \frac{\partial T_s}{\partial \phi}
|
| 87 |
+
|
| 88 |
+
Plugging this into our model equation we get
|
| 89 |
+
|
| 90 |
+
.. math::
|
| 91 |
+
|
| 92 |
+
C \frac{\partial T_s}{\partial t} = \frac{1}{\cos\phi} \frac{\partial}{\partial \phi} \left( \cos\phi D_m \frac{\partial T_s}{\partial \phi} \left(1 + \frac{L^2 r q^*(T_s)}{c_p R_v T_s^2} \right)\right)
|
| 93 |
+
|
| 94 |
+
This is now in a form that is compatible with our diffusion solver.
|
| 95 |
+
|
| 96 |
+
Just let
|
| 97 |
+
|
| 98 |
+
.. math::
|
| 99 |
+
|
| 100 |
+
D = D_m \left( 1 + f(T_s) \right)
|
| 101 |
+
|
| 102 |
+
where
|
| 103 |
+
|
| 104 |
+
.. math::
|
| 105 |
+
|
| 106 |
+
f(T_s) = \frac{L^2 r q^*(T_s)}{c_p R_v T_s^2}
|
| 107 |
+
|
| 108 |
+
or, equivalently,
|
| 109 |
+
|
| 110 |
+
.. math::
|
| 111 |
+
|
| 112 |
+
f(T_s) = \frac{L r }{c_p} \frac{\partial q^*}{dT}\bigg|_{T_s}
|
| 113 |
+
|
| 114 |
+
Given a temperature distribution :math:`T_s(\phi)` at any given time,
|
| 115 |
+
we can calculate the diffusion coefficient :math:`D(\phi)` from this formula.
|
| 116 |
+
|
| 117 |
+
This calculation is implemented in the ``MeridionalMoistDiffusion`` class.
|
| 118 |
+
"""
|
| 119 |
+
import numpy as np
|
| 120 |
+
from .meridional_heat_diffusion import MeridionalHeatDiffusion
|
| 121 |
+
from climlab.utils.thermo import qsat
|
| 122 |
+
from climlab import constants as const
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class MeridionalMoistDiffusion(MeridionalHeatDiffusion):
|
| 126 |
+
def __init__(self, D=0.24, relative_humidity=0.8, **kwargs):
|
| 127 |
+
self.relative_humidity = relative_humidity
|
| 128 |
+
super(MeridionalMoistDiffusion, self).__init__(D=D, **kwargs)
|
| 129 |
+
self._update_diffusivity()
|
| 130 |
+
|
| 131 |
+
def _update_diffusivity(self):
|
| 132 |
+
Tinterp = np.interp(self.lat_bounds, self.lat, np.squeeze(self.Ts))
|
| 133 |
+
Tkelvin = Tinterp + const.tempCtoK
|
| 134 |
+
f = moist_amplification_factor(Tkelvin, self.relative_humidity)
|
| 135 |
+
heat_capacity = self.Ts.domain.heat_capacity
|
| 136 |
+
self.K = self.D / heat_capacity * const.a**2 * (1+f)
|
| 137 |
+
|
| 138 |
+
def _implicit_solver(self):
|
| 139 |
+
self._update_diffusivity()
|
| 140 |
+
# and then do all the same stuff the parent class would do...
|
| 141 |
+
return super(MeridionalMoistDiffusion, self)._implicit_solver()
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def moist_amplification_factor(Tkelvin, relative_humidity=0.8):
|
| 145 |
+
'''Compute the moisture amplification factor for the moist diffusivity
|
| 146 |
+
given relative humidity and reference temperature profile.'''
|
| 147 |
+
deltaT = 0.01
|
| 148 |
+
# slope of saturation specific humidity at 1000 hPa
|
| 149 |
+
dqsdTs = (qsat(Tkelvin+deltaT/2, 1000.) - qsat(Tkelvin-deltaT/2, 1000.)) / deltaT
|
| 150 |
+
return const.Lhvap / const.cp * relative_humidity * dqsdTs
|
climlab/source/climlab/model/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
This package contains ready-made models that can be run "off-the-shelf".
|
| 3 |
+
|
| 4 |
+
:Example:
|
| 5 |
+
|
| 6 |
+
.. code-block:: python
|
| 7 |
+
|
| 8 |
+
import climlab
|
| 9 |
+
# create a 1D Energy Balance Model
|
| 10 |
+
mymodel = climlab.EBM()
|
| 11 |
+
# see what you just created
|
| 12 |
+
print(mymodel)
|
| 13 |
+
# run the model
|
| 14 |
+
mymodel.integrate_years(2.)
|
| 15 |
+
# display the current state
|
| 16 |
+
mymodel.state
|
| 17 |
+
# see what diagnostics have been computed
|
| 18 |
+
mymodel.diagnostics.keys()
|
| 19 |
+
|
| 20 |
+
These modules are fully functional and tested.
|
| 21 |
+
However users are encouraged to build their own models
|
| 22 |
+
by explicitly creating individual processes and coupling together
|
| 23 |
+
as subprocesses of a parent process.
|
| 24 |
+
|
| 25 |
+
See the documentation for the RRTMG scheme for an example of building a
|
| 26 |
+
radiative-convective column model from individual components.
|
| 27 |
+
'''
|
| 28 |
+
from .column import GreyRadiationModel, RadiativeConvectiveModel, BandRCModel
|
| 29 |
+
from .ebm import EBM, EBM_annual, EBM_seasonal
|
climlab/source/climlab/model/column.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Object-oriented code for radiative-convective models with grey-gas radiation.
|
| 2 |
+
|
| 3 |
+
Code developed by Brian Rose, University at Albany
|
| 4 |
+
brose@albany.edu
|
| 5 |
+
|
| 6 |
+
Note that the column models by default represent global, time averages.
|
| 7 |
+
Thus the insolation is a prescribed constant.
|
| 8 |
+
|
| 9 |
+
Here is an example to implement seasonal insolation at 45 degrees North
|
| 10 |
+
|
| 11 |
+
:Example:
|
| 12 |
+
|
| 13 |
+
.. code-block:: python
|
| 14 |
+
|
| 15 |
+
import climlab
|
| 16 |
+
|
| 17 |
+
# create the column model object
|
| 18 |
+
col = climlab.GreyRadiationModel()
|
| 19 |
+
|
| 20 |
+
# create a new latitude axis with a single point
|
| 21 |
+
lat = climlab.domain.Axis(axis_type='lat', points=45.)
|
| 22 |
+
|
| 23 |
+
# add this new axis to the surface domain
|
| 24 |
+
col.Ts.domain.axes['lat'] = lat
|
| 25 |
+
|
| 26 |
+
# create a new insolation process using this domain
|
| 27 |
+
Q = climlab.radiation.insolation.DailyInsolation(domains=col.Ts.domain, **col.param)
|
| 28 |
+
|
| 29 |
+
# replace the fixed insolation subprocess in the column model
|
| 30 |
+
col.add_subprocess('insolation', Q)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
This model is now a single column with seasonally varying insolation
|
| 34 |
+
calculated for 45N.
|
| 35 |
+
|
| 36 |
+
"""
|
| 37 |
+
import numpy as np
|
| 38 |
+
from climlab import constants as const
|
| 39 |
+
from climlab.process import TimeDependentProcess
|
| 40 |
+
from climlab.domain import column_state, Field
|
| 41 |
+
from climlab.radiation import (FixedInsolation, GreyGas, GreyGasSW,
|
| 42 |
+
ThreeBandSW, FourBandLW, ManabeWaterVapor)
|
| 43 |
+
from climlab.convection import ConvectiveAdjustment
|
| 44 |
+
|
| 45 |
+
class GreyRadiationModel(TimeDependentProcess):
|
| 46 |
+
def __init__(self,
|
| 47 |
+
num_lev=30,
|
| 48 |
+
num_lat=1,
|
| 49 |
+
lev=None,
|
| 50 |
+
lat=None,
|
| 51 |
+
water_depth=1.0,
|
| 52 |
+
albedo_sfc=0.299,
|
| 53 |
+
timestep=const.seconds_per_day,
|
| 54 |
+
Q=341.3,
|
| 55 |
+
# absorption coefficient in m**2 / kg
|
| 56 |
+
abs_coeff=1.229E-4,
|
| 57 |
+
**kwargs):
|
| 58 |
+
# Check to see if an initial state is already provided
|
| 59 |
+
# If not, make one
|
| 60 |
+
if 'state' in kwargs:
|
| 61 |
+
state = kwargs.pop('state')
|
| 62 |
+
else:
|
| 63 |
+
state = column_state(num_lev, num_lat, lev, lat, water_depth)
|
| 64 |
+
super(GreyRadiationModel, self).__init__(timestep=timestep, state=state, **kwargs)
|
| 65 |
+
self.param['water_depth'] = water_depth
|
| 66 |
+
self.param['albedo_sfc'] = albedo_sfc
|
| 67 |
+
self.param['Q'] = Q
|
| 68 |
+
self.param['abs_coeff'] = abs_coeff
|
| 69 |
+
|
| 70 |
+
sfc = self.Ts.domain
|
| 71 |
+
atm = self.Tatm.domain
|
| 72 |
+
# create sub-models for longwave and shortwave radiation
|
| 73 |
+
dp = self.Tatm.domain.lev.delta
|
| 74 |
+
absorbLW = compute_layer_absorptivity(self.param['abs_coeff'], dp)
|
| 75 |
+
absorbLW = Field(np.tile(absorbLW, sfc.shape), domain=atm)
|
| 76 |
+
absorbSW = np.zeros_like(absorbLW)
|
| 77 |
+
longwave = GreyGas(state=self.state, absorptivity=absorbLW,
|
| 78 |
+
albedo_sfc=0, **kwargs)
|
| 79 |
+
shortwave = GreyGasSW(state=self.state, absorptivity=absorbSW,
|
| 80 |
+
albedo_sfc=self.param['albedo_sfc'], **kwargs)
|
| 81 |
+
# sub-model for insolation ... here we just set constant Q
|
| 82 |
+
thisQ = self.param['Q']*np.ones_like(self.Ts)
|
| 83 |
+
Q = FixedInsolation(S0=thisQ, domains=sfc, **self.param, **kwargs)
|
| 84 |
+
self.add_subprocess('LW', longwave)
|
| 85 |
+
self.add_subprocess('SW', shortwave)
|
| 86 |
+
self.add_subprocess('insolation', Q)
|
| 87 |
+
newdiags = ['OLR',
|
| 88 |
+
'LW_down_sfc',
|
| 89 |
+
'LW_up_sfc',
|
| 90 |
+
'LW_absorbed_sfc',
|
| 91 |
+
'ASR',
|
| 92 |
+
'SW_absorbed_sfc',
|
| 93 |
+
'SW_up_sfc',
|
| 94 |
+
'SW_up_TOA',
|
| 95 |
+
'SW_down_TOA',
|
| 96 |
+
'SW_down_sfc',
|
| 97 |
+
'planetary_albedo']
|
| 98 |
+
pressure_diags = ['LW_emission', 'LW_absorbed_atm', 'SW_absorbed_atm']
|
| 99 |
+
for name in newdiags:
|
| 100 |
+
self.add_diagnostic(name, 0. * self.Ts)
|
| 101 |
+
for name in pressure_diags:
|
| 102 |
+
self.add_diagnostic(name, 0. * self.Tatm)
|
| 103 |
+
# This process has to handle the coupling between
|
| 104 |
+
# insolation and column radiation
|
| 105 |
+
self.subprocess['SW'].flux_from_space = \
|
| 106 |
+
self.subprocess['insolation'].diagnostics['insolation']
|
| 107 |
+
|
| 108 |
+
def _compute(self):
|
| 109 |
+
# set diagnostics
|
| 110 |
+
self.do_diagnostics()
|
| 111 |
+
# no tendencies for the parent process
|
| 112 |
+
tendencies = {}
|
| 113 |
+
for name, var in self.state.items():
|
| 114 |
+
tendencies[name] = var * 0.
|
| 115 |
+
return tendencies
|
| 116 |
+
|
| 117 |
+
def do_diagnostics(self):
|
| 118 |
+
'''Set all the diagnostics from long and shortwave radiation.'''
|
| 119 |
+
self.OLR = self.subprocess['LW'].flux_to_space
|
| 120 |
+
self.LW_down_sfc = self.subprocess['LW'].flux_to_sfc
|
| 121 |
+
self.LW_up_sfc = self.subprocess['LW'].flux_from_sfc
|
| 122 |
+
self.LW_absorbed_sfc = self.LW_down_sfc - self.LW_up_sfc
|
| 123 |
+
self.LW_absorbed_atm = self.subprocess['LW'].absorbed
|
| 124 |
+
self.LW_emission = self.subprocess['LW'].emission
|
| 125 |
+
# contributions to OLR from surface and atm. levels
|
| 126 |
+
#self.diagnostics['OLR_sfc'] = self.flux['sfc2space']
|
| 127 |
+
#self.diagnostics['OLR_atm'] = self.flux['atm2space']
|
| 128 |
+
self.ASR = (self.subprocess['SW'].flux_from_space -
|
| 129 |
+
self.subprocess['SW'].flux_to_space)
|
| 130 |
+
#self.SW_absorbed_sfc = (self.subprocess['surface'].SW_from_atm -
|
| 131 |
+
# self.subprocess['surface'].SW_to_atm)
|
| 132 |
+
self.SW_absorbed_atm = self.subprocess['SW'].absorbed
|
| 133 |
+
self.SW_down_sfc = self.subprocess['SW'].flux_to_sfc
|
| 134 |
+
self.SW_up_sfc = self.subprocess['SW'].flux_from_sfc
|
| 135 |
+
self.SW_absorbed_sfc = self.SW_down_sfc - self.SW_up_sfc
|
| 136 |
+
self.SW_up_TOA = self.subprocess['SW'].flux_to_space
|
| 137 |
+
self.SW_down_TOA = self.subprocess['SW'].flux_from_space
|
| 138 |
+
self.planetary_albedo = (self.subprocess['SW'].flux_to_space /
|
| 139 |
+
self.subprocess['SW'].flux_from_space)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class RadiativeConvectiveModel(GreyRadiationModel):
|
| 143 |
+
def __init__(self,
|
| 144 |
+
# lapse rate for convective adjustment, in K / km
|
| 145 |
+
adj_lapse_rate=6.5,
|
| 146 |
+
**kwargs):
|
| 147 |
+
super(RadiativeConvectiveModel, self).__init__(**kwargs)
|
| 148 |
+
self.param['adj_lapse_rate'] = adj_lapse_rate
|
| 149 |
+
self.add_subprocess('convective adjustment', \
|
| 150 |
+
ConvectiveAdjustment(state=self.state, **self.param))
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class BandRCModel(RadiativeConvectiveModel):
|
| 154 |
+
def __init__(self, **kwargs):
|
| 155 |
+
super(BandRCModel, self).__init__(**kwargs)
|
| 156 |
+
# Initialize specific humidity
|
| 157 |
+
h2o = ManabeWaterVapor(state=self.state, **self.param)
|
| 158 |
+
self.add_subprocess('H2O', h2o)
|
| 159 |
+
|
| 160 |
+
# initialize radiatively active gas inventories
|
| 161 |
+
self.absorber_vmr = {}
|
| 162 |
+
self.absorber_vmr['CO2'] = 380.E-6 * np.ones_like(self.Tatm)
|
| 163 |
+
self.absorber_vmr['O3'] = np.zeros_like(self.Tatm)
|
| 164 |
+
# water vapor is actually specific humidity, not VMR.
|
| 165 |
+
self.absorber_vmr['H2O'] = h2o.q
|
| 166 |
+
|
| 167 |
+
longwave = FourBandLW(state=self.state,
|
| 168 |
+
absorber_vmr=self.absorber_vmr,
|
| 169 |
+
albedo_sfc=0.)
|
| 170 |
+
shortwave = ThreeBandSW(state=self.state,
|
| 171 |
+
absorber_vmr=self.absorber_vmr,
|
| 172 |
+
emissivity_sfc=0.,
|
| 173 |
+
albedo_sfc=self.param['albedo_sfc'])
|
| 174 |
+
self.add_subprocess('LW', longwave, verbose=False) # Suppress warning about replacing LW and SW
|
| 175 |
+
self.add_subprocess('SW', shortwave, verbose=False)
|
| 176 |
+
# This process has to handle the coupling between
|
| 177 |
+
# insolation and column radiation
|
| 178 |
+
self.subprocess['SW'].flux_from_space = \
|
| 179 |
+
self.subprocess['insolation'].insolation
|
| 180 |
+
|
| 181 |
+
def do_diagnostics(self):
|
| 182 |
+
'''Set all the diagnostics from long and shortwave radiation.
|
| 183 |
+
Here we need to sum over the spectral bands.'''
|
| 184 |
+
self.OLR[:] = np.sum(self.subprocess['LW'].flux_to_space, axis=0)
|
| 185 |
+
self.LW_down_sfc[:] = np.sum(self.subprocess['LW'].flux_to_sfc, axis=0)
|
| 186 |
+
self.LW_up_sfc[:] = np.sum(self.subprocess['LW'].flux_from_sfc, axis=0)
|
| 187 |
+
self.LW_absorbed_sfc[:] = self.LW_down_sfc - self.LW_up_sfc
|
| 188 |
+
self.LW_absorbed_atm[:] = np.sum(self.subprocess['LW'].absorbed, axis=0)
|
| 189 |
+
self.LW_emission[:] = np.sum(self.subprocess['LW'].emission, axis=0)
|
| 190 |
+
self.SW_down_TOA[:] = self.subprocess['SW'].flux_from_space
|
| 191 |
+
self.SW_up_TOA[:] = np.sum(self.subprocess['SW'].flux_to_space, axis=0)
|
| 192 |
+
self.ASR[:] = (self.SW_down_TOA - self.SW_up_TOA)
|
| 193 |
+
self.SW_absorbed_atm[:] = np.sum(self.subprocess['SW'].absorbed, axis=0)
|
| 194 |
+
self.SW_down_sfc[:] = np.sum(self.subprocess['SW'].flux_to_sfc, axis=0)
|
| 195 |
+
self.SW_up_sfc[:] = np.sum(self.subprocess['SW'].flux_from_sfc, axis=0)
|
| 196 |
+
self.SW_absorbed_sfc[:] = self.SW_down_sfc - self.SW_up_sfc
|
| 197 |
+
self.planetary_albedo[:] = self.SW_up_TOA / self.SW_down_TOA
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def compute_layer_absorptivity(abs_coeff, dp):
|
| 201 |
+
'''Compute layer absorptivity from a constant absorption coefficient.'''
|
| 202 |
+
return (2. / (1 + 2. * const.g / abs_coeff /
|
| 203 |
+
(dp * const.mb_to_Pa)))
|
climlab/source/climlab/model/ebm.py
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
r"""Convenience classes for pre-made Energy Balance Models in CLIMLAB.
|
| 2 |
+
|
| 3 |
+
These models all solve some form of the equation
|
| 4 |
+
|
| 5 |
+
.. math::
|
| 6 |
+
|
| 7 |
+
C \frac{\partial}{\partial t} T_s(\phi,t) = (1-\alpha)S(\phi,t) - \left[A + B T_s \right] + \frac{1}{\cos\phi} \frac{\partial}{\partial \phi} \left[ \cos\phi ~ D ~ \frac{\partial T_s}{\partial \phi} \right]
|
| 8 |
+
|
| 9 |
+
where
|
| 10 |
+
|
| 11 |
+
- :math:`\phi` is latitude
|
| 12 |
+
- :math:`T_s` is a zonally averaged surface temperature
|
| 13 |
+
- :math:`C` is a depth-integrated heat capacity
|
| 14 |
+
- :math:`\alpha` is an albedo (which may depend on latitude and/or temperature)
|
| 15 |
+
- :math:`S(\phi, t)` is the insolation
|
| 16 |
+
- :math:`\left[A + B T_s \right]` is a parameterization of the Outgoing Longwave Radiation to space
|
| 17 |
+
- the last term on the right hand side is a diffusive heat transport convergence with thermal diffusivity :math:`D` in the same units as :math:`B`
|
| 18 |
+
|
| 19 |
+
Three classes are provided, which differ in the type of insolation :math:`S`:
|
| 20 |
+
|
| 21 |
+
- ``climlab.EBM`` uses a steady idealized annual insolation (second Legendre polynomial form)
|
| 22 |
+
- ``climlab.EBM_annual`` uses realistic steady annual-mean insolation
|
| 23 |
+
- ``climlab.EBM_seasonal`` uses realistic seasonally varying insolation
|
| 24 |
+
|
| 25 |
+
The ``__init__`` method of class ``EBM`` shows how these models are assembled
|
| 26 |
+
from subprocesses representing each term in the above equation.
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
Building the Moist EBM
|
| 30 |
+
----------------------
|
| 31 |
+
|
| 32 |
+
There is currently no ready-made convenience class for the **moist EBM**,
|
| 33 |
+
but it can be readily built by swapping out the dry heat diffusion process ``climlab.dynamics.MeridionalHeatDiffusion``
|
| 34 |
+
with the moist equivalent ``climlab.dynamics.MeridionalMoistDiffusion``.
|
| 35 |
+
|
| 36 |
+
This sort of mixing and matching of model components is at the heart of CLIMLAB
|
| 37 |
+
design and functionality.
|
| 38 |
+
|
| 39 |
+
:Example:
|
| 40 |
+
|
| 41 |
+
.. code-block:: python
|
| 42 |
+
|
| 43 |
+
import climlab
|
| 44 |
+
# create and display a 1D Energy Balance Model
|
| 45 |
+
dry = climlab.EBM()
|
| 46 |
+
print(dry)
|
| 47 |
+
# clone this model and swap out the diffusion subprocess
|
| 48 |
+
moist = climlab.process_like(dry)
|
| 49 |
+
diff = climlab.dynamics.MeridionalMoistDiffusion(state=moist.state, timestep=moist.timestep)
|
| 50 |
+
moist.add_subprocess('diffusion', diff)
|
| 51 |
+
print(moist)
|
| 52 |
+
|
| 53 |
+
We can run both models out to equilibrium and compare the results as follows:
|
| 54 |
+
|
| 55 |
+
:Example:
|
| 56 |
+
|
| 57 |
+
.. code-block:: python
|
| 58 |
+
|
| 59 |
+
# Run both models out to quasi-equilibrium
|
| 60 |
+
# print out the global mean planetary energy budget -- should be very small
|
| 61 |
+
for m in [dry, moist]:
|
| 62 |
+
m.integrate_years(10)
|
| 63 |
+
print(climlab.global_mean(m.net_radiation))
|
| 64 |
+
# plot and compare the temperatures
|
| 65 |
+
import matplotlib.pyplot as plt
|
| 66 |
+
plt.figure()
|
| 67 |
+
plt.plot(dry.lat, dry.Ts, label='Dry')
|
| 68 |
+
plt.plot(moist.lat, moist.Ts, label='Moist')
|
| 69 |
+
plt.legend()
|
| 70 |
+
plt.show()
|
| 71 |
+
# plot and compare the heat transport
|
| 72 |
+
plt.figure()
|
| 73 |
+
plt.plot(dry.lat_bounds, dry.heat_transport, label='Dry')
|
| 74 |
+
plt.plot(moist.lat_bounds, moist.heat_transport, label='Moist')
|
| 75 |
+
plt.legend()
|
| 76 |
+
plt.show()
|
| 77 |
+
|
| 78 |
+
"""
|
| 79 |
+
import numpy as np
|
| 80 |
+
from math import pi
|
| 81 |
+
from climlab import constants as const
|
| 82 |
+
from climlab.domain.field import Field, global_mean
|
| 83 |
+
from climlab.process import EnergyBudget, TimeDependentProcess
|
| 84 |
+
from climlab.utils import legendre
|
| 85 |
+
from climlab.domain import domain
|
| 86 |
+
from climlab.radiation import AplusBT, P2Insolation, AnnualMeanInsolation, DailyInsolation, SimpleAbsorbedShortwave
|
| 87 |
+
from climlab.surface import albedo
|
| 88 |
+
from climlab.dynamics import MeridionalHeatDiffusion
|
| 89 |
+
from climlab.domain.initial import surface_state
|
| 90 |
+
from scipy import integrate
|
| 91 |
+
|
| 92 |
+
# A lot of this should be re-written / simplified
|
| 93 |
+
# using more up-to-date climlab APIs for coupling processes together
|
| 94 |
+
# Making sure that each subprocess properly declares inputs and diagnostics
|
| 95 |
+
|
| 96 |
+
# For example, the basic EBM should be created with something like
|
| 97 |
+
# ebm = climlab.couple([asr,olr,diff])
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class EBM(TimeDependentProcess):
|
| 101 |
+
"""A parent class for all Energy-Balance-Model classes.
|
| 102 |
+
|
| 103 |
+
This class sets up a typical EnergyBalance Model with following subprocesses:
|
| 104 |
+
|
| 105 |
+
* Outgoing Longwave Radiation (OLR) parametrization through
|
| 106 |
+
:class:`~climlab.radiation.AplusBT`
|
| 107 |
+
* Absorbed Shortwave Radiation (ASR) through
|
| 108 |
+
:class:`~climlab.radiation.SimpleAbsorbedShortwave`
|
| 109 |
+
* solar insolation paramtrization through
|
| 110 |
+
:class:`~climlab.radiation.P2Insolation`
|
| 111 |
+
* albedo parametrization in dependence of temperature through
|
| 112 |
+
:class:`~climlab.surface.StepFunctionAlbedo`
|
| 113 |
+
* energy diffusion through
|
| 114 |
+
:class:`~climlab.dynamics.MeridionalHeatDiffusion`
|
| 115 |
+
|
| 116 |
+
**Initialization parameters** \n
|
| 117 |
+
|
| 118 |
+
An instance of ``EBM`` is initialized with the following
|
| 119 |
+
arguments *(for detailed information see Object attributes below)*:
|
| 120 |
+
|
| 121 |
+
:param int num_lat: number of equally spaced points for the
|
| 122 |
+
latitue grid. Used for domain intialization of
|
| 123 |
+
:class:`~climlab.domain.domain.zonal_mean_surface`
|
| 124 |
+
\n
|
| 125 |
+
- default value: ``90``
|
| 126 |
+
:param int num_lon: number of equally spaced points in longitude
|
| 127 |
+
\n
|
| 128 |
+
- default value: ``None``
|
| 129 |
+
:param float S0: solar constant \n
|
| 130 |
+
- unit: :math:`\\frac{\\textrm{W}}{\\textrm{m}^2}` \n
|
| 131 |
+
- default value: ``1365.2``
|
| 132 |
+
:param float A: parameter for linear OLR parametrization
|
| 133 |
+
:class:`~climlab.radiation.AplusBT.AplusBT` \n
|
| 134 |
+
- unit: :math:`\\frac{\\textrm{W}}{\\textrm{m}^2}` \n
|
| 135 |
+
- default value: ``210.0``
|
| 136 |
+
:param float B: parameter for linear OLR parametrization
|
| 137 |
+
:class:`~climlab.radiation.AplusBT.AplusBT` \n
|
| 138 |
+
- unit: :math:`\\frac{\\textrm{W}}{\\textrm{m}^2 \\ ^{\circ} \\textrm{C}}` \n
|
| 139 |
+
- default value: ``2.0``
|
| 140 |
+
:param float D: diffusion parameter for Meridional Energy Diffusion
|
| 141 |
+
:class:`~climlab.dynamics.diffusion.MeridionalDiffusion`
|
| 142 |
+
\n
|
| 143 |
+
- unit: :math:`\\frac{\\textrm{W}}{\\textrm{m}^2 \\ ^{\circ} \\textrm{C}}` \n
|
| 144 |
+
- default value: ``0.555``
|
| 145 |
+
:param float water_depth: depth of :class:`~climlab.domain.domain.zonal_mean_surface`
|
| 146 |
+
domain, which the heat capacity is dependent on
|
| 147 |
+
\n
|
| 148 |
+
- unit: meters \n
|
| 149 |
+
- default value: ``10.0``
|
| 150 |
+
:param float Tf: freezing temperature \n
|
| 151 |
+
- unit: :math:`^{\circ} \\textrm{C}` \n
|
| 152 |
+
- default value: ``-10.0``
|
| 153 |
+
:param float a0: base value for planetary albedo parametrization
|
| 154 |
+
:class:`~climlab.surface.albedo.StepFunctionAlbedo`
|
| 155 |
+
\n
|
| 156 |
+
- unit: dimensionless
|
| 157 |
+
- default value: ``0.3``
|
| 158 |
+
:param float a2: parabolic value for planetary albedo parametrization
|
| 159 |
+
:class:`~climlab.surface.albedo.StepFunctionAlbedo`
|
| 160 |
+
\n
|
| 161 |
+
- unit: dimensionless
|
| 162 |
+
- default value: ``0.078``
|
| 163 |
+
:param float ai: value for ice albedo paramerization in
|
| 164 |
+
:class:`~climlab.surface.albedo.StepFunctionAlbedo`
|
| 165 |
+
\n
|
| 166 |
+
- unit: dimensionless
|
| 167 |
+
- default value: ``0.62``
|
| 168 |
+
:param float timestep: specifies the EBM's timestep \n
|
| 169 |
+
- unit: seconds
|
| 170 |
+
- default value: (365.2422 * 24 * 60 * 60 ) / 90 \n
|
| 171 |
+
-> (90 timesteps per year)
|
| 172 |
+
:param float T0: base value for initial temperature \n
|
| 173 |
+
- unit :math:`^{\circ} \\textrm{C}` \n
|
| 174 |
+
- default value: ``12``
|
| 175 |
+
:param float T2: factor for 2nd Legendre polynomial
|
| 176 |
+
:class:`~climlab.utils.legendre.P2`
|
| 177 |
+
to calculate initial temperature \n
|
| 178 |
+
- unit: dimensionless
|
| 179 |
+
- default value: ``40``
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
**Object attributes** \n
|
| 185 |
+
|
| 186 |
+
Additional to the parent class :class:`~climlab.process.EnergyBudget`
|
| 187 |
+
following object attributes are generated and updated during initialization:
|
| 188 |
+
|
| 189 |
+
:ivar dict param: The parameter dictionary is updated with a couple
|
| 190 |
+
of the initatilzation input arguments, namely
|
| 191 |
+
``'S0'``, ``'A'``, ``'B'``, ``'D'``, ``'Tf'``,
|
| 192 |
+
``'water_depth'``, ``'a0'``, ``'a2'`` and ``'ai'``.
|
| 193 |
+
:ivar dict domains: If the object's ``domains`` and the ``state``
|
| 194 |
+
dictionaries are empty during initialization
|
| 195 |
+
a domain ``sfc`` is created through
|
| 196 |
+
:func:`~climlab.domain.domain.zonal_mean_surface`.
|
| 197 |
+
In the meantime the object's ``domains`` and
|
| 198 |
+
``state`` dictionaries are updated.
|
| 199 |
+
:ivar dict subprocess: Several subprocesses are created (see above)
|
| 200 |
+
through calling
|
| 201 |
+
:func:`~climlab.process.process.Process.add_subprocess`
|
| 202 |
+
and therefore the subprocess dictionary is updated.
|
| 203 |
+
:ivar bool topdown: is set to ``False`` to call subprocess compute
|
| 204 |
+
methods first.
|
| 205 |
+
See also
|
| 206 |
+
:class:`~climlab.process.time_dependent_process.TimeDependentProcess`.
|
| 207 |
+
:ivar dict diagnostics: is initialized with keys: ``'OLR'``, ``'ASR'``,
|
| 208 |
+
``'net_radiation'``, ``'albedo'``, ``'icelat'`` and
|
| 209 |
+
``'ice_area'`` through
|
| 210 |
+
:func:`~climlab.process.process.Process.add_diagnostic`.
|
| 211 |
+
|
| 212 |
+
:Example:
|
| 213 |
+
|
| 214 |
+
Creation and integration of the preconfigured Energy Balance Model::
|
| 215 |
+
|
| 216 |
+
>>> import climlab
|
| 217 |
+
>>> model = climlab.EBM()
|
| 218 |
+
|
| 219 |
+
>>> model.integrate_years(2.)
|
| 220 |
+
Integrating for 180 steps, 730.4844 days, or 2.0 years.
|
| 221 |
+
Total elapsed time is 2.0 years.
|
| 222 |
+
|
| 223 |
+
For more information how to use the EBM class, see the :ref:`Tutorial`
|
| 224 |
+
chapter.
|
| 225 |
+
|
| 226 |
+
"""
|
| 227 |
+
def __init__(self,
|
| 228 |
+
num_lat=90,
|
| 229 |
+
num_lon=None,
|
| 230 |
+
S0=const.S0,
|
| 231 |
+
s2=-0.48,
|
| 232 |
+
A=210.,
|
| 233 |
+
B=2.,
|
| 234 |
+
D=0.555, # in W / m^2 / degC, same as B
|
| 235 |
+
water_depth=10.0,
|
| 236 |
+
Tf=-10.,
|
| 237 |
+
a0=0.3,
|
| 238 |
+
a2=0.078,
|
| 239 |
+
ai=0.62,
|
| 240 |
+
timestep=const.seconds_per_year/90.,
|
| 241 |
+
initial_time=np.datetime64('1970-01-01T00:00'),
|
| 242 |
+
T0 = 12., # initial temperature parameters
|
| 243 |
+
T2 = -40., # (2nd Legendre polynomial)
|
| 244 |
+
**kwargs):
|
| 245 |
+
# Check to see if an initial state is already provided
|
| 246 |
+
# If not, make one
|
| 247 |
+
if 'state' in kwargs:
|
| 248 |
+
state = kwargs.pop('state')
|
| 249 |
+
else:
|
| 250 |
+
state = surface_state(num_lat=num_lat, num_lon=num_lon,
|
| 251 |
+
water_depth=water_depth, T0=T0, T2=T2)
|
| 252 |
+
super(EBM, self).__init__(timestep=timestep, state=state, initial_time=initial_time, **kwargs)
|
| 253 |
+
sfc = self.Ts.domain
|
| 254 |
+
self.param['S0'] = S0
|
| 255 |
+
self.param['s2'] = s2
|
| 256 |
+
self.param['A'] = A
|
| 257 |
+
self.param['B'] = B
|
| 258 |
+
self.param['D'] = D
|
| 259 |
+
self.param['Tf'] = Tf
|
| 260 |
+
self.param['water_depth'] = water_depth
|
| 261 |
+
self.param['a0'] = a0
|
| 262 |
+
self.param['a2'] = a2
|
| 263 |
+
self.param['ai'] = ai
|
| 264 |
+
# create sub-models
|
| 265 |
+
lw = AplusBT(state=self.state, initial_time=initial_time, **self.param)
|
| 266 |
+
ins = P2Insolation(domains=sfc, initial_time=initial_time, **self.param)
|
| 267 |
+
alb = albedo.StepFunctionAlbedo(state=self.state, initial_time=initial_time, **self.param)
|
| 268 |
+
sw = SimpleAbsorbedShortwave(state=self.state,
|
| 269 |
+
insolation=ins.insolation,
|
| 270 |
+
albedo=alb.albedo,
|
| 271 |
+
initial_time=initial_time,
|
| 272 |
+
**self.param)
|
| 273 |
+
diff = MeridionalHeatDiffusion(state=self.state, use_banded_solver=False, initial_time=initial_time, **self.param)
|
| 274 |
+
self.add_subprocess('LW', lw)
|
| 275 |
+
self.add_subprocess('insolation', ins)
|
| 276 |
+
self.add_subprocess('albedo', alb)
|
| 277 |
+
self.add_subprocess('SW', sw)
|
| 278 |
+
self.add_subprocess('diffusion', diff)
|
| 279 |
+
self.topdown = False # call subprocess compute methods first
|
| 280 |
+
self.add_diagnostic('net_radiation', 0.*self.Ts)
|
| 281 |
+
|
| 282 |
+
@property
|
| 283 |
+
def S0(self):
|
| 284 |
+
return self.subprocess['insolation'].S0
|
| 285 |
+
@S0.setter
|
| 286 |
+
def S0(self, value):
|
| 287 |
+
self.param['S0'] = value
|
| 288 |
+
self.subprocess['insolation'].S0 = value
|
| 289 |
+
|
| 290 |
+
def _compute(self):
|
| 291 |
+
self.net_radiation[:] = self.subprocess['SW'].ASR - self.subprocess['LW'].OLR
|
| 292 |
+
return super(EBM, self)._compute()
|
| 293 |
+
|
| 294 |
+
def global_mean_temperature(self):
|
| 295 |
+
"""Convenience method to compute global mean surface temperature.
|
| 296 |
+
|
| 297 |
+
Calls :func:`~climlab.domain.field.global_mean` method which
|
| 298 |
+
for the object attriute ``Ts`` which calculates the latitude weighted
|
| 299 |
+
global mean of a field.
|
| 300 |
+
|
| 301 |
+
:Example:
|
| 302 |
+
|
| 303 |
+
Calculating the global mean temperature of initial EBM temperature::
|
| 304 |
+
|
| 305 |
+
>>> import climlab
|
| 306 |
+
>>> model = climlab.EBM(T0=14., T2=-25)
|
| 307 |
+
|
| 308 |
+
>>> model.global_mean_temperature()
|
| 309 |
+
Field(13.99873037400856)
|
| 310 |
+
|
| 311 |
+
"""
|
| 312 |
+
return global_mean(self.Ts)
|
| 313 |
+
|
| 314 |
+
def inferred_heat_transport(self):
|
| 315 |
+
"""Calculates the inferred heat transport by integrating the TOA
|
| 316 |
+
energy imbalance from pole to pole.
|
| 317 |
+
|
| 318 |
+
The method is calculating
|
| 319 |
+
|
| 320 |
+
.. math::
|
| 321 |
+
|
| 322 |
+
H(\\varphi) = 2 \pi R^2 \int_{-\pi/2}^{\\varphi} cos\phi \ R_{TOA} d\phi
|
| 323 |
+
|
| 324 |
+
where :math:`R_{TOA}` is the net radiation at top of atmosphere.
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
:return: total heat transport on the latitude grid in unit :math:`\\textrm{PW}`
|
| 328 |
+
:rtype: array of size ``np.size(self.lat_lat)``
|
| 329 |
+
|
| 330 |
+
:Example:
|
| 331 |
+
|
| 332 |
+
.. plot:: code_input_manual/example_EBM_inferred_heat_transport.py
|
| 333 |
+
:include-source:
|
| 334 |
+
|
| 335 |
+
"""
|
| 336 |
+
phi = np.deg2rad(self.lat)
|
| 337 |
+
energy_in = np.squeeze(self.net_radiation)
|
| 338 |
+
return (1E-15 * 2 * pi * const.a**2 *
|
| 339 |
+
integrate.cumulative_trapezoid(np.cos(phi)*energy_in, x=phi, initial=0.))
|
| 340 |
+
|
| 341 |
+
def diffusive_heat_transport(self):
|
| 342 |
+
"""Compute instantaneous diffusive heat transport in unit :math:`\\textrm{PW}`
|
| 343 |
+
on the staggered grid (bounds) through calculating:
|
| 344 |
+
|
| 345 |
+
.. math::
|
| 346 |
+
|
| 347 |
+
H(\\varphi) = - 2 \pi R^2 cos(\\varphi) D \\frac{dT}{d\\varphi}
|
| 348 |
+
\\approx - 2 \pi R^2 cos(\\varphi) D \\frac{\Delta T}{\Delta \\varphi}
|
| 349 |
+
|
| 350 |
+
:rtype: array of size ``np.size(self.lat_bounds)``
|
| 351 |
+
|
| 352 |
+
THIS IS DEPRECATED AND WILL BE REMOVED IN THE FUTURE. Use the diagnostic
|
| 353 |
+
``heat_transport`` instead, which implements the same calculation.
|
| 354 |
+
"""
|
| 355 |
+
phi = np.deg2rad(self.lat)
|
| 356 |
+
phi_stag = np.deg2rad(self.lat_bounds)
|
| 357 |
+
D = self.param['D']
|
| 358 |
+
T = np.squeeze(self.Ts)
|
| 359 |
+
dTdphi = np.diff(T) / np.diff(phi)
|
| 360 |
+
dTdphi = np.append(dTdphi, 0.)
|
| 361 |
+
dTdphi = np.insert(dTdphi, 0, 0.)
|
| 362 |
+
return (1E-15*-2*pi*np.cos(phi_stag)*const.a**2*D*dTdphi)
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
class EBM_seasonal(EBM):
|
| 366 |
+
def __init__(self, a0=0.33, a2=0.25, ai=None, **kwargs):
|
| 367 |
+
"""A class that implements Energy Balance Models with realistic
|
| 368 |
+
daily insolation.
|
| 369 |
+
|
| 370 |
+
This class is inherited from the general :class:`~climlab.EBM`
|
| 371 |
+
class and uses the insolation subprocess
|
| 372 |
+
:class:`~climlab.radiation.DailyInsolation` instead of
|
| 373 |
+
:class:`~climlab.radiation.P2Insolation` to compute a
|
| 374 |
+
realisitc distribution of solar radiation on a daily basis.
|
| 375 |
+
|
| 376 |
+
If argument for ice albedo ``'ai'`` is not given, the model will not
|
| 377 |
+
have an albedo feedback.
|
| 378 |
+
|
| 379 |
+
An instance of ``EBM_seasonal`` is initialized with the following
|
| 380 |
+
arguments:
|
| 381 |
+
|
| 382 |
+
:param float a0: base value for planetary albedo parametrization
|
| 383 |
+
:class:`~climlab.surface.albedo.StepFunctionAlbedo`
|
| 384 |
+
[default: 0.33]
|
| 385 |
+
:param float a2: parabolic value for planetary albedo parametrization
|
| 386 |
+
:class:`~climlab.surface.albedo.StepFunctionAlbedo`
|
| 387 |
+
[default: 0.25]
|
| 388 |
+
:param float ai: value for ice albedo paramerization in
|
| 389 |
+
:class:`~climlab.surface.albedo.StepFunctionAlbedo`
|
| 390 |
+
(optional)
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
**Object attributes** \n
|
| 394 |
+
|
| 395 |
+
Following object attributes are updated during initialization: \n
|
| 396 |
+
|
| 397 |
+
:ivar dict param: The parameter dictionary is updated with
|
| 398 |
+
``'a0'`` and ``'a2'``.
|
| 399 |
+
:ivar dict subprocess: suprocess ``'insolation'`` is overwritten by
|
| 400 |
+
:class:`~climlab.radiation.insolation.DailyInsolation`.
|
| 401 |
+
|
| 402 |
+
*if* ``'ai'`` *is not given*:
|
| 403 |
+
|
| 404 |
+
:ivar dict param: ``'ai'`` and ``'Tf'`` are removed from the
|
| 405 |
+
parameter dictionary (initialized by parent class
|
| 406 |
+
:class:`~climlab.model.ebm.EBM`)
|
| 407 |
+
:ivar dict subprocess: suprocess ``'albedo'`` is overwritten by
|
| 408 |
+
:class:`~climlab.surface.albedo.P2Albedo`.
|
| 409 |
+
|
| 410 |
+
*if* ``'ai'`` *is given*:
|
| 411 |
+
|
| 412 |
+
:ivar dict param: The parameter dictionary is updated with
|
| 413 |
+
``'ai'``.
|
| 414 |
+
:ivar dict subprocess: suprocess ``'albedo'`` is overwritten by
|
| 415 |
+
:class:`~climlab.surface.albedo.StepFunctionAlbedo`
|
| 416 |
+
(which basically has been there before but now is
|
| 417 |
+
updated with the new albedo parameter values).
|
| 418 |
+
:Example:
|
| 419 |
+
|
| 420 |
+
The annual distribution of solar insolation:
|
| 421 |
+
|
| 422 |
+
.. plot:: code_input_manual/example_EBM_seasonal.py
|
| 423 |
+
:include-source:
|
| 424 |
+
|
| 425 |
+
"""
|
| 426 |
+
if ai is None:
|
| 427 |
+
no_albedo_feedback = True
|
| 428 |
+
ai = 0. # ignored but need to set a number
|
| 429 |
+
else:
|
| 430 |
+
no_albedo_feedback = False
|
| 431 |
+
super(EBM_seasonal, self).__init__(a0=a0, a2=a2, ai=ai, **kwargs)
|
| 432 |
+
self.param['a0'] = a0
|
| 433 |
+
self.param['a2'] = a2
|
| 434 |
+
sfc = self.domains['Ts']
|
| 435 |
+
ins = DailyInsolation(domains=sfc, initial_time=self.time['initial_time'], **self.param)
|
| 436 |
+
if no_albedo_feedback:
|
| 437 |
+
# Remove unused parameters here for clarity
|
| 438 |
+
_ = self.param.pop('ai')
|
| 439 |
+
_ = self.param.pop('Tf')
|
| 440 |
+
alb = albedo.P2Albedo(domains=sfc, initial_time=self.time['initial_time'], **self.param)
|
| 441 |
+
else:
|
| 442 |
+
self.param['ai'] = ai
|
| 443 |
+
alb = albedo.StepFunctionAlbedo(state=self.state, initial_time=self.time['initial_time'], **self.param)
|
| 444 |
+
sw = SimpleAbsorbedShortwave(state=self.state,
|
| 445 |
+
insolation=ins.insolation,
|
| 446 |
+
albedo=alb.albedo,
|
| 447 |
+
initial_time=self.time['initial_time'],
|
| 448 |
+
**self.param)
|
| 449 |
+
self.add_subprocess('insolation', ins, verbose=False)
|
| 450 |
+
self.add_subprocess('albedo', alb, verbose=False)
|
| 451 |
+
self.add_subprocess('SW', sw, verbose=False)
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
class EBM_annual(EBM_seasonal):
|
| 455 |
+
def __init__(self, **kwargs):
|
| 456 |
+
"""A class that implements Energy Balance Models with annual mean insolation.
|
| 457 |
+
|
| 458 |
+
The annual solar distribution is calculated through averaging the
|
| 459 |
+
:class:`~climlab.radiation.insolation.DailyInsolation` over time
|
| 460 |
+
which has been used in used in the parent class
|
| 461 |
+
:class:`~climlab.EBM_seasonal`. That is done by the subprocess
|
| 462 |
+
:class:`~climlab.radiation.AnnualMeanInsolation` which is
|
| 463 |
+
more realistic than the :class:`~climlab.radiation.P2Insolation`
|
| 464 |
+
module used in the classical :class:`~climlab.EBM` class.
|
| 465 |
+
|
| 466 |
+
According to the parent class :class:`~climlab.EBM_seasonal`
|
| 467 |
+
the model will not have an ice-albedo feedback, if albedo ice parameter
|
| 468 |
+
``'ai'`` is not given. For details see there.
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
**Object attributes** \n
|
| 472 |
+
|
| 473 |
+
Following object attributes are updated during initialization: \n
|
| 474 |
+
|
| 475 |
+
:ivar dict subprocess: suprocess ``'insolation'`` is overwritten by
|
| 476 |
+
:class:`~climlab.radiation.AnnualMeanInsolation`
|
| 477 |
+
|
| 478 |
+
:Example:
|
| 479 |
+
|
| 480 |
+
The :class:`~climlab.EBM_annual` class uses a different
|
| 481 |
+
insolation subprocess than the :class:`~climlab.EBM` class::
|
| 482 |
+
|
| 483 |
+
>>> import climlab
|
| 484 |
+
>>> model_annual = climlab.EBM_annual()
|
| 485 |
+
|
| 486 |
+
>>> print model_annual
|
| 487 |
+
|
| 488 |
+
.. code-block:: none
|
| 489 |
+
:emphasize-lines: 9
|
| 490 |
+
|
| 491 |
+
climlab Process of type <class 'climlab.model.ebm.EBM_annual'>.
|
| 492 |
+
State variables and domain shapes:
|
| 493 |
+
Ts: (90, 1)
|
| 494 |
+
The subprocess tree:
|
| 495 |
+
top: <class 'climlab.EBM_annual'>
|
| 496 |
+
diffusion: <class 'climlab.dynamics.MeridionalHeatDiffusion'>
|
| 497 |
+
LW: <class 'climlab.radiation.AplusBT'>
|
| 498 |
+
albedo: <class 'climlab.surface.P2Albedo'>
|
| 499 |
+
insolation: <class 'climlab.radiation.AnnualMeanInsolation'>
|
| 500 |
+
|
| 501 |
+
"""
|
| 502 |
+
super(EBM_annual, self).__init__(**kwargs)
|
| 503 |
+
sfc = self.domains['Ts']
|
| 504 |
+
ins = AnnualMeanInsolation(domains=sfc, initial_time=self.time['initial_time'], **self.param)
|
| 505 |
+
self.add_subprocess('insolation', ins, verbose=False)
|
| 506 |
+
self.subprocess['SW'].insolation = ins.insolation
|
| 507 |
+
|
| 508 |
+
# an EBM that computes degree-days has an additional state variable.
|
| 509 |
+
# Need to implement that
|
| 510 |
+
# could make a good working example to document creating a new model class
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
#==============================================================================
|
| 516 |
+
#
|
| 517 |
+
# class _EBM(TimeDependentProcess):
|
| 518 |
+
# def __init__(self, num_points=90, K=0.555, **kwargs):
|
| 519 |
+
# # first create the model domains
|
| 520 |
+
# doms = domain.zonal_mean_surface(num_points=num_points)
|
| 521 |
+
# # initial surface temperature
|
| 522 |
+
# lat = doms['sfc'].grid['lat'].points
|
| 523 |
+
# initial = {}
|
| 524 |
+
# initial['Ts'] = 12. - 40. * legendre.P2(np.sin(np.deg2rad(lat)))
|
| 525 |
+
# # Create process data structures
|
| 526 |
+
# super(_EBM, self).__init__(domains=doms, state=initial, **kwargs)
|
| 527 |
+
# # first set all parameters to sensible default values
|
| 528 |
+
# #self.num_points = num_points
|
| 529 |
+
# # self.K = 2.2E6 # in m^2 / s
|
| 530 |
+
# self.K = 0.555 # in W / m^2 / degC, same as B
|
| 531 |
+
# self.A = 210.
|
| 532 |
+
# self.B = 2.
|
| 533 |
+
# # self.water_depth = 10.0
|
| 534 |
+
# self.Tf = 0.0
|
| 535 |
+
# self.S0 = const.S0
|
| 536 |
+
# self.make_grid()
|
| 537 |
+
# # self.albedo_noice = 0.303 + 0.0779 * P2( np.sin( self.phi ) )
|
| 538 |
+
# self.albedo_noice = 0.33 + 0.25 * legendre.P2(np.sin(self.phi))
|
| 539 |
+
# # self.albedo_ice = 0.62 * np.ones_like( self.phi )
|
| 540 |
+
# self.albedo_ice = self.albedo_noice # default to no albedo feedback
|
| 541 |
+
# self.T = 12. - 40. * legendre.P2(np.sin(self.phi))
|
| 542 |
+
# # A dictionary of the model state variables
|
| 543 |
+
# self.state = {'T': self.T}
|
| 544 |
+
# self.positive_degree_days = np.zeros_like(self.phi)
|
| 545 |
+
# # self.make_insolation_array() # now called from inside set_timestep()
|
| 546 |
+
# self.external_heat_source = np.zeros_like(self.phi)
|
| 547 |
+
# self.set_timestep()
|
| 548 |
+
#
|
| 549 |
+
# def make_grid(self):
|
| 550 |
+
# '''Build the grid for the computation, evenly spaced in latitude.'''
|
| 551 |
+
# # dlat will be our grid spacing
|
| 552 |
+
# # lat will be our temperature grid:
|
| 553 |
+
# # an array with exactly num_points evenly spaced points
|
| 554 |
+
# # lat_stag will be a staggered grid with numpoints+1 points,
|
| 555 |
+
# # where the end points are the North and South poles
|
| 556 |
+
# # Then we convert these all to radians for the computation.
|
| 557 |
+
# self.dlat = 180. / self.num_points
|
| 558 |
+
# self.lat = np.linspace(-90. + self.dlat/2,
|
| 559 |
+
# 90. - self.dlat/2, self.num_points)
|
| 560 |
+
# self.lat_stag = np.linspace(-90., 90., self.num_points+1)
|
| 561 |
+
# self.dphi = np.deg2rad(self.dlat)
|
| 562 |
+
# self.phi = np.deg2rad(self.lat)
|
| 563 |
+
# self.phi_stag = np.deg2rad(self.lat_stag)
|
| 564 |
+
#
|
| 565 |
+
# def set_timestep(self, num_steps_per_year=90):
|
| 566 |
+
# '''Change the timestep, given a number of steps per calendar year.'''
|
| 567 |
+
# super(_EBM, self).set_timestep(num_steps_per_year)
|
| 568 |
+
# self.set_water_depth()
|
| 569 |
+
# self.make_insolation_array()
|
| 570 |
+
#
|
| 571 |
+
# def set_water_depth(self, water_depth=10.):
|
| 572 |
+
# '''Method for changing the water depth (heat capacity) with depth in m.
|
| 573 |
+
# Also recomputes the tridiagonal diffusion matrix.'''
|
| 574 |
+
# if water_depth is None:
|
| 575 |
+
# try:
|
| 576 |
+
# water_depth = self.water_depth
|
| 577 |
+
# except:
|
| 578 |
+
# ValueError("water_depth parameter is not specified.")
|
| 579 |
+
# self.water_depth = water_depth
|
| 580 |
+
# self.C = const.cw * const.rho_w * self.water_depth
|
| 581 |
+
# self.delta_time_over_C = self.timestep / self.C
|
| 582 |
+
# self.set_diffusivity(self.K)
|
| 583 |
+
#
|
| 584 |
+
# def set_diffusivity(self, K=None):
|
| 585 |
+
# '''Method for changing the diffusivity, with K in W/m^2/degC.
|
| 586 |
+
# Recomputes the tridiagonal diffusion matrix.'''
|
| 587 |
+
# if K is None:
|
| 588 |
+
# try:
|
| 589 |
+
# K = self.K
|
| 590 |
+
# except:
|
| 591 |
+
# ValueError("Diffusivity parameter K is not specified.")
|
| 592 |
+
# self.K = K
|
| 593 |
+
# self.diffTriDiag = self._make_diffusion_matrix()
|
| 594 |
+
#
|
| 595 |
+
# def _make_diffusion_matrix(self):
|
| 596 |
+
# J = self.num_points
|
| 597 |
+
# # Ka = (const.cp * const.ps * const.mb_to_Pa / const.g / const.a**2 *
|
| 598 |
+
# # self.K * np.ones_like(self.phi_stag))
|
| 599 |
+
# # cosKa = np.cos(self.phi_stag) * Ka
|
| 600 |
+
# cosKa = np.cos(self.phi_stag) * self.K
|
| 601 |
+
# Ka1 = (cosKa[0:J] / np.cos(self.phi) *
|
| 602 |
+
# self.delta_time_over_C / self.dphi**2)
|
| 603 |
+
# Ka3 = (cosKa[1:J+1] / np.cos(self.phi) *
|
| 604 |
+
# self.delta_time_over_C / self.dphi**2)
|
| 605 |
+
# Ka2 = np.insert(Ka1[1:J], 0, 0) + np.append(Ka3[0:J-1], 0)
|
| 606 |
+
# # Atmosphere tridiagonal matrix
|
| 607 |
+
# diag = np.empty((3, J))
|
| 608 |
+
# diag[0, 1:] = -Ka3[0:J-1]
|
| 609 |
+
# diag[1, :] = 1 + Ka2
|
| 610 |
+
# diag[2, 0:J-1] = -Ka1[1:J]
|
| 611 |
+
# return diag
|
| 612 |
+
#
|
| 613 |
+
# def compute_OLR(self):
|
| 614 |
+
# return self.A + self.B * self.T
|
| 615 |
+
#
|
| 616 |
+
# def make_insolation_array(self):
|
| 617 |
+
# # will be overridden by daughter classes
|
| 618 |
+
# raise NotImplementedError("Subclasses of _EBM must implement a method for computing insolation.")
|
| 619 |
+
#
|
| 620 |
+
# def compute_insolation(self):
|
| 621 |
+
# return self.insolation_array[:, self.day_of_year_index]
|
| 622 |
+
#
|
| 623 |
+
# def compute_albedo(self):
|
| 624 |
+
# '''Simple step-function albedo based on ice line at temperature Tf.'''
|
| 625 |
+
# return np.where(self.T >= self.Tf, self.albedo_noice, self.albedo_ice)
|
| 626 |
+
#
|
| 627 |
+
# def compute_radiation(self):
|
| 628 |
+
# self.ASR = (1 - self.compute_albedo()) * self.compute_insolation()
|
| 629 |
+
# self.OLR = self.compute_OLR()
|
| 630 |
+
# self.net_radiation = self.ASR - self.OLR
|
| 631 |
+
#
|
| 632 |
+
# def step_forward(self):
|
| 633 |
+
# self.compute_radiation()
|
| 634 |
+
# # updated temperature due to radiation:
|
| 635 |
+
# Trad = (self.T + (self.net_radiation + self.external_heat_source) *
|
| 636 |
+
# self.delta_time_over_C)
|
| 637 |
+
# # Time-stepping the diffusion is just inverting this matrix problem:
|
| 638 |
+
# # self.T = np.linalg.solve( self.diffTriDiag, Trad )
|
| 639 |
+
# self.T = solve_banded((1, 1), self.diffTriDiag, Trad)
|
| 640 |
+
# self.positive_degree_days += self.compute_degree_days()
|
| 641 |
+
# super(_EBM, self).step_forward()
|
| 642 |
+
#
|
| 643 |
+
# def compute_degree_days(self, threshold=0.):
|
| 644 |
+
# """Return temperature*time in degree-days,
|
| 645 |
+
# wherever temperature is above the threshold, otherwise zero."""
|
| 646 |
+
# return np.where(self.T > threshold, self.T * self.timestep /
|
| 647 |
+
# const.seconds_per_day, np.zeros_like(self.T))
|
| 648 |
+
#
|
| 649 |
+
# def do_new_calendar_year(self):
|
| 650 |
+
# """This function is called once at the end of every calendar year."""
|
| 651 |
+
# super(_EBM, self).do_new_calendar_year()
|
| 652 |
+
# self.previous_positive_degree_days = self.positive_degree_days
|
| 653 |
+
# self.positive_degree_days = np.zeros_like(self.phi)
|
| 654 |
+
#
|
| 655 |
+
# def heat_transport(self):
|
| 656 |
+
# '''Returns instantaneous heat transport in units on PW,
|
| 657 |
+
# on the staggered grid.'''
|
| 658 |
+
# return self.diffusive_heat_transport()
|
| 659 |
+
#
|
| 660 |
+
# def diffusive_heat_transport( self ):
|
| 661 |
+
# '''Compute instantaneous diffusive heat transport in units of PW, on the staggered grid.'''
|
| 662 |
+
# #return ( 1E-15 * -2 * pi * np.cos(self.phi_stag) * const.cp * const.ps * const.mb_to_Pa / const.g * self.K *
|
| 663 |
+
# # np.append( np.append( 0., np.diff( self.T ) ), 0.) / self.dphi )
|
| 664 |
+
# return ( 1E-15 * -2 * pi * np.cos(self.phi_stag) * const.a**2 * self.K *
|
| 665 |
+
# np.append( np.append( 0., np.diff( self.T ) ), 0.) / self.dphi )
|
| 666 |
+
#
|
| 667 |
+
# def heat_transport_convergence( self ):
|
| 668 |
+
# '''Returns instantaneous convergence of heat transport in units of W / m^2.'''
|
| 669 |
+
# return ( -1./(2*pi*const.a**2*np.cos(self.phi)) * np.diff( 1.E15*self.heat_transport() )
|
| 670 |
+
# / np.diff(self.phi_stag) )
|
| 671 |
+
#
|
| 672 |
+
# def inferred_heat_transport( self ):
|
| 673 |
+
# '''Returns the inferred heat transport (in PW) by integrating the TOA energy imbalance from pole to pole.'''
|
| 674 |
+
# return ( 1E-15 * 2 * pi * const.a**2 * integrate.cumtrapz( np.cos(self.phi)*self.net_radiation,
|
| 675 |
+
# x=self.phi, initial=0. ) )
|
| 676 |
+
#
|
| 677 |
+
# def find_icelines( self ):
|
| 678 |
+
# '''Returns the instantaneous latitudes of any ice edges.'''
|
| 679 |
+
# # This probably won't work in cases with multiple ice lines per hemisphere!
|
| 680 |
+
# # Revise!
|
| 681 |
+
# iceindices = np.squeeze( np.where( self.T < self.Tf ) )
|
| 682 |
+
# if iceindices.size == 0:
|
| 683 |
+
# return 90.
|
| 684 |
+
# elif iceindices.size == self.lat.size:
|
| 685 |
+
# return 0.
|
| 686 |
+
# else:
|
| 687 |
+
# icelines = np.squeeze( np.where( np.diff(iceindices)>1) )
|
| 688 |
+
# icelat1 = self.lat_stag[ iceindices[icelines]+1 ]
|
| 689 |
+
# icelat2 = self.lat_stag[ iceindices[icelines+1] ]
|
| 690 |
+
# return icelat1, icelat2
|
| 691 |
+
#
|
| 692 |
+
# def global_mean( self, field ):
|
| 693 |
+
# '''Compute the area-weighted global mean of a vector field on the latitude grid.'''
|
| 694 |
+
# #return np.sum( field * np.cos( self.phi ) ) / np.sum( np.cos( self.phi ) )
|
| 695 |
+
# return global_mean( field, self.phi )
|
| 696 |
+
#
|
| 697 |
+
# def global_mean_temperature( self ):
|
| 698 |
+
# '''Convenience method to compute global mean temperature.'''
|
| 699 |
+
# return self.global_mean( self.T )
|
| 700 |
+
#==============================================================================
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
#==============================================================================
|
| 705 |
+
# class EBM_landocean( EBM_seasonal ):
|
| 706 |
+
# '''A model with both land and ocean, based on North and Coakley (1979)
|
| 707 |
+
# Essentially just invokes two different EBM_seasonal objects, one for ocean, one for land.
|
| 708 |
+
# '''
|
| 709 |
+
# def __str__(self):
|
| 710 |
+
# return ( "Instance of EBM_landocean class with " + str(self.num_points) + " latitude points." )
|
| 711 |
+
#
|
| 712 |
+
# def __init__( self, num_points = 90 ):
|
| 713 |
+
# super(EBM_landocean,self).__init__( num_points )
|
| 714 |
+
# self.land_ocean_exchange_parameter = 1.0 # in W/m2/K
|
| 715 |
+
#
|
| 716 |
+
# self.land = EBM_seasonal( num_points )
|
| 717 |
+
# self.land.make_insolation_array( self.orb )
|
| 718 |
+
# self.land.Tf = 0.
|
| 719 |
+
# self.land.set_timestep( timestep = self.timestep )
|
| 720 |
+
# self.land.set_water_depth( water_depth = 2. )
|
| 721 |
+
#
|
| 722 |
+
# self.ocean = EBM_seasonal( num_points )
|
| 723 |
+
# self.ocean.make_insolation_array( self.orb )
|
| 724 |
+
# self.ocean.Tf = -2.
|
| 725 |
+
# self.ocean.set_timestep( timestep = self.timestep )
|
| 726 |
+
# self.ocean.set_water_depth( water_depth = 75. )
|
| 727 |
+
#
|
| 728 |
+
# self.land_fraction = 0.3 * np.ones_like( self.land.phi )
|
| 729 |
+
# self.C_ratio = self.land.water_depth / self.ocean.water_depth
|
| 730 |
+
# self.T = self.zonal_mean_temperature()
|
| 731 |
+
#
|
| 732 |
+
# def zonal_mean_temperature( self ):
|
| 733 |
+
# return self.land.T * self.land_fraction + self.ocean.T * (1-self.land_fraction)
|
| 734 |
+
#
|
| 735 |
+
# def step_forward( self ):
|
| 736 |
+
# # note.. this simple implementation is possibly problematic
|
| 737 |
+
# # because the exchange should really occur simultaneously with radiation
|
| 738 |
+
# # and before the implicit heat diffusion
|
| 739 |
+
# self.exchange = (self.ocean.T - self.land.T) * self.land_ocean_exchange_parameter
|
| 740 |
+
# self.land.step_forward()
|
| 741 |
+
# self.ocean.step_forward()
|
| 742 |
+
# self.land.T += self.exchange / self.land_fraction * self.land.delta_time_over_C
|
| 743 |
+
# self.ocean.T -= self.exchange / (1-self.land_fraction) * self.ocean.delta_time_over_C
|
| 744 |
+
# self.T = self.zonal_mean_temperature()
|
| 745 |
+
# self.update_time()
|
| 746 |
+
#
|
| 747 |
+
# # This code should be more accurate, but it's ungainly and seems to produce just about the same result.
|
| 748 |
+
# #def step_forward( self ):
|
| 749 |
+
# # self.exchange = (self.ocean.T - self.land.T) * self.land_ocean_exchange_parameter
|
| 750 |
+
# # self.land.compute_radiation( )
|
| 751 |
+
# # self.ocean.compute_radiation( )
|
| 752 |
+
# # Trad_land = ( self.land.T + ( self.land.net_radiation + self.exchange / self.land_fraction )
|
| 753 |
+
# # * self.land.delta_time_over_C )
|
| 754 |
+
# # Trad_ocean = ( self.ocean.T + ( self.ocean.net_radiation - self.exchange / (1-self.land_fraction) )
|
| 755 |
+
# # * self.ocean.delta_time_over_C )
|
| 756 |
+
# # self.land.T = solve_banded((1,1), self.land.diffTriDiag, Trad_land )
|
| 757 |
+
# # self.ocean.T = solve_banded((1,1), self.ocean.diffTriDiag, Trad_ocean )
|
| 758 |
+
# # self.T = self.zonal_mean_temperature()
|
| 759 |
+
# # self.land.update_time()
|
| 760 |
+
# # self.ocean.update_time()
|
| 761 |
+
# # self.update_time()
|
| 762 |
+
#
|
| 763 |
+
# def integrate_years(self, years=1.0, verbose=True ):
|
| 764 |
+
# # Here we make sure that both sub-models have the current insolation.
|
| 765 |
+
# self.land.make_insolation_array( self.orb )
|
| 766 |
+
# self.ocean.make_insolation_array( self.orb )
|
| 767 |
+
# super(EBM_landocean,self).integrate_years( years, verbose )
|
| 768 |
+
#==============================================================================
|
| 769 |
+
|
| 770 |
+
|
| 771 |
+
# To do:
|
| 772 |
+
# - use integrated positive degree days to calculate implicit ice sheet melt potential
|
| 773 |
+
# - also use these to set up a version of the model with vegetation-albedo feedback
|
| 774 |
+
# - Create option to have specified extra ocean heat transport in the ocean component
|
| 775 |
+
# - Create a default land-fraction that looks more like reality for the land-ocean model
|
| 776 |
+
# - add diffusion of moist static energy
|
| 777 |
+
# (would require re-computing the diffusion operator at each timestep, probably somewhat slower)
|
| 778 |
+
|
| 779 |
+
#==============================================================================
|
| 780 |
+
#
|
| 781 |
+
# class EBM_annual_moist( EBM_annual ):
|
| 782 |
+
# def __str__(self):
|
| 783 |
+
# return ( "Instance of EBM_annual_moist class with " + str(self.num_points) + " latitude points \n" +
|
| 784 |
+
# "and global mean temperature " + str(self.global_mean_temperature()) + " degrees C.")
|
| 785 |
+
#
|
| 786 |
+
# def __init__( self, num_points = 90 ):
|
| 787 |
+
# _EBM.__init__( self, num_points )
|
| 788 |
+
# self.K0 = self.K # constant
|
| 789 |
+
# self.Kperdegree = self.K0/20. # 5% increase per degree
|
| 790 |
+
# self.Tref = 15.
|
| 791 |
+
# self.set_diffusivity( K = self.compute_K() )
|
| 792 |
+
#
|
| 793 |
+
# def compute_K(self):
|
| 794 |
+
# # formula to compute diffusivity, linear in global mean temperature
|
| 795 |
+
# return self.K0 + self.Kperdegree * (self.global_mean_temperature()-self.Tref)
|
| 796 |
+
#
|
| 797 |
+
# def step_forward( self ):
|
| 798 |
+
# # set the diffusivity, depends on global mean temperature
|
| 799 |
+
# self.set_diffusivity( K = self.compute_K() )
|
| 800 |
+
# _EBM.step_forward(self)
|
| 801 |
+
#==============================================================================
|
climlab/source/climlab/model/stommelbox.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## NEED TO FIX THE PASSING OF INITIAL PARAMETERS
|
| 2 |
+
# especially timestep
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
# how easy is to implement the Stommel 1961 box model in climlab?
|
| 6 |
+
# currently... it still requires a fair bit of code:
|
| 7 |
+
import numpy as np
|
| 8 |
+
from climlab.process.time_dependent_process import TimeDependentProcess
|
| 9 |
+
from climlab.domain import domain, field
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
box = domain.box_model_domain()
|
| 13 |
+
print(box.shape)
|
| 14 |
+
|
| 15 |
+
# initial condition
|
| 16 |
+
x = field.Field([1.,0.], domain=box)
|
| 17 |
+
y = field.Field([1.,1.], domain=box)
|
| 18 |
+
state = {'x':x, 'y':y}
|
| 19 |
+
# define the process
|
| 20 |
+
class StommelBox(TimeDependentProcess):
|
| 21 |
+
def _compute(self):
|
| 22 |
+
x = self.state['x']
|
| 23 |
+
y = self.state['y']
|
| 24 |
+
term = np.abs(-y + self.param['R']*x) / self.param['lam']
|
| 25 |
+
tendencies = {}
|
| 26 |
+
tendencies['y'] = (1 - y - y * term)
|
| 27 |
+
tendencies['x'] = (self.param['delta'] * (1 - x) - x * term)
|
| 28 |
+
return tendencies
|
| 29 |
+
|
| 30 |
+
# make a parameter dictionary
|
| 31 |
+
param = {'R': 2., 'lam': 1., 'delta': 1., 'timestep':0.01}
|
| 32 |
+
# instantiate the process
|
| 33 |
+
boxmodel = StommelBox(state=state, **param)
|
| 34 |
+
# change the timestep
|
| 35 |
+
# boxmodel.set_timestep(num_steps_per_year=1E9)
|
| 36 |
+
boxmodel.timestep *= 2
|
climlab/source/climlab/process/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''The base classes for all climlab processes.'''
|
| 2 |
+
from .process import Process, process_like, get_axes
|
| 3 |
+
from .time_dependent_process import TimeDependentProcess, couple
|
| 4 |
+
from .implicit import ImplicitProcess
|
| 5 |
+
from .diagnostic import DiagnosticProcess
|
| 6 |
+
from .energy_budget import EnergyBudget
|
| 7 |
+
from .external_forcing import ExternalForcing
|
| 8 |
+
from .limiter import Limiter
|
climlab/source/climlab/process/diagnostic.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .time_dependent_process import TimeDependentProcess
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class DiagnosticProcess(TimeDependentProcess):
|
| 5 |
+
"""A parent class for all processes that are strictly diagnostic,
|
| 6 |
+
namely that do **not** contribute directly to tendencies of state variables.
|
| 7 |
+
|
| 8 |
+
During initialization following attribute is set:
|
| 9 |
+
|
| 10 |
+
:ivar time_type: is set to ``'diagnostic'``
|
| 11 |
+
:vartype time_type: str
|
| 12 |
+
|
| 13 |
+
"""
|
| 14 |
+
def __init__(self, **kwargs):
|
| 15 |
+
super(DiagnosticProcess, self).__init__(**kwargs)
|
| 16 |
+
self.time_type = 'diagnostic'
|
climlab/source/climlab/process/energy_budget.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from .time_dependent_process import TimeDependentProcess
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class EnergyBudget(TimeDependentProcess):
|
| 6 |
+
r"""A parent class for explicit energy budget processes.
|
| 7 |
+
|
| 8 |
+
This class solves equations that include a heat capacitiy term like
|
| 9 |
+
:math:`C \frac{dT}{dt} = \textrm{flux convergence}`
|
| 10 |
+
|
| 11 |
+
In an Energy Balance Model with model state :math:`T` this equation
|
| 12 |
+
will look like this:
|
| 13 |
+
|
| 14 |
+
.. math::
|
| 15 |
+
|
| 16 |
+
C \frac{dT}{dt} = R\downarrow - R\uparrow - H \\
|
| 17 |
+
\frac{dT}{dt} = \frac{R\downarrow}{C} - \frac{R\uparrow}{C} - \frac{H}{C}
|
| 18 |
+
|
| 19 |
+
Every EnergyBudget object has a ``heating_rate`` dictionary with items
|
| 20 |
+
corresponding to each state variable. The heating rate accounts the actual
|
| 21 |
+
heating of a subprocess, namely the contribution to the energy budget
|
| 22 |
+
of :math:`R\\downarrow, R\\uparrow` and :math:`H` in this case.
|
| 23 |
+
The temperature tendencies for each subprocess are then calculated
|
| 24 |
+
through dividing the heating rate by the heat capacitiy :math:`C`.
|
| 25 |
+
|
| 26 |
+
**Initialization parameters** \n
|
| 27 |
+
|
| 28 |
+
An instance of ``EnergyBudget`` is initialized with the forwarded
|
| 29 |
+
keyword arguments ``**kwargs`` of the corresponding children classes.
|
| 30 |
+
|
| 31 |
+
**Object attributes** \n
|
| 32 |
+
|
| 33 |
+
Additional to the parent class
|
| 34 |
+
:class:`~climlab.process.timedependentprocess.TimeDependentProcess`
|
| 35 |
+
following object attributes are generated or modified during initialization:
|
| 36 |
+
|
| 37 |
+
:ivar str time_type: is set to ``'explicit'``
|
| 38 |
+
:ivar dict heating_rate: energy share for given subprocess in unit
|
| 39 |
+
:math:`\textrm{W}/ \textrm{m}^2` stored
|
| 40 |
+
in a dictionary sorted by model states
|
| 41 |
+
|
| 42 |
+
"""
|
| 43 |
+
def __init__(self, **kwargs):
|
| 44 |
+
super(EnergyBudget, self).__init__(**kwargs)
|
| 45 |
+
self.time_type = 'explicit'
|
| 46 |
+
self.heating_rate = {}
|
| 47 |
+
|
| 48 |
+
def _compute_heating_rates(self):
|
| 49 |
+
"""Computes energy flux convergences to get heating rates in unit
|
| 50 |
+
:math:`\\textrm{W}/ \\textrm{m}^2`.
|
| 51 |
+
|
| 52 |
+
This method should be over-ridden by daughter classes.
|
| 53 |
+
|
| 54 |
+
"""
|
| 55 |
+
for varname in list(self.state.keys()):
|
| 56 |
+
self.heating_rate[varname] = self.state[varname] * 0.
|
| 57 |
+
|
| 58 |
+
def _temperature_tendencies(self):
|
| 59 |
+
self._compute_heating_rates()
|
| 60 |
+
tendencies = {}
|
| 61 |
+
for varname, value in self.state.items():
|
| 62 |
+
#C = self.state_domain[varname].heat_capacity
|
| 63 |
+
C = value.domain.heat_capacity
|
| 64 |
+
try: # there may be state variables without heating rates
|
| 65 |
+
tendencies[varname] = (self.heating_rate[varname] / C)
|
| 66 |
+
except:
|
| 67 |
+
pass
|
| 68 |
+
return tendencies
|
| 69 |
+
|
| 70 |
+
def _compute(self):
|
| 71 |
+
tendencies = self._temperature_tendencies()
|
| 72 |
+
return tendencies
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class ExternalEnergySource(EnergyBudget):
|
| 76 |
+
"""A fixed energy source or sink to be specified by the user.
|
| 77 |
+
|
| 78 |
+
**Object attributes** \n
|
| 79 |
+
|
| 80 |
+
Additional to the parent class :class:`~climlab.process.energy_budget.EnergyBudget`
|
| 81 |
+
the following object attribute is modified during initialization:
|
| 82 |
+
|
| 83 |
+
:ivar dict heating_rate: energy share dictionary for this subprocess
|
| 84 |
+
is set to zero for every model state.
|
| 85 |
+
|
| 86 |
+
After initialization the user should modify the fields in the
|
| 87 |
+
``heating_rate`` dictionary, which contain heating rates in
|
| 88 |
+
unit :math:`\\textrm{W}/ \\textrm{m}^2` for all state variables.
|
| 89 |
+
|
| 90 |
+
:Example:
|
| 91 |
+
|
| 92 |
+
Creating an Energy Balance Model with a uniform external energy source
|
| 93 |
+
of :math:`10 \\ \\textrm{W}/ \\textrm{m}^2` for all latitudes::
|
| 94 |
+
|
| 95 |
+
>>> import climlab
|
| 96 |
+
>>> from climlab.process.energy_budget import ExternalEnergySource
|
| 97 |
+
>>> import numpy as np
|
| 98 |
+
|
| 99 |
+
>>> # create model & external energy subprocess
|
| 100 |
+
>>> model = climlab.EBM(num_lat=36)
|
| 101 |
+
>>> ext_en = ExternalEnergySource(state= model.state,**model.param)
|
| 102 |
+
|
| 103 |
+
>>> # modify external energy rate
|
| 104 |
+
>>> ext_en.heating_rate.keys()
|
| 105 |
+
['Ts']
|
| 106 |
+
|
| 107 |
+
>>> np.squeeze(ext_en.heating_rate['Ts'])
|
| 108 |
+
Field([-0., -0., -0., -0., -0., -0., -0., -0., -0., 0., 0., 0., 0.,
|
| 109 |
+
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
|
| 110 |
+
0., -0., -0., -0., -0., -0., -0., -0., -0., -0.])
|
| 111 |
+
|
| 112 |
+
>>> ext_en.heating_rate['Ts'][:]=10
|
| 113 |
+
|
| 114 |
+
>>> np.squeeze(ext_en.heating_rate['Ts'])
|
| 115 |
+
Field([ 10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10.,
|
| 116 |
+
10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10.,
|
| 117 |
+
10., 10., 10., 10., 10., 10., 10., 10., 10., 10., 10.,
|
| 118 |
+
10., 10., 10.])
|
| 119 |
+
|
| 120 |
+
>>> # add subprocess to model
|
| 121 |
+
>>> model.add_subprocess('ext_energy',ext_en)
|
| 122 |
+
|
| 123 |
+
>>> print model
|
| 124 |
+
climlab Process of type <class 'climlab.model.ebm.EBM'>.
|
| 125 |
+
State variables and domain shapes:
|
| 126 |
+
Ts: (36, 1)
|
| 127 |
+
The subprocess tree:
|
| 128 |
+
top: <class 'climlab.model.ebm.EBM'>
|
| 129 |
+
diffusion: <class 'climlab.dynamics.diffusion.MeridionalDiffusion'>
|
| 130 |
+
LW: <class 'climlab.radiation.AplusBT.AplusBT'>
|
| 131 |
+
ext_energy: <class 'climlab.process.energy_budget.ExternalEnergySource'>
|
| 132 |
+
albedo: <class 'climlab.surface.albedo.StepFunctionAlbedo'>
|
| 133 |
+
iceline: <class 'climlab.surface.albedo.Iceline'>
|
| 134 |
+
cold_albedo: <class 'climlab.surface.albedo.ConstantAlbedo'>
|
| 135 |
+
warm_albedo: <class 'climlab.surface.albedo.P2Albedo'>
|
| 136 |
+
insolation: <class 'climlab.radiation.insolation.P2Insolation'>
|
| 137 |
+
|
| 138 |
+
"""
|
| 139 |
+
def __init__(self, **kwargs):
|
| 140 |
+
super(ExternalEnergySource, self).__init__(**kwargs)
|
| 141 |
+
for varname in list(self.state.keys()):
|
| 142 |
+
self.heating_rate[varname] = self.state[varname] * 0.
|
| 143 |
+
|
| 144 |
+
def _compute_heating_rates(self):
|
| 145 |
+
pass
|
climlab/source/climlab/process/external_forcing.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .time_dependent_process import TimeDependentProcess
|
| 2 |
+
|
| 3 |
+
class ExternalForcing(TimeDependentProcess):
|
| 4 |
+
"""A Process class for user-defined tendencies of state variables.
|
| 5 |
+
Useful for combining some prescribed external forcing with an interactive model.
|
| 6 |
+
|
| 7 |
+
:Example:
|
| 8 |
+
The user can invoke the process on a dicionary of state variables ``mystate`` like this::
|
| 9 |
+
|
| 10 |
+
myforcing = climlab.process.ExternalForcing(state=mystate)
|
| 11 |
+
|
| 12 |
+
and then set the desired tendencies in the dictionary ``myforcing.forcing_tendencies``,
|
| 13 |
+
in units of [state variable unit] per second.
|
| 14 |
+
"""
|
| 15 |
+
def __init__(self,**kwargs):
|
| 16 |
+
super(ExternalForcing, self).__init__(**kwargs)
|
| 17 |
+
self.forcing_tendencies = {}
|
| 18 |
+
for var in self.state:
|
| 19 |
+
self.forcing_tendencies[var] = 0. * self.state[var]
|
| 20 |
+
|
| 21 |
+
def _compute(self):
|
| 22 |
+
return self.forcing_tendencies
|
climlab/source/climlab/process/implicit.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .time_dependent_process import TimeDependentProcess
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class ImplicitProcess(TimeDependentProcess):
|
| 6 |
+
"""A parent class for modules that use implicit time discretization.
|
| 7 |
+
|
| 8 |
+
During initialization following attributes are intitialized:
|
| 9 |
+
|
| 10 |
+
:ivar time_type: is set to ``'implicit'``
|
| 11 |
+
:vartype time_type: str
|
| 12 |
+
|
| 13 |
+
:ivar adjustment: the model state adjustments due to this implicit
|
| 14 |
+
subprocess
|
| 15 |
+
:vartype adjustment: dict
|
| 16 |
+
|
| 17 |
+
"""
|
| 18 |
+
def __init__(self, **kwargs):
|
| 19 |
+
super(ImplicitProcess, self).__init__(**kwargs)
|
| 20 |
+
self.time_type = 'implicit'
|
| 21 |
+
self.adjustment = {}
|
| 22 |
+
|
| 23 |
+
def _compute(self):
|
| 24 |
+
"""Computes the state variable tendencies in time for implicit processes.
|
| 25 |
+
|
| 26 |
+
To calculate the new state the :func:`_implicit_solver()` method is
|
| 27 |
+
called for daughter classes. This however returns the new state of the
|
| 28 |
+
variables, not just the tendencies. Therefore, the adjustment is
|
| 29 |
+
calculated which is the difference between the new and the old state
|
| 30 |
+
and stored in the object's attribute adjustment.
|
| 31 |
+
|
| 32 |
+
Calculating the new model states through solving the matrix problem
|
| 33 |
+
already includes the multiplication with the timestep. The derived
|
| 34 |
+
adjustment is divided by the timestep to calculate the implicit
|
| 35 |
+
subprocess tendencies, which can be handeled by the
|
| 36 |
+
:func:`~climlab.process.time_dependent_process.TimeDependentProcess.compute`
|
| 37 |
+
method of the parent
|
| 38 |
+
:class:`~climlab.process.time_dependent_process.TimeDependentProcess` class.
|
| 39 |
+
|
| 40 |
+
:ivar dict adjustment: holding all state variables' adjustments
|
| 41 |
+
of the implicit process which are the
|
| 42 |
+
differences between the new states (which have
|
| 43 |
+
been solved through matrix inversion) and the
|
| 44 |
+
old states.
|
| 45 |
+
|
| 46 |
+
"""
|
| 47 |
+
newstate = self._implicit_solver()
|
| 48 |
+
adjustment = {}
|
| 49 |
+
tendencies = {}
|
| 50 |
+
for name, var in self.state.items():
|
| 51 |
+
adjustment[name] = newstate[name] - var
|
| 52 |
+
tendencies[name] = adjustment[name] / self.timestep_in_seconds
|
| 53 |
+
# express the adjustment (already accounting for the finite time step)
|
| 54 |
+
# as a tendency per unit time, so that it can be applied along with explicit
|
| 55 |
+
self.adjustment = adjustment
|
| 56 |
+
self._update_diagnostics(newstate)
|
| 57 |
+
return tendencies
|
| 58 |
+
|
| 59 |
+
def _update_diagnostics(self, newstate):
|
| 60 |
+
'''This method is called each timestep after the new state is computed
|
| 61 |
+
with the implicit solver. Daughter classes can implement this method to
|
| 62 |
+
compute any diagnostic quantities using the new state.'''
|
| 63 |
+
pass
|
climlab/source/climlab/process/limiter.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from climlab.process import TimeDependentProcess
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Limiter(TimeDependentProcess):
|
| 6 |
+
'''A process that implements strict bounds on the allowable range of values of state variables.
|
| 7 |
+
Values outside the given bounds are adjusted back to the bounding value at each timestep.
|
| 8 |
+
|
| 9 |
+
Bounding values are stored in a dictionary ``.bounds`` which has identical keys to ``.state``
|
| 10 |
+
|
| 11 |
+
Each item in the ``.bounds`` dict is another dict containing the keys ``'minimum'`` and ``'maximum'``.
|
| 12 |
+
By default these are initialized to ``None`` and ``np.inf`` respectively,
|
| 13 |
+
which means the process produces zero adjustment.
|
| 14 |
+
|
| 15 |
+
The user needs to specify desired minimum and/or maximum values for each state variable.
|
| 16 |
+
These can be specified at process creation time using the keyword argument ``bounds``,
|
| 17 |
+
or modified in-place (see example below).
|
| 18 |
+
|
| 19 |
+
For diagnostic purposes, we can always access the adjustments (in state variable units)
|
| 20 |
+
and the tendencies (in state variable units per second) produced by the Limiter
|
| 21 |
+
just like any other process (see example below)
|
| 22 |
+
|
| 23 |
+
Example use: an EBM with surface temperature limited to <= 25 degrees C::
|
| 24 |
+
|
| 25 |
+
import climlab
|
| 26 |
+
ebm = climlab.EBM()
|
| 27 |
+
# Create the Limiter process, and make sure it has a matching timestep
|
| 28 |
+
mylimiter = climlab.process.Limiter(state=ebm.state, timestep=ebm.timestep)
|
| 29 |
+
# Now set our desired upper bound on the temperature
|
| 30 |
+
mylimiter.bounds['Ts']['maximum'] = 25.
|
| 31 |
+
# And couple it to the rest of the model
|
| 32 |
+
ebm.add_subprocess('TempLimiter', mylimiter)
|
| 33 |
+
# Take a step forward and verify that surface temperatures do not exceed 25 degrees C
|
| 34 |
+
ebm.step_forward()
|
| 35 |
+
assert np.all(ebm.Ts<=25)
|
| 36 |
+
# Examine the tendencies (in degrees C / second) produced by the Limiter:
|
| 37 |
+
# They should be zero everywhere the temperaure is less than 25 degrees:
|
| 38 |
+
print(ebm.subprocess['TempLimiter'].tendencies)
|
| 39 |
+
'''
|
| 40 |
+
def __init__(self, bounds={}, **kwargs):
|
| 41 |
+
super(Limiter, self).__init__(**kwargs)
|
| 42 |
+
# Initialize bounds for all state variables. `None` means no bounds
|
| 43 |
+
# By default the process should produce zero adjustment
|
| 44 |
+
# Note that in numpy 2.0 and above, we can do this by setting `None` on both bounds
|
| 45 |
+
# But in numpy < 2.0 that's not allowed, so we use `np.inf` as upper bound instead
|
| 46 |
+
self.bounds = {}
|
| 47 |
+
for name in self.state:
|
| 48 |
+
self.bounds[name] = {'minimum': None, 'maximum': np.inf}
|
| 49 |
+
# Now override with any user-specified values
|
| 50 |
+
for name, thisbounddict in bounds.items():
|
| 51 |
+
if 'minimum' in thisbounddict:
|
| 52 |
+
self.bounds[name]['minimum'] = thisbounddict['minimum']
|
| 53 |
+
if 'maximum' in thisbounddict:
|
| 54 |
+
self.bounds[name]['maximum'] = thisbounddict['maximum']
|
| 55 |
+
self.time_type = 'adjustment'
|
| 56 |
+
self.adjustment = {}
|
| 57 |
+
|
| 58 |
+
def _compute(self):
|
| 59 |
+
for name, value in self.state.items():
|
| 60 |
+
min = self.bounds[name]['minimum']
|
| 61 |
+
max = self.bounds[name]['maximum']
|
| 62 |
+
clipped = np.clip(value, a_min=min, a_max=max)
|
| 63 |
+
self.adjustment[name] = clipped - value
|
| 64 |
+
return self.adjustment
|
climlab/source/climlab/process/process.py
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
#==============================================================================
|
| 3 |
+
# Principles of the new `climlab` API design:
|
| 4 |
+
#
|
| 5 |
+
# * `climlab.Process` object has several iterable dictionaries of named,
|
| 6 |
+
# gridded variables:
|
| 7 |
+
#
|
| 8 |
+
# * `process.state`
|
| 9 |
+
#
|
| 10 |
+
# * state variables, usually time-dependent
|
| 11 |
+
#
|
| 12 |
+
# - `process.input`
|
| 13 |
+
# - boundary conditions and other gridded quantities independent of the
|
| 14 |
+
# `process`
|
| 15 |
+
# - often set by a parent `process`
|
| 16 |
+
# - `process.param` (which are basically just scalar `input`)
|
| 17 |
+
# - `process.tendencies`
|
| 18 |
+
# - iterable `dict` of time-tendencies (d/dt) for each state variable
|
| 19 |
+
# - `process.diagnostics`
|
| 20 |
+
# - any quantity derived from current state
|
| 21 |
+
# - The `process` is fully described by contents of `state`, `input` and `param`
|
| 22 |
+
# dictionaries. `tendencies` and `diagnostics` are always computable from current
|
| 23 |
+
# state.
|
| 24 |
+
# - `climlab` will remain (as much as possible) agnostic about the data formats
|
| 25 |
+
# - Variables within the dictionaries will behave as `numpy.ndarray` objects
|
| 26 |
+
# - Grid information and other domain details accessible as attributes
|
| 27 |
+
# of each variable
|
| 28 |
+
# - e.g. Tatm.lat
|
| 29 |
+
# - Shortcuts like `process.lat` will work where these are unambiguous
|
| 30 |
+
# - Many variables will be accessible as process attributes `process.name`
|
| 31 |
+
# - this restricts to unique field names in the above dictionaries
|
| 32 |
+
# - There may be other dictionaries that do have name conflicts
|
| 33 |
+
# - e.g. dictionary of tendencies, with same keys as `process.state`
|
| 34 |
+
# - These will *not* be accessible as `process.name`
|
| 35 |
+
# - but *will* be accessible as `process.dict_name.name`
|
| 36 |
+
# (as well as regular dict interface)
|
| 37 |
+
# - There will be a dictionary of named subprocesses `process.subprocess`
|
| 38 |
+
# - Each item in subprocess dict will itself be a `climlab.Process` object
|
| 39 |
+
# - For convenience with interactive work, each subprocess should be accessible
|
| 40 |
+
# as `process.subprocess.name` as well as `process.subprocess['name']`
|
| 41 |
+
# - `process.compute()` is a method that computes tendencies (d/dt)
|
| 42 |
+
# - returns a dictionary of tendencies for all state variables
|
| 43 |
+
# - keys for this dictionary are same as keys of state dictionary
|
| 44 |
+
# - tendency dictionary is the total tendency including all subprocesses
|
| 45 |
+
# - method only computes d/dt, does not apply changes
|
| 46 |
+
# - thus method is relatively independent of numerical scheme
|
| 47 |
+
# - may need to make exception for implicit scheme?
|
| 48 |
+
# - method *will* update variables in `process.diagnostic`
|
| 49 |
+
# - will also *gather all diagnostics* from `subprocesses`
|
| 50 |
+
# - `process.step_forward()` updates the state variables
|
| 51 |
+
# - calls `process.compute()` to get current tendencies
|
| 52 |
+
# - implements a particular time-stepping scheme
|
| 53 |
+
# - user interface is agnostic about numerical scheme
|
| 54 |
+
# - `process.integrate_years()` etc will automate time-stepping
|
| 55 |
+
# - also computation of time-average diagnostics.
|
| 56 |
+
# - Every `subprocess` should work independently of its parent `process` given
|
| 57 |
+
# appropriate `input`.
|
| 58 |
+
# - investigating an individual `process` (possibly with its own
|
| 59 |
+
# `subprocesses`) isolated from its parent needs to be as simple as doing:
|
| 60 |
+
# - `newproc = climlab.process_like(procname.subprocess['subprocname'])`
|
| 61 |
+
#
|
| 62 |
+
# - `newproc.compute()`
|
| 63 |
+
# - anything in the `input` dictionary of `subprocname` will remain fixed
|
| 64 |
+
#==============================================================================
|
| 65 |
+
|
| 66 |
+
from builtins import object
|
| 67 |
+
import time, copy
|
| 68 |
+
import numpy as np
|
| 69 |
+
from climlab.domain.field import Field
|
| 70 |
+
from climlab.domain.domain import _Domain, zonal_mean_surface
|
| 71 |
+
from climlab.utils import walk, ProcNameWarning, _make_dict
|
| 72 |
+
from climlab.utils.attrdict import AttrDict
|
| 73 |
+
from climlab.domain.xarray import state_to_xarray
|
| 74 |
+
from warnings import warn
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class Process(object):
|
| 78 |
+
"""A generic parent class for all climlab process objects.
|
| 79 |
+
Every process object has a set of state variables on a spatial grid.
|
| 80 |
+
|
| 81 |
+
For more general information about `Processes` and their role in climlab,
|
| 82 |
+
see :ref:`process_architecture` section climlab-architecture.
|
| 83 |
+
|
| 84 |
+
**Initialization parameters** \n
|
| 85 |
+
|
| 86 |
+
An instance of ``Process`` is initialized with the following
|
| 87 |
+
arguments *(for detailed information see Object attributes below)*:
|
| 88 |
+
|
| 89 |
+
:param Field state: spatial state variable for the process.
|
| 90 |
+
Set to ``None`` if not specified.
|
| 91 |
+
:param domains: domain(s) for the process
|
| 92 |
+
:type domains: :class:`~climlab.domain.domain._Domain` or dict of
|
| 93 |
+
:class:`~climlab.domain.domain._Domain`
|
| 94 |
+
:param subprocess: subprocess(es) of the process
|
| 95 |
+
:type subprocess: :class:`~climlab.process.process.Process` or dict of
|
| 96 |
+
:class:`~climlab.process.process.Process`
|
| 97 |
+
:param array lat: latitudinal points (optional)
|
| 98 |
+
:param lev: altitudinal points (optional)
|
| 99 |
+
:param int num_lat: number of latitudional points (optional)
|
| 100 |
+
:param int num_levels:
|
| 101 |
+
number of altitudinal points (optional)
|
| 102 |
+
:param dict input: collection of input quantities
|
| 103 |
+
:param bool verbose: Flag to control text output during instantiation
|
| 104 |
+
of the Process [default: True]
|
| 105 |
+
|
| 106 |
+
**Object attributes** \n
|
| 107 |
+
|
| 108 |
+
Additional to the parent class :class:`~climlab.process.process.Process`
|
| 109 |
+
following object attributes are generated during initialization:
|
| 110 |
+
|
| 111 |
+
:ivar dict domains: dictionary of process :class:`~climlab.domain.domain._Domain`
|
| 112 |
+
:ivar dict state: dictionary of process states
|
| 113 |
+
(of type :class:`~climlab.domain.field.Field`)
|
| 114 |
+
:ivar dict param: dictionary of model parameters which are given
|
| 115 |
+
through ``**kwargs``
|
| 116 |
+
:ivar dict diagnostics: a dictionary with all diagnostic variables
|
| 117 |
+
:ivar dict _input_vars: collection of input quantities like boundary conditions
|
| 118 |
+
and other gridded quantities
|
| 119 |
+
:ivar str creation_date:
|
| 120 |
+
date and time when process was created
|
| 121 |
+
:ivar subprocess: dictionary of suprocesses of the process
|
| 122 |
+
:vartype subprocess: dict of :class:`~climlab.process.process.Process`
|
| 123 |
+
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
def __str__(self):
|
| 127 |
+
str1 = 'climlab Process of type {0}. \n'.format(type(self))
|
| 128 |
+
str1 += 'State variables and domain shapes: \n'
|
| 129 |
+
for varname in list(self.state.keys()):
|
| 130 |
+
str1 += ' {0}: {1} \n'.format(varname, self.domains[varname].shape)
|
| 131 |
+
str1 += 'The subprocess tree: \n'
|
| 132 |
+
str1 += walk.process_tree(self, name=self.name)
|
| 133 |
+
return str1
|
| 134 |
+
|
| 135 |
+
def __init__(self, name='Untitled', state=None, domains=None, subprocess=None,
|
| 136 |
+
lat=None, lev=None, num_lat=None, num_levels=None,
|
| 137 |
+
input=None, verbose=True, **kwargs):
|
| 138 |
+
# verbose flag used to control text output at process creation time
|
| 139 |
+
self.verbose = verbose
|
| 140 |
+
self.name = name
|
| 141 |
+
# dictionary of domains. Keys are the domain names
|
| 142 |
+
self.domains = _make_dict(domains, _Domain)
|
| 143 |
+
# If lat is given, create a simple domains
|
| 144 |
+
if lat is not None:
|
| 145 |
+
sfc = zonal_mean_surface()
|
| 146 |
+
self.domains.update({'default': sfc})
|
| 147 |
+
# dictionary of state variables (all of type Field)
|
| 148 |
+
self.state = AttrDict()
|
| 149 |
+
states = _make_dict(state, Field)
|
| 150 |
+
for name, value in states.items():
|
| 151 |
+
self.set_state(name, value)
|
| 152 |
+
# dictionary of model parameters
|
| 153 |
+
self.param = kwargs
|
| 154 |
+
self._diag_vars = []
|
| 155 |
+
if input is None:
|
| 156 |
+
self._input_vars = []
|
| 157 |
+
else:
|
| 158 |
+
self.add_input(list(input.keys()))
|
| 159 |
+
for name, var in input:
|
| 160 |
+
self.__dict__[name] = var
|
| 161 |
+
self.creation_date = time.strftime("%a, %d %b %Y %H:%M:%S %z",
|
| 162 |
+
time.localtime())
|
| 163 |
+
# subprocess is a dictionary of any sub-processes
|
| 164 |
+
self.subprocess = AttrDict()
|
| 165 |
+
if subprocess is not None:
|
| 166 |
+
self.add_subprocesses(subprocess)
|
| 167 |
+
|
| 168 |
+
def add_subprocesses(self, procdict):
|
| 169 |
+
"""Adds a dictionary of subproceses to this process.
|
| 170 |
+
|
| 171 |
+
Calls :func:`add_subprocess` for every process given in the
|
| 172 |
+
input-dictionary. It can also pass a single process, which will
|
| 173 |
+
be given the name *default*.
|
| 174 |
+
|
| 175 |
+
:param procdict: a dictionary with process names as keys
|
| 176 |
+
:type procdict: dict
|
| 177 |
+
|
| 178 |
+
"""
|
| 179 |
+
if isinstance(procdict, Process):
|
| 180 |
+
try:
|
| 181 |
+
name = procdict.name
|
| 182 |
+
except:
|
| 183 |
+
name = 'default'
|
| 184 |
+
self.add_subprocess(name, procdict)
|
| 185 |
+
else:
|
| 186 |
+
for name, proc in procdict.items():
|
| 187 |
+
self.add_subprocess(name, proc)
|
| 188 |
+
|
| 189 |
+
def add_subprocess(self, name, proc, verbose=True):
|
| 190 |
+
"""Adds a single subprocess to this process.
|
| 191 |
+
|
| 192 |
+
:param string name: name of the subprocess
|
| 193 |
+
:param proc: a Process object
|
| 194 |
+
:type proc: :class:`~climlab.process.process.Process`
|
| 195 |
+
:raises: :exc:`ValueError`
|
| 196 |
+
if ``proc`` is not a process
|
| 197 |
+
|
| 198 |
+
:Example:
|
| 199 |
+
|
| 200 |
+
Replacing an albedo subprocess through adding a subprocess with
|
| 201 |
+
same name::
|
| 202 |
+
|
| 203 |
+
>>> from climlab.model.ebm import EBM_seasonal
|
| 204 |
+
>>> from climlab.surface.albedo import StepFunctionAlbedo
|
| 205 |
+
|
| 206 |
+
>>> # creating EBM model
|
| 207 |
+
>>> ebm_s = EBM_seasonal()
|
| 208 |
+
|
| 209 |
+
>>> print ebm_s
|
| 210 |
+
|
| 211 |
+
.. code-block:: none
|
| 212 |
+
:emphasize-lines: 8
|
| 213 |
+
|
| 214 |
+
climlab Process of type <class 'climlab.model.ebm.EBM_seasonal'>.
|
| 215 |
+
State variables and domain shapes:
|
| 216 |
+
Ts: (90, 1)
|
| 217 |
+
The subprocess tree:
|
| 218 |
+
top: <class 'climlab.model.ebm.EBM_seasonal'>
|
| 219 |
+
diffusion: <class 'climlab.dynamics.diffusion.MeridionalDiffusion'>
|
| 220 |
+
LW: <class 'climlab.radiation.AplusBT.AplusBT'>
|
| 221 |
+
albedo: <class 'climlab.surface.albedo.P2Albedo'>
|
| 222 |
+
insolation: <class 'climlab.radiation.insolation.DailyInsolation'>
|
| 223 |
+
|
| 224 |
+
::
|
| 225 |
+
|
| 226 |
+
>>> # creating and adding albedo feedback subprocess
|
| 227 |
+
>>> step_albedo = StepFunctionAlbedo(state=ebm_s.state, **ebm_s.param)
|
| 228 |
+
>>> ebm_s.add_subprocess('albedo', step_albedo)
|
| 229 |
+
>>>
|
| 230 |
+
>>> print ebm_s
|
| 231 |
+
|
| 232 |
+
.. code-block:: none
|
| 233 |
+
:emphasize-lines: 8
|
| 234 |
+
|
| 235 |
+
climlab Process of type <class 'climlab.model.ebm.EBM_seasonal'>.
|
| 236 |
+
State variables and domain shapes:
|
| 237 |
+
Ts: (90, 1)
|
| 238 |
+
The subprocess tree:
|
| 239 |
+
top: <class 'climlab.model.ebm.EBM_seasonal'>
|
| 240 |
+
diffusion: <class 'climlab.dynamics.diffusion.MeridionalDiffusion'>
|
| 241 |
+
LW: <class 'climlab.radiation.AplusBT.AplusBT'>
|
| 242 |
+
albedo: <class 'climlab.surface.albedo.StepFunctionAlbedo'>
|
| 243 |
+
iceline: <class 'climlab.surface.albedo.Iceline'>
|
| 244 |
+
cold_albedo: <class 'climlab.surface.albedo.ConstantAlbedo'>
|
| 245 |
+
warm_albedo: <class 'climlab.surface.albedo.P2Albedo'>
|
| 246 |
+
insolation: <class 'climlab.radiation.insolation.DailyInsolation'>
|
| 247 |
+
|
| 248 |
+
"""
|
| 249 |
+
if isinstance(proc, Process):
|
| 250 |
+
if name in self.subprocess and verbose:
|
| 251 |
+
warn('Process name {} is already in the subprocess dictionary. It is being replaced.'.format(name),
|
| 252 |
+
category=ProcNameWarning)
|
| 253 |
+
self.subprocess.update({name: proc})
|
| 254 |
+
self.has_process_type_list = False
|
| 255 |
+
# Add subprocess diagnostics to parent
|
| 256 |
+
# (same-named diagnostics are assumed to be additive)
|
| 257 |
+
for diagname, value in proc.diagnostics.items():
|
| 258 |
+
self.add_diagnostic(diagname, 0.*value)
|
| 259 |
+
else:
|
| 260 |
+
raise ValueError('subprocess must be Process object')
|
| 261 |
+
|
| 262 |
+
def remove_subprocess(self, name, verbose=True):
|
| 263 |
+
"""Removes a single subprocess from this process.
|
| 264 |
+
|
| 265 |
+
:param string name: name of the subprocess
|
| 266 |
+
:param bool verbose: information whether warning message
|
| 267 |
+
should be printed [default: True]
|
| 268 |
+
|
| 269 |
+
:Example:
|
| 270 |
+
|
| 271 |
+
Remove albedo subprocess from energy balance model::
|
| 272 |
+
|
| 273 |
+
>>> import climlab
|
| 274 |
+
>>> model = climlab.EBM()
|
| 275 |
+
|
| 276 |
+
>>> print model
|
| 277 |
+
climlab Process of type <class 'climlab.model.ebm.EBM'>.
|
| 278 |
+
State variables and domain shapes:
|
| 279 |
+
Ts: (90, 1)
|
| 280 |
+
The subprocess tree:
|
| 281 |
+
top: <class 'climlab.model.ebm.EBM'>
|
| 282 |
+
diffusion: <class 'climlab.dynamics.diffusion.MeridionalDiffusion'>
|
| 283 |
+
LW: <class 'climlab.radiation.AplusBT.AplusBT'>
|
| 284 |
+
albedo: <class 'climlab.surface.albedo.StepFunctionAlbedo'>
|
| 285 |
+
iceline: <class 'climlab.surface.albedo.Iceline'>
|
| 286 |
+
cold_albedo: <class 'climlab.surface.albedo.ConstantAlbedo'>
|
| 287 |
+
warm_albedo: <class 'climlab.surface.albedo.P2Albedo'>
|
| 288 |
+
insolation: <class 'climlab.radiation.insolation.P2Insolation'>
|
| 289 |
+
|
| 290 |
+
>>> model.remove_subprocess('albedo')
|
| 291 |
+
|
| 292 |
+
>>> print model
|
| 293 |
+
climlab Process of type <class 'climlab.model.ebm.EBM'>.
|
| 294 |
+
State variables and domain shapes:
|
| 295 |
+
Ts: (90, 1)
|
| 296 |
+
The subprocess tree:
|
| 297 |
+
top: <class 'climlab.model.ebm.EBM'>
|
| 298 |
+
diffusion: <class 'climlab.dynamics.diffusion.MeridionalDiffusion'>
|
| 299 |
+
LW: <class 'climlab.radiation.AplusBT.AplusBT'>
|
| 300 |
+
insolation: <class 'climlab.radiation.insolation.P2Insolation'>
|
| 301 |
+
|
| 302 |
+
"""
|
| 303 |
+
try:
|
| 304 |
+
self.subprocess.pop(name)
|
| 305 |
+
except KeyError:
|
| 306 |
+
if verbose:
|
| 307 |
+
warn('{} not found in subprocess dictionary.'.format(name))
|
| 308 |
+
self.has_process_type_list = False
|
| 309 |
+
|
| 310 |
+
def set_state(self, name, value):
|
| 311 |
+
"""Sets the variable ``name`` to a new state ``value``.
|
| 312 |
+
|
| 313 |
+
:param string name: name of the state
|
| 314 |
+
:param value: state variable
|
| 315 |
+
:type value: :class:`~climlab.domain.field.Field` or *array*
|
| 316 |
+
:raises: :exc:`ValueError`
|
| 317 |
+
if state variable ``value`` is not having a domain.
|
| 318 |
+
:raises: :exc:`ValueError`
|
| 319 |
+
if shape mismatch between existing domain and
|
| 320 |
+
new state variable.
|
| 321 |
+
|
| 322 |
+
:Example:
|
| 323 |
+
|
| 324 |
+
Resetting the surface temperature of an EBM to
|
| 325 |
+
:math:`-5 ^{\circ} \\textrm{C}` on all latitues::
|
| 326 |
+
|
| 327 |
+
>>> import climlab
|
| 328 |
+
>>> from climlab import Field
|
| 329 |
+
>>> import numpy as np
|
| 330 |
+
|
| 331 |
+
>>> # setup model
|
| 332 |
+
>>> model = climlab.EBM(num_lat=36)
|
| 333 |
+
|
| 334 |
+
>>> # create new temperature distribution
|
| 335 |
+
>>> initial = -5 * ones(size(model.lat))
|
| 336 |
+
>>> model.set_state('Ts', Field(initial, domain=model.domains['Ts']))
|
| 337 |
+
|
| 338 |
+
>>> np.squeeze(model.Ts)
|
| 339 |
+
Field([-5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5.,
|
| 340 |
+
-5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5.,
|
| 341 |
+
-5., -5., -5., -5., -5., -5., -5., -5., -5., -5.])
|
| 342 |
+
|
| 343 |
+
"""
|
| 344 |
+
if isinstance(value, Field):
|
| 345 |
+
# populate domains dictionary with domains from state variables
|
| 346 |
+
self.domains.update({name: value.domain})
|
| 347 |
+
else:
|
| 348 |
+
try:
|
| 349 |
+
thisdom = self.state[name].domain
|
| 350 |
+
domshape = thisdom.shape
|
| 351 |
+
except:
|
| 352 |
+
raise ValueError('State variable needs a domain.')
|
| 353 |
+
value = np.atleast_1d(value)
|
| 354 |
+
if value.shape == domshape:
|
| 355 |
+
value = Field(value, domain=thisdom)
|
| 356 |
+
else:
|
| 357 |
+
raise ValueError('Shape mismatch between existing domain and new state variable.')
|
| 358 |
+
# set the state dictionary
|
| 359 |
+
self.state[name] = value
|
| 360 |
+
for name, value in self.state.items():
|
| 361 |
+
#convert int dtype to float
|
| 362 |
+
if np.issubdtype(self.state[name].dtype, np.dtype('int').type):
|
| 363 |
+
value = self.state[name].astype(float)
|
| 364 |
+
self.state[name]=value
|
| 365 |
+
self.__setattr__(name, value)
|
| 366 |
+
|
| 367 |
+
def _guess_state_domains(self):
|
| 368 |
+
for name, value in self.state.items():
|
| 369 |
+
for domname, dom in self.domains.items():
|
| 370 |
+
if value.shape == dom.shape:
|
| 371 |
+
# same shape, assume it's the right domain
|
| 372 |
+
self.state_domain[name] = dom
|
| 373 |
+
|
| 374 |
+
def _add_field(self, field_type, name, value):
|
| 375 |
+
"""Adds a new field to a specified dictionary. The field is also added
|
| 376 |
+
as a process attribute. field_type can be 'input', 'diagnostics' """
|
| 377 |
+
try:
|
| 378 |
+
self.__getattribute__(field_type).update({name: value})
|
| 379 |
+
except:
|
| 380 |
+
raise ValueError('Problem with field_type %s' %field_type)
|
| 381 |
+
# Note that if process has attribute name, this will trigger The
|
| 382 |
+
# setter method for that attribute
|
| 383 |
+
self.__setattr__(name, value)
|
| 384 |
+
|
| 385 |
+
def add_diagnostic(self, name, value=None):
|
| 386 |
+
"""Create a new diagnostic variable called ``name`` for this process
|
| 387 |
+
and initialize it with the given ``value``.
|
| 388 |
+
|
| 389 |
+
Quantity is accessible in two ways:
|
| 390 |
+
|
| 391 |
+
* as a process attribute, i.e. ``proc.name``
|
| 392 |
+
* as a member of the diagnostics dictionary,
|
| 393 |
+
i.e. ``proc.diagnostics['name']``
|
| 394 |
+
|
| 395 |
+
Use attribute method to set values, e.g.
|
| 396 |
+
```proc.name = value ```
|
| 397 |
+
|
| 398 |
+
:param str name: name of diagnostic quantity to be initialized
|
| 399 |
+
:param array value: initial value for quantity [default: None]
|
| 400 |
+
|
| 401 |
+
:Example:
|
| 402 |
+
|
| 403 |
+
Add a diagnostic CO2 variable to an energy balance model::
|
| 404 |
+
|
| 405 |
+
>>> import climlab
|
| 406 |
+
>>> model = climlab.EBM()
|
| 407 |
+
|
| 408 |
+
>>> # initialize CO2 variable with value 280 ppm
|
| 409 |
+
>>> model.add_diagnostic('CO2',280.)
|
| 410 |
+
|
| 411 |
+
>>> # access variable directly or through diagnostic dictionary
|
| 412 |
+
>>> model.CO2
|
| 413 |
+
280
|
| 414 |
+
>>> model.diagnostics.keys()
|
| 415 |
+
['ASR', 'CO2', 'net_radiation', 'icelat', 'OLR', 'albedo']
|
| 416 |
+
|
| 417 |
+
"""
|
| 418 |
+
self._diag_vars.append(name)
|
| 419 |
+
self.__setattr__(name, value)
|
| 420 |
+
|
| 421 |
+
def add_input(self, name, value=None):
|
| 422 |
+
'''Create a new input variable called ``name`` for this process
|
| 423 |
+
and initialize it with the given ``value``.
|
| 424 |
+
|
| 425 |
+
Quantity is accessible in two ways:
|
| 426 |
+
|
| 427 |
+
* as a process attribute, i.e. ``proc.name``
|
| 428 |
+
* as a member of the input dictionary,
|
| 429 |
+
i.e. ``proc.input['name']``
|
| 430 |
+
|
| 431 |
+
Use attribute method to set values, e.g.
|
| 432 |
+
```proc.name = value ```
|
| 433 |
+
|
| 434 |
+
:param str name: name of diagnostic quantity to be initialized
|
| 435 |
+
:param array value: initial value for quantity [default: None]
|
| 436 |
+
'''
|
| 437 |
+
self._input_vars.append(name)
|
| 438 |
+
self.__setattr__(name, value)
|
| 439 |
+
|
| 440 |
+
def declare_input(self, inputlist):
|
| 441 |
+
'''Add the variable names in ``inputlist`` to the list of necessary inputs.'''
|
| 442 |
+
for name in inputlist:
|
| 443 |
+
self._input_vars.append(name)
|
| 444 |
+
|
| 445 |
+
def declare_diagnostics(self, diaglist):
|
| 446 |
+
'''Add the variable names in ``inputlist`` to the list of diagnostics.'''
|
| 447 |
+
for name in diaglist:
|
| 448 |
+
self._diag_vars.append(name)
|
| 449 |
+
|
| 450 |
+
def remove_diagnostic(self, name):
|
| 451 |
+
""" Removes a diagnostic from the ``process.diagnostic`` dictionary
|
| 452 |
+
and also delete the associated process attribute.
|
| 453 |
+
|
| 454 |
+
:param str name: name of diagnostic quantity to be removed
|
| 455 |
+
|
| 456 |
+
:Example:
|
| 457 |
+
|
| 458 |
+
Remove diagnostic variable 'icelat' from energy balance model::
|
| 459 |
+
|
| 460 |
+
>>> import climlab
|
| 461 |
+
>>> model = climlab.EBM()
|
| 462 |
+
|
| 463 |
+
>>> # display all diagnostic variables
|
| 464 |
+
>>> model.diagnostics.keys()
|
| 465 |
+
['ASR', 'OLR', 'net_radiation', 'albedo', 'icelat']
|
| 466 |
+
|
| 467 |
+
>>> model.remove_diagnostic('icelat')
|
| 468 |
+
>>> model.diagnostics.keys()
|
| 469 |
+
['ASR', 'OLR', 'net_radiation', 'albedo']
|
| 470 |
+
|
| 471 |
+
>>> # Watch out for subprocesses that may still want
|
| 472 |
+
>>> # to access the diagnostic 'icelat' variable !!!
|
| 473 |
+
|
| 474 |
+
"""
|
| 475 |
+
try:
|
| 476 |
+
delattr(self, name)
|
| 477 |
+
self._diag_vars.remove(name)
|
| 478 |
+
except:
|
| 479 |
+
warn('No diagnostic named {} was found.'.format(name))
|
| 480 |
+
|
| 481 |
+
def to_xarray(self, diagnostics=False, timeave=False):
|
| 482 |
+
""" Convert process variables to ``xarray.Dataset`` format.
|
| 483 |
+
|
| 484 |
+
With ``diagnostics=True``, both state and diagnostic variables are included.
|
| 485 |
+
|
| 486 |
+
Otherwise just the state variables are included.
|
| 487 |
+
|
| 488 |
+
Returns an ``xarray.Dataset`` object with all spatial axes,
|
| 489 |
+
including 'bounds' axes indicating cell boundaries in each spatial dimension.
|
| 490 |
+
|
| 491 |
+
:Example:
|
| 492 |
+
|
| 493 |
+
Create a single column radiation model and view as ``xarray`` object::
|
| 494 |
+
|
| 495 |
+
>>> import climlab
|
| 496 |
+
>>> state = climlab.column_state(num_lev=20)
|
| 497 |
+
>>> model = climlab.radiation.RRTMG(state=state)
|
| 498 |
+
|
| 499 |
+
>>> # display model state as xarray:
|
| 500 |
+
>>> model.to_xarray()
|
| 501 |
+
<xarray.Dataset>
|
| 502 |
+
Dimensions: (depth: 1, depth_bounds: 2, lev: 20, lev_bounds: 21)
|
| 503 |
+
Coordinates:
|
| 504 |
+
* depth (depth) float64 0.5
|
| 505 |
+
* depth_bounds (depth_bounds) float64 0.0 1.0
|
| 506 |
+
* lev (lev) float64 25.0 75.0 125.0 175.0 225.0 275.0 325.0 ...
|
| 507 |
+
* lev_bounds (lev_bounds) float64 0.0 50.0 100.0 150.0 200.0 250.0 ...
|
| 508 |
+
Data variables:
|
| 509 |
+
Ts (depth) float64 288.0
|
| 510 |
+
Tatm (lev) float64 200.0 204.1 208.2 212.3 216.4 220.5 224.6 ...
|
| 511 |
+
|
| 512 |
+
>>> # take a single timestep to populate the diagnostic variables
|
| 513 |
+
>>> model.step_forward()
|
| 514 |
+
>>> # Now look at the full output in xarray format
|
| 515 |
+
>>> model.to_xarray(diagnostics=True)
|
| 516 |
+
<xarray.Dataset>
|
| 517 |
+
Dimensions: (depth: 1, depth_bounds: 2, lev: 20, lev_bounds: 21)
|
| 518 |
+
Coordinates:
|
| 519 |
+
* depth (depth) float64 0.5
|
| 520 |
+
* depth_bounds (depth_bounds) float64 0.0 1.0
|
| 521 |
+
* lev (lev) float64 25.0 75.0 125.0 175.0 225.0 275.0 325.0 ...
|
| 522 |
+
* lev_bounds (lev_bounds) float64 0.0 50.0 100.0 150.0 200.0 250.0 ...
|
| 523 |
+
Data variables:
|
| 524 |
+
Ts (depth) float64 288.7
|
| 525 |
+
Tatm (lev) float64 201.3 204.0 208.0 212.0 216.1 220.2 ...
|
| 526 |
+
ASR (depth) float64 240.0
|
| 527 |
+
ASRcld (depth) float64 0.0
|
| 528 |
+
ASRclr (depth) float64 240.0
|
| 529 |
+
LW_flux_down (lev_bounds) float64 0.0 12.63 19.47 26.07 32.92 40.1 ...
|
| 530 |
+
LW_flux_down_clr (lev_bounds) float64 0.0 12.63 19.47 26.07 32.92 40.1 ...
|
| 531 |
+
LW_flux_net (lev_bounds) float64 240.1 231.2 227.6 224.1 220.5 ...
|
| 532 |
+
LW_flux_net_clr (lev_bounds) float64 240.1 231.2 227.6 224.1 220.5 ...
|
| 533 |
+
LW_flux_up (lev_bounds) float64 240.1 243.9 247.1 250.2 253.4 ...
|
| 534 |
+
LW_flux_up_clr (lev_bounds) float64 240.1 243.9 247.1 250.2 253.4 ...
|
| 535 |
+
LW_sfc (depth) float64 128.9
|
| 536 |
+
LW_sfc_clr (depth) float64 128.9
|
| 537 |
+
OLR (depth) float64 240.1
|
| 538 |
+
OLRcld (depth) float64 0.0
|
| 539 |
+
OLRclr (depth) float64 240.1
|
| 540 |
+
SW_flux_down (lev_bounds) float64 341.3 323.1 318.0 313.5 309.5 ...
|
| 541 |
+
SW_flux_down_clr (lev_bounds) float64 341.3 323.1 318.0 313.5 309.5 ...
|
| 542 |
+
SW_flux_net (lev_bounds) float64 240.0 223.3 220.2 217.9 215.9 ...
|
| 543 |
+
SW_flux_net_clr (lev_bounds) float64 240.0 223.3 220.2 217.9 215.9 ...
|
| 544 |
+
SW_flux_up (lev_bounds) float64 101.3 99.88 97.77 95.64 93.57 ...
|
| 545 |
+
SW_flux_up_clr (lev_bounds) float64 101.3 99.88 97.77 95.64 93.57 ...
|
| 546 |
+
SW_sfc (depth) float64 163.8
|
| 547 |
+
SW_sfc_clr (depth) float64 163.8
|
| 548 |
+
TdotLW (lev) float64 -1.502 -0.6148 -0.5813 -0.6173 -0.6426 ...
|
| 549 |
+
TdotLW_clr (lev) float64 -1.502 -0.6148 -0.5813 -0.6173 -0.6426 ...
|
| 550 |
+
TdotSW (lev) float64 2.821 0.5123 0.3936 0.3368 0.3174 0.3299 ...
|
| 551 |
+
TdotSW_clr (lev) float64 2.821 0.5123 0.3936 0.3368 0.3174 0.3299 ...
|
| 552 |
+
|
| 553 |
+
"""
|
| 554 |
+
if timeave and hasattr(self, 'timeave'):
|
| 555 |
+
dic = self.state.copy()
|
| 556 |
+
dic.update(self.timeave)
|
| 557 |
+
return state_to_xarray(dic)
|
| 558 |
+
elif diagnostics:
|
| 559 |
+
dic = self.state.copy()
|
| 560 |
+
dic.update(self.diagnostics)
|
| 561 |
+
return state_to_xarray(dic)
|
| 562 |
+
else:
|
| 563 |
+
return state_to_xarray(self.state)
|
| 564 |
+
|
| 565 |
+
@property
|
| 566 |
+
def diagnostics(self):
|
| 567 |
+
"""Dictionary access to all diagnostic variables
|
| 568 |
+
|
| 569 |
+
:type: dict
|
| 570 |
+
|
| 571 |
+
"""
|
| 572 |
+
diag_dict = {}
|
| 573 |
+
for key in self._diag_vars:
|
| 574 |
+
try:
|
| 575 |
+
diag_dict[key] = self.__dict__[key]
|
| 576 |
+
except:
|
| 577 |
+
pass
|
| 578 |
+
return diag_dict
|
| 579 |
+
@property
|
| 580 |
+
def input(self):
|
| 581 |
+
"""Dictionary access to all input variables
|
| 582 |
+
|
| 583 |
+
That can be boundary conditions and other gridded quantities
|
| 584 |
+
independent of the `process`
|
| 585 |
+
|
| 586 |
+
:type: dict
|
| 587 |
+
|
| 588 |
+
"""
|
| 589 |
+
input_dict = {}
|
| 590 |
+
for key in self._input_vars:
|
| 591 |
+
try:
|
| 592 |
+
input_dict[key] = getattr(self,key)
|
| 593 |
+
except:
|
| 594 |
+
pass
|
| 595 |
+
return input_dict
|
| 596 |
+
|
| 597 |
+
# Some handy shortcuts... only really make sense when there is only
|
| 598 |
+
# a single axis of that type in the process.
|
| 599 |
+
@property
|
| 600 |
+
def lat(self):
|
| 601 |
+
"""Latitude of grid centers (degrees North)
|
| 602 |
+
|
| 603 |
+
:getter: Returns the points of axis ``'lat'`` if availible in the
|
| 604 |
+
process's domains.
|
| 605 |
+
:type: array
|
| 606 |
+
:raises: :exc:`ValueError`
|
| 607 |
+
if no ``'lat'`` axis can be found.
|
| 608 |
+
|
| 609 |
+
"""
|
| 610 |
+
try:
|
| 611 |
+
for domname, dom in self.domains.items():
|
| 612 |
+
try:
|
| 613 |
+
thislat = dom.axes['lat'].points
|
| 614 |
+
except:
|
| 615 |
+
pass
|
| 616 |
+
return thislat
|
| 617 |
+
except:
|
| 618 |
+
raise ValueError('Can\'t resolve a lat axis.')
|
| 619 |
+
@property
|
| 620 |
+
def lat_bounds(self):
|
| 621 |
+
"""Latitude of grid interfaces (degrees North)
|
| 622 |
+
|
| 623 |
+
:getter: Returns the bounds of axis ``'lat'`` if availible in the
|
| 624 |
+
process's domains.
|
| 625 |
+
:type: array
|
| 626 |
+
:raises: :exc:`ValueError`
|
| 627 |
+
if no ``'lat'`` axis can be found.
|
| 628 |
+
|
| 629 |
+
"""
|
| 630 |
+
try:
|
| 631 |
+
for domname, dom in self.domains.items():
|
| 632 |
+
try:
|
| 633 |
+
thislat = dom.axes['lat'].bounds
|
| 634 |
+
except:
|
| 635 |
+
pass
|
| 636 |
+
return thislat
|
| 637 |
+
except:
|
| 638 |
+
raise ValueError('Can\'t resolve a lat axis.')
|
| 639 |
+
@property
|
| 640 |
+
def lon(self):
|
| 641 |
+
"""Longitude of grid centers (degrees)
|
| 642 |
+
|
| 643 |
+
:getter: Returns the points of axis ``'lon'`` if availible in the
|
| 644 |
+
process's domains.
|
| 645 |
+
:type: array
|
| 646 |
+
:raises: :exc:`ValueError`
|
| 647 |
+
if no ``'lon'`` axis can be found.
|
| 648 |
+
|
| 649 |
+
"""
|
| 650 |
+
try:
|
| 651 |
+
for domname, dom in self.domains.items():
|
| 652 |
+
try:
|
| 653 |
+
thislon = dom.axes['lon'].points
|
| 654 |
+
except:
|
| 655 |
+
pass
|
| 656 |
+
return thislon
|
| 657 |
+
except:
|
| 658 |
+
raise ValueError('Can\'t resolve a lon axis.')
|
| 659 |
+
@property
|
| 660 |
+
def lon_bounds(self):
|
| 661 |
+
"""Longitude of grid interfaces (degrees)
|
| 662 |
+
|
| 663 |
+
:getter: Returns the bounds of axis ``'lon'`` if availible in the
|
| 664 |
+
process's domains.
|
| 665 |
+
:type: array
|
| 666 |
+
:raises: :exc:`ValueError`
|
| 667 |
+
if no ``'lon'`` axis can be found.
|
| 668 |
+
|
| 669 |
+
"""
|
| 670 |
+
try:
|
| 671 |
+
for domname, dom in self.domains.items():
|
| 672 |
+
try:
|
| 673 |
+
thislon = dom.axes['lon'].bounds
|
| 674 |
+
except:
|
| 675 |
+
pass
|
| 676 |
+
return thislon
|
| 677 |
+
except:
|
| 678 |
+
raise ValueError('Can\'t resolve a lon axis.')
|
| 679 |
+
@property
|
| 680 |
+
def lev(self):
|
| 681 |
+
"""Pressure levels at grid centers (hPa or mb)
|
| 682 |
+
|
| 683 |
+
:getter: Returns the points of axis ``'lev'`` if availible in the
|
| 684 |
+
process's domains.
|
| 685 |
+
:type: array
|
| 686 |
+
:raises: :exc:`ValueError`
|
| 687 |
+
if no ``'lev'`` axis can be found.
|
| 688 |
+
|
| 689 |
+
"""
|
| 690 |
+
try:
|
| 691 |
+
for domname, dom in self.domains.items():
|
| 692 |
+
try:
|
| 693 |
+
thislev = dom.axes['lev'].points
|
| 694 |
+
except:
|
| 695 |
+
pass
|
| 696 |
+
return thislev
|
| 697 |
+
except:
|
| 698 |
+
raise ValueError('Can\'t resolve a lev axis.')
|
| 699 |
+
@property
|
| 700 |
+
def lev_bounds(self):
|
| 701 |
+
"""Pressure levels at grid interfaces (hPa or mb)
|
| 702 |
+
|
| 703 |
+
:getter: Returns the bounds of axis ``'lev'`` if availible in the
|
| 704 |
+
process's domains.
|
| 705 |
+
:type: array
|
| 706 |
+
:raises: :exc:`ValueError`
|
| 707 |
+
if no ``'lev'`` axis can be found.
|
| 708 |
+
|
| 709 |
+
"""
|
| 710 |
+
try:
|
| 711 |
+
for domname, dom in self.domains.items():
|
| 712 |
+
try:
|
| 713 |
+
thislev = dom.axes['lev'].bounds
|
| 714 |
+
except:
|
| 715 |
+
pass
|
| 716 |
+
return thislev
|
| 717 |
+
except:
|
| 718 |
+
raise ValueError('Can\'t resolve a lev axis.')
|
| 719 |
+
@property
|
| 720 |
+
def depth(self):
|
| 721 |
+
"""Depth at grid centers (m)
|
| 722 |
+
|
| 723 |
+
:getter: Returns the points of axis ``'depth'`` if availible in the
|
| 724 |
+
process's domains.
|
| 725 |
+
:type: array
|
| 726 |
+
:raises: :exc:`ValueError`
|
| 727 |
+
if no ``'depth'`` axis can be found.
|
| 728 |
+
|
| 729 |
+
"""
|
| 730 |
+
try:
|
| 731 |
+
for domname, dom in self.domains.items():
|
| 732 |
+
try:
|
| 733 |
+
thisdepth = dom.axes['depth'].points
|
| 734 |
+
except:
|
| 735 |
+
pass
|
| 736 |
+
return thisdepth
|
| 737 |
+
except:
|
| 738 |
+
raise ValueError('Can\'t resolve a depth axis.')
|
| 739 |
+
@property
|
| 740 |
+
def depth_bounds(self):
|
| 741 |
+
"""Depth at grid interfaces (m)
|
| 742 |
+
|
| 743 |
+
:getter: Returns the bounds of axis ``'depth'`` if availible in the
|
| 744 |
+
process's domains.
|
| 745 |
+
:type: array
|
| 746 |
+
:raises: :exc:`ValueError`
|
| 747 |
+
if no ``'depth'`` axis can be found.
|
| 748 |
+
|
| 749 |
+
"""
|
| 750 |
+
try:
|
| 751 |
+
for domname, dom in self.domains.items():
|
| 752 |
+
try:
|
| 753 |
+
thisdepth = dom.axes['depth'].bounds
|
| 754 |
+
except:
|
| 755 |
+
pass
|
| 756 |
+
return thisdepth
|
| 757 |
+
except:
|
| 758 |
+
raise ValueError('Can\'t resolve a depth axis.')
|
| 759 |
+
|
| 760 |
+
|
| 761 |
+
def process_like(proc):
|
| 762 |
+
"""Make an exact clone of a process, including state and all subprocesses.
|
| 763 |
+
|
| 764 |
+
The creation date is updated.
|
| 765 |
+
|
| 766 |
+
:param proc: process
|
| 767 |
+
:type proc: :class:`~climlab.process.process.Process`
|
| 768 |
+
:return: new process identical to the given process
|
| 769 |
+
:rtype: :class:`~climlab.process.process.Process`
|
| 770 |
+
|
| 771 |
+
:Example:
|
| 772 |
+
|
| 773 |
+
::
|
| 774 |
+
|
| 775 |
+
>>> import climlab
|
| 776 |
+
>>> from climlab.process.process import process_like
|
| 777 |
+
|
| 778 |
+
>>> model = climlab.EBM()
|
| 779 |
+
>>> model.subprocess.keys()
|
| 780 |
+
['diffusion', 'LW', 'albedo', 'insolation']
|
| 781 |
+
|
| 782 |
+
>>> albedo = model.subprocess['albedo']
|
| 783 |
+
>>> albedo_copy = process_like(albedo)
|
| 784 |
+
|
| 785 |
+
>>> albedo.creation_date
|
| 786 |
+
'Thu, 24 Mar 2016 01:32:25 +0000'
|
| 787 |
+
|
| 788 |
+
>>> albedo_copy.creation_date
|
| 789 |
+
'Thu, 24 Mar 2016 01:33:29 +0000'
|
| 790 |
+
|
| 791 |
+
"""
|
| 792 |
+
newproc = copy.deepcopy(proc)
|
| 793 |
+
newproc.creation_date = time.strftime("%a, %d %b %Y %H:%M:%S %z",
|
| 794 |
+
time.localtime())
|
| 795 |
+
return newproc
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
def get_axes(process_or_domain):
|
| 799 |
+
"""Returns a dictionary of all Axis in a domain or dictionary of domains.
|
| 800 |
+
|
| 801 |
+
:param process_or_domain: a process or a domain object
|
| 802 |
+
:type process_or_domain: :class:`~climlab.process.process.Process` or
|
| 803 |
+
:class:`~climlab.domain.domain._Domain`
|
| 804 |
+
:raises: :exc: `TypeError` if input is not or not having a domain
|
| 805 |
+
:returns: dictionary of input's Axis
|
| 806 |
+
:rtype: dict
|
| 807 |
+
|
| 808 |
+
:Example:
|
| 809 |
+
|
| 810 |
+
::
|
| 811 |
+
|
| 812 |
+
>>> import climlab
|
| 813 |
+
>>> from climlab.process.process import get_axes
|
| 814 |
+
|
| 815 |
+
>>> model = climlab.EBM()
|
| 816 |
+
|
| 817 |
+
>>> get_axes(model)
|
| 818 |
+
{'lat': <climlab.domain.axis.Axis object at 0x7ff13b9dd2d0>,
|
| 819 |
+
'depth': <climlab.domain.axis.Axis object at 0x7ff13b9dd310>}
|
| 820 |
+
|
| 821 |
+
"""
|
| 822 |
+
if isinstance(process_or_domain, Process):
|
| 823 |
+
dom = process_or_domain.domains
|
| 824 |
+
else:
|
| 825 |
+
dom = process_or_domain
|
| 826 |
+
if isinstance(dom, _Domain):
|
| 827 |
+
return dom.axes
|
| 828 |
+
elif isinstance(dom, dict):
|
| 829 |
+
axes = {}
|
| 830 |
+
for thisdom in list(dom.values()):
|
| 831 |
+
assert isinstance(thisdom, _Domain)
|
| 832 |
+
axes.update(thisdom.axes)
|
| 833 |
+
return axes
|
| 834 |
+
else:
|
| 835 |
+
raise TypeError('dom must be a domain or dictionary of domains.')
|