From 066fe58f892730c6227ad1ec89b8ba2ed8b4f0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E9=91=AB?= Date: Tue, 23 Dec 2025 08:38:08 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20init(init):=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile | 90 +++ README.en.md | 36 + README.md | 38 +- config.nifi | 11 + dockerfile | 9 + irrgiation/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 163 bytes .../crop_growth_stages.cpython-310.pyc | Bin 0 -> 1223 bytes .../dailyEvaporation.cpython-310.pyc | Bin 0 -> 7951 bytes .../__pycache__/db_connect.cpython-310.pyc | Bin 0 -> 5607 bytes .../irrigateDecisionDemo.cpython-310.pyc | Bin 0 -> 15550 bytes .../__pycache__/mathuntils.cpython-310.pyc | Bin 0 -> 9002 bytes .../__pycache__/soil_Ke.cpython-310.pyc | Bin 0 -> 3162 bytes .../weatherAndSoilDataRequest.cpython-310.pyc | Bin 0 -> 8136 bytes irrgiation/crop_growth_stages.py | 68 ++ irrgiation/dailyEvaporation.py | 365 +++++++++ irrgiation/db_connect.py | 199 +++++ irrgiation/irrigateDecisionDemo.py | 620 +++++++++++++++ irrgiation/mathuntils.py | 371 +++++++++ irrgiation/soil_Ke.py | 133 ++++ irrgiation/utils.py | 10 + irrgiation/weatherAndSoilDataRequest.py | 336 ++++++++ main.py | 42 + pyeto/__init__.py | 99 +++ pyeto/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 1488 bytes pyeto/__pycache__/_check.cpython-310.pyc | Bin 0 -> 2036 bytes pyeto/__pycache__/convert.cpython-310.pyc | Bin 0 -> 1424 bytes pyeto/__pycache__/fao.cpython-310.pyc | Bin 0 -> 27714 bytes .../__pycache__/thornthwaite.cpython-310.pyc | Bin 0 -> 4066 bytes pyeto/_check.py | 69 ++ pyeto/convert.py | 52 ++ pyeto/fao.py | 737 ++++++++++++++++++ pyeto/thornthwaite.py | 119 +++ requirements.txt | Bin 0 -> 1646 bytes 34 files changed, 3402 insertions(+), 2 deletions(-) create mode 100644 Jenkinsfile create mode 100644 README.en.md create mode 100644 config.nifi create mode 100644 dockerfile create mode 100644 irrgiation/__init__.py create mode 100644 irrgiation/__pycache__/__init__.cpython-310.pyc create mode 100644 irrgiation/__pycache__/crop_growth_stages.cpython-310.pyc create mode 100644 irrgiation/__pycache__/dailyEvaporation.cpython-310.pyc create mode 100644 irrgiation/__pycache__/db_connect.cpython-310.pyc create mode 100644 irrgiation/__pycache__/irrigateDecisionDemo.cpython-310.pyc create mode 100644 irrgiation/__pycache__/mathuntils.cpython-310.pyc create mode 100644 irrgiation/__pycache__/soil_Ke.cpython-310.pyc create mode 100644 irrgiation/__pycache__/weatherAndSoilDataRequest.cpython-310.pyc create mode 100644 irrgiation/crop_growth_stages.py create mode 100644 irrgiation/dailyEvaporation.py create mode 100644 irrgiation/db_connect.py create mode 100644 irrgiation/irrigateDecisionDemo.py create mode 100644 irrgiation/mathuntils.py create mode 100644 irrgiation/soil_Ke.py create mode 100644 irrgiation/utils.py create mode 100644 irrgiation/weatherAndSoilDataRequest.py create mode 100644 main.py create mode 100644 pyeto/__init__.py create mode 100644 pyeto/__pycache__/__init__.cpython-310.pyc create mode 100644 pyeto/__pycache__/_check.cpython-310.pyc create mode 100644 pyeto/__pycache__/convert.cpython-310.pyc create mode 100644 pyeto/__pycache__/fao.cpython-310.pyc create mode 100644 pyeto/__pycache__/thornthwaite.cpython-310.pyc create mode 100644 pyeto/_check.py create mode 100644 pyeto/convert.py create mode 100644 pyeto/fao.py create mode 100644 pyeto/thornthwaite.py create mode 100644 requirements.txt diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..ed58203 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,90 @@ +pipeline { + agent any + stages { + stage('develop build and upload') { + when { + branch "develop" + } + steps { + script { + try { + sh ''' + docker login http://172.16.102.3:30648 -u ${reg_username} -p ${reg_passwd} + docker build -t 172.16.102.3:30648/maimai/${service_name}:latest . + docker push 172.16.102.3:30648/maimai/${service_name}:latest + kubectl rollout restart deployment ${service_name} -n dev + ''' + } catch(err) { + sh 'exit 1' + } + } + } + } + stage('test build only') { + when { + expression {ref ==~ 'refs/tags/test-.*' } + } + steps { + script { + try { + sh ''' + docker login http://172.16.102.3:30648 -u ${reg_username} -p ${reg_passwd} + docker build -t 172.16.102.3:30648/maimai/${service_name}:${result} . + docker push 172.16.102.3:30648/maimai/${service_name}:${result} + ''' + } catch(err) { + exit 1 + } + } + } + } + stage('master build only') { + when { + expression {ref ==~ 'refs/tags/prod-.*' } + } + steps { + script { + try { + sh ''' + docker login http://172.16.102.3:30648 -u ${reg_username} -p ${reg_passwd} + docker build -t 172.16.102.3:30648/maimai/${service_name}:${result} . + docker push 172.16.102.3:30648/maimai/${service_name}:${result} + ''' + } catch(err) { + exit 1 + } + } + } + } + } + environment { + reg_username = 'maimai' + reg_passwd = 'M9hUQk4Ti0l0lHZi' + service_name = 'xj-irrigation-model' + result = sh(script: """echo $ref | awk -F"/" '{print \$NF}'""", returnStdout: true).trim() + git_commit_msg = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim() + git_commit_user = sh (script: 'git show -s --pretty=%an', returnStdout: true).trim() + } + post { + success { + sh """curl --location --request POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=d31913db-bb53-4081-9e2d-f707111dbee1' \ + --data-raw '{\"msgtype\":\"text\",\"text\":{\"content\":\"成功: [${env.JOB_NAME} [${env.BUILD_NUMBER}]](${git_commit_msg}) @${git_commit_user}\"}}'""" + } + failure { + sh """curl --location --request POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=d31913db-bb53-4081-9e2d-f707111dbee1' \ + --data-raw '{\"msgtype\":\"text\",\"text\":{\"content\":\"失败: [${env.JOB_NAME} [${env.BUILD_NUMBER}]](${git_commit_msg}) @${git_commit_user}\"}}'""" + } + } + triggers { + GenericTrigger( + genericVariables: [[key: 'ref', value: '$.ref']], + causeString: 'Triggered on $ref', + token: 'xj-irrigation-model', + printContributedVariables: true, + printPostContent: true, + silentResponse: false, + regexpFilterText: '$ref/'+BRANCH_NAME, + regexpFilterExpression: '(^refs/heads/develop/develop$)|(^refs/tags/test-.*/test$)|(^refs/tags/prod-.*/master$)' + ) + } +} \ No newline at end of file diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..80ab61d --- /dev/null +++ b/README.en.md @@ -0,0 +1,36 @@ +# xj-irrigation-model + +#### Description +智能水肥模型 + +#### Software Architecture +Software architecture description + +#### Installation + +1. xxxx +2. xxxx +3. xxxx + +#### Instructions + +1. xxxx +2. xxxx +3. xxxx + +#### Contribution + +1. Fork the repository +2. Create Feat_xxx branch +3. Commit your code +4. Create Pull Request + + +#### Gitee Feature + +1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md +2. Gitee blog [blog.gitee.com](https://blog.gitee.com) +3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) +4. The most valuable open source project [GVP](https://gitee.com/gvp) +5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) +6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md index 5e0d62d..302c50a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,37 @@ -# irrigation-model +# xj-irrigation-model -判断是否需要灌溉 \ No newline at end of file +#### 介绍 +智能水肥模型 + +#### 软件架构 +软件架构说明 + + +#### 安装教程 + +1. xxxx +2. xxxx +3. xxxx + +#### 使用说明 + +1. xxxx +2. xxxx +3. xxxx + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 +4. 新建 Pull Request + + +#### 特技 + +1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md +2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) +3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 +4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 +5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) +6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/config.nifi b/config.nifi new file mode 100644 index 0000000..fd218ec --- /dev/null +++ b/config.nifi @@ -0,0 +1,11 @@ +[postgresql] +host = localhost +port = 5432 +database = datastore +user = postgres +password = postgres +schema = public +tablename=irrigation_data + +[logging] +level = INFO \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..7f9796c --- /dev/null +++ b/dockerfile @@ -0,0 +1,9 @@ +FROM python:3.10.5 + +WORKDIR /app +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple + +COPY . . +RUN pip install -r requirements.txt +ENTRYPOINT [ "python", "main.py" ] +EXPOSE 8001 \ No newline at end of file diff --git a/irrgiation/__init__.py b/irrgiation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/irrgiation/__pycache__/__init__.cpython-310.pyc b/irrgiation/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..957bca8fffa2b776c002b28105ce5af36b4b698a GIT binary patch literal 163 zcmd1j<>g`kf~u8`86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6ww#VV$tC_gJT zxdcYXS7hd8WhUmO$5dqLW)>A?rYDwU=I80==BK3Q!~g}-GrKw>S{{8+o;8w@lN8#b3NtXHR%}%y%j&t#-#T{qAmp*i^;!}Nv3IvFO2=Rsp2}nW;(jY-sBLx}sx-r@VS?GnnFxn6OFo4lPI0Qp* z7={rw(nzCDBFm9R9FAhu2{3g-kmXtm$NBp3(cXi5)|1Cvd@VFyYqIDk@!02^ z@rO`_Cm*%i2zq%ne^F6q6m@p*?z70r&sy8#cfKudKh-S6puE%M6zit0>*==XSYC-` zp+*glxQ-4+y~@P3WtQ7tLR4(W`0O7Qv>H&+!xDY1eJs+3(Y?IH#&5(J8WQ(vgmnKX~Unr&W*VUg-O qDlsOBVl4V4_NA`K7Gs?r`ad(7=rr*wBSkhxW;*iz=OtcBzyAPTGgQz3 literal 0 HcmV?d00001 diff --git a/irrgiation/__pycache__/dailyEvaporation.cpython-310.pyc b/irrgiation/__pycache__/dailyEvaporation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74b779e5514f809757a940dac6be8460a8f9e642 GIT binary patch literal 7951 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 a0Y9?a=rTHuexuJ=Vzgr?J#j_*-~R_;{nZTs literal 0 HcmV?d00001 diff --git a/irrgiation/__pycache__/db_connect.cpython-310.pyc b/irrgiation/__pycache__/db_connect.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..434ea44990984df80b6ef133ee2cfd8686b6451b GIT binary patch literal 5607 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 N`%!Bn{?JIF<$tYk?%V(X literal 0 HcmV?d00001 diff --git a/irrgiation/__pycache__/irrigateDecisionDemo.cpython-310.pyc b/irrgiation/__pycache__/irrigateDecisionDemo.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4c56e87c200eb068c7870ecdde1345bacb94fbd GIT binary patch literal 15550 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 literal 0 HcmV?d00001 diff --git a/irrgiation/__pycache__/weatherAndSoilDataRequest.cpython-310.pyc b/irrgiation/__pycache__/weatherAndSoilDataRequest.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf3190bb756f4a73fb1a9d40e40a7fa97980b1f0 GIT binary patch literal 8136 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 8 + mad = 0.23 + return mad +def calculate_weighted_soil_parameters(root_distribution, soil_layers): + # 初始化累计变量 + sum_fc = 0.0 + sum_wp = 0.0 + # 处理每个根系分布层 + for root_depth, root_ratio in root_distribution['root_length'].items(): + # 找到包含该深度的土壤层 + for layer in soil_layers: + min_depth, max_depth = layer['depth_range'] + if min_depth <= root_depth < max_depth: + # 计算该层贡献 (简单比例分配) + layer_thickness = max_depth - min_depth + depth_in_layer = min(root_depth, max_depth) - min_depth + contribution_ratio = depth_in_layer / layer_thickness * root_ratio + sum_fc += layer['theta_33'] * contribution_ratio + sum_wp += layer['theta_1500'] * contribution_ratio + break + + return sum_fc, sum_wp +def cal_stagebyDate(monitor_date,zwlx_growth_stages): + for stage, details in zwlx_growth_stages.items(): + time_range_str = details['time_range'] + if type(monitor_date) is str: + monitor_date= datetime.strptime(monitor_date, '%Y-%m-%d') + if '-' in time_range_str: + start_str, end_str = time_range_str.split('至') + start_date = datetime.strptime(start_str, '%Y-%m-%d') + end_date = datetime.strptime(end_str, '%Y-%m-%d') + if start_date <= monitor_date <= end_date: + return stage,details['root_length'],details['hight'],details['k_rain'] + raise ValueError(f"监测日期 {monitor_date.strftime('%Y-%m-%d')} 不在任何生育阶段范围内") +#根据日期计算主根长度 +def calculate_average_root_length(root_length_dist): + return sum(length * prob for length, prob in root_length_dist.items()) +def cal_soil_fc_wp(): + # 土壤分层数据 (33kPa和1500kPa对应的含水率) + soil_data = [ + {'depth_range': (0, 5), 'theta_33': 0.296, 'theta_1500': 0.103}, + {'depth_range': (5, 15), 'theta_33': 0.284, 'theta_1500': 0.104}, + {'depth_range': (15, 30), 'theta_33': 0.286, 'theta_1500': 0.105}, + {'depth_range': (30, 60), 'theta_33': 0.290, 'theta_1500': 0.114}, + {'depth_range': (60, 100), 'theta_33': 0.286, 'theta_1500': 0.117}, + {'depth_range': (100, 200), 'theta_33': 0.284, 'theta_1500': 0.114} + ] + # theta_fc, theta_wp = calculate_weighted_soil_parameters( + # growth_stages_roots[stage], + # soil_data + # ) + theta_fc=0.2884 + theta_wp=0.1086 + return theta_fc,theta_wp + +if __name__ == "__main__": + # #cal_et0_List(datetime(2026, 6, 18),datetime(2026, 6, 22)) + et0_daily=[23,45,67,89] + # etc,dates,kc_daily=calculate_etc_List(et0_daily, datetime(2026, 6, 18),datetime(2026, 6, 22), get_daily_kc(growth_stages_roots)) + # calculate_etc_dayily(45, datetime(2026, 6, 18), datetime(2026, 6, 22), get_daily_kc(growth_stages)) + # df=export_etc_et0_date(et0_daily,etc,dates,kc_daily) + # print(df) + # cal_soil_fc_wp('initial') + # stage,details = cal_stagebyDate(datetime(2025, 7, 18)) + # print(details) + # rd=calculate_average_root_length(details) + # print(rd) \ No newline at end of file diff --git a/irrgiation/db_connect.py b/irrgiation/db_connect.py new file mode 100644 index 0000000..c580557 --- /dev/null +++ b/irrgiation/db_connect.py @@ -0,0 +1,199 @@ +import configparser +import os +from pathlib import Path +from typing import Optional, Union, List + +import pandas as pd +import psycopg2 +from dotenv import load_dotenv +from psycopg2 import sql +from psycopg2.extras import execute_batch +from sqlalchemy import create_engine, text + + +def get_config(): + config = configparser.ConfigParser() + try: + project_nifi = Path(__file__).parent.parent / 'config.nifi' + config.read(project_nifi) + # 确保所有必要键都存在 + required_keys = ['host', 'port', 'database', 'user', 'password','schema'] + for key in required_keys: + if key not in config['postgresql']: + raise ValueError(f"Missing required config key: {key}") + + return { + "host": config['postgresql']['host'], + "port": config['postgresql']['port'], + "database": config['postgresql']['database'], + "user": config['postgresql']['user'], + "password": config['postgresql']['password'], + "schema": config['postgresql']['schema'], + "tablename": config['postgresql']['tablename'] + } + except Exception as e: + print(f"配置读取错误: {e}") + # 返回默认配置或退出 + return None +def connect(): + """建立数据库连接""" + try: + config_params=get_config() + conn = psycopg2.connect(host=config_params['host'],port=config_params['port'],dbname=config_params['database'], + user=config_params['user'],password=config_params['password'] + ) + cursor = conn.cursor() + except Exception as e: + print(f"❌ 连接失败: {e}") + raise + +# 根据条件查询 +def query_postgresql_to_dataframe( + condition: Optional[dict] = None, + condition_operator: str = "AND", + columns: Union[str, List[str]] = "*",): + + # 连接数据库 + config_params = get_config() + conn = psycopg2.connect(host=config_params['host'], port=config_params['port'], dbname=config_params['database'], + user=config_params['user'], password=config_params['password'] + ) + try: + # 构建列选择部分 + if isinstance(columns, list): + columns_sql = sql.SQL(", ").join(sql.Identifier(col) for col in columns) + else: + columns_sql = sql.SQL(columns) + + # 构建基础查询 + query = sql.SQL("SELECT {} FROM {}").format( + columns_sql, + sql.Identifier(config_params['tablename']) + ) + + # 构建条件部分(参数化) + params = {} + if condition: + conditions = [] + for i, (key, value) in enumerate(condition.items()): + param_name = f"param_{i}" + conditions.append(sql.SQL("{} = %({})s").format( + sql.Identifier(key), + sql.SQL(param_name) + )) + params[param_name] = value + + where_clause = sql.SQL(" WHERE {}").format( + sql.SQL(f" {condition_operator} ").join(conditions) + ) + query = query + where_clause + print(query) + # 执行查询 + with conn.cursor() as cursor: + cursor.execute(query, params) + if cursor.description: + columns = [desc[0] for desc in cursor.description] + data = cursor.fetchall() + return pd.DataFrame(data, columns=columns) + return pd.DataFrame() + + finally: + conn.close() + +# 插入库表 +def dataframe_to_postgresql_batch(df,batch_size=1000): + """ + 使用execute_batch批量插入DataFrame数据 + + 参数: + df: 要插入的DataFrame + table_name: 目标表名 + config_params: 数据库连接配置 + batch_size: 每批插入的行数 + """ + if df.empty: + print("DataFrame为空,无需插入") + return + + # 获取列名 + columns = df.columns.tolist() + data = [tuple(x) for x in df.to_numpy()] + + try: + config_params = get_config() + conn = psycopg2.connect(host=config_params['host'], port=config_params['port'], + dbname=config_params['database'], + user=config_params['user'], password=config_params['password'] + ) + + cursor = conn.cursor() + + # 构建INSERT语句 + insert_query = sql.SQL("INSERT INTO {} ({}) VALUES ({})").format( + sql.Identifier(config_params['tablename']), + sql.SQL(', ').join(map(sql.Identifier, columns)), + sql.SQL(', ').join([sql.Placeholder()] * len(columns)) + ) + + # 批量执行 + execute_batch(cursor, insert_query, data, batch_size) + + conn.commit() + + except Exception as e: + conn.rollback() + print(f"批量插入时出错: {e}") + finally: + if conn is not None: + conn.close() +# 根据条件更新某个字段 +def update_irrigation_data(date_value, id_value, field_to_update, new_value): + """ + 更新表中单条记录的单个字段 + + 参数: + db_url: 数据库连接字符串 + table_name: 表名 + date_field: 日期字段名 + id_field: ID/地块字段名 + date_value: 日期值 + id_value: ID/地块值 + field_to_update: 要更新的字段名 + new_value: 新值 + """ + try: + # 数据库配置 + db_config = { + "db_url": "postgresql://postgres:postgres@localhost:5432/datastore", + "table_name": "irrigation_data", + "date_field": "Date", + "id_field": "dkbm" + } + engine = create_engine(db_config["db_url"]) + table_name=db_config["table_name"] + date_field=db_config["date_field"] + id_field=db_config["id_field"] + with engine.begin() as conn: # 自动提交事务 + # 使用参数化查询防止SQL注入 + update_sql = text(f""" + UPDATE {table_name} + SET "{field_to_update}" = :new_value + WHERE "{date_field}" = :date_value AND "{id_field}" = :id_value""") + conn.execute(update_sql, { + 'new_value': new_value, + 'date_value': date_value, + 'id_value': id_value + }) + + return True + except Exception as e: + print(f"更新失败: {str(e)}") + return False + finally: + engine.dispose() # 确保连接关闭 + + +if __name__ == '__main__': + config = configparser.ConfigParser() + project_nifi = Path(__file__).parent.parent / 'config.nifi' + config.read(project_nifi) \ No newline at end of file diff --git a/irrgiation/irrigateDecisionDemo.py b/irrgiation/irrigateDecisionDemo.py new file mode 100644 index 0000000..4906775 --- /dev/null +++ b/irrgiation/irrigateDecisionDemo.py @@ -0,0 +1,620 @@ +import statistics +import uuid + +from collections import defaultdict +from datetime import datetime, timedelta + +import pandas as pd + +from irrgiation.db_connect import query_postgresql_to_dataframe, dataframe_to_postgresql_batch,update_irrigation_data + +from irrgiation.weatherAndSoilDataRequest import getWeatherAndSoilData, loginAuth, dateToTimestamp, get_soil_data_dk1, \ + getFutureWeather, weather_params, get_soil_data_dk2 +from irrgiation.dailyEvaporation import get_daily_kc, cal_et0_List, calculate_etc_List, export_etc_et0_date, \ + cal_soil_fc_wp, estimate_mad_from_et, cal_stagebyDate, calculate_average_root_length,calculate_etc_List_new +from irrgiation.mathuntils import day_of_year +from irrgiation.soil_Ke import calculate_ke,calculate_D_ei +from irrgiation.crop_growth_stages import crop_growth_stages + +#解析数据接口,计算均值、最大、最小值 +def analy_soil_data(data): + soil_result = {} + for key in data: + daily_data = defaultdict(lambda: defaultdict(list)) + for metric, entries in data[key].items(): + for entry in entries: + dt = datetime.strptime(entry["datetime"], "%Y-%m-%d %H:%M:%S") + date_key = dt.date() # 按日期分组 + value = float(entry["value"]) + daily_data[date_key][metric].append(value) + result = {} + for date, metrics in daily_data.items(): + date_stats = {} + for metric, values in metrics.items(): + date_stats[metric] = { + "max": max(values), + "min": min(values), + "mean": statistics.mean(values), + 'count': len(values), + 'sum': sum(values) + } + result[str(date)] = date_stats # 将日期转为字符串作为键 + soil_result[key] = result + return soil_result + + +#获取地块6个传感器的土壤湿度均值 +def analy_soil_data_mean(soil_data): + result = defaultdict(lambda: { + "SOIL_MOISTURE_SURFACE": [], + "SOIL_MOISTURE_MIDDLE": [], + "SOIL_MOISTURE_BOTTOM": [] + + }) + # 填充数据 + for sensor, dates in soil_data.items(): + for date, layers in dates.items(): + result[date]["SOIL_MOISTURE_SURFACE"].append(layers["SOIL_MOISTURE_SURFACE"]["mean"]) + result[date]["SOIL_MOISTURE_MIDDLE"].append(layers["SOIL_MOISTURE_MIDDLE"]["mean"]) + result[date]["SOIL_MOISTURE_BOTTOM"].append(layers["SOIL_MOISTURE_BOTTOM"]["mean"]) + # 转换为普通字典 + final_result = dict(result) + result_soil = {} + for date, layers in final_result.items(): + result_soil[date] = { + "SOIL_MOISTURE_SURFACE_MEAN": sum(layers["SOIL_MOISTURE_SURFACE"]) / len(layers["SOIL_MOISTURE_SURFACE"]), + "SOIL_MOISTURE_MIDDLE_MEAN": sum(layers["SOIL_MOISTURE_MIDDLE"]) / len(layers["SOIL_MOISTURE_MIDDLE"]), + "SOIL_MOISTURE_BOTTOM_MEAN": sum(layers["SOIL_MOISTURE_BOTTOM"]) / len(layers["SOIL_MOISTURE_BOTTOM"]) + } + + return final_result, result_soil + + +def analy_soil_dataNight_mean(soil_data): + daily_means = defaultdict(lambda: defaultdict(list)) + # 计算平均值 + for sensor, measurements in soil_data.items(): + for measure_type, dates in measurements.items(): + for date, value in dates.items(): + daily_means[date][measure_type].append(float(value)) + + # 计算并格式化结果 + result = {} + for date, measures in daily_means.items(): + result[date] = { + f"{measure_type}_MEAN": round(sum(values) / len(values), 2) + for measure_type, values in measures.items() + } + + return result + + +#获取地块6个传感器中土壤湿度最大值 +def analy_soil_data_max(soil_data): + result = defaultdict(lambda: { + "SOIL_MOISTURE_SURFACE": [], + "SOIL_MOISTURE_MIDDLE": [], + "SOIL_MOISTURE_BOTTOM": [] + + }) + # 填充数据 + for sensor, dates in soil_data.items(): + for date, layers in dates.items(): + result[date]["SOIL_MOISTURE_SURFACE"].append(layers["SOIL_MOISTURE_SURFACE"]["mean"]) + result[date]["SOIL_MOISTURE_MIDDLE"].append(layers["SOIL_MOISTURE_MIDDLE"]["mean"]) + result[date]["SOIL_MOISTURE_BOTTOM"].append(layers["SOIL_MOISTURE_BOTTOM"]["mean"]) + # 转换为普通字典 + final_result = dict(result) + result_soil = {} + for date, layers in final_result.items(): + result_soil[date] = { + "SOIL_MOISTURE_SURFACE_MEAN": max(layers["SOIL_MOISTURE_SURFACE"]), + "SOIL_MOISTURE_MIDDLE_MEAN": max(layers["SOIL_MOISTURE_MIDDLE"]), + "SOIL_MOISTURE_BOTTOM_MEAN": max(layers["SOIL_MOISTURE_BOTTOM"]) + } + + return final_result, result_soil + + +def get_soil_data_night_eyveryDay(data): + daily_data = {} + for sersor in data: + data_depth = {} + for key in data[sersor]: + target_time = (21, 59, 59) + result = {} # 时、分、秒元组 + daily_records = {} # 临时存储每天的所有记录,用于找不到22点数据时获取最新记录 + for record in data[sersor][key]: + try: + dt = datetime.strptime(record["datetime"], "%Y-%m-%d %H:%M:%S") + date_str = record["datetime"].split(" ")[0] # 提取日期部分 + # 严格匹配 02:00:00 + # 提取时间部分(时、分、秒) + current_time = (dt.hour, dt.minute, dt.second) + # 存储每天的所有记录(按时间排序) + if date_str not in daily_records: + daily_records[date_str] = [] + daily_records[date_str].append((dt, record['value'])) + # 比较时间部分 + if current_time >= target_time: + if date_str not in result: + result[date_str] = record['value'] + except: + continue + # 对于没有22点数据的日期,取当天最新的一条数据 + for date_str in daily_records: + if date_str not in result and daily_records[date_str]: + daily_records[date_str].sort(key=lambda x: x[0]) # 按datetime排序 + result[date_str] = daily_records[date_str][-1][1] # 取最后一个值 + data_depth[key] = result + daily_data[sersor] = data_depth + return daily_data + + +def analyze_weather_data1(data): + # 1. 按日期分组数据 + daily_data = defaultdict(lambda: defaultdict(list)) + for metric, entries in data.items(): + for entry in entries: + dt = datetime.strptime(entry["datetime"], "%Y-%m-%d %H:%M:%S") + date_key = dt.date() # 按日期分组 + if metric == "AIR_LUX": + value = float(entry["value_solar"]) + else: + value = float(entry["value"]) + daily_data[date_key][metric].append(value) + # 2. 计算每日统计值 + result = {} + for date, metrics in daily_data.items(): + date_stats = {} + for metric, values in metrics.items(): + date_stats[metric] = { + "max": max(values), + "min": min(values), + "mean": statistics.mean(values), + 'count': len(values), + 'sum': sum(values) + } + result[str(date)] = date_stats # 将日期转为字符串作为键 + + return result + + +# 获取彭曼公式的入参 +def get_params(start_date, monitor_date, data): + delta = monitor_date - start_date + days = delta.days + dates = [start_date + timedelta(days=i) for i in range(days)] + date_strings = [date.strftime("%Y-%m-%d") for date in dates] + date_params = {} + for date in date_strings: + result_params = get_weather_params(data, date) + date_params[date] = result_params + return date_params + + +def get_weather_params(data, date): + result = {} + elevation = 865 # 海拔(m) + lat = 41.34252110189814 # 纬度(度) + #计算当前日期位于当年的多少天 + doy = day_of_year(date) + + for item in data['AIR_LUX']: + item['value_solar'] = float(item['value']) * 0.0081 * 3600 + data_tj = analyze_weather_data1(data) + data_tj_date = data_tj[date]; + tmax = data_tj_date['AIR_TEMPERATURE']['max'] + tmin = data_tj_date['AIR_TEMPERATURE']['min'] + tmean = data_tj_date['AIR_TEMPERATURE']['mean'] + rh_mean = data_tj_date['AIR_HUMIDITY']['mean'] + wind_speed = data_tj_date['WIND_SPEED']['mean'] + solar = data_tj_date['AIR_LUX']['sum'] / 1000000 + result['tmax'] = round(tmax, 3) + result['tmin'] = round(tmin, 3) + result['tmean'] = round(tmean, 3) + result['rh_mean'] = round(rh_mean, 3) + result['wind_speed'] = round(wind_speed, 3)*0.94 + result['solar_rad'] = round(solar, 3) + result['elevation'] = elevation + result['lat'] = round(lat, 3) + result['doy'] = doy + return result + + +# 判断是否需要灌溉 +def isNeedirrigate(df, qx_coff, soil_coff,Di_1,zwlx_growth_stages): + df['RD'] = 0.0 + df['Di'] = 0.0 + df['Di-1'] = Di_1 + df['mad'] = 0.0 + df['irrigation_depth'] = 0.0 + df['irrigation_acre'] = 0.0 + df['isNeedirrigate'] = False + df['Reset_Flag'] = False + # df['irrigate_sd_predict'] = 0.0 + df['f_t'] = 0.0 + df['f_js'] = 0.0 + df['f_soil'] = 0.0 + for i in range(len(df)): + # 设置Di-1(前一天累计值) + if i > 0: + df.at[i, 'Di-1'] = df.at[i - 1, 'Di'] + # 计算当前累计值(Di-1 + 当天ETc) + raw, rd, mad, k_rain = cal_raw(df.at[i, 'ETc'], df.at[i, 'Date'],zwlx_growth_stages) + if df.at[i, 'rain_effective']<5: + df.at[i, 'rain_effective'] = 0 + else: + df.at[i, 'rain_effective'] = df.at[i, 'rain_effective'] * k_rain + current_cum = df.at[i, 'Di-1'] + df.at[i, 'ETc']- df.at[i, 'rain_effective'] + df['RD'] = rd + df.at[i,'mad'] = mad + df.at[i, 'raw'] = round(raw, 3) + f_t = qx_coff[df.at[i, 'Date']]['qw'] + f_js = qx_coff[df.at[i, 'Date']]['js'] + f_soil = soil_coff[df.at[i, 'Date']] + raw_adj = cal_raw_adjust(raw, f_t, f_js, f_soil) + df.at[i, 'f_t'] = f_t + df.at[i, 'f_js'] = f_js + df.at[i, 'f_soil'] = f_soil + df.at[i, 'raw_adjust'] = round(raw_adj, 3) + # 判断是否需要重置 + if current_cum > raw_adj: + df.at[i, 'Reset_Flag'] = True + df.at[i, 'irrigation_depth'] = round(current_cum, 3) + df.at[i, 'isNeedirrigate'] = True + df.at[i, 'Di'] = 0 # 保留当天ETc值 + df.at[i, 'irrigation_acre'] = round((current_cum*667/1000),3) + else: + df.at[i, 'Di'] = round(current_cum, 3) # 正常累加 + return df + + +def cal_raw(etc, monitor_date,zwlx_growth_stages): + theta_fc, theta_wp = cal_soil_fc_wp() + stage, details,hight,k_rain= cal_stagebyDate(monitor_date,zwlx_growth_stages) + rd = calculate_average_root_length(details) + mad = estimate_mad_from_et(etc) + raw = mad * (theta_fc - theta_wp) * rd * 10 + return raw, rd,mad,k_rain + + +def cal_raw_adjust(raw, f_t, f_rain, f_soil): + raw_adjust = raw * f_soil * f_t * f_rain + return raw_adjust + + +def cal_qxcoefficient_adjust(start_date, monitor_date, qx_data): + + qx_coefficient = {} + current_date = start_date + qxData_filter = {} + for key in qx_data: + if key == "AIR_TEMPERATURE" or key == "RAIN_FALL_REALTIME": + qxData_filter[key] = qx_data[key] + qxData_filter_tj = analyze_weather_data1(qxData_filter) + now_date = datetime.now().date() + while current_date < monitor_date: + qx_futureData = [] + result = {} + if (current_date + timedelta(days=3)).date() < now_date: + start = datetime.strptime((current_date + timedelta(days=1)).strftime("%Y-%m-%d"), "%Y-%m-%d") + end = datetime.strptime((current_date + timedelta(days=3)).strftime("%Y-%m-%d"), "%Y-%m-%d") + # 筛选日期范围内的数据 + filter = { + date: values + for date, values in qxData_filter_tj.items() + if start <= datetime.strptime(date, "%Y-%m-%d") <= end + } + qx_futureData = transform_data(filter) + else: + try: + # print(f"进入else分支: {current_date}") + qx_futureData = getFutureWeather(current_date) + except Exception as e: + # print(f"else分支异常: {e}") + raise # 保留异常以便调试 + qw_mean = (qx_futureData['qw_day1']['mean'] + qx_futureData['qw_day2']['mean'] + qx_futureData['qw_day3']['mean']) / 3 + js_sum = qx_futureData['js_day1']['max'] + qx_futureData['js_day2']['max'] + qx_futureData['js_day3']['max'] + qw_coff = 1.0 + js_coff = 1.0 + if qw_mean < 10: + qw_coff = 1.1 + elif 10 < qw_mean <= 25: + qw_coff = 1 + elif 25 < qw_mean <= 30: + qw_coff = 0.85 + else: + qw_coff = 0.75 + if js_sum <= 5: + js_coff = 1.0 # 少雨 + elif 5 < js_sum <= 10: + js_coff = 1.2 # 中雨 + elif 10 < js_sum <= 20: + js_coff = 1.4 # 大雨 + else: + js_coff = 1.6 # 暴雨 + result['qw'] = qw_coff + result['js'] = js_coff + qx_coefficient[current_date.strftime("%Y-%m-%d")] = result + current_date = current_date + timedelta(days=1) + return qx_coefficient + + +def transform_data(original_data): + transformed = {} + # 处理温度数据 (qw_day) + for i, (date, metrics) in enumerate(original_data.items(), 1): + temp_data = metrics["AIR_TEMPERATURE"] + transformed[f"qw_day{i}"] = { + "max": round(temp_data["max"], 2), + "min": round(temp_data["min"], 2), + "mean": round(temp_data["mean"], 2), + "sum": round(temp_data["sum"], 2) + } + + # 处理降雨数据 (js_day) + for i, (date, metrics) in enumerate(original_data.items(), 1): + rain_data = metrics["RAIN_FALL_REALTIME"] + transformed[f"js_day{i}"] = { + "max": round(rain_data["max"], 2), + "min": round(rain_data["min"], 2), + "mean": round(rain_data["mean"], 2), + "sum": round(rain_data["sum"], 2) + } + + return transformed + + +def cal_soil_coff(soil_data): + soil_data_coff = {} + for sensor in soil_data: + for date in soil_data[sensor]: + soil_cof = 1.0 + sd_surf = float(soil_data['SOIL_MOISTURE_SURFACE'][date]) + sd_mid = float(soil_data['SOIL_MOISTURE_MIDDLE'][date]) + if sd_surf < 10: + soil_cof = 0.5 + if 10 0 and rain < 4: + fw = 0.35 + elif I > 0 and rain < 4: + fw = 1 + elif rain > 4 and I == 0: + fw = 1 + else: + fw = ke_data[prev_date_key]['fw'] + et0 = 0.0 + kc = 0.0 + if i < len(et0_list): + et0 = et0_list[i] + kc = kc_list[i] + ke,few=calculate_ke(hight, u2, rh_min, kc, DE_i_1, REW, fw, theta_fc,theta_wp) + et_soil=ke*et0 + DE_i = calculate_D_ei(DE_i_1, rain, I,et_soil, fw,few) + #创建当天的数据字典 + daily_data = { + 'ke': ke, + 'DE_i': DE_i, + 'et0': et0, + 'et_soil': et_soil, + 'fw':fw, + "rain":rain, + "irrigation":I + } + ke_data[date_key]=daily_data + current_date = current_date + timedelta(days=1) + i=i+1 + return ke_data +# 单天监测 +def cal_irrigationByDay(zwlx_name,dkname,start_date,end_date,soil_key,weather_key,irrigation_really): + yesterday_date=(start_date - timedelta(days=1)).strftime("%Y-%m-%d") + condition = {"Date": yesterday_date,"dkbm":dkname} + df=query_postgresql_to_dataframe(condition) + Di_1 = 0 + if irrigation_really>0: + Di_1 = 0 + update_irrigation_data(yesterday_date,dkname,"irrigation_really",irrigation_really) + else: + # 将Date列转换为日期时间类型 + df['Date'] = pd.to_datetime(df['Date']) + if len(df[df.Date == (start_date - timedelta(days=1))])!=0: + isNeedjg=df[df.Date == (start_date- timedelta(days=1))]['isNeedirrigate'].values[0] + if isNeedjg: + Di_1 = df[df.Date == (start_date - timedelta(days=1))]['Di-1'].values[0]+df[df.Date == (start_date - timedelta(days=1))]['ETc'].values[0] + else: + Di_1 = df[df.Date == (start_date - timedelta(days=1))]['Di'].values[0] + dataframe=cal_dk_isNeegirrigate_Day(zwlx_name,dkname,soil_key,start_date,end_date,Di_1,weather_key) + condition_new = {"Date": start_date.strftime("%Y-%m-%d"), "dkbm": dkname} + df_query=query_postgresql_to_dataframe(condition_new) + if len(df_query)==0: + dataframe_to_postgresql_batch(dataframe) + else: + print("库表中已存在该数据") + return dataframe +if __name__ == "__main__": + #调用气象数据接口--获取具体日期的气象数据(气象) + # auth_token = loginAuth() + start_date = datetime(2025, 7, 16) + monitor_date = datetime(2025, 7, 17) + dk_name = "5" + zwlx_name="甘草" + soil_key = "8637b970-561b-11f0-a556-4f10f26fc07f" + weather_key="18d121f0-561b-11f0-a556-4f10f26fc07f" + irrigation_really=5 + cal_irrigationByDay(zwlx_name,dk_name,start_date,monitor_date,soil_key,weather_key,irrigation_really) + # cal_dk_isNeegirrigate(dk_name, planting_date, monitor_date,0) diff --git a/irrgiation/mathuntils.py b/irrgiation/mathuntils.py new file mode 100644 index 0000000..d9c981b --- /dev/null +++ b/irrgiation/mathuntils.py @@ -0,0 +1,371 @@ +#根据日期计算当年天数 +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt + + +def is_leap_year(year): + return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0) + +def day_of_year(date_str): + date_part = date_str.split()[0] + year = int(date_str[:4]) # 取前4位 → 2025 + month = int(date_str[5:7]) # 取第5-6位 → 06 → 6 + day = int(date_str[8:10]) # 取第8-9位 → 30 + month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + if is_leap_year(year): + month_days[1] = 29 # 闰年2月29天 + total = sum(month_days[:month-1]) + day + return total +#计算每天有效日照时长 +def count_above_threshold(data, threshold,date): + filtered = filter(lambda x: float(x['value_cal']) >= threshold and x['datetime'].split(" ")[0] == date, data) + return len(list(filtered)) +# 示例计算函数:Lux转W/m² +def lux_to_wm2(lux_value): + """将Lux值转换为W/m²(使用标准系数0.0081)""" + return lux_value * 0.0081 +def convert_lux_to_Radiant_Flux(data_dict): + """ + 递归地将字典中的所有'timestamp'字段转换为可读的日期时间格式 + 保留原始时间戳的同时添加新的'datetime'字段 + """ + if 'value' in data_dict: + radiant_Flux=data_dict["value"]*0.001496 + data_dict["value_radiant"] =radiant_Flux + return data_dict +def lux_to_solar_rad(lux,h): + + return lux * 0.001496*h * 3.6 / 1000 +def plot(soil_data,name): + import matplotlib.dates as mdates + # 创建画布 + plt.figure(figsize=(14, 8)) + + # 处理并绘制地表湿度数据 + surface_df = pd.DataFrame(soil_data["SOIL_MOISTURE_SURFACE"]) + surface_df['datetime'] = pd.to_datetime(surface_df['datetime']) + surface_df['value'] = surface_df['value'].astype(float) + plt.plot(surface_df['datetime'], surface_df['value'], + label='Surface Moisture (0-5cm)', color='#FF6B6B', linewidth=2, marker='o', markersize=5) + + # 处理并绘制中层湿度数据 + middle_df = pd.DataFrame(soil_data["SOIL_MOISTURE_MIDDLE"]) + middle_df['datetime'] = pd.to_datetime(middle_df['datetime']) + middle_df['value'] = middle_df['value'].astype(float) + plt.plot(middle_df['datetime'], middle_df['value'], + label='Middle Layer Moisture', color='#4ECDC4', linewidth=2, marker='s', markersize=5) + # 处理并绘制中层湿度数据 + middle_df = pd.DataFrame(soil_data["SOIL_MOISTURE_BOTTOM"]) + middle_df['datetime'] = pd.to_datetime(middle_df['datetime']) + middle_df['value'] = middle_df['value'].astype(float) + plt.plot(middle_df['datetime'], middle_df['value'], + label='BOTTOM Layer Moisture', color='blue', linewidth=2, marker='s', markersize=5) + + # 设置图表格式 + plt.title('Soil Moisture Variation (Jun 18 - Jul 3, 2025)', fontsize=16, pad=20) + plt.xlabel('Date', fontsize=12) + plt.ylabel('Soil Moisture (%)', fontsize=12) + plt.grid(True, linestyle='--', alpha=0.7) + + # 设置x轴日期格式 + ax = plt.gca() + ax.xaxis.set_major_locator(mdates.DayLocator(interval=2)) + ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%d')) + plt.xticks(rotation=45) + + # 添加图例和注释 + plt.legend(loc='upper right', fontsize=10) + plt.tight_layout() + # 保存图片到本地 + output_path = 'D:\悦柠\遥感\甘草\ceshi/'+name+".png" + plt.savefig(output_path, dpi=300, bbox_inches='tight') + # 显示图表 + # plt.show() + + +def rh_to_theta_vg(rh, soil_params, temperature=25): + """使用Van Genuchten模型将相对湿度转换为体积含水量""" + # 计算土壤水势(kPa) + psi = -145 * np.log(rh / 100) + + # 提取土壤参数 + theta_s = soil_params["theta_s"] # 饱和含水量 + theta_r = soil_params["theta_r"] # 残余含水量 + alpha = soil_params["alpha"] # Van Genuchten参数 + n = soil_params["n"] # Van Genuchten参数 + m = 1 - 1 / n + + # 计算体积含水量 + theta = theta_r + (theta_s - theta_r) / ((1 + (alpha * abs(psi)) ** n) ** m) + print(theta) + return theta + + +def calculate_soil_moisture_after_irrigation( + initial_moisture: float, + field_capacity: float, + irrigation_volume: float, + soil_volume: float, + area: float = None, + verbose: bool = False +) -> float: + """ + 不考虑降雨量和蒸散量时,计算灌溉后的土壤相对湿度 + + 参数: + initial_moisture: 初始土壤相对湿度 (%) + field_capacity: 田间持水量 (%) + irrigation_volume: 灌溉量 (m³) + soil_volume: 土壤体积 (m³) + area: 灌溉面积 (㎡,可选,仅用于打印提示) + verbose: 是否打印详细计算过程 + + 返回: + float: 灌溉后土壤相对湿度 (%) + """ + # 参数验证 + if not (0 <= initial_moisture <= 100): + raise ValueError("初始湿度必须在0-100%之间") + if not (0 < field_capacity <= 100): + raise ValueError("田间持水量必须在0-100%之间且大于0") + if irrigation_volume < 0: + raise ValueError("灌溉量不能为负数") + if soil_volume <= 0: + raise ValueError("土壤体积必须大于0") + + # 计算初始土壤含水量(m³) + initial_water = (initial_moisture / 100) * (field_capacity / 100) * soil_volume + + # 灌溉后总含水量(m³) + total_water = initial_water + irrigation_volume + + # 计算田间持水总量(m³) + max_water = (field_capacity / 100) * soil_volume + + # 计算灌溉后湿度(超过田间持水量时取100%) + final_moisture = min((total_water / max_water) * 100, 100.0) + + # if verbose: + # area_info = f"(面积: {area}㎡)" if area else "" + # print(f"===== 不考虑降雨与蒸散的灌溉计算 {area_info} =====") + # print(f"初始湿度: {initial_moisture}%") + # print(f"田间持水量: {field_capacity}%") + # print(f"土壤体积: {soil_volume}m³") + # print(f"灌溉量: {irrigation_volume}m³") + # print(f"初始含水量: {initial_water:.2f}m³") + # print(f"灌溉后含水量: {total_water:.2f}m³") + # print(f"田间最大持水量: {max_water:.2f}m³") + # print(f"灌溉后土壤湿度: {final_moisture:.2f}%") + + return final_moisture +def calculate_irrigation_volume( + initial_moisture: float, + target_moisture: float, + field_capacity: float, + soil_volume: float +) -> float: + """ + 计算为达到目标土壤相对湿度所需的灌溉量 + + 参数: + initial_moisture: 初始土壤相对湿度 (%) + target_moisture: 目标土壤相对湿度 (%) + field_capacity: 田间持水量 (%) + soil_volume: 土壤体积 (m³) + area: 灌溉面积 (㎡,可选,仅用于打印提示) + verbose: 是否打印详细计算过程 + + 返回: + float: 需要的灌溉量 (m³) + """ + # 参数验证 + if not (0 <= initial_moisture <= 100): + raise ValueError("初始湿度必须在0-100%之间") + if not (0 <= target_moisture <= 100): + raise ValueError("目标湿度必须在0-100%之间") + if not (0 < field_capacity <= 100): + raise ValueError("田间持水量必须在0-100%之间且大于0") + if soil_volume <= 0: + raise ValueError("土壤体积必须大于0") + if target_moisture <= initial_moisture: + raise ValueError("目标湿度必须大于初始湿度") + + # 计算初始土壤含水量(m³) + initial_water = (initial_moisture / 100) * (field_capacity / 100) * soil_volume + + # 计算目标含水量(m³) + target_water = (target_moisture / 100) * (field_capacity / 100) * soil_volume + + # 需要的灌溉量 + irrigation_volume = target_water - initial_water + + return irrigation_volume +def irriDepth_irrvolume(area,irrDepth): + volume=area*667*(irrDepth/1000) + return volume + + +def calculate_irrigation(dry_soil_weight, current_humidity, target_humidity, + irrigation_efficiency=0.8, water_density=1000): + """ + 计算达到目标湿度所需的灌溉量 + + Args: + dry_soil_weight (float): 土壤干重(kg) + current_humidity (float): 当前相对湿度(%) + target_humidity (float): 目标相对湿度(%) + irrigation_efficiency (float): 灌溉效率(0~1) + water_density (float): 水密度(kg/m³) + + Returns: + float: 灌溉量(立方米) + """ + if current_humidity >= target_humidity: + return 0.0 + + delta_humidity = (target_humidity - current_humidity) / 100 + water_weight = dry_soil_weight * delta_humidity + water_volume = water_weight / water_density + return water_volume / irrigation_efficiency +def calculate_rh_target( + initial_moisture: float, + root: float, + field_capacity: float, + irrigation:float +) -> float: + initial_moisture=(initial_moisture / 100) * field_capacity + iir_volume=irrigation/(10*root) + target_moisture = initial_moisture + (iir_volume/field_capacity) + return target_moisture + + +def calculate_rh_target( + initial_moisture: float, # 初始土壤相对湿度(%) + root: float, # 根系深度(cm) + field_capacity: float, # 田间持水量(体积含水量 cm³/cm³) + irrigation: float # 灌溉量(mm) +) -> float: + """ + 计算灌溉后的目标土壤相对湿度(%) + + 参数: + initial_moisture: 初始土壤相对湿度(%) + root: 根系深度(cm) + field_capacity: 田间持水量(体积含水量 cm³/cm³) + irrigation: 灌溉量(mm) + + 返回: + float: 灌溉后的目标土壤相对湿度(%) + """ + # 参数验证 + if initial_moisture < 0 or initial_moisture > 100: + raise ValueError("初始湿度必须在0-100%之间") + if root <= 0: + raise ValueError("根系深度必须大于0") + if field_capacity <= 0 or field_capacity > 1: + raise ValueError("田间持水量必须在0-1 cm³/cm³之间") + if irrigation < 0: + raise ValueError("灌溉量不能为负值") + + # 将相对湿度转为体积含水量 + initial_volume_moisture = (initial_moisture / 100) * field_capacity + + # 计算灌溉增加的体积含水量 + # 将mm转为cm,并除以根系深度得到体积含水量增量 + moisture_increase = (irrigation / 10) / root + + # 计算灌溉后的体积含水量 + target_volume_moisture = initial_volume_moisture + moisture_increase + # 转换回相对湿度(%) + target_rh = (target_volume_moisture / field_capacity) * 100 + + # 确保不超过100% + target_rh = min(target_rh, 100.0) + + # # 3. 计算目标体积含水量 (不超过田间持水量) + # target_volume = min(initial_volume + moisture_increase, field_capacity) + # + # # 4. 转换回相对湿度 (%) + # target_rh = (target_volume / field_capacity) * 100 + + return target_rh + + +def calculate_rh_target_rz( + initial_moisture: float, # 初始土壤相对湿度(%) + soil_root: float, # 根系深度(cm) + area: float, # 灌溉面积(亩) + irrigation: float, # 灌溉量(mm) + field_capacity: float = 28.84, # 田间持水量(%),默认35% + soil_bulk_density: float = 1.5 # 土壤容重(g/cm³),默认1.5 +) -> float: + """ + 计算灌溉后的目标根区土壤相对湿度 + + 参数: + initial_moisture: 初始土壤相对湿度(%) + soil_root: 根系深度(cm) + area: 灌溉面积(亩) + irrigation: 灌溉量(mm) + field_capacity: 田间持水量(%),默认35% + soil_bulk_density: 土壤容重(g/cm³),默认1.5 + + 返回: + float: 灌溉后的目标土壤相对湿度(%) + """ + # 参数验证 + if initial_moisture < 0 or initial_moisture > 100: + raise ValueError("初始湿度必须在0-100%之间") + if soil_root <= 0: + raise ValueError("根系深度必须大于0") + if area <= 0: + raise ValueError("面积必须大于0") + if irrigation < 0: + raise ValueError("灌溉量不能为负值") + + # 计算根区体积 + root_volume = area * 667 * (soil_root / 100) # m³,将cm转为m + + # 计算灌溉水体积 + water_volume = (irrigation / 1000) * area * 667 # m³,将mm转为m + + # 计算灌溉增加的土壤湿度(%) + moisture_increase = (water_volume / (root_volume * soil_bulk_density)) * 100 + + # 计算目标湿度 + target_moisture = initial_moisture + moisture_increase + + # 确保不超过田间持水量 + target_moisture = min(target_moisture, field_capacity) + + return target_moisture +if __name__ == '__main__': + # volume=irriDepth_irrvolume(10.95,11.123) + # soil_volume_dk1 = 10.95 * 667 * 0.15 + # soil_volume_dk2 = 8.92 * 667 * 0.15 + # result1 = calculate_soil_moisture_after_irrigation( + # initial_moisture=15.2, + # field_capacity=28.84, + # irrigation_volume=168, + # soil_volume=soil_volume_dk1, + # verbose=True + # ) + # print(f"\n示例1结果: 灌溉后湿度 = {result1:.2f}%\n") + # print(soil_volume_dk1) + # result2=calculate_irrigation_volume(initial_moisture=15.44, + # field_capacity=28.84, + # target_moisture=70, + # soil_volume=soil_volume_dk1) + # print(f"\n达到目标湿度所需灌溉量 = {result2:.2f}\n") + # # 调用示例 + # dry_soil_weight=10.95*667*0.1*1200 + # volume = calculate_irrigation( + # dry_soil_weight=dry_soil_weight, + # current_humidity=15.4, + # target_humidity=70 + # ) + # print(f"灌溉量: {volume:.2f} m³") # 输出: 0.09 m³ + rh=calculate_rh_target(67.96,6,0.2884,33.14) + rh1 = rh*0.7 + print(rh,rh1) diff --git a/irrgiation/soil_Ke.py b/irrgiation/soil_Ke.py new file mode 100644 index 0000000..c320f93 --- /dev/null +++ b/irrgiation/soil_Ke.py @@ -0,0 +1,133 @@ +# 计算土壤蒸发系数 +def calculate_ke(h, u2, rh_min, k_cb, DE_i_1,REW,fw,theta_fc,theta_wp): + """ + 计算土壤蒸发系数 ke + 参数: + h: 株高(m) + u2: 2米高处风速(m/s) + rh_min: 最小相对湿度(%) + k_cb: 基础作物系数 + DE_i_1: 前一天从土壤表层蒸发的累积深度(mm) + TEW: 总可蒸发水量(mm) + REW: 易蒸发水量(mm) + few: 裸露湿润的土壤的比值 + 返回: + ke: 土壤蒸发系数 + """ + K_cmin = 0.175 + Z_e=0.12 + kc_max = calculate_kc_max(h, u2, rh_min, k_cb) + fc = calculate_fc(k_cb, K_cmin, kc_max, h) + few = calculate_fev(fc, fw) + TEW = calculate_tew(theta_fc, theta_wp, Z_e) + kr = calculate_Kr(DE_i_1, TEW, REW) + term1 = kr * (kc_max - k_cb) + term2 = few * k_cb + return min(term1, term2),few + +# 每日土壤蒸发量累积量 +#土壤蒸发有效部分 +def calculate_fev(fc, fw): + fev = min(1 - fc, fw) + return fev +#计算作物覆盖率fc +def calculate_fc(Kcb, Kcmin, Kcmax, h): + #Kcb:作物基础系数,Kcmin:无地表覆盖的干燥土壤最小 值(约为 0.15-0.20),Kcmax:紧随湿润过程的土壤最大 值 + + fc = ((Kcb- Kcmin) / (Kcmax - Kcmin)) ** (1 + 0.5 * h) + return fc +# 计算蒸发减少系数 +def calculate_Kr(DE_i_1, TEW, REW): + if DE_i_1 <= REW: + Kr = 1 + else: + print("dd") + Kr = (TEW - DE_i_1) / (TEW - REW) + return Kr +#计算Kcmax +def calculate_kc_max(h: float, u2: float, rh_min: float, k_cb: float) -> float: + """ + 计算作物系数上限 kc_max + 参数: + h: 株高(m) + u2: 2米高处风速(m/s) + rh_min: 最小相对湿度(%) + k_cb: 基础作物系数 + 返回: + kc_max: 作物系数上限 + """ + term1 = 1.2 + (0.04 * (u2 - 2) - 0.004 * (rh_min - 45)) * (h / 3) ** 0.3 + term2 = k_cb + 0.5 + return max(term1, term2) +#计算TEW +def calculate_tew(theta_fc: float, theta_wp: float, Z_e: float) -> float: + """ + 计算总可蒸发水量 TEW + 参数: + theta_fc: 田间持水量(m³/m³) + theta_wp: 萎蔫系数(m³/m³) + Z_e: 土壤蒸发层深度(cm) + 返回: + TEW: 总可蒸发水量(mm) + """ + return 1000 * (theta_fc - theta_wp) * Z_e +# 计算深层渗漏损失 +def calculate_DP_ei(P_i, RO_i, I_i, D_e_prev, f_w): + if (P_i - RO_i) + I_i / f_w - D_e_prev >= 0: + return (P_i - RO_i) + I_i / f_w - D_e_prev + else: + return 0 + +# 计算每日土壤蒸发量累积量 +def calculate_D_ei(D_e_prev, P_i, I_i, E_i, f_w, f_ew): + RO_i=0 + T_ew_i=0 + DP_ei = calculate_DP_ei(P_i, RO_i, I_i, D_e_prev, f_w) + D_ei = D_e_prev - (P_i - RO_i) - I_i / f_w + E_i / f_ew + T_ew_i + DP_ei + return D_ei +# 示例计算 +if __name__ == "__main__": + # # 示例参数 + # h = 0.07 # 株高(m) + # u2 = 3.0 # 2米高处风速(m/s) + # rh_min = 20.0 # 最小相对湿度(%) + # k_cb = 0.4 # 基础作物系数 + # K_s = 0.6 # 土壤导水率 + # K_cmin = 0.175 + # + # fw = 0.35 # 土壤表层被灌溉浸湿的部分 + # DE_i = 6.24 # 前一天从土壤表层蒸发的累积深度(mm) + # DE_i_1 = 2.965 # 前一天从土壤表层蒸发的累积深度(mm) + # theta_fc = 0.2884 # 田间持水量(m³/m³) + # theta_wp = 0.1086 # 萎蔫系数(m³/m³) + # Z_e = 0.1 # 土壤蒸发层深度(m) + # + # # # # 计算 TEW + # # TEW = calculate_tew(theta_fc, theta_wp, Z_e) + # REW = 5.178 # 假设REW为TEW的40% + # # # + # # # # 计算各参数 + # # kc_max = calculate_kc_max(h, u2, rh_min, k_cb) + # # fc = calculate_fc(k_cb, K_cmin, kc_max, h) + # # few= calculate_fev(fc, fw) + # # kr = calculate_Kr(DE_i_1,DE_i, TEW, REW) + # ke=calculate_ke(h, u2, rh_min, k_cb, DE_i_1,DE_i,REW) + # # 输出结果 + # + # print(f"土壤蒸发系数 Ke: {ke:.4f}") + # 假设初始参数 + TEW = 1000 # 总可蒸发水量,单位:mm + D_e_start = TEW # 模拟开始时的初始D_e + D_e_prev = 0 # 模拟第一天的D_e,假设充分灌溉后为0 + # 假设一些每日数据,实际应用中需替换为真实数据 + P_i = 10 # 第i天的降雨量,单位:mm + RO_i = 0 # 第i天的降雨形成地表径流量,这里假定为0,单位:mm + I_i = 19 # 第i天渗入土壤的灌溉量,单位:mm + k_e = 0.14 # 土壤蒸发系数 + ET_o = 8.188 # 参考作物蒸散量,单位:mm + E_i = k_e * ET_o # 第i天的蒸发量 + T_ew_i = 0 # 第i天湿润裸露表层土壤的蒸腾量,这里假定为0,单位:mm + f_w = 0.8 # 经灌溉湿润的土壤面积比值 + f_ew = 0.6 # 裸露湿润土壤面积比值 + D_ei = calculate_D_ei(D_e_prev, P_i, RO_i, I_i, f_w, f_ew) + print(f"第i天末土壤完全湿润后的累积蒸发(消耗)量为: {D_ei} mm") \ No newline at end of file diff --git a/irrgiation/utils.py b/irrgiation/utils.py new file mode 100644 index 0000000..54d3eb4 --- /dev/null +++ b/irrgiation/utils.py @@ -0,0 +1,10 @@ +dk_irriationInfo={ + "1号地":{ + "dkbm":1, + "irriationFile":"D:/悦柠/遥感/甘草/model_result/1号地_灌溉数据.xlsx" + }, + "5号地":{ + "dkbm":5, + "irriationFile":"D:/悦柠/遥感/甘草/model_result/5号地_灌溉数据.xlsx" + } +} \ No newline at end of file diff --git a/irrgiation/weatherAndSoilDataRequest.py b/irrgiation/weatherAndSoilDataRequest.py new file mode 100644 index 0000000..297ddbd --- /dev/null +++ b/irrgiation/weatherAndSoilDataRequest.py @@ -0,0 +1,336 @@ +from datetime import datetime, timezone, timedelta, date + +import pandas as pd +import requests +import json + +from matplotlib import pyplot as plt + + +#登录接口认证 + +def loginAuth(): + # 接口地址 + global auth_token + url = "https://xj-tb.maimaiag.com/api/auth/login" # 请替换为实际地址 + + # 请求头 + headers = { + "Content-Type": "application/json" + } + + # 请求体数据 + payload = { + "username": "read@xj.com", + "password": "maimai" + } + + try: + # 发送POST请求 + response = requests.post( + url, + headers=headers, + data=json.dumps(payload) # 或者直接使用 json=payload + ) + + # 检查响应状态码 + if response.status_code == 200: + print("登录成功!") + print("响应数据:", response.json()) + + # 通常登录接口会返回token,可以这样获取 + response_data = response.json() + auth_token = response_data.get("token") # 根据实际返回字段调整 + if auth_token: + print("获取到的认证Token:", auth_token) + else: + print(f"登录失败,状态码: {response.status_code}") + print("错误信息:", response.text) + + except requests.exceptions.RequestException as e: + print(f"请求发生异常: {str(e)}") + except json.JSONDecodeError as e: + print(f"JSON解析错误: {str(e)}") + return auth_token +# 调用数据接口--历史数据 +def getWeatherAndSoilData(auth_token,device_id,startTs,endTs,agg,interval,limit,orderBy,keys): + # 配置信息 + BASE_URL = "https://xj-tb.maimaiag.com/" # 请替换为实际API地址 + TOKEN = auth_token # 替换为实际的Bearer token + DEVICE_ID = device_id # 替换为实际的设备ID + # 构造请求URL + endpoint = f"/api/plugins/telemetry/DEVICE/{DEVICE_ID}/values/timeseries" + url = BASE_URL + endpoint + # 请求头 + headers = { + "Content-Type": "application/json", + "X-Authorization": f"Bearer+{TOKEN}" + } + # 查询参数 + params = { + "keys": keys, # 要查询的参数,多个用逗号分隔 + "startTs": startTs, # 开始时间戳(毫秒)- 可选 + "endTs": endTs, # 结束时间戳(毫秒)- 可选 + "agg": agg, # 聚合函数 - 可选 + "interval": interval, # 聚合间隔(毫秒)- 可选 + "limit": limit, # 返回条数 - 可选 + "orderBy": orderBy # 排序方式 - 可选 + } + try: + # 发送GET请求 + response = requests.get( + url, + headers=headers, + params=params + ) + # 检查响应状态码 + if response.status_code == 200: + # print("请求成功!") + data = response.json() + # print("响应数据:", json.dumps(data, indent=2)) + converted_data = {k: convert_timestamp_to_datetime(v) for k, v in data.items()} + return converted_data + elif response.status_code == 401: + print("认证失败,请检查Token是否正确") + elif response.status_code == 404: + print("设备不存在或路径错误") + else: + print(f"请求失败,状态码: {response.status_code}") + print("错误信息:", response.text) + + except requests.exceptions.RequestException as e: + print(f"请求发生异常: {str(e)}") + except json.JSONDecodeError as e: + print(f"JSON解析错误: {str(e)}") + print("原始响应:", response.text) +def convert_timestamp_to_datetime(data_dict): + """ + 递归地将字典中的所有'timestamp'字段转换为可读的日期时间格式 + 保留原始时间戳的同时添加新的'datetime'字段 + """ + if isinstance(data_dict, dict): + if "ts" in data_dict: + # 转换毫秒时间戳为datetime对象 + timestamp_sec = data_dict["ts"] / 1000 + dt = datetime.fromtimestamp(timestamp_sec) + # 添加新字段,保留原始时间戳 + data_dict["datetime"] = dt.strftime("%Y-%m-%d %H:%M:%S") + return data_dict + elif isinstance(data_dict, list): + return [convert_timestamp_to_datetime(item) for item in data_dict] + else: + return data_dict + +def dateToTimestamp(date): + # dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") + if isinstance(date, datetime): + # 如果已经是datetime对象,直接使用 + dt = date + elif isinstance(date, str): + # 如果是字符串,解析为datetime对象 + dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S.%f") + else: + raise TypeError("date参数必须是字符串或datetime对象") + timestamp_ms = int(dt.timestamp() * 1000) + return timestamp_ms + +#调用接口获取未来气象数据 +def getFutureWeather(monitor_date): + base_url_future="https://api.open-meteo.com/" + end_point="v1/forecast" + current_date = datetime.now().date() + params = { + "latitude": "41.34252110189814", # 纬度 + "longitude": "86.28810755462379", # 经度 + "hourly": "temperature_2m,precipitation,precipitation_probability" + } + # if monitor_date.date()= 22] + plt.scatter(night_data['datetime'], night_data['value'], + color='red', + s=30, + label='22:00-06:00 Data') + + # 设置标题和坐标轴标签 + plt.title('Soil Moisture Time Series (2025-06-22 to 2025-06-30)', + fontsize=14, + pad=15) + plt.xlabel('Datetime', fontsize=12, labelpad=10) + plt.ylabel('Soil Moisture (%)', fontsize=12, labelpad=10) + + # 设置x轴刻度旋转,避免重叠 + plt.xticks(rotation=45, ha='right', fontsize=10) + + # 添加网格线 + plt.grid(axis='y', linestyle='--', alpha=0.7) + + # 添加图例 + plt.legend(loc='upper right', fontsize=10) + + # 调整布局 + plt.tight_layout() + + # 显示图表 + plt.show() +if __name__ == "__main__": + auth_token = loginAuth() + device_id="18d121f0-561b-11f0-a556-4f10f26fc07f" + agg="AVG" + interval=21600000 + limit="" + orderBy="" + keys="RAIN_FALL_REALTIME" + date_start = "2025-06-25 00:00:00.123" + date_end = "2025-07-01 23:59:59.123" + startTs=dateToTimestamp(date_start) + endTs=dateToTimestamp(date_end) + # data=get_soil_data_dk1(auth_token,startTs,endTs) + # print(data) + data=getWeatherAndSoilData(auth_token,device_id,startTs,endTs,"AVG",3600000,limit,orderBy,keys) + print(data['RAIN_FALL']) + # get_plot(data['1号传感器']['SOIL_MOISTURE_SURFACE']) + get_plot(data['RAIN_FALL_REALTIME']) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..44eee24 --- /dev/null +++ b/main.py @@ -0,0 +1,42 @@ +from datetime import datetime, timedelta + +import uvicorn +from fastapi import FastAPI, Query +from pydantic import BaseModel + +from irrgiation.irrigateDecisionDemo import cal_irrigationByDay +# 创建 FastAPI 应用 +app = FastAPI() +# 定义请求模型 +class IrrigationRequest(BaseModel): + zwlx_name:str + dkbm: str + monitor_date: str + soil_key: str + weather_key: str + irrigation:float +@app.post("/calculate_irrigation/") +async def create_item(data: IrrigationRequest): + # 调用气象数据接口--获取具体日期的气象数据(气象) + try: + start_date = datetime.strptime(data.monitor_date, '%Y-%m-%d') + end_date =start_date+timedelta(days=1) + result = cal_irrigationByDay( + data.zwlx_name, + data.dkbm, + start_date, + end_date, + data.soil_key, + data.weather_key, + data.irrigation + ) + return {"status": "success", "data": result.to_dict(orient="records")} + except Exception as e: + return {"status": "error", "message": str(e)} + +@app.get("/items/lib") +async def get_lib(): + return {"message": "Library data"} + return result +if __name__ == "__main__": + uvicorn.run(app, host="127.0.0.1", port=8001) \ No newline at end of file diff --git a/pyeto/__init__.py b/pyeto/__init__.py new file mode 100644 index 0000000..d74a91e --- /dev/null +++ b/pyeto/__init__.py @@ -0,0 +1,99 @@ + +import pyeto + +from pyeto.convert import ( + celsius2kelvin, + kelvin2celsius, + deg2rad, + rad2deg, +) + +from pyeto.fao import ( + atm_pressure, + avp_from_tmin, + avp_from_rhmin_rhmax, + avp_from_rhmax, + avp_from_rhmean, + avp_from_tdew, + avp_from_twet_tdry, + cs_rad, + daily_mean_t, + daylight_hours, + delta_svp, + energy2evap, + et_rad, + fao56_penman_monteith, + hargreaves, + inv_rel_dist_earth_sun, + mean_svp, + monthly_soil_heat_flux, + monthly_soil_heat_flux2, + net_in_sol_rad, + net_out_lw_rad, + net_rad, + psy_const, + psy_const_of_psychrometer, + rh_from_avp_svp, + SOLAR_CONSTANT, + sol_dec, + sol_rad_from_sun_hours, + sol_rad_from_t, + sol_rad_island, + STEFAN_BOLTZMANN_CONSTANT, + sunset_hour_angle, + svp_from_t, + wind_speed_2m, +) + +from pyeto.thornthwaite import ( + thornthwaite, + monthly_mean_daylight_hours, +) + +__all__ = [ + # Unit conversions + 'celsius2kelvin', + 'deg2rad', + 'kelvin2celsius', + 'rad2deg', + + # FAO equations + 'atm_pressure', + 'avp_from_tmin', + 'avp_from_rhmin_rhmax', + 'avp_from_rhmax', + 'avp_from_rhmean', + 'avp_from_tdew', + 'avp_from_twet_tdry', + 'cs_rad', + 'daily_mean_t', + 'daylight_hours', + 'delta_svp', + 'energy2evap', + 'et_rad', + 'fao56_penman_monteith', + 'hargreaves', + 'inv_rel_dist_earth_sun', + 'mean_svp', + 'monthly_soil_heat_flux', + 'monthly_soil_heat_flux2', + 'net_in_sol_rad', + 'net_out_lw_rad', + 'net_rad', + 'psy_const', + 'psy_const_of_psychrometer', + 'rh_from_avp_svp', + 'SOLAR_CONSTANT', + 'sol_dec', + 'sol_rad_from_sun_hours', + 'sol_rad_from_t', + 'sol_rad_island', + 'STEFAN_BOLTZMANN_CONSTANT', + 'sunset_hour_angle', + 'svp_from_t', + 'wind_speed_2m', + + # Thornthwaite method + 'thornthwaite', + 'monthly_mean_daylight_hours', +] diff --git a/pyeto/__pycache__/__init__.cpython-310.pyc b/pyeto/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee472221ed9870c9bbbf7fb37f2e13c13b9278ad GIT binary patch literal 1488 zcmdUu%WmT~6ozHT_tYTHTfHQnZrEfaS4{N00k^UF-QGNvV>(QV+AT$g(}vdhRd*wb*N(l8t8+M zO=#i@tl%oFVhdWh25Yzu>$m|M*oHQ4!X|FP7IvV6+pvv0u!Fm>i+iw#`>>A(aDaz! zh(~aQ$8d}%aDu0Bif3?!=Wvb}aDkU_ndMWEU3>#?@GZQ}c_S|<{+@%+g50gwE&c5( z@&?_4^@l?0Xsmlb3pt5m>(ATXbGI!AV$@SSuw{py6WwE5=LT7-gw|svY=ciy_Mj4E z23>&lSE)j$nInI){$j7w%|$JEyci`AQ`>qSH&bDl3DnG%hMG|(^?*lm#?Vb;#`*!D z$!HWB7A9k*Z8Z?maHc1zT^6xWqghW(cxp>749(tn;K}D-uvEn8=3^2Y5t-1|Las(i z@QKj29mNx-gk(Xa4HI0Mkm+%3E7_71xlQvzmxN9t$wI*mdywNNyZPV!J?qDAdFO^s zWR^vyld)lPn&~o)Q+m}@&)6`DwXvPoPnJ9|Cx)SOSs0;gQ-yQO$tFt3zrA_af6uY`|uF8R%PJd(oJTn^7?z&55)959^<5wIS1^R{lnL^->rJTk)WyjYZ(`RV3$FBRQ6 zRUj-8ii8ruT~JR|2vq{zT(wN76B-1c&?Kx7RtYV_8eyHVL1+^;30s5?VVkf+*d^={ z_6Y}sL&6c^m~cWkC7cn?2^WM*Lf4^tWsB)dm;@SGbM3ZtB9xiemz9F;PDpD#M waT6u+6(;T)-)BqQGk0+!!`O8?vz4#h$XJT6b1tB!Lr}?W<$o)$oYjl}0=k!|VgLXD literal 0 HcmV?d00001 diff --git a/pyeto/__pycache__/_check.cpython-310.pyc b/pyeto/__pycache__/_check.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..592489e4a47f64783e2f19bbf07321a36ae5b253 GIT binary patch literal 2036 zcmb_d&rcgi6y9AgwlOAzAEiH{O4FV&jg2vZR94i)1=J!Xt`t{MgpgK~oxxsM?^?6t z6g#A9ll~#ek$cs1{|;Y!;+$I#O}{tB)PQn8>R9vM+qX0O=6&Cr=XiQLXJPbDer^4c zv8=!7VSG4vcz~I-AcVDR30pX!-F9{z+men*y|#B#&@QwKJpnxdorX?}%x(sD7Is!l zirjD3?xdI!dFY&&7B`@$#EiHJoforW4tiS5H>|=!|5a`$(o#h{WY2jR2(E)TV*B05 zCnG84a#cU>^i=9Ro>~Y|?xDr@b4Fg|BiLA138(VCxxEb

3-n-#@Hlb)?#p8Pp79-J9> zN1nT99xOZk9l|H{MuY4L%uUQ>8A8~{mTRS~LtEv9b7&{?k!@Sr5~(-#G2Xp(UfZv1 z;R26|{(6pKY__C-$aITqw%m%lDp_Vh!h#4c$rO*8lC3fwv&x;4S+8uwIsF8p|K*GF zOO{|$DA+Sedxq=F#mb#3d$Ib3dRael2Dv9Z?8B*5y`vyH2ym3Wql03gK#-xX*p7t^_c}eP<2}!(JW8G3V8&RU z;62ZT_!wVJ=+ld?W9Mx5z58C}@#s?`qd~TdX^>okaRte_F(j?G_85+ZF&q(<#rp=k zp5%(4G_^Lw?v%m=5oCXD>(a_45%&tL-A(ibELV$b8yf(#OkgS^8|9P2T7%Y5Ou;*t zJWD(7F+83poE;`S1K*W|({P%EZ^O{mf2E^sl+J=l*b(-dl(sY#4=yH($3pQq_5MZ( z5_DHc7I$?LK$}F*O2C|#%EnXyoB}yikVC;8%y^G>>JMu_dXH;cJGFX)9JQxo4n+r3 zXdRSmb`e^+2HhNy>;7D{G~7#v{SLG8S&Vl9D+mLC1c5F| z(Ovnu&6AKvhkV6E`A){Aj@UQ5`uCG7fv?3t9TdzF1&tpbZf$R#vjAW1=|$fKBAi5A zreJnR0S@E=2V$TMtlt?1(qjxzxA-6fY5{M7sv3gQMPt>BZZ3dphJc7CW4)+MMYZ~M z8NmN9SY8QtZ7fYWH@>f(18K^6%y9w5RDwSd>PBN%_tqJV<}9XZc_wuN^d2q!l*H!{ z^}>xo#`8q%d){E$biR@wN6)3wivA4LO%k&tJ|eM5;xiJrNDwiKT7kI}ifA#P9XpUY lnrSH6d+4v-P<}&AE1{3LRz96|vvxLxu7U|YiC-q4{}&|k{6YW# literal 0 HcmV?d00001 diff --git a/pyeto/__pycache__/convert.cpython-310.pyc b/pyeto/__pycache__/convert.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36cfff5b1ee57712454d5c1f3f05426b313c6439 GIT binary patch literal 1424 zcma)+&u`N(6vv(PSC+07LI{w!m>ZSWY7}va09AB5AYcb3I8>D?FLqaVX%ZY~YO4u> z#D9R`!e6j|saFn&GdE5=J6_kcOv6&16X(5e-uw7DQLW}881wT_qo+lLeyE4lVZ%cQ zu4ESsMLk3ciMY=^@m`l_?M2+hFgZnn*68lkj%0$AW z5D(MPmvED`T-WoXcqaJqNP75z-@xtNhmY~m48JDg1i#__hzJUPUch}8Cd|Xn5BKn+ zW;Y;d!gg@N7=HDlyMJ)F-;z^_hmpXONH9#9Bs_q~UywCA@GUi&q3%^xUAPhl%o)^l z2SK*>{F{qmbak>4OE!s_ATk9-9)jqxU@XC179>1Pu#A>VFIjNPLnugvdodAYg8j_R z!+ZI-%ttVg4KH723g(@h#Zc=oh=|lHKZk1EeBU`o4Xal%%Ax?tYxAeG=k;R|jhQbm z=E0PQV@|^3{&d{r0w5Dby*Y^}3;OYl$*7<0ifqNRMcs(DPjo3&kp8VGIgTj7bUnel z8NshQ;cEzH#jXN0O!EY@RIXsO#?J^`0Z^z?G%Da%&U7#F321z@sAtP<1LC%dC@WI| zvr)fduREv?H)lwqu|=(W=vGGIOO66;LK+aU>=tMoK`87^l4}^#Umku(f;8805OBoI z8Ip{)OqB+~()~hIH=)>QB1jaWkHgNny(mtI94+jaH>^d)e0QCPT5p5oeTdSnP?mKg z+v0`_C@(650ge2@K&U^cJ|((UG$ie_qNI~ literal 0 HcmV?d00001 diff --git a/pyeto/__pycache__/fao.cpython-310.pyc b/pyeto/__pycache__/fao.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a64cc048adae7296a3fd8cc756548f6a5ddf5ad GIT binary patch literal 27714 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

2OHN8eb#eBhuqZ7HQ(O?6oUD1qPxDYE5*0wzw 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 1 there is no sunset, i.e. 24 hours of daylight + # If tmp is <= 1 there is no sunrise, i.e. 24 hours of darkness + # See http://www.itacanet.org/the-sun-as-a-source-of-energy/ + # part-3-calculating-solar-angles/ + # Domain of acos is -1 <= x <= 1 radians (this is not mentioned in FAO-56!) + return math.acos(min(max(cos_sha, -1.0), 1.0)) + + +def svp_from_t(t): + """ + Estimate saturation vapour pressure (*es*) from air temperature. + + Based on equations 11 and 12 in Allen et al (1998). + + :param t: Temperature [deg C] + :return: Saturation vapour pressure [kPa] + :rtype: float + """ + return 0.6108 * math.exp((17.27 * t) / (t + 237.3)) + + +def wind_speed_2m(ws, z): + """ + Convert wind speed measured at different heights above the soil + surface to wind speed at 2 m above the surface, assuming a short grass + surface. + + Based on FAO equation 47 in Allen et al (1998). + + :param ws: Measured wind speed [m s-1] + :param z: Height of wind measurement above ground surface [m] + :return: Wind speed at 2 m above the surface [m s-1] + :rtype: float + """ + return ws * (4.87 / math.log((67.8 * z) - 5.42)) diff --git a/pyeto/thornthwaite.py b/pyeto/thornthwaite.py new file mode 100644 index 0000000..18e7f7c --- /dev/null +++ b/pyeto/thornthwaite.py @@ -0,0 +1,119 @@ +""" +Calculate potential evapotranspiration using the Thornthwaite (1948 method) + +:copyright: (c) 2015 by Mark Richards. +:license: BSD 3-Clause, see LICENSE.txt for more details. + +References +---------- +Thornthwaite CW (1948) An approach toward a rational classification of + climate. Geographical Review, 38, 55-94. +""" + +import calendar + +from . import fao +from ._check import check_latitude_rad as _check_latitude_rad + +_MONTHDAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) +_LEAP_MONTHDAYS = (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + + +def thornthwaite(monthly_t, monthly_mean_dlh, year=None): + """ + Estimate monthly potential evapotranspiration (PET) using the + Thornthwaite (1948) method. + + Thornthwaite equation: + + *PET* = 1.6 (*L*/12) (*N*/30) (10*Ta* / *I*)***a* + + where: + + * *Ta* is the mean daily air temperature [deg C, if negative use 0] of the + month being calculated + * *N* is the number of days in the month being calculated + * *L* is the mean day length [hours] of the month being calculated + * *a* = (6.75 x 10-7)*I***3 - (7.71 x 10-5)*I***2 + (1.792 x 10-2)*I* + 0.49239 + * *I* is a heat index which depends on the 12 monthly mean temperatures and + is calculated as the sum of (*Tai* / 5)**1.514 for each month, where + Tai is the air temperature for each month in the year + + :param monthly_t: Iterable containing mean daily air temperature for each + month of the year [deg C]. + :param monthly_mean_dlh: Iterable containing mean daily daylight + hours for each month of the year (hours]. These can be calculated + using ``monthly_mean_daylight_hours()``. + :param year: Year for which PET is required. The only effect of year is + to change the number of days in February to 29 if it is a leap year. + If it is left as the default (None), then the year is assumed not to + be a leap year. + :return: Estimated monthly potential evaporation of each month of the year + [mm/month] + :rtype: List of floats + """ + if len(monthly_t) != 12: + raise ValueError( + 'monthly_t should be length 12 but is length {0}.' + .format(len(monthly_t))) + if len(monthly_mean_dlh) != 12: + raise ValueError( + 'monthly_mean_dlh should be length 12 but is length {0}.' + .format(len(monthly_mean_dlh))) + + if year is None or not calendar.isleap(year): + month_days = _MONTHDAYS + else: + month_days = _LEAP_MONTHDAYS + + # Negative temperatures should be set to zero + adj_monthly_t = [t * (t >= 0) for t in monthly_t] + + # Calculate the heat index (I) + I = 0.0 + for Tai in adj_monthly_t: + if Tai / 5.0 > 0.0: + I += (Tai / 5.0) ** 1.514 + + a = (6.75e-07 * I ** 3) - (7.71e-05 * I ** 2) + (1.792e-02 * I) + 0.49239 + + pet = [] + for Ta, L, N in zip(adj_monthly_t, monthly_mean_dlh, month_days): + # Multiply by 10 to convert cm/month --> mm/month + pet.append( + 1.6 * (L / 12.0) * (N / 30.0) * ((10.0 * Ta / I) ** a) * 10.0) + + return pet + + +def monthly_mean_daylight_hours(latitude, year=None): + """ + Calculate mean daylight hours for each month of the year for a given + latitude. + + :param latitude: Latitude [radians] + :param year: Year for the daylight hours are required. The only effect of + *year* is to change the number of days in Feb to 29 if it is a leap + year. If left as the default, None, then a normal (non-leap) year is + assumed. + :return: Mean daily daylight hours of each month of a year [hours] + :rtype: List of floats. + """ + _check_latitude_rad(latitude) + + if year is None or not calendar.isleap(year): + month_days = _MONTHDAYS + else: + month_days = _LEAP_MONTHDAYS + monthly_mean_dlh = [] + doy = 1 # Day of the year + for mdays in month_days: + dlh = 0.0 # Cumulative daylight hours for the month + for daynum in range(1, mdays + 1): + sd = fao.sol_dec(doy) + sha = fao.sunset_hour_angle(latitude, sd) + dlh += fao.daylight_hours(sha) + doy += 1 + # Calc mean daylight hours of the month + monthly_mean_dlh.append(dlh / mdays) + return monthly_mean_dlh diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f541c9ef0c7e84986f47fe74337666a0f6227f6a GIT binary patch literal 1646 zcmZ{l!E)1J41{%NhDULyc3RpVdf?85;Q=xsIE|@e8{27;K0L6WAkl1_D)faPAg?_cBAia^%j1%RZg$$ z%657=FO*ey1PHm}!PwefIKA*oMQ6_6+d<#e%6oMbvRBH1A)#}+wP#1Zcf2dRpAcK+ zw!+*vPSQ>GhdH7VTWd!}b*h1wd_u{mn2Yf1nV-sYvGW*k`Mddqdm4Q}GrZgfZd76qj<_#>$E=+z zbqlS<)L&d9ntHg?OSNedlOxM?WX{hV99=^_Yd}AhGJ*6VRr_Zy9cSuaJdeIC9Yo8h zEfQ6w{S%Xg{vYCu9xuMH-2KowsM45a5aK2+=+t+fS@SoWagZK1AG>dvm+%0OzSw8` zWFOTN_Ft*liOETQznQudo1s@6+mSla5#49{!;#q0;q_WE=gATDv456*k^2pd`tgO% uvwQyK54(}6smQO=z7(8rw4Ru=RW|SIPe=GJ@2Rh literal 0 HcmV?d00001